62 Commits

Author SHA1 Message Date
Adriano Dal Pastro b691f48f43 docs(diary): incident 2026-07-09 — "BOOK LIVE conto offline" = cert Traefik di default
Traefik auto-upgrade (latest+Watchtower -> 3.7.7) ha scartato il resolver ACME
per acme.json a 660 (3.x pretende 600) -> cert self-signed su tutto il VPS ->
DeribitRead SSLError -> book online=False. Zero trade persi (target flat, gate
di sicurezza OK). Fix: chmod 600 acme.json + restart; pin traefik:3.7 nel compose.
Diario con diagnosi, verifica end-to-end e runbook.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 20:09:41 +00:00
Adriano Dal Pastro 822aa1307e research(crt): video-claim "CRT top-down 74% win rate" SCARTATO — il 74% e' un knob, non un edge
Meccanizzato e falsificato il claim ICT/SMC top-down (H1 setup CRT -> M15/M5
conferma -> entry, uscita parziale+BE+runner) su BTC/ETH certificati.

- Setup H1 CRT gia' triplo-refutato (onda 2026-07-02); qui testato l'angolo nuovo:
  la gestione d'uscita e il "74% WR".
- WR reale 30-37% a RR 1.5-2, SOTTO il null gambler's-ruin 40% -> il ritest tocca
  il target meno di una moneta (conferma "il ritest e' informazione negativa").
- Il 74% si fabbrica avvicinando il target: sweep rr1 0.5->2.0 = WR sale, expectancy
  R INVARIANTE e negativa (-1.2..-3.3R netto). DSR 0.000; a fee 0 ancora negativa
  -> non morte-per-fee, l'edge lordo non c'e'.
- Non eseguibile pulito a $600 (3-4 ordini/trade, alcuni sub-min-order).
- Regola candidata: convertire ogni claim "WR X%" da schema parziale+BE in
  expectancy R netto fee (WR ~= 1/(1+rr1), non un merito).

Nessuna modifica a src/config/live/tests. Book e pesi INVARIATI.
Diario: docs/diary/2026-07-07-crt-topdown-74winrate.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 06:02:38 +00:00
Adriano Dal Pastro cf7de40dc0 feat(live): cap/asset dinamico = equity/2 — operazionalizza la decisione frontier (inerte a $600)
Il cap notional per-asset non e' piu' fisso a $300: con max_notional_per_asset_frac
in config diventa equity/2, cosi' cresce col capitale e un deposito futuro non resta
strozzato (frontiera 2026-07-03). Guardrail: dinamico SOLO con equity reale fidata;
su fallback/offline si torna al cap fisso (protezione downside quando E e' ignota).
Live dry-run: cap $299 a equity $598 -> inerte oggi. Suite 172 passed (+4 test).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 09:42:13 +00:00
Adriano Dal Pastro eef200cf14 research(frontier): frontiera onesta del book — il 6% e' un miraggio di regime, gira gia' alla vol nativa
Misura (non ricerca segnali) della frontiera CAGR/DD/P-rovina vs
target-vol, pesata per confidenza per-sleeve, a 2k/5k. 7 agenti (3
filoni + refuter fat-tail + scettico), sanity bit-exact dei due book.

Reframe decisivo: il "6% attuale" NON e' un target, e' la vol realizzata
in risk-off; a lambda=1 il book LIVE 2-sleeve gira gia' a ~11% vol/9.4%
DD -> "saltare a 10%" e' assumere non-risk-off, non aggiungere leva.

- Target-vol fidato book LIVE (TP01+SKH01, unico eseguibile <20k):
  ~10% (banda 8-11%, lambda ~0.9). Scale-invariante: stesso % a 2k/5k.
- P(rovina-50%) e' la metrica SBAGLIATA (slack a 22%); il vincolo che
  morde e' P(DD>30%), super-lineare. Muro onesto ~12% (gap-through SKH
  reale non nei rendimenti modellati; 2-sleeve 4x crash-sensibile).
- Vol differenziata-per-confidenza REFUTATA: taglia la diversificazione;
  la bassa fiducia va nel DE-MEAN (haircut media), non nella leva.
- Reward onesto (HOLD<<FULL): ~EUR 0.3-0.7/g@2k, 0.8-1.8/g@5k; warm-up
  vs 6% risk-off = ~+EUR 0.2/g@2k. EUR 50/g resta ~130k (capitale+tempo).
- UNICA azione config (proposta, NON applicata): cap $300 -> equity/2,
  altrimenti a 5k il cap raffredda paradossalmente il book sotto il punto
  fidato (6% vol ceiling).

config/live.json NON toccato. Book invariato. 168 test verdi.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 08:43:59 +00:00
Adriano Dal Pastro 116abf5203 docs(diary): snapshot stato completo del progetto (2026-07-03)
Foto d'insieme dopo il blocco di ricerca 1-3 luglio: book live
(TP01+SKH01 flat, $598), portafoglio research 5-sleeve (de-luckato
HOLD ~2.0), anchor-audit completo 4/4, edge morti da non ri-testare,
~10 gate anti-illusione codificati, mappa capital-scaling 2-5k,
aperti/prossimi passi. Punto di riferimento unico dello stato.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 07:16:17 +00:00
Adriano Dal Pastro 26f8d27a61 research(wave-0703): migliora+proteggi VRP01 — 7 filoni, 0 miglioramenti, anchor-audit VRP01 chiuso (4/4 sleeve)
Goal: migliorare la strategia short-vol (famiglia Albimarini/VRP01) e
proteggerla dai DD. Workflow 26 agenti (7 filoni + 2 lenti avversariali
+ scettico incrociato). Esito: NON migliorabile; la protezione DD si
compra SOLO con la size.

- Griglia 288 strutture: nessuna batte VRP01 (DSR 0.000; meta' griglia
  = 3a occorrenza "0-perdite/Sharpe implausibile" dopo CC01/ALB-A).
- 4 overlay DD (exit-spike/SL-MTM/ala-coda/cooldown): 4/4 REFUTED dal
  null de-levering — la protezione crash vive gia' nel gate IV-rank.
- Gate nuovi: 4o fallimento su 4 (l'alpha e' il binario IV-rank>0.30).
- Sizing: 12% deploy ~ 0.27 Kelly onesto (anti-rovina); disambiguazione
  unita' obbligatoria vs 12% peso book (0.014 Kelly, fattore 19x).
- Gate term-structure VIX/VXV su SPX (dSh +0.90, DSR 0.992) = confound
  di modello al 100% -> nuova regola: riprezzare term-structure-consistent
  prima di credere a un gate vol su strutture BS-flat.
- ANCHOR-AUDIT VRP01 CHIUSO: primo sleeve SENZA luck (fase canonica =
  peggiore delle 7 -> numeri di ammissione conservativi). Audit anchor
  ora completo su 4/4 sleeve ancorati.

Book/pesi INVARIATI. Nessun nuovo sleeve. 168 test verdi.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 07:05:19 +00:00
Adriano Dal Pastro 76120b59c2 research(wave-0702bis): ondata video-claims — Elliott 3/3 e Albimarini 2/2 scartati, mappa capital scaling 2-5k
Sei agenti, nessun sopravvissuto:
- ELL-A range-cycle: rumore (0/24 Bonferroni; nessuna cella weekly regge
  a tutte le 7 ancore). Lezione pandas: resample("7D", origin) IGNORA
  origin -> usare "168h" per le bande d'ancora weekly.
- ELL-B Fibonacci: l'edge apparente e' la POSIZIONE dei livelli, non i
  numeri (null location-matched: pctl 0.39-0.68); confluenza FAIL 4/4.
- ELL-C canale: Donchian travestito (non batte il Donchian equivalente,
  DSR 0.685, IS 1.40 -> HOLD -0.87; target 1.618 = caso; anchor-luck 4h).
- ALB-A diagonale: il condor stessa-scadenza la batte a ogni f; senza
  gate IV-rank tutte le strutture perdono (3a conferma: l'alpha del VRP
  e' il gate); fee-negativa su Deribit a qualsiasi size; 2o caso
  "0-perdite = Sharpe implausibile" dopo CC01.
- ALB-B claims: 82%/PF 5.16/"420%" consistente con zero skill (P=20-45%,
  78.6% delle finestre 6-mesi lo produce); replay con code reali =
  rovina 1998/2002/2020; la diagonale passa il 12-40% della perdita naked.
- Capital scaling 600->2-5k: unico vincolo binding = cap $300/asset
  (a 5k book al 49% del target) -> AL DEPOSITO alzare a equity/2;
  min_order $5 lasciare; XS01 ~20k confermata; aspettativa onesta
  de-luckata 2k ~EUR 0.6-0.8/g, 5k ~EUR 1.4-2/g.

Nessun nuovo sleeve, book live invariato. 168 test verdi.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 23:31:38 +00:00
Adriano Dal Pastro a74cc69583 research(anchor-audit): timing-luck confermato su XS01 e SKH01 — 3/3 sleeve ancorati, book de-luckato HOLD ~2.0
Chiude il pendente dell'ondata timing 2026-07-02. Due audit indipendenti
(sanity replica bit-exact, ancore a priori, zero tuning per-fase, bootstrap):

- XS01 (10 fasi ciclo H=10): fortuna nel DD (15° pctl: 10.8% vs 15.5% tipico,
  29% peggiore) e FULL (85°), non nell'hold-out (65°); P(spike)~0.91-0.94.
  Lens onesta = ensemble di fase FULL 1.25 / HOLD 1.31 / DD 11%. Ammissione
  @15% regge, i numeri 1.50/1.71/11% no.
- SKH01 (23 offset griglia 230m/690m): canonico = 93-98° pctl di OGNI metrica,
  minHold/blend/book-HOLD = massimo dei 23; il gate DD<30% (criterio di
  selezione V2-DD) fallisce in 15/23 offset. Regge: uplift blend positivo a
  tutte le 23 fasi (min +0.18) + corr ~0.08 -> ADDS ridimensionato. Path live
  reale (cron orario + exit software): book FULL 1.46->1.19 / HOLD 1.64->1.15 /
  DD 18->25%, gap-through-stop nei crash (sl2% -> -11/-23%).
- Book 5-sleeve: HOLD 2.46 eredita ~+0.10/+0.17/+0.5 di fortuna d'ancora
  (TP01/XS01/SKH01) -> stima de-luckata HOLD ~1.9-2.1, FULL ~2.0-2.2, DD ~6%.

Nessun cambio operativo (pesi/book live invariati; ogni cambio passa
weights_tilt_null). Narrativa aggiornata (CLAUDE.md, docstring skyhook).
Follow-up: anchor_luck_band() in altlib, cadenza 230m, peso SKH live.
168 test verdi.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 22:34:35 +00:00
Adriano Dal Pastro 73d74c5e53 research(wave-0702): ondata timing + CRT — 8 filoni, 0 nuovi sleeve, finding anchor timing-luck TP01
Goal: "altre strategie su Deribit con timing differenti". 8 filoni multi-agente + scettico:
- event-clock bars, expiry calendar Deribit, clock lenti/bande, regime-speed: SCARTATI
- CRT (Candle Range Theory) base/multi-TF/contesto: SCARTATA 3/3 (DSR~0, ritest =
  informazione negativa; sottoprodotto: FOLLOW>FADE sui livelli prior-day ogni anno,
  conferma il lead prevday)
- FINDING (confermato da scettico indipendente): hold-out 0.31 di TP01 = migliore delle
  24 ancore orarie (mediana 0.04, banda [-0.13,+0.30]) -> narrativa corretta in CLAUDE.md
  e docstring: l'hold-out non risolve l'edge di ritorno, regge il taglio DD a ogni ancora.
  Tranching K=2/4 = solo varianza della stima, no deploy a $600. Audit d'ancora pendente
  su XS01/SKH01. Book live e portafoglio INVARIATI. Test 168/168.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 22:12:22 +00:00
Adriano Dal Pastro e6657fcb16 feat(portfolio): GTAA01 promosso a 5° sleeve @20% — FULL 2.12→2.24, HOLD 2.21→2.46, DD 7.8→6.2%
Il diversificatore strutturale validato il 2026-06-22 (trend difensivo equity
6-ETF su IB, 30y storia, OOS 2015+ indipendente dall'hold-out crypto, corr al
book ~+0.10) era rimasto in paper_combo senza mai essere valutato come sleeve.
Valutazione onesta (r0701_gtaa_5th_sleeve): uplift positivo in-sample e su
TUTTE le finestre disgiunte (+0.05/+0.19/+0.25), multi-cut +0.21..+0.25,
plateau monotono w10-30% — passa dove EW-STR era morto. Ingresso @20% (IS-best
30%, scelta strutturale dichiarata). Convenzioni: weekend equity=0 (capitale
IB fermo, non riciclato), attivazione all'era book 2019-03. Il book live
Deribit (TP01+SKH01) NON cambia. Suite 168/168.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-01 23:31:38 +00:00
Adriano Dal Pastro 491411ac77 research(wave-0701): 6 filoni multi-agente — 0 nuovi sleeve, pesi confermati, gate weights_tilt_null
Ondata onesta su angoli non coperti: funding-TS (chiude il filone funding su 3
lati), breadth alt (non-ridondante ma DSR 0.43, rivisitabile con storia),
XS-residmom (REDUNDANT), pesi+guardia-DD (EW-STR refutato dallo scettico come
selezione-sull'hold-out di 2° ordine, firma best-of-15), VRP-refine (filone
esaurito), stagionalità-XS (morta allo step statistico).

Lezione codificata: weights_tilt_null + combine_outer in src/portfolio
(ogni cambio-pesi vs null di tilt casuali cap-respecting + delta in-sample>=0);
5 test nuovi, suite 165/165.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-01 23:21:59 +00:00
Adriano Dal Pastro ccf5e38101 feat(dashboard): banner alert operativi del book live (SKH/posizione/equity)
Il dashboard non mostrava nessuno dei tre segnali di health introdotti nei
commit 31369b3 (skh_error) e 5670469 (pos_error/eq_fallback): finivano solo su
Telegram + log cron, invisibili a chi guarda il monitor. pos_error/eq_fallback
erano gia' in shadow_report (usato dal dashboard) ma ignorati dal template;
skh_error non era nemmeno recuperato (il dashboard usa shadow_report, non
book_report).

Fix:
- _alerts_banner(): helper puro (verde "nessun alert" se pulito, rosso con una
  riga per errore) che rispecchia i tre alert di book_execute.
- build(): raccoglie pos_error/eq_fallback dallo shadow gia' fetchato + skh_error
  da book_report(offline=True) (feed certificato, nessuna rete extra).
- html(): renderizza il banner in cima alla sezione ① LIVE.
- tests/test_dashboard.py: +4 (verde, 3 alert, parziale, chiave-ignota).

Suite 160/160. Render end-to-end verde (conto sano $598.06 flat). Chiude la
copertura: i 3 pattern di errore silenzioso ora visibili su log + Telegram + dashboard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 22:47:35 +00:00
Adriano Dal Pastro 567046953d fix(book): gate fail-safe posizione + diagnostica equity — niente errori silenziosi nel path live
Terza+quarta chiusura del pattern "eccezione ingoiata -> stato safe silenzioso"
in src/live, dopo skh_error (31369b3). Emerse durante la verifica del gate SKH01.

Opzione A (gate fail-safe posizione):
- shadow._positions: se la read della posizione reale Deribit lancia, assume flat
  MA ora ritorna un pos_error esplicito (prima solo una note mai propagata).
- Propagato shadow_report -> book_report -> book_execute: se ONLINE ma posizione
  IGNOTA, l'esecutore NON opera a cieco (return + alert), come gia' per 'online'.

Opzione B (diagnostica equity, no halt):
- shadow._equity/shadow_report: se ONLINE ma equity reale non leggibile, il book
  ripiega su paper_cap (~$2000) invece del conto reale (~$598) -> sovradimensiona
  ~33%, ma l'hard-cap $300/asset limita il downside. Nuovo flag eq_fallback ->
  book_execute stampa warning + alert Telegram MA prosegue (niente gate: la scelta
  e' solo diagnostica, l'hard-cap gia' protegge).

Test: +8 (4 gate A, 4 diagnostica B). Suite 156/156. Path live pulito
(skh_error/pos_error/eq_fallback = None). Diario 2026-07-01-book-live-error-surfacing.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 22:39:59 +00:00
Adriano Dal Pastro 31369b358c fix(book): esponi skh_error nel book live — niente flat SKH silenzioso
book_report scriveva skh_error in un dict locale MAI incluso nel return ->
r.get("skh_error") era sempre None. E book_execute non lo leggeva comunque.
Risultato: se il feed SKH (fresh_5m) fallisse, il book forzava SKH a flat in
silenzio, indistinguibile da un flat legittimo -> entry SKH reale mancato,
nessun alert.

Fix:
- src/live/book.py: skh_error come variabile esplicita, esposta nel dict di
  ritorno (None normalmente). Il fail-safe (SKH->flat, TP01 indipendente sul
  suo feed certificato) resta invariato.
- scripts/live/book_execute.py: emette la riga di log + alert Telegram quando
  skh_error e' presente.
- tests: book_report cattura-e-flagga l'errore; book_execute lo fa emergere
  (log + notify). Suite 148/148.

Contesto: verifica del gate SKH01 dopo 193 run flat dal 06-23 (gate SANO —
335/341 entry storici, shorta i crash; flat legittimo: ultimo entry 06-06
chiuso ~06-08 pre-arming). Questo chiude il rischio latente scoperto durante
la verifica.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 22:26:02 +00:00
Adriano Dal Pastro 88f5f5a02a docs(memory): CLAUDE.md — completa il log dei filoni 2026-06-29 (1ª ondata A/C/D)
Aggiunge il bullet riassuntivo dei filoni della prima ondata del 29-06 (gia' nei diari
ma mancanti dal log curato in CLAUDE.md): A DVOL-direzionale=HEDGE, B intraday ERM=falso
positivo (gia' coperto dal gate), C xsec-v2 low-vol=debole/STAT-MODE, D macro-gate=ridondante.
Memoria curata ora completa per la sessione 2026-06-29.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 21:29:37 +00:00
Adriano Dal Pastro ee82e0a056 feat(dashboard): box forward-monitor STATARB-RESID (lead ortogonale+eseguibile)
Aggiunge il forward-monitor STATARB-RESID al dashboard accanto a PREVDAY (sezione
③·c FORWARD-MONITOR): carica data/paper_statarb/state.json, mostra doppio libro
MODELED/REAL-$600, ret/maxDD/fill-haircut, posizione spread corrente e giorni forward.
Warn aggiornato: LEAD sotto deflated-Sharpe -> forward per confermare l'edge, non deploy.

Smoke-test html() OK (box presente, dati live). Test 146/146.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 21:20:56 +00:00
Adriano Dal Pastro c52e0ab3f8 feat(forward): cabla STATARB-RESID nel forward-monitor PAPER (lead ortogonale ETH/BTC)
Forward-monitor del LEAD dello sweep 2026-06-29 (relative-value ETH/BTC, dollar-neutral 2 gambe),
il primo stream insieme ORTOGONALE (corr->book 0.027, beta-mkt 0.013) ED eseguibile a $600.

- scripts/live/paper_statarb.py: forward-only, doppio libro MODELED($2000)/REAL-$600 (haircut fill),
  riusa il segnale ESATTO di orthogonal_signals.py (niente reimplementazione). Config CONGELATA
  W=45 sgn=+1.
- Cablato in scripts/cron_daily.sh accanto a paper_prevday. Stato runtime in data/paper_statarb/
  (gitignored).
- test tests/test_paper_statarb.py (frozen config + advance forward/idempotente + haircut $600 basso).

Correzione di etichetta (verificata): la cella vincente e' sgn=+1 -> NON mean-reversion ma
relative-MOMENTUM sul residuo (dislocazioni ETH-vs-BTC continuano a 1d; sgn=-1 perde -1.4 IS).
Diario + CLAUDE.md aggiornati. Test 146/146. Nessun deploy, forward-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 20:58:29 +00:00
Adriano Dal Pastro cff5fa2bf5 research(sweep): 5 thread paralleli — 0 nuovi sleeve, STATARB-RESID LEAD ortogonale+eseguibile
Ricerca onesta su aree inesplorate (harness altlib+xsec_v2_nonmom, tutti i gate incl.
study_family_honest anti-selection-on-holdout). Branch main, nessun impatto live, test 143/143.

1 XSEC low-risk cousins (MAX/idio-vol/Amihud) -> 1 LEAD (IVOL), STAT-MODE, DSR 0.37<0.95
2 XSEC momentum-structure vs XS01            -> tutto REDUNDANT (sostituire XS01 distrugge hold)
3 Meta-allocazione dinamica (4 sleeve)       -> pesi fissi vincono (gia quasi risk-parity)
4 Segnali ortogonali ETH/BTC (2 gambe)       -> STATARB-RESID + DVOLSPREAD LEAD
5 1-gamba a segnale (MACD/RSI/Supertrend/...) -> 0/12 earns_slot (trend=TP01, MR morta, hedge)

LEAD principale STATARB-RESID (mean-rev residuo ETH-b*BTC, OLS rolling, 2 gambe): primo stream
INSIEME ortogonale (corr->book 0.027, beta-mkt 0.013) ED eseguibile a $600 (haircut ~0, NON
STAT-MODE) -> cadono i 2 muri di XS01/opzioni. Resta solo il muro dell'edge (Sharpe 0.84,
DSR 0.929 same-sign <0.95). Causalita+fee verificate dal coordinatore. Forward-monitor, non sleeve.

Soffitto direzionale ~1.3 riconfermato. Diario 2026-06-29-strategy-search-5threads.md, CLAUDE.md agg.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 20:50:33 +00:00
Adriano Dal Pastro 02d875b07b merge(research): equity IB screeners + 4 filoni crypto 2026-06-29 + gate selection-on-holdout
Ricerca onesta, branch separato (nessun impatto sul book live). Test 119/119 verdi.

Equity (IB/Yahoo):
- scalping 'sottoquotate' su IB + check dati dalla rete (forward, edge non provato)
- screener short 'fond/news neg ma prezzo su' (forward, edge non provato)

Crypto BTC/ETH + universo HL (harness altlib, causale, fee 0.10% RT, marginal vs TP01):
- A DVOL direzionale  -> LEAD hedge/DD-dampener, NON sleeve
- B Intraday ERM      -> falso positivo: earns_slot da selezione-sull'hold-out + coda 2026
- C Cross-sectional non-mom (low-vol HL) -> DEBOLE/forward-monitor (deflated-Sh 0.13) STAT-MODE
- D Macro regime-gate -> RIDONDANTE col trend (corr->TP01 0.989), SCARTATO

Harness indurito (LESSON 4 in altlib): deflated_sharpe + select_cell_insample +
study_family_honest -> chiude il punto cieco "selection-on-holdout".
2026-06-29 20:09:18 +00:00
Adriano Dal Pastro b4ec92734c research(intraday): de-bias del lead ERM (filone B) — falso positivo + gate selection-on-holdout
L'analisi di robustezza affonda lo "earns_slot=True" di ERM: era prodotto da
selezione-sull'hold-out + coda 2026 + multiple-testing non corretto.
  A) deflated-Sharpe FAIL: 0.00 (tutti 122 trial) / 0.16 (no-TOD) / 0.24 (solo-ERM) << 0.95
  B) selezione in-sample-only -> ALTRA cella (long-flat, corr->TP01 0.53) = NEUTRAL, no slot
  C) ensemble del plateau (no cherry-pick) -> ADDS ma robust_oos=False -> no slot
  D) uplift FULL solo +0.10, negativo 2021/2022; uplift HOLD +0.30 concentrato nel 2026
=> ERM SCARTATO come sleeve. Conferma ennesima del soffitto BTC/ETH-direzionale ~1.3.

Lezione CODIFICATA in altlib (LESSON 4, test in tests/test_harness_realism.py):
  - deflated_sharpe()       Bailey & Lopez de Prado, PASS >= 0.95
  - select_cell_insample()  scelta cella col solo Sharpe pre-HOLDOUT (no peeking)
  - study_family_honest()   gate combinato: earns_slot[cella in-sample] AND DSR>=0.95
Regola: una strategia direzionale grid-searched si giudica con study_family_honest,
non chiamando study_marginal sulla cella a max hold-out. Verificato end-to-end su ERM
(earns_slot_honest=False). Chiude il punto cieco gemello di CC01.

Diario aggiornato (verdetto downgrade), CLAUDE.md aggiornato. Test 119/119 verdi.
Nessun impatto live (branch separato).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 20:04:36 +00:00
Adriano Dal Pastro aad69f9790 research(crypto): 4 filoni 2026-06-29 — ERM lead sub-daily (forward), 3 scartati/deboli
Ricerca onesta su BTC/ETH + universo HL, branch separato (nessun impatto live).
Harness condiviso altlib (causale, fee 0.10% RT, marginal vs TP01, day-boundary,
haircut $600). Test 19/19 verdi.

- A DVOL direzionale  -> LEAD hedge/DD-dampener, NON sleeve (buy-the-fear; is_hedge).
- B Intraday ERM 8h   -> LEAD forte / forward-monitor: earns_slot=True, ADDS oltre
                         SKH01 (TP01+SKH+ERM 60/25/15 FULL 1.88/HOLD 1.46/DD 8.9%).
                         Caveat: plateau hold-out single-row, multiple-testing non
                         deflazionato, exec 8h. Controllo TOD = FAIL atteso.
- C Cross-sectional non-mom (low-vol HL) -> DEBOLE/forward-monitor (deflated-Sh 0.13,
                         storia 2.5a, non eseguibile $600) STAT-MODE.
- D Macro regime-gate -> RIDONDANTE col trend (corr->TP01 0.989), SCARTATO.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 19:48:36 +00:00
Adriano Dal Pastro a158d0e2ae research(equity): screener short 'fond/news neg ma prezzo su' (forward, edge non provato)
Goal chiarito: short su titoli con fondamentali/notizie negativi ma prezzo in salita.

Gate dati (v2.0.0): NON backtestabile -> fondamentali da rete = snapshot correnti,
applicarli a prezzi passati = look-ahead. Come la term-structure: solo screener forward.

Costruito screener da dati di rete tokenless: fondamentali strutturati Yahoo
(quoteSummary via crumb: recommendationMean/surprise/revGrowth/sell-skew) + sentiment
headline + momentum 1m/3m. SHORT cand = (fond/news neg) AND prezzo su. Run live oggi:
nessun candidato (mercato in flessione ampia -> gamba 'prezzo su' non scatta).

Intuizione chiave: shortare la FORZA combatte momentum + PEAD; il rialzo 'malgrado'
brutte notizie spesso prezza info che i fondamentali trailing non hanno -> e' la
versione contrarian/rischiosa dell'anomalia. La versione pulita = short su fondamentali
deboli quando il prezzo CONFERMA (scende), non quando diverge.

Eseguibilita': borrow/squeeze/perdita illimitata/PDT 5k/IB instabile/00 -> non deployabile.

- scripts/research/eq_fundnews_short.py (+ forward log data/raw/fundnews_short_screen.parquet)
- tests/test_eq_fundnews_short.py (scoring puro offline)
- docs/diary/2026-06-26-equity-fundnews-short.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 19:51:22 +00:00
Adriano Dal Pastro e28176efef research(equity): scalping 'sottoquotate' su IB + check dati dalla rete
Goal: comprare azioni IB quando sottoquotate, con verifica incrociata dei dati.

1) CHECK DATI DALLA RETE (il pezzo richiesto): IB certificato (eq_*, ADJUSTED) vs
   Yahoo adjclose (sorgente rete indipendente, tokenless). Tutti CONCORDE <=1.2bps
   sui rendimenti. Teaching: un confronto naif (adjusted vs grezzo) falso-allarmava
   4/6 ticker a 30-52bps = TUTTO stacco dividendo -> ogni divergenza va SPIEGATA
   prima di gridare 'feed sporco' (v2.0.0). Strumento riutilizzabile (validatore feed
   / pre-trade price-check).

2) Scalping 'sottoquotate': NON testabile (no dati intraday) ne' eseguibile (PDT rule
   5k blocca il day-trading; IB instabile). Versione testabile = swing MR (Connors
   RSI2<10 + MA200, exit MA5): Sharpe modesto (SPY 0.75/0.70) ma NON batte il buy&hold
   (B&H hold 0.81), expo 13%, CAGR 2-5%, fee-sensitive. Niente edge schierabile.

- scripts/research/eq_meanrev_ib.py
- tests/test_eq_meanrev.py (incl. network check)
- docs/diary/2026-06-26-equity-meanrev-network-check.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 19:42:14 +00:00
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
Adriano Dal Pastro 7eb0f67956 feat(dashboard): show SKH01 sleeve in 4-sleeve portfolio view
active_sleeves() already feeds the per-sleeve table & combined metrics, so SKH01
appears automatically. Manual touch-ups: title/docstring -> +SKH01; position label
is now sleeve-aware (the None fallback used to mislabel every pos-fn-less sleeve as
XS01's "book 19 gambe" — now XS01/SKH01/VRP01 get correct labels); footer note adds
SKH01 (quasi-orthogonal @25%, FULL Sharpe 1.68->2.13, DD 14->8%, research/forward-monitor).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 16:41:52 +00:00
Adriano Dal Pastro 8d1fe173f7 feat(portfolio): wire SKH01-V2-DD sleeve @25% effective -> 4-sleeve book
Add Skyhook (SKH01_V2_DD) as a portfolio sleeve. Effective weight 25%: the three
existing sleeves scaled into the remaining 0.75 keeping their 55:25:20 ratio
(TP01 41.25% / XS01 18.75% / VRP01 15% / SKH01 25%).

_skyhook_returns(): 50/50 BTC+ETH daily series of the dual-TF regime+breakout engine
(causal, net 0.10% RT), same convention as the marginal lens.

Portfolio impact (run_portfolio.py), 3-sleeve -> 4-sleeve:
  FULL Sharpe 1.68 -> 2.13 (+0.45), FULL maxDD 14.3% -> 7.8% (halved)
  HOLD-OUT Sharpe 1.63 -> 2.30 (+0.67), HOLD-OUT maxDD ~3.5% (flat)
  Positive every year 2019-26 (annual DD <=7.8%) vs buy&hold 50/50 FULL Sh 0.93 / DD 76%.

Skyhook is quasi-orthogonal (corr ~0.09 to TP01) so it lifts Sharpe AND cuts DD.
Research portfolio (fixed weights, no real rebalancing cost at $600; Skyhook daily
Sharpe is the step-marked lens convention) -> forward-monitor, not deploy.
Tests: 25 pass (skyhook 8 + portfolio 7 + vrp 4 + trend 6). Diary + CLAUDE.md updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 16:22:15 +00:00
Adriano Dal Pastro de72e3ce1f feat(skyhook): SKH01-V2-DD — asymmetric %-exits cut standalone DD <30% (2-wave agent research)
Second agent wave (skyhook-improve-v2, 14 DD-reduction families, each adversarially
verified by 2 skeptics) beats the prior winner on the only unmet goal (DD<30%).

Winner = ASYM_LS -> promoted to engine as SKH01_V2_DD:
  same signal (ptn_n=45, vola[35,95], vol_lo=0, exit-bars 24/16) but exits switched
  from ATR to FIXED-PCT ASYMMETRIC — long sl4%/tp10%, short sl2%(tighter)/tp8%.
  The tight short %-SL caps the per-trade loss that forms the maxDD in vol spikes.

Verified (sk.study, independent re-run): standalone maxDD BTC 21.4% / ETH 27.4% (<30%),
minFull +0.99, minHold +1.26, causality 0/400 both assets, fee-surviving to 0.40%RT,
marginal vs TP01 ADDS (corr 0.09, in-sample edge, robust_oos, multicut, clean-year +0.57),
blend 0.75*TP01+0.25*SKH uplift_hold +0.87; blend 50/50 full 1.84/hold 1.59/DD 10.7%.
Plateau (not knife-edge); both skeptics holds_up=high, killer=null.

Engine: per-direction short exit overrides (exit_mode_short/sl_*_short/tp_*_short),
backward-compatible (None -> symmetric, V1/intermediate-winner unchanged). +3 tests (8/8 pass).

Lessons: DD is cut by changing the exit MECHANISM (%-SL, L/S asymmetry, ensembles), NOT by
entry-only kill-switch / vol-target / cadence. PATTERN_CONF killed as overfit (knife-edge).
PCTL_DD unverified (rate-limit) and ENS_PARAM/TPSL_DD recency/hedge-loaded -> forward-monitor.
NOT yet wired to live sleeves: re-verify blend@0.25 + causality on execution code before deploy.

Includes both waves' research scripts (runs/SKH_* wave 1, runs/SKH2_* wave 2).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 16:10:38 +00:00
Adriano Dal Pastro 8e46a62e67 docs(skyhook): diario porting SKH01 + V1 (sintesi onda agenti in aggiornamento)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:47:41 +00:00
Adriano Dal Pastro c7c07f4c35 test(skyhook): demo anchors + dual-TF alignment + causality + V1 robustness (5 pass)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:46:47 +00:00
Adriano Dal Pastro 2d8faf3896 research(skyhook): inline lever-scout -> shorts essential, regime gate matters, ptn_n=55/vol_lo=40/wider-stops lift hold-out
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:35:35 +00:00
Adriano Dal Pastro 64d98a070d feat(skyhook): SKH01 dual-TF regime+breakout engine + honest eval harness
Porting onesto del sistema ES Skyhook su BTC/ETH certificati:
- src/strategies/skyhook.py: 690m(segnale)+230m(exec) da 5m; BuzVola/BuzVolume
  Chande 0-100 (ancore demo verificate); Donchian breakout HTF; regime gate;
  composer; entries asimmetrici (uscitalong/short + stop/profit ATR) per backtest_signals.
- scripts/research/skyhook/skyhooklib.py: study (FULL/HOLD/fee-sweep/per-anno BTC&ETH),
  causality guard (0 mismatch), marginal-vs-TP01.
Baseline: BTC FULL Sh +0.91/+581%, ETH +0.64/+255%, fee-surviving, ma HOLD-OUT debole -> da migliorare.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:33:54 +00:00
Adriano Dal Pastro 237ca8da13 research(report): resoconto PROTETTO (soft-guard DD -4%) anno-per-anno -> combo + TP01 + GTAA singoli
Report che mette combo (TP01+GTAA 50/50), TP01 e GTAA sulla stessa griglia giorni-di-borsa
(esposizione 1x, come dentro al combo), applica la guardia-DD -4% a ciascuna serie e tira fuori
per anno: NL (net liquidation da $2000), DD intra-anno, rendimento, Sharpe + riga TOT con CAGR.
Combo protetto: CAGR +9.1% / DD 5.8% / Sh 1.38 (2022 -1.8%); baseline +11.3% / 8.4% / 1.48.
Aggiunto data/paper_combo/ al .gitignore (stato paper runtime, come gli altri paper dir).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:16:59 +00:00
Adriano Dal Pastro 856a02fcc5 research(stops): SL classici vs soft-guard -> il soft-guard vince (lo SL duro whippa nel grind)
Goal 'prova anche SL'. Test equo (trigger/re-entry sul NAV mercato). soft-guard -4% Sh 1.38/DD 5.8%
resta il migliore; trail-stop -6% valido ma inferiore (1.34/6.6%); -4% whipsaw (1.07, inMkt 42%);
stop mensile/vol inutili. Per un DD da grind, de-risk parziale > uscita totale.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 13:09:43 +00:00
Adriano Dal Pastro 010d1f0733 feat(combo): paper combo NUDO vs PROTETTO (guardia-DD -4%) affiancati + dashboard
paper_combo traccia forward entrambe le versioni; dashboard mostra nudo + protetto. Guardia-DD:
de-risk 0.4x a DD>-4%, ri-rischia a -1.6% (backtest MaxDD 8.4->5.8%, 2022 -4.4->-1.8%). Opzioni
escluse (non aiutano il grind). Container ricostruito.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 12:59:07 +00:00
Adriano Dal Pastro 1c15c3c1be research(tail-hedge): protezione DD combo (incl. opzioni) -> vince la guardia-drawdown
Goal: sleeve/overlay protettivo per il combo (TP01+GTAA), anni tipo 2022, valutare opzioni.
Diagnosi: DD combo 8.4% e' grind-lento (2022 -4.4%), non crash -> il doppio trend gia' taglia i crash.

Test (tail_hedge_lab.py): guardia-DD -4% -> MaxDD 8.4->5.8%, 2022 -4.4->-1.8%, Sharpe 1.48->1.38,
CAGR 9.2%. Vol-target NON aiuta (2022 non e' vol-spike). OPZIONI (put/put-spread LONG su BTC/ETH,
premio BS su DVOL): sempre-on ~50%/anno -> con budget 3%/y effetto ~nullo, e nel grind 2022 sanguinano;
pagano solo nei crash secchi (stress -30%: put +25% netto). -> black-swan insurance cara, fuori
bersaglio per il 2022. A leva: guard rende il 2x sopportabile (2022 -10.9%), il 3x resta margin-call.

RACCOMANDAZIONE: aggiungere guardia-drawdown di portafoglio (no premio); opzioni solo eventuale
micro-hedge black-swan. Costo onesto del guard: -2.1pp CAGR per dimezzare il MaxDD.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 12:55:14 +00:00
Adriano Dal Pastro 3a3bdd5c7b research(report): sim leva 1x/2x/3x combo vs TP01 (DD reale + margin-call), da 2k/5k
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 12:44:54 +00:00
Adriano Dal Pastro f983cc2447 research(report): resoconto anno-per-anno combo/TP01/GTAA da $2k (PnL/MaxDD/NumTrades)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 12:37:23 +00:00
Adriano Dal Pastro 389573e517 feat(dashboard): pannello COMBO cross-venue (TP01 Deribit + GTAA IB)
Aggiunge alla dashboard la sezione "COMBO DEPLOYABLE — cross-venue (paper)": equity paper forward
del blend 50/50 TP01+GTAA (da data/paper_combo/state.json) + posizioni azionabili IB correnti
(gtaa_weights: peso ETF + cash, asof). Nota onesta: paper rischio-zero, Sharpe ~1.5 ottimistico, il
robusto e' la diversificazione (corr 0.21, DD dimezzato); XS01/VRP01 esclusi (STAT-MODE).
Container ricostruito (codice baked nell'immagine).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 12:32:19 +00:00
Adriano Dal Pastro 3b552a92da feat(combo): paper-trade cross-venue TP01 (Deribit) + GTAA (IB), forward-only
L'unica cosa vera/deployabile della ricerca: diversificazione TP01+GTAA (corr 0.21, blend Sharpe ~1.5,
DD dimezzato). Si va in PAPER cross-venue.

- src/portfolio/gtaa.py: GTAA sleeve di prima classe (trend difensivo TSMOM vol-target 12% su
  SPY/QQQ/IWM/TLT/GLD/HYG). gtaa_returns() Sharpe 0.64; gtaa_weights() = pesi ETF correnti azionabili.
- scripts/live/paper_combo.py: tracker forward-only blend 50/50 TP01+GTAA (crypto compoundato su grid
  giorni-di-borsa), mostra posizioni azionabili su entrambi i venue. Solo gambe eseguibili.
- fetch_ib_equities.py --only: refresh mirato dei 6 ETF GTAA per il cron.
- cron_daily.sh: up gateway IB + refresh ETF GTAA + avanza paper_combo (dipendenza cross-venue gestita).

Init 2026-06-23: TP01 flat (risk-off), GTAA SPY13/QQQ8/IWM9/TLT17/GLD2/HYG17/cash34. Catena
gateway->refresh->paper testata end-to-end. PAPER (rischio zero), valida l'operativita' cross-venue.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 12:26:51 +00:00
Adriano Dal Pastro 09c8bb7de8 research(cross-market): oltre SP500 (bond/commodity/indici esteri) -> niente, artefatto di confine UTC
Esteso il test crypto-lead a ZN(bond), ESTX50/DAX(Europa), NKD(Nikkei) via futures orari IB
(commodity GC/CL/HG bloccate da subscription). Test non-sovrapposto crypto[T-8h->T]->future[T->T+6h].

ES/NQ/RTY niente (gia'); ZN negativo; NKD debole (~overnight drift). ESTX50/DAX SEMBRANO fortissimi
(t_crypto 7.8, Sharpe 2.5, 3/3 anni) MA e' artefatto di confine UTC: picco a coltello a T=00:00,
morto a T=1h; GAP di 1h uccide l'effetto (Sharpe 2.45->-0.52); tutto l'edge nella singola barra
00:00->01:00 (Sh +2.93) vs ora dopo (-1.02). Firma esatta di day_boundary_robust (CLAUDE.md).

VERDETTO: nessuna anticipazione crypto->mercato sfruttabile, ne' SP500 ne' altro. Sempre co-movimento
contemporaneo (risk-beta) o artefatto di confine. Resta valido solo il diversificatore TP01+GTAA.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 12:12:16 +00:00
Adriano Dal Pastro c4bc336a53 research(cross-market): futures overnight non-sovrapposto -> edge ~0 su SP500, soffio su small-cap
Test ONESTO dell'idea "monitor Deribit/trade IB": entra mid-notte sul future indice, cattura il moto
SUCCESSIVO (finestre non sovrapposte, no look-ahead). Dati: ES/NQ/RTY orari da IB (fut_*_1h, ~3y).

RISULTATO: ES (S&P500) nessun edge (Sharpe ~0/neg, t_crypto 0-1.5); NQ momentum del future non crypto;
RTY (small-cap) unico con t_crypto incrementale 2.0-2.7 e crypto che aggiunge oltre il moto proprio del
future, ma Sharpe 0.4-0.5, 24 config (multiple-testing), 2.3y, per-anno incoerente (2026 negativo).

VERDETTO: l'idea NON da' edge tradabile, men che meno su SP500. Il forte crypto<->equity e' co-movimento
contemporaneo (risk-beta), non anticipazione: imposta una finestra causale non-sovrapposta e svanisce.
Il "Sharpe 5" del gap era look-ahead. RTY -> forward-monitor al piu'. Coerente col soffitto del progetto.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 11:51:15 +00:00
Adriano Dal Pastro 55c337144e research(cross-market): "monitor Deribit/trade IB" gap = LOOK-AHEAD, edge tradabile ~0
L'idea (segnale crypto overnight -> trade indice IB) sembrava Sharpe 3.6-5.9 ma e' look-ahead:
la finestra segnale crypto [P21:00->D13:00] e il gap equity [Pclose->Dopen] coprono le stesse ore.
All'entrata (D13:00, pre-open) il gap e' gia' avvenuto -> non catturabile con l'ETF.
Decomposizione (net 2bps, sqrt252): OVERLAP gap Sh ~3.6-4.0 (artefatto) vs TRADABILE intraday
Sh -0.03..0.25 (reale, ~0, muore a costi). Conferma/rafforza "non deployabile" del workflow.
Resta possibile solo la versione futures mid-overnight (finestre non sovrapposte) -> serve dato
intraday ES/NQ, non in cache.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 11:23:18 +00:00
Adriano Dal Pastro d916520f2c research(cross-market): sweep 65-agenti crypto->mercati IB -> fenomeno gap robustissimo ma NON edge
Goal: >=50 agenti, migliore soluzione, diversi mercati e timing, su piu' anni.
Setup: 26 ETF certificati (IB) + BTC/ETH 1h; harness parametrizzato (lead overnight crypto ->
gap/intraday equity, t-incrementale, Sharpe IS/OOS, hit per-anno); workflow 416 config = 52 sweep +
12 verifica avversariale + 1 sintesi = 65 agenti.

RISULTATO: cluster fortissimo crypto-overnight -> GAP apertura equity (tutti i target risk-on).
Migliori: ETH->IWM/QQQ/XLK gap (Sh OOS 2.4-2.5, t 17), BTC->QQQ gap (Sh OOS 2.31, t 15, 9/9 ANNI).
Regge stress 10bps e OOS recente. MA due killer (verificatori concordi):
  1. NON tradabile via ETF (gap gia' all'open) -> serve future overnight (MNQ/MES), fuori dal
     capitale $0.5-2k (margin/liquidazione);
  2. e' RISK-BETA non alpha: finestra-lead ~contemporanea al gap (stesso shock macro), forza solo
     negli anni alta-vol (2022), beta implicito ~37%.
Unico ETF-tradabile (ETH->XLE intraday) crolla a 10bps (0.48->0.15), t 2.38 sotto Bonferroni/416.

VERDETTO: nessun edge proprietario deployabile a basso capitale. Migliore FENOMENO da forward-monitor
= BTC->QQQ gap overnight (9/9 anni). Coerente col soffitto del progetto. Valore: aver classificato il
fenomeno (risk-beta overnight) invece di scambiarlo per alpha.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 22:32:28 +00:00
Adriano Dal Pastro d2d535cf6a research(cross-market): crypto x mercati IB -> trovata ANTICIPAZIONE weekend-crypto -> lunedi' equity
Goal: cercare correlazioni/anticipazioni crypto<->IB. Dati cache (BTC/ETH Deribit 1h->1d; ETF eq_*).

(1) Correlazione contemporanea: crypto = risk-on (BTC ~0.32 SPY/QQQ/IWM, 0.25 HYG, 0.13 GLD, ~0 TLT).
(2) Lead-lag GIORNALIERO: NIENTE (picco k=0, rumore a |k|>=1) -> nessuno anticipa l'altro al daily.
(3) EFFETTO WEEKEND (anticipazione pulita): crypto Sab+Dom (equity chiuso) anticipa il lunedi'.
    GAP lunedi' corr +0.22/0.24 (SPY/QQQ/IWM/HYG), hit 59-62%, si RAFFORZA OOS22+ (+0.30/0.36).

Validazione avversariale:
  (A) INCREMENTALE vs venerdi': beta weekend-crypto significativo (QQQ gap t=+4.7, intr +2.9; SPY
      +4.4/+2.0; IWM +4.7/+2.7), friday_eq NON signif. -> info crypto-specifica, non momentum equity.
  (B) TRADABILE (entro Mon open, esco close, net 4bps): QQQ hit 60%, Sharpe 1.46 (OOS 1.33), long-flat
      OOS 1.91 ~+9%/yr; SPY/IWM piu' deboli ma OOS positivi.

VERDETTO: prima anticipazione cross-mercato reale. Crypto = proxy 24/7 risk-sentiment; lunedi' equity
recupera la direzione del weekend. Caveat: capacita' bassa (~52 lun/anno), tattico non cornerstone;
gap catturabile via futures IB (MNQ domenica sera) da validare. Coerente su 3 ETF (no cherry-pick).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 21:55:01 +00:00
Adriano Dal Pastro 67850b0dd8 research(equities): valida combo DEPLOYABLE TP01+GTAA (le due gambe eseguibili)
Combo onesto/eseguibile a basso capitale: TP01 (Deribit, gia' armato) + GTAA vt12 (IB), senza
XS01/VRP01 STAT-MODE. Finestra 2019-2026, TP01 compoundato sui giorni di borsa.

RISULTATO: corr TP01<->GTAA +0.21; blend 50/50 Sharpe 1.48 (40/60 e risk-parity 1.52) > best solo
1.25, maxDD 14%->8%. DIVERSIFICA anche da deployable.
CAVEAT: 2022 negativo (-2.64, trend whipsaw su entrambe), anni boom gonfiano l'assoluto (recenti
~0.95) -> il dato robusto e' il +0.27 di diversificazione, non il livello. Costo deployability:
crypto-pieno+GTAA 1.81 vs 1.48 (i ~0.33 persi = XS01/VRP01 non eseguibili). Cross-venue Deribit+IB.

Migliore config rischio-aggiustata EFFETTIVAMENTE eseguibile trovata post-reset. Non risolve EUR50/g
(capitale). Prossimo: paper-trade GTAA su IB (forward-only) per validare l'esecuzione cross-venue.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 21:48:35 +00:00
Adriano Dal Pastro 437cf11199 research(equities): EQ-GTAA01 trend multi-asset + COMBO cross-mercato (diversifica il crypto)
(1) GTAA: trend difensivo long-flat su SPY/QQQ/IWM/TLT/GLD/HYG (EW sugli asset disponibili).
  GTAA lf vt12%: Sharpe 0.64 (OOS 0.89), maxDD 15% (8% sui 6-asset 2016+), corr SPY 0.64.
  Migliore sleeve equity: rischio-aggiustato > mono-SPY, DD bassissimo, diversificatore migliore.
  Difensiva (CAGR basso). Bear DD: GFC 14% vs 55%, COVID 10% vs 34%.

(2) COMBO cross-mercato: crypto (TP01+XS01+VRP01) x equity (GTAA vt12), finestra 2019-2026.
  corr crypto<->equity = +0.17 (bassissima). blend 50/50 Sharpe 1.81 > crypto solo 1.60 >
  equity 1.12; maxDD dimezzato 14%->7%. DIVERSIFICA: primo miglioramento STRUTTURALE del
  rischio-aggiustato complessivo della ricerca post-reset (diversificazione vera, non alpha).

CAVEAT: finestra crypto corta/favorevole (Sharpe assoluti ottimistici), cross-venue Deribit+IB,
XS01/VRP01 STAT-MODE -> il combo deployable reale e' ~TP01+GTAA. Non risolve EUR50/g (capitale).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 21:42:42 +00:00
Adriano Dal Pastro c9b89739c1 research(equities): EQ-TREND01 trend difensivo su SPY = edge difensivo REALE (analogo TP01)
Il cross-section e' morto (EQ-MOM01), ma il trend DIFENSIVO time-series su SPY regge — stesso
tipo di TP01 nel crypto. TSMOM multi-orizzonte / SMA-200 long-flat, causale, netto fee, OOS 2015+.

RISULTATO: Sharpe 0.54->0.62/0.65, maxDD DIMEZZATO (55%->~27%; nei bear lenti piu': GFC 19% vs
55%, dot-com 26% vs 49%, COVID 17% vs 34%). Plateau robusto (0.56-0.65), fee-robusto (0.48 a
0.10%/lato), basso turnover, eseguibile a $0.5-2k (switch mensile SPY/cash). SMA-200 = piu'
semplice E migliore.

CAVEAT: e' risk-management non alpha (CAGR -2/3pp); i tagli grossi sono in-sample (OOS 2015-26
quasi tutto toro -> ha seguito SPY a beta minore, ma COVID dimezzato). long-bonds TLT non convince.

Lezione cross-mercato confermata: il valore robusto e' ridurre il rischio (trend long-flat), non
battere il buy&hold. Prossimo: trend multi-asset/GTAA + diversifica la sleeve crypto?

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 21:37:37 +00:00
Adriano Dal Pastro 03267b8fc3 research(equities): EQ-MOM01 momentum settoriale -> NON batte SPY
Primo backtest del fronte equity. Momentum cross-sectional settoriale (9 SPDR, 1998-2026),
causale, netto fee, OOS 2015+, giudicato marginale vs SPY buy&hold (il baseline equity).

VERDETTO: nessun edge. Long-short Sharpe -0.08 (alpha cross-sectional MORTO su 27y,
decadimento post-2000 noto). Long-only ~= SPY (corr 0.85, uplift marginale ~0.00) = SPY a
beta piu' basso. Plateau stabile ~0.50 vs SPY 0.51; sugli 11 settori (2018+) peggio (0.69
vs 0.82). L'unico beneficio (maxDD 55->39%) e' del vol-target, non del momentum.

Coerente col progetto: il relative-value momentum e' morto anche in equity (come ortho wave
nel crypto). Prossimo angolo: TS-trend difensivo su SPY (analogo equity di TP01) per tagliare
il drawdown, non per battere il CAGR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 21:33:38 +00:00
Adriano Dal Pastro 1c0b5f1869 research(equities): apre il fronte azioni/ETF via IB — dati certificati + cache su disco
Branch dedicato. Disciplina v2.0.0: prima il dato certificato, poi la strategia.
IB paper (gnzsnz/ib-gateway) da' storia daily ADJUSTED_LAST (div+split) profonda.

- ib_equities_probe.py: sonda fattibilita' dati (profondita', adjusted, subscription).
- fetch_ib_equities.py: FETCH+CERTIFY universo -> data/raw/eq_<sym>_1d.parquet (ms epoch,
  namespace dedicato). RIPARTIBILE (salta i parquet gia' scritti) -> niente refetch da IB.
  Certifica: integrita', gap lunghi, sanita' ritorni, sanita' adjustment.
- eqlib.py: harness ricerca equity. Legge la CACHE su disco (lru_cache) MAI da IB; universi
  (11 settori SPDR + 9 classici 1998+ + broad), panel allineato, riusa lo scorer indurito altlib.

UNIVERSO CERTIFICATO (17, data/raw/eq_* gitignored = cache locale):
  9 settori classici dal 1998-12-22 (27.5y) + XLRE(2015)/XLC(2018) + SPY(1996,30y)/QQQ/IWM/
  GLD(2004)/HYG(2007)/TLT(2016). Tutti integri (monotoni, no dup, no spike>50%, gap-lunghi 0).
  Start comune: 9 classici 1998, 11 settori 2018.

Prossimo passo: prima ricerca = momentum cross-sectional settoriale, gauntlet onesto.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 21:29:11 +00:00
Adriano Dal Pastro 61180637eb research(funding-carry): FC01 cross-sectional su HL -> fragile, NON regge + infra IB paper
Onda "nuova ricerca mirata". Unico meccanismo non coperto dalle 2 ondate: carry da
funding (cashflow perp, delta-neutral). Scan dati: price-clock gia' FAIL (intraday),
Deribit ccxt 0 righe, Cerbero solo candele -> fonte = API pubblica Hyperliquid.

- fetch_hl_funding.py: 19 major, funding orario reale dal 2023-05, certificato
  (0 gap, cov 98-100%, ann +1.0% APT .. +21.6% NEAR). backoff anti-429.
- funding_carry_hl.py: book dollar-neutral short-alto-funding/long-basso, causale come
  XS01, vol-target 20%, fee 0.05%/lato. Giudizio: marginal_vs_tp01 indurito + overlap XS01.

VERDETTO: il premio esiste (carry >> anti) ma il book NON regge il gauntlet.
  FULL -0.12, HOLD -0.50, DILUTES vs TP01, in-sample edge <0.5, no multicut.
  Jackknife universo: FULL oscilla [-0.39,+0.30] togliendo UN asset -> FRAGILE/overfit.
  (preview a 17 asset era +0.62 ADDS: fortuna, mancavano NEAR/AAVE). corr XS01 -0.19
  (ortogonale, non re-skin). Meccanismo: carry-vs-momentum, gli alto-funding pompano.
  -> NON entra in portafoglio, fetcher NON in cron. Diario completo.

Infra IB (thread parallelo): gateway paper gnzsnz/ib-gateway (127.0.0.1:4002, READ_ONLY)
in docker-compose + ib_probe.py. Esito dati basis CME micro: backtest NON fattibile
(ContFuture back-adjusted, scaduti=1 barra). IB ok per esecuzione/forward, non ricerca.
.env.ibgw gitignored (credenziali paper), template in .env.ibgw.example.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 20:56:18 +00:00
201 changed files with 37992 additions and 49 deletions
+7
View File
@@ -0,0 +1,7 @@
# Credenziali IB Gateway PAPER per la ricerca dati (account paper, es. DUQ513966).
# COPIA questo file in .env.ibgw (gitignored) e riempi i valori REALI.
# cp .env.ibgw.example .env.ibgw && chmod 600 .env.ibgw && nano .env.ibgw
# NON committare mai .env.ibgw. Sono credenziali del CONTO PAPER (nessun denaro reale),
# l'API e' comunque READ_ONLY (solo dati storici, nessun ordine).
TWS_USERID=il_tuo_username_paper
TWS_PASSWORD=la_tua_password_paper
+4
View File
@@ -6,6 +6,8 @@ build/
.venv/
.env
!.env.example
.env.ibgw
!.env.ibgw.example
.vscode/
.idea/
.DS_Store
@@ -63,3 +65,5 @@ scripts/research/blind/leaderboard.json
# forward-monitor runtime state (regenerable, forward-only)
data/paper_prevday/
data/paper_combo/
data/paper_statarb/
+240 -6
View File
@@ -40,6 +40,14 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis
Deploy/paper a **1d**. Diari `2026-06-19-tp01-verification.md` / `-tp01-lookahead-fix-lf.md`.
Paper trader: `scripts/live/paper_trend.py` (1d). Test: `tests/test_trend_portfolio.py`.
Ri-verifica: `scripts/analysis/{verify_tp01,stress_tp01,tp01_lowfreq}.py`.
⚠️ **ANCHOR TIMING-LUCK (2026-07-02, confermato da scettico):** l'hold-out ~0.31 è calcolato
sull'ancora daily 00:00 UTC, che è la **migliore delle 24 possibili** (mediana ancore 0.04, banda
[0.13,+0.30]; P~0.86 che una qualsiasi ancora mostri uno spike così per puro caso) → l'hold-out
2025-26 NON risolve l'edge di ritorno di TP01; ciò che regge a OGNI ancora è il **taglio del DD**
(7-10% vs ~60% B&H). FULL/plateau/deflated-Sharpe/gate INVARIATI (h=0 al 31° pctl su FULL).
Regola: i futuri numeri hold-out di strategie a ribilanciamento ancorato si citano con la banda
d'ancora. Diario `2026-07-02-timing-crt-wave.md`; script `scripts/research/r0702_tp01_offset.py`
+ `r0702_skeptic_offset.py`.
- **XS01 Cross-Sectional Momentum (Hyperliquid) — DIVERSIFICATORE che migliora il portafoglio** —
`src/portfolio/sleeves.py:_xsec_returns`. Market-neutral su **19 alt liquidi major** Hyperliquid (1d,
dal 2024): ogni 10g long i 5 più forti / short i 5 più deboli, vol-target 20%. **Scorrelato a TP01
@@ -49,13 +57,55 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis
regime compatto). Standalone FULL Sh **1.50** / HOLD 1.71 / DD 11%, plateau robusto (lookback, gate
p15-35). **Caveat:** storia ~2.5 anni; STAT-MODE (book a 19 gambe non eseguibile a 2k, serve ~20k) →
monitor forward. NB il gate concentra XS nei regimi dispersi (2025-26 = hold-out alta-dispersione).
⚠️ **PHASE TIMING-LUCK (2026-07-02):** i numeri headline sono sulla fase 0 del ciclo H=10, che è
al **15° pctl di DD** (10.8% vs ~15.5% fase tipica, 29% peggiore) e 85° di FULL fra le 10 fasi
(HOLD solo 65°, non estremo); P(spike per caso)≈0.91-0.94. Lens onesta = **ensemble di fase:
FULL 1.25 / HOLD 1.31 / DD 10.9%**; a fase mediana FULL 1.08/HOLD 1.10/DD 21%. La decisione di
ammissione @15% regge (0 fasi negative, 8/10 FULL≥1.0), i numeri 1.50/1.71/11% no → citarli con
banda di fase. Ora-del-giorno NON testabile (solo 1d HL). Script `scripts/research/r0702_anchor_xs01.py`;
diario `2026-07-02-anchor-audit-xs01-skh01.md`.
Ricerca `scripts/portfolio/{xsec_research,xsec_blend,xsec_dispgate}.py`. Diari `2026-06-19-hyperliquid-xsec`
/ `-xsec-blend` / `-xsec-dispgate` / `-xsec-universe-expansion` / `-trend-multiasset`.
- **PORTAFOGLIO ATTIVO = TP01 (55%) + XS01 (25%) + VRP01 (20%)** (`src/portfolio/sleeves.active_sleeves`):
TP01+XS01 combinato **FULL Sharpe 1.55, HOLD-OUT 1.55, DD 4.4%**. Aggiunto **VRP01** (options
short-vol, sotto): TP01+VRP01 da solo fa FULL Sh 1.30→1.44 / HOLD 0.31→0.40 a peso 20% (3-way da
validare locale con dati HL). Report `scripts/portfolio/run_portfolio.py`. Sleeve a date d'inizio
diverse → outer-join con pesi rinormalizzati (TP01 da solo 2019-20, VRP dal 2021, blend pieno dal 2024).
- **PORTAFOGLIO ATTIVO = TP01 (33%) + XS01 (15%) + VRP01 (12%) + SKH01 (20%) + GTAA01 (20%)**
(`src/portfolio/sleeves.active_sleeves`): TP01+XS01 combinato **FULL Sharpe 1.55, HOLD-OUT 1.55,
DD 4.4%**. Aggiunto **VRP01** (options short-vol, sotto): TP01+VRP01 da solo fa FULL Sh 1.30→1.44 /
HOLD 0.31→0.40 a peso 20%. **Aggiunto SKH01-V2-DD @25% effettivo (2026-06-23, sotto)**: 4-sleeve
**FULL Sharpe 1.68→2.13, HOLD-OUT 1.63→2.30, DD full 14.3%→7.8%** (Skyhook quasi-ortogonale,
corr ~0.09). **Aggiunto GTAA01 @20% effettivo (2026-07-01, i 4 preesistenti scalati ×0.80):**
trend difensivo equity 6-ETF su IB (`src/portfolio/gtaa.py`, ~30 anni storia, validato 2026-06-22
su OOS equity 2015+ INDIPENDENTE dall'hold-out crypto, corr al book ~+0.10) → 5-sleeve
**FULL Sharpe 2.12→2.24, HOLD-OUT 2.21→2.46, DD full 7.8%→6.2%** (costo dichiarato: CAGR full
23.3→18.8%; 2022 unico anno con dSh). Uplift positivo in-sample E su tutte le finestre disgiunte
(vs EW-STR refutato lo stesso giorno). Convenzioni: weekend/festivi equity = 0.0 (capitale IB
fermo, non riciclato); attivazione nel book all'era crypto 2019-03; **il book live Deribit
(`deribit_book_sleeves` TP01+SKH01 75/25) NON lo include** (GTAA in paper_combo dal 2026-06-23).
Test `tests/test_gtaa_sleeve.py`; diario `2026-07-01-strategy-wave-6threads.md` (addendum GTAA).
Report `scripts/portfolio/run_portfolio.py`. Sleeve a date d'inizio diverse → outer-join con pesi
rinormalizzati (TP01/SKH01/GTAA dal 2019*, VRP dal 2021, XS dal 2024; *GTAA troncato all'era book).
⚠️ **ANCHOR-LUCK del book (2026-07-02):** l'HOLD 2.46 è calcolato con TUTTI gli sleeve ancorati
alla loro ancora canonica, che per TP01/XS01/SKH01 è al top della rispettiva banda (eredità di
fortuna ~+0.10/+0.17/+0.5 HOLD). **Stima de-luckata onesta: HOLD ~1.9-2.1, FULL ~2.0-2.2, DD ~6%
invariato** — il book resta positivo e diversificato a ogni ancora testata, ma 2.46 è un massimo
di configurazioni d'ancora, non la stima centrale. Diario `2026-07-02-anchor-audit-xs01-skh01.md`.
- **SKH01-V2-DD "Skyhook" — DIVERSIFICATORE quasi-ortogonale (research)** — `src/strategies/skyhook.SKH01_V2_DD`,
sleeve `src/portfolio/sleeves._skyhook_returns`. Sistema dual-TF (segnale 690m / exec 230m) regime
(BuzVola/BuzVolume tipo-Chande) AND pattern (Donchian breakout), NON trend-follower, L/S. Vincitrice
di 2 onde multi-agente (la 2ª = DD-reduction): exit a **percentuale fissa ASIMMETRICA** (long sl4%/tp10%,
short sl2%/tp8% più stretto) → standalone **maxDD BTC 21% / ETH 27% (<30%)**, minFull +0.99, minHold
+1.26, causale (0/400), fee-surviving 0.40%RT. Marginal vs TP01 **ADDS** (corr 0.09, has_insample_edge,
robust_oos multicut 7/7, is_hedge=False); blend 0.75·TP01+0.25·SKH **hold-out 0.31→1.17**. Verificato
leak-free + 2 scettici. **CAVEAT:** equity daily-step (Sharpe lens), ETH DD margine sottile, book 230m
(costi ribilanciamento da verificare a deploy) → research win, forward-monitor. Diario `2026-06-23-skyhook.md`.
⚠️ **GRID TIMING-LUCK (2026-07-02, più forte di TP01):** i numeri headline sono sull'offset 0 della
griglia 230m/690m, al **93-98° pctl dei 23 offset a priori** — minHold +1.26, blend 1.17 e book
HOLD 2.44 sono il MASSIMO dei 23 (mediane: minHold +0.39, blend 0.72, book 1.96); spike, non
plateau (±30m crolla); P(spike)≈0.70. **Il gate DD<30% (criterio di selezione di V2-DD) fallisce
in 15/23 offset** (mediana ETH 29.2%). Regge de-luckato: uplift blend positivo a TUTTE le 23 fasi
(min +0.18, med +0.42) + corr 0.05-0.11 → ADDS sopravvive ridimensionato. **LIVE (SKH=25% del book
Deribit):** path reale cron orario + exit software → book 50/50 FULL 1.46→1.19 / HOLD 1.64→1.15 /
DD 18→25%; nei crash gap-through-stop reale (sl2% modellato → 11/23% realizzato). Pesi/book
INVARIATI (ogni cambio passa weights_tilt_null); follow-up: cadenza 230m, peso live da rivedere.
Script `scripts/research/r0702_anchor_skh01.py`; diario `2026-07-02-anchor-audit-xs01-skh01.md`.
- **VRP01 Options Short-Vol — DIVERSIFICATORE da FinanceOld/OptionsAgent** — `src/portfolio/sleeves._vrp_combo_returns`.
Put credit spread settimanale (vendi put -0.28, compra put -0.10) gated su IV-rank. Idee portate da
`../FinanceOld/OptionsAgent` (Bear Call Spread + gate d'ingresso). Migliora il lead VRP nudo
@@ -65,6 +115,22 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis
crash incluso). Scorrelato a TP01 (~+0.01-0.07). **CAVEAT:** premio MODELLATO su DVOL ATM (skew non
esplicito), book a 1d, f di stress reale non catturato → LEAD robusto, non deploy pieno. Ricerca
`scripts/research/options_vrp_v2.py` (vs baseline `options_vrp_lab.py`). Test `tests/test_vrp_sleeve.py`.
⚠️ **ANCHOR-AUDIT CHIUSO + ondata "migliora e proteggi" (2026-07-03, 7 filoni + 2 lenti + scettico):
VRP01 NON è migliorabile e la protezione DD si compra SOLO con la size.** (a) **Anchor-luck (ciclo
settimanale, 7 fasi): PRIMO sleeve SENZA firma di luck** — la fase canonica è la PEGGIORE delle 7 su
FULL (1.09 = 7° pctl) e su DD (11.8% = 93° pctl), mediana su HOLD (0.59); spike bootstrap NEGATIVO →
i numeri di ammissione FULL 1.10/HOLD 0.60/DD 12% sono CONSERVATIVI, non gonfiati. Da ora si citano
con banda: ShFULL [1.09,1.83], ShHOLD [0.03,1.11], DD [5.7,11.8%]; edge OOS f-dipendente (f=0.8 →
HOLD~0). **Con questo l'audit anchor è completo su 4/4 sleeve ancorati.** (b) Griglia 288 strutture:
nessuna batte VRP01 (DSR 0.000; metà griglia = 3ª occorrenza "0-perdite/Sharpe implausibile" dopo
CC01/ALB-A → gate `implausible_sharpe` alzato di priorità). (c) 4 overlay DD (exit-spike/SL-MTM/
ala-coda/cooldown): 4/4 REFUTED dal null de-levering — la protezione crash vive già nel gate
d'ingresso IV-rank. (d) Gate nuovi: 4° fallimento su 4 (l'alpha è il binario IV-rank>0.30). (e)
Sizing: 12% deploy ≈ 0.27 Kelly onesto (anti-rovina); ⚠️ NON confondere col 12% di PESO del book
(~0.014 Kelly, fattore 19x). (f) Gate term-structure VIX/VXV su SPX (ΔSh +0.90, DSR 0.992) =
**confound di modello al 100%** (la var del gate coincide con l'errore BS-flat vs term-structure) →
nuova regola: riprezzare term-structure-consistent prima di credere a un gate vol su strutture
BS-flat. Book/pesi INVARIATI. Diario `2026-07-03-vrp-improve-dd.md`; script `scripts/research/r0703_vrpimp_*.py` (7 file).
Diario `2026-06-20-financeold-analysis-vrp-v2.md`.
- **Universo Hyperliquid: ESPANDERLO NON aiuta XS01** (provato): 52-asset / top-liquidità dinamico /
trend-multi-asset → tutti peggiori (small-cap/memecoin diluiscono il momentum relativo; il trend
@@ -84,6 +150,163 @@ 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).
- **MORTO/confermato artefatto:** mean-reversion / fade (negativo anche a fee zero — la vecchia
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`.
- **Filoni 2026-06-29 (1ª ondata) — tutti scartati/forward.** (A) **DVOL-DIREZIONALE** standalone BTC/ETH
(buy-the-fear / IV-RV come segnale di livello): l'unico edge è un **HEDGE** (is_hedge=True, paga solo
quando TP01 è debole), non alpha → earns_slot=False, forward-monitor come DD-dampener (diario
`2026-06-29-dvol-directional.md`). (B) **INTRADAY ERM** (efficiency-ratio regime momentum sub-daily):
**falso positivo da selezione-sull'hold-out** → SCARTATO (vedi gate SELECTION-ON-HOLDOUT;
`2026-06-29-intraday-regime.md`). (C) **XSEC-V2 NON-MOMENTUM** su HL (reversal/idio-reversal/low-vol/BAB):
solo LOWVOL 19-major regge standalone (FULL/HOLD 1.07) ma deflated-Sharpe 0.13 + storia 2.5a → **DEBOLE/
forward STAT-MODE** (`2026-06-29-xsec-v2-nonmom.md`). (D) **MACRO regime-gate** (equity/credito/oro/tassi →
de-risk crypto): **RIDONDANTE col trend** (corr→TP01 0.989; il gate lavora solo nel 2-3% dei giorni, TP01
già flat nei crash) → SCARTATO (`2026-06-29-macro-regime-gate.md`).
- **Sweep strategie a 5 thread (2026-06-29) — 0 nuovi sleeve, 1 LEAD che rompe 2 muri su 3.** Ricerca
parallela onesta su aree inesplorate (harness `altlib`+`xsec_v2_nonmom`, tutti i gate incl. il nuovo
`study_family_honest`): (1) low-risk cross-sectional, (2) momentum-structure vs XS01, (3) meta-allocazione
dinamica, (4) segnali ortogonali ETH/BTC, (5) 1-gamba a segnale. Esito: soffitto ~1.3 riconfermato; ogni
candidato ucciso dal gate giusto (deflated-Sharpe, is_hedge, selection-on-holdout, sostituzione-XS01,
multi-cut). Niente batte/diversifica XS01 (varianti = REDUNDANT); meta-allocazione < pesi fissi (i 4
sleeve già quasi-risk-parity); 1-gamba a segnale = TP01 travestito (trend) o hedge a DSR<0.95.
**LEAD forward-monitor:** **STATARB-RESID** (relative-MOMENTUM del residuo ETH−β·BTC, β OLS rolling, 2 gambe,
cella vincente **sgn=+1**: le dislocazioni ETH-vs-BTC CONTINUANO a 1d, la MR pura sgn=1 perde) — primo
stream **insieme ortogonale (corr→book 0.027, β-mkt 0.013) ED eseguibile a $600** (haircut ~0, NON
STAT-MODE come XS01/opzioni): marginal ADDS, robust_oos, fee-survive 0.30%/gamba; resta sotto soglia solo
sull'**edge** (Sharpe 0.84, DSR 0.929 same-sign <0.95). **CABLATO in forward-monitor PAPER:**
`scripts/live/paper_statarb.py` (W=45/sgn=+1 congelati, doppio libro MODELED/REAL-$600), nel cron
giornaliero accanto a `paper_prevday`; test `tests/test_paper_statarb.py`. Se la finestra forward conferma
l'edge è *deployabile* (2 gambe BTC/ETH perp). Altri LEAD: IVOL (idio-vol XS, STAT-MODE), DVOLSPREAD
(storia DVOL corta). Diario `2026-06-29-strategy-search-5threads.md`. Script
`scripts/research/{xsec_v3_lowrisk,xsec_v3_momstruct,meta_allocation,orthogonal_signals,signal_inout_1leg}.py`.
- **Ondata 2026-07-01 (6 filoni multi-agente + scettico) — 0 edge nuovi dai filoni, 1 gate nuovo,
e 1 sleeve promosso DALL'ARCHIVIO (GTAA01, sotto nel bullet portafoglio).** Filoni su angoli non coperti dalle ondate precedenti: (1) **funding time-series**
BTC/ETH (posizionamento) = SCARTATO — FOLLOW è trend-beta ritardato, FADE shorta il toro, il gate è
TP01 travestito (DSR 0.215); **il filone funding è chiuso su 3 lati** (FC01 carry, price-clock, TS).
(2) **breadth/internals alt** (51 HL) = SCARTATO ma unico NON-ridondante col trend (corr→TP01 0.40);
muore su jackknife (uplift su 1 mese) + DSR 0.433 con ~8 mesi IS → **rivisitabile tra 1-2 anni di
storia HL nativa**. (3) **residual momentum XS** (β-hedged, 19 major) = REDUNDANT — cross-section la
residualizzazione è un no-op (lo z-score di XS01 rimuove già il mercato); l'edge resta solo nella
coppia ETH/BTC (STATARB-RESID). (4) **ri-ottimizzazione pesi + guardia-DD**: il candidato EW-STR
(TP30/XS25/VRP15/SKH30, HOLD 2.21→2.35) **refutato dallo scettico come selezione-sull'hold-out di 2°
ORDINE** — SKH01/XS01 furono ammessi/affinati *perché* forti su quell'hold-out; pre-2025 ΔSh 0.05,
finestre disgiunte 0.12/+0.06/+0.14, percentile 94-100° fra 500 tilt casuali ≈ firma best-of-15.
Guardia-DD 5%/0.5: inerte OOS (la diversificazione fa già il lavoro; solo circuit-breaker d'emergenza).
(5) **affinamento VRP01** = NON MIGLIORA (l'alpha è tutto nel gate binario IV-rank; gate TP01 =
trappola in-sample; **3° fallimento → filone "VRP dentro il modello" esaurito** fino a f di stress
reale). (6) **stagionalità cross-sectional HL** = morta allo step statistico (null permutato).
**GATE nuovo codificato: `weights_tilt_null`** in `src/portfolio/portfolio.py` (+ `combine_outer`
riusabile): ogni proposta di CAMBIO PESI si giudica vs il null dei tilt casuali cap-respecting —
gate_pass solo se delta_insample≥0 E percentile < firma best-of-k (necessario, non sufficiente);
test `tests/test_weights_tilt_null.py`. ⚠️ Lezione tecnica: `DatetimeIndex.view("int64")` su indici
tz-aware non-ns (pandas 2.x) → scala sbagliata → `merge_asof` broadcasta = **look-ahead che
`causality_ok` non vede**; usare epoca esplicita in ms (altlib verificato pulito). Diario di sintesi
`2026-07-01-strategy-wave-6threads.md` + 6 diari di filone; script `scripts/research/r0701_*.py`.
- **Ondata 2026-07-02 (TIMING + CRT, 8 filoni multi-agente + scettico) — 0 nuovi sleeve, 1 finding
strutturale (anchor timing-luck di TP01, vedi ⚠️ nel bullet TP01).** Goal: "strategie con timing
differenti". (1) **Event-clock bars** (volume/vol/range da 5m, TSMOM/Donchian/EWMA in tempo-informazione):
batte il wall-clock a pari segnale/frequenza solo in 4/45 coppie; cella best IS 1.45 → HOLD 0.46,
NEUTRAL (corr 0.74 = trend travestito) → SCARTATO: il clock non è dove vive l'edge. (2) **Calendario
scadenze Deribit** (expiry weekly/monthly/quarterly ven 08:00 UTC): 0/24 celle a Bonferroni; il drift
post-expiry monthly fallisce placebo-weekday e permutation e si INVERTE sul quarterly (dove l'OI massimo
dovrebbe amplificarlo); unico pattern robusto = gio→ven negativo, ma è day-of-week (SEA morta) a Sharpe
netto ~0 → SCARTATO. (3) **Anchor timing-luck TP01 + tranching**: finding confermato (dettagli nel
bullet TP01); tranching K=2/4 = sola riduzione della varianza della STIMA (ΔSharpe n.s., ΔDD ~0.5pt),
NO deploy a $600 (il min-order lo degenera in K=1; serve feed intraday fuori path certificato) —
rivalutare a ≥5-10k. **Audit d'ancora ESEGUITO su XS01 e SKH01 (stesso giorno): il finding si
replica su 3/3 sleeve ancorati** — vedi ⚠️ nei rispettivi bullet e la stima de-luckata del book
nel bullet portafoglio; diario `2026-07-02-anchor-audit-xs01-skh01.md`.
(4) **Clock lenti (2-7g) + bande isteresi**: fee drag di TP01 = ~0.4%/anno = tetto di ogni risparmio;
il lag costa più del risparmio (HOLD ensemble 0.34→0.11 da N=2 a 7); a $600 **il min-order $5 è GIÀ la
banda ottimale** (ordini 74% a costo ~0) → nessun cambio al book. (5) **Velocità trend
regime-condizionata** (pesi tra orizzonti 30/90/180g vs percentile vol RV/DVOL): pctl 0.71 vs null
pesi-statici-casuali = tilt-30d statico travestito (trappola EW-STR); pesi canonici 1/3 confermati →
SCARTATO. (6-8) **CRT "Candle Range Theory"** (sweep-and-reclaim 3 candele, mai coperto da MRV/MIC):
base 864 trial DSR 0.000 + anchor-flip + short "smart-money" negativo perfino in-sample; multi-TF
(4h→15m, 1h→5m, ~10k trade) expectancy negativa ovunque anche a fee zero, e **il ritest è informazione
negativa** (pattern con-ritest 40bps vs senza +52bps: aspettarlo seleziona i peggiori); contesto
(FVG/equal-highs/sessioni, 22 trial) non salva il fade, cella "Asia" = artefatto anchor-flip →
SCARTATO 3/3. Sottoprodotto: sugli stessi livelli prior-day **FOLLOW > FADE ogni anno 2019-26**
(conferma indipendente del lead prevday in forward-monitor). Lezione: il timing-luck d'ancora è
multiple-testing che il deflated-Sharpe NON conta (candidato gate futuro `anchor_luck_band`).
Diario `2026-07-02-timing-crt-wave.md`; script `scripts/research/r0702_*.py` (9 file).
- **Ondata 2026-07-02-bis ("video claims": Elliott 3 filoni + Albimarini 2 + capital scaling) — 0 nuovi
sleeve, 0 forward-monitor, 1 azione config pendente sul deposito.** Meccanizzazione onesta di claim da
video didattici: (1) **Elliott range-cycle** (onda1 compressa→onda3 ampia): rumore, 0/24 celle a
Bonferroni, nessuna cella weekly regge a tutte le 7 ancore → SCARTATO. (2) **Confluenza Fibonacci**:
vs null ingenui sembra buona (pctl 0.82-1.00), vs null **location-matched** (Fib±jitter: "0.618 vs
0.58?") crolla a 0.39-0.68 = l'apparente edge è la POSIZIONE dei livelli, non i numeri; confluenza
FAIL 4/4 → SCARTATO (il null location-matched è IL test per ogni claim su livelli "speciali").
(3) **Tecnica del canale Elliott**: Donchian travestito — non batte il Donchian a pari geometria
(corr 0.43-0.53), DSR 0.685, cella in-sample collassa in hold-out (1.40→−0.87), target 1.618 = caso
(5/6 celle), anchor-luck di nuovo (4h banda hold [0.35,1.54], 00:00 la migliore) → SCARTATO.
(4) **Albimarini double-diagonal** (short T + long T+1, deep OTM, via motore DVOL di VRP01): il
condor stessa-scadenza la batte a ogni f (la long T+1 = assicurazione di coda +12/33bps nel tail,
~1bps costo medio, non dominanza); senza gate IV-rank TUTTE le strutture perdono (3ª conferma:
l'alpha del VRP è il gate); su Deribit fee-negativa a QUALSIASI size (fee 8 gambe = 194-221% del
theta); celle deep-OTM 0-perdite/142 trade = 2° caso "Sharpe implausibile" dopo CC01 → gate
`implausible_sharpe` raccomandato con più forza. VRP01 resta superiore su tutta la banda skew →
nessun LEAD. (5) **Audit claims** (28 trade, 82% win, PF 5.16, "420%/anno"): consistente con ZERO
skill (P=20-45%; il 78.6% delle finestre 6-mesi 1996-2026 lo produce); replay con code reali =
rovina 1998/2002/2020 col sizing dichiarato; la diagonale lascia passare il 12-40% della perdita
naked. (6) **Capital scaling 600→2-5k** (`r0702_capital_scaling.py`): l'unico vincolo binding è
`max_notional_per_asset_usd=300` (a 5k il book live girerebbe al 49% del target) → **al deposito
alzare il cap a equity/2** ($1000/$1750/$2500 a 2k/3.5k/5k); min_order $5 da LASCIARE; tranching
K=2 non cablare (blocco feed intraday); opzioni ETH eseguibili da ~2.6k ma la regola no-short-vol-
da-modello non decade col capitale; XS01 ~20k confermata, CC01 fuori per struttura. Aspettativa
onesta col CAGR de-luckato (10-15%): 2k ≈ €0.6-0.8/g, 5k ≈ €1.4-2/g (€50/g resta ≈130k).
⚠️ Lezione pandas: `resample("7D", origin=...)` IGNORA origin (pandas 2.x, solo RuntimeWarning) →
bande d'ancora weekly finte; usare `"168h"`. Diario `2026-07-02-elliott-albimarini-capital.md`;
script `scripts/research/r0702_{ell_*,alb_*,capital_scaling}.py` (6 file).
- **Video-claim "CRT top-down multi-TF, 74% win rate" — SCARTATO (2026-07-07): il 74% è un KNOB, non
un edge.** `scripts/research/r0707_crt_topdown.py`. Metodo ICT/SMC top-down (H1 setup CRT → M15
struttura/FVG → M5 conferma → M1 entry) con uscita parziale-70/80%-a-1.5R + break-even + runner verso
prev-daily high/low. Il setup H1 CRT è **già triplo-refutato** (onda 2026-07-02: base DSR 0.000, MTF
expectancy neg. ovunque "il ritest è informazione negativa", contesto/FVG peggiora, fade<follow) →
qui testato l'UNICO angolo nuovo: la **gestione d'uscita** e il **claim 74%**. Su BTC/ETH certificati
(H1→5m/15m; M1 non nel feed): **WR reale 3037%** a RR 1.52 (SOTTO il null gambler's-ruin 40% =
P(+1.5R prima di 1R)=1/(1+1.5) → il ritest tocca il target MENO di una moneta), **expectancy R
negativa a ogni schema/fee/finestra/asset** (1.2…−3.3R netto). **Il 74% si fabbrica avvicinando il
target**: sweep rr1 0.5→2.0 mostra WR salire (51→32%) con expectancy R INVARIANTE (WR alto = target
vicino, non direzione). DSR 0.000 (48 trial); a **fee 0** expR 0.10/Sh 0.63 → **non è morte-per-fee,
l'edge lordo non c'è** (residuo = beta di trend dei time-exit); parziale+BE+runner = 3-4 ordini/trade,
alcuni sub-min-order a $600. **Regola candidata:** il win-rate di uno schema parziale+BE non è merito
(≈1/(1+rr1)); convertire ogni claim "WR X%" in **expectancy R netto fee** prima di crederci (helper
`winrate_is_a_knob()`). Diario `2026-07-07-crt-topdown-74winrate.md`. Book/pesi INVARIATI.
- **Soffitto strutturale BTC/ETH-direzionale ~1.3** superato SOLO espandendo a un meccanismo diverso:
cross-sectional su universo Hyperliquid certificato (XS01) → portafoglio Sharpe ~1.55.
- **Sweep "strategie alternative" (2026-06-20) — 104 ipotesi / 153 agenti / NIENTE di nuovo regge.**
@@ -130,6 +353,17 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis
migliaia di micro-trade sub-dollaro (tipici di un overlay vol-target) è **finzione**. Salta i
sub-min_order e riporta lo **Sharpe haircut** reale vs modellato. **Vale per OGNI sleeve a questo
capitale, TP01 incluso** — lo Sharpe netto onesto a $600 è quello small-cap, non quello modellato.
- **SELECTION-ON-HOLDOUT gate (codificato 2026-06-29, filone B intraday ERM)** — terzo gate in
`altlib.py`, test `tests/test_harness_realism.py`. Il lead ERM faceva `earns_slot=True` MA lo script
di scoperta sceglieva la cella per **`min_hold` massimo** su 60+ celle = **selezione-sull'hold-out**:
scegliendola in-sample-only ne esce un'altra (trend-beta corr→TP01 0.53, NEUTRAL) e il deflated-Sharpe
crolla (DSR 0.0-0.24 su 122 trial). `study_marginal` da solo non lo vede (giudica UNO stream, non *come*
è scelto). Tre funzioni: **`deflated_sharpe()`** (Bailey & Lopez de Prado, PASS ≥0.95), **`select_cell_insample()`**
(cella scelta col solo Sharpe pre-HOLDOUT), e il gate combinato **`study_family_honest(name, factory, grid, tfs)`**
`earns_slot_honest = earns_slot[cella in-sample] AND deflated-Sharpe≥0.95`. **Regola: una strategia
direzionale grid-searched si giudica con `study_family_honest`, non chiamando `study_marginal` sulla
cella a max hold-out.** Chiude il punto cieco gemello di CC01 ("Sharpe implausibile"). Diario
`2026-06-29-intraday-regime.md` (analisi `scripts/research/intraday_regime_analysis.py`).
- **Onestà sul target €50/giorno:** NON raggiungibile su 2000 in 1-2 anni (servono ~130k di
capitale o un DD da rovina). La leva non è la scorciatoia; la via è target-vol + capitale +
tempo. La strategia che *guadagna* esiste, ma a ~+€1.5/giorno su 2000.
@@ -158,7 +392,7 @@ src/strategies/indicators.py → indicatori condivisi (ema, atr, keltner, ...)
src/strategies/trend_portfolio.py → TP01: strategia DIFENSIVA robusta (PORT LF1d, >=12h), causale
src/portfolio/ → PORTAFOGLIO DI STRATEGIE estensibile (Sleeve + StrategyPortfolio)
portfolio.py → combina N sleeve per peso su griglia giornaliera; metriche FULL/hold-out/anno
sleeves.py → REGISTRY sleeve attivi: TP01 (55%) + XS01 (25%) + VRP01 (20%). Aggiungere = una riga
sleeves.py → REGISTRY sleeve attivi: TP01 33 / XS01 15 / VRP01 12 / SKH01 20 / GTAA01 20. Aggiungere = una riga
src/fractal/ → indicatori frattali (patterns.py, indicators.py, similarity.py)
src/backtest/engine.py → engine di backtesting riusabile
src/backtest/harness.py → harness ONESTO (load BTC/ETH, backtest_signals no-leakage, OOS)
+3 -1
View File
@@ -1,7 +1,9 @@
{
"_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). disaster-SL on-book -30% sulla posizione netta. Tutto flat all'arming -> nessun ordine finche' un segnale non arma.",
"_nota_cap": "Cap notional per-asset DINAMICO (frontiera 2026-07-03): con max_notional_per_asset_frac=0.5 il cap = equity/2, cosi' cresce col capitale e un deposito non resta strozzato. A ~$600 equity/2=~$300 -> INERTE (identico al vecchio cap fisso). Il cap dinamico si usa solo con equity reale fidata; su fallback/offline si ripiega su max_notional_per_asset_usd (protezione downside). Per tornare al cap fisso: rimuovere max_notional_per_asset_frac.",
"execution_enabled": true,
"max_notional_per_asset_usd": 300,
"max_notional_per_asset_frac": 0.5,
"min_order_usd": 5,
"disaster_sl_pct": 0.30
}
+17
View File
@@ -13,3 +13,20 @@ services:
# token mainnet (sola lettura) per lo "Shadow live": conto/posizioni reali sulla dashboard.
# Montato a runtime (NON nell'immagine: .env.mainnet e' dockerignored). Solo letture, nessun ordine.
- ./.env.mainnet:/app/.env.mainnet:ro
# IB Gateway (PAPER) per la RICERCA DATI Interactive Brokers — replica il setup provato di BuzWay
# (scout). IBC fa login automatico headless; nessuna GUI desktop. API READ-ONLY (solo dati storici,
# MAI ordini). Bind SOLO su 127.0.0.1 -> non esposto in rete. Credenziali in .env.ibgw (gitignored).
# host 4002 -> container 4004 (socat paper), esattamente come nel connect("127.0.0.1", 4002).
ib-gateway:
image: ghcr.io/gnzsnz/ib-gateway:stable
container_name: pythagoras-ibgw
restart: unless-stopped
env_file: .env.ibgw
environment:
TRADING_MODE: paper
READ_ONLY_API: "yes" # SOLO dati: nessun ordine possibile via API
TWOFA_TIMEOUT_ACTION: restart
TIME_ZONE: Europe/Rome
ports:
- "127.0.0.1:4002:4004" # gateway paper (socat) raggiungibile solo da localhost dell'host
@@ -0,0 +1,62 @@
# 2026-06-22 — Sweep 65-agenti: crypto -> mercati IB (mercati × timing × anni)
## Obiettivo (goal utente)
Usare >=50 agenti per prendere l'anticipazione crypto->equity e trovare la MIGLIORE soluzione,
provando diversi mercati e timing, su piu' anni.
## Setup
- **Dati**: universo IB esteso a **26 ETF certificati** (azioni US/settori/intl/bond/credito/oro/
commodity/REIT), cache su disco (`fetch_ib_equities.py` + BROAD2). Crypto BTC/ETH 1h (Deribit).
- **Harness onesto** (`crypto_lead_harness.py`): per ogni sessione equity, lead = crypto nella
finestra equity-CHIUSO [P 21:00 -> D 13:00 UTC] (overnight; il weekend e' il caso lungo). Predice
gap/intraday/full. Metriche: corr, **t incrementale vs sessione equity precedente**, Sharpe
eseguibile (sign(lead)*predict, net costi) FULL/IS/OOS, **hit per-anno**.
- **Workflow** (`wf_crypto_lead.js`): grid 416 config (2 lead × 26 mercati × 2 giorni × 2 predict ×
2 finestre). **52 agenti sweep** -> **12 agenti verifica avversariale** (stress 10bps + OOS 2024+ +
multi-anno) -> **1 sintesi**. Totale **65 agenti**, 1.7M token.
## Risultato
### Fenomeno fortissimo: crypto overnight -> GAP di apertura equity
Cluster coerente in cima, TUTTI predict=gap/overnight, su ogni target risk-on:
| lead->target | t-incr | Sh OOS@4bps | @10bps | OOS-recente | anni+ |
|---|---|---|---|---|---|
| ETH->IWM gap | 17.1 | 2.49 | 1.96 | 2.41 | 7/8 |
| ETH->QQQ gap | 17.9 | 2.36 | 1.83 | 2.31 | 7/8 |
| ETH->XLK gap | 17.4 | 2.40 | 1.93 | 2.30 | 7/8 |
| **BTC->QQQ gap** | 15.0 | 2.31 | 1.78 | 2.16 | **9/9** |
| BTC->SPY gap | 14.4 | 2.14 | 1.69(lf) | 2.03 | 9/9 |
Statisticamente schiacciante (t 14-18, sopra Bonferroni su 416 test), regge stress costi e OOS
recente, **positivo 8-9 anni su 8-9**.
### Ma DUE killer (i verificatori avversariali concordi)
1. **NON tradabile via ETF**: il gap e' gia' prezzato all'open dell'ETF -> serve un FUTURE indice
tenuto overnight (MNQ/MES/M2K). A $0.5-2k il margin overnight di anche un micro consuma il
capitale e rischia la liquidazione su un gap avverso -> **fuori portata per costruzione**.
2. **E' RISK-BETA, non alpha**: la finestra-lead crypto e' quasi CONTEMPORANEA al gap (stesso shock
macro overnight, equity chiuso). t enorme = co-movimento risk-on/off, non ETH/BTC che *anticipa*.
Firma: la forza e' negli anni alta-vol (2022 hit 0.71-0.75), piatta negli anni calmi (2019/21/23).
corr ~0.37 -> beta implicito ~37%, alpha residuo piccolo.
### L'unico tradabile via ETF e' troppo debole
ETH->XLE intraday 6h (compri XLE al day-open, chiudi +6h): Sh OOS 0.48@4bps **-> 0.15@10bps** (annuo
4.1%->1.0%), t-incr 2.38 **sotto Bonferroni** (~3.5 su 416 test). Edge netto onesto ~ZERO.
## Verdetto (sintesi multi-agente)
**Nessun edge proprietario deployabile a basso capitale.** Il fenomeno crypto->equity-overnight e'
statisticamente reale e robustissimo su 9 anni, ma e' (a) risk-beta condiviso, non anticipazione
sfruttabile, e (b) catturabile solo con futures overnight, fuori dal nostro capitale. L'unica
versione ETF-eseguibile e' dentro il rumore da multiple-testing. Coerente col soffitto del progetto:
"niente di nuovo regge" alla verifica onesta.
**Migliore soluzione (come FENOMENO da forward-monitor, non deploy):** BTC->QQQ gap overnight — la
storia piu' lunga (9/9 anni), lead noto prima dell'open. Da monitorare; deployabile solo con capitale
~>$20-30k su micro-futures indice e con i costi notturni modellati.
## Lezione
Anche con 65 agenti e una ricerca esaustiva su mercati/timing/anni, la disciplina onesta
(tradabilita' al capitale reale + multiple-testing + beta-vs-alpha) riduce un "Sharpe 2.5 su 9 anni"
a un non-edge per noi. Il valore della ricerca: aver QUANTIFICATO e CLASSIFICATO il fenomeno
(risk-beta overnight) invece di scambiarlo per alpha.
Artefatti: `crypto_lead_harness.py`, `wf_crypto_lead.js`.
@@ -0,0 +1,53 @@
# 2026-06-22 — Crypto × mercati IB: correlazioni e ANTICIPAZIONI (lead-lag)
## Obiettivo
Cercare correlazioni e soprattutto ANTICIPAZIONI tra crypto e mercati IB: un mercato fa capire
l'andamento dell'altro? Dati: cache su disco (BTC/ETH Deribit 1h->1d UTC; ETF eq_* con OPEN). Nessun
IB online. Disciplina: attenzione ai tranelli di timing daily (crypto chiude 00:00 UTC, US equity
21:00 -> lag-0 contaminato), test del segno + OOS + multiple-testing.
Script: `crypto_macro_leadlag.py`, `crypto_weekend_signal.py`.
## (1) Correlazione contemporanea
Crypto = asset RISK-ON: corr BTC/ETH ~ **+0.32/0.37** con SPY/QQQ/IWM, **+0.25/0.28** con HYG
(credito), **+0.13** GLD, **~-0.02** TLT (bond). Atteso.
## (2) Lead-lag giornaliero: NIENTE
corr(BTC_{t-k}, ETF_t) ha picco a **k=0** (~0.32) e crolla a rumore (±0.05) per |k|>=1. Al daily
**nessuno anticipa l'altro** (ne' crypto->equity ne' viceversa). Honest negative.
## (3) EFFETTO WEEKEND: anticipazione PULITA, significativa, OOS-robusta
La crypto si muove Sab+Dom (azionario chiuso) -> quel movimento e' info PRIOR al lunedi'.
- **Anticipa il GAP del lunedi'**: corr +0.22/+0.24 (SPY/QQQ/IWM/HYG), hit 59-62%, e **si RAFFORZA
OOS (2022+): +0.30/+0.36**. Coerente su 4 ETF (non cherry-pick).
- Intraday del lunedi' (open->close) piu' debole ma presente (corr 0.10-0.15, OOS 0.18-0.22).
### Validazione avversariale
- **(A) INCREMENTALE vs venerdi'**: regressione `Mon ~ weekend_crypto + friday_eq`. Coeff weekend
crypto significativo ovunque (QQQ gap **t=+4.7**, intr t=+2.9; SPY +4.4/+2.0; IWM +4.7/+2.7);
friday_eq NON significativo. -> e' info CRYPTO-SPECIFICA del weekend, non momentum equity.
- **(B) TRADABILE** (osservo weekend crypto Dom 24:00, entro Monday OPEN, esco CLOSE, net 4bps):
| ETF | hit | Sharpe FULL / IS / OOS22+ | long-flat OOS | ann |
|---|---|---|---|---|
| QQQ | 60% | 1.46 / 1.61 / 1.33 | **1.91** | ~+9-11% |
| SPY | 60% | 0.96 / 0.91 / 1.01 | 1.70 | ~+5% |
| IWM | 56% | 0.89 / 0.73 / 1.04 | 1.07 | ~+6% |
## Verdetto
**Trovata UNA anticipazione reale**: il weekend crypto anticipa il lunedi' azionario (massimo su QQQ,
risk-on/tech). Significativa (t>4 sul gap), incrementale al venerdi', tradabile net costi, **regge e
si rafforza OOS**, coerente su piu' ETF. Meccanismo economico sensato: crypto = proxy 24/7 del
risk-sentiment; nel weekend l'equity e' chiuso e lunedi' "recupera" la direzione crypto.
### Caveat onesti
- **Capacita' bassa**: ~52 lunedi'/anno, intraday -> ~+9%/yr sul capitale impiegato il lunedi', non
una macchina da compounding. E' un segnale TATTICO, non un cornerstone.
- Il GAP (t=4.7) e' piu' forte dell'intraday (t=2.9) ma per catturarlo serve entrare PRIMA del Monday
open -> via **futures indice IB (MNQ/MES, aperti la domenica sera)**: enhancement eseguibile da
validare (cattura gap+sessione).
- Multiple-testing 3 ETF x 2 target: ma TUTTI significativi e coerenti -> effetto ampio, non fortuna.
- Niente IB online qui (cache); per il deploy servirebbe il feed crypto live la domenica sera.
## Prossimo (se si procede)
Validare la variante FUTURES (MNQ domenica sera -> cattura il gap del lunedi') e il sizing a basso
capitale; eventualmente paper-trade. E' la prima ANTICIPAZIONE cross-mercato trovata: crypto come
lead di sentiment sul lunedi' equity.
@@ -0,0 +1,40 @@
# 2026-06-22 — Combo DEPLOYABLE: TP01 (Deribit) + GTAA (IB)
## Perche'
Il combo crypto-pieno (TP01+XS01+VRP01)+GTAA diversificava (Sharpe 1.81), ma XS01/VRP01 sono
STAT-MODE (non eseguibili a $600). Validazione del combo ONESTO/eseguibile: solo le gambe deployable
a basso capitale — TP01 (gia' armato live su Deribit) + GTAA vt12 (eseguibile su IB, frazioni,
switch mensile). `eq_tp01_gtaa_combo.py`. TP01 compoundato sul calendario giorni-di-borsa.
## Risultati (finestra comune 2019-03 .. 2026-06, ~7y)
| | Sharpe | CAGR | volAnn | maxDD |
|---|---|---|---|---|
| TP01 (crypto, Deribit) | 1.25 | 16.4% | 12.9% | 14% |
| GTAA vt12 (equity, IB) | 1.12 | 6.0% | 5.3% | 8% |
| **blend 50/50** | **1.48** | 11.3% | 7.5% | **8%** |
| blend 40/60 (best cap-mix) | 1.52 | 10.2% | 6.6% | 8% |
| risk-parity (29c/71e) | 1.52 | 9.1% | 5.9% | 8% |
**corr TP01<->GTAA = +0.21**. Il blend (1.48-1.52) batte entrambe le gambe (best solo 1.25),
maxDD 14%->8%. **DIVERSIFICA anche da deployable.**
## Caveat onesti
- Per-anno 50/50: 2019 2.11, 2020 2.51, 2021 1.66, **2022 -2.64**, 2023 1.40, 2024 1.73, 2025 0.98,
2026 0.94. Anni boom iniziali gonfiano lo Sharpe assoluto; il **2022 e' negativo** (trend whipsaw
su entrambe le gambe nel bear). Recenti ~0.95. -> il numero robusto e' il GUADAGNO da
diversificazione (+0.27 Sharpe del blend vs solo), non il livello assoluto.
- **Costo deployability**: crypto-pieno+GTAA = 1.81 vs deployable = 1.48. I ~0.33 di Sharpe persi sono
cio' che XS01/VRP01 darebbero se eseguibili (servirebbe ~20k).
- **Cross-venue** Deribit+IB: due conti, capitale split. Entrambe switch mensile/basso turnover,
frazionabili a $0.5-2k.
## Verdetto
Combo deployable VALIDO: due trend difensivi scorrelati (corr 0.21) su mercati diversi -> Sharpe
~1.5 / maxDD ~8%, meglio di ciascuna gamba. E' il candidato concreto per un paper-trade cross-venue.
NON risolve EUR50/g (resta capitale), ma e' la migliore configurazione rischio-aggiustata
EFFETTIVAMENTE eseguibile trovata finora. Lezione cross-mercato confermata: il salto di qualita' non
e' un nuovo alpha ma un SECONDO mercato scorrelato.
## Prossimo (se si procede)
Paper-trade della gamba GTAA su IB (forward-only, come paper_trend per TP01), per validare
l'esecuzione cross-venue a rischio zero prima di qualunque capitale reale.
@@ -0,0 +1,57 @@
# 2026-06-22 — EQ-GTAA01 (trend multi-asset) + COMBO cross-mercato equity×crypto
## (1) EQ-GTAA01 — trend difensivo multi-asset (GTAA)
EQ-TREND01 (trend su SPY) taglia il DD. Diversificare le SORGENTI di trend (azioni US/tech/small +
bond + oro + high-yield) migliora il rischio-aggiustato. `eq_gtaa_trend.py`: ogni asset gestito col
proprio trend long-flat (TSMOM multi-orizzonte), equal-weight tra gli asset disponibili (outer-join,
cash dove off/assente). Universo SPY/QQQ/IWM/TLT/GLD/HYG. Causale, netto fee, OOS 2015+.
| strategia | CAGR | Sharpe (pre15/OOS) | maxDD | corr SPY |
|---|---|---|---|---|
| SPY buy&hold | 9.7% | 0.58 (0.45/0.82) | 55% | — |
| EW statico (no trend) | 9.4% | 0.59 | 62% | 0.89 |
| SPY-trend mono | 5.5% | 0.56 (/0.78) | 30% | 0.72 |
| **GTAA lf vt12%** | 3.8% | **0.64** (0.53/**0.89**) | **15%** | **0.64** |
| GTAA vt12 (6-asset, 2016+) | 5.6% | **1.08** | **8%** | 0.60 |
DD nei bear (GTAA vs SPY): dot-com 32%/49% · GFC **14%/55%** · COVID **10%/34%** · 2022 11%/24%.
Marginale vs SPY: corr 0.64; 50/50 uplift +0.041 FULL / **+0.086 OOS** (meglio del mono-SPY). Plateau
stabile (Sh 0.55-0.61, DD 25-35%). **Migliore sleeve equity**: Sharpe più alto, maxDD bassissimo
(8-15%), corr SPY più bassa (0.64) = diversificatore migliore. Tradeoff: CAGR molto più basso
(fortemente difensiva). Caveat: la finestra 6-asset (Sh 1.08) è tutta OOS ma un solo regime (toro).
## (2) COMBO cross-mercato — equity-trend × crypto
La via che alza il Sharpe COMPLESSIVO senza nuovo alpha: combinare due book scorrelati.
`eq_crypto_combo.py`: crypto = portafoglio attivo TP01+XS01+VRP01 (`StrategyPortfolio.combined_daily`,
rinormalizzato); equity = GTAA lf vt12%. Crypto compoundato sul calendario giorni-di-borsa (cattura
i weekend). Finestra comune = era crypto (2019-03 .. 2026-06, 1827 giorni di borsa).
| | Sharpe | CAGR | volAnn | maxDD |
|---|---|---|---|---|
| crypto TP01+XS01+VRP01 | 1.60 | 18.7% | 11.1% | 14% |
| equity GTAA vt12 | 1.12 | 6.0% | 5.3% | 8% |
| **blend 50/50** | **1.81** | 12.4% | 6.6% | **7%** |
| risk-parity (32c/68e) | 1.78 | 10.1% | 5.5% | 8% |
**Correlazione crypto↔equity = +0.167** (bassissima). Il blend 50/50 fa **Sharpe 1.81 > di ciascuno**
(crypto 1.60, equity 1.12), **maxDD dimezzato 14%→7%**. VERDETTO: DIVERSIFICA (blend > miglior solo
di +0.21 Sharpe). È il guadagno STRUTTURALE: due fonti di rischio scorrelate alzano il Sharpe
complessivo senza cercare un nuovo edge.
### Caveat onesti
- **Finestra crypto corta (~7y) e favorevole**: il crypto Sharpe 1.60 e' alto (regime toro + XS01/VRP01
STAT-MODE a storia corta). Gli SHARPE ASSOLUTI sono ottimistici. Ma il PUNTO della diversificazione
(corr 0.17, blend > solo, DD dimezzato) è robusto al livello assoluto.
- **Cross-venue**: crypto su Deribit, equity su IB → due conti, due percorsi d'esecuzione. A $0.5-2k
totali, ogni sleeve è minuscola. La parte equity (GTAA) e la TP01 sono entrambe eseguibili a basso
capitale; XS01/VRP01 restano STAT-MODE (il blend "reale" deployable è ~TP01 + GTAA).
## Lettura strategica
Il fronte equity da' due cose: (a) una sleeve difensiva robusta (GTAA, maxDD ~10%), (b) — piu'
importante — un DIVERSIFICATORE quasi-scorrelato al crypto che alza il Sharpe del portafoglio
complessivo da ~1.6 a ~1.8. Non risolve €50/g (resta capitale), ma e' il primo miglioramento
STRUTTURALE del rischio-aggiustato complessivo trovato in tutta la ricerca post-reset, ed e' del tipo
giusto (diversificazione vera, non alpha fittizio). Prossimo: validare il combo deployable TP01+GTAA
(solo le due gambe eseguibili), e valutare l'operativita' cross-venue.
@@ -0,0 +1,45 @@
# 2026-06-22 — Fronte EQUITY aperto + EQ-MOM01 (momentum settoriale): NON batte SPY
## Apertura fronte (branch research/equities-ib)
Le 4 ondate crypto hanno esaurito gli angoli su BTC/ETH (soffitto ~1.3). L'unico modo di superarlo è
un **mercato diverso**. Aperto il fronte azioni/ETF via IB (paper, `gnzsnz/ib-gateway`, read-only).
**Dati certificati + cache su disco** (`fetch_ib_equities.py``data/raw/eq_*.parquet`, ADJUSTED_LAST
div+split, gitignored = cache locale; loader `eqlib.py` con lru_cache → ricerca legge da disco, MAI
da IB). Universo: 9 SPDR settoriali classici dal **1998 (27.5y)** + XLRE(2015)/XLC(2018) + SPY(1996,
30y)/QQQ/IWM/GLD/HYG/TLT. Tutti integri (monotoni, no dup, no spike>50%, 0 gap lunghi).
NB bug timestamp risolto: `pd.Timestamp` a risoluzione µs → salvati in secondi, corretti a ms.
## EQ-MOM01 — momentum cross-sectional settoriale
Costruzione causale (`eq_sector_momentum.py`): ogni 21g, momentum = blend lookback [63,126,252]g con
skip-21 (12-1 classico), z-score cross-sectional. long-only top-k (full-invested, confronto
like-for-like con SPY) e long-short (dollar-neutral, test alpha puro). Netto fee, hold-out OOS 2015+.
### Risultati (9 settori, 1998-2026)
| strategia | CAGR | Sharpe (pre15/OOS15+) | maxDD | corr SPY |
|---|---|---|---|---|
| **SPY buy&hold** | 8.2% | **0.51** (0.31/0.82) | 55% | — |
| EW 9 settori | 8.9% | 0.56 (0.44/0.76) | 53% | 0.96 |
| MOM long top-3 | 7.7% | 0.50 (0.32/0.76) | 47% | 0.85 |
| MOM long vol-target 15% | 7.3% | 0.52 | 39% | 0.75 |
| **MOM long-short top-3** | 0.9% | **0.08** (0.19/0.08) | 32% | 0.20 |
### Verdetto: NESSUN edge vs SPY
- **Long-short Sharpe 0.08** → l'alpha cross-sectional di momentum settoriale è **morto** su 27 anni
(decadimento post-2000 noto in letteratura). Niente alpha market-neutral.
- **Long-only ≈ SPY**: corr 0.85, **uplift marginale ~0.00** (blend 75/25 +0.012 FULL / +0.001 OOS;
50/50 +0.015 / 0.010). È un SPY a beta più basso, non un edge. Plateau stabile ma sempre ~0.50
(vs SPY 0.51); sugli 11 settori (2018+) fa peggio (0.69 vs 0.82). Fee-robusto (ma niente da salvare).
- L'unico beneficio (maxDD 55%→39%) è del **vol-target**, non del momentum (lo daresti a SPY stesso).
## Lezione (coerente col progetto)
Il momentum **relative-value** è morto anche in equity, come nel crypto (ortho wave). Il baseline
equity da battere è SPY buy&hold (Sharpe ~0.51 full / 0.82 OOS), ostico come il toro crypto.
## Prossimo angolo plausibile (NON ancora testato)
L'analogo equity di TP01 (l'unica cosa che ha retto nel crypto = trend DIFENSIVO): **time-series
trend su SPY long-flat/long-bonds** — non per battere il CAGR ma per **tagliare il 55% di drawdown**
restando vicino al ritorno. È il punto dove vive il valore robusto in equity (e dove il cross-section
NON guarda). Da provare con lo stesso gauntlet: marginale vs SPY, OOS lungo, plateau.
@@ -0,0 +1,48 @@
# 2026-06-22 — EQ-TREND01: trend DIFENSIVO su SPY = edge difensivo REALE (analogo di TP01)
## Contesto
Il momentum cross-sectional settoriale è morto (EQ-MOM01: long-short Sharpe 0.08, long-only ≈ SPY).
Ma nel crypto l'unica cosa che ha retto NON era relative-value: era **TP01**, un trend DIFENSIVO che
taglia il drawdown. L'equity ha lo stesso buco: SPY buy&hold Sharpe ~0.54 ma maxDD **55%**.
## Costruzione (causale, stile TP01)
`eq_spy_trend.py`. TSMOM multi-orizzonte [21,63,126,252]g, target = frazione di orizzonti in
trend-up (allocazione graduale 0..1), opz. vol-target. Posizione decisa a ≤i-1, tenuta da i. Netto
fee. Varianti: long-flat (cash in risk-off), long-bonds (TLT, solo 2016+), SMA-200 (Faber). Dati da
cache eqlib (ADJUSTED, nessun IB). Periodo 1997-2026, OOS 2015+.
## Risultati
| strategia | CAGR | Sharpe (pre15/OOS) | maxDD | in-mkt |
|---|---|---|---|---|
| SPY buy&hold | 9.0% | 0.54 (0.38/0.82) | 55% | 99% |
| **SMA-200 (Faber)** | 7.0% | **0.65** (0.52/0.88) | **29%** | 76% |
| TSMOM lf cap1.0 | 5.7% | 0.57 (0.44/0.78) | 30% | 92% |
| TSMOM lf vt15% | 5.7% | 0.62 (0.51/0.78) | **25%** | 92% |
**Drawdown nei bear (TSMOM vs SPY):** dot-com 26%/49% · GFC **19%/55%** · COVID 17%/34% · 2022 16%/24%.
**Plateau** (long-flat): ogni config Sharpe 0.56-0.65 (> SPY 0.54), maxDD 25-31% (~metà di SPY).
SMA-200 il più semplice E il migliore (Sh 0.65, OOS 0.88, DD 29%). **Fee-robusto** (Sh 0.48 a
0.10%/lato), basso turnover.
**Marginale vs SPY:** corr 0.73. blend 50/50 uplift +0.035 FULL / +0.031 OOS (modesto positivo);
100% trend uplift 0.012 / 0.041 (nel toro recente la difesa costa).
## Verdetto: edge DIFENSIVO reale (non alpha) — analogo di TP01
- ✅ Sharpe 0.54→0.62/0.65, **maxDD dimezzato** (55%→~27%, nei bear lenti più che dimezzato),
plateau robusto, fee-robusto, **eseguibile a $0.5-2k** (switch mensile SPY/cash).
- ⚠️ NON genera ritorno (CAGR 2/3pp): è risk-management, come TP01.
- ⚠️ I tagli grossi (dot-com/GFC) sono IN-SAMPLE; l'OOS 2015-26 è quasi tutto toro → lì ha seguito
SPY a beta minore (ma COVID, OOS, dimezzato). La difesa "serve" nei bear, rari nell'OOS.
- ⚠️ long-bonds (TLT) non convince (TLT distrutto 2022).
## Lettura strategica
Primo positivo del fronte equity, e dello stesso TIPO che ha retto nel crypto: trend difensivo, non
relative-value. Conferma la lezione cross-mercato: **il valore robusto è nel ridurre il rischio
(trend long-flat), non nel battere il buy&hold**. Da solo non risolve €50/g (problema di capitale).
## Prossimo angolo plausibile
**Trend multi-asset / GTAA** sull'universo ETF in cache (SPY/QQQ/IWM + TLT/GLD/HYG): un portafoglio
di trend long-flat su classi d'attivo diverse di solito batte il trend mono-SPY sul rischio-aggiustato
(diversificazione dei trend). + domanda cross-mercato: la sleeve equity-trend DIVERSIFICA il
portafoglio crypto (TP01+XS01+VRP01)? (esecuzione split Deribit+IB).
+96
View File
@@ -0,0 +1,96 @@
# 2026-06-22 — Funding-CARRY cross-sectional su Hyperliquid (FC01): LEAD fragile, NON regge
## Contesto
Onda "nuova ricerca mirata" (l'utente ha chiesto di cercare un angolo non coperto dalle due grandi
ondate — sweep 104-ipotesi e ortho relative-value, entrambe esaurite sul *prezzo* BTC/ETH). L'unico
meccanismo con una **fonte di ritorno diversa** non ancora testato su dati certi è il **carry da
funding**: incassare il cashflow dei perp stando delta-neutral.
### Scan di fattibilità dati (prima di tutto, lezione v2.0.0)
- **Funding price-clock** (drift attorno agli stamp 00/08/16) sul feed Deribit certificato →
già testato nell'onda intraday (`agent_03_funding_clock_15m`) = **FAIL** ("il funding è un cashflow
perp-vs-spot; il prezzo index non ha drift tradabile attorno allo stamp al netto del trend").
- **Funding carry su Deribit** (dove eseguiamo) → ccxt `fetch_funding_rate_history` = **0 righe**
(bloccato), Cerbero MCP espone solo `get_historical` (candele), endpoint funding = 404.
- **Funding carry su Hyperliquid** → API pubblica `/info {"type":"fundingHistory"}` = **disponibile**,
oraria, tokenless, serie native dal 2023-05. HL è già l'universo certificato di XS01.
### Dato scaricato e certificato
`scripts/research/fetch_hl_funding.py` (backoff anti-429) → **19 major** (gli stessi di XS01),
`data/raw/hlfund_<sym>_1h.parquet`. Certificazione: cadenza ~1h, **0 gap**, copertura 98-100%,
funding annualizzato per asset da **APT +1.0%** a **NEAR +21.6%** (mediana ~+11.7%). Pochi `cap_hit`
(ore con |funding|>0.06%/h) su INJ/TIA/SEI, plausibili in alt ad alta vol. Dato pulito.
## Ipotesi e costruzione (FC01)
Book dollar-neutral che **SHORTA i k perp ad alto funding** e **COMPRA i k a basso** → incassa il
premio (chi è long paga il funding). Ritorno perp per un long = `price_ret funding`. Causale come
XS01: ogni H=10 giorni, segnale = media causale del funding giornaliero realizzato sugli ultimi L
giorni (shift 1), rank cross-section, vol-target 20%, fee 0.05%/lato sul turnover.
`scripts/research/funding_carry_hl.py`. Domanda chiave: **edge reale e ORTOGONALE a XS01**, o XS01
travestito? (gli alt ad alto funding sono spesso i pompati = quelli che XS01 *compra*; qui li
*shortiamo* → potenziale anti-correlazione, oppure il carry domina).
## Risultati
### Premio reale ma direzione-dipendente
`carry` (short alto-funding) batte sistematicamente `anti` (long alto-funding, sempre molto negativo)
**il premio di funding esiste**: shortare i perp ad alto funding paga, in aggregato.
### Ma il book NON regge il gauntlet (19 asset, 2024-2026, 904g)
- **Standalone base (L=7 k=5): FULL Sharpe 0.12, in-sample 0.44, HOLD 0.50, DD 28.6%**, 2.8%/anno.
Decadimento netto: 2024 **+0.44** → 2025 0.06 → 2026 **1.42**.
- Correlazioni: TP01 0.02, **XS01 0.19** (ortogonale, come da ipotesi — NON è XS01 travestito),
VRP01 +0.05.
- **`marginal_vs_tp01` = DILUTES**: `has_insample_edge=False` (in-sample 0.44 < 0.5),
`multicut_persistent=False`, blend w25 uplift FULL 0.21 / HOLD 0.39.
- **Non aggiunge a XS01**: uplift w25 FULL 0.04 / HOLD 0.19.
### Il colpo di grazia: FRAGILITÀ all'universo
Un preview su 17 asset (mancavano NEAR e AAVE) dava FULL **+0.62**, ADDS, +0.22 uplift — un PASS
tentatore. Sui 19 completi: **DILUTES**. Jackknife lascia-fuori-uno (base L=7 k=5):
```
19 asset: FULL -0.12 HOLD -0.50
-SUI FULL -0.39 ... -BTC FULL +0.17
-SEI FULL -0.31 -AAVE FULL +0.26
-BNB FULL -0.29 -NEAR FULL +0.30
=> FULL oscilla in [-0.39, +0.30] togliendo UN solo asset (range 0.70), attraversa lo zero.
```
Togliere **NEAR o AAVE** (i due assenti nel preview) **recupera il segno** → il preview era fortunato
*proprio* perché quei due non c'erano ancora. **Un edge robusto non cambia segno per un singolo nome.**
Le poche celle "buone" del plateau (es. L=7 k=3: HOLD 0.91) hanno **in-sample debole + hold-out forte**
= la firma del hold-out-luck che la metodologia indurita uccide.
## Perché fallisce (meccanismo)
Tensione fondamentale **carry vs momentum**: il funding-carry shorta i forti (alto funding = domanda
long aggressiva), ma in un mercato alt toro i forti **continuano a correre** (NEAR/AAVE: alto funding
*e* prezzo su → shortarli perde più del premio incassato). Il premio di funding è reale in aggregato,
ma il book cross-sectional equal-weight top-k è dominato da pochi nomi a funding estremo che *anche*
trendano, e su 2.5 anni / 19 nomi questo basta a ribaltare il segno.
## Verdetto
**FC01 NON è uno sleeve.** Né deploy (è STAT-MODE: 10 gambe market-neutral, non eseguibile a $600),
né lead affidabile: fragile all'universo (sign-flip su un nome), DILUTES vs TP01, non aggiunge a XS01,
in-sample edge < 0.5, niente persistenza multi-cut, decadimento 2026. Conferma — di nuovo — il
soffitto del progetto: promettente su un sottoinsieme fortunato, collassa sotto il gauntlet onesto.
**Win metodologico:** lo scorer indurito + il jackknife d'universo hanno intercettato un falso
positivo che il preview a 17 asset avrebbe promosso.
## Lascito / lavoro futuro (NON inseguire ora)
- I 19 parquet funding (`hlfund_*`) restano certificati per ricerca futura. Il fetcher NON va in cron
(FC01 fallito → niente da monitorare in forward).
- Idee se mai si tornasse sul carry (NON ora): (a) **gate sul LIVELLO** di funding (short solo quando
estremo, regime-filter alla VRP01 IV-rank) invece dello short-top-k incondizionato; (b) cap sul peso
per-nome / neutralizzazione momentum per togliere il dominio NEAR/AAVE. Entrambe rischiano
overfitting su storia corta — soglia di prova alta.
## Nota IB (thread parallelo, stessa sessione)
Esplorato come fonte per il **basis CME crypto** (cugino eseguibile del carry). Gateway paper
`gnzsnz/ib-gateway` su `127.0.0.1:4002` (read-only, `docker-compose.yml`), sonda `ib_probe.py`.
Esito dati: **backtest del basis NON fattibile** (ContFuture back-adjusted; contratti scaduti = 1
barra). IB resta valido per esecuzione/forward, non per scoprire l'edge. Dettagli nel corpo sessione.
@@ -0,0 +1,33 @@
# 2026-06-23 — Combo DEPLOYABLE in PAPER: TP01 (Deribit) + GTAA (IB), cross-venue
## Decisione
Dopo aver esaurito (onestamente) la ricerca di nuovi edge e anticipazioni cross-mercato, l'unica cosa
VERA e deployabile e' la DIVERSIFICAZIONE: TP01 (crypto, Deribit) + GTAA (equity, IB), corr ~0.21 ->
blend Sharpe ~1.5, maxDD dimezzato (diari 2026-06-22-deployable-combo). Si va in PAPER cross-venue.
## Costruito
- **`src/portfolio/gtaa.py`** — GTAA come sleeve di prima classe: trend difensivo long-flat TSMOM
[21/63/126/252g], vol-target 12%, EW su SPY/QQQ/IWM/TLT/GLD/HYG. Espone `gtaa_returns()` (Sharpe
full 0.64, 7542 barre 1996+) e `gtaa_weights()` (pesi ETF CORRENTI azionabili). Legge cache eq_*.
- **`scripts/live/paper_combo.py`** — paper-tracker FORWARD-ONLY del blend 50/50 TP01+GTAA (crypto
compoundato sul grid giorni-di-borsa). Stato in data/paper_combo/. Mostra le posizioni azionabili
su entrambi i venue. SOLO le gambe eseguibili (XS01/VRP01 STAT-MODE esclusi).
- **`fetch_ib_equities.py --only SPY,QQQ,...`** — refresh mirato dei 6 ETF GTAA (per il cron).
- **`scripts/cron_daily.sh`** — aggiunto: up gateway IB (idempotente) -> refresh ETF GTAA ->
avanza paper_combo. Dipendenza cross-venue gestita (gateway paper sempre-up, restart unless-stopped).
## Stato iniziale (2026-06-23)
Paper combo init a 2000, forward da 2026-06-22. Posizioni azionabili:
- TP01 (Deribit): BTC/ETH 0.0x (flat, TSMOM risk-off — coerente col live).
- GTAA (IB): SPY 13% / QQQ 8% / IWM 9% / TLT 17% / GLD 2% / HYG 17% / cash 34% (difensivo).
Catena end-to-end testata: gateway -> refresh ETF -> avanza paper. OK.
## Onesta'
- E' PAPER (rischio zero). Valida l'OPERATIVITA' cross-venue prima di capitale reale.
- Sharpe atteso ~1.5 e' ottimistico (finestra crypto corta/favorevole); il dato robusto e' la
diversificazione (corr 0.21, DD dimezzato), non il livello assoluto.
- A capitale reale e' un portafoglio su DUE conti (Deribit ~$600 + IB); GTAA frazionabile a basso
capitale, TP01 gia' armato. Prossimo passo eventuale: dashboard del combo + (molto dopo) capitale.
## Prossimo
Lasciar girare il paper forward (cron giornaliero) e ricontrollare l'equity tra qualche settimana.
@@ -0,0 +1,38 @@
# 2026-06-23 — Cross-market crypto-lead OLTRE l'SP500: bond, commodity, indici esteri -> niente
## Obiettivo
Estendere il test "crypto anticipa il mercato?" oltre SP500/azionario USA: commodity, bond, indici
ESTERI (Europa/Asia, fasi orarie diverse = il caso a priori piu' favorevole a un lead vero).
## Dati (IB, orari, cache fut_*_1h)
ES/NQ/RTY (gia'); + ZN (T-note 10y), ESTX50 (Euro Stoxx50), DAX, NKD (Nikkei). Storia ~2-2.4y (2024+).
Commodity GC/CL/HG: VUOTE (market-data subscription COMEX/NYMEX mancante sul paper) -> non testate.
## Test (`fut_leadlag_generic.py`): crypto[T-8h->T] -> future[T->T+6h], non-sovrapposto, controllo=moto proprio future
- ES/NQ/RTY: nessun edge (gia' noto).
- ZN (bond): NEGATIVO (0/3 anni).
- NKD (Nikkei): debole (t_crypto 0.2, Sharpe 0.66 ~ overnight drift, non crypto).
- **ESTX50 / DAX: forte all'apparenza** — BTC->T0h: t_crypto ~7.8, Sharpe 2.5, ann ~22%, 3/3 anni.
## Ma e' un ARTEFATTO DI CONFINE UTC (deep-dive `eu_overnight_deepdive.py`)
- **Picco a coltello a T=00:00 UTC**: t/Sharpe salgono T20->T0 (2.5->7.8 / 0.24->2.45) e CROLLANO a
T=1h (t 1.3, Sharpe -0.09). Un lead vero non e' a coltello su una sola ora.
- **GAP test**: inserendo 1h tra fine-segnale (00:00) e inizio-cattura, l'effetto MUORE
(Sharpe 2.45 -> -0.52, t 7.8 -> 1.6).
- **Singola ora**: T=0h/H=1h (cattura 00:00->01:00) Sharpe +2.93 (t 8.7); T=1h/H=1h (01:00->02:00)
Sharpe -1.02. L'INTERO "edge" e' la barra di confine 00:00->01:00.
- vs SEMPRE-LONG: always-long overnight e' negativo (-0.8/-1.2), quindi non e' overnight-drift; ma
l'uplift del crypto e' tutto nella barra di confine.
-> Firma esatta di `day_boundary_robust` (CLAUDE.md): effetto che vive/muore spostando il confine del
giorno UTC di poche ore = etichettatura/contaminazione, NON anticipazione economica.
## Verdetto
NIENTE di tradabile oltre l'SP500 nemmeno. Su TUTTI i mercati il legame crypto->X e' o co-movimento
contemporaneo (risk-beta) o artefatto di confine. L'anticipazione crypto->altri-mercati sfruttabile
NON esiste su dati onesti (finestre non-sovrapposte + boundary-robust + gap). Conferma definitiva del
soffitto del progetto, ora anche cross-mercato.
## Cosa resta di valore (immutato)
La diversificazione TP01(crypto)+GTAA(equity), corr 0.21 -> Sharpe portafoglio ~1.5, DD dimezzato.
Quello e' strutturale e deployabile; l'anticipazione cross-mercato no.
Script: fetch_ib_futures.py (multi-exchange), fut_leadlag_generic.py, eu_overnight_deepdive.py.
@@ -0,0 +1,29 @@
# 2026-06-23 — "Monitor Deribit / trade IB": il gap crypto->equity e' LOOK-AHEAD
## Idea testata (utente)
Guardare crypto live su Deribit (24/7) e tradare l'indice su IB sul segnale del gap overnight.
## Trappola trovata
Il segnale crypto [P 21:00 -> D 13:00 UTC] e il "gap" equity [P close -> D open 13:30] coprono QUASI
LE STESSE ORE. Condizionare il gap sul crypto-overnight = correlare due ritorni dello STESSO intervallo
notturno -> look-ahead. All'entrata (D 13:00, pre-open) il gap e' GIA' avvenuto: non catturabile.
## Prova (net 2bps, sqrt(252))
| target | OVERLAP gap (look-ahead) | TRADABILE intraday (post-entrata) |
|---|---|---|
| SPY | Sharpe 3.60 (OOS 5.23) | -0.03 (OOS 0.12) |
| QQQ | Sharpe 4.01 (OOS 5.47) | 0.25 (OOS 0.43) |
| IWM | Sharpe 3.98 (OOS 5.72) | 0.15 (OOS 0.44) |
Lo "Sharpe 5" e' artefatto. L'edge REALE tradabile via ETF (intraday, entri all'open) ~0, muore a costi.
NB: anche i Sharpe "gap" del workflow 65-agenti erano (a) look-ahead di overlap e (b) sotto-annualizzati
(sqrt(52) invece di sqrt(252)); il verdetto "non deployabile" resta, rafforzato.
## Cosa resta possibile (non testato, serve dato)
L'unica versione onesta dell'idea: entrare a META' notte via FUTURES IB e vedere se crypto [P21:00->T]
predice il future indice [T->open] su finestre NON sovrapposte (crypto come sensore di rischio piu'
veloce). Richiede dati INTRADAY dei futures (ES/NQ/RTY), non in cache -> data step se si vuole indagare.
## Lezione
Un risultato "troppo bello" (Sharpe 5) e' un test di disciplina: era overlap di finestre. Catturato.
Script: crypto_overnight_equity.py (versione artefatto), crypto_overnight_honest.py (decomposizione).
@@ -0,0 +1,37 @@
# 2026-06-23 — "Monitor Deribit / trade IB" su futures: test ONESTO non-sovrapposto -> edge ~0
## Idea
Monitorare crypto live (Deribit 24/7) ed entrare sul FUTURE indice IB (ES/NQ/RTY, tradato di notte)
a meta' notte, catturando il moto SUCCESSIVO -> finestre NON sovrapposte (no look-ahead, vs il "gap"
che era contemporaneo al segnale).
## Dati
`fetch_ib_futures.py` -> data/raw/fut_{es,nq,rty}_1h.parquet (ContFuture orario, UTC). ES 3y (2023-06+),
NQ 2.75y, RTY 2.3y. (NB: ContFuture NON accetta endDateTime -> chiamata singola "4 Y" = ~3y max orari.)
## Test (`fut_overnight_leadlag.py`)
entrata a T (ora UTC notturna): segnale = crypto[P21:00->T]; controllo = future[P21:00->T] (moto
PROPRIO del future); cattura = future[T->open 13:00]. Incrementale: crypto predice la cattura OLTRE il
moto proprio del future? Trade: sign(crypto[P21:00->T]) * future[T->open], net 2bps. T in {0,3,6,9}h.
## Risultati
| future | miglior Sharpe (trade crypto) | t_crypto incrementale | esito |
|---|---|---|---|
| ES (S&P500) | ~0 / negativo (-0.03..-0.93) | 0..1.5 | NESSUN edge |
| NQ (Nasdaq) | 0.41 (T=3h) | 0.5 (debole) | momentum del future, non crypto |
| RTY (Russell) | 0.40-0.77 | 2.0-2.7 (BTC->RTY) | soffio debole, non robusto |
- **SP500: NIENTE.** Il crypto della prima notte non predice l'ES della seconda. Il "Sharpe 5" del gap
era interamente look-ahead (finestre sovrapposte): catturato e ucciso.
- **RTY (small-cap)** e' l'unico con t_crypto incrementale ~2-2.7 e crypto che AGGIUNGE oltre il moto
proprio del future (futOwn Sharpe negativo). MA: Sharpe 0.4-0.5 modesto, 24 config (multiple-testing),
storia 2.3y, per-anno INCOERENTE (BTC->RTY T=3h: 2024 +0.99 / 2025 +0.52 / 2026 -0.31).
## Verdetto
L'idea "monitor Deribit / trade IB" NON da' un edge tradabile, men che meno su SP500. Il forte
fenomeno crypto<->equity e' CO-MOVIMENTO contemporaneo (risk-beta overnight), non anticipazione: quando
si impone una finestra causale non-sovrapposta, l'edge svanisce (efficienza di mercato). L'unico
residuo (crypto->small-cap overnight) e' debole, borderline su multiple-testing e instabile per anno
-> forward-monitor al piu', NON deploy. Coerente col soffitto del progetto.
Script: fetch_ib_futures.py, fut_overnight_leadlag.py. (look-ahead documentato: 2026-06-23-crypto-overnight-lookahead.md)
+141
View File
@@ -0,0 +1,141 @@
# 2026-06-23 — SKH01 "Skyhook": porting onesto del sistema ES dual-timeframe su BTC/ETH
Branch: `strategy_skyhook`. Engine: `src/strategies/skyhook.py`. Harness: `scripts/research/skyhook/skyhooklib.py`.
Test: `tests/test_skyhook.py` (5 pass). Ricerca: `scripts/research/skyhook/{sweep,grid,check_v1}.py` + `runs/`.
## Il brief
Sistema "Skyhook" (origine ES / E-mini S&P, genetico, a doppio timeframe), da portare su crypto:
- **data2 = 690 min (segnale)**, **data1 = 230 min (esecuzione)**. NB **690 = 3 × 230**.
- NON trend-follower: entra **solo** quando coincidono (a) un **regime** di volatilità/volume e
(b) un **pattern** di breakout.
- Pipeline per barra: indicatori (BuzVola su ATR, BuzVolume su volume, tipo-Chande 0-100) →
fasce regime → pattern (Donchian/breakout su data2) → composer (regime AND pattern) →
ingresso (max 1/giorno, stop-and-reverse) → uscite (time-based asimmetrico uscitalong=24 /
uscitashort=18 + stop/profit).
- Ancore demo: trend lineare → **BuzVola=50** (vol steady → neutro), **BuzVolume=100** (volume in rampa).
## Ricostruzione (fedele + onesta)
- **Resample dal feed 5m certificato** con `origin='epoch'`: 230 min = 46×5m, 690 min = 138×5m,
e i confini 690 sono un **sottoinsieme** dei confini 230 → una barra HTF chiude esattamente su
una chiusura LTF. Merge HTF→LTF causale: `merge_asof` backward sulla **chiusura HTF** (≤ chiusura
LTF), così una barra HTF è usata solo quando è davvero chiusa. (~2287 barre/anno LTF, ~762 HTF.)
- **BuzVola / BuzVolume = `chande01`** (Chande Momentum Oscillator normalizzato 0-100): serie
steady → 50, rampa-su → 100, rampa-giù → 0. Le ancore demo sono soddisfatte a livello di
indicatore (è la lettura fedele: "vol steady → neutro"). NB: l'EMA-ATR su un *linspace* sintetico
dà 100 per drift di warm-up/floating-point, non per comportamento reale — su BTC reale BuzVola
oscilla intorno a 50 (EMA-ATR vs SMA-ATR corr 0.90).
- **Pattern** = Donchian breakout leak-free (shift(1)) su HTF, `ptn_n` barre (default 13 da 13/13/1).
- **Regime** = bande-soglia tunabili su BuzVola/BuzVolume (i magici interi 4/3/2 - 4/2/2 non sono
nel brief; ricostruiti come `[vola_lo,vola_hi]` × `[vol_lo,vol_hi]`).
- **Composer** = regime AND pattern. **Ingressi** ≤1/giorno (prima barra qualificante).
- **Uscite**: time-based asimmetrico (`uscitalong`/`uscitashort` barre LTF) + hard stop/profit. Lo
"stop 2000 / profit 5000" in $ del sistema ES → **multipli di ATR LTF** (scale-free): default
`sl_atr=2.0`, `tp_atr=5.0` (~ rapporto 40:100 pt ES), con modalità `pct` alternativa.
- Engine espresso come **entries `{dir,tp,sl,max_bars}`** per `backtest_signals` (motore onesto del
progetto: TP/SL intrabar, max_bars, non-overlap). Causalità verificata con prefix-recompute
(0 mismatch).
## Baseline → V1 (lever scout + grid, inline, veloce)
- **Baseline** (default 13/13, sl2/tp5, vola[35,95], vol_lo50): causale, fee-surviving, FULL Sharpe
BTC +0.91 / ETH +0.64, ma **HOLD-OUT debole** (BTC 0.09 / ETH +0.17) → FAIL del gate onesto.
- **Lever scout** (`sweep.py`): gli **short servono** (long_only → HOLD 0.52); il **regime gate
conta** (togliere la banda vola → HOLD 0.80); il **floor di volume** a 50 *frenava* l'hold-out
(vol_lo=40 o 0 → PASS); **breakout più lento** (ptn_n=55) e **stop più larghi** (sl2.5/tp6)
alzano l'hold-out.
- **Grid combinato** (`grid.py`): vincitrice **SKH01-V1**
`SkyhookParams(ptn_n=55, sl_atr=2.5, tp_atr=6.0, vola_lo=35, vola_hi=95, vol_lo=0.0)`:
- **min-asset FULL +0.69, HOLD-OUT +0.64** (BTC 0.64 / ETH 0.64), **PASS**, fee-surviving a 0.30%RT.
- BTC FULL +0.69/+275% DD49% ; ETH FULL +1.01/+871% DD31% ; entrambi HOLD-OUT positivi.
- **Marginal vs TP01 = ADDS** e regge i gate induriti: **corr 0.06** (ortogonale, NON trend-beta),
`has_insample_edge=True` (Sharpe in-sample standalone 1.15), `is_hedge=False`, multi-cut
persistente. Blend **0.75·TP01 + 0.25·SKH01: HOLD Sharpe 0.31 → 0.74 (+0.44), DD 11.9%**;
blend 50/50 HOLD 0.88, DD 17.8%.
- Unico sub-gate fallito: `clean_year_uplift` +0.014 (sotto 0.02) → `earns_slot=False` per un pelo,
nonostante tutto il resto sia forte. **Debolezza principale: DD standalone alto (40-49%).**
→ SKH01 è un **diversificatore quasi-ortogonale** reale (non un TP01 travestito): da solo è
volatile, ma come sleeve al 25% migliora moltissimo l'hold-out del portafoglio a DD bassissimo.
## Onda 1 (`skyhook-improve`, 30 agenti) — winner intermedio
Famiglie: param (RR, ptn_n, regime bands, exit bars, chande, local), regime-redef (percentile,
realized-vol, vol-expansion, LTF), pattern (confirmation, ROC, Keltner, NR, dual), exit + overlay,
ognuna verificata da 2 scettici. Risultato: **winner intermedio**
`SkyhookParams(ptn_n=45, sl_atr=2.5, tp_atr=7.0, uscitalong=24, uscitashort=16, vola_lo=35, vola_hi=95, vol_lo=0)`
**minFull +0.83, minHold +0.81** (vs V1 +0.69/+0.64), causale, fee-surviving 0.30%RT, marginal
**ADDS** (corr 0.05, has_insample_edge, robust_oos, multicut, clean_year_uplift +0.37), blend w25
uplift_hold +0.58. **MA standalone maxDD ancora 34% (BTC) / 31% (ETH) → l'unico goal mancato era il DD<30%.**
## Onda 2 (`skyhook-improve-v2`, 14 famiglie DD-reduction) — SKH01-V2-DD vince
Obiettivo: tagliare il **DD standalone <30%** tenendo hold-out + `earns_slot`, e alzare l'uplift di
portafoglio. 14 famiglie (ensemble param/struct, vol-target, DD kill-switch, RR/stop grid, regime
tight, percentile, vol-expansion, breakout confirmation, dual-TF, asimmetria L/S, cadenza, chande,
Keltner), ognuna verificata da 2 scettici avversariali (window-luck/multicut/jackknife +
causalità/fee/plateau/overfit). Esito: **il winner intermedio cade.** Nuovo campione **SKH01-V2-DD**
(famiglia ASYM_LS, `src/strategies/skyhook.py:SKH01_V2_DD`, run `runs/SKH2_ASYM_LS.py`):
- **Config:** stesso SEGNALE del winner (`ptn_n=45, vola_lo=35, vola_hi=95, vol_lo=0, exit-bars 24/16`)
ma EXIT commutati da ATR a **percentuale fissa ASIMMETRICA** — long `sl=4% / tp=10%`, short
`sl=2% (più stretto) / tp=8%`. Motivazione meccanica: in crypto lo short si fa steamrollare da uno
spike vola e lo stop-ATR si allarga lasciando correre la perdita → il %-SL stretto sullo short
**cappa la perdita per-trade** che FORMA il maxDD. (Implementato come override per-direzione nel
motore, backward-compatible: campi `*_short=None` → comportamento simmetrico invariato.)
- **Numeri veri (verificati indipendentemente via `sk.study(SKH01_V2_DD)`):** standalone maxDD
**BTC 21.4% / ETH 27.4%** (<30% ✓, vs 34.4/30.5 del winner) — **goal RAGGIUNTO**; minFull **+0.99**,
minHold **+1.26**; causalità **0/400** entrambi gli asset; fee@0.30%RT BTC +1.05 / ETH +0.80
(positiva anche a 0.40%). Marginal vs TP01 **ADDS** (corr 0.09, has_insample_edge, is_hedge=False,
robust_oos, multicut, clean_year_uplift +0.57). **Blend 0.75·TP01 + 0.25·SKH: uplift_hold +0.87**
(vs +0.58 del winner); **blend 50/50: full 1.84 / hold 1.59 / DD 10.7%**. earns_slot=True,
beats_winner=True. **Plateau reale** (i vicini Spct_mb14/16 sl2% tengono DD 27-28%), non knife-edge.
Entrambi gli scettici: holds_up=True, confidence high, killer_finding=null.
**Top-3 dell'onda 2 (criteri onesti):**
| # | Famiglia | maxDD (BTC/ETH) | minHold | w25 uplift_hold | Verifica |
|---|---|---|---|---|---|
| **1** | **ASYM_LS → SKH01-V2-DD** | 27.4% (21.4/27.4) | +1.26 | **+0.87** | 2/2 high, killer=null ✅ |
| 2 | ENS_STRUCT (3-regime ensemble) | **22.9%** (21.2/22.8) | +1.00 | +0.67 | 2/2 high — ma 3 motori da eseguire |
| 3 | TPSL_DD (%-SL/TP hard) | 28.0% (28/25.5) | +1.11 | +0.75 | 1/1 (rate-limit) — caveat hedge-like |
**Lezioni anti-DD:**
- **Ha funzionato (STRUTTURA dell'exit, non i parametri):** cambiare il MECCANISMO di uscita — %-SL
hard, asimmetria L/S, o ensemble di exit/regime diversi (decorrelazione). Il DD del winner nasce
dalla coda intra-trade negli spike ATR; il %-SL la cappa.
- **NON ha funzionato (la leva non raggiunge il DD vincolante):** DD kill-switch entry-only (sopprime
solo le NUOVE entry, non chiude il trade aperto che forma il maxDD → floor 33-36%); vol-target
causale (DD<30 e uplift≥0.55 mutuamente esclusivi; cap>1 PEGGIORA il DD levereggiando nel pre-crash);
cadenza/FREQ (accorciare gli hold short fa esplodere ETH a 50-66%); dual-TF (LTF è resample dello
stesso prezzo → quasi-tautologico, DD invariato).
- **Bocciato dagli scettici come overfit:** PATTERN_CONF (sub-30 solo a vola_lo=45, knife-edge: sl_atr
±0.5 → ETH 40-47%; la conferma "close_loc" da sola NON taglia il DD). Esempio canonico del perché
serviva la doppia verifica.
- **Non promuovibili:** PCTL_DD (numeri spettacolari ma **0 verifiche**, le 2 sono morte per rate-limit
→ forward-monitor, non fidato); ENS_PARAM / TPSL_DD (battono i gate ma uplift recency/hedge-loaded,
concentrato nei regimi TP01-down → forward-monitor).
**Promozione (questa sessione):** `SKH01_V2_DD` canonico nel motore + override exit-short
asimmetrici (backward-compatible, V1/winner invariati) + 3 test nuovi (8/8 pass).
**Sleeve cablato @0.25 effettivo** (`src/portfolio/sleeves.skyhook_sleeve``active_sleeves`): i tre
sleeve preesistenti scalati nel restante 0.75 mantenendo il rapporto 55:25:20 → **TP01 41.25% / XS01
18.75% / VRP01 15% / SKH01 25%**. Report del portafoglio (4 sleeve, `run_portfolio.py`):
| | FULL Sharpe | FULL DD | HOLD-OUT Sharpe | HOLD-OUT DD |
|---|---|---|---|---|
| 3 sleeve (TP01+XS01+VRP01) | 1.68 | 14.3% | 1.63 | 3.4% |
| **+ SKH01 @25%** | **2.13** | **7.8%** | **2.30** | 3.5% |
| Δ | **+0.45** | **6.5pt** | **+0.67** | ~0 |
→ aggiungere Skyhook **alza lo Sharpe full +0.45 e DIMEZZA il DD full (14.3→7.8%)**, e alza l'hold-out
+0.67 a DD invariato. Portafoglio combinato: FULL Sh 2.13 / ret +365% / DD 7.8%, HOLD Sh 2.30 / DD 3.5%,
positivo ogni anno (2019-26, DD annuo ≤7.8%) vs buy&hold 50/50 FULL Sh 0.93 / DD 76%.
**Caveat onesti / NON deploy:** è un portafoglio di **ricerca** (peso fisso, no costi di ribilanciamento
reale a $600; lo Sharpe daily-step di Skyhook è la convenzione del lens). ETH DD standalone 27.4% ha
margine sottile vs 30%. Prima di un eventuale deploy: ri-verificare la causalità sul **codice di
esecuzione reale** (qui è l'harness di ricerca) e i costi del book a 230m (ribilanciamento più frequente
del resto). XS01/VRP01 restano STAT-MODE/lead. Per ora: research win + sleeve cablato, forward-monitor.
+66
View File
@@ -0,0 +1,66 @@
# 2026-06-23 — Tail-hedge / protezione DD del combo (incl. OPZIONI): vince la guardia-drawdown
## Obiettivo (goal utente)
Trovare uno sleeve/overlay da AGGIUNGERE al combo (TP01+GTAA) per proteggere il drawdown e gli anni
tipo 2022. Valutare anche le opzioni.
## Diagnosi del rischio (decisiva)
Il MaxDD del combo (1x) e' **8.4%** e il 2022 fu **-4.4%**: NON un crash, un **grind lento** (peggior
giorno -2.8%, peggior mese 2022 -1.6%). Il doppio trend (TP01+GTAA long-flat) gia' taglia i crash
veloci. Il tail residuo = (a) whipsaw da mercato choppy (2022), (b) gap/crash overnight LATENTE (TP01
non reagisce intraday, non nel campione storico), (c) la LEVA.
## Candidati testati (`tail_hedge_lab.py`)
| protezione | MaxDD | 2022 | Sharpe | CAGR |
|---|---|---|---|---|
| combo baseline | 8.4% | -4.4% | 1.48 | 11.3% |
| **+ guardia-DD -4%** | **5.8%** | **-1.8%** | 1.38 | 9.2% |
| + vol-target 5% | 8.4% | -5.9% | 1.46 | 8.0% |
| + opzioni (put/put-spread, budget 3%/y) | 8.4% | -4.4% | 1.48 | 11.3% |
### OPZIONI (valutate): NON adatte al 2022
- Put-spread/long-put LONG su indice 50/50 BTC/ETH (mirror di VRP01), premio BS su DVOL reale, payoff
sul path. Strike corretti (compra -0.30delta, vende -0.10delta).
- Sempre-on costa **~50%/anno** di premio -> con budget 3%/anno size ~0.06-0.10x = effetto ~nullo.
- Nel 2022 (grind, niente crash settimanali) **sanguinano** (scadono inutili) -> Δ2022 ~0.
- Pagano SOLO nei crash secchi: stress -30% overnight -> put paga **+25% netto**, put-spread +3.8%.
- Verdetto: assicurazione BLACK-SWAN cara e fuori-bersaglio per il grind. Utile solo come piccola
copertura del gap overnight latente, NON come fix del 2022.
### GUARDIA DRAWDOWN: centra il rischio
De-risk (esposizione 1.0->0.4) quando il DD da picco supera -4%, ri-rischia a -1.6%. Targetizza il
grind: MaxDD 8.4%->5.8%, 2022 -4.4%->-1.8%, ogni anno DD intra <=5.4%. Costo: Sharpe 1.48->1.38,
CAGR -2.1pp (de-risca sui cali, perde rimbalzo — prezzo onesto della protezione).
### A LEVA (dove il tail morde)
guard applicato pre-leva: 2x 2022 -15.6%->-10.9%, MaxDD 28%->24%; 3x resta MARGIN-CALL (DD 39%>=33%).
-> la protezione rende il 2x sopportabile; il 3x va evitato comunque.
## Raccomandazione
AGGIUNGERE una **guardia-drawdown a livello di portafoglio** al combo (overlay, niente premio):
e' la protezione che colpisce il rischio reale (grind/2022) a costo Sharpe minimo. Le opzioni NO come
fix del 2022; eventualmente una micro-allocazione deep-OTM come assicurazione black-swan separata.
Vol-target NON aiuta (il 2022 non e' uno spike di vol).
## Onesta'
- Il guard e' REATTIVO (de-risca dopo l'inizio del DD, restituisce un po' di rimbalzo) -> costa CAGR.
- Trade: -2.1pp CAGR per dimezzare il MaxDD e azzerare quasi il 2022. Sensato se la priorita' e' il DD.
- Parametri (-4% trigger) semplici; il meccanismo (non la soglia esatta) e' la sostanza.
Script: tail_hedge_lab.py.
## Aggiornamento — protezioni CLASSICHE (stop-loss), goal "prova anche SL" (`stops_lab.py`)
Confronto equo (trigger/re-entry sul NAV di mercato, non sull'equity congelata):
| protezione | Sharpe | MaxDD | 2022 | CAGR | in-mkt |
|---|---|---|---|---|---|
| baseline | 1.48 | 8.4% | -4.4% | 11.3% | 100% |
| **soft-guard -4% (0.4x)** | **1.38** | **5.8%** | **-1.8%** | 9.2% | 100% |
| trail-stop -4% (uscita tot.) | 1.07 | 7.5% | +0.0% | 6.6% | 42% |
| trail-stop -6% (re:newhigh) | 1.34 | 6.6% | -2.1% | 9.0% | 72% |
| trail-stop -8% | 1.41 | 8.3% | -4.2% | 10.0% | 87% |
| stop mensile -5% | 1.48 | 8.4% | -4.4% | 11.3% | 100% (mai scatta) |
| vol-stop (>90pctl) | 1.48 | 8.4% | -4.4% | 10.4% | 100% |
VERDETTO: lo SL classico funziona solo a -6% (e resta inferiore al soft-guard); a -4% fa WHIPSAW
(Sh 1.48->1.07, fuori mercato 58%) perche' l'uscita TOTALE viene choppata nel grind. Il soft-guard
alla stessa soglia non whippa (de-risk parziale 0.4x). Stop mensile/vol inutili (bersaglio sbagliato).
Conferma: per un DD da grind, de-risk PARZIALE > stop-loss duro. Soft-guard -4% confermato come scelta.
+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,58 @@
# 2026-06-26 — "Short su fondamentali/notizie NEG ma prezzo SU": screener forward, edge non provato
**Goal utente (chiarito):** se i dati finanziari/notizie di un'azienda sono negativi ma la
quotazione è positiva (sale), andare SHORT — scommessa che il prezzo scenda a riallinearsi ai
fondamentali. Branch `research/equity-meanrev-ib`, script `scripts/research/eq_fundnews_short.py`.
## Gate dati (lezione v2.0.0) — NON backtestabile
I fondamentali e le notizie scaricabili dalla rete (Yahoo) sono **snapshot CORRENTI**, non
point-in-time storici. Applicarli a prezzi passati = **look-ahead** (restatement + survivorship):
esattamente la classe di errore che ha prodotto la libreria fasulla v2.0.0. Un backtest onesto
richiede un DB point-in-time (Compustat PIT, news storiche con timestamp) — **assente**. Quindi,
come per la vol term-structure: **niente backtest**, l'unica via onesta è uno **screener forward**.
## Cosa è scaricabile (feasibility, fatto)
- **Fondamentali strutturati** via Yahoo quoteSummary (flusso cookie+**crumb**, tokenless):
`recommendationMean` (1=buy..5=sell), `revenueGrowth`, `earningsHistory` surprise%,
`recommendationTrend` (skew buy/sell). ✅
- **Notizie**: Yahoo news search (headline + publisher), sentiment lessicale crudo. ✅ (proxy debole)
- **Momentum**: chart API (return 1m/3m). ✅
## Screener costruito + eseguito dal vivo
`fund_neg` ∈ [0,1] (media di: recMean→sell, surprise negative, ricavi in calo, analisti a sell) +
`news_neg` lessicale + momentum. **SHORT candidate = (fond/news neg) AND prezzo in salita** (la
divergenza). Idempotente, logga forward su `data/raw/fundnews_short_screen.parquet`.
**Run di oggi (2026-06-26): NESSUN candidato.** Quasi tutto l'universo ha momentum 1m **negativo**
(mercato in flessione ampia, coerente col bear) → la gamba "prezzo che sale" non scatta. I peggiori
per fondamentali (TSLA 0.48, BA 0.39, NKE 0.38) stanno comunque *scendendo*. Output sensato.
## L'intuizione analitica chiave (il valore vero di questa analisi)
La premessa ha una **tensione interna** che la rende la versione *rischiosa* dell'anomalia:
- **Shortare un prezzo che SALE combatte il momentum** — una delle anomalie più robuste e persistenti
(i winner continuano a vincere su 3-12 mesi). Si scommette contro di essa.
- **Il PEAD** (post-earnings drift) dice che dopo cattive notizie il prezzo continua a **scendere**;
ma qui il prezzo *sale malgrado* le cattive notizie → spesso significa che il mercato sta
**prezzando informazione** che i fondamentali *trailing* non contengono (turnaround, M&A, squeeze).
- Quindi: la versione *pulita* dell'edge "short su fondamentali deboli" funziona meglio quando il
prezzo **conferma** (fondamentali deboli + prezzo che SCENDE = momentum e fondamentali allineati),
**non** quando diverge. La richiesta "shorta il titolo che sale su brutte notizie" è il caso
contrarian, più difficile e storicamente più punito (squeeze, perdita illimitata).
## Eseguibilità (muro)
Short = **borrow** (locate + fee; hard-to-borrow caro/assente), **perdita illimitata**, **squeeze**,
**PDT $25k** per i day-trade, IB instabile qui, $600, universo single-stock (non i nostri ETF). NON
deployabile.
## Verdetto
**Non un edge schierabile, né dimostrabile ora.** Deliverable onesto = lo **screener forward**
(funzionante, da dati di rete) + il log che accumula i candidati per una validazione futura, con
la chiara avvertenza che l'edge è **non provato** e che la *direzione contrarian* della scommessa
(short sulla forza) è la parte fragile. Strumento utile (fond+news+momentum da rete in un colpo),
tesi da maneggiare con scetticismo. Nessun impatto sul book live (branch separato).
@@ -0,0 +1,57 @@
# 2026-06-26 — Scalping azioni IB "quando sottoquotate" + check dati dalla rete
**Goal utente:** su IB, comprare azioni quando "sottoquotate" (oversold/sotto fair-value), con
verifica incrociata dei dati dalla rete. Branch `research/equity-meanrev-ib`,
script `scripts/research/eq_meanrev_ib.py`. Due pezzi.
## 1) CHECK DATI DALLA RETE — il pezzo richiesto (e perfettamente in-metodo)
Confronto i dati certificati IB (`data/raw/eq_*_1d.parquet`, ADJUSTED_LAST) con una **sorgente di
rete indipendente** (Yahoo Finance chart API, tokenless). È la lezione fondante del progetto: non
fidarti di un feed solo (il disastro v2.0.0 era un feed contaminato).
**Risultato (180g, BTC… no: 6 ETF):** dopo l'allineamento corretto **tutti CONCORDE**, deviazione
sui rendimenti **≤1.2bps**, ultimo close identico (0.0bps). Il feed IB è validato.
**La lezione (errore che ho fatto e corretto):** un primo confronto naïf (nostro close *adjusted*
vs `close` *grezzo* di Yahoo) ha **falso-allarmato 4/6 ticker** a 30-52bps (SPY 30, TLT 40, HYG 52).
Le divergenze erano **tutte su singoli giorni = stacco dividendo**: confrontavo dividend-adjusted vs
non-adjusted. Usando l'`adjclose` di Yahoo (apples-to-apples) → ~0bps. **Regola: ogni divergenza va
SPIEGATA prima di gridare "feed sporco" — e prima di tradarci sopra.** Lo strumento resta come
validatore di feed riutilizzabile / template del pre-trade price-check live.
## 2) "SCALPING quando sottoquotata" — non testabile/eseguibile; testato lo swing MR
Lo **scalping intraday non è valutabile**: (a) non abbiamo dati intraday (solo `eq_*_1d`), (b) non
è eseguibile — **PDT rule**: il day-trading di azioni US sotto **$25.000** è limitato a 3
day-trade/5gg → lo scalping è *regolatoriamente bloccato* al capitale del progetto (l'analogo equity
del muro STAT-MODE a $600 sul crypto), (c) IB Gateway è instabile qui (timeout ordini diagnosticato).
Versione onesta e testabile = **swing mean-reversion** "compra quando sottoquotata": Connors RSI2<10
+ filtro trend MA200, exit a MA5. Causale (segnale ≤ close[i], entry a close[i]), netto fee, hold-out 2015+:
| ETF | FULL Sh | DD | CAGR | expo | HOLD Sh | **B&H HOLD Sh** |
|---|---|---|---|---|---|---|
| SPY | +0.75 | 15% | +5% | 13% | +0.70 | **+0.81** |
| QQQ | +0.50 | 16% | +4% | 14% | +0.56 | **+0.92** |
| IWM | +0.32 | 22% | +2% | 13% | +0.15 | +0.53 |
| DIA | +0.38 | 13% | +2% | 14% | +0.55 | +0.74 |
| EEM | +0.41 | 20% | +3% | 13% | +0.36 | +0.46 |
Fee sweep SPY: Sh 0.82 (0bps) → 0.75 (3) → 0.70 (5) → 0.58 (10bps RT).
**Lettura onesta:** la mean-reversion "buy the dip" sugli indici equity ha un edge **reale ma
modesto** (Sharpe ~0.5-0.75, investito solo ~13% dei giorni) **che NON batte il buy&hold
risk-adjusted** (B&H hold-out 0.81 su SPY vs 0.70 della MR), con CAGR piccolo (2-5%) e sensibile ai
costi. È capital-efficient in teoria (cash 87% del tempo) ma il valore aggiunto vs "tieni l'indice"
è negativo o nullo, e la frequenza *scalping* che potrebbe aggiungere valore è bloccata dalla PDT.
## Verdetto
- **Check dati dalla rete: FATTO e utile** — feed IB validato (≤1.2bps vs Yahoo); strumento
riutilizzabile. Unico vero keeper della sessione equity.
- **Scalping "sottoquotate": NON deployabile** — non testabile (no intraday), non eseguibile (PDT
$25k, IB instabile). Lo swing MR daily testabile **non batte il buy&hold** → niente edge schierabile.
Conferma il pattern del progetto: l'idea promettente sopravvive solo finché non incontra
eseguibilità + benchmark onesto. Nessun impatto sul book live crypto (branch separato).
@@ -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).
+118
View File
@@ -0,0 +1,118 @@
# 2026-06-29 — DVOL come segnale DIREZIONALE/REGIME su BTC/ETH: l'unico edge è un HEDGE, non alpha
**Filone A — "DVOL-direzionale".** Tesi: usare la vol IMPLICITA Deribit (DVOL, `data/raw/dvol_*.parquet`,
2021-03→oggi) come **segnale direzionale o di regime** sul perp BTC/ETH (long-flat o L/S sul LIVELLO di
mercato), non come denominatore del vol-target. Script `scripts/research/dvol_directional.py`, test
`tests/test_dvol_directional.py`. Branch `research/equity-meanrev-ib` (separato, nessun impatto live).
## Cosa è diverso dal lavoro DVOL precedente
- `tp01_dvol_overlay.py` (2026-06-26): DVOL come **denominatore** del vol-target → solo de-levering, SCARTATO.
- VOL03/04/10/11 (sweep alt 100-agent): DVOL che **gata/scala un TSMOM** → eredita lo Sharpe di trend di
TP01 (il marginal scorer li boccia NEUTRAL/REDUNDANT).
- `agent_14_dvol_spread` (onda ortho): IV **relativa BTC-vs-ETH**, market-NEUTRAL 2-leg (l'unico LEAD vivo).
Qui invece: DVOL/IV-RV come segnale **direzionale STANDALONE sul livello di mercato**, per vedere se porta
alpha direzionale **ortogonale** a TP01 (non un overlay sul trend). Tutto causale (decisione ≤ `close[i]`,
tenuta in `i+1` dallo shift di `eval_weights`), fee 0.10% RT, vol-target 20%, leva cap 2x, harness onesto
condiviso `altlib`. Allineamento DVOL: `al.dvol` (merge_asof backward) → DVOL close del giorno *t* (≈
contemporaneo a `close[i]`), noto a decisione; verificato leak-free (`causality_ok` max_tail_diff 0.0).
DVOL parte 2021-03 → pre-DVOL il segnale è flat. Valuto sia **FULL** (col flat pre-2021, deflaziona lo
Sharpe) sia **ERA-only** (da 2021-10, equo per la tesi).
## 1) Probe direzionale (corr segnale[i] vs ritorno[i+1], pooled BTC+ETH, era DVOL)
| segnale | corr | long bps | flat bps | **edge bps** | frac_long |
|---|---|---|---|---|---|
| VRP-Z+ (long VRP ricco, IV-RV alto) | +0.010 | +7.8 | 0.9 | **+8.6** | 0.52 |
| VRP-Z (flip) | 0.017 | 0.9 | +7.8 | 8.6 | 0.48 |
| **DVOL-LV fear (rank>0.5 → long)** | **+0.024** | +20.4 | +0.9 | **+19.5** | 0.14 |
| DVOL-LV calm (rank<0.5 → long) | 0.013 | +0.8 | +21.2 | 20.4 | 0.86 |
| DVOL-MOM (DVOL in calo → long) | 0.041 | 10.0 | +22.9 | **32.9** | 0.59 |
| VRP>0 (quasi buy&hold) | +0.003 | +5.3 | 0.7 | +5.9 | 0.72 |
Lettura: l'unico contenuto direzionale reale è **"buy-the-fear"** (long quando il DVOL è alto nel suo
percentile espandente: edge +19.5 bps il giorno dopo). Il VRP-ricco→long è coerente ma debolissimo (+8.6).
Il "DVOL in calo → long" è **negativo** (33 bps): comprare quando la paura scende è perdente (regola
falsificata). Nota: il percentile espandente è dominato dai picchi 2021-22 → "fear" scatta solo nel 14%
delle barre (gli spike veri), non metà del tempo.
## 2) Robustezza assoluta (study_weights 1d, fee sweep 0.00-0.20% RT) — ERA-only il giudizio equo
| segnale | abs | minFull Sh | minHold Sh | feeOK | **ERA Sh** | ERA DD | ERA CAGR |
|---|---|---|---|---|---|---|---|
| **DVOL-fear q0.4 LF** | WEAK | +0.39 | +0.14 | sì | **+0.59** | **8%** | +5% |
| VRP-Z60 LF | FAIL | +0.42 | 0.39 | sì | +0.46 | 23% | +7% |
| DVOL-fear q0.5 LF | FAIL | +0.31 | 0.17 | sì | +0.40 | 10% | +3% |
| VRP-Z90 LF | FAIL | +0.32 | 0.26 | sì | +0.32 | 29% | +4% |
| VRP-Z90 L/S | FAIL | +0.25 | +0.32 | sì | +0.31 | 28% | +3% |
| DVOL-calm q0.5 LF | FAIL | +0.00 | 1.05 | no | +0.09 | 37% | 0% |
| DVOL-MOM k10/k20 LF | FAIL | 0.6/0.7 | <1 | no | 0.6/0.7 | ~40% | ~10% |
| DVOL-MOM k10 L/S | FAIL | 1.16 | 1.29 | no | 1.23 | 72% | 23% |
Nessun segnale supera lo Sharpe assoluto in **valore** (tutti < 0.6, soffitto direzionale BTC/ETH ~1.3
confermato). Unico con profilo decente: **DVOL-fear q0.4 long-flat** (ERA Sh +0.59, DD **8%**) — il DD
bassissimo perché è long solo nei picchi di paura (poche barre, mean-reversion). Le famiglie momentum e
"buy-the-calm" sono nettamente negative e non sopravvivono nemmeno alle fee.
## 3) Marginal vs TP01 (il gate vero) — DVOL-fear q0.4 long-flat
| | corr→TP01 | in-sample Sh | uplift_full w25 | uplift_hold w25 | multi-cut persistente | is_hedge | verdetto |
|---|---|---|---|---|---|---|---|
| **ERA-only** (2021-10+) | 0.37 | 0.54 (has_edge) | +0.06 | +0.17 | sì | **No** | **ADDS** (earns_slot=True) |
| **FULL** (gate ufficiale) | 0.28 | 0.64 (has_edge) | +0.03 | +0.17 | sì | **Sì** | **HEDGE** (earns_slot=**False**) |
Il candidato ha tutto ciò che serve a un nuovo sleeve **tranne una cosa**: il gate canonico full-history
lo classifica **HEDGE**, non alpha. `hedge_yearly_corr(TP01-Sh, uplift) = 0.76`, `uplift TP01-up 0.003`
/ `uplift TP01-down +0.194`**paga SOLO quando TP01 è debole**. Ha perfettamente senso: il DVOL esplode
durante i crash, proprio quando il trend di TP01 è flat/in-drawdown; comprare la paura cattura il rimbalzo
e smorza il DD del portafoglio, ma **non è un premio permanente**. Il blend 0.75·TP01 + 0.25·cand alza
l'hold-out 0.30→**0.47** (w50 → 0.62) e taglia il DD a ~11%/8% — un effetto **drawdown-dampener** reale,
ma classificato (giustamente) come hedge.
Perché ERA dice ADDS e FULL dice HEDGE: il rivelatore di hedge richiede ≥3 anni; sulla finestra ERA
(2021-10+) la `corr(TP01-Sh annuale, uplift)` è meno netta e non scatta. **Il gate full-history è quello
giusto** (più dati per il test di hedge) → mi attengo a quello: **HEDGE, non alpha.**
Gli altri tre candidati (VRP-Z60/90, DVOL-fear q0.5): **NEUTRAL o HEDGE**, mai ADDS. I VRP-Z hanno beta a
TP01 alto (0.88-0.90) → sono trend-correlati travestiti (resid Sharpe ~0), uplift hold-out negativo.
## 4) Causalità, alignment, eseguibilità, sign-falsification
- **Causale**: `causality_ok` ok=True, max_tail_diff **0.0** su tutti i candidati (rank espandente +
z-score ricalcolati su prefisso = identici → nessun future-peeking). Test dedicato `test_expanding_rank_is_causal`.
- **Alignment DVOL**: laggando il segnale di +1 giorno (extra-conservativo, usa solo DVOL di ieri) l'ERA
Sharpe del leader scende +0.59 → **+0.45**. L'edge sopravvive ma è **sensibile all'allineamento**
(parte del vantaggio è nel reagire allo spike DVOL del giorno stesso) → cautela in esecuzione.
- **Eseguibile a $600**: `eval_weights_smallcap` (min_order $5, cap $300/asset): haircut Sharpe **≈ 0.00**
su BTC ed ETH (segnale long-flat a basso turnover, ~210-270 trade su tutta la storia) → lo Sharpe netto
onesto a $600 = quello modellato. È l'unico aspetto pienamente promosso.
- **Sign-falsification**: tesi buy-the-fear ERA Sh +0.40 vs flip buy-the-calm +0.09; VRP-Z+ +0.32 vs
VRP-Z (flip) 0.14. Il segno della tesi è quello giusto (l'edge è reale, non rumore simmetrico).
## Verdetto onesto: **LEAD (hedge / drawdown-dampener), NON sleeve.** SCARTATO come alpha direzionale.
- Nessun segnale DVOL-direzionale ha Sharpe assoluto sopra ~0.6 → **conferma il soffitto BTC/ETH
direzionale ~1.3** (la via per superarlo resta il cross-sectional XS01, non un nuovo segnale a 2 asset).
- L'unico con edge in-sample reale + eseguibile + leak-free è **DVOL-fear "buy-the-fear" long-flat**, ma il
gate canonico lo bolla **HEDGE** (`is_hedge=True`, paga solo quando TP01 è in difficoltà). Per la regola
del progetto un hedge **non si arma come slot di alpha****earns_slot=False**.
- È comunque un **LEAD utile in forward-monitor** come **smorzatore di drawdown** del portafoglio: il blend
porta l'hold-out 0.30→0.47/0.62 e abbassa il DD. Se in futuro si vorrà esplicitamente un tail/DD-hedge
*eseguibile* (a differenza del tail-hedge in opzioni, non eseguibile a $600), questo è il candidato più
pulito visto finora — ma da prezzare **come hedge**, non come ritorno atteso permanente.
## Caveat
1. **Storia DVOL <5 anni** (2021-03→oggi, ~4.7 anni; era valutata 2021-10+) → multiple-testing e
finestra corta. Il multi-cut 2026 è un assurdo +1.944 (manciata di giorni, picco DVOL recente):
l'uplift hold-out è trascinato dal regime 2025-26 ad alta-paura, non da un edge stazionario.
2. **È un hedge, non alpha**: il valore è condizionato ai drawdown di TP01 (2022, ed eventuali futuri).
In un toro lungo a bassa vol il segnale resta **flat l'86% del tempo** e non contribuisce nulla.
3. **Sensibile all'allineamento** DVOL (haircut 0.14 Sharpe col lag +1g): l'esecuzione reale dovrebbe
usare il DVOL già stampato a decisione, non assumere il close contemporaneo.
4. Long-flat → cattura il rimbalzo post-paura ma **non protegge** durante la discesa (non è uno short):
è "buy-the-dip gated by fear", con tutti i rischi del comprare coltelli che cadono se il regime peggiora.
Nessun impatto sul book live (branch separato, nessuno sleeve registrato, config canonica invariata).
+145
View File
@@ -0,0 +1,145 @@
# 2026-06-29 — Filone B: INTRADAY REGIME BTC/ETH — ERM (efficiency-ratio) è un LEAD reale, forward-monitor
**Tesi.** Cercare un meccanismo **sub-daily** sui dati certificati 1h→12h BTC/ETH che sia ortogonale
**sia** a TP01 (TSMOM trend daily, long-flat) **sia** a SKH01 (regime BuzVola/BuzVolume + Donchian a
230m). SKH01 prova che il sub-daily PUÒ funzionare ed essere quasi-ortogonale: qui un MECCANISMO DIVERSO,
basato sulla **qualità del moto intraday** (efficiency-ratio / vol-expansion / thrust) come **regime** che
condiziona una posizione direzionale tenuta ~1 giorno. Script `scripts/research/intraday_regime.py`, test
`tests/test_intraday_regime.py`. Branch `research/equity-meanrev-ib` (separato, nessun impatto live).
Killer ricorrente del progetto sotto le 12h = **muro-fee 0.10% RT + overfitting**. Ricetta SKH01:
decisione sub-daily ma **hold ~1 giorno** → pochi trade → la fee non uccide. Ogni meccanismo qui è
costruito a basso turnover e giudicato col **fee-sweep alla sua frequenza reale**.
## Meccanismi provati (tutti come posizione CONTINUA decisa ≤ `close[i]`, causali)
| | meccanismo |
|---|---|
| **ERM** | **Efficiency-Ratio regime momentum** (Kaufman): ER = \|moto netto su L barre\| / \|percorso\|. Prendi la direzione del moto netto **solo quando ER ≥ soglia** (regime intraday "pulito"/trendy), altrimenti flat |
| VEM | Vol-Expansion Momentum: direzione = segno del moto, attiva solo quando vol-corta > vol-lunga |
| VBR | Volatility/thrust breakout (Larry-Williams ROLLING, no calendario): segui i moti > k·ATR |
| TOD | Time-of-day seasonality — **CONTROLLO calendario**, incluso APPOSTA per `day_boundary_robust` |
## Selezione + fee-sweep a frequenza reale (vincitori per famiglia, min-asset)
```
ERM: 8h L=2.0 thr=0.35 L/S minFull +0.88 minHold +0.99 f@.20 +0.55 feeOK=True turn/y 125
VEM: 12h Lmom=3 Ls=2 Ll=10 minFull +0.72 minHold -0.29 f@.20 +0.51 feeOK=True (hold<0 → out)
VBR: 12h k=1.0 atr=14 L/S minFull +0.33 minHold +1.20 f@.20 +0.25 feeOK=True
TOD: 1h minFull -3.99 minHold -4.49 feeOK=False (controllo → FAIL atteso)
```
Solo **ERM** e **VBR** sopravvivono al gate fee+OOS. VEM ha hold-out negativo. TOD esplode (vedi sotto).
## Il candidato: ERM 8h L=2.0 thr=0.35 (L/S) — `earns_slot=True`
```
abs=PASS marginal=ADDS EARNS_SLOT=True
standalone: cand FULL 0.919 / HOLD 1.105 (TP01 ref FULL 1.297 / HOLD 0.303)
corr->TP01 full 0.146 hold 0.057 beta 0.197 resid Sharpe 0.738 alpha/yr 0.121
robust_oos=True (clean-year +0.103, drop-best-month +0.283)
multi-cut persistence: 2020 .166 / 2021 .167 / 2022 .298 / 2023 .337 / 2024 .418 / 2025 .497 / 2026 2.861 persistent=True
has_insample_edge=True (standalone PRE-holdout Sharpe 0.874, > null-pctl 0.317)
is_hedge=False (yearly corr(TP01-Sh, uplift) -0.762; paga sia TP01-up 0.139 sia TP01-down 0.384)
blend w25: FULL 1.481 (+0.184) HOLD 0.80 (+0.497) DD 11.0%
corr con SKH01: full 0.278 hold 0.291
day_boundary=INVARIANT (spread 0.0) haircut $600 ≈ 0.00 su BTC/ETH
```
È il **primo sub-daily da SKH01** a passare TUTTI i gate del marginal scorer indurito: edge in-sample
reale (non diversification-math), persistenza multi-cut, robust_oos, NON hedge, scorrelato a TP01 (0.15).
### ERM aggiunge OLTRE SKH01? (gate #5 — il book esistente ne ha già uno sub-daily)
| blend | FULL Sh | FULL DD | HOLD Sh | HOLD DD |
|---|---|---|---|---|
| TP01 | +1.30 | 14.3% | +0.30 | 7.5% |
| TP01+SKH 75/25 | +1.78 | 9.4% | +1.17 | 6.2% |
| **TP01+SKH+ERM 60/25/15** | **+1.88** | 8.9% | **+1.46** | 5.3% |
| TP01+SKH+ERM 55/20/25 | +1.84 | 8.8% | +1.48 | 5.1% |
Sì: corr con SKH01 solo 0.28 → ERM **aggiunge oltre SKH** (FULL +0.10, HOLD +0.29 a peso 15%, DD ancora
giù). Non è SKH01 travestito.
## Il controllo TOD (calendario) — fa esattamente ciò che doveva
`TOD` (direzione per ora-del-giorno, media espandente causale) è incluso come **trappola**: è il tipo di
effetto che uccise `open_drive` (artefatto di etichettatura UTC). Esito: **FAIL** (FULL 3.99, 31.811
trade, fee-killed), marginal=DILUTES. `day_boundary_robust=INVARIANT` → l'effetto è **robustamente
negativo** a ogni offset (non un artefatto di confine giorno: è proprio che la time-of-day-direzionale non
ha edge e sanguina fee). Il controllo conferma che l'harness non si fa ingannare e che il segno
dei segnali di prezzo (ERM/VBR) è reale, non rumore di calendario.
## Causalità / eseguibilità
- **Leak-free**: `causality_ok=True`, max_tail_diff **0.0** su tutti i candidati (ER, rank espandente,
medie su prefisso = identiche → nessun future-peeking). Test dedicato in `test_intraday_regime.py`.
- **day_boundary_robust=INVARIANT** (spread 0.0) per ERM/VBR/TOD: segnali di prezzo, non di calendario.
- **Eseguibile a $600**: `eval_weights_smallcap` haircut **≈ 0.00** su BTC ed ETH. **MA** ERM a 8h fa
~3.158 trade BTC / ~2.823 ETH su tutta la storia (turnover 125/anno): haircut nullo nel modello, ma
l'esecuzione reale sub-daily sul book è operativamente più pesante di un segnale 1d (slippage/spread
intraday non interamente catturati dalla fee proporzionale).
## Caveat (perché LEAD, non sleeve)
1. **Plateau hold-out a UNA SOLA RIGA.** Il FULL è robusto su tutta la griglia L∈[2.0,3.0] (+0.6..+1.0),
ma l'**hold-out è positivo SOLO a L=2.0** (a L=2.5/3.0 crolla a 0.5..0.8). Il plateau sul full è
ampio, quello che conta — l'hold-out — è single-row. Da rinforzare prima di credere alla stazionarietà.
2. **Standalone FULL 0.92 < soffitto ~1.3.** Coerente col soffitto direzionale BTC/ETH: il valore di ERM
è **marginale/diversificante** (corr 0.15 a TP01, 0.28 a SKH01), non assoluto. Non rompe il soffitto.
3. **Multiple-testing non deflazionato.** 102 celle testate (60 ERM + 16 VEM + 24 VBR + 2 TOD) senza
deflated-Sharpe (a differenza del filone C). Il multi-cut 2026 = +2.861 è una manciata di giorni che
gonfia. Storia sub-daily certificata utile ~quanto SKH01 → finestra non lunghissima.
4. **Esecuzione 8h** = complessità operativa reale (vedi sopra), oltre il modello a haircut nullo.
## Analisi di robustezza / de-bias (`intraday_regime_analysis.py`) — il lead NON regge
I caveat #1 (plateau hold-out single-row) e #3 (multiple-testing) erano i sospetti giusti. Tre test di
de-bias li trasformano da sospetto in **bocciatura** dello slot:
| test | esito |
|---|---|
| **A) Deflated-Sharpe** (Bailey & Lopez de Prado) su 122 trial cercati | **FAIL.** DSR 0.000 (tutti) / **0.163 (escludendo i trap TOD)** / 0.241 (solo-ERM) — tutti ≪ 0.95. Lo Sharpe winner (0.92) è sotto lo Sharpe-max-atteso-null (1.162.51): il search ha trovato celle a 1.6 full / 1.7 in-sample, il winner 0.92 **non è eccezionale**. |
| **B) Selezione IN-SAMPLE-only** (scelgo la cella ERM col solo Sharpe < 2025) | **earns_slot=False.** La cella migliore pre-hold-out è un'**ALTRA** (8h L=2.0 thr=0.4 **long-flat**), con corr→TP01 **0.53** (è trend-beta travestito) → marginal=**NEUTRAL**. Il winner max-hold **non si seleziona senza guardare l'hold-out** → il suo `earns_slot=True` era **selezione-sull'hold-out**. |
| **C) Ensemble del plateau** (media 20 celle L×thr, niente cherry-pick) | **earns_slot=False.** marginal=ADDS, in-sample Sh 1.01, corr→TP01 0.18 — ma **`robust_oos=False`** (clean-year + jackknife): l'uplift hold-out è trascinato dal **2026 (+2.09 multicut)**, manciata di giorni. |
**Dove vive l'(eventuale) edge** (per-anno, blend 3-way 60/25/15 vs 2-way 75/25): uplift FULL solo **+0.10**,
**negativo nel 2021 (0.23) e 2022 (0.15)**, positivo altrove; l'uplift HOLD **+0.30 è concentrato nel
2026 (+0.46)**. corr(ERM,SKH) 0.28 full (fino a 0.42 in alcuni anni) → **parziale sovrapposizione con SKH**,
non ortogonalità piena.
**Lettura.** Il segnale efficiency-ratio non è rumore puro (l'ensemble ha in-sample Sh ~1.0, positivo nella
maggior parte degli anni), ma come **slot** fallisce ogni de-bias: il `earns_slot=True` della scoperta era
prodotto da **(1) selezione della cella sull'hold-out** + **(2) coda 2026** + **(3) multiple-testing non
corretto**. È lo stesso falso-positivo che l'alt-sweep 100-agent imparò a uccidere — qui ucciso dai gate.
## Caveat originari (ora risolti dall'analisi sopra)
1. ~~Plateau hold-out single-row~~**confermato fatale**: l'edge hold-out a L=2.0 è cell-selection.
2. ~~Multiple-testing non deflazionato~~**deflazionato: DSR FAIL** anche senza i trap.
3. Esecuzione 8h: irrilevante ormai (lo slot non c'è).
## Verdetto onesto: **NON è uno slot. Falso positivo da selezione-hold-out + coda 2026.** SCARTATO come sleeve.
Lo "earns_slot=True" della scoperta **non sopravvive** alla de-selezione: deflated-Sharpe FAIL (anche
escludendo i controlli), selezione in-sample-only → NEUTRAL su un'altra cella (trend-beta corr 0.53),
ensemble del plateau → robust_oos FAIL. **Conferma ennesima del soffitto direzionale BTC/ETH ~1.3**: un
segnale a 2 asset non lo supera; la via resta il cross-sectional (XS01). Resta al più una **curiosità in
forward-monitor** (l'efficiency-ratio ha un debole edge in-sample reale), ma da non armare come alpha.
Gli altri tre meccanismi: VBR=NOISE, VEM hold-out<0, TOD=controllo FAIL come atteso.
**Lezione harness (CODIFICATA).** Lo script di scoperta selezionava il vincitore per `min_hold` massimo
sulla griglia = **selezione-sull'hold-out**, il punto cieco che ha generato il falso PASS. Il marginal
scorer da solo non basta se la *cella* è scelta guardando l'hold-out: serve **(a)** scegliere la cella
in-sample-only (o walk-forward) **prima** di valutare il marginal, e **(b)** deflazionare per il numero di
celle/famiglie testate. Stesso buco segnalato per CC01 ("Sharpe implausibile") e per l'alt-sweep
(hold-out-fitting): qui in forma "selection-on-holdout". Ora è **codice** in `altlib` (LESSON 4): tre
funzioni nuove — `deflated_sharpe()` (Bailey & Lopez de Prado), `select_cell_insample()` (scelta cella
col solo Sharpe pre-HOLDOUT), e il gate combinato **`study_family_honest()`** (`earns_slot_honest =
earns_slot[cella in-sample] AND deflated-Sharpe≥0.95`). Verificato: su ERM il gate ritorna
`earns_slot_honest=False` (cella in-sample = trend-beta NEUTRAL, DSR 0.74). Analisi completa in
`scripts/research/intraday_regime_analysis.py`; test in `tests/test_harness_realism.py`.
**Regola nuova: una strategia direzionale grid-searched si giudica con `study_family_honest`, non
chiamando `study_marginal` sulla cella a max hold-out.**
Nessun impatto sul book live (branch separato, config canonica invariata).
@@ -0,0 +1,71 @@
# 2026-06-29 — Filone D: MACRO regime-gate sul book crypto — RIDONDANTE col trend, SCARTATO
**Tesi.** Usare segnali macro/cross-market — equity (SPY/QQQ/IWM), credito (HYG/LQD), oro (GLD/SLV),
tassi (TLT/IEF) — come **gate risk-on/risk-off** sul book BTC/ETH, per migliorare il *timing* del
drawdown di TP01: regime risk-off (credito che cede, equity sotto trend, fuga sui bond) → riduci/azzera
l'esposizione crypto; risk-on → lascia agire TP01. **È eseguibile** (gata solo BTC/ETH perp, non aggiunge
gambe). Script `scripts/research/macro_regime_gate.py`, test `tests/test_macro_regime_gate.py`. Branch
`research/equity-meanrev-ib` (separato, nessun impatto live). Dati: `data/raw/eq_*_1d.parquet` (ETF daily,
allineati causale `merge_asof backward` sul calendario SPY).
**NON è un lead-lag direzionale** (già morto: diari 2026-06-22/-23). L'angolo nuovo = un overlay
binario/continuo di **de-risk** sul book esistente.
## Il rischio noto in partenza (CLAUDE.md)
La lezione DVOL-spike (2026-06-26): un gate di de-risk rischia di essere **ridondante col trend** — TP01
è già long-flat e va a 0 nei crash (Δ 0.00). Questo script DOVEVA dimostrare che il gate aggiunge **oltre**
quel che TP01 fa da solo. **Non ci riesce.**
## Gate provati (tutti causali: SMA/ratio/z-score rolling, merge_asof backward)
`SPY>MA{50,100,150,200,250}`, `HYG>MA`, ratio di credito `HYG/LQD`, `HYG/IEF`, combo a maggioranza
(SPY+HYG+HYG/LQD, binario e continuo), flight-to-quality (`TLTup&SPYdn`, `GLDup&SPYdn`), regime di vol
equity `SPYvol_z1` (proxy "VIX-spike" senza VIX). g_off ∈ {0.0, 0.5}.
## Esiti
I gate di trend equity alzano lo Sharpe FULL della combo (SPY>MA150/200/250 → FULL ~1.22-1.25 vs TP01-solo
1.30) ma **non l'hold-out** (HOLD ~0.17-0.19) → è **de-levering**, non timing: tagliano DD spegnendo il
book in mercati equity laterali, non anticipando i crash crypto. Il miglior profilo DD è `SPYvol_z1`.
### Deep-dive `SPYvol_z1` — il controllo decisivo (ridondanza col trend)
| | exposure TP01 risk-off | exposure risk-on | giorni risk-off | **giorni in cui il gate LAVORA** | corr(1-gate, 1-expo) |
|---|---|---|---|---|---|
| BTC | 0.023 | 0.093 | 18% | **3%** | 0.278 |
| ETH | 0.013 | 0.068 | 16% | **2%** | 0.296 |
"Il gate lavora" = giorno risk-off **E** posizione TP01 non già flat. Accade nel **2-3% dei giorni**: nel
resto, quando il gate vorrebbe de-riskare, TP01 è **già flat da solo**. Il gate spegne un motore già spento.
### Marginal scorer vs TP01 (gate come candidato-sleeve)
```
abs=FAIL marginal=REDUNDANT EARNS_SLOT=False
corr->TP01 full 0.989 hold 0.984 beta 0.978 resid Sharpe 0.739
multi-cut persistent=True has_insample_edge=True (standalone 1.634) is_hedge=False
blend w25: full 1.323 (+0.026) hold 0.257 (-0.046) blend w50: full 1.348 (+0.051) hold 0.21 (-0.093)
```
corr→TP01 **0.989**, beta 0.98: lo stream gated **È** TP01. L'uplift hold-out è **negativo** (de-levering
che toglie ritorno fuori dai crash). overlay-delta (gatedsolo) come stream a sé: `verdict=NEUTRAL`.
## Causalità / eseguibilità (gli unici aspetti promossi)
- **Leak-free**: `causality_ok=True` (max_tail_diff 0.0); STRICT align (1 barra equity extra di lag) →
FULL invariato; `day_boundary_robust=INVARIANT` (spread 0.0). Allineamento macro→crypto backward-only.
- **Eseguibile a $600**: haircut `eval_weights_smallcap`**0.00** su BTC ed ETH. Fee-survivente
(0.00→0.30% RT: 1.42→1.33). Tecnicamente impeccabile — semplicemente **non aggiunge alpha**.
## Verdetto: **SCARTATO (ridondante col trend).** earns_slot=False.
Conferma esatta della lezione in CLAUDE.md: un overlay di de-risk su un book che è **già** long-flat e
già a 0 nei crash non ha quasi nulla da fare (lavora il 2-3% dei giorni, corr→TP01 0.99). Il taglio di DD
che mostra è **de-levering**, replicabile meglio con un `target_vol` più basso (stessa lezione del
TP01×DVOL overlay del 2026-06-26). Il macro-regime **non anticipa** i drawdown crypto meglio del prezzo
crypto stesso. Nessuno sleeve registrato; nessun impatto sul book live.
**Sottoprodotto utile:** il gate `redundancy_diag` (exposure risk-off vs risk-on + "quota giorni in cui il
gate lavora") è il test pulito da riusare ogni volta che si propone un overlay di de-risk — se "il gate
lavora" è basso e corr→TP01 ~1, è ridondante, a prescindere dallo Sharpe FULL che mostra.
@@ -0,0 +1,106 @@
# 2026-06-29 — Ricerca strategie a 5 thread paralleli: 0 nuovi sleeve, 1 LEAD che rompe 2 muri su 3
**Mandato.** "Cerca altre strategie" (+ "senza correlazioni, a segnale" + "a 1 gamba in/out a segnale").
Cinque ricerche parallele su aree genuinamente inesplorate, tutte sull'harness onesto condiviso
(`altlib` + `xsec_v2_nonmom`) con TUTTI i gate: causalità (prefix-check), netto fee 0.10% RT + sweep,
OOS hold-out 2025+, **deflated-Sharpe** (multiple-testing), **gate anti-selection-on-holdout**
(`study_family_honest`: cella scelta IN-SAMPLE-only), marginal vs TP01/XS01, corr vs book, haircut $600.
Branch `main`, **nessun impatto live** (solo file nuovi, nessuno sleeve registrato).
Premessa onesta (base-rate): il soffitto direzionale BTC/ETH ~1.3 è già confermato e lo sweep di 104
ipotesi (2026-06-20) non produsse nulla di nuovo che reggesse. L'attesa era "quasi tutto SCARTATO".
## Esiti per thread
| # | Thread | File | Verdetto |
|---|--------|------|----------|
| 1 | **XSEC low-risk cousins** (MAX/lottery, idio-vol, Amihud) | `xsec_v3_lowrisk.py` | 1 LEAD (IVOL), 0 sleeve |
| 2 | **XSEC momentum-structure** (risk-adj/accel/frog-in-pan/vol-managed vs XS01) | `xsec_v3_momstruct.py` | tutto REDUNDANT/SCARTATO |
| 3 | **Meta-allocazione** (allocazione dinamica tra i 4 sleeve) | `meta_allocation.py` | pesi fissi vincono |
| 4 | **Segnali ortogonali ETH/BTC** (relative-value dollar-neutral) | `orthogonal_signals.py` | 2 LEAD (STATARB, DVOLSPREAD) |
| 5 | **1-gamba a segnale** (MACD/RSI/Supertrend/Donchian/BBands/EMA) | `signal_inout_1leg.py` | 0/12 earns_slot |
**Netto: 0 nuovi sleeve.** Il soffitto regge. Test 143/143.
### Thread 1 — Low-risk cross-sectional (51 alt HL)
Tre fattori mai provati (il filone C aveva fatto total-vol e BAB, non questi). **IVOL** (idiosyncratic-vol
basso, 19-major B30 H5 k8): FULL 1.06 / HOLD **1.22** / corr ~0 a XS01 e TP01 / positivo ogni anno /
uplift portafoglio HOLD +0.42/+0.50 — l'unico LEAD di valore. **MAX** e **AMIHUD-liquido** sono lo stesso
tema "evita speculativo/illiquido/volatile" in altre vesti (corr fra loro 0.33-0.59). **AMIHUD
long-illiquido SCARTATO** (premio illiquidità invertito in crypto: vincono i major liquidi). Bocciatura
del claim forte: **deflated-Sharpe 0.30-0.37 ≪ 0.95** (96 trial), storia ~2.5 anni, book 10-16 gambe →
**STAT-MODE**. Nota di rigore: il naive best-HOLD atterrava su celle in-sample-negative (holdout-fitting) →
l'agente ha auto-aggiunto il gate has_insample_edge. → **forward-monitor IVOL**.
### Thread 2 — Momentum-structure vs XS01
4 varianti (risk-adjusted, acceleration, frog-in-pan, vol-managed). **Nessuna batte né diversifica XS01**
(standalone 1.42). Sostituire XS01 con una variante **distrugge l'hold-out** del portafoglio (0.58…−1.38)
— prova diretta che l'edge di XS01 sta nella sua struttura SPECIFICA (blend z-score [30,90] + gate
dispersione), non in varianti generiche. Deflated-Sharpe max 0.49. Tutte REDUNDANT/SCARTATO.
### Thread 3 — Meta-allocazione dinamica
Vol-parity / momentum-of-sleeves / dispersion-regime / drawdown-control vs pesi fissi, con costo di
ribilancio realistico. **Nessuno batte i pesi fissi OOS.** Vol-parity = trappola da manuale (+0.09…+0.44
sui tagli in-sample 2022-24, **0.11 sull'hold-out 2025** → intercettato dal multi-cut). Drawdown-control
RIDONDANTE (TP01 va già flat nei crash, il gate non si attiva mai). I 4 sleeve sono già quasi-scorrelati
(corr ~0.12 max) → i pesi fissi sono già vicini al risk-parity ottimo statico. **Mantenere i pesi fissi.**
### Thread 4 — Segnali ortogonali ETH/BTC (il risultato più notevole)
Relative-value dollar-neutral sul ratio log(ETH/BTC): 6 segnali, evaluator a 2 gambe (fee × 2), cella
scelta in-sample-only.
| segnale | FULL/HOLD | corr→book | β-mkt | marginal | DSR (grid/same-sign) | exec $600 | verdetto |
|---|---|---|---|---|---|---|---|
| **STATARB-RESID** (residual relative-mom ETH−β·BTC, W45 **sgn+1**) | 0.84/0.56 | **0.027** | 0.013 | **ADDS** | 0.056 / 0.929 | haircut ~0 | **LEAD** |
| DVOLSPREAD (IV relativa, W60) | 0.74/0.77 | 0.017 | 0.012 | ADDS | 0.082 / 0.907 | haircut ~0 | LEAD |
| RATIO-MOM/REV/ACCEL | 0.25-0.68 / ≤0 | ~0 | ~0 | NEUTRAL | <0.23 | ok | NEUTRAL (diversification-math) |
| VOLSPREAD | 0.24/1.47 | ~0 | 0.013 | DILUTES | — | — | SCARTATO |
**STATARB-RESID rompe 2 dei 3 muri storici:** è **ortogonale per costruzione** (corr→book 0.027, beta di
mercato 0.013 — meglio di SKH ~0.09) **ED eseguibile a $600** (book a 2 gambe BTC+ETH perp Deribit,
haircut ≈ 0, fee-survive fino a 0.30%/gamba → **NON STAT-MODE**, a differenza di XS01 e delle opzioni). Il
muro che **resta** è l'**edge**: Sharpe 0.84 / DSR 0.929 same-sign (ottimistico) ma comunque <0.95.
Verificato indipendentemente dal coordinatore: residuo causale (β OLS rolling backward, decisione a
close[i], return in i+1), fee a 2 gambe corretta. **⚠ Correzione di etichetta:** la cella vincente è
**sgn=+1**, NON sgn=1 → NON è mean-reversion ma **momentum relativo sul residuo** (long lo spread quando
ETH è ricco vs il suo fit su BTC: le dislocazioni ETH-vs-BTC *continuano* a 1d; la MR pura sgn=1 perde
1.4 in-sample). L'ortogonalità (β~0) viene dalla struttura dollar-neutral, non dal verso. → **LEAD
forward-monitor** (monitorabile a costo reale ~0), NON deploy. **CABLATO:** `scripts/live/paper_statarb.py`
(config CONGELATA W=45/sgn=+1, doppio libro MODELED/REAL-$600, riusa il segnale esatto di
`orthogonal_signals.py`), aggiunto al cron giornaliero accanto a `paper_prevday`; test `tests/test_paper_statarb.py`.
**DVOLSPREAD** ri-valida l'ex-lead `dvol_spread`: ADDS ma storia DVOL corta (2021+) → resta forward-monitor
come già noto. Secondario crypto-vs-macro = "TSMOM travestito" (corr→book 0.17-0.20), non ortogonale.
### Thread 5 — 1-gamba a segnale (eseguibile)
12 famiglie (MACD/RSI/Supertrend/Donchian/BBands/EMA, ±ADX) su 1d/12h/8h. **0/12 earns_slot_honest.**
**Eseguibilità validata** (haircut $600 = 0 ovunque; RSI-MR ~2.5 trade/anno) — ma è l'unica cosa che
regge. I trend-follower sono **TP01 travestito** (corr 0.44-0.79, full ~1.2-1.3 = il soffitto) e le celle
sub-daily scelte in-sample **collassano OOS** (dimostrazione da manuale del gate selection-on-holdout). La
mean-reversion è morta (BBands-MR has_insample_edge=False). I "low-corr interessanti" (RSI-MR, MACD-LS,
Donchian-LS) sono **HEDGE non alpha** (`is_hedge=True`, pagano solo quando TP01 è debole), e con
deflated-Sharpe <0.95 (RSI-MR 0.861; Donchian-LS passa DSR ma is_hedge). **A $600 un 1-gamba a segnale è
eseguibile ma non aggiunge nulla a TP01.**
## Sintesi / cosa ho imparato
1. **0 nuovi sleeve, soffitto ~1.3 riconfermato** dal lato direzionale e dal lato struttura-momentum. La
ricerca è onesta: ogni candidato è stato ucciso dal gate giusto (deflated-Sharpe, is_hedge,
selection-on-holdout, sostituzione-XS01, multi-cut), non da un giudizio a occhio.
2. **Il LEAD di valore è STATARB-RESID** (ETH/BTC residual mean-reversion). È il primo stream visto che è
**insieme ortogonale (β~0) ED eseguibile a 2 gambe** — cadono i due muri che bloccano XS01 (STAT-MODE)
e le opzioni. Manca solo l'edge sopra-soglia. È il candidato n.1 per il **forward-monitor**, e l'unico
che — se la finestra forward confermasse l'edge — sarebbe *deployabile* a $600 (non statistico).
3. **Forward-monitor (STAT-MODE / sub-soglia):** IVOL (idio-vol XS, 19-major), DVOLSPREAD (storia corta),
STATARB-RESID (eseguibile — il più promettente). Nessuno armato come sleeve.
4. **I gate nuovi funzionano:** `study_family_honest` (selection-on-holdout) ha intercettato i collassi
OOS dei trend 1-gamba e dei low-risk XS; il deflated-Sharpe ha tenuto sotto la soglia ogni Sharpe ~1
su storia corta. La lezione del filone B (de-bias prima di credere) è ora applicata di default.
## Caveat
- Universi/finestre: HL ~2.5 anni, DVOL dal 2021 → multiple-testing reale; tutti i LEAD sono sotto la
soglia deflazionata. Niente di questo va creduto come alpha finché una finestra forward non lo conferma.
- Nessuno sleeve registrato, config canonica invariata (TP01+XS01+VRP01+SKH01 a pesi fissi). Book live
intatto. Tutto il lavoro è statistico/forward, su `main` come ricerca.
Script: `scripts/research/{xsec_v3_lowrisk,xsec_v3_momstruct,meta_allocation,orthogonal_signals,signal_inout_1leg}.py`.
Test: i rispettivi `tests/test_*.py` (143/143 verdi).
+65
View File
@@ -0,0 +1,65 @@
# 2026-06-29 — Filone C: cross-sectional NON-momentum su Hyperliquid — DEBOLE / forward-monitor
**Tesi.** XS01 (sleeve attivo) è momentum cross-sectional sui 19 major HL. Lezione del progetto
(2026-06-19/20): **espandere il numero di asset NON aiuta il momentum** (gli small-cap diluiscono/
invertono il segnale). Quindi qui NON si ri-prova l'espansione-universo: si cerca un **meccanismo
DIVERSO dal momentum** che, market-neutral e scorrelato, possa diversificare il portafoglio. Script
`scripts/research/xsec_v2_nonmom.py`, test `tests/test_xsec_v2.py`. Branch `research/equity-meanrev-ib`.
## Meccanismi provati (tutti L/S dollar-neutral, vol-target ~20%, ribilancio periodico, CAUSALI)
| | meccanismo |
|---|---|
| REV | short-term reversal grezzo (long loser / short winner di breve) |
| IREV | reversal **idiosincratico** (sul residuo dopo aver tolto il mercato, beta-adjusted) |
| LOWVOL | factor **low-vol** (long bassa vol realizzata / short alta vol) |
| IMOM | momentum **idiosincratico** (sul residuo, ≠ raw mom) — *riferimento* |
| BAB | betting-against-beta (long basso beta / short alto beta) |
| MOM | momentum grezzo — *riferimento* |
Universi: **19 major** e **51 all** (i parquet certificati). Gate canonico di selezione: FULL>0.5,
HOLD>0, |corr→XS01|<0.6 (escluse le famiglie momentum MOM/IMOM, che sono riferimento, non candidati).
## Esiti — un solo candidato non-momentum supera il gate standalone
```
[OK] LOWVOL 19-major B30 H10 k5 FULL +1.07 HOLD +1.07 DD 19% corrXS +0.16 corrTP -0.28
[--] BAB 19-major B60 H10 k8 FULL +0.47 HOLD +1.15 DD 28% corrXS +0.20 corrTP -0.27
[--] REV/IREV (19 e 51) FULL ~0 o NEGATIVO (reversal cross-sectional non paga)
[--] LOWVOL 51-all FULL +0.42 (il long-tail diluisce, come per il momentum)
```
- **REV / IREV negativi** (FULL 0.10..0.70): il reversal cross-sectional grezzo o idiosincratico **non
è un edge** su questo universo/finestra — coerente con la morte storica del mean-reversion del progetto.
- **BAB** ha un buon hold-out ma FULL solo +0.47 (sotto soglia) e DD 28%.
- **LOWVOL 19-major** (betting-against-vol) è l'unico idoneo: FULL **1.07** / HOLD **1.07** / DD 19%,
causale (max_tail_diff 0), corr→XS01 +0.16 (scorrelato), corr→TP01 0.28.
- Espandere a **51 asset peggiora** anche il low-vol (FULL 1.07→0.42): riconferma che i margini
cross-sectional sono nella **struttura del segnale**, non nel numero di asset.
### Uplift portafoglio (4 → 5 sleeve, LOWVOL come 5° sleeve)
```
BASE (TP01+XS01+VRP01+SKH01) FULL 2.12 DD 8% | HOLD 2.25 DD 3%
+LOWVOL @10% FULL 2.17 (+0.05) | HOLD 2.57 (+0.32)
+LOWVOL @15% FULL 2.19 (+0.06) | HOLD 2.62 (+0.37)
```
## Il muro (perché NON è uno sleeve)
1. **Deflated Sharpe 0.130 ≪ 0.95** (N=216 trial globali; soglia Sharpe-max-null annualizz. 1.79). Con il
numero di celle/famiglie/universi testati, lo Sharpe 1.07 **non è significativo** dopo la correzione
per multiple-testing. Per-anno: 2024 +0.23 / 2025 +0.50 / **2026 0.09** (il 2026 è già negativo).
2. **Storia ~2.5 anni** (HL nativo 2024-2026): finestra troppo corta per un fattore cross-sectional.
3. **Non eseguibile a $600**: book L/S a molte gambe (10+ per lato) → **STAT-MODE**, mai deploy. Stesso
vincolo strutturale di XS01 (serve ~20k di capitale).
## Verdetto: **DEBOLE / forward-monitor (STAT-MODE).** Nessuno sleeve registrato.
Il **low-vol cross-sectional** (betting-against-vol sui 19 major) è l'unico meccanismo non-momentum con un
profilo decente e scorrelato sia a XS01 sia a TP01, e migliorerebbe l'hold-out del portafoglio (+0.32/+0.37).
**MA** non supera il deflated-Sharpe (multiple-testing), ha storia cortissima ed è non-eseguibile a questo
capitale. → lavoro **puramente statistico**, da tenere in forward-monitor come *secondo* segnale
cross-sectional a fianco di XS01, da rivalutare solo a scala (capitale ~20k+) e con più storia HL.
Reversal cross-sectional (REV/IREV) **confermato non-edge** su HL. Nessun impatto sul book live.
@@ -0,0 +1,91 @@
# 2026-07-01 — Breadth / market-internals del mercato ALT come segnale su BTC/ETH — SCARTATO
**Tesi.** Gli "internals" del mercato crypto — la partecipazione degli alt — come segnale
direzionale o gate di de-risk su BTC/ETH perp (2 gambe, eseguibile a ~$600): % di alt sopra la
propria SMA(N), advance/decline, % di alt che battono BTC (risk-appetite relativo), breadth-thrust.
Script `scripts/research/r0701_breadth_internals.py` (non committato per policy del filone; harness
`altlib`). Dati: i 51 parquet certificati `data/raw/hl_*_1d.parquet` → panel di **49 alt**
(esclusi hl_btc/hl_eth dalla breadth; hl_btc solo come riferimento per la famiglia RS), barre a
volume 0 mascherate (lezione backfill 2026-06-20; il fetch le aveva già ripulite: vol0=0 su tutti
i file, gate comunque attivo).
**Limite strutturale dichiarato in partenza:** l'universo HL parte dal 2024-01 → dopo il warmup
(max 120g) la finestra comune è **2024-05-05 → 2026-07-01 (~2.2 anni)** e l'in-sample pre-HOLDOUT
è **~8 mesi del toro 2024**. Qualunque esito poteva essere al massimo un lead.
## Famiglie e griglia (108 celle, tutte contate nel deflated Sharpe)
4 famiglie × N∈{20,50,100} × soglia∈{0.3,0.5,0.7} × forma∈{LS, long-flat, gate-su-TP01}:
- **FAM-MA**: % di alt validi con close > SMA(N) (breadth classica).
- **FAM-AD**: frazione di advancers (ret 1g>0), SMA(N).
- **FAM-RS**: % di alt che battono BTC sul ritorno a N giorni (≈ market-neutral, l'angolo più ortogonale).
- **FAM-TH**: breadth-thrust = Δ a N giorni della breadth MA20 (thrust/collapse), score 0.5+Δ.
Tutte causali (rolling/shift soltanto), MIN_VALID=20 asset per data (osservato: 42-49). LS/LF
vol-target 20% cap 2x (sizing TP01-style); GATE = posizione TP01 × gate binario. Fee 0.10% RT +
sweep 0-0.30%. Procedura onesta = mirror di `study_family_honest` sulla finestra comune (il
padding pre-2024 avrebbe contaminato il ranking full-history: le celle GATE con gate=1 pre-storia
erediterebbero lo Sharpe 2019-2024 di TP01): **cella scelta col SOLO Sharpe in-sample**, DSR su
tutti i trial, `marginal_vs_tp01` di altlib sulla cella scelta.
## ⚠️ Bug trovato e corretto: allineamento merge_asof su indice non-ns
La prima versione di `_align` usava `b.index.view("int64")//10**6` per i timestamp della breadth:
su un DatetimeIndex tz-aware **a risoluzione non-ns** (pandas 2.x) la scala esce sbagliata →
`merge_asof backward` matchava OGNI barra BTC/ETH all'**ultimo** valore della breadth = **il futuro
broadcast su tutta la storia**. Sintomo: decine di celle con lo stesso identico Sharpe (il segnale
era una costante). **`causality_ok` NON lo vede** — la serie breadth è un input esterno *fisso*,
quindi il target sul prefisso troncato coincide col target full per costruzione. Fix: epoca
esplicita `(idx - Timestamp(1970, tz=UTC)) // Timedelta(1ms)` (come `resample_tf`). **Lezione
harness:** ogni allineamento di un panel esterno via merge_asof va fatto con epoch-ms esplicita, e
il sintomo "molte celle con Sharpe identico" è un red-flag di allineamento rotto (gemella
dell'osservazione merge_asof del 2026-06-20).
## Esito (procedura onesta, finestra comune 2024-05+)
104/108 celle valutabili (4 degeneri flat), 47 con FULL>0. **Cella scelta in-sample** (IS 2.11,
la selezione non tocca mai l'hold-out): **TH N=100 thr=0.7 long-flat** — long solo dopo un
breadth-thrust (+0.2 di breadth MA20 in 100g), attiva il 32% dei giorni.
| metrica | valore |
|---|---|
| standalone finestra comune | IS **+2.11** → FULL **+0.99**, HOLD **+0.41** |
| assoluto (per-asset, trimmed) | **PASS** — minFull +0.68, minHold +0.38, fee-survive 0.30%RT, DD 7-11% |
| marginale vs TP01 | **ADDS** — corr full **0.401**/hold 0.358, resid Sharpe 0.87, multicut {2025:+0.076, 2026:+0.366} persistente, is_hedge=False |
| **robust_oos** | **False** — jackknife drop-best-month **0.068** (l'uplift hold-out sta in piedi su UN mese) |
| **deflated Sharpe** | **0.433** su 104 trial (null-max atteso 1.10) — lontanissimo dal PASS ≥0.95 |
| earns_slot / earns_slot_honest | **False / False** |
| causality_ok | True (post-fix) |
| smallcap $600 | haircut ≈ **0.00** (BTC 0.01, ETH +0.00; 137-152 trade) — eseguibile |
**Ridondanza col trend (il rischio n.1, lezione macro-gate):** qui NON è la modalità di morte.
corr(pos, TP01pos) 0.15-0.30 e il segnale "lavora" (off mentre TP01 non è flat) nel **24-32% dei
giorni** (il macro-gate lavorava nel 2-3%). Le forme direzionali LS/LF sono genuinamente diverse
da TP01 (corr→TP01 0.1-0.4). La forma **gate-su-TP01** invece sì: best gate = NEUTRAL a corr
0.803, e i gate a soglia bassa = REDUNDANT corr 1.0 (gate mai off → TP01 identico). **La breadth
come gate di de-risk non aggiunge nulla; come segnale direzionale è diversa dal trend ma l'edge
non è dimostrabile.**
**Il gemello in hindsight (trasparenza, NON selezionabile).** TH N=100 thr=0.7 **LS** fa FULL 1.13
/ HOLD 1.06, corr→TP01 **0.26**, marginal ADDS con robust_oos=True e jackknife +0.46. MA il suo
in-sample (1.30) è sotto quello della cella lf (2.11): sceglierlo = **selezione-sull'hold-out**
(esattamente il falso positivo ERM del 2026-06-29). E comunque **DSR 0.518 < 0.95**: anche la
lettura più caritatevole muore sulla deflazione multiple-testing.
## Verdetto: SCARTATO (niente sleeve, niente forward-monitor cablato)
1. La cella scelta onestamente è ADDS ma **robust_oos=False** (uplift a un mese) e **DSR 0.433**
su 104 trial: su ~8 mesi di in-sample (toro 2024) un IS di 2.1 è indistinguibile dal massimo
atteso per caso (1.10) e non regge.
2. Il candidato migliore col senno di poi (TH-LS) sarebbe selezione-sull'hold-out e fallisce
comunque il DSR (0.518) — il gate `study_family_honest` fa esattamente il suo lavoro.
3. Il limite è la **storia**: 2.2 anni totali, 8 mesi di in-sample. Non c'è modo onesto di
dimostrare l'edge oggi.
**Cosa resta.** (a) La famiglia **breadth-thrust (TH)** è l'unico angolo con celle OOS-positive e
bassa/negativa corr a TP01 — **rivisitabile quando il panel HL avrà ≥1-2 anni di storia nativa in
più** (in-sample multi-regime, DSR ricalcolabile); nessun cablaggio forward oggi, il costo
dell'attesa è zero. (b) Il breadth-**gate** su TP01 va in bacino con macro-gate e DVOL-gate: terza
conferma che TP01 non ha bisogno di de-risk esterno (long-flat, già flat nei crash). (c) Il fix
`_align` (epoch esplicita) è la lezione riusabile per ogni futuro panel esterno.
@@ -0,0 +1,58 @@
# 2026-07-01 — Book live: verifica gate SKH01 + emersione errori silenziosi
## Contesto
Il BOOK DERIBIT armato (TP01 0.75 + SKH01 0.25 nettati) è **flat da quando è stato armato
(2026-06-23)**: 193 run orari, tutte HOLD, zero ordini reali. Domanda: il flat è legittimo o il
gate di SKH01 è muto per un bug?
## Verifica del gate SKH01 — SANO
- **Logica viva:** `skyhook_entries` produce 335 entry (BTC, 222 long/113 short) e 341 (ETH,
226/115) su tutta la storia. Non è degenere.
- **Shorta davvero i crash:** ha aperto SHORT sulla gamba di crollo di fine maggio/inizio giugno —
BTC 05-28→06-04 ($73k→$64k), ETH 06-02→06-06 ($1865→$1555).
- **Flat da 06-23 = legittimo:** ultimo entry 06-06, `max_bars=16 × 230m ≈ 2,5g` → chiuso ~06-08/09,
**prima** dell'arming (06-23). Dal grind post-crash nessun setup breakout+regime valido → flat
corretto per design ("NON trend-follower"). Contesto prezzo al 01-07: BTC 53% dal picco, ETH
67,5%, momentum negativo su 1/3/6/12 mesi → anche TP01 (long-flat TSMOM) è a 0x.
- **Entrambi i feed valutano senza crash:** certificato e live `fresh_5m``skh_error=None`.
## Bug scoperto e corretto — pattern "eccezione ingoiata → stato safe silenzioso"
`src/live/` aveva più `try/except` che, in errore, producono uno stato safe (flat/fallback) **senza
farlo emergere**. Rischio: quando l'errore è transitorio ma reale, l'esecutore opera su dati sbagliati
credendoli legittimi.
### (1) `skh_error` — CORRETTO
`book_report` scriveva `skh_error` in un dict locale **mai incluso nel return**`r.get("skh_error")`
sempre `None`; e `book_execute` non lo leggeva comunque. Un feed SKH rotto avrebbe forzato SKH→flat in
silenzio (entry mancato, nessun alert). Fix: `skh_error` esposto nel return + `book_execute` emette
log `⚠️ SKH FEED ERRORE` + alert Telegram. (commit 31369b3)
### (2) Gate fail-safe posizione (Opzione A) — CORRETTO
`src/live/shadow.py::_positions`: se la read della posizione reale Deribit lancia, assume flat (0.0)
con una `note`, MAI propagata (`pos_src`) a `book_report`/`book_execute`. Severità mitigata a valle
da `rebalance_signed` che **rilegge la posizione fresca** (no doppia-posizione), ma resta il caso
"target≈0 + posizione aperta + read fallita → close saltato quel run" (transitorio, backstoppato dal
disaster-SL 30%). **Scelta A = gate fail-safe:** `_positions` ritorna un `pos_error` esplicito →
propagato `shadow_report → book_report → book_execute`; se presente (ONLINE ma posizione IGNOTA)
l'esecutore **NON opera a cieco** (return + alert), come già fa per `online`.
### (3) Diagnostica equity (Opzione B) — CORRETTO
`src/live/shadow.py::_equity`: se `account_summary('USDC')` e tutte le per-asset falliscono,
`real_eq=None` e `shadow_report` ripiega su `paper_cap` (~$2000 dallo state paper) mentre il conto
reale è ~$598 → il book dimensionerebbe su $2000 invece di $598 (**sovradimensiona ~33%**). Il danno
è limitato dall'**hard-cap $300/asset** in `book_net_target`. **Scelta B = SOLO DIAGNOSTICA** (niente
gate/halt, l'hard-cap già protegge il downside): `shadow_report` calcola `eq_fallback` (ONLINE +
equity reale non leggibile + no override), propagato `→ book_report → book_execute`, che stampa
`⚠️ EQUITY FALLBACK` + alert Telegram **ma prosegue** (a differenza del gate posizione, che blocca).
## Test
Suite completa **156/156** (era 148; +4 gate A + 4 diagnostica B). Dry-run reale di `book_execute`
invariato (nessuna riga d'errore, nessun ordine, conto $598.06 al target). Path live: `pos_error=None`,
`skh_error=None`, `eq_fallback=None`.
## Sintesi finale — i 3 pattern "errore silenzioso" del path live sono ora tutti tracciati
| # | Punto | Comportamento | Emersione |
|---|-------|---------------|-----------|
| 1 | `book_report` feed SKH | SKH → flat | log `⚠️ SKH FEED ERRORE` + alert |
| 2 | `_positions` read fallita | posizione → flat | **HALT** (non esegue) + alert (gate A) |
| 3 | `_equity` non leggibile | sizing → paper_cap | log `⚠️ EQUITY FALLBACK` + alert (diagnostica B) |
+114
View File
@@ -0,0 +1,114 @@
# 2026-07-01 — Funding rate come segnale TIME-SERIES direzionale su BTC/ETH (R0701): SCARTATO
## Contesto
Filone: il **funding orario Hyperliquid come proxy di POSIZIONAMENTO/sentiment** con valenza
direzionale a orizzonte giornaliero su BTC/ETH — NON carry. Angolo diverso dal prior art già
chiuso: **FC01** (carry cross-sectional delta-neutral 19 alt → LEAD fragile SCARTATO,
`2026-06-22-funding-carry-hl.md`) e **funding price-clock** intraday (drift attorno agli stamp →
FAIL, onda intraday). Qui l'ipotesi è: funding estremo = affollamento long → fade; funding in
espansione = domanda persistente → follow; funding affollato = de-risk del trend (gate).
Attrattiva a priori: 2 gambe BTC/ETH perp = **eseguibile al capitale reale ~$600** (non STAT-MODE
come XS01/opzioni) — se avesse retto sarebbe stato deployabile.
Script: `scripts/research/r0701_funding_ts.py`.
## Dati e causalità (data-first)
- `data/raw/hlfund_{btc,eth}_1h.parquet`: **2023-05-12 → 2026-06-22, 1138/1138 giorni validi,
0 gap** dopo il 2023-06 (primi ~27 giorni a cadenza 8h — la somma giornaliera degli stamp li
gestisce indifferentemente). Funding medio **+14.4%/anno BTC, +14.6% ETH**; std del funding
giornaliero ~5.5 bps.
- **Join causale** (il punto delicato): le barre 1d di `altlib.get` sono **open-labeled**
(datetime = 00:00 UTC del giorno D; il close della barra D è noto alle 00:00 di D+1). Il
feature-day D aggrega i SOLI stamp in **[D 00:00, D+24h)** — l'ultimo alle 23:00 — quindi tutto
è noto PRIMA della decisione al close della barra D; `eval_weights` shifta la posizione al
giorno D+1. Prefix-consistency check esplicito: **max diff 0.0** (nessun leak).
- **Valutazione su finestra TRONCATA** alla copertura funding, non sul frame prezzi 2018+: fuori
copertura il target sarebbe zero per costruzione e i giorni-zero (a) **gonfiano il T del
deflated-Sharpe** (anti-conservativo) e (b) diluiscono `cand_insample_sharpe` (il gate
`has_insample_edge` scatterebbe a vuoto). La logica di `study_family_honest` è replicata
ESATTAMENTE sui frame troncati coi primitivi altlib (selezione IN-SAMPLE-ONLY → gates
study_marginal → DSR sull'intera griglia). Cross-check `study_marginal` non-troncato in coda:
stessi verdetti (abs WEAK / marginal NEUTRAL / earns_slot False).
- ⚠️ **STORIA CORTA**: ~3.1 anni totali → in-sample pre-HOLDOUT ~1.6a (meno warmup z), hold-out
2025+ ~1.5a. Dichiarato: nessun numero qui ha il peso statistico di TP01 (7 anni).
## Metodo
Famiglia a griglia modesta, 1d, 50/50 BTC+ETH, vol-target 20% cap 2x (convenzione TP01), fee
0.10% RT + sweep 0.00-0.30%:
- **FADE**: z-score(funding daily, lb) ≥ thr → short; ≤ thr → long (fade dell'affollamento).
- **FOLLOW**: z ≥ thr → long; ≤ thr → short (sentiment momentum).
- **GATE**: trend long-flat TP01-like (sgn mom 30d+90d) ma FLAT quando z ≥ thr (de-risk crowding).
- **DIVERGE**: momentum prezzo 20d seguito se funding NON affollato (z ≤ thr), fatto se z ≥ thr.
Griglia: 4 forme × lb {7,14,30,60}g × thr {0.5,1.0,1.5} = **48 celle**. Selezione cella
**in-sample-only** (mai sul hold-out), deflated-Sharpe su tutte le 48, gates marginali
(`marginal_vs_tp01`), boundary-shift della finestra di aggregazione funding (0/3/6/9/12h),
`eval_weights_smallcap` a $600.
## Risultati
Best in-sample per forma: **fade 0.34, follow 0.81, gate 1.76, diverge 0.34** — il segnale
puro-funding è debole ovunque; solo la forma che CONTIENE il trend ha Sharpe.
**Cella scelta (in-sample): GATE lb=60 thr=1.5** — IS 1.76, FULL 1.16, HOLD 0.29.
| Gate | Esito |
|---|---|
| Deflated-Sharpe (48 celle) | **0.215** (null-max atteso 1.59) → **FAIL** (soglia 0.95) |
| Assoluto (frame troncati) | **WEAK** (minFull +0.97, ma minHold **+0.04** BTC); fee-survive fino a 0.30% RT |
| Marginale vs TP01 | **NEUTRAL**: corr **0.879** (hold 0.92), beta 0.77; uplift w25 hold **+0.001**; robust_oos=False (jackknife 0.074); multicut 2026 **0.423** |
| earns_slot / earns_slot_honest | **False / False** |
| Realism | prefix-diff 0.0 (causale); boundary-shift sign-stable (uplift spread 0.047); smallcap $600 haircut ~0.00 (eseguibile, irrilevante senza edge) |
**Controllo decisivo (lezione TP01-DVOL-overlay)** — lo stesso trend long-flat SENZA gate funding:
```
trend NUDO : IS 1.65 FULL 1.14 HOLD 0.37
trend+GATE : IS 1.76 FULL 1.16 HOLD 0.29 corr(gated,nudo)=0.928
delta del gate funding: FULL +0.013, HOLD 0.080
```
**tutto lo Sharpe della "vincitrice" è il trend**; il contributo del funding è ~zero in-sample
e **negativo sul hold-out**. Il gate è TP01 travestito.
**Miglior cella puro-funding** (la vera tesi del filone): FOLLOW lb=60 thr=0.5 — IS 0.81 ma
**FULL 0.28, HOLD 1.69**, marginale **DILUTES** (uplift w25 hold 0.778). L'in-sample 0.81 era
"long-il-toro-2023/24" travestito: z-alto di funding = rally già in corso; nel 2025-26 la stessa
regola è sistematicamente dalla parte sbagliata. FADE (shortare il crowding) perde sul toro e non
recupera abbastanza nel chop; DIVERGE negativa ovunque. NB: esistono celle gate con HOLD migliore
(es. gate 7/1.5 HOLD 0.96) — sceglierle sarebbe **selection-on-holdout**, il gate del 2026-06-29
esiste esattamente per questo.
## Perché fallisce (meccanismo)
1. Su asset MAJOR il funding è **quasi sempre positivo e segue il prezzo con lag**: lo z-score
alto coincide col momentum già espresso → FOLLOW è trend-beta ritardato (muore quando il regime
gira), FADE è shortare il toro. Non c'è l'anti-persistenza ("crowding → reversal") che la tesi
richiede — a 1d, su BTC/ETH, le liquidazioni-da-crowding sono già nel prezzo prima che lo
z-score giornaliero diventi estremo.
2. L'unico uso "funzionante" (gate di de-risk sul trend) è **ridondante**: TP01-like è già flat
quando serve. Stesso verdetto di macro-regime-gate (corr 0.989) e DVOL-overlay: la famiglia
"overlay prudenziale su TSMOM" è esaurita — eredita lo Sharpe del trend e non aggiunge nulla.
## Verdetto
**SCARTATO.** `earns_slot=False`, `earns_slot_honest=False` (DSR 0.215). Il funding time-series
non ha informazione direzionale propria su BTC/ETH a 1d; l'unica cella con Sharpe è trend
travestito con contributo funding OOS negativo. Niente forward-monitor: non c'è una componente
funding da monitorare. Con FC01 (carry cross-sectional) e il price-clock intraday, il filone
"funding come segnale" su dati certi è ora **chiuso su tre lati**.
## Lascito
- `scripts/research/r0701_funding_ts.py` riusabile: (a) loader funding→daily **causale** per barre
open-labeled (finestra [D, D+24h), gestione cadenza mista 8h/1h, flag validità copertura);
(b) pattern "**truncation-honest**" per dataset a copertura parziale — replica di
`study_family_honest` su finestra troncata per non gonfiare il T del DSR né diluire
`has_insample_edge` con anni di zeri (riusabile per ogni serie corta tipo DVOL/HL).
- I parquet `hlfund_*` restano certificati. Se mai si tornasse sul funding: gli estremi VERI
(squeeze) vivono sugli alt, ma è di nuovo cross-sectional → STAT-MODE a $600, soglia di prova
alta (e FC01 ha già mostrato la fragilità d'universo).
@@ -0,0 +1,174 @@
# 2026-07-01 — Pesi statici ri-ottimizzati + guardia-DD sul portafoglio a 4 sleeve
## Mandato
Migliorare il portafoglio ATTIVO (TP01 41.25 / XS01 18.75 / VRP01 15 / SKH01 25) SENZA nuove
strategie: (A) ri-ottimizzazione onesta dei pesi statici, (B) guardia-drawdown a livello
portafoglio (replica del soft-guard vincente del tail-hedge lab 2026-06-23), (C) combinazione.
NON meta-allocazione dinamica (già scartata, thread 3 del 2026-06-29).
Script: `scripts/research/r0701_portfolio_opt.py`. Solo ricerca: nessun file di src/ o live toccato,
i pesi sono PROPOSTI, non applicati.
## Baseline riprodotta
`run_portfolio.py` oggi: **FULL Sh 2.12 / HOLD 2.21 / DD full 7.8% / CAGR 23.3%** (vs 2.13/2.30
del diario Skyhook: i dati sono avanzati di una settimana, coerente). Matrice sleeve outer-join
2019-03-14 → 2026-07-01 (TP01/SKH01 dal 2019, VRP01 dal 2021-05, XS01 dal 2024-01), combine
identico a `portfolio.combined_daily` (pesi rinormalizzati per-riga sugli sleeve attivi).
## (A) Pesi statici — metodo
- Ottimizzazione SOLO in-sample (pre-cut), valutazione OOS (post-cut), multi-cut
{2024-01, 2024-07, 2025-01}. Sleeve con <120 giorni in-sample al cut (XS01 al 2024-01)
PINNED al peso corrente (l'ottimizzatore non ha dati per giudicarli).
- Criteri: MAXSH (max-Sharpe del combo simulato), RP (inverse-vol), ERC, MINVAR-R (min-var con
vincolo ritorno ≥ pesi correnti). Null: EW (25×4) e CURRENT. Vincoli 5%≤w≤60%, Σ=1.
- **Cap STRUTTURALI dichiarati** (prudenza, non statistica): **VRP01 ≤ 15%** (sleeve MODELLATO su
DVOL ATM, stress-f non catturato — "niente short-vol da modello in deploy"), **XS01 ≤ 25%**
(STAT-MODE, storia ~2.5 anni). Varianti sotto cap: MAXSH-STR (ottimizzato) e EW-STR
(EW proiettato sui cap, parameter-free → TP 30 / XS 25 / VRP 15 / SKH 30).
### Risultati (pesi ottimizzati pre-2025, valutati FULL e HOLD-OUT 2025-26)
| criterio | pesi TP/XS/VRP/SKH | FULL Sh | FULL DD | FULL CAGR | HOLD Sh | HOLD DD | HOLD CAGR | multi-cut ΔSh OOS | anni vinti |
|---|---|---|---|---|---|---|---|---|---|
| CURRENT | 41.2/18.8/15/25 | 2.12 | 7.8% | +23.3% | 2.21 | 3.5% | +16.0% | — | — |
| MAXSH | 27.6/17.5/**39.5**/15.4 | 2.17 | 6.3% | +21.8% | 2.27 | 2.8% | +12.7% | +0.24/+0.08/+0.06 | **1/8** |
| RP-invvol | 26.7/15.5/**41.6**/16.3 | 2.17 | 6.4% | +21.8% | 2.25 | 2.8% | +12.4% | +0.28/+0.07/+0.04 | — |
| ERC | 26.6/16.2/**41.6**/15.7 | 2.17 | 6.3% | +21.7% | 2.25 | 2.8% | +12.4% | +0.29/+0.07/+0.05 | — |
| MINVAR-R | 26.7/23.3/**29.6**/20.4 | 2.18 | 6.4% | +23.3% | 2.37 | 2.6% | +16.1% | +0.20/+0.10/+0.16 | 4/8 |
| MAXSH-STR | 38.6/25/15/21.4 | 2.14 | 7.5% | +23.4% | 2.28 | 3.3% | +16.8% | **0.00**/+0.03/+0.07 | 3/8 |
| EW | 25/25/25/25 | 2.14 | 7.0% | +24.5% | 2.39 | 2.8% | +18.1% | +0.16/+0.17/+0.18 | 6/8 |
| **EW-STR** | **30/25/15/30** | 2.10 | 8.5% | +25.2% | **2.35** | 3.2% | **+19.7%** | **+0.06/+0.13/+0.14** | **7/8** |
### Lettura onesta
1. **Gli OTTIMIZZATORI non aggiungono nulla — anzi overfittano.** Tutti (MAXSH/RP/ERC/MINVAR-R)
convergono sulla stessa mossa: scaricare TP01 e caricare **VRP01 al 30-42%** perché ha la vol
più bassa (7.7%/anno). Ma quella è la vol del MODELLO (premio BS su DVOL, coda di stress non
catturata): è esattamente il punto cieco tipo-CC01 ("Sharpe/vol implausibile → rischio
nascosto"). E per-anno MAXSH vince **1/8 anni** vs CURRENT — l'uplift multi-cut è smoothing di
vol modellata, non alpha di allocazione. **Dentro i cap** l'ottimizzatore (MAXSH-STR) converge
quasi sui pesi correnti (38.6/25/15/21.4) e NON è persistente (0.00 al cut 2024-01) →
**i pesi correnti sono già ~ottimi per ciò che il backtest può giudicare lecitamente**.
2. **L'unico segnale robusto è la DE-CONCENTRAZIONE parameter-free**: EW-STR (EW sotto i cap,
nessun parametro fittato) vince **7/8 anni** (unica perdita 2021 3.1pp), tutti i cut OOS
(ΔSh +0.06/+0.13/+0.14), HOLD Sh 2.21→2.35, HOLD CAGR +16.0→+19.7%. Classico "1/N batte
l'ottimizzazione" (DeMiguel). La sostanza: CURRENT dà il 41% al peggior Sharpe FULL del book
(TP01 1.30) e EW-STR sposta ~11pp da TP01 verso SKH01 (+5pp) e XS01 (+6.25pp).
3. **MA non è un free lunch risk-adjusted**: FULL Sh 2.12→**2.10** e FULL DD 7.8→**8.5%**
(più SKH01, che ha DD standalone 18%, pesa di più nei primi anni a 2 sleeve). L'uplift è un
TILT di ritorno verso gli sleeve research-grade con la storia più corta/caveat più pesanti
(SKH01 = research win, book 230m da verificare a deploy; XS01 = STAT-MODE non eseguibile a 2k).
E TP01 resta l'unico sleeve *deployed* live: il suo 41% nel report riflette una scelta di
prudenza/eseguibilità, non solo statistica.
### VERDETTO (A)
- **Ottimizzazione formale: NON MIGLIORA** (viola i cap strutturali o converge ai pesi correnti).
**Per il book reale i pesi correnti reggono.**
- **Candidato PROPOSTO (non applicato): EW-STR = TP01 30 / XS01 25 / VRP01 15 / SKH01 30** —
uplift OOS persistente e per-anno (7/8), dentro i cap, al costo di FULL DD +0.7pp e FULL Sh
0.02. È una decisione di risk-appetite (più ritorno atteso spostando peso su sleeve
research-grade), non un miglioramento dominante. Se si vuole adottare, farlo consapevoli che
aumenta la dipendenza da SKH01/XS01, e NON tocca il book Deribit eseguibile (TP75/SKH25).
### Caveat metodologici (A)
- Le 3 finestre multi-cut sono ANNIDATE (tutte contengono il 2025-26): non 3 conferme
indipendenti. Il test disgiunto vero è il per-anno (7/8 per EW-STR è il dato forte).
- EW-STR è stato costruito DOPO aver visto EW vincere (proiezione del null sui cap dichiarati):
costruzione principled e parameter-free, ma va detto — non era nella griglia iniziale.
- I pesi CURRENT stessi furono fissati (2026-06-23) conoscendo l'hold-out → il confronto
"in-sample-opt vs CURRENT su hold-out" è semmai sbilanciato A FAVORE di CURRENT; EW-STR vince
comunque.
## (B) Guardia-drawdown a livello portafoglio — metodo
Soft-guard causale sul combo 4-sleeve (pesi CURRENT): esposizione del giorno D decisa dal DD
dell'equity fino a **D1**; trigger/re-entry sul **NAV ombra** non-guardato (lezione stops_lab:
sull'equity congelata a expo=0 il DD non rientra mai); isteresi re-risk a 0.4·X come nel
tail-hedge lab. Griglia X∈{3,4,5,6}%, derisk∈{0,0.5}. Selezione cella IN-SAMPLE (max Calmar
pre-2025), verifica hold-out + multi-cut, **null de-levering** (expo costante c che pareggia il
MaxDD in-sample della guardia: Sharpe di c·r è invariante → la guardia vale solo se batte quello).
### Risultati
- Cella scelta in-sample: **X=5% / derisk=0.5** (IS Calmar 3.64 vs 3.24 baseline; IS DD 7.8→6.6%,
IS CAGR 25.3→23.9%, IS Sh 2.13→2.12). In-sample batte il null de-levering c=0.84 a pari DD
(CAGR 23.9% vs 21.0%) — il timing esiste in-sample.
- **MA OOS è INERTE**: a ogni cut la cella scelta non scatta MAI (ΔSh +0.00, ΔDD 0.0pp, ΔCAGR
0.0pp su tutti e 3 i cut) perché il DD OOS del 4-sleeve resta 3.5-4.8% < trigger 5%.
**La diversificazione a 4 sleeve fa già il lavoro della guardia** (il 2022 del book è +11.6%
con DD 3.6%: non c'è il grind da proteggere che aveva il combo TP01+GTAA del tail-hedge lab,
DD 8.4% / 2022 4.4%).
- Trigger più stretti che scattano davvero COSTANO: X=3%/d=0.5 → HOLD Sh 2.10 (< 2.21), CAGR
13.7% (< 16.0%); X=3-4%/d=0 whipsaw pieno (HOLD Sh 1.83-2.09; a X=4/d=0 il DD guardato IS
sale addirittura a 8.8% > 7.8% baseline — accumulo di perdite su cicli off/on, lo stesso
fenomeno visto in stops_lab con l'uscita totale).
### VERDETTO (B)
**NON MIGLIORA.** La guardia-DD che vinceva sul combo TP01+GTAA (DD 8.4%, 2022 4.4%) qui non ha
bersaglio: il 4-sleeve ha già DD ~3.5-4.8% OOS. Coerente col thread meta-allocazione 2026-06-29
(drawdown-control ridondante). Nota operativa: la cella X=5%/d=0.5 è *innocua* (non fa nulla
finché il DD non supera il 5%) e in-sample mostrava timing genuino sopra il null de-levering —
può valere come **circuit-breaker di emergenza sul live** (risk management, non alpha), ma non è
un miglioramento statistico e non la si adotta come tale.
## (C) Combinazione
(B) è inerte → niente da combinare. Per completezza: EW-STR + guard X=5%/d=0.5 → FULL Sh 2.08,
DD 6.7%, CAGR 23.7% | HOLD invariato 2.35/3.2%/19.7% (la guardia lima il DD 2021-2023 del tilt
a costo di 0.02 Sh FULL). Nessuna adozione proposta.
## Sintesi
| config | FULL Sh | FULL DD | FULL CAGR | HOLD Sh | HOLD DD | HOLD CAGR |
|---|---|---|---|---|---|---|
| **CURRENT (attivo)** | 2.12 | 7.8% | +23.3% | 2.21 | 3.5% | +16.0% |
| EW-STR (proposta A, non applicata) | 2.10 | 8.5% | +25.2% | 2.35 | 3.2% | +19.7% |
| CURRENT + guard 5%/0.5 (B) | 2.11 | 6.6% | +22.3% | 2.21 | 3.5% | +16.0% |
| EW-STR + guard (C) | 2.08 | 6.7% | +23.7% | 2.35 | 3.2% | +19.7% |
- **(A) NON MIGLIORA in senso forte** — i pesi correnti reggono; l'unico candidato onesto è il
tilt de-concentrante EW-STR (30/25/15/30), un trade-off di risk-appetite da decidere, non un
upgrade dominante. Gli ottimizzatori in-sample o violano i cap strutturali (VRP01 40%) o
riconvergono sui pesi correnti.
- **(B) NON MIGLIORA** — guardia inerte OOS sul 4-sleeve; utile al più come circuit-breaker.
- Nessuna modifica a `src/portfolio/sleeves.py` o al live. Test 160/160 verdi.
Script: `scripts/research/r0701_portfolio_opt.py` (~4s, vettoriale).
## Verifica avversariale (agente scettico)
Script: `scripts/research/r0701_portfolio_skeptic.py`. **VERDETTO: NON APPLICARE — CURRENT resta.**
I numeri dell'agente riproducono ESATTAMENTE (via `StrategyPortfolio.combined_daily`, path di
produzione: CURRENT 2.12/2.21, EW-STR 2.10/2.35, multi-cut +0.06/+0.13/+0.14, |diff|=0). Il claim
muore altrove: l'uplift è **selezione-sull'hold-out di 2° ordine**, non alpha di allocazione.
1. **L'uplift OOS vive TUTTO nel 2025-26 — la finestra che ha selezionato i beneficiari.**
Finestre OOS **DISGIUNTE** (i 3 cut del diario sono annidati): ΔSh EW-STR vs CURRENT =
**0.12** (2024H1) / +0.06 (2024H2) / **+0.14** (2025+). L'unica finestra OOS non toccata
dalla selezione hold-out è negativa. Sharpe standalone IS→HOLD per sleeve: TP01 1.49→**0.30**,
SKH01 1.42→**1.64**, XS01 1.22→**1.51**: EW-STR compra esattamente i due sleeve ammessi/affinati
(2026-06-23 / 2026-06-19) PERCHÉ forti sull'hold-out e vende quello che lì collassa. Meccanico.
2. **Pre-2025 (finestra non contaminata) EW-STR PERDE risk-adjusted**: ΔSh aggregato **0.05**,
Sharpe per-anno vinti **2/6** (2021 0.29, 2020 0.10, 2023/2024 negativi).
3. **"7/8 anni vinti" è un artefatto di metrica**: è RITORNO composto (un tilt a più vol/ritorno
li vince quasi per costruzione), non risk-adjusted. Su **Sharpe per-anno: 3/8**. E due dei 7
"vinti" (2023, 2024) hanno margine **+0.03pp** = tie. Bilancio materiale sul ritorno:
5 vinti / 1 perso (2021 3.1pp) / 2 pari.
4. **Plateau: liscio ma il gradiente è solo "meno TP01"**; a TP fisso, alzare SKH 25→30 è
piatto/negativo (min multi-cut a TP35: +0.07→+0.02). Scomposizione: il tilt **solo-SKH**
(+5pp, TP 36.25) da solo NON è persistente (multi-cut **0.00**/+0.05/+0.04); il tilt
**solo-XS** (TP35/XS25/VRP15/SKH25) è l'unico positivo su tutte le finestre disgiunte
(+0.05/+0.02/+0.11) — ma pre-2025 fa ΔSh 0.00 (edge ≈ zero fuori dal 2025-26) e carica lo
sleeve MENO eseguibile (STAT-MODE).
5. **Realismo**: con l'outer-join rinormalizzato EW-STR tiene SKH01 al **50% effettivo nel
2019-20 e 40% nel 2021-23** (book research 230m a metà portafoglio: da lì il FULL DD 8.5%).
Punto onesto A FAVORE: un haircut d'esecuzione a drag su SKH01(-20/30%) e SKH+XS non ribalta
il ranking hold-out (ΔSh resta +0.08..+0.13), ma i livelli assoluti crollano (HOLD Sh CURRENT
2.21→1.64 al 30% su entrambi) e il beneficio resta di CARTA: il book Deribit eseguibile
(TP75/SKH25) non è toccato dalla decisione.
6. **Forking paths quantificato**: ≥15 config valutate sull'hold-out (7 vettori pesi + 8 celle
guardia), EW-STR costruito dopo aver visto EW vincere. Su **500 tilt CASUALI cap-respecting**:
51% batte CURRENT sull'hold-out (mediana ΔSh +0.00), solo 11% sul FULL, 30% su tutti e 3 i cut.
EW-STR è al **94° percentile** dell'uplift hold-out fra i tilt casuali ≈ il **massimo atteso di
~15 tentativi** (k/(k+1)=93.8%): l'uplift osservato è indistinguibile dal "best of 15 tilt
casuali scelto sull'hold-out".
**Conclusione**: il tilt non è disonesto nei conti ma non è evidenza — è la stessa lezione del gate
SELECTION-ON-HOLDOUT applicata al livello dei PESI (2° ordine: gli sleeve stessi sono stati
selezionati su quell'hold-out). Unica direzione sopravvissuta: XS01 18.75→25 (solo-XS, positivo su
tutte le finestre disgiunte), MA edge ≈0 fuori dal 2025-26, sleeve STAT-MODE non eseguibile a $600,
e l'hold-out-strength di XS01 è a sua volta concentrata dal gate di dispersione nel regime 2025-26
→ da rivalutare SOLO con più storia forward, non ora. **CURRENT (41.25/18.75/15/25) resta.**
@@ -0,0 +1,132 @@
# 2026-07-01 — Ondata multi-agente a 6 filoni: 0 nuovi sleeve, pesi CURRENT confermati, 1 gate nuovo
**Goal utente:** "cerca con diversi agenti altre strategie di trading. Prova di tutto e arriva a
migliorare il portfolio presente. Lo scopo è sempre 50 euro days."
**Esito in una riga:** 6 filoni nuovi (scelti dopo scan dei ~65 diari per non ripetere strade morte)
+ 1 verifica avversariale → nessun candidato supera i gate onesti; il portafoglio a 4 sleeve
(41.25/18.75/15/25) **sopravvive a un attacco serio**, che è l'esito corretto per un book onesto.
Miglioramento concreto dell'ondata = metodologico: **gate `weights_tilt_null`** codificato (sotto).
## I 6 filoni (tutti su harness onesto: study_family_honest, marginal scorer, DSR≥0.95, smallcap $600)
| # | filone | verdetto | perché muore |
|---|---|---|---|
| 1 | **Funding time-series** BTC/ETH (posizionamento, non carry) — `r0701_funding_ts.py` | SCARTATO | FOLLOW = trend-beta ritardato (HOLD 1.69), FADE = shortare il toro; la cella gate è TP01 travestito (controllo senza funding = stessi numeri, corr 0.93, ΔHOLD 0.08); DSR 0.215. **Filone funding chiuso su 3 lati** (FC01 carry, price-clock, TS-signal). |
| 2 | **Breadth/internals alt** (51 HL) → BTC/ETH — `r0701_breadth_internals.py` | SCARTATO (rivisitabile) | Unico non-ridondante col trend (corr→TP01 0.40, lavora dove TP01 è attivo), assoluto PASS, marginal ADDS — ma jackknife 0.068 (uplift su UN mese) e DSR 0.433/104 celle. Con ~8 mesi di IS il 2024-toro non basta. **Rivisitare tra 1-2 anni di storia HL nativa.** |
| 3 | **Residual momentum XS** (β-hedged vs BTC, 19 major) — `r0701_xs_residmom.py` | REDUNDANT | Cross-section la residualizzazione è quasi un no-op (lo z-score di XS01 già rimuove il mercato): corr→XS01 0.54, HOLD 0.24, corr(IS,HOLD) tra le celle **0.37** (anti-predittivo = rumore). L'edge residuo-momentum vive nella coppia ETH/BTC (STATARB-RESID, resta in `paper_statarb`), non generalizza. |
| 4 | **Pesi + guardia-DD**`r0701_portfolio_opt.py` | vedi sotto | Unico candidato dell'ondata (EW-STR) → refutato dallo scettico. Guardia-DD X5%/d0.5: in-sample batte perfino il null de-levering, ma **OOS non scatta mai** (DD book 3.5-4.8% < trigger): la diversificazione fa già il lavoro. Utilizzabile solo come circuit-breaker di emergenza (risk mgmt, non alpha). |
| 5 | **Affinamento VRP01** (sizing IVRV, DVOL-mom, gate TP01) — `r0701_vrp_refine.py` | NON MIGLIORA | L'alpha è già tutto nel gate binario IV-rank; il gate TP01 è la trappola IS perfetta (schiva il 2022, ma taglia le settimane migliori dell'hold-out: multi-cut 0/5). **3° fallimento del filone "affinare VRP dentro il modello" → esaurito** finché cerbero-bite non cattura un crash reale (f di stress). |
| 6 | **Stagionalità cross-sectional** HL — `r0701_xs_seasonal.py` | SCARTATO (allo step statistico) | Nessuna persistenza split-half sopra il null permutato max-statistic (p 0.16-0.23); l'unica struttura è il canale-beta dell'effetto weekday di mercato (famiglia trackF, già morta). Turnover ~2×gross/die = fee-death comunque. |
## EW-STR: il candidato refutato (caso di scuola di selezione-sull'hold-out di 2° ORDINE)
Il filone 4 proponeva **EW-STR** (TP01 30 / XS01 25 / VRP01 15 / SKH01 30 = equal-weight proiettato
sui cap strutturali): HOLD Sh 2.21→2.35, multi-cut +0.06/+0.13/+0.14, "7/8 anni vinti" — dichiarando
onestamente i caveat. La verifica avversariale (`r0701_portfolio_skeptic.py`, addendum nel diario
pesi) lo uccide:
1. **Selezione di 2° ordine**: SKH01 fu ammesso (2026-06-23) *perché* alzava l'hold-out 2025-26;
XS01 fu affinato conoscendo l'hold-out. EW-STR vende lo sleeve che collassa OOS (TP01 IS 1.49→
HOLD 0.30) e compra i due selezionati su quell'OOS. Pre-2025: **ΔSh 0.05** (perde risk-adjusted).
2. **Su finestre OOS disgiunte**: 0.12 (2024H1) / +0.06 (2024H2) / +0.14 (2025+) — tutto l'uplift
vive nella finestra che ha selezionato gli sleeve. "7/8 anni" era sul ritorno composto; su
Sharpe è 3/8.
3. **Firma best-of-k**: su 500 tilt casuali cap-respecting, EW-STR siede al 94-100° percentile ≈
k/(k+1) con k≈15 config viste sull'hold-out → uplift indistinguibile dal *best-of-15 scelto
sull'hold-out*.
4. **Realismo**: in pesi effettivi (outer-join rinormalizzato) EW-STR = SKH01 al 40-50% del book
2019-23 (sleeve research a book 230m). Il book Deribit eseguibile (TP+SKH) non cambierebbe comunque.
Residuo onesto: il sub-tilt XS01 18.75→25 è positivo su tutte le finestre disgiunte ma edge≈0
pre-2025 e STAT-MODE → rivalutare solo con storia forward.
## GATE NUOVO codificato: `weights_tilt_null` (src/portfolio/portfolio.py)
La lezione è ora codice, come per marginal-scorer e study_family_honest:
- **`combine_outer(daily_cols, weights, lo, hi)`** — la combinazione outer-join/rinormalizzata di
`combined_daily` estratta come funzione su pesi arbitrari (riusabile per studi di tilt).
- **`weights_tilt_null(daily_cols, w_current, w_proposed, caps, floor, n, seed, k_seen)`** —
null di tilt casuali uniformi sul simplesso dentro floor/caps: riporta `delta_hold/full/insample`,
`frac_random_beat_hold`, `pctl_hold`, `bestofk_pctl=k/(k+1)` e `gate_pass`.
**Regola: un cambio-pesi si applica solo se (1) delta_insample ≥ 0 e (2) pctl_hold < firma
best-of-k** (necessario, non sufficiente: restano finestre disgiunte + realismo).
Sul caso reale EW-STR: delta_hold +0.139, delta_insample **0.054**, pctl **100°** vs best-of-15
93.75° → `gate_pass=False`. Test: `tests/test_weights_tilt_null.py` (5 test, sintetici,
deterministici; incluso lo sleeve "forte-solo-nell'hold-out" che DEVE fallire il gate).
## Lezione tecnica riusabile (dal filone 2)
`DatetimeIndex.view("int64")` su indici **tz-aware a risoluzione non-ns** (pandas 2.x) produce
timestamp in scala sbagliata → un `merge_asof` può broadcastare l'ultimo valore su tutta la storia
= **look-ahead che `causality_ok` non vede** (input fisso). Sintomo: decine di celle con Sharpe
identico. Fix: epoca esplicita in ms. Verificato: `altlib.py` non è affetto (grep pulito); dettagli
nel diario breadth.
## Onestà sul goal €50/giorno
Invariata (vedi CLAUDE.md): a capitale reale ~$600 il book onesto fa ~+€1.5/giorno atteso; la via
resta capitale + tempo + target-vol, non un tilt di pesi né leva. Questa ondata conferma che il
book attuale è vicino al massimo estraibile dai dati certificati disponibili oggi; le opzioni
reali di crescita restano (a) storia forward che promuova i lead (STATARB-RESID, breadth tra 1-2
anni), (b) capitale, (c) nuove fonti di dato (f di stress reale per VRP).
**File dell'ondata:** `scripts/research/r0701_{funding_ts,breadth_internals,xs_residmom,portfolio_opt,portfolio_skeptic,vrp_refine,xs_seasonal}.py`; diari `2026-07-01-{funding-ts-signal,alt-breadth-internals,xs-residual-momentum,portfolio-weights-ddguard,vrp-refinement,xs-seasonality}.md`; gate in `src/portfolio/portfolio.py` + `tests/test_weights_tilt_null.py`.
## ADDENDUM (stessa sera) — GTAA01 promosso a 5° SLEEVE: il miglioramento c'era, ma era in casa
Il secondo passaggio sull'archivio (non sui dati!) ha trovato il candidato che l'ondata di *ricerca*
non poteva trovare: **EQ-GTAA01** (`src/portfolio/gtaa.py` — trend difensivo equity 6-ETF, vt12%,
~30 anni di storia, netto fee IB) era stato validato il 2026-06-22 come *"primo miglioramento
STRUTTURALE del rischio-aggiustato trovato in tutta la ricerca post-reset"*, messo in paper
cross-venue (`paper_combo`, nel cron dal 2026-06-23) e **mai valutato come 5° sleeve del
portafoglio attivo** (il giorno dopo arrivò SKH01 e il filone si fermò).
### Valutazione onesta (`r0701_gtaa_5th_sleeve.py`) — perché GTAA passa dove EW-STR è morto
| gate | EW-STR (refutato) | +GTAA01 (passa) |
|---|---|---|
| uplift IN-SAMPLE (pre-2025) | **0.05** | **+0.09..+0.13** (2.134→2.23-2.26) |
| finestre OOS disgiunte | 0.12 / +0.06 / +0.14 | **+0.05 / +0.19 / +0.25** (tutte >0) |
| multi-cut (4 tagli) | solo annidate | **+0.21 / +0.21 / +0.22 / +0.25** |
| provenienza dell'edge | sleeve selezionati sull'hold-out crypto | validato su OOS equity 2015+, **indipendente** dall'hold-out crypto |
| corr al book | — | **+0.10** full, ~0.00 hold-out; is_hedge +0.30 (non-hedge) |
| plateau | gradiente generico anti-TP01 | **monotono su w 10-30%, ogni cella migliora IS/FULL/HOLD/DD** |
Convenzione di realismo cross-venue (nuova, cablata nello sleeve): nei giorni senza barra equity
(weekend/festivi US) il rendimento è **0.0** — capitale IB fermo, NON rinormalizzato sul crypto
(due conti reali non si travasano nel weekend). Numeri quasi identici alla versione outer-join
(differenze al 3° decimale) ma il modello è quello giusto. Attivazione nel book all'era crypto
(2019-03-14): senza troncatura l'outer-join farebbe partire il book nel 1996 con 23 anni di
solo-GTAA rinormalizzato al 100%.
### Decisione pesi
Cella IS-best = w=30% (bordo griglia), **ingresso @20%** per scelta strutturale dichiarata:
(a) cross-venue IB con paper operativo di soli 8 giorni — non diventa lo sleeve più grande del
book al day-one; (b) precedente: ogni sleeve nuovo è entrato ≤25% (SKH01 25%, VRP01 15%);
(c) costo CAGR (a 30%: 16.8% vs 18.8% a 20%) — e l'obiettivo €50/g vive di CAGR, non solo di
Sharpe. Tutte le celle 10-30% passano comunque ogni gate. Pesi nuovi: **TP01 33 / XS01 15 /
VRP01 12 / SKH01 20 / GTAA01 20** (i 4 preesistenti scalati ×0.80, rapporti conservati).
### Portafoglio 5-sleeve (run_portfolio, capitale 2k)
| | FULL Sh | FULL DD | FULL CAGR | HOLD Sh | HOLD DD | HOLD CAGR |
|---|---|---|---|---|---|---|
| 4-sleeve (baseline) | 2.12 | 7.8% | +23.3% | 2.21 | 3.5% | +16.0% |
| **5-sleeve (+GTAA01 20%)** | **2.24** | **6.2%** | +18.8% | **2.46** | **2.2%** | +14.6% |
Per-anno: tutti gli 8 anni positivi, 2022 +7.0% (era +11.6%: GTAA soffre il whipsaw 2022, unico
anno con dSh negativo 0.49 — dichiarato). Il trade-off è esplicito: 4.5pp di CAGR full per
+0.12/+0.25 di Sharpe e DD 7.8→6.2 / hold-out 3.5→2.2. A pesi research (XS/VRP/SKH modellati o
STAT-MODE) il CAGR era comunque di carta; lo Sharpe/DD del book è la metrica di verità.
### Cosa NON cambia
Il **book live Deribit** (`deribit_book_sleeves` TP01+SKH01 75/25, `book_execute`) è INTATTO —
GTAA è su IB, già tracciato forward da `paper_combo`. `paper_portfolio` da oggi traccia la
composizione a 5 (regime-break dichiarato qui). Test: `tests/test_gtaa_sleeve.py` (registry,
weekend-zero, attivazione, book live invariato) — suite 168/168.
### Lezione dell'addendum
Prima di una nuova ondata di *ricerca*, ri-leggere l'archivio dei **validati-ma-non-integrati**:
l'unico upgrade onesto del book in 3 ondate (104+5+6 filoni) era un edge già validato che
aspettava solo la valutazione da sleeve. La diversificazione strutturale (secondo mercato,
corr ~0.1) resta l'unica cosa che alza lo Sharpe del book senza nuovo alpha.
+98
View File
@@ -0,0 +1,98 @@
# 2026-07-01 — Affinamento VRP01 (sizing IVRV / filtro DVOL-momentum / gate TP01): NON MIGLIORA
**Script:** `scripts/research/r0701_vrp_refine.py` (solo ricerca: `src/` e `scripts/live/` non toccati).
**Esito: NON MIGLIORA — VRP01 baseline resta la config canonica.** La cella IS-best (gate TP01-skip)
è un falso positivo in-sample da manuale: IS Sharpe 2.07 ma multi-cut **0/5**, hold-out **0.14**
(vs 0.59 del baseline), DSR 0.71. Nessuna modifica a `sleeves._vrp_combo_returns`.
## Cosa testava (celle NUOVE, verificate non ripetute nei diari)
Stessa pipeline di `options_vrp_v2.py` (pricing BS su DVOL reale, payoff sul path certificato,
fee 12.5% del premio, f=1.0, tenor 7d, spread 0.28/0.10) — cambiati SOLO gate/sizing:
1. **Sizing sul gap IVRV** (carry atteso): size lineare `clip(vrp/scale,0,1)` (scale 8/12 pt) o
percentile espandente causale del VRP; in aggiunta o in sostituzione del gate binario IV-rank.
NB: il gate composito "IV-rank>0.30 AND IVRV>0" è **già il baseline** (`gate_vrp=True`).
2. **Filtro DVOL-momentum**: non vendere se `dvol[i]dvol[ik] > thr` (k∈{5,10}, thr∈{0, 5pt}).
(Diverso da `dvol_directional` 2026-06-29, dove il DVOL-mom era segnale direzionale sul perp.)
3. **Gate di regime da TP01**: skip o half-size quando TP01 è flat su entrambi gli asset (risk-off).
4. Croce completa: **105 celle**, tutte contate nel deflated-Sharpe.
NON ripetuto (già scartato): gestione attiva intra-trade (PT/SL/vol-stop/delta-exit,
`2026-06-20-vrp-active-management.md` → hold-to-expiry vince).
Metodo: selezione cella **in-sample (pre-2025)** con floor di attività IS ≥20%, hold-out 2025-26,
**multi-cut 5 tagli** (2023-01→2025-01, richiesti ≥4/5 uplift>0), `altlib.deflated_sharpe` su tutti
i 105 trial, effetto sul **portafoglio 4-sleeve** (TP01 41.25 / XS01 18.75 / VRP 15 / SKH01 25).
## Risultati (book 50/50 BTC+ETH, rendimenti settimanali)
Baseline riprodotto esattamente: **FULL Sh 1.09 / IS 1.26 / HOLD 0.59 / DD 12% / worst 7.4% / attivo 41%.**
| cella | IS Sh | FULL | HOLD | DD | worst | attivo |
|---|---|---|---|---|---|---|
| **BASELINE** bin+ivr30 | 1.26 | **1.09** | **0.59** | 12% | 7.4% | 41% |
| sizing lin 8pt (+ivr30) | 1.20 | 1.05 | 0.61 | 11% | 7.4% | 41% |
| sizing lin 12pt (+ivr30) | 1.12 | 0.98 | 0.54 | 11% | 7.4% | 41% |
| sizing rank (+ivr30) | 1.07 | 0.94 | 0.54 | 9% | 5.7% | 41% |
| sizing lin 8pt **al posto di** ivr | 0.89 | 0.58 | **0.38** | 21% | 7.4% | 86% |
| sizing rank al posto di ivr | 1.03 | 0.69 | **0.36** | 11% | 5.7% | 86% |
| mom k=5 thr=5pt | 1.24 | 1.02 | 0.37 | 9% | 6.5% | 34% |
| mom k=10 thr=5pt | 1.26 | 1.08 | 0.55 | 9% | 6.5% | 31% |
| mom k=5 thr=0 | 1.78 | 1.24 | 0.10 | 5% | 3.9% | 20% |
| mom k=10 thr=0 | 1.36 | 0.78 | **0.47** | 7% | 3.9% | 18% |
| tp01-skip | **2.05** | 1.43 | **0.14** | 7% | 4.7% | 27% |
| tp01-half | 1.72 | 1.34 | 0.27 | 7% | 4.7% | 41% |
| IS-best: lin8+tp01-skip | **2.07** | 1.43 | **0.14** | 6% | 4.2% | 27% |
**Multi-cut della IS-best vs baseline: uplift negativo in 5/5 tagli** (0.35 → 0.73, monotono
peggiorando verso il presente). **DSR** IS-best 0.709 (<0.95); baseline 0.472 dentro questa griglia
(contesto onesto: nemmeno il baseline è statisticamente distinguibile su 105 trial correlati — ma è
pre-committed, non selezionato qui). Celle che battono l'hold-out del baseline: **1/105** (lin8pt
tp-off, +0.02 — rumore; non selezionabile: sarebbe selection-on-holdout).
### Frequenza d'intervento dei gate (settimane-trade del baseline = 163)
- **TP01-skip: blocca il 32.5%** delle settimane tradabili (TP01 flat il 38% dei giorni della
finestra DVOL). NON è ridondante col baseline — lavora tanto — ma lavora **male** OOS.
- mom thr=5pt: blocca 24-32%; mom thr=0: blocca 60-64% (quasi metà del book).
### Portafoglio 4-sleeve (VRP @15%)
| VRP nello slot | FULL Sh | FULL DD | HOLD Sh | HOLD DD |
|---|---|---|---|---|
| **baseline** | **2.12** | 7.8% | **2.21** | 3.5% |
| IS-best (lin8+tp01-skip) | 2.11 | 7.8% | 2.12 | 3.5% |
Anche a livello portafoglio la variante **peggiora** l'hold-out (2.21→2.12) e non migliora nulla.
## Lettura (perché non migliora)
1. **L'alpha del VRP è già tutto nel gate IV-rank binario** ("vendi solo vol ricca"). Il sizing
proporzionale al gap IVRV è ridondante *dato* il gate (Δ ≈ ±0.05, dentro il rumore); usato al
posto del gate, riporta dentro le settimane di vol povera e distrugge l'hold-out (0.36/0.38).
Il carry atteso (IVRV) NON è un segnale di *intensità* utile oltre il suo segno+rank.
2. **Il gate TP01 è la trappola in-sample perfetta** (la lezione macro-gate al contrario): schiva il
2022 (6%→0%) gonfiando l'IS a 2.05, ma nel 2025-26 TP01 è flat proprio nei regimi di vol ricca
in cui la vendita gated di put **guadagna** — il gate taglia le settimane migliori dell'hold-out
(0.59→−0.14). Short-vol gated e trend sono complementari, non condizionabili l'uno sull'altro.
3. **Il DVOL-momentum non aggiunge protezione**: il crash-skip IV-rank>0.90 + il long wing dello
spread coprono già il run-up di vol; thr=0 è troppo aggressivo (blocca ~60%), thr=5pt è neutro
(mai meglio del baseline su hold-out).
## Onestà / caveat (invariati)
Premio MODELLATO su DVOL ATM (no skew), book 1d, f di stress reale non catturato → tutto questo
resta dentro il modello; il verdetto massimo possibile era "sleeve modellato migliorato", e non è
stato raggiunto. Il DSR sulla griglia è un limite *superiore* (celle correlate + ~20 config storiche
di options_vrp_lab/_v2 non contate).
## Azione
**Nessuna.** VRP01 resta com'è (`bin + gate_vrp + ivr30 + crash-skip 0.90`, hold-to-expiry).
Terzo tentativo di affinamento fallito (dopo active-management e — per il portafoglio — DVOL
overlay): il filone "migliorare VRP01 dentro il modello" è da considerarsi **esaurito**; il prossimo
passo utile resta quello noto: f di stress reale quando cerbero-bite cattura un crash + skew reale
sul long wing.
Script: `scripts/research/r0701_vrp_refine.py` (`--skip-portfolio` per la sola griglia).
@@ -0,0 +1,74 @@
# 2026-07-01 — Residual (idiosyncratic) momentum cross-sectional sui 19 major HL — SCARTATO/REDUNDANT
**Tesi.** STATARB-RESID (thread 4, 2026-06-29) ha mostrato che il momentum del residuo ETH−β·BTC
(β OLS rolling, **sgn=+1**: le dislocazioni continuano a 1d) passa quasi tutti i gate su 2 gambe,
fallendo solo il deflated-Sharpe (0.929<0.95). Ipotesi qui: portare lo **stesso meccanismo
cross-sectional** sui 19 major di XS01 — per ogni alt, residuo vs β·BTC (β OLS rolling B), momentum
del residuo (blend z-score [30,90] come XS01 o singolo L), long top-k / short bottom-k, vol-target
20% — sperando che la **breadth** (18 stream invece di 1) alzasse il DSR dove il 2-gambe falliva.
Script `scripts/research/r0701_xs_residmom.py` (riusa l'harness collaudato `xsec_v2_nonmom`:
load_matrix, xs_engine — uguaglianza verificata maxdiff 0 —, deflated_sharpe, StrategyPortfolio).
Distinzione dal già-testato: IREV (idio-**reversal**, sgn=1) era FALLITO nel filone C; IMOM di
xsec_v2 usava il mercato equal-weight come fattore, B=60 fisso, senza blend ed era solo riferimento.
Qui fattore = **BTC** (come STATARB-RESID, BTC escluso dal cross-section), B in griglia, blend
z-score, probe con gate di dispersione p30 (parità strutturale con XS01), **selezione in-sample-only**.
## Setup
- Universo: 19 major HL certificati (`data/raw/hl_*_1d.parquet`), 913 giorni [2024-01-01 → 2026-07-01],
barre vol=0 escluse. Fee 0.10% RT/gamba + sweep fino a 0.30%.
- Griglia (57 trial): β-window {60,90,120} × lookback {blend[30,90], 30, 90} × k {3,5} × H {5,10,20}
+ 3 probe gated (blend, k5, H10, gate dispersione p30). Cella scelta col solo Sharpe **pre-2025**;
hold-out 2025-01-01 bloccato. Causalità: prefix-check ok (max_tail_diff 0).
- Baseline (stessa finestra, sleeve attivi): XS01 FULL Sh **1.38** / HOLD **1.51** / DD 11%;
book 4-sleeve FULL 2.12 / HOLD 2.21.
## Esito — collasso OOS della cella in-sample e nessuna cella competitiva
Cella scelta in-sample: **B60 L30 k3 H20** (IS Sh 2.00):
```
FULL +0.67 | HOLD 0.24 | DD 35% | per-anno 2024 +54% / 2025 15% / 2026 +5%
corr→XS01 +0.54 | corr→TP01 0.09 | DSR 0.510 (57 trial) | fee-sweep ok (F+0.59 a 0.30%)
small-cap $600/min$5: haircut ≈ 0 (ma book multi-gamba → STAT-MODE comunque, come XS01)
```
Portafoglio (il bar del mandato):
```
BASE 4-sleeve FULL 2.12 DD 7.8% | HOLD 2.21 DD 3.5%
SOSTITUZIONE XS01 → cand FULL 2.01 (0.10) | HOLD 1.18 (1.03) ← distrugge l'hold-out
AGGIUNTA 5° sleeve @10/15% HOLD 0.31 / 0.53 ← diluisce
MARGINAL vs BOOK: corr +0.23, uplift@10% full 0.04 / hold 0.31, multi-cut 2025 0.31 / 2026 0.14
```
**Il verdetto non dipende dalla selezione** — aggregato delle 57 celle:
- FULL max **+0.94**, HOLD max **+0.82** (mediane +0.52/+0.33): *nessuna* cella si avvicina a XS01
(1.38/1.51), nemmeno cherry-pickando sull'hold-out.
- corr→XS01 in [+0.47, +0.71], mediana **+0.62**: è momentum XS01 travestito (la residualizzazione
vs β·BTC sposta poco: lo z-scoring cross-sectional di XS01 già rimuove implicitamente il mercato).
- Solo 4/57 celle passerebbero un gate "diversificatore" (|corrXS|<0.6, FULL>0.5, HOLD>0), tutte con
FULL ≤0.68 e IS incoerente.
- **corr(Sharpe in-sample, Sharpe hold-out) attraverso le celle = 0.37**: l'in-sample è
anti-predittivo dell'OOS su questa griglia — la firma del rumore, non di un edge con plateau.
## Perché la tesi è falsa (3 righe)
1. La breadth non ha alzato il DSR (0.51 ≪ 0.95) perché non c'è edge da deflazionare: il residuo
vs β·BTC sul cross-section degli alt NON eredita l'edge del 2-gambe ETH−β·BTC — quel lead vive
nella coppia specifica ETH/BTC (i due asset profondi/cointegrati), non nel long-tail degli alt.
2. Cross-sectional, la residualizzazione è quasi un no-op: demeaning z-score (XS01) ≈ togliere il
mercato → il candidato è una variante rumorosa di XS01 (corrXS mediana 0.62) con meno segnale.
3. In-sample = solo 2024 (~1 anno utile dopo il warmup): la selezione onesta atterra su una cella
2024-lucky che perde nel 2025-26 — l'ennesima conferma del gate selection-on-holdout.
## Verdetto: **SCARTATO/REDUNDANT.** Nessuno sleeve, nessun forward-monitor.
Coerente con il filone momentum-structure (2026-06-29): l'edge di XS01 sta nella sua struttura
specifica (blend + gate dispersione), e le sue varianti — incluso l'idio-momentum vs BTC — sono
REDUNDANT o peggio. Chiude anche il cerchio su STATARB-RESID: il meccanismo residuo-momentum resta
valido SOLO come coppia ETH/BTC (forward-monitor `paper_statarb`), non generalizza al cross-section.
Nessun impatto sul book live; config canonica invariata.
Script: `scripts/research/r0701_xs_residmom.py` (non committato, prefisso r0701_xs).
+69
View File
@@ -0,0 +1,69 @@
# 2026-07-01 — Stagionalità CROSS-SECTIONAL sull'universo Hyperliquid → SCARTATO
## Ipotesi
Effetti calendario **relativi tra i 19 alt major HL** (weekday tilt, turn-of-month,
pattern weekend→lunedì nel cross-section), long/short market-neutral. L'angolo è diverso
dal prior art già morto (trackF seasonality BTC/ETH = buy&hold travestito;
crypto_weekend_signal/overnight = look-ahead; open_drive = artefatto calendario ucciso da
`day_boundary_robust`): essendo il segnale **demeaned cross-section**, il buy&hold
travestito è strutturalmente escluso… *in prima battuta* (vedi diagnostica beta sotto).
## Metodo (test statistico PRIMA della strategia)
Script `scripts/research/r0701_xs_seasonal.py`. Dati `data/raw/hl_*_1d.parquet`, 19 major
XS01 (lista `src/portfolio/sleeves.XS_UNIVERSE`), **912 giorni** (2024-01-02 → 2026-07-01,
~2.5 anni), 0 barre vol=0. Ritorno relativo x[t,a] = r[t,a] media cross-section del
giorno. Per ogni bucket calendario: **tilt bucket-specifico** per asset = media di x nel
bucket tilt incondizionato dell'asset (isola il calendario dal drift relativo generico).
**Persistenza split-half**: Spearman rank-corr del vettore tilt (19 asset) H1 vs H2, per
bucket. **Null**: 2000 permutazioni delle etichette entro ciascuna metà → distribuzione del
**max-statistic** (controlla il multiple-testing sui bucket e il confound "alpha persistente
dell'asset"). Soglia severa p<0.05 sul max. Regola del mandato: senza persistenza →
**nessun backtest**.
## Numeri
| Test | Statistica | Null 95° pctl | p | Esito |
|---|---|---|---|---|
| [A] Weekday (7 bucket) | max rank-corr **+0.511** (Wed); Mon +0.38, Sun +0.37, Fri +0.04 | +0.604 | **0.159** | FAIL |
| [B] Weekend vs feriali | +0.268 | +0.428 | **0.178** | FAIL |
| [C] Turn-of-month (±2gg) | +0.221 | +0.451 | **0.226** | FAIL |
| [D] IC weekend→lunedì | FULL 0.053 (t=1.94); H1 0.006 (t=0.17) vs H2 0.100 (t=2.4) | — | non persiste | FAIL |
Con ~65 osservazioni per weekday per metà, lo std di una rank-corr su 19 asset è ~0.24 → il
max-su-7 sotto puro rumore arriva a ~0.60: il +0.51 reale è *dentro* il rumore.
## Diagnostica: perché tutte le rank-corr weekday sono positive?
Non è tilt idiosincratico, è il **canale-beta**: il mercato EW ha un pattern weekday di
*livello* che si ripete nelle due metà (Sun 42/30 bps, Sat +26/+25, Mon +35/+32) e la
dispersione di beta (0.53→1.26) lo proietta nel cross-section — rank-corr(tilt, beta):
**Sun 0.642**, Sat +0.325, feriali ~0. Cioè la poca "persistenza" visibile è l'effetto
weekday di MERCATO (dominio trackF, già scartato come buy&hold travestito) che rientra
dalla finestra via beta. Un eventuale L/S "weekday tilt" sarebbe stato in gran parte un
book short-beta-la-domenica — non un edge calendario cross-sectional.
## Note sui gate non raggiunti
- **day_boundary_robust** (obbligatorio per ogni effetto calendario): NON eseguibile sui
19 alt — i dati HL locali sono **solo 1d**, il confine giorno non è ri-tagliabile. Anche
con persistenza, il verdetto massimo sarebbe stato LEAD-forward *condizionato* a barre
orarie HL, mai sleeve. Con lo SCARTATO allo step 1 il punto è moot, ma resta il vincolo
per chiunque riapra il filone.
- **Fee**: mai arrivati al backtest; nota strutturale: un weekday-tilt L/S ribilancia ogni
giorno (weekday di domani ≠ oggi) → turnover ~2×gross/die, morte-per-fee quasi certa a
0.10% RT anche con edge lordo.
- **DSR**: non calcolato (nessuna cella backtestata — il branch strategia esiste nello
script ma non si attiva senza persistenza).
## Verdetto
**SCARTATO** allo step statistico, senza backtest. Tre motivi: (1) nessuna persistenza
split-half sopra il null permutato (p 0.160.23 su tutti i test, IC weekend→lunedì non
persiste tra le metà); (2) la sola struttura visibile è il canale-beta di un pattern
weekday di mercato — la stessa famiglia già scartata in trackF, non un effetto
cross-sectional; (3) ~2.5 anni × effetto calendario × turnover giornaliero = rumore +
morte-per-fee anche nello scenario migliore.
File: `scripts/research/r0701_xs_seasonal.py` (deterministico, seed 20260701, ~2 min).
@@ -0,0 +1,132 @@
# 2026-07-02 — Audit anchor timing-luck su XS01 e SKH01 (chiusura del pendente dell'ondata TIMING)
**Contesto.** L'ondata TIMING+CRT dello stesso giorno (`2026-07-02-timing-crt-wave.md`) ha stabilito
che l'hold-out di TP01 (0.31) è la **migliore delle 24 ancore daily possibili** (mediana 0.04,
P~0.86 di uno spike così per caso). Lo scettico ha flaggato l'audit come **pendente** sugli altri
due sleeve a ribilanciamento/griglia ancorata: **XS01** (fase del ciclo di ribilanciamento H=10
giorni) e **SKH01** (offset della griglia dual-TF 230m/690m, `origin='epoch'`). Questo diario
chiude entrambi. Due agenti indipendenti, metodologia identica allo scettico TP01
(`r0702_skeptic_offset.py`): ancore dichiarate **a priori**, parametri **identici** in ogni
fase/offset (zero tuning per-ancora), **sanity replica bit-exact** della sleeve canonica prima di
qualsiasi numero, block-bootstrap congiunto per P(spike).
**Script:** `scripts/research/r0702_anchor_xs01.py`, `scripts/research/r0702_anchor_skh01.py`.
Nessun file di produzione toccato dagli agenti (solo docstring/narrativa aggiornate a valle, qui).
---
## Verdetto in una riga
**Il finding TP01 si replica su TUTTI e tre gli sleeve ancorati — su SKH01 più forte che su TP01 —
e i numeri headline del book (HOLD 2.46) sono in parte best-of-anchors.** Ciò che sopravvive
de-luckato: XS01 resta un diversificatore ammissibile (ensemble di fase FULL 1.25 / HOLD 1.31 /
DD 11%); SKH01 resta ADDS (uplift del blend positivo a **tutte** le 23 fasi, corr ~0.08) ma i suoi
numeri headline e — cruciale — **il gate DD<30% che l'ha selezionata** non reggono alla fase
mediana. Stima onesta de-luckata del book 5-sleeve: **HOLD ~1.92.1, FULL ~2.02.2** (vs 2.46/2.24
dichiarati).
---
## XS01 — fase del ciclo di ribilanciamento H=10 (10 fasi)
Sanity: replica di `_xsec_returns` con parametro `phase`**max|Δ|=0.0 bit-exact** vs la canonica.
NB: la canonica sui dati di oggi fa FULL 1.367/HOLD 1.479/DD 10.8% (i dichiarati 1.50/1.71/11%
erano del 2026-06-19; deriva dati, confronto interno coerente).
| metrica | min | mediana | max | canonica (f0) | pctl f0 |
|---|---|---|---|---|---|
| Sh FULL | 0.41 | 1.21 | 1.64 | 1.37 | 85° |
| Sh HOLD | 0.15 | 1.17 | 1.71 | 1.48 | 65° |
| maxDD | 10.0% | 15.5% | 29.1% | 10.8% | **15°** |
- **Firma diversa da TP01: la fortuna NON è nell'hold-out** (65° pctl, non estremo) **ma nel
DD** (15° pctl: il "DD 11%" dichiarato è uno dei più bassi delle 10 fasi; la fase tipica fa
~15.5%, la peggiore 29%) e nel FULL (85°).
- **Bootstrap**: spike osservato +0.38 su hold-out; **P(una fase qualsiasi ≥ +0.38) = 0.910.94**
(block 10/20/40, B=4000) → la canonica è il massimo atteso di 10 stime correlate (corr mediana
fra fasi 0.71).
- **Ensemble 10 fasi** (lens di reporting onesta): **FULL 1.25 / HOLD 1.31 / DD 10.9%** — la
diversificazione di fase riporta il DD al livello canonico con Sharpe ≈ mediana.
- **Book 5-sleeve**: HOLD 2.44 (canonica) → 2.26 (fase mediana) → 1.75 (peggiore, marginalmente
PEGGIO del book senza XS01 = 1.77) → 2.35 (ensemble). **Eredità di fortuna ~+0.17 HOLD.**
FULL quasi insensibile (2.102.28).
- **Gate di ammissione a fase mediana**: FULL 1.08 / HOLD 1.10 / DD 20.7% — **i numeri dichiarati
non reggono** (Sharpe 0.3/0.6, DD raddoppia). **La decisione però regge**: nessuna fase
negativa, 8/10 con FULL≥1.0, book migliore a ogni fase tranne la peggiore (dove è neutro) →
slot da diversificatore @15% difendibile; cambia la NARRATIVA (banda/ensemble, non 1.50/1.71/11%).
- **Caveat**: storia ~2.5 anni (CI95 Sh HOLD canonica **[0.04, +2.72]** — nessuna stima puntuale
affidabile); dimensione **ora-del-giorno non testabile** (solo barre 1d HL: resta luck residua
10 fasi × 24 ore non certificabile); XS01 è STAT-MODE → impatto su reporting/gate, non sul book
live Deribit.
## SKH01-V2-DD — offset della griglia dual-TF 230m/690m (23 offset × 30m)
Sanity: offset 0 riproduce `_skyhook_returns()` **bit-exact** (max|Δ|=0.0 su 2666 giorni); maxDD
harness BTC 21.4%/ETH 27.4% = identici al diario di ammissione; riuso diretto di
`skyhook_entries`/`htf_features`/`merge_htf_to_ltf` con `resample(origin='epoch', offset=...)`,
confini 690 ⊂ 230 verificati a ogni offset.
| metrica (book 50/50) | min | mediana | max | off0 (canonico) | pctl off0 |
|---|---|---|---|---|---|
| Sh FULL | 0.78 | 1.05 | 1.48 | 1.46 | 93° |
| Sh HOLD | 0.44 | 0.93 | 1.64 | 1.64 | **98° (max)** |
| minFull (2 asset) | 0.35 | 0.67 | 1.01 | 0.99 | 93° |
| minHold | 0.10 | 0.39 | 1.25 | 1.25 | **98° (max)** |
| blend 0.75·TP01+0.25·SKH, HOLD | 0.49 | 0.72 | 1.16 | 1.16 | **98° (max)** |
- **È uno spike, non un plateau**: gli offset adiacenti ±30m crollano (book HOLD 1.64 → 0.88 a
off30). Firma identica all'ancora TP01, ma **più forte**: il canonico è al 9398° pctl di OGNI
metrica; minHold +1.26 e blend "0.31→1.17" sono il **massimo dei 23**.
- **Gate DD<30% — il criterio per cui V2-DD fu selezionata — FALLISCE in 15/23 offset**
(mediana DD: BTC 23.5%, ETH **29.2%** — pass per 0.8pt; peggiori 35.9%/42.1%). Il margine ETH
"già sottile" (27.4%) è in realtà **sotto la mediana di fase**: la proprietà DD<30% è in larga
parte fortuna di fase.
- **Ciò che regge de-luckato**: **uplift del blend positivo a TUTTE le 23 fasi** (min +0.18,
mediana +0.42, canonico +0.86) e corr→TP01 in [0.05, 0.11] a ogni fase → il verdetto
ADDS/diversificatore sopravvive, ridimensionato. Alla fase peggiore invece non regge
(minHold 0.10, DD fail).
- **Bootstrap**: spike blend +0.44; **P(un offset qualsiasi ≥ +0.44) ≈ 0.70** (block 10/20/40);
P(uplift≤0) = 0.030.04 → l'uplift è reale, l'ampiezza è best-of-23.
- **Book 5-sleeve**: HOLD 2.44 (off0 = max dei 23) → mediana **1.96** → ensemble 2.12; FULL 2.24 →
mediana 2.01. **Eredità di fortuna ~+0.5 HOLD** — molto più dei ~+0.10 dell'ancora TP01.
- **Rilevanza LIVE (SKH = 25% del book Deribit reale)**: il cron è orario, i confini 230m no →
ritardo uniforme {0..50} min (media 25). Re-sim del path reale (fill al close 5m del prossimo
run orario, exit SOFTWARE come da `book.py`): book 50/50 **FULL 1.46→1.19, HOLD 1.64→1.15,
DD 18.1%→24.6%**. La maggior parte del gap è l'assunzione fill-al-livello del backtest, non il
ritardo orario in sé: nei crash le barre 230m **gappano attraverso lo SL software** (short
sl2% modellato 2% → realizzato **11/23%**: ETH 2021-05-19, 2021-01-04, COVID 2020-03-13).
Il disaster-SL on-book 30% copre solo la coda estrema della posizione netta.
- **Caveat**: equity daily-step (lens Sharpe, come il canonico); l'ensemble di offset NON è
eseguibile live (gira una sola griglia) → serve solo come stima de-luckata; sim hourly senza
slippage/parziali.
## Book 5-sleeve — stima de-luckata congiunta (approssimata)
Ogni audit varia UN solo sleeve (gli altri restano canonici), quindi gli effetti non si sommano
esattamente; ordine di grandezza dell'eredità di fortuna sull'HOLD 2.46: TP01-ancora ~+0.10,
XS01-fase ~+0.17, SKH01-offset ~+0.5. **Stima onesta: HOLD ~1.92.1, FULL ~2.02.2, DD ~6%
(invariato)** — il book resta largamente positivo e diversificato a ogni combinazione testata,
ma il 2.46 è un massimo di ~24×10×23 configurazioni di ancora, non la stima centrale.
## Regole codificate in narrativa (CLAUDE.md aggiornato)
1. **I numeri headline di OGNI sleeve ancorato si citano con la banda d'ancora** (o come ensemble
di fase): vale già per TP01, ora anche per XS01 e SKH01 e per l'HOLD del book.
2. **Un gate di selezione (es. DD<30%) va verificato sulla banda di fase, non sull'ancora
canonica** — SKH01-V2-DD fu selezionata per una proprietà che tiene in 8/23 fasi.
3. Il candidato gate `anchor_luck_band()` in altlib sale di priorità: tre sleeve su tre ancorati
mostrano la stessa firma, e il deflated-Sharpe non conta questo multiple-testing.
## Follow-up (non eseguiti, da valutare)
- **Rivalutare il peso 25% di SKH01 nel book live Deribit**: l'aspettativa onesta della gamba SKH
è ≈ fase-mediana + path orario (blend HOLD ~0.72, non 1.17) con gap-through-stop reale nei
crash. L'uplift resta positivo a ogni fase → non c'è motivo di rimuoverla, ma il sizing fu
deciso su numeri best-of-23.
- Cadenza cron a 230m (o trigger sui confini di barra) per chiudere il gap di path orario.
- `anchor_luck_band()` in `altlib.py` come gate standard per candidati ancorati.
- Audit ora-del-giorno di XS01 quando esisterà un feed HL intraday certificato.
**Stato:** XS01/SKH01/pesi/book live INVARIATI (nessun cambio operativo da questo audit — è un
audit di reporting e di gate, e ogni cambio pesi dovrà passare `weights_tilt_null` +
selection-on-holdout). Test suite 168 verdi. Script audit committati.
@@ -0,0 +1,168 @@
# 2026-07-02 — Ondata "video claims": Elliott (3 filoni), Albimarini (2), capital scaling 2-5k
**Contesto.** Su richiesta: meccanizzare e testare le claim falsificabili di 4 video didattici
italiani (3 sulle onde di Elliott, 1 ricostruzione della "strategia Albimarini" — double diagonal
a credito su SPX), più l'audit dei muri di scala visto l'annuncio che il conto live verrà portato
a **2.000-5.000 $**. Sei agenti indipendenti, harness altlib, fee 0.10% RT, hold-out 2025-26 mai
usato per selezionare, banda d'ancora obbligatoria (regola dello stesso giorno). Nessun PASS →
nessuno scettico necessario. Nessun file di produzione toccato.
**Scripts:** `scripts/research/r0702_ell_{rangecycle,fibconfluence,channel}.py`,
`r0702_alb_{structure,claims}.py`, `r0702_capital_scaling.py`.
---
## Verdetto in una riga
**Elliott 3/3 SCARTATO** (range-cycle = rumore 0/24 Bonferroni; canale = Donchian travestito che
non batte il Donchian; Fibonacci = effetto posizione, non numeri magici). **Albimarini 2/2
SCARTATO** (la diagonale non domina nemmeno nel modello ed è fee-negativa su Deribit; il track
record dichiarato è consistente con zero skill al 20-45%, e va in rovina in ogni finestra storica
con una coda). **Capital scaling: l'unico vincolo vero a 2-5k è il cap $300/asset** — da alzare a
equity/2 al momento del deposito; tutto il resto si scioglie da solo o resta fuori comunque.
---
## ELL-A — "range-cycle" (onda 1 compressa → onda 3 ampia): RUMORE
Claim di Frost&Prechter/Larry Williams meccanizzato senza conteggio soggettivo: 2 definizioni di
segmento (flip TSMOM, zigzag k·ATR causale), impulso iniziale (N∈{3,5,10} barre, range/ATR
pre-segmento) vs ampiezza del seguito (claim A) e troncatura dell'ultimo terzo (claim B).
BTC/ETH 1d+1w, Spearman con null permutato (2000), Bonferroni m=24.
- **0/24 celle significative** (miglior p raw: A 0.061, B 0.058; Bonferroni 1.00 ovunque, FULL e
pre-2025). 7-9/12 celle col segno "giusto" = moneta su celle correlate.
- **Nessuna cella weekly regge a tutte e 7 le ancore** (p 0.02-0.05 di singole ancore = il
timing-luck d'ancora che da oggi non crediamo più; alcune celle flippano segno tra ancore).
- **Artefatto previsto e smascherato**: normalizzando col range POST-impulso la correlazione
diventa negativa per pura meccanica del denominatore — chi "conferma" il claim così misura
l'artefatto.
- Coerente con le famiglie di compressione già fallite nello sweep 104 (BRK08 NR7, BRK10
BB-squeeze, CMB05, VOL12). Campione strutturalmente piccolo (mediana 78 segmenti daily, ~30
weekly). Strategizzazione correttamente non eseguita (statistica prima della strategia).
⚠️ **LEZIONE PANDAS (gemella di `view("int64")` del 2026-07-01):** `resample("7D",
origin=...)` in pandas 2.x **ignora silenziosamente `origin`** ("D" non è Tick-like, solo un
RuntimeWarning) → tutte le ancore weekly uscivano identiche e la banda d'ancora sarebbe stata una
banda finta di larghezza zero. Serve **`"168h"`**. Da ricordare per ogni futura banda d'ancora
settimanale.
## ELL-B — Confluenza di Fibonacci: EFFETTO POSIZIONE, non numeri magici
Zigzag causale non-repainting (livelli usabili solo dalla conferma del pivot, causality_ok
max_tail_diff=0.0), tocco fresh intrabar, reazione ATR-normalizzata a 5/20 barre, ~24k/14k tocchi
IS a 1h. **Tre null in scala di durezza**: (A) rapporti uniformi casuali, (B) ri-sorteggiati per
swing, **(C) location-matched** (Fib ± jitter 2-8%: "0.618 è speciale vs 0.58/0.66?").
- Vs null A/B i Fib sembrano buoni (pctl 0.82-1.00) — **vs null C crollano a 0.39-0.68 = puro
caso**. L'apparente vantaggio è interamente l'effetto di *posizione* (i rapporti Fib cadono in
zone mediamente decenti del range), non dei numeri di Fibonacci.
- **Confluenza** (2 griglie entro 0.25·ATR): FAIL 4/4 celle — non reagisce più delle confluenze
di rapporti casuali. Break-rate al tocco ~46-50% (moneta); su 1h i Fib veri vengono bucati più
spesso del placebo.
- In assoluto comunque nulla da tradare: +2.6/+5.6 bps a 5 barre (fee = 10 bps), negativo a 20
barre e su 1d. Strategizzazione correttamente saltata da gate a priori.
- **Pattern di test riusabile**: il null location-matched è IL test giusto per ogni claim su
"livelli speciali" (Fib, pivot, round numbers) — separa la qualità della posizione dal numero.
## ELL-C — Tecnica del canale (Ftaonline): DONCHIAN TRAVESTITO
Implementazione onesta completa (zigzag causale, vincolo onda-2>origine, canale 0→min-2 con
parallela dal max-1, entry al close della barra fuori, target 1.618×, stop=invalidazione,
speculare short, 27 celle TF×k×varianti).
- **family_honest boccia**: cella scelta in-sample (1h k4 long, IS 1.40) → hold-out **0.87**;
DSR 0.685 < 0.95; earns_slot=False. Marginale vs TP01: uplift positivo solo 2020-21 =
beta-trend del toro (corr 0.35).
- **Il Donchian banale a pari geometria/frequenza eguaglia o batte il canale a ogni TF**
(corr 0.43-0.53): vincoli d'onda + 1.618 non aggiungono nulla. Il segnale-2 ("onda 5") è
quasi inerte (+0.00-0.07).
- **Target 1.618 = geometria, non magia**: toccato con la frequenza di distanze identiche
piazzate a caso (5/6 celle p>0.18). Claim discriminante "in canale = correttivo":
direzionalmente sensata, mai significativa (p 0.08-0.45); l'uscita dal canale è momentum da
breakout, già in casa.
- **Anchor-luck di nuovo**: la miglior cella 4h fa hold 1.54 all'ancora 00:00, la migliore delle
4 (banda [0.35, 1.54]) — quarta conferma della firma nello stesso giorno.
## ALB-A — Double diagonal "Albimarini" su BTC/ETH: NON DOMINA, niente LEAD
Portata su Deribit (che ha le scadenze giornaliere per la long T+1) col motore DVOL di VRP01
(riprodotto esatto: 1.09/0.59/11.8%), strike scalati in vol (2σ/5g ≈ il "9% su SPY"), gate
IV-rank canonico non riottimizzato, 2021-26 con LUNA/FTX/2022, banda skew f∈{0.6,0.8,1.0,1.3}.
- **Il claim strutturale del corso è falsificato**: gated, l'iron condor stessa-scadenza batte la
diagonale a ogni f (1.05 vs 0.95 a f=1.0), Δ negativo ogni anno. La long T+1 è **assicurazione
di coda quantificata** (+12/+33 bps nei 5% peggiori vs ~1 bps di costo medio), non dominanza.
- **Senza gate tutte le strutture perdono** a f≤1.0 → terza conferma indipendente: nel VRP
l'alpha è il filtro di regime, non la vendita di premio. VRP01 resta superiore su tutta la
banda f (CAGR 8.3% vs 1.4%).
- **Il meccanismo dell'illusione riprodotto in vitro**: celle z≥2.5 = 0 perdite su 142 trade,
Sharpe 5.9-∞ (il punto cieco CC01 in forma opzioni); la best in-sample (7.88) crolla a 0.51
in hold-out. Seconda occorrenza del pattern "0-perdite = Sharpe implausibile" in una settimana
**si rafforza il caso per il gate `implausible_sharpe` in altlib** (raccomandato dal filone
CC01, ora con 2 casi d'uso).
## ALB-B — Audit dei numeri dichiarati (28 trade, 82%, PF 5.16, "420% annuo"): ZERO SKILL
SPY 1d 1996-2026 (parquet già in repo dal filone GTAA) + BTC/ETH certificati; MC 200k; replay
claim-anchored calibrato per riprodurre esattamente il video.
- **L'82% è incoerente col "deep OTM" in entrambe le letture**: strike a 9% → win strutturale
99.5% (P(|move 4td|>9%) = 0.53%, fat-tail 88× la normale); 82% letterale → strike a 2.63% ≈
1.2σ, non deep. In entrambi i casi win-rate = delta venduto.
- **P(track record del video senza edge) = 20-45%**; il **78.6%** di tutte le finestre 6-mesi
1996-2026 lo avrebbe prodotto (lettura deep). Il PF ~5.1 esce per costruzione quando la coda
non è campionata.
- **Replay attraverso le code reali**: rovina nel 1998/2002/2020 col sizing 1→4 dichiarato;
perfino regalando +20% di premio, 1 contratto fisso fa CAGR +4.1% con maxDD 95%. La stessa
semestrale del video annualizza a +73-77%, non 420%. Su BTC/ETH a EV=0: +12%/DD68% e
+0%/DD88%.
- **La diagonale lascia passare il 12-40%** della perdita naked in un 10% overnight (dipende dal
vol-spike). Su Deribit la double diagonal è **fee-negativa a qualsiasi size** (fee 8 gambe =
194-221% del theta raccolto a questi strike): il capitale sposta solo il muro di margine.
- Che cosa distingue VRP01 (sopravvissuto allo stesso tipo di audit): defined-risk per
costruzione, gate IV-rank che ribalta l'hold-out, sizing fisso — l'opposto esatto su tutti e
tre gli assi.
## Capital scaling 600 → 2000/3500/5000 (r0702_capital_scaling.py)
| capitale | TP01 haircut Sh | book investibile % (cap $300) | K=2 exec-turn % | statarb haircut | spread ETH conc. | XS01 exec-turn % |
|---|---|---|---|---|---|---|
| 600 | +0.010 | 100.0% | 90.5 | 0.004 | 0 | 56.9 |
| 2000 | 0.002 | 85.9% | 95.7 | 0.000 | 3 | 87.4 |
| 3500 | 0.002 | 63.7% | 97.5 | 0.000 | 5 | 89.3 |
| 5000 | 0.002 | **48.9%** | 98.2 | 0.000 | 7 | 90.8 |
- **Il vincolo dominante è UNO: `max_notional_per_asset_usd`=300.** A 5k il book girerebbe a
metà del target (cap binding nel 39-46% delle barre). **Proposta config (da applicare AL
deposito): cap = equity/2** → $1000 a 2k, $1750 a 3.5k, $2500 a 5k. `min_order_usd`=5 va
LASCIATO (il fee drag risale solo al tetto noto ~0.4%/anno; l'isteresi artificiale costerebbe
più in lag — lezione ondata timing).
- Min-order/haircut TP01 ≈ 0 a ogni capitale (conferma indipendente del "74% ordini" a 600 →
39-43% saltati a 5k, tutti micro). Statarb: haircut 0 da 2k (il muro era l'edge, non la scala).
- Tranching K=2: matematicamente regge a 5k (98.2% turnover-tranche), ma il blocco vero resta il
feed intraday fuori path certificato + beneficio solo di varianza → **non cablare**.
- Opzioni: il muro d'ingresso ETH cade a ~2.6k (3-7 spread concorrenti a 2-5k, fee solo 5-6% del
credito grazie al cap 12.5%; rettifica vs ALB-A: margine spread ETH oggi $66-76). BTC granulare
da ~$4.7k. **La regola "niente short-vol da modello" non decade col capitale.**
- XS01: soglia ~20k confermata (a 5k il 9%+ del turnover non esegue). CC01: il capitale non è
mai stato il muro (fuori per ragioni strutturali).
- **Aspettativa onesta**: col CAGR de-luckato del book live (10-15%, audit SKH01): **2k ≈
€0.55-0.82/giorno; 5k ≈ €1.37-2.05/giorno**. Il salto di capitale cambia cosa è eseguibile,
non l'ordine di grandezza del ritorno (€50/g ≈ 130k, invariato).
## Lezioni
1. **Il null giusto smonta il livello "speciale"**: location-matched (ELL-B) per i livelli,
Donchian-equivalente (ELL-C) per i pattern, EV=0-calibrato (ALB-B) per i track record. In
tutti e tre i casi il claim sopravviveva ai null ingenui e moriva su quello giusto.
2. **`resample("7D", origin=...)` ignora origin in pandas 2.x** → usare `"168h"` per le bande
d'ancora settimanali.
3. Secondo caso "0-perdite = Sharpe implausibile" (celle deep-OTM di ALB-A, dopo CC01) → il gate
`implausible_sharpe` in altlib sale in coda ai lavori insieme ad `anchor_luck_band`.
4. L'anchor-luck è comparso spontaneamente anche in questa ondata (celle weekly ELL-A, 4h ELL-C)
— la regola della banda d'ancora sta già pagando.
**Stato:** nessun nuovo sleeve, nessun forward-monitor aggiunto, book live INVARIATO. Un'azione
operativa pende sul FUTURO deposito: alzare `max_notional_per_asset_usd` a equity/2 (decisione
utente al momento del funding). Test suite verde. 6 script committati.
+186
View File
@@ -0,0 +1,186 @@
# 2026-07-02 — Ondata timing + CRT (8 filoni multi-agente + scettico): 0 nuovi sleeve, 1 finding strutturale (anchor timing-luck di TP01)
**Obiettivo (goal utente):** "cerca su Deribit altre strategie profittevoli magari con timing
differenti oltre a quelle già valutate". Prima ondata interamente dedicata al TIMING: dopo che le
ondate precedenti hanno coperto segnali (104 famiglie alt-sweep), universi (HL cross-sectional),
opzioni (VRP/gamma) e pesi (EW-STR/weights_tilt_null), qui si attacca la dimensione rimasta: *quando*
si campiona, si decide e si esegue. Su richiesta utente, seconda tranche dedicata alla **CRT (Candle
Range Theory)**, il pattern sweep-and-reclaim a 3 candele di scuola ICT/Wyckoff.
Inventario preventivo per non rifare lavoro: SEA01-09 coprivano già hour-of-day, day-of-week,
weekend, turn-of-month, sessioni; l'onda intraday 2026-06-21 il sub-daily (con il lead prevday in
forward-monitor); `tp01_lowfreq` i TF 4h/12h/1d. Angoli NON coperti individuati e testati: clock a
eventi, calendario scadenze Deribit, ancora di ribilanciamento, clock più lenti del daily, velocità
del segnale regime-condizionata — più i 3 tagli CRT (base, multi-TF, contesto).
**Esito: 7 filoni FAIL + 1 finding strutturale confermato dallo scettico (timing-luck dell'ancora
di TP01). Nessun nuovo sleeve. Nessuna modifica al book live. Il soffitto ~1.3 direzionale
BTC/ETH resta intatto anche cambiando il clock.**
---
## Filone 1 — Event-clock bars (tempo-informazione): FAIL
`scripts/research/r0702_eventclock.py`. Volume/vol/range bars da 5m certificato (soglie causali
EWMA-90g shift(1), barre medie 4h/12h/24h), 5 strategie (TSMOM multi-orizzonte, Donchian 10/30d,
EWMA-cross), 45 celle event + 15 controlli wall-clock = 60 trial.
- Null decisivo (stesso segnale, stessa frequenza, wall-clock): event batte wall su ENTRAMBE le
finestre solo in **4/45 coppie (9%)** — rumore. Pattern incoerenti (EWMA event vince IS 9/9 ma
perde HOLD 9/9; Donchian-30d l'esatto opposto = selection-on-holdout in agguato).
- Cella scelta in-sample (volume-bars 24h, DONCH-10d): IS 1.45 → **HOLD 0.46** (2025 7.4%,
2026 18.1%). Marginal NEUTRAL (corr 0.74 a TP01 = trend travestito), earns_slot_honest=False.
- Executability comunque assente: in alta attività le barre "24h" chiudono in 3.6-4.6h (p5) → il
cron orario le eseguirebbe in ritardo; i clock 4h richiedono monitoraggio sub-orario.
**Lezione: campionare a tempo-informazione non normalizza nulla di monetizzabile sul feed
certificato; il clock non è la dimensione dove vive l'edge.**
## Filone 2 — Calendario scadenze Deribit: FAIL (0/24 celle)
`scripts/research/r0702_expiry_calendar.py`. Griglia dichiarata a priori: 4 finestre × 3 tipi expiry
(weekly ven 08:00 UTC / monthly ultimo ven / quarterly) × 2 asset, Bonferroni |t|≥3.08; 3 null
(placebo weekday, anchor-shift ±2/4h, permutation 500 calendari).
- Nessuna cella passa Bonferroni (max |t| 2.69). Il post-expiry drift monthly (+0.61/+0.77%) ha il
segno della teoria ma: non batte il placebo giovedì, permutation pctl 84-95 (non estremo), si
INVERTE sul quarterly (dove l'OI massimo dovrebbe amplificarlo) ed è guidato dal 2019 → rumore.
- Unica cella robusta ai null: weekly [-24,0) negativa (gio→ven), ma è indistinguibile dal
day-of-week (famiglia SEA già morta) e netta fee fa Sharpe ~0.08 (52 eventi/anno × 0.10% RT).
- Best-in-sample tradabile: IS +0.90 → **HOLD 0.52**; DSR 0.230 su 24 trial.
- Nota tecnica: colpito ESATTAMENTE il pitfall pandas 2.x documentato (epoca in secondi da
`date_range` su tz-aware → matching vuoto); fix con epoca ms esplicita. La lezione 2026-07-01
ha pagato.
## Filone 3 — Anchor timing-luck di TP01 + tranching: FINDING (verificato dallo scettico)
`scripts/research/r0702_tp01_offset.py` + scettico indipendente `r0702_skeptic_offset.py`
(ricostruzione bit-exact con codice diverso, zero riuso; h=0 riproduce `tp01_baseline_daily`
esattamente; vol-target ricalcolata per ancora; nessun look-ahead — verificato con troncamenti).
**Claim 1 CONFERMATO: l'hold-out 2025-26 "Sharpe 0.31" di TP01 è in larga parte fortuna
dell'ancora 00:00 UTC.** La stessa strategia CANONICAL alle altre 23 ancore orarie: HOLD mediano
**0.04**, banda **[0.13, +0.30]** — e il massimo è proprio h=0 (98° pctl; in-sample era al 10°:
sfortunato lì, fortunato qui). Spike, non plateau (h=23/h=1 fanno 0.13/0.04). Bootstrap dello
scettico: P(una qualsiasi ancora mostri uno spike ≥ così) = **0.86** — è il massimo atteso di 24
stime correlate, non un'anomalia; best-anchor per finestra annuale ruota a caso (16→23→21→12→5→0,
Spearman ≈ 0, nessuna seasonality d'ancora). Formulazione corretta (NON "il vero hold-out è 0.07",
che ha CI95 [1.2,+1.4] su 547 giorni): **l'hold-out 2025-26 non risolve l'edge di RITORNO di TP01**
(ritorno totale mediano ancore ≈ 0%); ciò che regge A OGNI ancora è il claim DIFENSIVO
(DD 6.7-10.1% vs ~60% B&H). Che è da sempre il vero valore dichiarato di TP01.
**Claim 2 (tranching multi-ancora) RIDIMENSIONATO:** K=4 ≡ esattamente EW di 4 book ancorati
(diff 1.4e-17), turnover identico (~8.3/y), haircut $600 ≈ 0. MA il "miglioramento" IS 1.49→1.56 è
per ~90% *tornare alla media delle ancore* (h=0 era sfortunato in-sample): vs ancora tipica
+0.005 mediano, P(K4≤h0)=0.18 n.s.; il taglio DD strutturale è ~0.5pt (non 2.8: la mediana delle
singole è già 12.6%). Ciò che regge: la compressione della VARIANZA DELLA STIMA (HOLD
[0.12,+0.30]→[0.01,+0.13] a K=4) → **lens di reporting, non alpha**.
**Impatto a valle (quantificato dallo scettico):**
- Blend SKH: HOLD 1.16 (h=0) → 0.97 mediana ancore, ma l'UPLIFT a mediana è +0.93 > +0.86
dichiarato → **il verdetto ADDS di SKH01 era conservativo, regge**.
- Book 5-sleeve: HOLD 2.44 → 2.34 mediana (min 2.22), FULL 2.24→2.22 → eredita **~+0.10** di
fortuna d'ancora (non +0.25: il peso di TP01 diluisce). Nessuna decisione (GTAA/SKH/pesi) cambia.
- NON toccati: tutti i FULL (h=0 al 31° pctl = normale), plateau, deflated-Sharpe, fee-sweep,
causalità, validazione GTAA (equity OOS), gate weights_tilt_null (relativo).
**Raccomandazione operativa (sottoscritta dallo scettico): cambio di narrativa e di standard di
reporting, NESSUN cambio del book live oggi.** (1) I numeri hold-out di TP01 si citano d'ora in poi
come banda d'ancora (mediana ~0.04, [0.13,+0.30]) con valore = taglio DD; (2) valutazione
anchor-agnostic (media 24 ancore) come lens di reporting per i futuri hold-out; (3) NO deploy
K=2/K=4 a $600: guadagno ≈ 0, e a $225/asset di quota TP01 i delta per-ancora (~$1-2) sono sotto il
min-order $5 → degenererebbe comunque in K=1; inoltre le ancore intraday richiederebbero un feed
fresco fuori dal path certificato (staleness ×4 finestre di guasto). **Rivalutare K=2 (0,12) a
capitale ≥ ~5-10k.** (4) ⚠️ Audit analogo RACCOMANDATO su XS01 (rebalance 10g ancorato = spazio di
luck 10g×24h, potenzialmente peggiore) e SKH01 (fase griglia 230m/690m) — TP01 è oggi l'unico
sleeve de-luckato.
## Filone 4 — Clock lenti + banded rebalancing: FAIL (negativo utile)
`scripts/research/r0702_slow_clock.py`. Fatto a monte che chiude il filone: il fee drag di TP01 a
1d è **~0.40%/anno ≈ 0.03 Sharpe** (8 turnover/y × 0.10% RT) — il tetto di QUALSIASI risparmio.
- Clock N∈{2,3,5,7}g: degrado monotono dell'ensemble di fase (HOLD 0.34→0.11 da N=2 a N=7); lo
spread TRA fasi esplode a N≥5 (HOLD 0.19…+0.37 a N=7) = timing luck pura, coerente col filone 3.
- Bande di isteresi {2.5,5,10,20}%: tagliano il 77-94% degli ORDINI ma quasi zero TURNOVER (gli
ordini di TP01 sono micro-aggiustamenti del vol-target). Cella best-IS (band 20%): hold-out
0.13 vs 0.30 baseline, e il "guadagno" IS è effetto-segnale (posizioni stantie fortunate), non
effetto-costo → fitting. Non-monotonia sulla griglia = firma di rumore.
- **Finding utile per il live: a $600 il vincolo min-order $5 È GIÀ la banda ottimale** (ordini
427→111/y a costo Sharpe ~0, banda implicita 1.67%): cattura ~100% del risparmio catturabile.
Nessun cambio al book, a nessun capitale testato (600/2k/10k).
## Filone 5 — Velocità del trend regime-condizionata: FAIL
`scripts/research/r0702_regime_speed.py`. Pesi tra gli orizzonti TSMOM 30/90/180g condizionati al
percentile espandente di vol (realized e DVOL), 16 celle, sanity = riproduzione esatta del canonico.
- Cella best-IS (alta-vol→lento, linear, rv): FULL +0.06 ma **HOLD BTC 0.31**, 50/50 +0.005 →
dominanza fallita; multi-cut a segno instabile (+0.18/0.08/+0.00).
- Null decisivo (300 pesi statici Dirichlet): la cella sta al pctl 0.71-0.72, sotto il p90 —
un peso statico casuale la batte spesso. Meccanismo smascherato: corr(peso-30d, Sharpe) = +0.93 →
le celle "regime" vincono in-sample perché tengono il tilt-30d la maggior parte dei giorni =
**tilt statico travestito da regime**, la stessa trappola di EW-STR. E il tilt statico verso il
30d NON regge su BTC hold-out (0.15). RV e DVOL indistinguibili come misura di regime (coerente
con l'esito overlay DVOL 2026-06-26). Pesi canonici 1/3-1/3-1/3 confermati.
## Filoni 6-8 — CRT "Candle Range Theory" (base / multi-TF / contesto): FAIL 3/3
Il pattern sweep-and-reclaim a 3 candele (C1 range forte; C2 rompe un estremo ma chiude dentro =
"manipolazione"; C3 ingresso contro il breakout, stop dietro lo sweep, target all'estremo opposto),
meccanizzato onestamente in tre tagli. Overlap dichiarato: MRV01-11 e MIC07 non coprivano questa
meccanica; ora è coperta.
**6. Base single-TF** (`r0702_crt_base.py`, 864 trial su 1h/4h/12h/1d, motore trade-level
conservativo SL-first cross-checkato con l'harness): **DSR 0.000** (il null best-of-grid atteso è
Sharpe 2.27, ottenuto 0.74); cella best-IS (4h long color-rule) IS 0.90 → HOLD 0.07 con 2026 a
WR 0%; anchor-shift +1/+2h flippa l'hold-out a 1.0 (artifact-risk); lo **short su sweep dell'alto
(la narrativa smart-money canonica) perde perfino in-sample (0.59)**. Autopsia: l'expectancy IS
veniva dal time-exit in trend (beta del toro ETH 2021-23), non dal target strutturale. Dettaglio
informativo: CRT batte nettamente sia il fade incondizionato (IS 0.49) sia il breakout-confermato
(IS 0.31) → il close-back-inside FILTRA davvero tossicità, ma "meno tossico del fade morto" non è
un edge.
**7. Multi-timeframe** (`r0702_crt_mtf.py`, 4h→15m e 1h→5m, ~10k trade): expectancy netta negativa
OVUNQUE (FULL e HOLD, entrambi gli asset); il MTF alza il R:R medio da ~3 a ~10-20 ma il WR collassa
da ~36% a 9-16% → "migliora" solo perdendo meno. **Refutazione strutturale: il ritest è informazione
negativa** — pattern CON ritest 40 bps, SENZA +52 bps (non tradabile: condiziona sul futuro):
aspettare il ritest per entrare seleziona sistematicamente i pattern peggiori; i buoni scappano
subito e il metodo non li prende mai. A fee ZERO l'edge lordo è ~0 (non è morte per fee: non c'è).
Stop 0.1-0.35% ineseguibili col cron orario (26-75% dei segnali già invalidati all'esecuzione).
**8. Contesto** (`r0702_crt_context.py`, 22 trial: livelli prevday/Donchian/prevweek × equal-H/L ×
FVG × sessioni): baseline incondizionata morta (8/8 celle HOLD<0); FVG semmai peggiora; sessione
EU consistentemente negativa; unica cella IS-positiva (sweep in Asia su livello prevday) uccisa 4
volte (DSR 0.001, anchor-flip a 2/4h con la firma di open_drive — la sessione "Asia" inizia dove
il livello prevday viene ricreato —, solo-ETH, morta a 0.20% fee). **Sottoprodotto prezioso: sugli
STESSI livelli prior-day, FOLLOW batte FADE ogni singolo anno 2019-2026** (follow IS/HOLD
+1.22/+1.25 vs fade 0.66/1.46, corr 0.19/0.27) → conferma indipendente e rafforzativa del lead
prevday-breakout in forward-monitor (paper_prevday).
**Verdetto CRT: concetto Wyckoff riverniciato che, spogliato della discrezionalità, non contiene
edge su BTC/ETH Deribit certificati. La direzione giusta sui livelli affollati resta il FOLLOW
(breakout), non il fade — coerente con SKH01 (breakout) vivo e mean-reversion morta.**
---
## Lezioni codificabili
1. **Il timing-luck dell'ancora è una dimensione di multiple-testing NON coperta dai gate
esistenti** (deflated-Sharpe conta i trial, non le ancore implicite). Nuova regola di reporting:
ogni metrica hold-out di una strategia a ribilanciamento ancorato si cita con la banda d'ancora
(o la media anchor-agnostic). Candidato gate futuro: `anchor_luck_band()` in altlib. Audit
pendente: XS01 (10g), SKH01 (fase 230m/690m).
2. **La famiglia day_boundary_robust / anchor-shift continua a uccidere** (CRT base, CRT-Asia,
6/24 celle expiry): ogni effetto legato a etichettatura di barre VA spostato d'ancora prima di
crederci. Confermata la regola 2026-06-21.
3. **Il fee drag di TP01 (~0.4%/anno) non è un problema da risolvere** — chiude a priori il filone
"esecuzione più furba" a questo turnover; il min-order small-cap è già la banda ottimale.
4. Il pitfall pandas tz-aware/epoca (2026-07-01) è stato evitato/gestito 2 volte su 9 agenti grazie
alla documentazione in CLAUDE.md — il costo di documentare i bug paga.
## Stato finale
- 0 nuovi sleeve; portafoglio e book live INVARIATI (5-sleeve 33/15/12/20/20; book Deribit
TP01+SKH01 75/25, flat da armamento — il segnale resta risk-off).
- Narrativa TP01 aggiornata (CLAUDE.md + docstring): hold-out = banda d'ancora, valore = difesa DD.
- Script: `scripts/research/r0702_{eventclock,expiry_calendar,tp01_offset,slow_clock,regime_speed,
crt_base,crt_mtf,crt_context,skeptic_offset}.py`. Test suite: invariata, verde.
+111
View File
@@ -0,0 +1,111 @@
# 2026-07-03 — Frontiera rischio/rendimento del book: quanto caldo correre a 2k-5k
**Contesto.** Dopo la diagnosi strategica ("€50/g è un problema di capitale e di frontiera, non di
alpha; hai costruito una macchina difensiva a DD 6% ma hai capitale piccolo e obiettivo di
crescita"), goal: **misurare** (non cercare segnali) la frontiera CAGR/DD/P-rovina in funzione del
target-vol, pesata per confidenza per-sleeve, a €2k e €5k — per decidere quanto caldo girare il book.
Workflow: 3 filoni (curva / onestà-della-coda / eseguibile+Kelly), refuter fat-tail su ciascuno,
scettico sintetizzatore. 7 agenti, 0 errori. Sanity bit-exact di entrambi i book prima di ogni numero.
**Scripts:** `scripts/research/r0703_frontier_{curve,tail,exec}.py`. Nessun file di produzione
toccato, nessun commit dagli agenti, `config/live.json` NON modificato (cap solo proposto).
---
## Verdetto in una riga
**La tesi "corri più caldo" era in gran parte un MIRAGGIO DI REGIME.** Il "~6% attuale" non è un
target scelto: è la vol *realizzata* in regime risk-off (TP01 spesso flat 0.0x → il conto è flat
*by design*). A lambda=1 il book LIVE a 2 sleeve gira **già a ~11% vol / 9.4% DD**. Quindi "saltare
da 6% a 10%" NON è aggiungere leva — è solo assumere che il book non sia risk-off. **Il numero
onesto è: gira alla vol NATIVA (~10%, banda 8-11%), non levare.** Guadagno reale vs il 6% risk-off:
**+€0.2/g @2k, +€0.5/g @5k** — reale ma modesto, comprato con un DD plausibile di ~€500 su €2k. €50/g
resta ~130k, invariato.
---
## I numeri (frontiera fidata, non full-Sharpe)
Sanity (deriva nulla): 5-sleeve FULL Sh **2.238** / HOLD 2.452 / DD 6.17% / vol nativa 7.84%;
2-sleeve live FULL Sh **1.776** / HOLD 1.162 / DD 9.42% / vol nativa 10.96%.
**Scale-invarianza confermata da tutti e 3 i refuter:** il capitale NON cambia la matematica del
target-vol (le tabelle sono in %), cambia solo l'ESEGUIBILITÀ → **stesso % a 2k e 5k.**
### Book LIVE 2-sleeve (TP01+SKH01) — l'unico eseguibile <20k
- **Target-vol raccomandato: ~10%, banda [8%, 11%], lambda ≈ 0.9.** Sopra la vol nativa non si va.
- €/giorno al punto fidato (haircut): CAGR full-sample 12.9% → €0.71@2k / €1.77@5k; ma **HOLD-onesto**
(de-luck) CAGR ~4.5% → **€0.25@2k / €0.62@5k**. Il lato-reward headline è ~3× l'onesto (HOLD≪FULL).
- **Stima centrale onesta: €0.3-0.7/g @2k, €0.8-1.8/g @5k.**
### Book PIENO 5-sleeve — ASPIRAZIONALE, non gira <20k
Frontiera fidata regge ~12-15% sotto tail estremo, ~20% sotto tail mite — ma XS01 serve ~20k e VRP01
è modellato → rilevante solo per il tracking di `paper_combo`, non per una decisione di deploy a 2-5k.
---
## I quattro insegnamenti strutturali
### 1. La rovina-50% è la metrica SBAGLIATA (trappola esposta dal curve-refuter)
P(rovina-50%|5y) resta <1% fino a ~20-22% di target-vol su ogni book — i book sono troppo lisci/
diversificati per bruciare metà capitale entro la distribuzione campionaria. **La risposta letterale
"max CAGR a P(rovina)<1%" direbbe "gira al 22%" — ed è un suicidio**: lì P(DD>30%|5y) ≈ 20-25%. Il
vincolo che MORDE è **P(DD>30%), super-lineare** (raddoppia ~ogni 2.5 punti di vol). E la rovina-50%
non è nemmeno robusta ad alta vol (block-15 vs block-20 danno 7.1% vs 0.9% a tv30%) → altro motivo
per stare bassi.
### 2. Il muro fat-tail ONESTO è ~12%, più basso della carta (~15%)
Tre ragioni che il modello sottostima: **(a) gap-through-stop SKH già osservato reale** (sl2%
modellato → 11/23% realizzato — la coda vera NON è nei rendimenti modellati; l'iniezione dei
finder usava lo *clean-stop* → ~2× troppo mite); **(b) il 2-sleeve è 4× più crash-sensibile del
5-sleeve** (+14.5pt vs +3.5pt di P(DD>30) all'iniezione, diversificazione sottile); **(c)
vol-targeting a lambda costante lagga nello spike** → sei più levato *entrando* nel crash.
**LIVE 2-sleeve: non superare ~12%; mai 15-20%** (triplica il DD-in-euro senza margine di coda).
### 3. Aggressività differenziata-per-confidenza (TP01 più caldo di VRP) — REFUTATA
La mia proposta era intuitiva ma **sbagliata, e i numeri lo dicono netto**: differenziare la vol per
confidenza PEGGIORA la frontiera (5s Sharpe 2.24→2.11, 2s 1.78→1.69) perché **taglia la
diversificazione** di VRP/XS (corr uniforme-vs-differenziato 0.98-0.99). **Regola corretta: la bassa
fiducia sull'EDGE si esprime DE-MEANando lo sleeve in aspettativa (= la frontiera haircut), NON
riducendone l'esposizione/vol.** La confidenza vive nell'haircut della media, non nella leva. Tutti
gli sleeve alla stessa leva.
### 4. La diversificazione LIMA la coda sinistra in-sample (sorpresa onesta)
Per il 5-sleeve ben diversificato il block-bootstrap dà P(DD>30) SOTTO il Gaussiano iso-vol (30%:
10.6% reale vs 21.1% gauss) — la coda in-sample è benigna. Il segnale fat-tail vive in due posti:
l'**iniezione di crash sintetico** (il campione non contiene il crash peggiore di ogni sleeve) e
il **book live sottile** (2 gambe, drift haircut) dove il bootstrap SUPERA il Gaussiano. Cioè: la
frontiera è affidabile finché il futuro somiglia al passato campionato, e fragile esattamente dove
le storie sono corte (VRP mai stressato reale, XS 2.5y, nessun 2018/pre-2018).
---
## Azione concreta (proposta, NON applicata)
1. **Nessun cambio di leva sul codice.** Il book gira già ≈ al punto fidato: LASCIARE `target_vol=20%`
di TP01 (canonico, la vol nativa del book emerge a ~8-11%). Non c'è una leva da girare su.
2. **⚠️ L'UNICO cambio che conta: `config/live.json` `max_notional_per_asset_usd` da $300 a
equity/2** ($1000@2k / $1750@3.5k / $2500@5k). Senza, il cap $300 tappa la vol raggiungibile a
13%@2k / 8%@3.5k / **6%@5k****throttling paradossale: più capitale = book più freddo**, forzato
sotto il punto fidato. Da applicare AL deposito. (config NON toccato ora.)
3. `min_order_usd=5`: LASCIARE (banda d'isteresi gratuita). Tranching K=2: NON cablare. Vol
differenziata: NON implementare (Q3).
---
## Brutalità sui limiti
- **Storie corte = il difetto strutturale.** TP01/SKH dal 2019 (no 2018), VRP dal 2021, XS **2.5
anni** (no 2022). Il block-bootstrap non può campionare un crash mai visto; l'iniezione sintetica
è l'unico proxy, e quella dei finder era ~2× troppo mite.
- **VRP01 è MODELLATO** (f-di-stress mai catturato su un crash reale) → l'haircut 50% è stima, non
misura; resta STAT-MODE anche a 5k (regola: niente short-vol da modello in deploy).
- **HOLD ≪ FULL sul book live** (1.78 vs 1.16) → il lato-reward headline è in gran parte artefatto
full-sample; citare i €/g su base HOLD-onesta.
- **€50/g NON è risolto e non lo sarà da questo.** Il warm-up compra ~€0.2/g@2k. La via resta
**capitale + tempo**; la regola "niente leva alta come base" è rispettata (la raccomandazione è
"gira alla vol nativa", non "corri caldo").
**Numero unico da portare a casa:** book LIVE a **~10% target-vol (lambda ≈ 0.9, banda 8-11%)**,
uguale a 2k e 5k, con **cap → equity/2** come unica azione config; muro invalicabile a **~12%**;
guadagno reale vs il 6% risk-off ~**+€0.2/g@2k / +€0.5/g@5k** — reale ma modesto.
+47
View File
@@ -0,0 +1,47 @@
# 2026-07-03 — Cap notional per-asset DINAMICO (= equity/2): operazionalizza la decisione frontier
**Contesto.** La frontiera del 2026-07-03 (`docs/diary/2026-07-03-book-frontier.md`) aveva identificato
**una sola** azione config con impatto reale: portare `max_notional_per_asset_usd` da $300 fisso a
**equity/2**, perché il cap fisso strozza il book man mano che si deposita (paradosso: più capitale =
book più freddo — a $5k il cap $300 fa girare il libro al ~49% del target). Il diario la lasciava
"da applicare AL deposito". Qui la **operazionalizzo in codice** rendendo il cap dinamico, così è
sempre corretta e non richiede un edit manuale al deposito.
## Cosa è cambiato
- **`src/live/book.py``_cap()`**: se in config c'è `max_notional_per_asset_frac` (opt-in), il cap
per-asset diventa **`equity × frac`** (0.5 → equity/2). Senza la chiave → comportamento vecchio
(cap fisso `max_notional_per_asset_usd`). Il call site in `book_report()` passa
`equity`, `real_equity`, `eq_fallback` a `_cap()`.
- **`config/live.json`**: aggiunto `max_notional_per_asset_frac: 0.5` (+ nota). `max_notional_per_asset_usd`
resta come **cap fisso di fallback**.
- **`tests/test_book_live.py`**: +4 test (scaling con equity fidata, fallback su eq_fallback/real=None/
equity 0, no-frac, integrazione online). Suite: **172 passed**.
## Il guardrail di sicurezza (il punto delicato)
Il cap dinamico si usa **SOLO con equity reale fidata** (letta dal conto). Se l'equity reale non è
leggibile — `eq_fallback` attivo, `real_equity=None`, offline, o equity ≤ 0 — si **torna al cap fisso
$300**. Motivo: in fallback `equity` è il paper-cap ($2000 ipotetico); calcolare `equity/2 = $1000` su
un conto di importo reale **ignoto** sizerebbe troppo. Preserva esattamente la protezione downside che
il test `test_book_execute_eq_fallback_warns_but_proceeds` verificava (l'hard-cap $/asset protegge
quando E è ignota). Regola: **equity/2 solo quando ci fidiamo di E; altrimenti $300 fisso.**
## Verifica
- **Live dry-run reale** (nessun `--execute`, nessun ordine): `cap/asset **$299**` = $598.06/2 →
**INERTE** oggi (≡ vecchio $300). Book flat, HOLD.
- Offline / `equity_override`: cap resta **$300 fisso** (nessuna equity reale da fidarsi) → il test
di parità pre-esistente regge invariato.
- A $5.000 di equity il cap diventerebbe automaticamente **$2.500** (equity/2), rimuovendo il
throttling che la frontiera aveva quantificato.
## Impatto e limiti
- **Già attivo**: il cron orario (`cron_book.sh`) esegue dal working tree → il prossimo run legge già
`frac=0.5`. Nessun deploy separato. **Inerte a $600**, si "accende" da sé quando l'equity cresce.
- **NON avvicina €50/g**: la frontiera lo aveva già detto — è una miglioria marginale (+€0.2/g @2k,
+€0.5/g @5k), il cui unico scopo è non lasciare capitale futuro strozzato. €50/g resta un problema
di **capitale + tempo** (~130k @ CAGR onesto), non di questo cap.
- Nessun cambio di **leva** (TP01 `target_vol=20%` invariato, la vol nativa ~10% emerge da sola —
coerente col verdetto frontier "gira alla vol nativa, non levare").
@@ -0,0 +1,156 @@
# 2026-07-03 — STATO COMPLETO DEL PROGETTO (snapshot)
Foto d'insieme dopo le ondate di ricerca del 1-3 luglio 2026 (timing/CRT, anchor-audit,
video-claims Elliott/Albimarini, capital-scaling, migliora-e-proteggi VRP01). Serve come punto di
riferimento unico: *dove siamo, cosa gira, cosa è provato, cosa resta aperto.*
---
## 1. In una frase
Progetto post-reset v2.0.0: su dati Deribit mainnet certificati (solo BTC/ETH) abbiamo costruito un
**portafoglio di 5 strategie diversificate** (research Sharpe ~2.2, DD ~6%), di cui **2 sono
eseguite live su Deribit** in un book nettato in software. Il conto reale è **~$598, flat** perché
il segnale trend è risk-off. Il valore delle strategie è **difensivo** (taglio del drawdown), non
generazione di ritorno: il target €50/giorno resta lontano (serve ~130k di capitale). L'onestà
metodologica è il vero asset accumulato: ~10 gate anti-illusione codificati, decine di edge
falsificati.
---
## 2. Il book LIVE (soldi reali)
- **Conto:** Deribit mainnet, USDC linear, **equity reale $598.06** (NON i €2000 nominali del paper).
- **Cosa gira:** `deribit_book_sleeves` = **TP01 (0.75) + SKH01 (0.25)** nettati in software su un
solo conto; UN ordine con segno per asset (BTC, ETH). `config/live.json`
**`execution_enabled=true`** → esecuzione ARMATA e reale.
- **Cadenza:** cron ORARIO `scripts/cron_book.sh``scripts/live/book_execute.py` (SKH01 decide su
griglia 230m; exit software, latenza fino a fine barra).
- **Guardrail:** cap **$300 notional/asset**, min order $5, **disaster-SL on-book 30%** sulla
posizione netta, alert Telegram su esecuzione/errori, gate fail-safe su posizione/equity non
leggibili (non opera a cieco).
- **Stato corrente (ultimo cron 2026-07-03 07:00Z):** BTC e ETH **flat, HOLD a target** — il target
TSMOM è risk-off (trend 0.0x) e SKH01 è flat (nessun breakout) → **zero ordini reali mai eseguiti**
da quando è armato (2026-06-23): il conto è flat *by design*, non per un errore.
- **NON nel book live:** XS01, VRP01, GTAA01 (research/paper/STAT-MODE).
## 3. Il portafoglio RESEARCH (5 sleeve) — `src/portfolio/sleeves.active_sleeves`
| Sleeve | Peso | Meccanismo | Dati | Stato |
|---|---|---|---|---|
| **TP01** | 33% | TSMOM difensivo trend BTC/ETH 1d, long-flat, vol-target 20% | 2019→ | **Deploy pieno** (nel book live) |
| **SKH01** | 20% | Dual-TF regime+breakout BTC/ETH, L/S, exit %-asimmetrici | 2019→ | Research → nel book live |
| **GTAA01** | 20% | Trend difensivo equity 6-ETF (IB), ~30 anni storia | 1996→ | Paper (paper_combo) |
| **VRP01** | 12% | Options short-vol (put credit spread + gate IV-rank) | 2021→ | LEAD modellato (STAT-MODE) |
| **XS01** | 15% | Cross-sectional momentum 19 alt Hyperliquid | 2024→ | STAT-MODE (serve ~20k) |
- **Combinato (research, ancore canoniche):** FULL Sharpe **2.24**, HOLD-OUT **2.46**, DD full **6.2%**.
- **⚠️ Stima DE-LUCKATA onesta** (dopo gli anchor-audit, vedi §5): **HOLD ~1.9-2.1, FULL ~2.0-2.2,
DD ~6%**. Il 2.46 è un massimo di configurazioni d'ancora, non la stima centrale.
- Sleeve a date d'inizio diverse → outer-join con pesi rinormalizzati (`combine_outer`).
- Report: `scripts/portfolio/run_portfolio.py`. Il book live (TP01+SKH01 75/25) è un sottoinsieme.
## 4. Paper trader / forward-monitor attivi (nessun soldo reale)
- **paper_trend** — TP01 1d, capitale nominale 2000, forward-only.
- **paper_combo** — book 5-sleeve completo (incl. GTAA/VRP/XS) dal 2026-06-22.
- **paper_statarb** — STATARB-RESID (ETH−β·BTC residual momentum, W=45/sgn=+1 congelati), doppio
libro MODELED/REAL-$600. LEAD eseguibile a $600 sotto soglia solo sull'edge (DSR 0.929).
- **paper_prevday** — FOLLOW prior-day levels (3 trade registrati); confermato indipendentemente da
più ondate (FOLLOW > FADE ogni anno 2019-26).
---
## 5. Finding strutturale del blocco 1-3 luglio: ANCHOR TIMING-LUCK (audit COMPLETO 4/4)
Le strategie a ribilanciamento/griglia ancorata hanno un'ancora "canonica" che è spesso il
**migliore** di molte configurazioni equivalenti (giorno/ora/offset) — un multiple-testing che il
deflated-Sharpe NON conta. Audit eseguito su tutti e 4 gli sleeve ancorati:
| Sleeve | Spazio d'ancora | Firma | Conseguenza |
|---|---|---|---|
| **TP01** | 24 ore (barra daily) | canonica = migliore (P=0.86) | hold-out 0.31 non risolve l'edge di ritorno; regge solo il taglio DD |
| **XS01** | 10 fasi (ciclo 10g) | canonica al 15° pctl di DD | numeri headline conservativi sul DD, non sull'hold-out |
| **SKH01** | 23 offset (griglia 230/690m) | canonica = 93-98° pctl (peggiore firma) | gate DD<30% fallisce in 15/23 offset; ~+0.5 HOLD di fortuna sul book |
| **VRP01** | 7 giorni (ciclo settimanale) | **canonica = PEGGIORE** (7° pctl) | **numeri CONSERVATIVI**, non gonfiati — unico sleeve senza luck |
**Regola codificata:** i numeri hold-out di strategie ancorate si citano **con banda d'ancora**.
Candidato gate futuro: `anchor_luck_band()` in altlib. Diari: `2026-07-02-timing-crt-wave.md`,
`2026-07-02-anchor-audit-xs01-skh01.md`, `2026-07-03-vrp-improve-dd.md`.
---
## 6. Cosa è PROVATO e MORTO (per non ri-testarlo)
- **Soffitto BTC/ETH-direzionale ~1.3** confermato da ogni ondata; superato solo dal cross-sectional
(XS01) e dalla diversificazione multi-sleeve.
- **Sweep 104 famiglie** (2026-06-20): 1 sola sopravvissuta, comunque non deployabile.
- **Opzioni long-vol** (gamma scalping): specchio del VRP, perde ogni anno/variante/frequenza.
- **Cash-and-carry (CC01):** premio reale ma Sharpe artefatto (manca il 2022, procyclico) → STAT-MODE.
- **Timing/clock alternativi** (event-clock, expiry calendar, clock lenti, regime-speed): 0 edge.
- **CRT "Candle Range Theory"** (3/3 FAIL): DSR 0.000, expectancy negativa anche a fee zero, il
ritest è informazione negativa; sui livelli prior-day FOLLOW > FADE.
- **Elliott (3/3 FAIL):** range-cycle = rumore; Fibonacci = effetto posizione non numeri magici;
canale = Donchian travestito.
- **Albimarini double-diagonal (2/2 FAIL):** non domina il condor, fee-negativa su Deribit, track
record consistente con zero skill (P=20-45%), rovina nei replay con code reali.
- **VRP01 miglioramenti (0/7 filoni):** struttura, overlay-DD, gate nuovi (4° fallimento), sizing,
gate SPX (confound di modello) — **VRP01 non migliorabile; la protezione DD si compra con la size.**
## 7. Regole/gate anti-illusione codificati (l'asset metodologico)
- **Certificazione dati** obbligatoria (solo Deribit mainnet, BTC/ETH; alt esclusi).
- **`marginal_vs_tp01` / `study_marginal`** (earns_slot) — Sharpe MARGINALE, non assoluto.
- **`study_family_honest` / `select_cell_insample` / `deflated_sharpe≥0.95`** — anti selection-on-holdout.
- **`day_boundary_robust`** — effetti calendario che si invertono spostando il confine = artefatto.
- **`eval_weights_smallcap`** — Sharpe haircut reale a capitale piccolo (sub-min-order = finzione).
- **`weights_tilt_null`** — ogni cambio pesi vs null dei tilt casuali cap-respecting.
- **Banda d'ancora** (nuova, luglio) — per ogni costrutto ancorato.
- **Candidati in coda:** `anchor_luck_band` (multiple-testing d'ancora); `implausible_sharpe`
(0-perdite / Sharpe implausibile — 3 occorrenze: CC01, ALB-A, griglia struttura VRP).
- **Lezioni pandas:** mai `DatetimeIndex.view("int64")` su tz-aware; `resample("7D", origin=)`
ignora origin (usare `"168h"`); un gate che si correla con un errore di modello è indistinguibile
da alpha finché non correggi il modello.
---
## 8. Onestà sui numeri e sul target
- **€50/giorno da €1.000-2.000 NON è raggiungibile** in 1-2 anni: serve ~130k di capitale o un DD da
rovina. La leva non è la scorciatoia (5x raddoppia il DD). La via è target-vol + capitale + tempo.
- Aspettativa onesta col CAGR de-luckato del book (10-15%): **~€0.6-0.8/giorno a 2k, ~€1.4-2/giorno
a 5k**. Il salto di capitale cambia *cosa è eseguibile*, non l'ordine di grandezza del ritorno.
- Il valore reale di TP01/del book è il **taglio del drawdown ~6× vs buy&hold**, non l'alpha.
## 9. Capitale in arrivo 2-5k — cosa cambia (audit r0702_capital_scaling)
- **Unico vincolo binding: il cap `max_notional_per_asset_usd=300`.** A 5k il book live girerebbe al
~49% del target. **Azione pendente sul deposito: alzare il cap a equity/2** ($1000/$1750/$2500 a
2k/3.5k/5k). `min_order_usd=5` da lasciare.
- Tranching K=2: matematicamente regge a 5k ma serve feed intraday fuori path certificato → non cablare.
- Opzioni ETH eseguibili da ~2.6k (spread), ma **la regola "niente short-vol da modello in deploy"
non decade col capitale**. XS01 resta fuori fino a ~20k. CC01 fuori per struttura.
- ⚠️ Disambiguazione unità: il "12%" come peso book (~0.014 Kelly) ≠ "12%" come margine/equity
(~0.27 Kelly) — **fattore 19x**.
## 10. Aperto / prossimi passi
1. **Al deposito 2-5k:** alzare il cap $/asset (proposta pronta, `config/live.json` non ancora toccato).
2. **Forward-monitor da maturare:** paper_statarb (edge sotto soglia DSR), paper_prevday (FOLLOW),
paper_combo (book completo). Se la finestra forward conferma STATARB-RESID → deployabile a 2 gambe.
3. **Gate da codificare:** `anchor_luck_band`, `implausible_sharpe`.
4. **Rivisite temporizzate:** term-structure DVOL (logger dal 2026-06-26, rivalutare a 6-12 mesi);
breadth/internals HL (1-2 anni di storia nativa); tranching a ≥5-10k; VRP stress-f su un crash reale.
5. **Ricerca strategie nuove:** il soffitto direzionale ~1.3 è saturo; i margini restano in meccanismi
ORTOGONALI (cross-sectional, relative-value, options gated) e nella diversificazione, non in nuovi
segnali direzionali BTC/ETH.
---
## 11. Stato tecnico
- **Versione:** 2.0.0 (semver in `VERSION`). **Test:** 168 verdi (28 file). **Diari:** 76.
- **Ultimi commit:** `26f8d27` (migliora+proteggi VRP01), `76120b5` (video-claims + capital-scaling),
`a74cc69` (anchor-audit XS01/SKH01), `e6657fc` (GTAA01 5° sleeve).
- **Working tree pulito, tutto pushato.** Book live sano e invariato da tutta la ricerca (research
isolata: nessun agente tocca src/config/live).
+167
View File
@@ -0,0 +1,167 @@
# 2026-07-03 — Ondata "migliora e proteggi VRP01 dai DD" (7 filoni + verifica a 2 lenti + scettico incrociato)
**Contesto.** Dopo la chiusura del dossier Albimarini (3 backtest indipendenti convergenti: premio
piccolo e reale, code non campionate, numeri del corso spiegabili al 100% dalla struttura del
payoff), goal esplicito: **migliorare la strategia short-vol** — la famiglia Albimarini → il nostro
**VRP01**, l'unica forma sopravvissuta agli audit — **e proteggerla dai drawdown**, con
orchestrazione a decine di agenti. Workflow deterministico: 7 filoni di ricerca in parallelo, 2
refuter avversariali (lente selection-bias + lente de-levering) su ogni miglioramento proposto,
scettico incrociato finale sui sopravvissuti. **26 agenti, 0 errori.**
**Scripts:** `scripts/research/r0703_vrpimp_{structgrid,ddoverlay,sizing,newgates,anchor,spxgate,stresslab}.py`.
Nessun file di produzione toccato, nessun commit dagli agenti. Motore riusato: `r0702_alb_structure.py`
(DVOL, riproduce VRP01 bit-exact: baseline bridge max|diff|=0.00e+00 su 266 settimane).
---
## Verdetto in una riga
**VRP01 non è migliorabile oltre lo status quo, e la protezione DD si compra SOLO con la size.**
Zero nuovi sleeve, zero overlay che reggono, zero gate nuovi, zero cambio di sizing. I due unici
"sopravvissuti" alla verifica **non propongono alcun cambiamento** (una conferma dello status quo +
una lente di misura). Il candidato più luccicante dell'ondata — un gate term-structure VIX su SPX
con ΔSharpe +0.90 e DSR 0.992 — è stato smontato dallo scettico come **confound di modello al
100%**. Deliverable reale = **audit anchor-luck di VRP01 CHIUSO** (ultimo sleeve ancorato): la fase
canonica è all'estremo **conservativo** della banda, quindi i numeri di ammissione reggono e da ora
si citano con banda.
---
## I 7 filoni
### 1. Griglia di struttura (288 celle) — NESSUNA batte VRP01
6 strutture (vertical put/call, condor, 3 diagonali) × distanza {1,1.5,2,2.5}σ × ali {0.5,1,2}σ ×
tenor {3,5,7,10}g, gate IV-rank canonico sempre attivo. La cella scelta col **solo in-sample**
(DIAG-PUT z=1.0 3g, ShIS 2.28) crolla in hold-out a **+0.19** vs 0.86 del bridge stesso-motore;
DSR 0.000/0.004 su 288 trial; multi-cut negativo a 3-4 cut su 4; banda d'ancora che non tiene
nemmeno il segno (ShH [0.21,+0.64]). **Metà griglia (150/288) è il pattern "0-perdite = Sharpe
implausibile"** (celle z≥2 lunghe, 0 perdite su ~97 trade, ShH fasulli 4-6.5) — **terza occorrenza
dopo CC01 e ALB-A**. Quarta conferma indipendente: nel VRP l'alpha sta nel gate di regime + nella
zona ~δ-0.28 dove la coda è campionata, non nella geometria.
### 2. Overlay di protezione DD (14 celle) — 4/4 REFUTED (2 lenti ciascuno)
(a) exit intra-settimana su spike DVOL, (b) stop-loss MTM, (c) ala di coda comprata (broken-wing),
(d) cooldown post-perdita. **Nessuno batte il null del de-levering** a pari maxDD su tutta la banda
f; nessuno passa multi-cut (max 1/4) né DSR (0.70-0.88, tutti <0.95). Esempio decisivo: SL MTM 3x fa
DD 7.9%/worst 5.7%, ma VRP01 × 0.66 (semplice riduzione di size) fa lo **stesso DD a Sharpe
invariato e worst-week migliore (4.9%)**. Meccanismo confermato: i trigger di spike scattano solo
2-3 volte in 5 anni **perché il gate IV-rank/crash-skip evita già gli ingressi vicino alle
esplosioni di vol** — la protezione crash vive nel gate d'ingresso, non serve un overlay a valle.
Coerente con TP01×DVOL (2026-06-26): "per meno DD la leva è la size, non un overlay".
### 3. Sizing anti-rovina — CONFERMA (12% ≈ quarter-Kelly), 1 candidato REFUTED
Il sizing non crea alpha (Sharpe del flusso invariante ~0.95 a ogni frazione). Frontiera onesta
CAGR-DD-P(rovina) mappata su 92 trade gated ETH 2021-26 (pooled banda-f + coda sintetica full-loss):
Kelly onesto q*=44% → il **12% del book = 0.27 Kelly, già in zona quarter-Kelly anti-rovina**
(P(rovina-50%|5y) 0.0-0.1%, P(DD>30%) 9-18%, CAGR med 13.5-17% a 2k/5k). Salire al 25% → P(DD>30%)
> 92%; Albimarini 1→4 → P(rovina) 53-55%. **Nessun cambio raccomandato.** Il candidato anti-streak
(N=2, size 12%→3% dopo 2 vittorie, l'opposto di Albimarini) era il più luccicante mai visto sullo
sleeve (FULL CAGR +19.4%/DD 6.3%/Sh 2.08, random-null 0.996, DSR 0.999) ma **auto-refutato come
artefatto di selezione**: plateau assente (solo N=2 vince, N=3/5 collassano), P(loss|streak) non
monotona, delever-null perde in 2/7 ancore — tutto l'effetto sono ~6 perdite su 15 trade a
streak==2 (binomtest p=0.033, CI contiene la base). Istruttivo: mostra come un DSR 0.999 possa
convivere con l'assenza di meccanismo.
### 4. Gate nuovi — 4° FALLIMENTO su 4 (barra alta)
(a) term-structure DVOL **non testabile oggi** (storia vol_term = 7 giorni, logger forward dal
2026-06-26 → rivalutare a 6-12 mesi); (b) vol-of-vol, (c) RV-acceleration, (d) combinazioni-AND. Il
candidato IS-best (vol-of-vol pctl>0.80) passa DSR 0.960 e il de-levering all'ancora di default ma
la **banda d'ancora lo smaschera** (mediana uplift FULL 0.07 su 7 ancore, uplift concentrato in UNA
finestra 2021-22 di 5 settimane, hold-out uplift 0.00). Bonus strutturale: i veti RV-acceleration
bloccano settimane **profittevoli** (+0.93/+1.25% vs +0.6% tenute) — dopo il gate IV-rank, la vol
che accelera è dove il premio venduto è più ricco. Conferma il verdetto 2026-07-01: l'alpha è tutto
nel binario IV-rank>0.30.
### 5. Audit anchor-luck VRP01 — CHIUSO: primo sleeve SENZA firma di luck
VRP01 è l'ultimo sleeve ancorato dopo TP01/XS01/SKH01. Ciclo settimanale → 7 ancore (giorno di
apertura). Replica bit-exact della sleeve (max|diff|=0.0). Le 7 fasi con parametri identici:
| metrica | canonica (fase-0) | mediana 7 fasi | banda | pctl canonica |
|---|---|---|---|---|
| Sh FULL | 1.09 | 1.32 | [1.09, 1.83] | **7° (peggiore)** |
| Sh HOLD | 0.59 | 0.59 | [0.03, 1.11] | 50° (mediana) |
| maxDD | 11.8% | 7.2% | [5.7, 11.8%] | **93° (peggiore)** |
**Firma OPPOSTA a TP01/XS01/SKH01**: lo spike bootstrap è NEGATIVO (g0 HOLD 0.06, FULL 0.21,
P(g0≤0) 0.54/0.74) → niente da de-luckare. La fase canonica ha beccato le settimane crash 2022
**peggio** di ogni altra (unico anno-fase negativo, 5.8%). **I numeri di ammissione FULL 1.10 /
HOLD 0.60 / DD 12% sono quindi CONSERVATIVI, non gonfiati** — il timore di "protezione fittizia" è
refutato: il DD 12% è il caso peggiore della banda, non quello fortunato. Impatto della fase sul
book 5-sleeve ≤ 0.04 Sh (banda ShFULL 2.23-2.27, ShHOLD 2.35-2.48, DD 6.2-6.3%). **Con questo
l'audit anchor-luck è completo su tutti gli sleeve ancorati (4/4).**
### 6. Gate di regime sulla diagonale SPX (l'esperimento mancante) — REFUTED, con confound istruttivo
L'esperimento che mancava ai 3 backtest Albimarini: il gate di regime sulla diagonale SPX (VIX +
VXV da FRED, motore BS sintetico con skew log-lineare). **Domanda secca: il gate IV-rank ribalta gli
anni di coda su SPX come su crypto? NO.** Il gate VIX-rank canonico *abbassa* lo Sharpe (1.36→1.19),
non ribalta nessun anno di coda, peggiora il 2018, muore sul de-levering (ΔSh 0.17). Meccanismo:
su SPX il VRP è già positivo l'**84% dei giorni** (vs ~58% delle settimane crypto) → il filtro di
rank non ha lavoro discriminante; su equity IV-rank alto = crisi, non vol ricca.
Il candidato **term-structure VIX/VXV<1 (contango)** sembrava reale — Sh 1.18→2.08, DD 3.5→1.3%,
ΔSh **+0.90** a pari DD, multicut 3/3, flippa il 2020, DSR 0.992, plateau su c∈[0.93,1.05], jackknife
robusto — **e passa la lente selection-bias pulito**. Ma la lente de-levering lo affonda:
**l'intero uplift è un confound di modello, quantificato al 100%.** La variabile del gate (VIX/VXV>1)
coincide 1:1 con la variabile dell'errore di pricing (tenor 6g prezzato a VIX-30g). Il refuter ha
riprezzato con σ(6g)=VIX·(VIX/VXV)^α term-structure-consistent (α=1.42 = estrapolazione log-lineare
zero-parametri della pendenza osservata 30g→93g) → l'edge sparisce. **Lezione: un gate che si
correla con un errore di modello è indistinguibile da alpha finché non correggi il modello** — vale
per ogni futuro test su strutture prezzate BS-flat.
### 7. Stress lab di coda — fisica dello strumento, nessun cambio
La protezione di coda del VRP01 viene per ~metà dall'**ala far-OTM** (nel replay COVID taglia ~90%
della perdita naked) e ~metà dal **gate canonico** (sulle 10 peggiori finestre 14g taglia la perdita
cumulata del 44-71% saltando i re-entry a IV-rank>0.90). L'ala T+1 "Albimarini" è assicurazione solo
contro i gap **medi** (15/20%, dove atterra ATM con un giorno di time value: +150bps) e **NON**
contro i gap profondi (k(30%)=0.98 → il de-levering la batte), con valore interamente dipendente
dal vol-spike (banda f/mult da 324 a +263bps). Worst-case onesto a sizing 12%: piccolo in
convenzione book (−€12/sett a 2k, −€31 a 5k alla cella 30%) ma **è il margine intero** se il 12%
fosse deployato fisicamente come margine (−€164..227).
---
## Scettico incrociato — verdetto e disambiguazione critica
I 2 sopravvissuti (sizing-conferma, anchor-ensemble) usano leve diverse (posizione sulla frontiera
vs varianza della stima), ma lo scettico ha trovato **tre sovrapposizioni che i refuter singoli non
potevano vedere**:
1. **Stessa dipendenza dal modello f** — entrambi flippano segno allo stesso punto (f=0.6-0.8 →
negativo). Due sopravvissuti = **un solo grado di libertà di rischio-modello** (il caveat "premio
MODELLATO su DVOL ATM" resta il collo di bottiglia dell'intera famiglia).
2. **⚠️ CONFLITTO DI UNITÀ sul "12%"** (il punto più pericoloso per la narrativa): il **12% di PESO
del book** (convenzione pnl/Ks di `sleeves.py`) = ~**0.6% margine/equity ≈ 0.014 Kelly**, mentre
il "FISSO 12%" del filone sizing = **12% margine/equity ≈ 0.27 Kelly****fattore 19x**. Entrambe
corrette internamente, ma non vanno confuse: worst-week 12.2% del conto (convenzione margine) vs
0.64% (convenzione peso book).
3. **L'ensemble-7-fasi è ridondante con il tranching TP01** (matematica di diversificazione: corr fra
fasi ρ=0.355, boost teorico ×1.50 vs osservato ×1.53, residuo alpha ≈ 0) → **non promuovere a LEAD
autonomo**; la coda strutturale è invariata (worst = 7 tranche a full-loss simultaneo), il DD 4.7%
è campionario, non un cap.
**Effetto congiunto sul book 5-sleeve: ZERO** (entrambi propongono nessun cambio; controfattuale
|ΔSh| ≤ 0.04). Il book live Deribit (TP01+SKH01) non contiene VRP01 → impatto operativo nullo.
---
## Cosa entra nella narrativa (una sola voce, status = misura research)
- **Audit anchor-luck VRP01 CHIUSO** → i numeri di ammissione reggono e si citano con banda
(ShFULL [1.09,1.83], ShHOLD [0.03,1.11], DD [5.7,11.8%]); l'edge OOS resta f-dipendente.
Con questo l'audit anchor è completo su 4/4 sleeve ancorati.
- **Sizing (conferma, con disambiguazione unità obbligatoria):** 12% deploy ≈ 0.27 Kelly onesto,
anti-rovina; NON confondere col 12% di peso book (0.014 Kelly, 19x).
- **VRP01 canonico resta lo sleeve giusto, INVARIATO** — non migliorabile per struttura, gate,
overlay o sizing; la protezione DD si compra con la size.
## Regole/candidati rafforzati
- **`implausible_sharpe` gate in altlib**: 3ª occorrenza del pattern "0-perdite = Sharpe
implausibile" (griglia struttura, dopo CC01 e ALB-A) → priorità alzata.
- **Nuova regola metodologica**: un gate che si correla con un errore di modello (term-structure vs
BS-flat) è indistinguibile da alpha finché non si corregge il modello — riprezzare
term-structure-consistent prima di credere a qualsiasi gate vol su strutture BS-flat.
- **Conferma (4ª): l'alpha del VRP è il gate IV-rank binario**, non struttura/sizing/overlay/gate
aggiuntivi.
**Stato:** book live INVARIATO, nessun nuovo sleeve, nessun forward-monitor. Test suite verde. 7
script committati.
@@ -0,0 +1,109 @@
# 2026-07-07 — Video-claim "CRT top-down multi-TF, 74% win rate": SCARTATO (il 74% è un knob, non un edge)
**Richiesta utente:** analizzare e testare una strategia da video, metodo ICT/SMC top-down
multi-timeframe con claim **74% win rate**:
1. **H1** — setup CRT: candela di displacement forte, poi candela che prende liquidità (spike oltre
il range di C1) ma richiude dentro → falso breakout → fade/reversal.
2. **M15** — struttura: swing (U a 3 candele) + imbalance che diventa inverse-imbalance (FVG mitigato).
3. **M5** — conferma: displacement direzionale + zona di protezione (order block / imbalance).
4. **M1** — entry: attende la correzione, entra con SL dietro la zona di protezione.
Uscita: **TP1 a fine zona CRT (RR ~1.52), chiude 7080% lì → resto a break-even → runner 2030%**
verso la "liquidità successiva" (previous daily high/low). Due esempi mostrati (gold, nasdaq), entrambi
vincenti. Metà finale del video = vendita academy → lead magnet.
La valutazione critica dell'utente ("occhio Milito") aveva già isolato le red flag giuste (74% non
dimostrato, cherry-picking di 2 esempi, concetti discrezionali non falsificabili, RR basso + WR alto =
fragile, funnel commerciale). Questo diario **meccanizza e falsifica** la parte testabile.
## Inventario preventivo (non rifare lavoro)
Il setup H1 CRT è **già stato meccanizzato e scartato 3 volte** nell'onda 2026-07-02:
- `r0702_crt_base` (864 trial, 1h/4h/12h/1d): **DSR 0.000**; short "smart-money" negativo perfino IS.
- `r0702_crt_mtf` (4h→15m, 1h→5m, ~10k trade): **expectancy netta negativa OVUNQUE**; refutazione
strutturale = **"il ritest è informazione negativa"** (pattern CON ritest 40bps, SENZA +52bps).
- `r0702_crt_context` (FVG/sessioni/livelli): FVG semmai peggiora; **FADE < FOLLOW ogni anno 2019-26**.
**Angolo NUOVO non coperto** = la **gestione d'uscita (parziale+BE+runner)** e il **claim 74% WR**.
È lì che ho concentrato il test: `scripts/research/r0707_crt_topdown.py` (non tocca src/config/live;
nessun file scritto). Riusa l'harness certificato (dati/atr/sharpe/DSR di `altlib`).
## Disegno
- Detection CRT C1-C2 su **H1** (causale, nota alla chiusura di C2). Direzione = fade dello sweep.
- Entry sul TF basso (**5m/15m**, proxy di M5/M1 — ⚠️ **M1 non è nel feed certificato**): ritest della
zona violata + close-back attraverso il livello ("displacement di conferma"), **SL = estremo dello
swing basso = zona di protezione**.
- Tre schemi d'uscita sugli **stessi ingressi**: `fix1.5` (full, TP 1.5R), `fix2.0` (full, TP 2.0R),
`managed` (75% a 1.5R → resto 25% a BE, runner target = prev-daily high/low o estremo opposto C1).
- Gate progetto: hold-out 2025-01-01, griglia (k, s, d, tf, scheme) con **selezione cella solo
in-sample** + **DSR su tutti i trial**, **fee sweep** 0/0.10/0.20% RT, executability $600.
- Metrica invariante = **expectancy in R netto fee** (non il win-rate).
## Esito — il 74% è un artefatto della distanza del target, non un edge
**1) Win-rate reale (fade, tutti gli schemi, pooled BTC+ETH):**
| schema | WR mediana | expectancy R (netto 10bps) |
|---|---|---|
| fix1.5 | **35.8%** | 2.38 |
| fix2.0 | 31.3% | 2.34 |
| managed | **35.8%** | 2.17 |
Il WR a RR 1.5 è **~35%**, cioè **SOTTO** il null gambler's-ruin di un random walk senza edge
(P(+1.5R prima di 1R) = 1/(1+1.5) = **40%**). Il setup tocca 1.5R **meno spesso di una moneta**
conferma indipendente che *il ritest è informazione negativa*. Lo schema `managed` **non gonfia** il
WR sopra `fix1.5` (per costruzione: "vinci" solo se raggiungi il parziale a 1.5R, che è la stessa
condizione di `fix1.5`). **In nessuna cella si avvicina al 74%.**
**2) Il win-rate È un knob — sweep del target parziale `rr1` (managed, 5m, cella tipica):**
| rr1 | WR reale | null 1/(1+rr1) | expR (fee 10bps) | expR (fee 0) |
|------|----------|----------------|------------------|--------------|
| 0.50 | 51.4% | 66.7% | **2.54** | +0.11 |
| 0.75 | 46.9% | 57.1% | 2.51 | +0.14 |
| 1.00 | 43.2% | 50.0% | 2.47 | +0.17 |
| 1.50 | 36.3% | 40.0% | 2.45 | +0.19 |
| 2.00 | 31.8% | 33.3% | 2.42 | +0.23 |
Avvicinando il target il WR sale (esattamente come previsto dalla formula gambler's-ruin): **un "74%"
si fabbrica scegliendo un parziale vicino** (~0.35R), NON perché il metodo indovina la direzione. Ma
l'**expectancy R è costante e negativa a ogni rr1**: il WR alto non porta soldi. NB anche a WR massimo
il setup resta *sotto* il null teorico ad ogni rr1 → è pure peggio di un random walk nel raggiungere
target vicini.
**3) Gate statistici (cella scelta in-sample = 15m, k1.5, s0.1, d0.15, fix2.0):**
- **DSR = 0.000** su 48 trial (null max-Sharpe atteso 4.35). FAIL.
- **Fee sweep:** a **0 bps** expR = 0.10 e Sharpe book 0.63 → **non è morte-per-fee: l'edge lordo
non esiste** (il residuo lordo marginalmente positivo su 5m è la beta di trend dei time-exit, la
stessa autopsia del CRT base). A 10bps expR 1.26; a 20bps 2.43. Stop stretti (0.130.24%) →
la fee 0.10% pesa 0.752.6 R per trade → qualunque micro-edge lordo è polverizzato.
- **Hold-out 2025-26:** WR 32%, expR 1.56 (peggio dell'IS). Nessuna reggenza fuori campione.
**4) Executability $600:** SL sul TF basso = 0.13% (5m) / 0.24% (15m). Lo schema parziale+BE+runner
richiede **34 ordini per trade**, alcuni **sotto il min-order $5** a nozionale $1200 → non eseguibile
pulito al capitale reale.
## Verdetto
**SCARTATO.** Il claim "74% win rate" è **coerente con ZERO edge**: è la scelta di un parziale vicino
(gambler's ruin), non capacità direzionale. Smontato lo schema d'uscita, resta il setup H1 CRT che
questo progetto ha già triplo-refutato (DSR 0.000, ritest = informazione negativa, fade < follow).
Sul feed certificato BTC/ETH il metodo ha **win-rate reale ~3037% e expectancy R negativa a ogni
schema, fee, finestra e asset**; lordo di fee non c'è edge; a $600 non è nemmeno eseguibile pulito.
La parte *utile* del video è il framework di lettura (CRT + imbalance + parziali), non il numero — come
aveva già concluso la valutazione dell'utente. Da trattare come lettura del mercato, non come sistema.
## Lezione codificabile (candidata)
**Il win-rate di uno schema parziale+BE non è un parametro di merito: è ~P(tocca il primo target prima
dello stop) = 1/(1+rr1) per un processo senza drift.** Ogni claim "WR X%" da schema SMC va convertito
in **expectancy R netto fee** prima di crederci; il WR alto si compra con un target vicino a costo di
un'expectancy identica (o peggiore). Prossimo giro di gate: un helper `winrate_is_a_knob()` che, dato
un book a schema parziale, riporta expectancy-R vs WR al variare del target (rende esplicito l'artefatto).
## File
- `scripts/research/r0707_crt_topdown.py` — il test (griglia + 74%-check + knob-sweep + DSR + fee + exec).
- Prior: `r0702_crt_{base,mtf,context}.py`, diario `2026-07-02-timing-crt-wave.md`.
- Nessuna modifica a src/, config/, scripts/live/, tests/. Book live e pesi INVARIATI.
@@ -0,0 +1,108 @@
# 2026-07-09 — Incident infra: "BOOK LIVE conto offline" = cert Traefik di default (non un problema del conto)
**Tipo:** incident operativo (infra VPS), non ricerca. Nessun edge, nessun cambio a strategie/pesi/config
di trading. Documentato qui perché ha **bloccato la lettura del conto del book live** e ha prodotto
l'alert Telegram `⚠️ BOOK LIVE — conto offline`.
## Sintomo
Alert dal `cron_book` orario (`scripts/live/book_execute.py --execute`):
```
⚠️ BOOK LIVE — conto offline
nota: salto l'esecuzione, non opero a cieco
```
Nel log (`logs/cron_book.log`) le run recenti mostravano `conto non leggibile (offline) -> stop, non
eseguo a cieco`. 10 run offline su 382 totali → **regressione recente**, non uno stato storico.
## Diagnosi (catena completa)
Il book legge il conto Deribit mainnet in sola lettura via **Cerbero MCP** (`DeribitRead`,
`src/live/deribit.py``https://cerbero-mcp.tielogic.xyz`). `book_report` marca `online=False` quando
`DeribitRead.mark_price(BTC)` lancia. Riproduzione diretta:
```
mark_price BTC: FAIL -> SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] self-signed certificate
```
Il proxy presentava il **cert self-signed di default di Traefik** (`CN = TRAEFIK DEFAULT CERT`,
`notBefore Jul 9 19:55 2026`) al posto del vero Let's Encrypt. `curl -k` → HTTP 401 (backend
`cerbero-mcp` healthy, routing OK): **solo la presentazione del cert TLS era rotta**. `requests`
verifica il cert → fallisce → `online=False` → il book salta l'esecuzione (comportamento di sicurezza
**corretto**: non opera a cieco).
**Causa a monte** (log di avvio Traefik, 11:37:16Z, container `traefik-traefik-1`):
```
ERR The ACME resolve is skipped from the resolvers list
error="unable to get ACME account: permissions 660 for /acme.json are too open, please use 600"
resolver=mytlschallenge
→ (a cascata) Router uses a nonexistent certificate resolver certificateResolver=mytlschallenge
routerName=cerbero-mcp@docker [+ ~14 altri router: git, vault, portfolio, budget, cerbero-bite, ...]
```
Sequenza:
1. Immagine `traefik:latest` + **Watchtower** (`com.centurylinklabs.watchtower.enable=true`) →
auto-upgrade a **Traefik 3.7.7** con restart alle 11:37 di oggi.
2. Traefik 3.x **rifiuta** `acme.json` con permessi più larghi di `600`; il file era `660`
(`-rw-rw---- root:adriano`) → **resolver `mytlschallenge` scartato**.
3. Tutti i ~15 router che lo referenziano → "nonexistent certificate resolver" → Traefik serve il
**cert di default self-signed** a ogni host HTTPS del VPS (non solo cerbero-mcp).
4. I cert veri erano **già dentro `acme.json`** (14 domini, incl. `cerbero-mcp.tielogic.xyz`) → nessuna
ri-emissione necessaria, **zero rischio rate-limit** Let's Encrypt.
## Impatto
- **VPS-wide:** ogni servizio HTTPS dietro Traefik serviva un cert invalido (warning browser, fallimento
di ogni client che verifica il TLS). Non solo il trading.
- **Trading:** **nessun trade perso.** Target del book **flat** (TP01/SKH01 risk-off) + conto reale
~$598 (non finanziato al livello nominale): anche online il book avrebbe fatto HOLD. Il doppio gate
di sicurezza ha lavorato come previsto.
## Fix
**Immediato** (eseguito dall'utente):
```bash
sudo chmod 600 /opt/docker/traefik/acme.json
docker restart traefik-traefik-1
```
Il resolver ricarica i cert già presenti in `acme.json` e li ripresenta subito.
**Durevole** (pin dell'immagine, `/opt/docker/traefik/docker-compose.yml`):
```yaml
- image: "traefik" # = traefik:latest → Watchtower salta ai major (causa dell'incident)
+ image: "traefik:3.7" # Watchtower resta su 3.7.x: patch sì, salto a 3.8+ breaking no
```
Applicato con `docker compose up -d` (container ricreato su `traefik:3.7`, 20:05:05Z).
## Verifica (end-to-end, post-fix)
| Check | Risultato |
|---|---|
| `acme.json` perms | `600` |
| Cert `cerbero-mcp.tielogic.xyz` | issuer **Let's Encrypt** (fino 2026-09-27) |
| Traefik log | nessun `ACME resolve is skipped` / `too open` |
| Container image | `traefik:3.7` (running) |
| `DeribitRead.mark_price` | BTC 63.193 / ETH 1.746 |
| `book_report.online` | **True** · `eq_basis=mainnet USDC` · `real_equity=$598.06` · `pos_error=None` |
## Lezioni
- **`traefik:latest` + Watchtower = auto-upgrade non deterministico.** Un bump di major (qui il giro
di vite sui permessi di `acme.json`) diventa un'interruzione TLS di tutto il VPS senza intervento
umano. Pinnare la minor (`3.7`) rende i restart prevedibili pur ricevendo le patch.
- **Traefik 3.x pretende `acme.json` a `600`** (non solo warning: **scarta il resolver**). Se il file
è group/other-readable, tutti i router cadono sul cert di default.
- **Il gate `online` del book ha fatto il suo lavoro:** un problema *infra di lettura* non ha prodotto
ordini a cieco. L'alert "conto offline" va letto prima come possibile problema di **connettività/cert
al Cerbero MCP**, non del conto Deribit.
- **Runbook rapido** per "BOOK LIVE conto offline":
1. `python -c "from src.live.deribit import DeribitRead; DeribitRead().mark_price('BTC-PERPETUAL')"`
→ se `SSLError`/`CERTIFICATE_VERIFY_FAILED` è il cert del proxy, non il conto.
2. `openssl s_client -connect cerbero-mcp.tielogic.xyz:443 -servername cerbero-mcp.tielogic.xyz | openssl x509 -noout -issuer`
→ se `TRAEFIK DEFAULT CERT`, Traefik non serve il cert reale.
3. `docker logs traefik-traefik-1 | grep -iE "ACME resolve is skipped|too open|nonexistent"`
→ conferma resolver scartato; controlla `stat -c %a /opt/docker/traefik/acme.json` (deve essere `600`).
Nessun file di trading toccato; `config/live.json`, sleeve e pesi **invariati**.
+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
+9 -1
View File
@@ -10,6 +10,14 @@ mkdir -p logs
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_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)
uv run python scripts/live/paper_statarb.py # forward-monitor lead STATARB-RESID ETH/BTC ortogonale (PAPER, non deploy)
# 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 ---
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
uv run --with ib_async python scripts/research/fetch_ib_equities.py --only SPY,QQQ,IWM,TLT,GLD,HYG # ETF GTAA freschi
uv run python scripts/live/paper_combo.py # avanza paper combo (forward-only)
echo "===== done $(date -u '+%H:%M:%SZ') ====="
} >> logs/cron_daily.log 2>&1
+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
+162
View File
@@ -0,0 +1,162 @@
"""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 r.get("skh_error"): # SKH feed fallito -> book.py ha forzato flat IN SILENZIO
print(f" ⚠️ SKH FEED ERRORE (SKH forzato flat!): {r['skh_error']}")
notify("⚠️ BOOK — SKH feed fallito (sleeve forzato flat)", {"error": r["skh_error"]})
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
if r.get("pos_error"): # ONLINE ma posizione IGNOTA (read fallita -> assunta flat)
print(f" 🛑 POSIZIONE NON LEGGIBILE -> NON eseguo a cieco: {r['pos_error']}")
if do_execute:
notify("🛑 BOOK LIVE — posizione non leggibile", {"error": r["pos_error"],
"nota": "salto l'esecuzione, non opero a cieco"})
return
if r.get("eq_fallback"): # equity reale non leggibile -> sizing su paper_cap
print(f" ⚠️ EQUITY FALLBACK (sizing su paper_cap, NON blocco): {r['eq_fallback']}")
if do_execute: # solo diagnostica: l'hard-cap $/asset limita il downside
notify("⚠️ BOOK LIVE — equity fallback (sizing su paper_cap)", {"nota": r["eq_fallback"]})
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()
+117
View File
@@ -0,0 +1,117 @@
"""PAPER COMBO — forward-only del combo cross-venue TP01 (Deribit) + GTAA (IB), NUDO vs PROTETTO.
Le due gambe eseguibili a basso capitale (XS01/VRP01 STAT-MODE esclusi), scorrelate (corr ~0.21) ->
blend Sharpe ~1.5, DD dimezzato. Traccia FORWARD-ONLY DUE versioni in parallelo:
* NUDO = blend 50/50 TP01+GTAA
* PROTETTO = stesso blend + GUARDIA-DRAWDOWN -4% (de-risk a 0.4x quando il DD da picco supera -4%,
ri-rischia a -1.6%). Backtest: MaxDD 8.4%->5.8%, 2022 -4.4%->-1.8%, Sharpe 1.48->1.38
(diario 2026-06-23-tail-hedge-lab). Le opzioni NON aiutano il grind del 2022 -> escluse.
Crypto compoundato sul grid giorni-di-borsa. NESSUNA esecuzione reale. Mostra posizioni azionabili.
uv run python scripts/live/paper_combo.py [--status|--reset]
"""
from __future__ import annotations
import sys, json
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
import numpy as np, pandas as pd
from src.portfolio.sleeves import _tp01_returns, _tp01_positions
from src.portfolio.gtaa import gtaa_returns, gtaa_weights
STATE_DIR = PROJECT_ROOT / "data" / "paper_combo"
STATE = STATE_DIR / "state.json"
EQ = STATE_DIR / "equity.csv"
INITIAL = 2000.0
W_CRYPTO = 0.5
DD_TRIGGER = 0.04 # guardia-drawdown della versione PROTETTA
def combo_daily(wc: float = W_CRYPTO) -> pd.Series:
tp = _tp01_returns()
if tp.index.tz is None:
tp.index = tp.index.tz_localize("UTC")
eq = gtaa_returns().dropna()
grid = eq.index[eq.index >= tp.index[0]]
cum = (1.0 + tp).cumprod()
tpg = (cum.reindex(cum.index.union(grid)).ffill().reindex(grid)).pct_change()
J = pd.concat({"c": tpg, "e": eq.reindex(grid)}, axis=1).dropna()
return (wc * J["c"] + (1 - wc) * J["e"]).dropna()
def apply_dd_guard(r: pd.Series, trigger: float = DD_TRIGGER) -> pd.Series:
"""De-risk a 0.4x quando il DD da picco > trigger; ri-rischia a 1.0x quando < 0.4*trigger."""
rv = r.values; n = len(rv); eq = np.cumprod(1 + rv); pk = np.maximum.accumulate(eq)
expo = np.ones(n); on = True
for i in range(1, n):
ddi = (pk[i - 1] - eq[i - 1]) / pk[i - 1] if pk[i - 1] > 0 else 0.0
if ddi > trigger: on = False
if ddi < trigger * 0.4: on = True
expo[i] = 1.0 if on else 0.4
return pd.Series(expo * rv, index=r.index)
def both_daily():
naked = combo_daily()
return naked, apply_dd_guard(naked)
def load():
return json.loads(STATE.read_text()) if STATE.exists() else None
def save(st):
STATE_DIR.mkdir(parents=True, exist_ok=True)
STATE.write_text(json.dumps(st, indent=2))
def advance():
naked, guard = both_daily()
st = load()
if st is None or "equity_g" not in st: # init (o migrazione a doppia versione)
last = str(naked.index[-1])
st = dict(start=last, last=last, initial=INITIAL, n_days=0, w_crypto=W_CRYPTO, dd_trigger=DD_TRIGGER,
equity=INITIAL, peak=INITIAL, max_dd=0.0,
equity_g=INITIAL, peak_g=INITIAL, max_dd_g=0.0)
save(st); EQ.write_text("date,nudo,protetto\n" + f"{last},{INITIAL},{INITIAL}\n")
return st
last = pd.Timestamp(st["last"])
nn = naked[naked.index > last]; gg = guard[guard.index > last]
if len(nn):
e = st["equity"]; pk = st["peak"]; dd = st["max_dd"]
eg = st["equity_g"]; pkg = st["peak_g"]; ddg = st["max_dd_g"]; lines = []
for d in nn.index:
e *= (1 + float(nn[d])); pk = max(pk, e); dd = max(dd, (pk - e) / pk if pk > 0 else 0)
eg *= (1 + float(gg[d])); pkg = max(pkg, eg); ddg = max(ddg, (pkg - eg) / pkg if pkg > 0 else 0)
lines.append(f"{d},{e:.4f},{eg:.4f}")
st.update(equity=e, peak=pk, max_dd=dd, equity_g=eg, peak_g=pkg, max_dd_g=ddg,
last=str(nn.index[-1]), n_days=st["n_days"] + len(nn))
save(st)
with open(EQ, "a") as f:
f.write("\n".join(lines) + "\n")
return st
def main():
a = sys.argv[1:]
if "--reset" in a:
for f in (STATE, EQ):
f.unlink(missing_ok=True)
print("paper combo azzerato.")
st = load() if "--status" in a else advance()
if st is None or "equity_g" not in st:
st = advance()
days = (pd.Timestamp(st["last"]) - pd.Timestamp(st["start"])).days
rn = st["equity"] / st["initial"] - 1; rg = st["equity_g"] / st["initial"] - 1
gw = gtaa_weights(); asof = gw.pop("_asof", "?"); cash = gw.pop("_cash", None)
print("PAPER COMBO — TP01 (Deribit) + GTAA (IB), forward-only, blend 50/50")
print(f" start {st['start'][:10]} -> last {st['last'][:10]} ({days}g, {st['n_days']} barre)")
print(f" NUDO : eq {st['equity']:.2f} ret {rn*100:+.2f}% maxDD {st['max_dd']*100:.1f}%")
print(f" PROTETTO : eq {st['equity_g']:.2f} ret {rg*100:+.2f}% maxDD {st['max_dd_g']*100:.1f}% (guardia-DD -{st.get('dd_trigger',DD_TRIGGER)*100:.0f}%)")
print(f" --- POSIZIONI AZIONABILI ---")
print(f" TP01 (Deribit): {_tp01_positions()}")
print(f" GTAA (IB, asof {asof}): " + ", ".join(f"{k} {v:.0%}" for k, v in gw.items() if v) + f" | cash {cash:.0%}")
if __name__ == "__main__":
main()
+176
View File
@@ -0,0 +1,176 @@
"""FORWARD-MONITOR — STATARB-RESID (relative-value ETH/BTC, dollar-neutral 2 gambe), PAPER.
NON è esecuzione reale. È il monitoraggio forward-only del LEAD dello sweep 2026-06-29 (thread
"segnali ortogonali"): il PRIMO stream visto che è INSIEME ortogonale al book (corrbook 0.027,
beta-mkt 0.013) ED eseguibile a $600 (book a 2 gambe BTC+ETH perp, NON STAT-MODE come XS01). Resta
sotto la soglia solo sull'EDGE (Sharpe ~0.84, deflated-Sharpe <0.95 su storia ~2.5 anni). Questo
monitor accumula la finestra OUT-OF-SAMPLE VERA per decidere se l'edge regge — stesso trattamento di
PREVDAY/STA05/XS01 STAT-MODE. Nessun edge creduto prima.
NATURA DEL SEGNALE (onesto): la cella vincente è W=45 **sgn=+1**, NON sgn=-1. Cioè il residuo di
ETH dopo β·BTC (β OLS rolling causale) viene seguito in MOMENTUM (long lo spread quando ETH è ricco
vs il suo fit su BTC), non in mean-reversion: le dislocazioni relative ETH-vs-BTC CONTINUANO a 1d
(la MR pura, sgn=-1, perde -1.4 in-sample). È relative-MOMENTUM, dollar-neutral ortogonale al
trend assoluto di TP01.
CONFIG CONGELATA (dallo sweep, selezione IN-SAMPLE-only, mai sul max hold-out):
W=45, sgn=+1, tf=1d, vol-target 20%, leva cap 2x. Riusa il segnale ESATTO di
scripts/research/orthogonal_signals.py (nessuna reimplementazione niente drift).
DESIGN (onesto, come paper_prevday):
- Legge BTC/ETH 1d certificati (build_joint), calcola il segnale CAUSALE su tutta la storia.
- Alla prima esecuzione fissa l'INCEPTION all'ultima barra 1d chiusa (forward-only: lo storico
NON entra nel PnL di paper).
- Ogni run processa le NUOVE barre 1d chiuse: applica il rendimento dello spread tenuto,
addebita le fee su 2 GAMBE (ogni Δpos muove ETH e BTC 2×|Δpos|), registra i flip di segno.
- DUE libri in parallelo (onestà sull'esecuzione a piccolo capitale):
* MODELED : capitale nominale $2000, ribilanciamento continuo (fee su ogni |Δpos|).
* REAL-$600: capitale reale $600, salta i ribilanci di nozionale-per-gamba < min_order ($5)
il gap MODELED-REAL = l'haircut di fill reale (qui ~0: turnover bassissimo a 1d).
- Per barre fresche aggiornare prima i dati: uv run python scripts/analysis/rebuild_history.py --asset BTC ETH
Stato: data/paper_statarb/{state.json, trades.jsonl, returns.jsonl} (append-only).
uv run python scripts/live/paper_statarb.py # avanza col dato disponibile
uv run python scripts/live/paper_statarb.py --status # solo stato, non avanza
uv run python scripts/live/paper_statarb.py --reset # azzera (riparte da ora)
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
import numpy as np
import pandas as pd
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
sys.path.insert(0, str(PROJECT_ROOT / "scripts" / "research"))
# Segnale ESATTO dello sweep (nessuna reimplementazione → identità garantita col backtest).
from orthogonal_signals import build_joint, f_statarb_resid, spread_ret # noqa: E402
STATE_DIR = PROJECT_ROOT / "data" / "paper_statarb"
STATE_FILE = STATE_DIR / "state.json"
TRADES_FILE = STATE_DIR / "trades.jsonl"
RETURNS_FILE = STATE_DIR / "returns.jsonl"
# --- CONFIG CONGELATA (frozen) -----------------------------------------------------------
W_FROZEN = 45
SGN_FROZEN = +1
FEE_SIDE = 0.0005 # 0.05%/side = 0.10% round-trip (Deribit taker)
MODELED_CAPITAL = 2000.0 # nominale, ribilanciamento continuo
REAL_CAPITAL = 600.0 # capitale mainnet reale
MIN_ORDER = 5.0 # min order Deribit -> sotto, il conto vero NON ribilancia
def _signal(j: pd.DataFrame):
"""(ts, dt, pos, sr): posizione vol-targeted sullo spread (decisa <= close[i]) + ritorno spread."""
pos = np.nan_to_num(f_statarb_resid(W=W_FROZEN, sgn=SGN_FROZEN)(j))
return (j["timestamp"].values.astype("int64"),
pd.to_datetime(j["datetime"]).values, pos, spread_ret(j))
def _state_io(write: dict | None = None):
if write is not None:
STATE_DIR.mkdir(parents=True, exist_ok=True)
STATE_FILE.write_text(json.dumps(write, indent=2))
return write
return json.loads(STATE_FILE.read_text()) if STATE_FILE.exists() else None
def _append(path: Path, rec: dict):
STATE_DIR.mkdir(parents=True, exist_ok=True)
with open(path, "a") as f:
f.write(json.dumps(rec) + "\n")
def init_state(j: pd.DataFrame) -> dict:
ts, _, pos, _ = _signal(j)
last = int(ts[-1])
return dict(start_ts=last, last_ts=last, n_bars=0,
pos_modeled=float(pos[-1]), pos_real=float(pos[-1]),
cap_modeled=MODELED_CAPITAL, cap_real=REAL_CAPITAL,
peak_modeled=MODELED_CAPITAL, peak_real=REAL_CAPITAL,
dd_modeled=0.0, dd_real=0.0, n_trades=0)
def advance(st: dict, j: pd.DataFrame) -> dict:
ts, dt, pos, sr = _signal(j)
new = [i for i in range(len(ts)) if ts[i] > st["last_ts"]]
if not new:
return st
pm, pr = st["pos_modeled"], st["pos_real"] # posizioni TENUTE (decise alla barra precedente)
cm, cr = st["cap_modeled"], st["cap_real"]
pkm, pkr, ddm, ddr = st["peak_modeled"], st["peak_real"], st["dd_modeled"], st["dd_real"]
ntr = st.get("n_trades", 0)
for i in new:
s = float(sr[i])
tgt = float(pos[i]) # bersaglio deciso a close[i] (eseguito da i+1)
# MODELED: rendimento dello spread tenuto durante la barra i, poi ribilancia al target
net_m = pm * s - FEE_SIDE * 2.0 * abs(tgt - pm)
# REAL-$600: salta i ribilanci sub-min_order (nozionale per-gamba |Δpos|*cap < $5)
executed = abs(tgt - pr) * REAL_CAPITAL >= MIN_ORDER
new_pr = tgt if executed else pr
net_r = pr * s - FEE_SIDE * 2.0 * abs(new_pr - pr)
if np.sign(new_pr) != np.sign(pr):
_append(TRADES_FILE, dict(ts=int(ts[i]), dt=str(pd.Timestamp(dt[i])),
action="FLIP", from_pos=round(pr, 4), to_pos=round(new_pr, 4)))
ntr += 1
cm *= (1.0 + max(net_m, -0.99)); cr *= (1.0 + max(net_r, -0.99))
pkm = max(pkm, cm); pkr = max(pkr, cr)
ddm = max(ddm, (pkm - cm) / pkm if pkm > 0 else 0.0)
ddr = max(ddr, (pkr - cr) / pkr if pkr > 0 else 0.0)
pm, pr = tgt, new_pr
_append(RETURNS_FILE, dict(ts=int(ts[i]), dt=str(pd.Timestamp(dt[i])),
net_modeled=round(net_m, 6), net_real=round(net_r, 6),
pos=round(new_pr, 4), cap_modeled=round(cm, 2), cap_real=round(cr, 2)))
st.update(last_ts=int(ts[new[-1]]), n_bars=st.get("n_bars", 0) + len(new),
pos_modeled=pm, pos_real=pr, cap_modeled=cm, cap_real=cr,
peak_modeled=pkm, peak_real=pkr, dd_modeled=ddm, dd_real=ddr, n_trades=ntr)
return st
def print_status(st: dict, j: pd.DataFrame):
last_data = int(j["timestamp"].iloc[-1])
days = (last_data - st["start_ts"]) / 86400_000
rm = st["cap_modeled"] / MODELED_CAPITAL - 1
rr = st["cap_real"] / REAL_CAPITAL - 1
print("\n STATARB-RESID forward-monitor (PAPER — relative-value ETH/BTC, ortogonale, NON deploy)")
print(f" config CONGELATA: W={W_FROZEN} sgn={SGN_FROZEN:+d} (residual relative-momentum, dollar-neutral)")
print(f" forward da {pd.Timestamp(st['start_ts'], unit='ms', tz='UTC').date()} "
f"({st['n_bars']} barre 1d ~{days:.0f}g) flip di segno: {st['n_trades']}")
print(f" posizione spread corrente (long ETH / short BTC per unità): {st['pos_real']:+.3f}")
print(f" MODELED ($2000 nominale): {rm*100:+6.2f}% eq ${st['cap_modeled']:.2f} maxDD {st['dd_modeled']*100:.1f}%")
print(f" REAL-$600 (min-order $5) : {rr*100:+6.2f}% eq ${st['cap_real']:.2f} maxDD {st['dd_real']*100:.1f}%")
print(f" -> fill-haircut MODELED-REAL: {(rm-rr)*100:+.2f} pp (atteso ~0: turnover basso a 1d)")
print(f" log: {RETURNS_FILE}\n")
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--status", action="store_true")
ap.add_argument("--reset", action="store_true")
args = ap.parse_args()
j = build_joint("1d")
if args.reset:
for p in (STATE_FILE, TRADES_FILE, RETURNS_FILE):
if p.exists():
p.unlink()
st = init_state(j); _state_io(st)
print("forward-monitor STATARB-RESID inizializzato (forward-only da ora).")
print_status(st, j); return
st = _state_io()
if st is None:
st = init_state(j); _state_io(st)
print("forward-monitor STATARB-RESID inizializzato (forward-only da ora).")
print_status(st, j); return
if not args.status:
st = advance(st, j); _state_io(st)
print_status(st, j)
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()
+90
View File
@@ -29,12 +29,14 @@ from __future__ import annotations
import inspect
import json
import math
import sys
from functools import lru_cache
from pathlib import Path
import numpy as np
import pandas as pd
from scipy.stats import norm
# --- make `from src...` work no matter where the agent's script lives -------
_ROOT = Path(__file__).resolve().parents[3]
@@ -670,6 +672,94 @@ def causality_ok(target_fn, tf: str = "1h", assets=CERTIFIED,
reason=("length-mismatch on prefix" if bad else None))
# ===========================================================================
# SELECTION-ON-HOLDOUT GATE — codified 2026-06-29 from filone B (intraday ERM).
#
# LESSON 3: intraday_regime.py picked its "winner" cell by MAX hold-out Sharpe over a ~60-cell
# grid, then ran study_marginal on THAT cell -> earns_slot=True. But the slot was an artifact of
# SELECTING THE CELL ON THE HOLD-OUT: picking the cell IN-SAMPLE-ONLY (no peeking) lands on a
# DIFFERENT, TP01-correlated cell that scores NEUTRAL, and the standalone Sharpe deflates to
# DSR~0.0-0.24 over the trials searched. study_marginal alone can't catch this — it judges ONE
# stream and never sees how the cell was chosen. The fix is two-fold and lives here:
# (1) choose the cell IN-SAMPLE-ONLY (or walk-forward) BEFORE scoring the marginal, and
# (2) DEFLATE the standalone Sharpe for the number of cells/families searched.
# Twin of the CC01 ("implausible Sharpe -> hidden risk") and alt-sweep ("hold-out-fitting") blind
# spots, in its "selection-on-holdout" form.
# ===========================================================================
def deflated_sharpe(sr_ann, all_sr_ann, daily_ret, dpy: float = 365.25):
"""Deflated Sharpe Ratio (Bailey & Lopez de Prado): P(true Sharpe > the MAX Sharpe expected
under the null of N independent trials). Penalizes multiple-testing a standalone Sharpe ~1
over a 100+ cell grid is routinely NOT significant once deflated. sr_ann = annualized Sharpe
of the CHOSEN config; all_sr_ann = the Sharpe of EVERY cell searched; daily_ret = the chosen
config's daily returns (for skew/kurt/T). Returns (DSR, expected_null_max_sharpe_ann);
PASS if DSR >= 0.95."""
r = np.asarray(pd.Series(daily_ret).dropna().values, float)
T = len(r)
if T < 30 or np.std(r) == 0:
return float("nan"), float("nan")
sr = sr_ann / math.sqrt(dpy)
trials = np.asarray([s / math.sqrt(dpy) for s in all_sr_ann if np.isfinite(s)], float)
N = max(len(trials), 2)
var_tr = float(np.var(trials, ddof=1)) if N > 1 else 0.0
emc = 0.5772156649
z1 = norm.ppf(1 - 1.0 / N)
z2 = norm.ppf(1 - 1.0 / (N * math.e))
sr0 = math.sqrt(var_tr) * ((1 - emc) * z1 + emc * z2)
sk = float(pd.Series(r).skew())
ku = float(pd.Series(r).kurt()) + 3.0 # pandas kurt = excess
den = math.sqrt(max(1e-9, 1 - sk * sr + (ku - 1) / 4.0 * sr ** 2))
dsr = float(norm.cdf((sr - sr0) * math.sqrt(T - 1) / den))
return dsr, sr0 * math.sqrt(dpy)
def select_cell_insample(factory, grid, tfs, fee_side: float = FEE_SIDE) -> dict:
"""Pick a config WITHOUT looking at the hold-out: rank grid cells by IN-SAMPLE (pre-HOLDOUT)
standalone Sharpe of the 50/50 candidate. `factory(tf=..., **params)` -> target_fn; each grid
item is a dict of factory kwargs (besides tf). Returns the in-sample-best cell, all rows
(sorted), and EVERY cell's FULL Sharpe (for deflated_sharpe). This is the honest replacement
for picking the max-hold-out cell."""
rows = []
for tf in tfs:
for params in grid:
try:
daily = candidate_daily(factory(tf=tf, **params), tf=tf, fee_side=fee_side)
except Exception:
continue
ins = daily[daily.index < HOLDOUT]
is_sh = _sh(ins) if len(ins) > 60 else float("nan")
rows.append(dict(tf=tf, params=params, insample_sharpe=round(is_sh, 3),
full_sharpe=round(_sh(daily), 3)))
valid = [r for r in rows if np.isfinite(r["insample_sharpe"])]
chosen = max(valid, key=lambda r: r["insample_sharpe"]) if valid else None
return dict(chosen=chosen,
rows=sorted(valid, key=lambda r: r["insample_sharpe"], reverse=True),
all_full_sharpe=[r["full_sharpe"] for r in rows])
def study_family_honest(name: str, factory, grid, tfs, fee_side: float = FEE_SIDE,
dsr_min: float = 0.95) -> dict:
"""HARDENED family gate. A grid-searched directional candidate earns a slot ONLY if, picking
the cell IN-SAMPLE-ONLY (no hold-out peeking), it STILL earns_slot via study_marginal AND its
standalone Sharpe survives deflation for the WHOLE grid searched. Use this INSTEAD of
cherry-picking the max-hold cell and calling study_marginal on it."""
sel = select_cell_insample(factory, grid, tfs, fee_side=fee_side)
ch = sel["chosen"]
if ch is None:
return dict(name=name, chosen=None, earns_slot_honest=False,
reason="no valid in-sample cell")
fn = factory(tf=ch["tf"], **ch["params"])
sm = study_marginal(f"{name} ISpick {ch['params']}", fn, tf=ch["tf"], fee_side=fee_side)
daily = candidate_daily(fn, tf=ch["tf"], fee_side=fee_side)
dsr, sr0 = deflated_sharpe(_sh(daily), sel["all_full_sharpe"], daily)
dsr_pass = bool(np.isfinite(dsr) and dsr >= dsr_min)
return dict(name=name, n_cells=len(sel["all_full_sharpe"]), chosen=ch, rows=sel["rows"],
marginal=sm, earns_slot_marginal=bool(sm["earns_slot"]),
deflated_sharpe=round(dsr, 3) if np.isfinite(dsr) else None,
expected_null_max=round(sr0, 3) if np.isfinite(sr0) else None,
dsr_pass=dsr_pass,
earns_slot_honest=bool(sm["earns_slot"] and dsr_pass))
# ===========================================================================
# DRIVERS — run a hypothesis across both assets, several TFs, with a fee sweep.
# ===========================================================================
+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()
+74
View File
@@ -0,0 +1,74 @@
"""Simulazione LEVA 1x/2x/3x su COMBO (TP01+GTAA) e TP01-solo, da $2k e $5k.
Leva modellata onestamente: ritorno_giorno = L*r - (L-1)*financing/252 (costo del nozionale preso a
prestito ~8%/anno blended: perp funding crypto + margin IB). MaxDD calcolato sul PERCORSO LEVATO REALE
(non scalato: il compounding peggiora il DD oltre ×L). Check RUINA/margin-call: se l'equity tocca la
soglia di liquidazione (perdita cumulata >= 1/L del picco -> margin call).
CLAUDE.md: la leva NON e' la scorciatoia; raddoppia (e oltre) il drawdown. Caso base = 1x.
"""
import sys
from pathlib import Path
import numpy as np, pandas as pd
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT)); sys.path.insert(0, str(ROOT / "scripts" / "research"))
from combo_yearly_report import combo_daily
from src.portfolio.sleeves import _tp01_returns
FIN = 0.08 # costo finanziamento annuo sul nozionale preso a prestito (perp funding + margin IB), blended
def lever(ret: pd.Series, L: float) -> pd.Series:
return L * ret - (L - 1) * FIN / 252.0
def analyze(ret: pd.Series, L: float, cap0: float):
r = lever(ret.dropna().sort_index(), L)
curve = cap0 * np.cumprod(1 + r.values)
peak = np.maximum.accumulate(curve)
dd = (peak - curve) / peak
maxdd = float(np.max(dd))
# margin call: perdita dal picco >= 1/L (a leva L, un drawdown del sottostante di 1/L azzera il margine)
ruin = bool(np.any(dd >= 1.0 / L - 1e-9)) if L > 1 else False
yrs = (r.index[-1] - r.index[0]).days / 365.25
cagr = (curve[-1] / cap0) ** (1 / yrs) - 1 if yrs > 0 and curve[-1] > 0 else -1
sh = float(r.mean() / r.std() * np.sqrt(252)) if r.std() > 0 else 0
worst_y = min((np.prod(1 + r[r.index.year == y].values) - 1) for y in sorted(set(r.index.year)))
return dict(L=L, final=float(curve[-1]), cagr=cagr, maxdd=maxdd, sharpe=sh, ruin=ruin,
worst_y=float(worst_y), perday=(curve[-1] - cap0) / yrs / 365)
def main():
print("=" * 92)
print(" LEVA su COMBO vs TP01-solo — percorso reale (fin 8%/anno sul prestito), 2019-2026 (~7.3y)")
print("=" * 92)
strat = {"COMBO TP01+GTAA": combo_daily(), "TP01 solo (crypto)": _tp01_returns()}
for nm, r in strat.items():
if r.index.tz is None:
r.index = r.index.tz_localize("UTC")
for cap0 in (2000.0, 5000.0):
print(f"\n ##### capitale iniziale ${cap0:,.0f} #####")
print(f" {'strategia':20}{'leva':>5}{'CAGR':>8}{'MaxDD':>8}{'Sharpe':>8}{'pegg.anno':>10}{'$/giorno':>10}{'eq fine':>12}{' RUINA?':>9}")
for nm, r in strat.items():
for L in (1, 2, 3):
a = analyze(r, L, cap0)
flag = "MARGIN-CALL" if a["ruin"] else "ok"
print(f" {nm:20}{L:>4}x{a['cagr']*100:>7.1f}%{a['maxdd']*100:>7.1f}%{a['sharpe']:>8.2f}"
f"{a['worst_y']*100:>9.1f}%{('$'+format(a['perday'],',.0f')):>10}{('$'+format(a['final'],',.0f')):>12}{flag:>11}")
# per-anno del COMBO a 2x e 3x da 2k (dettaglio)
print(f"\n ##### COMBO per-anno a leva, da $2.000 #####")
cd = combo_daily()
for L in (2, 3):
print(f"\n --- COMBO {L}x ---")
print(f" {'anno':6}{'PnL %':>9}{'MaxDD %':>9}{'eq fine':>11}")
eq = 2000.0; r = lever(cd, L)
for y in sorted(set(r.index.year)):
ry = r[r.index.year == y]
if len(ry) < 5: continue
eq0 = eq; curve = eq0 * np.cumprod(1 + ry.values); peak = np.maximum.accumulate(curve)
ddp = float(np.max((peak - curve) / peak)); eq = float(curve[-1])
print(f" {y:<6}{(eq/eq0-1)*100:>+8.1f}%{ddp*100:>8.1f}%{('$'+format(eq,',.0f')):>11}")
if __name__ == "__main__":
main()
@@ -0,0 +1,90 @@
"""Resoconto anno-per-anno CON PROTEZIONE (soft-guard DD -4%) — combo e singoli.
Mette combo (TP01+GTAA 50/50), TP01 e GTAA sulla STESSA griglia giorni-di-borsa (come dentro al
combo), applica la guardia-DD -4% a ciascuna serie (de-risk 1.0->0.4 a -4% dal picco, ri-rischia a
-1.6%), e per ogni anno riporta: NL (net liquidation da $2000), DD intra-anno, rendimento (=CAGR
1y), Sharpe. Riga TOT con CAGR e Sharpe complessivi.
"""
import sys
from pathlib import Path
import numpy as np, pandas as pd
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from src.portfolio.sleeves import _tp01_returns
from src.portfolio.gtaa import gtaa_returns
INITIAL = 2000.0
ANN = np.sqrt(252.0)
DD_TRIG = 0.04
def dd_guard(r, dd_trigger=DD_TRIG):
"""De-risk: esposizione 1.0->0.4 se DD da picco > dd_trigger; ri-rischia a dd_trigger*0.4."""
r = r.values; n = len(r); eq = np.cumprod(1 + r); pk = np.maximum.accumulate(eq)
expo = np.ones(n); on = True
for i in range(1, n):
ddi = (pk[i - 1] - eq[i - 1]) / pk[i - 1]
if ddi > dd_trigger: on = False
if ddi < dd_trigger * 0.4: on = True
expo[i] = 1.0 if on else 0.4
return pd.Series(expo * r, index=_idx) # set below
def legs_on_grid(wc=0.5):
"""TP01(crypto, compoundato sul grid) e GTAA(equity) sulla stessa griglia giorni-di-borsa."""
tp = _tp01_returns()
if tp.index.tz is None:
tp.index = tp.index.tz_localize("UTC")
eq = gtaa_returns().dropna()
grid = eq.index[eq.index >= tp.index[0]]
cum = (1 + tp).cumprod()
tpg = cum.reindex(cum.index.union(grid)).ffill().reindex(grid).pct_change()
J = pd.concat({"c": tpg, "e": eq.reindex(grid)}, axis=1).dropna()
combo = wc * J["c"] + (1 - wc) * J["e"]
return combo, J["c"], J["e"]
def sh(r): r = r.dropna().values; return float(np.mean(r) / np.std(r) * ANN) if len(r) > 5 and np.std(r) > 0 else 0.0
def maxdd(curve): pk = np.maximum.accumulate(curve); return float(np.max((pk - curve) / pk)) if len(curve) else 0.0
def yearly(ret, label):
ret = ret.dropna().sort_index()
print(f"\n ===== {label} (guardia-DD -4%) =====")
print(f" {'anno':6}{'NL inizio':>11}{'NL fine':>11}{'rend%':>9}{'DD%':>8}{'Sharpe':>9}")
eq = INITIAL
for y in sorted(set(ret.index.year)):
r = ret[ret.index.year == y]
if len(r) < 5: continue
eq0 = eq
curve = eq0 * np.cumprod(1 + r.values)
eq = float(curve[-1])
print(f" {y:<6}{eq0:>11,.0f}{eq:>11,.0f}{(eq/eq0-1)*100:>+8.1f}%{maxdd(curve)*100:>7.1f}%{sh(r):>9.2f}")
yrs = (ret.index[-1] - ret.index[0]).days / 365.25
cagr = (eq / INITIAL) ** (1 / yrs) - 1 if yrs > 0 else 0
full_curve = INITIAL * np.cumprod(1 + ret.values)
print(f" {'TOT':<6}{INITIAL:>11,.0f}{eq:>11,.0f}{(eq/INITIAL-1)*100:>+8.1f}%{maxdd(full_curve)*100:>7.1f}%{sh(ret):>9.2f}"
f" | CAGR {cagr*100:+.1f}% ({yrs:.1f}y)")
def main():
global _idx
print("=" * 78)
print(" RESOCONTO PROTETTO (soft-guard DD -4%) — da $2.000, anno per anno")
print(" Tutte e tre sulla griglia giorni-di-borsa del combo (dal 2019), esposizione 1x.")
print("=" * 78)
combo, tp, g = legs_on_grid()
for ret, lbl in [(combo, "COMBO TP01+GTAA 50/50"), (tp, "solo TP01 (crypto)"), (g, "solo GTAA (equity)")]:
_idx = ret.index
yearly(dd_guard(ret), lbl)
# confronto NON protetto (baseline) in coda, una riga TOT per riferimento
print("\n --- riferimento NON protetto (baseline, TOT) ---")
for ret, lbl in [(combo, "COMBO"), (tp, "TP01"), (g, "GTAA")]:
yrs = (ret.index[-1] - ret.index[0]).days / 365.25
eqf = INITIAL * np.prod(1 + ret.values)
print(f" {lbl:6} CAGR {((eqf/INITIAL)**(1/yrs)-1)*100:>+5.1f}% DD {maxdd(INITIAL*np.cumprod(1+ret.values))*100:>4.1f}% Sharpe {sh(ret):.2f}")
if __name__ == "__main__":
main()
+99
View File
@@ -0,0 +1,99 @@
"""Resoconto anno-per-anno della strategia combo (TP01+GTAA) + componenti, da $2.000.
Per anno: PnL ($ e %), MaxDD (intra-anno), NumTrades, equity di fine anno (compounding da 2k).
Combo = blend 50/50 TP01(Deribit) + GTAA(IB) (crypto compoundato su grid giorni-di-borsa).
NumTrades: TP01 = cambi di target BTC/ETH (>0.05); GTAA = ribilanci MENSILI per-gamba (>2%).
Onesto: il combo parte dal 2019 (crypto). GTAA-solo dato anche su 10y come contesto.
"""
import sys
from pathlib import Path
import numpy as np, pandas as pd
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT)); sys.path.insert(0, str(ROOT / "scripts" / "live"))
from src.data.downloader import load_data
from src.strategies.trend_portfolio import TrendPortfolio, CANONICAL, resample_1d
from src.portfolio.sleeves import _tp01_returns
from src.portfolio.gtaa import gtaa_returns, _exposure, _close, EQ_UNIVERSE
INITIAL = 2000.0
def tp01_trades_per_year():
tp = TrendPortfolio(**CANONICAL); cnt = {}
for a in ("BTC", "ETH"):
df = resample_1d(load_data(a, "1h")); tgt = tp.target_series(df)
idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"]))
chg = pd.Series(np.abs(np.diff(tgt, prepend=tgt[0])) > 0.05, index=idx)
for y, c in chg.groupby(idx.year).sum().items():
cnt[int(y)] = cnt.get(int(y), 0) + int(c)
return cnt
def gtaa_trades_per_year():
# pesi giornalieri -> ribilancio MENSILE realistico -> conta gambe cambiate >2%
W = {}
for a in EQ_UNIVERSE:
ex = _exposure(_close(a)) / len(EQ_UNIVERSE)
W[a] = ex
Wd = pd.concat(W, axis=1).dropna()
Wm = Wd.resample("ME").last() # peso a fine mese
chg = (Wm.diff().abs() > 0.02).sum(axis=1) # gambe ribilanciate quel mese
return chg.groupby(chg.index.year).sum().astype(int).to_dict()
def yearly(ret: pd.Series, trades: dict, label: str, start_capital=INITIAL):
ret = ret.dropna().sort_index()
print(f"\n ===== {label} =====")
print(f" {'anno':6}{'eq inizio':>12}{'PnL $':>12}{'PnL %':>9}{'MaxDD %':>9}{'NumTrades':>11}{'eq fine':>12}")
eq = start_capital
for y in sorted(set(ret.index.year)):
r = ret[ret.index.year == y]
if len(r) < 5:
continue
eq0 = eq
curve = eq0 * np.cumprod(1 + r.values)
peak = np.maximum.accumulate(curve)
dd = float(np.max((peak - curve) / peak)) if len(curve) else 0.0
eq = float(curve[-1])
pnl = eq - eq0
nt = trades.get(y, None)
print(f" {y:<6}{eq0:>12,.0f}{pnl:>+12,.0f}{(eq/eq0-1)*100:>+8.1f}%{dd*100:>8.1f}%"
f"{(str(nt) if nt is not None else ''):>11}{eq:>12,.0f}")
tot = eq / start_capital - 1
yrs = (ret.index[-1] - ret.index[0]).days / 365.25
cagr = (eq / start_capital) ** (1 / yrs) - 1 if yrs > 0 else 0
sh = float(r.mean()) if False else float(ret.mean() / ret.std() * np.sqrt(252))
print(f" {'TOT':<6}{start_capital:>12,.0f}{eq-start_capital:>+12,.0f}{tot*100:>+8.1f}%"
f"{'':>9}{sum(v for v in trades.values()) if trades else 0:>11}{eq:>12,.0f}")
print(f" -> da ${start_capital:,.0f} a ${eq:,.0f} in {yrs:.1f}y | CAGR {cagr*100:+.1f}% | Sharpe {sh:.2f}")
def combo_daily(wc=0.5):
tp = _tp01_returns()
if tp.index.tz is None:
tp.index = tp.index.tz_localize("UTC")
eq = gtaa_returns().dropna()
grid = eq.index[eq.index >= tp.index[0]]
cum = (1 + tp).cumprod()
tpg = (cum.reindex(cum.index.union(grid)).ffill().reindex(grid)).pct_change()
J = pd.concat({"c": tpg, "e": eq.reindex(grid)}, axis=1).dropna()
return (wc * J["c"] + (1 - wc) * J["e"]).dropna()
def main():
print("=" * 80)
print(" RESOCONTO STRATEGIA — da $2.000, anno per anno")
print("=" * 80)
tpt = tp01_trades_per_year(); gtt = gtaa_trades_per_year()
combo_tr = {y: tpt.get(y, 0) + gtt.get(y, 0) for y in set(tpt) | set(gtt)}
# COMBO (la strategia deployata)
yearly(combo_daily(), combo_tr, "COMBO TP01+GTAA 50/50 (deployabile, dal 2019)")
# componenti
tp = _tp01_returns(); tp.index = tp.index.tz_localize("UTC") if tp.index.tz is None else tp.index
yearly(tp, tpt, "solo TP01 (crypto, Deribit)")
g = gtaa_returns(); g10 = g[g.index >= (g.index[-1] - pd.Timedelta(days=3660))]
yearly(g10, gtt, "solo GTAA (equity, IB) — ULTIMI 10 ANNI")
if __name__ == "__main__":
main()
+126
View File
@@ -0,0 +1,126 @@
"""HARNESS parametrizzato — anticipazione crypto -> mercato (lead-lag eseguibile, onesto).
Generalizza l'effetto weekend: la finestra-LEAD e' l'intervallo in cui l'equity e' CHIUSO e la crypto
no (prev close 21:00 UTC -> next open 13:30 UTC). Il weekend e' il caso lungo (Ven 21:00 -> Lun 13:30).
Per ogni sessione equity D (con sessione precedente P):
lead = crypto return su [lead_start, D 13:00] (lead_start = P 21:00 se hours='overnight', else D13:00-hours)
predict target: gap = open[D]/close[P]-1 ; intraday = close[D]/open[D]-1 ; full = close[D]/close[P]-1
control = rendimento sessione precedente equity (close[P]/close[P_prev]-1) -> test INCREMENTALE
Filtro giorni: all | mon (solo lunedi'/weekend) | nonmon.
Output JSON per config: n, corr, beta+t-stat del lead AL NETTO del control (incrementale), Sharpe
settimanale/annualizzato del trade eseguibile (sign(lead)*predict - costo) FULL/IS/OOS(2022+),
hit-rate, e PER-ANNO (hit e mean) per la robustezza multi-anno.
uv run python scripts/research/crypto_lead_harness.py --configs '[{"lead":"BTC","target":"QQQ","day":"mon","predict":"intraday","hours":"overnight"}]'
Dati: cache su disco (crypto 1h, ETF eq_*). Nessun IB online. Vettoriale, veloce.
"""
import sys, json, argparse
from pathlib import Path
import numpy as np, pandas as pd
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT)); sys.path.insert(0, str(ROOT / "scripts" / "research"))
from src.data.downloader import load_data
import eqlib
OOS_DEFAULT = "2022-01-01"
OPEN_H = 13 # ~apertura US 13:30 UTC -> uso barra 13:00 (info nota prima dell'open per il lead)
CLOSE_H = 21 # ~chiusura US 21:00 UTC
_CRYPTO = {}
def crypto_hourly(asset):
if asset not in _CRYPTO:
s = load_data(asset, "1h").set_index("datetime")["close"].astype(float)
full = pd.date_range(s.index[0].floor("h"), s.index[-1].ceil("h"), freq="h", tz="UTC")
_CRYPTO[asset] = s.reindex(s.index.union(full)).ffill().reindex(full)
return _CRYPTO[asset]
def at(series, ts):
try:
return float(series.asof(ts))
except Exception:
return np.nan
def evaluate(cfg, cost_rt=0.0004, oos=OOS_DEFAULT):
OOS = pd.Timestamp(oos, tz="UTC")
lead = cfg["lead"]; tgt = cfg["target"]; day = cfg.get("day", "all")
predict = cfg.get("predict", "intraday"); hours = cfg.get("hours", "overnight")
bc = crypto_hourly(lead)
try:
oc = eqlib.load_eq(tgt)["open"].astype(float); cc = eqlib.load_eq(tgt)["close"].astype(float)
except Exception as e:
return {**cfg, "err": f"no data {tgt}"}
idx = cc.index
rows = []
for j in range(2, len(idx)):
D = idx[j]; P = idx[j-1]; Pp = idx[j-2]
if day == "mon" and D.weekday() != 0: continue
if day == "nonmon" and D.weekday() == 0: continue
d_open = D.normalize() + pd.Timedelta(hours=OPEN_H)
p_close = P.normalize() + pd.Timedelta(hours=CLOSE_H)
lead_start = p_close if hours == "overnight" else d_open - pd.Timedelta(hours=int(hours))
c1 = at(bc, d_open); c0 = at(bc, lead_start)
if not (np.isfinite(c1) and np.isfinite(c0) and c0 > 0): continue
ld = c1 / c0 - 1.0
gap = oc[D] / cc[P] - 1.0
intr = cc[D] / oc[D] - 1.0
full = cc[D] / cc[P] - 1.0
ctrl = cc[P] / cc[Pp] - 1.0
rows.append((D, ld, gap, intr, full, ctrl))
if len(rows) < 60:
return {**cfg, "err": f"n={len(rows)}"}
D_ = pd.DataFrame(rows, columns=["d", "lead", "gap", "intraday", "full", "ctrl"]).set_index("d")
y = D_[predict].values; x = D_["lead"].values; ctrl = D_["ctrl"].values
def z(a):
sd = a.std(); return (a - a.mean()) / sd if sd > 0 else a * 0
corr = float(np.corrcoef(x, y)[0, 1])
# incrementale vs control (OLS standardizzato)
X = np.column_stack([np.ones(len(y)), z(x), z(ctrl)])
beta, *_ = np.linalg.lstsq(X, z(y), rcond=None)
resid = z(y) - X @ beta
dof = max(len(y) - 3, 1)
se = np.sqrt(np.sum(resid**2) / dof * np.diag(np.linalg.inv(X.T @ X)))
t_inc = float(beta[1] / se[1]) if se[1] > 0 else 0.0
# trade eseguibile: long-short e long-flat su segno del lead, intraday/predict, net costi
sign = np.sign(x)
def sharpe(r):
r = r[np.isfinite(r)]
return float(np.mean(r) / np.std(r) * np.sqrt(52)) if len(r) > 5 and np.std(r) > 0 else 0.0
ls = sign * y - cost_rt
lf = np.where(x > 0, y, 0.0) - np.where(x > 0, cost_rt, 0.0)
yrs = D_.index.year.values
def per_year(r):
out = {}
for yv in sorted(set(yrs)):
m = yrs == yv
if m.sum() >= 8:
out[int(yv)] = round(float(np.mean(np.sign(x[m]) == np.sign(y[m]))), 2)
return out
is_m = D_.index < OOS; oos_m = D_.index >= OOS
py = per_year(ls)
return {**cfg, "n": len(D_), "corr": round(corr, 3), "t_incremental": round(t_inc, 2),
"hit": round(float(np.mean(sign == np.sign(y))), 3),
"sh_ls_full": round(sharpe(ls), 2), "sh_ls_is": round(sharpe(ls[is_m]), 2),
"sh_ls_oos": round(sharpe(ls[oos_m]), 2),
"sh_lf_full": round(sharpe(lf), 2), "sh_lf_oos": round(sharpe(lf[oos_m]), 2),
"ann_ls_pct": round(float(np.nanmean(ls) * 52 * 100), 1),
"years_pos": int(sum(1 for v in py.values() if v > 0.5)), "years_tot": len(py),
"per_year_hit": py}
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--configs", required=True)
ap.add_argument("--cost", type=float, default=0.0004)
ap.add_argument("--oos", default=OOS_DEFAULT)
args = ap.parse_args()
cfgs = json.loads(args.configs)
print(json.dumps([evaluate(c, cost_rt=args.cost, oos=args.oos) for c in cfgs]))
if __name__ == "__main__":
main()
+120
View File
@@ -0,0 +1,120 @@
"""CRYPTO x MERCATI IB — correlazioni e ANTICIPAZIONI (lead-lag).
Obiettivo: la crypto (24/7) anticipa i mercati IB (azioni/bond/oro/credito), o viceversa?
Disciplina onesta: i tranelli di timing daily sono enormi (crypto chiude 00:00 UTC, US equity 21:00
UTC -> il lag-0 e' contaminato), quindi (1) allineo i rendimenti sullo STESSO intervallo
(compounding crypto sul grid giorni-di-borsa), (2) guardo i lag >=1 giorno, (3) test del segno con
hit-rate e split in-sample/OOS, (4) flag multiple-testing.
Ipotesi piu' pulita = EFFETTO WEEKEND: la crypto si muove Sab+Dom (azionario chiuso) -> quel
movimento e' informazione PRIOR al lunedi'. Predice il gap/intraday del lunedi' azionario?
Uso gli OPEN dei parquet eq_ (Monday open noto alle 13:30 UTC, weekend crypto noto alle 00:00 UTC).
DATI: cache su disco (BTC/ETH Deribit 1h->1d UTC; ETF IB eq_*). Nessun IB online.
"""
import sys
from pathlib import Path
import numpy as np, pandas as pd
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT)); sys.path.insert(0, str(ROOT / "scripts" / "research"))
from src.data.downloader import load_data
import eqlib
ETFS = ["SPY", "QQQ", "IWM", "TLT", "GLD", "HYG"]
def crypto_daily_close(asset="BTC") -> pd.Series:
df = load_data(asset, "1h").set_index("datetime")["close"].astype(float)
return df.resample("1D").last().dropna() # close ~00:00 UTC del giorno dopo
def _ret(s):
return s.pct_change()
def _corr_lags(x: pd.Series, y: pd.Series, lags=range(-5, 6)):
"""corr(x_{t-k}, y_t): k>0 => x ANTICIPA y di k giorni. Allineati sullo stesso grid."""
J = pd.concat({"x": x, "y": y}, axis=1, join="inner").dropna()
out = {}
for k in lags:
out[k] = round(float(J["x"].shift(k).corr(J["y"])), 3)
return out
def main():
print("=" * 98)
print(" CRYPTO x MERCATI IB — correlazioni & anticipazioni (lead-lag)")
print("=" * 98)
btc = crypto_daily_close("BTC"); eth = crypto_daily_close("ETH")
btc_r = _ret(btc); eth_r = _ret(eth)
# equity close + grid giorni-di-borsa
eq_close = {s: eqlib.load_eq(s)["close"].astype(float) for s in ETFS}
eq_open = {s: eqlib.load_eq(s)["open"].astype(float) for s in ETFS}
grid = eq_close["SPY"].index
grid = grid[grid >= btc.index[0]]
# crypto compoundato sul grid giorni-di-borsa (stesso intervallo dell'equity ret)
def to_grid(s):
cum = (1 + _ret(s)).cumprod()
return (cum.reindex(cum.index.union(grid)).ffill().reindex(grid)).pct_change()
btc_g = to_grid(btc); eth_g = to_grid(eth)
print(f" overlap dal {grid[0].date()} ({len(grid)} giorni di borsa)\n")
print(" --- (1) CORRELAZIONE CONTEMPORANEA (stesso intervallo; lag0 contaminato da timing) ---")
print(f" {'ETF':5} {'corr BTC':>9} {'corr ETH':>9}")
for s in ETFS:
er = _ret(eq_close[s]).reindex(grid)
cb = round(float(pd.concat([btc_g, er], axis=1).dropna().corr().iloc[0, 1]), 3)
ce = round(float(pd.concat([eth_g, er], axis=1).dropna().corr().iloc[0, 1]), 3)
print(f" {s:5} {cb:>9} {ce:>9}")
print("\n --- (2) LEAD-LAG BTC vs ETF: corr(BTC_{t-k}, ETF_t), k>0 = BTC ANTICIPA ---")
print(f" {'ETF':5} " + " ".join(f"k{ k:+d}" for k in range(-3, 4)) + " picco")
for s in ETFS:
er = _ret(eq_close[s]).reindex(grid)
cl = _corr_lags(btc_g, er, range(-3, 4))
peak = max(cl, key=lambda k: abs(cl[k]))
row = " ".join(f"{cl[k]:+.2f}" for k in range(-3, 4))
tag = f"k={peak:+d} ({'BTC->ETF' if peak>0 else 'ETF->BTC' if peak<0 else 'contemp'})"
print(f" {s:5} {row} {tag}")
print("\n --- (3) EFFETTO WEEKEND: crypto Sab+Dom -> lunedi' azionario (anticipazione pulita) ---")
# weekend crypto = close(Dom 00:00 lun) / close(Ven) - 1 ; calcolato su crypto daily (calendario)
cal = pd.date_range(btc.index[0], btc.index[-1], freq="D", tz="UTC")
bc = btc.reindex(cal).ffill()
for s in ["SPY", "QQQ", "IWM", "HYG"]:
oc = eq_open[s]; cc = eq_close[s]
rows = []
for mon in grid:
if mon.weekday() != 0: # solo lunedi'
continue
fri = mon - pd.Timedelta(days=3)
if fri not in cc.index: # venerdi' non di borsa (festa) -> salta
continue
wk = float(bc.get(mon, np.nan) / bc.get(fri + pd.Timedelta(days=0), np.nan) - 1) if fri in bc.index else np.nan
# weekend crypto: da venerdi 00:00(close ven) a lunedi 00:00 -> usa bc[fri]..bc[mon]
wk = float(bc.loc[mon] / bc.loc[fri] - 1) if (mon in bc.index and fri in bc.index) else np.nan
gap = float(oc.loc[mon] / cc.loc[fri] - 1) if (mon in oc.index and fri in cc.index) else np.nan
intr = float(cc.loc[mon] / oc.loc[mon] - 1) if mon in oc.index else np.nan
rows.append((mon, wk, gap, intr))
D = pd.DataFrame(rows, columns=["mon", "wk", "gap", "intr"]).dropna().set_index("mon")
if len(D) < 50:
print(f" {s}: pochi dati ({len(D)})"); continue
def stat(col):
c = float(D["wk"].corr(D[col]))
hit = float((np.sign(D["wk"]) == np.sign(D[col])).mean())
return c, hit
cg, hg = stat("gap"); ci, hi = stat("intr")
# OOS: split 2022
Dh = D[D.index >= pd.Timestamp("2022-01-01", tz="UTC")]
cg_o = float(Dh["wk"].corr(Dh["gap"])); ci_o = float(Dh["wk"].corr(Dh["intr"]))
print(f" {s}: n={len(D)} | weekend-crypto -> Mon GAP corr {cg:+.2f} hit {hg*100:.0f}% (OOS22+ {cg_o:+.2f}) "
f"| Mon INTRADAY corr {ci:+.2f} hit {hi*100:.0f}% (OOS {ci_o:+.2f})")
print("\n NB: lag-0/contemporanea contaminata dal timing (crypto chiude 00:00, equity 21:00 UTC).")
print(" Il GAP del lunedi' e' il test pulito (weekend crypto = info prior all'apertura).")
if __name__ == "__main__":
main()
@@ -0,0 +1,88 @@
"""DERIBIT-monitor -> IB-trade: il segnale crypto overnight AGGIUNGE al long-overnight indice?
Idea utente: guardare crypto live su Deribit (24/7) e tradare l'indice su IB. Il GAP di apertura =
movimento OVERNIGHT dei futures (MES/MNQ/M2K, tradati di notte) -> catturabile, net ~2bps.
DOMANDA DECISIVA (test onesto): l'azionario ha gia' un OVERNIGHT PREMIUM noto (il drift positivo
notturno). Quindi "long indice overnight" rende di suo. Il segnale crypto MIGLIORA quel baseline,
o e' solo overnight-premium + beta? Confronto:
A) ALWAYS-LONG overnight (incondizionato) = cattura il premio notturno puro
B) LONG se crypto-overnight>0, else FLAT = usa il crypto come filtro
C) LONG/SHORT sul segno del crypto = il segnale pieno
La metrica chiave e' B,C VS A (l'uplift del crypto sul puro premio notturno), non A in assoluto.
Dati: cache su disco (crypto 1h Deribit; ETF eq_* = proxy del future indice). net 2bps RT (futures).
"""
import sys
from pathlib import Path
import numpy as np, pandas as pd
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT)); sys.path.insert(0, str(ROOT / "scripts" / "research"))
import eqlib
from crypto_lead_harness import crypto_hourly, at, OPEN_H, CLOSE_H
OOS = pd.Timestamp("2022-01-01", tz="UTC")
COST = 0.0002 # ~2bps RT micro-future (commissione + spread)
def _sh(r):
r = np.asarray(r, float); r = r[np.isfinite(r)]
return float(np.mean(r) / np.std(r) * np.sqrt(252)) if len(r) > 5 and np.std(r) > 0 else 0.0
def build(target="SPY", lead="BTC"):
bc = crypto_hourly(lead)
oc = eqlib.load_eq(target)["open"].astype(float); cc = eqlib.load_eq(target)["close"].astype(float)
idx = cc.index; rows = []
for j in range(1, len(idx)):
D = idx[j]; P = idx[j-1]
d_open = D.normalize() + pd.Timedelta(hours=OPEN_H)
p_close = P.normalize() + pd.Timedelta(hours=CLOSE_H)
c1 = at(bc, d_open); c0 = at(bc, p_close)
if not (np.isfinite(c1) and np.isfinite(c0) and c0 > 0):
continue
crypto = c1 / c0 - 1.0
overnight = oc[D] / cc[P] - 1.0 # gap = ritorno overnight del future indice (catturabile)
rows.append((D, crypto, overnight))
return pd.DataFrame(rows, columns=["d", "crypto", "on"]).set_index("d")
def main():
print("=" * 92)
print(" CRYPTO overnight -> LONG INDICE OVERNIGHT (Deribit-monitor / IB-trade): aggiunge al premio?")
print("=" * 92)
print(f" net {COST*1e4:.0f}bps RT (micro-future). overnight = open[D]/close[P]-1 (= move notturno future).\n")
for tgt in ("SPY", "QQQ", "IWM"):
for lead in ("BTC", "ETH"):
D = build(tgt, lead)
up = D[D["crypto"] > 0]["on"]; dn = D[D["crypto"] <= 0]["on"]
# strategie
A = D["on"].values - COST # always-long
B = np.where(D["crypto"] > 0, D["on"], 0.0) - np.where(D["crypto"] > 0, COST, 0.0) # long se crypto su
C = np.sign(D["crypto"]) * D["on"] - COST # long/short
shA, shB, shC = _sh(A), _sh(B), _sh(C)
# OOS
m = D.index >= OOS
print(f" {tgt} <- {lead}: n={len(D)} | notti crypto-SU {len(up)} ret medio {up.mean()*1e4:+.1f}bps "
f"| crypto-GIU {len(dn)} ret medio {dn.mean()*1e4:+.1f}bps (spread {(up.mean()-dn.mean())*1e4:+.1f}bps)")
print(f" Sharpe: A always-long {shA:.2f} | B long-if-cryptoUp {shB:.2f} | C long/short {shC:.2f} "
f"-> UPLIFT crypto B-A {shB-shA:+.2f}, C-A {shC-shA:+.2f}")
print(f" OOS22+: A {_sh(A[m]):.2f} | B {_sh(B[m]):.2f} | C {_sh(C[m]):.2f} | "
f"ann: A {np.nanmean(A)*252*100:+.1f}% B {np.nanmean(B)*252*100:+.1f}% C {np.nanmean(C)*252*100:+.1f}%")
print()
# focus SPY-BTC: per-anno A vs C, e sketch deploy
D = build("SPY", "BTC")
A = D["on"].values - COST; C = np.sign(D["crypto"]) * D["on"] - COST
print(" --- SPY<-BTC per-anno: Sharpe A(always-long) vs C(crypto long/short) ---")
for y in sorted(set(D.index.year)):
mm = D.index.year == y
if mm.sum() >= 40:
print(f" {y}: A {_sh(A[mm]):+.2f} C {_sh(C[mm]):+.2f} (n={mm.sum()})")
print("\n NB: se C-A ~ 0, il crypto NON aggiunge al premio overnight (e' solo il premio + beta).")
print(" Se C-A >> 0 e A gia' alto, il crypto e' un filtro REALE sul rischio notturno.")
if __name__ == "__main__":
main()
@@ -0,0 +1,71 @@
"""ONESTA' DI TIMING: il 'Sharpe 5' del crypto->overnight equity e' look-ahead?
Test: separare la versione OVERLAP (segnale e ritorno coprono le stesse ore = look-ahead) dalla
versione TRADABILE (segnale finisce all'ENTRATA, catturo solo il moto SUCCESSIVO).
segnale crypto = BTC [P 21:00 -> D 13:00 UTC] (noto solo a D 13:00, poco prima dell'open 13:30)
- OVERLAP capture = gap = open[D]/close[P]-1 [P 21:00 -> D 13:30] <-- sovrappone il segnale
- TRADABILE capture = intra = close[D]/open[D]-1 [D 13:30 -> D 21:00] <-- DOPO l'entrata, pulito
Se OVERLAP >> TRADABILE, il numero alto e' artefatto: all'open il gap e' gia' avvenuto, non lo catturi.
Annualizzazione CORRETTA sqrt(252) (giornaliero), non sqrt(52).
"""
import sys
from pathlib import Path
import numpy as np, pandas as pd
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT)); sys.path.insert(0, str(ROOT / "scripts" / "research"))
import eqlib
from crypto_lead_harness import crypto_hourly, at, OPEN_H, CLOSE_H
OOS = pd.Timestamp("2022-01-01", tz="UTC"); COST = 0.0002
def _sh(r):
r = np.asarray(r, float); r = r[np.isfinite(r)]
return float(np.mean(r) / np.std(r) * np.sqrt(252)) if len(r) > 5 and np.std(r) > 0 else 0.0
def build(target="SPY", lead="BTC"):
bc = crypto_hourly(lead)
oc = eqlib.load_eq(target)["open"].astype(float); cc = eqlib.load_eq(target)["close"].astype(float)
idx = cc.index; rows = []
for j in range(1, len(idx)):
D = idx[j]; P = idx[j-1]
c1 = at(bc, D.normalize() + pd.Timedelta(hours=OPEN_H)) # crypto a D 13:00 (entrata)
c0 = at(bc, P.normalize() + pd.Timedelta(hours=CLOSE_H)) # crypto a P 21:00
if not (np.isfinite(c1) and np.isfinite(c0) and c0 > 0):
continue
crypto = c1 / c0 - 1.0
gap = oc[D] / cc[P] - 1.0 # OVERLAP col segnale
intra = cc[D] / oc[D] - 1.0 # DOPO l'entrata (tradabile)
rows.append((D, crypto, gap, intra))
return pd.DataFrame(rows, columns=["d", "crypto", "gap", "intra"]).set_index("d")
def main():
print("=" * 90)
print(" TIMING ONESTO: OVERLAP (look-ahead) vs TRADABILE — crypto overnight -> equity")
print("=" * 90)
print(" segnale noto a D13:00. OVERLAP=gap [copre il segnale]. TRADABILE=intraday [dopo entrata]. net 2bps, sqrt(252)\n")
for tgt in ("SPY", "QQQ", "IWM"):
D = build(tgt, "BTC")
for name, col in (("OVERLAP gap (look-ahead)", "gap"), ("TRADABILE intraday", "intra")):
C = np.sign(D["crypto"]) * D[col] - COST
m = D.index >= OOS
print(f" {tgt} {name:26}: Sharpe long/short {_sh(C):.2f} (OOS {_sh(C[m]):.2f}) ann {np.nanmean(C)*252*100:+.1f}%")
print()
print(" --- VERDETTO ---")
D = build("QQQ", "BTC")
cg = _sh(np.sign(D["crypto"]) * D["gap"] - COST)
ci = _sh(np.sign(D["crypto"]) * D["intra"] - COST)
print(f" QQQ: gap(overlap) Sharpe {cg:.2f} vs intraday(tradabile) Sharpe {ci:.2f}")
print(f" -> il moto che il crypto 'predice' avviene NELLA finestra del segnale (overnight), non dopo.")
print(f" All'entrata (D13:00, pre-open) il gap e' gia' realizzato -> NON catturabile con l'ETF.")
print(f" Cio' che resta da catturare (intraday) ha Sharpe ~{ci:.1f}: l'edge tradabile e' quello.")
print(f" Per catturare PARTE dell'overnight servirebbe entrare a META' notte via FUTURES IB e")
print(f" testare se il crypto [P21:00->T] predice il future [T->open] (finestre NON sovrapposte):")
print(f" richiede dati intraday dei futures indice (ES/NQ), che NON abbiamo in cache -> data step.")
if __name__ == "__main__":
main()
+97
View File
@@ -0,0 +1,97 @@
"""WEEKEND CRYPTO -> LUNEDI' AZIONARIO — validazione avversariale dell'anticipazione.
L'analisi lead-lag ha trovato UNA anticipazione pulita: il movimento crypto del weekend (Sab+Dom,
azionario chiuso) anticipa il lunedi' azionario (gap corr ~0.24, OOS piu' forte). Prima di crederci,
due test scettici:
(A) INCREMENTALE: aggiunge info OLTRE il rendimento del VENERDI'? (o e' solo momentum equity?)
regressione Mon ~ weekend_crypto + friday_equity ; il coeff del crypto resta significativo?
(B) TRADABILE: segnale eseguibile = osservo weekend crypto (noto Dom 24:00 UTC), entro al Monday
OPEN, esco al Monday CLOSE. Net di costi (4 bps RT ETF). Sharpe/hit/OOS vs sempre-long lunedi'.
DATI: cache su disco (BTC Deribit 1h->1d; ETF eq_* con OPEN). Nessun IB online.
"""
import sys
from pathlib import Path
import numpy as np, pandas as pd
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT)); sys.path.insert(0, str(ROOT / "scripts" / "research"))
from src.data.downloader import load_data
import eqlib
OOS = pd.Timestamp("2022-01-01", tz="UTC")
COST_RT = 0.0004 # 4 bps round-trip ETF (entry open + exit close)
def _sh_weekly(r):
r = np.asarray(pd.Series(r).dropna(), float)
return float(np.mean(r) / np.std(r) * np.sqrt(52)) if len(r) > 2 and np.std(r) > 0 else 0.0
def build(asset_etf="QQQ"):
btc = load_data("BTC", "1h").set_index("datetime")["close"].astype(float).resample("1D").last()
cal = pd.date_range(btc.index[0], btc.index[-1], freq="D", tz="UTC")
bc = btc.reindex(cal).ffill()
oc = eqlib.load_eq(asset_etf)["open"].astype(float)
cc = eqlib.load_eq(asset_etf)["close"].astype(float)
rows = []
for mon in cc.index:
if mon.weekday() != 0:
continue
fri = mon - pd.Timedelta(days=3)
thu = mon - pd.Timedelta(days=4)
if fri not in cc.index or fri not in bc.index or mon not in bc.index:
continue
wk = bc.loc[mon] / bc.loc[fri] - 1.0 # weekend crypto (Ven 00:00 -> Lun 00:00)
fri_eq = (cc.loc[fri] / cc.loc[thu] - 1.0) if thu in cc.index else np.nan # rendimento venerdi'
gap = oc.loc[mon] / cc.loc[fri] - 1.0
intr = cc.loc[mon] / oc.loc[mon] - 1.0 # tradabile: open->close lunedi'
rows.append((mon, wk, fri_eq, gap, intr))
return pd.DataFrame(rows, columns=["mon", "wk", "fri_eq", "gap", "intr"]).dropna().set_index("mon")
def main():
print("=" * 92)
print(" WEEKEND CRYPTO -> LUNEDI' AZIONARIO — validazione avversariale")
print("=" * 92)
for etf in ("QQQ", "SPY", "IWM"):
D = build(etf)
print(f"\n ===== {etf} (n={len(D)} lunedi', {D.index[0].date()}..{D.index[-1].date()}) =====")
# (A) INCREMENTALE vs venerdi' — regressione OLS standardizzata, t-stat su weekend_crypto
for tgt in ("gap", "intr"):
y = (D[tgt] - D[tgt].mean()) / D[tgt].std()
x1 = (D["wk"] - D["wk"].mean()) / D["wk"].std()
x2 = (D["fri_eq"] - D["fri_eq"].mean()) / D["fri_eq"].std()
X = np.column_stack([np.ones(len(D)), x1.values, x2.values])
beta, *_ = np.linalg.lstsq(X, y.values, rcond=None)
resid = y.values - X @ beta
se = np.sqrt(np.sum(resid**2) / (len(D) - 3) * np.diag(np.linalg.inv(X.T @ X)))
t_wk = beta[1] / se[1]; t_fri = beta[2] / se[2]
partial = float(pd.Series(resid).corr(x1)) # ~ contributo crypto al netto del resto
print(f" [{tgt:4}] beta_weekendCrypto {beta[1]:+.3f} (t={t_wk:+.1f}) | "
f"beta_fridayEq {beta[2]:+.3f} (t={t_fri:+.1f}) -> crypto {'INCREMENTALE' if abs(t_wk)>2 else 'non signif.'}")
# (B) TRADABILE: long lunedi' intraday se weekend crypto > 0, short se < 0 (net costi)
sig = np.sign(D["wk"].values)
gross = sig * D["intr"].values
net = gross - COST_RT
D2 = D.assign(net=net)
full = D2["net"]; oos = D2[D2.index >= OOS]["net"]; ins = D2[D2.index < OOS]["net"]
bh = D["intr"] # baseline: sempre-long lunedi' intraday
hit = float((np.sign(gross) > 0).mean()) if False else float((sig == np.sign(D["intr"].values)).mean())
print(f" TRADE (long/short Mon intraday su segno weekend-crypto, net {COST_RT*1e4:.0f}bps):")
print(f" hit-rate segno {hit*100:.0f}% | Sharpe(sett.) FULL {_sh_weekly(full):.2f} IS {_sh_weekly(ins):.2f} OOS22+ {_sh_weekly(oos):.2f}")
print(f" ritorno medio/lun {full.mean()*1e4:+.1f}bps (net) | baseline sempre-long {bh.mean()*1e4:+.1f}bps | "
f"ann.~{full.mean()*52*100:+.1f}%")
# long-flat (piu' realistico: long se crypto su, altrimenti cash)
lf = np.where(D["wk"].values > 0, D["intr"].values, 0.0) - np.where(D["wk"].values > 0, COST_RT, 0.0)
lfs = pd.Series(lf, index=D.index)
print(f" variante LONG-FLAT (long se crypto su, else cash): Sharpe FULL {_sh_weekly(lfs):.2f} "
f"OOS {_sh_weekly(lfs[lfs.index>=OOS]):.2f} ann.~{lfs.mean()*52*100:+.1f}%")
print("\n NB: ~52 lunedi'/anno -> Sharpe settimanale; OOS = 2022+. Multiple-testing: 3 ETF x 2 target.")
if __name__ == "__main__":
main()
+352
View File
@@ -0,0 +1,352 @@
"""DVOL-DIRECTIONAL — la vol IMPLICITA (Deribit DVOL) come SEGNALE DIREZIONALE/REGIME sul perp BTC/ETH.
Filone A. Diverso da TUTTO il lavoro DVOL precedente:
* `tp01_dvol_overlay.py` (2026-06-26): DVOL come DENOMINATORE del vol-target -> solo de-levering, SCARTATO.
* VOL03/04/10/11 (sweep alt): DVOL che GATA/SCALA un TSMOM (eredita lo Sharpe di trend di TP01 ->
il marginal scorer li boccia NEUTRAL/REDUNDANT).
* agent_14_dvol_spread (onda ortho): IV RELATIVA BTC-vs-ETH, market-NEUTRAL 2-leg (l'unico LEAD).
Qui invece: usare DVOL/IV-RV come segnale DIREZIONALE STANDALONE sul LIVELLO di mercato (long-flat o
L/S sul perp), per provare se il DVOL porta alpha DIREZIONALE ORTOGONALE a TP01 (non un overlay sul trend).
Angoli (tutti CAUSALI: decisi <= close[i], tenuti in i+1 dallo shift di eval_weights):
VRP-Z : z-score causale del vol-risk-premium (IV-RV). VRP ricco (IV>>RV, paura sovra-prezzata)
=> LONG underlying (fear-reversal, l'analogo direzionale dell'edge VRP); flip = falsifica.
DVOL-LV : percentile espandente causale del DVOL. "Buy-the-fear" (rank alto => long) vs
"buy-the-calm" (rank basso => long).
DVOL-MOM : DVOL vs ema(DVOL,k). DVOL in calo (risk-on) => long; in salita (risk-off) => flat (o short, L/S).
VRP-LS : variante long-short del VRP-Z (short quando VRP compresso).
DVOL parte 2021-03 -> pre-DVOL il segnale e' FLAT (0). Valuto sia FULL (con flat pre-periodo, deflaziona
lo Sharpe) sia DVOL-ERA-ONLY (giusto per la tesi). Gate: study_weights (abs+fee sweep), marginal_vs_tp01
(earns_slot), causality_ok, eval_weights_smallcap ($600), sign-falsification. Storia DVOL <5 anni -> caveat.
uv run python scripts/research/dvol_directional.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 / "scripts" / "research" / "alt"))
import altlib as al # noqa: E402
# warmup: prima data DVOL (2021-03-24) + finestra max (z/rank) -> evita celle a campione corto
DVOL_ERA = pd.Timestamp("2021-10-01", tz="UTC")
TVOL = 0.20
VOLWIN = 30
LEVCAP = 2.0
# ===========================================================================
# CONTESTO causale per-asset (IV, RV, VRP) — tutto <= close[i]
# ===========================================================================
def _ctx(df: pd.DataFrame, asset: str):
c = df["close"].values.astype(float)
r = al.simple_returns(c)
bpd = al.bars_per_day(df)
iv = al.dvol(df, asset) / 100.0 # IV implicita 30d annualizzata (causale)
rv = al.realized_vol(r, max(2, 30 * bpd), bpd * 365.25) # RV trailing 30d annualizzata
return c, r, bpd, iv, rv
_RANK_CACHE: dict = {}
def _expanding_rank_arr(x: np.ndarray, min_p: int = 120) -> np.ndarray:
"""Percentile espandente causale: rank[i] = frazione di x[:i] < x[i].
Usa SOLO barre STRETTAMENTE precedenti + il valore corrente (noto a close[i]) niente
future-peeking. Calcolato sull'array PASSATO (cosi' su un prefisso ridà i valori del prefisso,
=> causality_ok lo verifica davvero). O(n^2), n~3k OK."""
finite = np.isfinite(x)
out = np.full(len(x), np.nan)
for i in range(min_p, len(x)):
if not finite[i]:
continue
prev = x[:i][finite[:i]]
if len(prev) >= min_p // 2:
out[i] = float(np.mean(prev < x[i]))
return out
def dvol_rank(df: pd.DataFrame, asset: str, min_p: int = 120) -> np.ndarray:
x = np.asarray(al.dvol(df, asset), float)
key = (asset, len(x), round(float(np.nan_to_num(x[-1])), 4))
c = _RANK_CACHE.get(key)
if c is None:
c = _expanding_rank_arr(x, min_p)
_RANK_CACHE[key] = c
return c
def _finalize(direction: np.ndarray, df: pd.DataFrame, long_only: bool) -> np.ndarray:
d = np.nan_to_num(direction, nan=0.0)
if long_only:
d = np.clip(d, 0.0, 1.0)
return al.vol_target(d, df, TVOL, VOLWIN, LEVCAP)
# ===========================================================================
# FAMIGLIE DI SEGNALI (factory -> target_fn(df, asset))
# ===========================================================================
def make_vrp_z(win=90, thr=0.0, sign=+1, long_only=True):
"""LONG quando z(IV-RV) * sign > thr. sign=+1 => long su VRP RICCO (fear-reversal)."""
def fn(df, asset):
c, r, bpd, iv, rv = _ctx(df, asset)
vrp = iv - rv
z = al.zscore(vrp, max(5, win * bpd))
sig = sign * z
if long_only:
d = np.where(sig > thr, 1.0, 0.0)
else:
d = np.clip(np.tanh(sig), -1.0, 1.0)
return _finalize(d, df, long_only)
return fn
def make_dvol_level(q=0.5, side="fear", long_only=True):
"""side='fear': long quando rank DVOL > q (buy-the-fear). side='calm': long quando rank < q."""
def fn(df, asset):
rk = dvol_rank(df, asset)
if side == "fear":
base = (rk > q)
else:
base = (rk < q)
if long_only:
d = np.where(base, 1.0, 0.0)
else:
d = np.where(base, 1.0, -1.0)
return _finalize(d, df, long_only)
return fn
def make_dvol_mom(k=10, long_only=True):
"""DVOL in calo (dvol < ema(dvol,k)) => risk-on => long; in salita => flat (o short se L/S)."""
def fn(df, asset):
dv = al.dvol(df, asset)
em = al.ema(np.nan_to_num(dv, nan=np.nan), k)
falling = dv < em
if long_only:
d = np.where(falling, 1.0, 0.0)
else:
d = np.where(falling, 1.0, -1.0)
d = np.where(np.isfinite(dv) & np.isfinite(em), d, 0.0)
return _finalize(d, df, long_only)
return fn
def make_vrp_positive(long_only=True):
"""Baseline 'quasi buy&hold': long quando IV>RV (VRP positivo, ~80% del tempo)."""
def fn(df, asset):
c, r, bpd, iv, rv = _ctx(df, asset)
d = np.where((iv - rv) > 0, 1.0, 0.0)
return _finalize(d, df, long_only)
return fn
# ===========================================================================
# DIAGNOSTICA: il DVOL ha CONTENUTO DIREZIONALE? (probe corr signal[i] vs r[i+1])
# ===========================================================================
def _raw_direction(fn_dir, df, asset):
"""Estrae la DIREZIONE grezza (pre-vol-target) per la probe: rifa' il calcolo del segno."""
return fn_dir(df, asset)
def probe(name, dir_fn, era_only=True):
"""corr(dir[i], r[i+1]) e media r[i+1] per dir>0 vs dir<=0, pooled BTC+ETH (1d)."""
dd, rr = [], []
for a in al.CERTIFIED:
df = al.get(a, "1d")
idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True))
d = np.nan_to_num(dir_fn(df, a), nan=0.0)
r = al.simple_returns(df["close"].values.astype(float))
rnext = np.roll(r, -1); rnext[-1] = 0.0
mask = np.ones(len(df), bool)
if era_only:
mask &= np.asarray(idx >= DVOL_ERA)
mask[-1] = False
dd.append(d[mask]); rr.append(rnext[mask])
d = np.concatenate(dd); r = np.concatenate(rr)
corr = float(np.corrcoef(d, r)[0, 1]) if np.std(d) > 0 and np.std(r) > 0 else 0.0
up = float(np.mean(r[d > 0])) * 1e4 if (d > 0).any() else 0.0 # bps
dn = float(np.mean(r[d <= 0])) * 1e4 if (d <= 0).any() else 0.0
return dict(name=name, corr=round(corr, 4), n=int(len(d)),
long_bps=round(up, 1), flat_bps=round(dn, 1), edge_bps=round(up - dn, 1),
frac_long=round(float(np.mean(d > 0)), 3))
# ===========================================================================
# MARGINAL su finestra DVOL-ERA (piu' giusto della full che include il flat pre-2021)
# ===========================================================================
def cand_daily_era(target_fn, tf="1d", start=DVOL_ERA, fee_side=al.FEE_SIDE) -> pd.Series:
series = {}
for a in al.CERTIFIED:
df = al.get(a, tf)
ev = al.eval_weights(df, al._call_target(target_fn, df, a), fee_side=fee_side)
series[a] = pd.Series(ev["net"], index=ev["idx"])
J = pd.concat(series, axis=1, join="inner").fillna(0.0)
d = al._to_daily(0.5 * J[al.CERTIFIED[0]] + 0.5 * J[al.CERTIFIED[1]])
return d[d.index >= start]
def era_full_sharpe(target_fn, tf="1d"):
"""Sharpe/DD del candidato sulla SOLA era DVOL (no flat pre-2021), 50/50."""
s = cand_daily_era(target_fn, tf)
r = s.values
sh = float(np.mean(r) / np.std(r) * np.sqrt(365.25)) if np.std(r) > 0 else 0.0
eq = np.cumprod(1 + r); pk = np.maximum.accumulate(eq)
dd = float(np.max((pk - eq) / pk)) if len(eq) else 0.0
yrs = (s.index[-1] - s.index[0]).days / 365.25 if len(s) > 1 else 1.0
cagr = eq[-1] ** (1 / yrs) - 1 if yrs > 0 and eq[-1] > 0 else -1.0
return dict(sharpe=round(sh, 3), dd=round(dd, 4), cagr=round(cagr, 4), n=len(s))
def earns_slot_era(target_fn, tf="1d"):
"""Replica del gate study_marginal MA sulla candidate-daily ristretta all'era DVOL (inner-join
con TP01 baseline limita anche TP01 alla stessa finestra). Piu' equo per un segnale DVOL-only."""
marg = al.marginal_vs_tp01(cand_daily_era(target_fn, tf))
v = marg.get("marginal_verdict")
earns = (v == "ADDS" and marg.get("robust_oos", False)
and marg.get("beats_noise_null", False) and not marg.get("is_hedge", False))
return marg, earns
# ===========================================================================
# MAIN
# ===========================================================================
def hr(t=""):
print("=" * 100)
if t:
print(t)
print("=" * 100)
def main():
hr("DVOL-DIREZIONALE — DVOL/IV-RV come segnale DIREZIONALE standalone su BTC/ETH (perp). 1d, causale.")
print(f" Era DVOL valutata da {DVOL_ERA.date()} (warmup z/rank). FULL include flat pre-2021 (deflaziona Sharpe).")
print(f" vol-target {TVOL:.0%}, leva cap {LEVCAP}x, fee 0.10% RT. TP01 baseline come riferimento marginale.\n")
# ---- 1) PROBE: contenuto direzionale (corr signal[i] vs r[i+1]) ---------
hr("1) PROBE DIREZIONALE — il DVOL predice il ritorno del giorno dopo? (pooled BTC+ETH, era DVOL)")
probes = [
("VRP-Z+ (long VRP ricco)", make_vrp_z(90, 0.0, +1)),
("VRP-Z- (long VRP basso)", make_vrp_z(90, 0.0, -1)),
("DVOL-LV fear (rank>0.5)", make_dvol_level(0.5, "fear")),
("DVOL-LV calm (rank<0.5)", make_dvol_level(0.5, "calm")),
("DVOL-MOM falling=long", make_dvol_mom(10)),
("VRP>0 (quasi buy&hold)", make_vrp_positive()),
]
print(f" {'segnale':<28}{'corr':>9}{'long bps':>10}{'flat bps':>10}{'edge bps':>10}{'frac_long':>10}")
for nm, fn in probes:
p = probe(nm, fn)
print(f" {nm:<28}{p['corr']:>+9.4f}{p['long_bps']:>+10.1f}{p['flat_bps']:>+10.1f}"
f"{p['edge_bps']:>+10.1f}{p['frac_long']:>10.3f}")
print(" (edge bps = ritorno medio giorno-dopo quando long MENO quando flat; >0 = il segnale separa)\n")
# ---- 2) ROBUSTEZZA ASSOLUTA (study_weights) + fee sweep + era-only ------
hr("2) ROBUSTEZZA ASSOLUTA — study_weights su BTC+ETH (1d), fee sweep 0.00-0.20% RT")
grid = {
"VRP-Z90 long-flat": make_vrp_z(90, 0.0, +1, True),
"VRP-Z60 long-flat": make_vrp_z(60, 0.0, +1, True),
"VRP-Z120 long-flat": make_vrp_z(120, 0.0, +1, True),
"VRP-Z90 L/S": make_vrp_z(90, 0.0, +1, False),
"DVOL-fear q0.4 LF": make_dvol_level(0.4, "fear", True),
"DVOL-fear q0.5 LF": make_dvol_level(0.5, "fear", True),
"DVOL-calm q0.5 LF": make_dvol_level(0.5, "calm", True),
"DVOL-MOM k10 LF": make_dvol_mom(10, True),
"DVOL-MOM k20 LF": make_dvol_mom(20, True),
"DVOL-MOM k10 L/S": make_dvol_mom(10, False),
}
reps = {}
for nm, fn in grid.items():
rep = al.study_weights(nm, fn, tfs=("1d",))
reps[nm] = rep
era = era_full_sharpe(fn)
v = rep["verdict"]
c = rep["cells"][0]
print(f" {nm:<22} abs={v['grade']:<4} minFull={c['min_asset_full_sharpe']:+.2f} "
f"minHold={c['min_asset_holdout_sharpe']:+.2f} feeOK={c['fee_survives']!s:<5} "
f"|| ERA-only Sh={era['sharpe']:+.2f} DD={era['dd']*100:.0f}% CAGR={era['cagr']*100:+.0f}%")
print(" (FULL include 2018-2021 flat => Sharpe basso e' atteso; ERA-only e' il giudizio equo del segnale)\n")
# candidati da portare al marginal: i migliori per ERA Sharpe + abs non-FAIL
ranked = sorted(grid.items(), key=lambda kv: era_full_sharpe(kv[1])["sharpe"], reverse=True)
top = ranked[:4]
# ---- 3) MARGINAL vs TP01 (era DVOL) — il gate vero -----------------------
hr("3) MARGINAL vs TP01 (finestra DVOL-era) — earns_slot? (il gate decisivo)")
for nm, fn in top:
marg, earns = earns_slot_era(fn)
bl = marg.get("blends", {}).get("w25", {})
print(f"\n --- {nm} ---")
print(f" verdetto={marg.get('marginal_verdict')} EARNS_SLOT(era)={earns}")
print(f" corr->TP01 full {marg.get('corr_full')} hold {marg.get('corr_hold')} "
f"beta {marg.get('beta_to_tp01')} resid Sh {marg.get('resid_sharpe_full')}")
print(f" cand standalone full {marg.get('cand_full_sharpe')}/hold {marg.get('cand_hold_sharpe')} "
f"in-sample Sh {marg.get('cand_insample_sharpe')} has_edge={marg.get('has_insample_edge')}")
print(f" blend w25: full {bl.get('full')} (uplift {bl.get('uplift_full')}) "
f"hold {bl.get('hold')} (uplift {bl.get('uplift_hold')}) DD {bl.get('dd')}")
print(f" multi-cut {marg.get('multicut_uplift')} persistent={marg.get('multicut_persistent')} "
f"robust_oos={marg.get('robust_oos')} is_hedge={marg.get('is_hedge')}")
# ---- 3b) study_marginal canonico (full history, gate di progetto) --------
hr("3b) study_marginal CANONICO (full history, include flat pre-DVOL) — gate ufficiale di progetto")
leader_nm, leader_fn = top[0]
sm = al.study_marginal(leader_nm, leader_fn, tf="1d")
print(al.fmt_marginal(sm))
# ---- 4) CAUSALITA' (look-ahead guard) -----------------------------------
hr("4) CAUSALITA' — causality_ok (ricalcolo su prefisso, nessun future-peeking)")
for nm, fn in top:
co = al.causality_ok(fn, tf="1d")
print(f" {nm:<22} ok={co['ok']!s:<5} max_tail_diff={co['max_tail_diff']} checked={co['checked']}")
# robustezza alignment DVOL: lag +1 giorno (extra-conservativo) sul leader
hr("4b) ROBUSTEZZA ALIGNMENT — DVOL laggato +1g (extra-conservativo) sul leader: l'edge sopravvive?")
def _lag(fn):
# ricostruisce il segnale con dvol shiftato di 1 barra (usa solo DVOL di IERI)
def g(df, asset):
# monkey: usa al.dvol ma shift -> approssimo rifacendo via wrapper sul ctx non e' banale;
# piu' semplice: confronto Sharpe era con e senza lag costruendo direzione laggata generica.
base = fn(df, asset)
lagged = np.zeros_like(base); lagged[1:] = base[:-1]
return lagged
return g
lag_era = era_full_sharpe(_lag(leader_fn))
base_era = era_full_sharpe(leader_fn)
print(f" {leader_nm}: ERA Sh base {base_era['sharpe']:+.2f} -> con segnale laggato +1g "
f"{lag_era['sharpe']:+.2f} (calo grande = edge fragile all'alignment)")
# ---- 5) ESEGUIBILITA' $600 — eval_weights_smallcap ----------------------
hr("5) ESEGUIBILITA' a $600 — eval_weights_smallcap (min_order $5, haircut reale vs modellato)")
for nm, fn in top:
for a in al.CERTIFIED:
df = al.get(a, "1d")
sc = al.eval_weights_smallcap(df, al._call_target(fn, df, a), capital=600, min_order=5)
print(f" {nm:<22} {a}: modellato Sh {sc['modeled']['sharpe']:+.2f} -> reale $600 "
f"Sh {sc['realistic']['sharpe']:+.2f} (haircut {sc['sharpe_haircut']:+.2f}) "
f"trade eseguiti {sc['n_executed_trades']}")
# ---- 6) SIGN-FALSIFICATION sul leader -----------------------------------
hr("6) SIGN-FALSIFICATION — invertire il segno del leader deve PEGGIORARE (altrimenti e' rumore)")
# leader e' VRP-Z+ se nel top; provo l'inverso esplicito su VRP-Z e DVOL
flips = [
("VRP-Z90 sign +1 (tesi)", make_vrp_z(90, 0.0, +1, True)),
("VRP-Z90 sign -1 (flip)", make_vrp_z(90, 0.0, -1, True)),
("DVOL-fear q0.5 (tesi)", make_dvol_level(0.5, "fear", True)),
("DVOL-calm q0.5 (flip)", make_dvol_level(0.5, "calm", True)),
]
for nm, fn in flips:
e = era_full_sharpe(fn)
print(f" {nm:<26} ERA Sh {e['sharpe']:+.2f} DD {e['dd']*100:.0f}% CAGR {e['cagr']*100:+.0f}%")
hr()
print("FINE. Leggere il verdetto onesto nel diario docs/diary/2026-06-29-dvol-directional.md")
if __name__ == "__main__":
main()
+84
View File
@@ -0,0 +1,84 @@
"""CROSS-MARKET COMBO — la sleeve equity-trend DIVERSIFICA il portafoglio crypto?
(2) dopo EQ-GTAA01. La via che alza il Sharpe COMPLESSIVO senza cercare nuovo alpha: combinare due
book scorrelati su mercati diversi. crypto = portafoglio attivo TP01+XS01+VRP01 (src.portfolio);
equity = GTAA lf vt12% (la migliore sleeve equity, corr SPY 0.64, maxDD ~15%). Se la correlazione
crypto<->equity e' bassa, il blend ha Sharpe > di ciascuno.
ALLINEAMENTO ONESTO: crypto e' calendario-giornaliero (7gg), equity giorni di borsa. Compoundo i
rendimenti crypto sul grid dei giorni di borsa (cattura i weekend) prima di combinare. Finestra =
era crypto (TP01 dal 2019). Esecuzione split Deribit+IB (lo noto: e' un portafoglio cross-venue).
"""
import sys
from pathlib import Path
import numpy as np, pandas as pd
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT)); sys.path.insert(0, str(ROOT / "scripts" / "research"))
from eq_sector_momentum import _sh, _cagr, _dd
from eq_gtaa_trend import gtaa
from eq_spy_trend import tsmom_exposure, backtest, _series
from src.portfolio.sleeves import active_sleeves
from src.portfolio.portfolio import StrategyPortfolio
ANN = np.sqrt(252.0)
def _ann_vol(r):
return float(np.std(np.asarray(r, float)) * ANN)
def compound_to_grid(daily: pd.Series, grid: pd.DatetimeIndex) -> pd.Series:
"""Compounda una serie di rendimenti (calendario) sul grid dato (giorni di borsa): per ogni data
del grid, somma-composta i rendimenti dal punto precedente."""
cum = (1.0 + daily).cumprod()
cum = cum.reindex(cum.index.union(grid)).ffill().reindex(grid)
return (cum / cum.shift(1) - 1.0).dropna()
def main():
print("=" * 96)
print(" CROSS-MARKET COMBO — equity-trend (GTAA) x crypto (TP01+XS01+VRP01)")
print("=" * 96)
# crypto blend (rinormalizzato, date diverse)
crypto = StrategyPortfolio(active_sleeves()).combined_daily()
if crypto.index.tz is None:
crypto.index = crypto.index.tz_localize("UTC")
# equity sleeve (giorni di borsa)
eq = gtaa(target_vol=0.12).dropna()
# allinea: compounda crypto sui giorni di borsa dell'equity
grid = eq.index[eq.index >= crypto.index[0]]
cr = compound_to_grid(crypto, grid)
J = pd.concat({"crypto": cr, "equity": eq.reindex(cr.index)}, axis=1).dropna()
print(f" finestra comune {J.index[0].date()}..{J.index[-1].date()} ({len(J)} giorni di borsa)\n")
c, e = J["crypto"], J["equity"]
print(" --- STANDALONE (sulla finestra comune) ---")
for nm, r in (("crypto TP01+XS01+VRP01", c), ("equity GTAA vt12", e)):
print(f" {nm:24} Sh {_sh(r):>5.2f} CAGR {_cagr(r.values,r.index)*100:>5.1f}% volAnn {_ann_vol(r)*100:>4.1f}% maxDD {_dd(r.values)*100:>4.0f}%")
print(f"\n --- CORRELAZIONE crypto <-> equity = {c.corr(e):+.3f} (bassa = diversifica) ---")
print("\n --- BLEND (capitale) ---")
print(f" {'mix':18} {'Sharpe':>7} {'CAGR%':>6} {'volAnn%':>7} {'maxDD%':>6}")
for wc in (1.0, 0.75, 0.5, 0.25, 0.0):
b = wc * c + (1 - wc) * e
print(f" crypto {int(wc*100):>3}/{int((1-wc)*100):<3} eq {_sh(b):>7.2f} {_cagr(b.values,b.index)*100:>6.1f} {_ann_vol(b)*100:>7.1f} {_dd(b.values)*100:>6.0f}")
# risk-parity (peso inverso alla vol) — il blend "giusto" quando le vol differiscono
vc, ve = _ann_vol(c), _ann_vol(e)
wc_rp = (1/vc) / (1/vc + 1/ve)
b_rp = wc_rp * c + (1 - wc_rp) * e
print(f"\n --- RISK-PARITY (inv-vol: crypto {wc_rp*100:.0f}% / eq {(1-wc_rp)*100:.0f}%) ---")
print(f" Sharpe {_sh(b_rp):.2f} CAGR {_cagr(b_rp.values,b_rp.index)*100:.1f}% volAnn {_ann_vol(b_rp)*100:.1f}% maxDD {_dd(b_rp.values)*100:.0f}%")
# verdetto: il blender batte il migliore dei due?
best_solo = max(_sh(c), _sh(e))
best_blend = max(_sh(0.5*c+0.5*e), _sh(b_rp))
print(f"\n --- VERDETTO ---")
print(f" miglior standalone Sharpe = {best_solo:.2f} | miglior blend Sharpe = {best_blend:.2f} "
f"-> {'DIVERSIFICA (blend > solo)' if best_blend > best_solo + 0.03 else 'nessun guadagno netto'}")
print(f" (nota: portafoglio cross-venue Deribit+IB; finestra crypto corta ~{len(J)//252}y)")
if __name__ == "__main__":
main()
+194
View File
@@ -0,0 +1,194 @@
"""EQ-FUNDNEWS-SHORT — "fondamentali/notizie NEGATIVI ma prezzo SU -> short". Screener FORWARD.
Idea utente: se i dati finanziari/notizie di un'azienda sono negativi ma la quotazione e' positiva
(sale), andare SHORT (scommessa che il prezzo scenda a riallinearsi ai fondamentali).
GATE DATI (lezione v2.0.0, PRIMA della strategia): NON backtestabile su dati certi.
- I fondamentali da rete (Yahoo) sono SNAPSHOT CORRENTI, non point-in-time storici. Applicarli a
prezzi passati = LOOK-AHEAD (restatement/survivorship). E' la trappola che ha creato la libreria
fasulla v2.0.0. -> nessun backtest. Serve un DB point-in-time (Compustat PIT / news storiche), assente.
- Quindi: come per la vol term-structure, l'unica via onesta e' uno SCREENER FORWARD che genera i
candidati short OGGI da dati di rete e li LOGGA in avanti. L'edge resta NON PROVATO finche' non
accumulato e validato forward. Questo script NON afferma un edge: produce candidati + li registra.
SEGNALE (tutto da rete, tokenless):
- "dati finanziari negativi" = score strutturato da Yahoo quoteSummary (crumb flow):
recommendationMean alto (->sell), earnings surprise recenti negative, revenueGrowth<0,
recommendationTrend sbilanciato a sell.
- "notizie negative" = sentiment lessicale crudo sulle headline (Yahoo news search).
- "quotazione positiva" = momentum 1m/3m > 0 (chart API).
-> SHORT candidate = (fond_neg alto OPPURE news_neg alto) AND momentum positivo (la DIVERGENZA).
ESEGUIBILITA' (muro): shortare richiede BORROW (locate+fee, hard-to-borrow caro/assente), perdita
illimitata, squeeze; PDT $25k per i day-trade; IB instabile qui; $600 di capitale; universo single-stock
(non i nostri ETF). Shortare la FORZA combatte il momentum (anomalia forte) -> premessa rischiosa.
uv run python scripts/research/eq_fundnews_short.py
"""
from __future__ import annotations
import sys
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
import numpy as np
import pandas as pd
import requests
RAW = ROOT / "data" / "raw"
H = {"User-Agent": "Mozilla/5.0"}
# universo dimostrativo: large/mid cap liquide, vari settori
UNIVERSE = ["AAPL", "MSFT", "NVDA", "TSLA", "META", "AMZN", "INTC", "F", "GM", "BA",
"DIS", "PYPL", "NKE", "PFE", "T", "WBA", "XOM", "KO", "CVX", "AMD"]
NEG_WORDS = {"downgrade", "miss", "missed", "cut", "cuts", "lawsuit", "probe", "fraud", "plunge",
"plunges", "warn", "warns", "warning", "slump", "loss", "losses", "weak", "weakness",
"decline", "declines", "fall", "falls", "drop", "sinks", "slashes", "recall", "halt",
"bankruptcy", "default", "layoff", "layoffs", "sell-off", "bearish", "underperform"}
POS_WORDS = {"beat", "beats", "upgrade", "surge", "surges", "record", "strong", "raise", "raises",
"soar", "soars", "rally", "jumps", "tops", "bullish", "outperform", "growth"}
def yahoo_session():
s = requests.Session(); s.headers.update(H)
s.get("https://fc.yahoo.com/", timeout=20)
crumb = s.get("https://query2.finance.yahoo.com/v1/test/getcrumb", timeout=20).text.strip()
return s, crumb
def fund_neg_score(rec_mean, surp, rev_g, sell_skew):
"""Pura: score di negativita' fondamentale [0..1] dai componenti disponibili (media)."""
comp = []
if rec_mean is not None:
comp.append(float(np.clip((rec_mean - 1) / 4, 0, 1))) # 1=buy ->0, 5=sell ->1
if surp:
comp.append(1.0 if np.mean(surp[:2]) < 0 else 0.0) # ultime 2 surprise negative
if rev_g is not None:
comp.append(1.0 if rev_g < 0 else float(max(0.0, 1 - rev_g * 5)))
if sell_skew is not None:
comp.append(float(np.clip(sell_skew * 3, 0, 1)))
return float(np.mean(comp)) if comp else None
def headline_sentiment(titles):
"""Pura: frazione di sentiment negativo sulle headline (lessico crudo). None se nessun hit."""
neg = pos = 0
for t in titles:
w = set(t.lower().replace(",", " ").replace(".", " ").split())
neg += len(w & NEG_WORDS); pos += len(w & POS_WORDS)
tot = neg + pos
return (neg / tot) if tot else 0.0, neg, pos
def momentum(sym):
u = f"https://query1.finance.yahoo.com/v8/finance/chart/{sym}?range=3mo&interval=1d"
j = requests.get(u, headers=H, timeout=25).json()["chart"]["result"][0]
c = pd.Series(j["indicators"]["quote"][0]["close"]).dropna().values
if len(c) < 25:
return None
m1 = c[-1] / c[-21] - 1.0 # ~1 mese
m3 = c[-1] / c[0] - 1.0 # ~3 mesi
return dict(last=float(c[-1]), mom_1m=float(m1), mom_3m=float(m3))
def fundamentals(s, crumb, sym):
mods = "financialData,recommendationTrend,earningsHistory"
u = f"https://query2.finance.yahoo.com/v10/finance/quoteSummary/{sym}?modules={mods}&crumb={crumb}"
res = s.get(u, timeout=25).json()["quoteSummary"]["result"][0]
fd = res.get("financialData", {})
rec_mean = fd.get("recommendationMean", {}).get("raw")
rev_g = fd.get("revenueGrowth", {}).get("raw")
eh = res.get("earningsHistory", {}).get("history", [])
surp = [h.get("surprisePercent", {}).get("raw") for h in eh if h.get("surprisePercent")]
rt = res.get("recommendationTrend", {}).get("trend", [])
sell_skew = None
if rt:
t = rt[0]; tot = sum(t.get(k, 0) for k in ("strongBuy", "buy", "hold", "sell", "strongSell"))
sell_skew = (t.get("sell", 0) + t.get("strongSell", 0)) / tot if tot else None
return dict(fund_neg=fund_neg_score(rec_mean, surp, rev_g, sell_skew),
rec_mean=rec_mean, rev_growth=rev_g, last_surprise=surp[0] if surp else None,
sell_skew=sell_skew)
def news_sentiment(sym):
u = f"https://query1.finance.yahoo.com/v1/finance/search?q={sym}&newsCount=8&quotesCount=0"
news = requests.get(u, headers=H, timeout=25).json().get("news", [])
if not news:
return dict(news_neg=None, n=0)
nn, neg, pos = headline_sentiment([n.get("title", "") for n in news])
return dict(news_neg=nn, n=len(news), neg_hits=neg, pos_hits=pos)
def screen(universe):
s, crumb = yahoo_session()
rows = []
for sym in universe:
try:
mom = momentum(sym)
if mom is None:
continue
fun = fundamentals(s, crumb, sym)
nw = news_sentiment(sym)
fneg = fun["fund_neg"]
nneg = nw["news_neg"]
rising = mom["mom_1m"] > 0.02 # quotazione positiva
fund_bad = fneg is not None and fneg >= 0.5
news_bad = nneg is not None and nneg >= 0.5
short_cand = rising and (fund_bad or news_bad)
rows.append(dict(sym=sym, mom_1m=mom["mom_1m"], mom_3m=mom["mom_3m"],
fund_neg=fneg, news_neg=nneg, rec_mean=fun["rec_mean"],
rev_growth=fun["rev_growth"], last_surprise=fun["last_surprise"],
short_cand=short_cand))
time.sleep(0.3)
except Exception as e:
rows.append(dict(sym=sym, error=repr(e)[:60]))
return pd.DataFrame(rows)
def main():
print("=" * 100)
print(" EQ-FUNDNEWS-SHORT — divergenza fondamentali/notizie NEG vs prezzo SU -> short candidate")
print(" (SCREENER FORWARD da dati di rete — NON un backtest: edge non provato, vedi header)")
print("=" * 100)
df = screen(UNIVERSE)
ok = df[df.get("error").isna()] if "error" in df else df
ok = ok.sort_values("fund_neg", ascending=False, na_position="last")
print(f" {'sym':5} {'mom1m':>7} {'mom3m':>7} {'fund_neg':>8} {'news_neg':>8} "
f"{'recMean':>7} {'revGr':>7} {'surp':>7} SHORT?")
for _, r in ok.iterrows():
fn = f"{r['fund_neg']:.2f}" if pd.notna(r['fund_neg']) else " n/a"
nn = f"{r['news_neg']:.2f}" if pd.notna(r['news_neg']) else " n/a"
rm = f"{r['rec_mean']:.2f}" if pd.notna(r['rec_mean']) else " n/a"
rg = f"{r['rev_growth']*100:+.0f}%" if pd.notna(r['rev_growth']) else " n/a"
sp = f"{r['last_surprise']*100:+.0f}%" if pd.notna(r['last_surprise']) else " n/a"
flag = " <<< SHORT" if r["short_cand"] else ""
print(f" {r['sym']:5} {r['mom_1m']*100:>+6.1f}% {r['mom_3m']*100:>+6.1f}% {fn:>8} {nn:>8} "
f"{rm:>7} {rg:>7} {sp:>7}{flag}")
cands = ok[ok["short_cand"]]["sym"].tolist()
print(f"\n CANDIDATI SHORT oggi (fond/news neg + prezzo su): {cands or 'nessuno'}")
# log forward (idempotente per giorno)
today = pd.Timestamp.now("UTC").normalize()
ok2 = ok.copy(); ok2.insert(0, "date", today)
fp = RAW / "fundnews_short_screen.parquet"
hist = pd.read_parquet(fp) if fp.exists() else pd.DataFrame()
if len(hist):
hist = hist[hist["date"] != today]
pd.concat([hist, ok2], ignore_index=True).to_parquet(fp, index=False)
print(f" -> snapshot loggato in {fp.name} (forward dataset; serve accumulo+validazione)")
print("\n" + "=" * 100)
print(" ONESTA' / ESEGUIBILITA'")
print("=" * 100)
print(" - NON backtestabile: fondamentali = snapshot correnti, non point-in-time -> look-ahead (v2.0.0).")
print(" - Premessa RISCHIOSA: shortare un prezzo che SALE combatte il momentum (anomalia forte);")
print(" il rialzo 'malgrado' notizie cattive spesso PREZZA info che i fondamentali trailing non hanno.")
print(" - Eseguibilita': borrow (locate/fee, hard-to-borrow), perdita illimitata, squeeze, PDT $25k,")
print(" IB instabile, $600. -> NON deployabile. Deliverable = screener forward + log, edge da provare.")
if __name__ == "__main__":
main()
+109
View File
@@ -0,0 +1,109 @@
"""EQ-GTAA01 — Trend difensivo MULTI-ASSET (GTAA) sull'universo ETF.
EQ-TREND01 ha mostrato che il trend long-flat su SPY taglia il DD (analogo TP01). La diversificazione
delle SORGENTI di trend (azioni US/tech/small + bond + oro + high-yield) di solito migliora il
rischio-aggiustato del trend mono-asset. Qui: ogni asset gestito col proprio trend long-flat
(TSMOM multi-orizzonte), equal-weight tra gli asset DISPONIBILI (la quota "off" o assente -> cash).
DATI: cache eqlib (ADJUSTED, nessun IB). Start diversi -> outer-join con peso rinormalizzato sugli
asset esistenti (come gli sleeve crypto). Finestra lunga: SPY/QQQ/IWM da ~2000; TLT(2016)/GLD(2004)/
HYG(2007) entrano dopo. Riporto anche la finestra 6-asset comune (2016+).
GIUDIZIO: vs SPY buy&hold, vs EW statico (isola il valore del TIMING di trend), vs SPY-trend mono;
Sharpe full/pre15/OOS + maxDD + plateau. Causale, netto fee.
"""
import sys
from pathlib import Path
import numpy as np, pandas as pd
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "scripts" / "research"))
import eqlib
from eqlib import load_eq
from eq_sector_momentum import _sh, _cagr, _dd, EQ_HOLDOUT, spy_bh
from eq_spy_trend import tsmom_exposure, backtest, _series
ASSETS = ["SPY", "QQQ", "IWM", "TLT", "GLD", "HYG"]
def gated_returns(sym, horizons=(21, 63, 126, 252), fee_side=0.0002, target_vol=None, lev_cap=1.0):
"""Rendimenti netti daily di UN asset gestito col proprio trend long-flat (cash quando off)."""
px = _series(sym)
ex = tsmom_exposure(px, horizons=horizons, target_vol=target_vol, lev_cap=lev_cap)
return backtest(px, ex, fee_side=fee_side)
def gtaa(assets=ASSETS, horizons=(21, 63, 126, 252), fee_side=0.0002, target_vol=None):
"""Portafoglio GTAA: media (equal-weight) dei rendimenti trend-gated sugli asset disponibili
ogni giorno (outer-join). La quota di asset assenti/in-cash resta in cash."""
cols = {a: gated_returns(a, horizons, fee_side, target_vol) for a in assets}
R = pd.concat(cols, axis=1).sort_index()
return R.mean(axis=1, skipna=True) # EW sugli asset esistenti quel giorno
def ew_buyhold(assets=ASSETS):
cols = {a: _series(a).pct_change() for a in assets}
return pd.concat(cols, axis=1).sort_index().mean(axis=1, skipna=True)
def _row(name, r, common=None, bench=None):
r = r.dropna() if common is None else r.reindex(common).fillna(0.0)
h = r[r.index >= EQ_HOLDOUT]; ii = r[r.index < EQ_HOLDOUT]
tim = float((r != 0).mean()) * 100
extra = ""
if bench is not None:
J = pd.concat({"r": r, "b": bench}, axis=1, join="inner").dropna()
extra = f" corrSPY {J['r'].corr(J['b']):+.2f}"
print(f" {name:26} CAGR {_cagr(r.values, r.index)*100:>5.1f}% Sh {_sh(r):>5.2f} "
f"(pre15 {_sh(ii):>5.2f}|OOS {_sh(h):>5.2f}) maxDD {_dd(r.values)*100:>4.0f}% inMkt {tim:>3.0f}%{extra}")
def main():
print("=" * 100)
print(" EQ-GTAA01 — Trend difensivo MULTI-ASSET (GTAA)")
print("=" * 100)
spy = spy_bh()
g = gtaa() # outer-join, finestra lunga
gl = g.dropna()
print(f" finestra lunga (outer-join) {gl.index[0].date()}..{gl.index[-1].date()} ({len(gl)}g) OOS {EQ_HOLDOUT.date()}+\n")
print(" --- BASELINE & confronti (finestra lunga) ---")
cl = gl.index
_row("SPY buy&hold", spy.reindex(cl).fillna(0))
_row("EW statico (no trend)", ew_buyhold().reindex(cl).fillna(0), bench=spy.reindex(cl).fillna(0))
_row("SPY-trend mono (TREND01)", backtest(_series("SPY"), tsmom_exposure(_series("SPY"))).reindex(cl).fillna(0), bench=spy.reindex(cl).fillna(0))
print("\n --- GTAA (multi-asset trend) ---")
_row("GTAA lf", gl, bench=spy.reindex(cl).fillna(0))
_row("GTAA lf vt12%", gtaa(target_vol=0.12).reindex(cl).fillna(0), bench=spy.reindex(cl).fillna(0))
# finestra 6-asset comune (tutti gli ETF esistono): 2016+
tlt0 = _series("TLT").index[0]
c6 = gl.index[gl.index >= tlt0]
print(f"\n --- finestra 6-asset comune ({c6[0].date()}+) ---")
_row("SPY buy&hold (6a win)", spy.reindex(c6).fillna(0))
_row("GTAA lf (6a win)", g.reindex(c6).fillna(0), bench=spy.reindex(c6).fillna(0))
_row("GTAA lf vt12 (6a win)", gtaa(target_vol=0.12).reindex(c6).fillna(0), bench=spy.reindex(c6).fillna(0))
# MARGINALE vs SPY
print("\n --- MARGINALE vs SPY (GTAA lf, finestra lunga) ---")
J = pd.concat({"spy": spy, "c": gl}, axis=1, join="inner").dropna(); JH = J[J.index >= EQ_HOLDOUT]
print(f" corr full {J['spy'].corr(J['c']):+.3f} | OOS {JH['spy'].corr(JH['c']):+.3f}")
for wt in (0.5, 1.0):
bf = _sh((1-wt)*J['spy']+wt*J['c'])-_sh(J['spy']); bh = _sh((1-wt)*JH['spy']+wt*JH['c'])-_sh(JH['spy'])
lbl = "100% GTAA" if wt == 1.0 else "50/50 SPY/GTAA"
print(f" {lbl:16}: uplift Sharpe FULL {bf:+.3f} OOS {bh:+.3f}")
print(" DD nei bear (GTAA vs SPY):")
for lo, hi, lbl in [("2000-03-01","2002-12-31","dot-com"),("2007-10-01","2009-06-30","GFC"),
("2020-02-01","2020-04-30","COVID"),("2022-01-01","2022-12-31","2022")]:
seg=lambda s: _dd(s.reindex(cl).fillna(0)[(cl>=pd.Timestamp(lo,tz='UTC'))&(cl<=pd.Timestamp(hi,tz='UTC'))].values)*100
print(f" {lbl:8} GTAA {seg(gl):.0f}% | SPY {seg(spy):.0f}%")
print("\n --- PLATEAU (Sharpe FULL/pre15/OOS, DD, CAGR) GTAA lf, finestra lunga ---")
print(f" {'horizons':22} {'FULL':>6} {'pre15':>6} {'OOS':>6} {'DD%':>5} {'CAGR%':>6}")
for hz in [(63,126,252),(21,63,126,252),(126,252),(252,)]:
r = gtaa(horizons=hz).reindex(cl).fillna(0); h=r[r.index>=EQ_HOLDOUT]; ii=r[r.index<EQ_HOLDOUT]
print(f" {'x'.join(map(str,hz)):22} {_sh(r):>6.2f} {_sh(ii):>6.2f} {_sh(h):>6.2f} {_dd(r.values)*100:>5.0f} {_cagr(r.values,r.index)*100:>6.1f}")
if __name__ == "__main__":
main()
+172
View File
@@ -0,0 +1,172 @@
"""EQ-MR — "scalping azioni IB quando sottoquotate" + CHECK DATI DALLA RETE. Analisi onesta.
L'utente chiede: su IB, comprare azioni quando "sottoquotate" (oversold/sotto fair-value), con
verifica incrociata dei dati dalla rete. Due pezzi, entrambi in-metodo (la lezione fondante del
progetto e' "non fidarti di un feed solo" — il disastro v2.0.0):
1) CHECK DATI DALLA RETE confronta i nostri dati certificati IB (data/raw/eq_*_1d.parquet,
ADJUSTED_LAST) con una SORGENTE INDIPENDENTE di rete (Yahoo Finance chart API, tokenless).
Confronto sui RENDIMENTI giornalieri (invarianti all'aggiustamento) + ultimo close. Verdetto:
feed CONCORDE (bps piccoli) o DIVERGENTE. E' il template del pre-trade price-check live.
2) MEAN-REVERSION "SOTTOQUOTATA" sul daily lo *scalping* intraday NON e' backtestabile (non
abbiamo dati intraday, solo eq_*_1d) ne' eseguibile (vedi sotto), quindi si testa la versione
onesta: swing mean-reversion su ETF indice (RSI2 oversold + filtro trend MA200, exit a MA5 =
Connors). Causale (segnale <= close[i], entry a close[i]), netto fee, hold-out OOS, per-anno.
ESEGUIBILITA' (muro, da dire subito):
- PDT RULE: il day-trading di azioni US sotto $25.000 e' limitato a 3 day-trade/5gg -> lo scalping
e' regolatoriamente BLOCCATO al capitale del progetto. (l'analogo equity del muro STAT-MODE a $600).
- Niente dati INTRADAY -> scalping non backtestabile.
- IB Gateway in questo ambiente e' instabile (timeout ordini diagnosticato) -> niente HFT affidabile.
uv run python scripts/research/eq_meanrev_ib.py
"""
from __future__ import annotations
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "scripts" / "research"))
import numpy as np
import pandas as pd
import requests
from eqlib import load_eq # type: ignore
SQ = np.sqrt(252)
H = {"User-Agent": "Mozilla/5.0"}
# ----------------------------- 1) CHECK DATI DALLA RETE -----------------------------
def yahoo_daily(sym: str, rng: str = "1y") -> pd.DataFrame:
u = f"https://query1.finance.yahoo.com/v8/finance/chart/{sym}?range={rng}&interval=1d"
j = requests.get(u, headers=H, timeout=30).json()["chart"]["result"][0]
ts = pd.to_datetime(j["timestamp"], unit="s", utc=True).normalize()
q = j["indicators"]["quote"][0]
adj = j["indicators"].get("adjclose", [{}])[0].get("adjclose", q["close"])
return pd.DataFrame({"close": q["close"], "adjclose": adj}, index=ts).dropna()
def cross_check(syms):
print("=" * 90)
print(" 1) CHECK DATI DALLA RETE — IB certificato (eq_*, ADJUSTED) vs Yahoo adjclose (rete indip.)")
print("=" * 90)
print(f" {'sym':5} {'n com':>6} {'ret maxΔ':>9} {'ret medΔ':>9} {'last IB':>10} {'last YHOO':>10} {'Δbps':>7} esito")
for s in syms:
try:
ib = load_eq(s)["close"].astype(float)
ib.index = ib.index.normalize()
yh = yahoo_daily(s)["adjclose"] # adjusted vs adjusted (apples-to-apples)
J = pd.concat({"ib": ib, "yh": yh}, axis=1, join="inner").dropna().tail(180)
if len(J) < 20:
print(f" {s:5} overlap insufficiente"); continue
R = pd.concat({"a": J["ib"].pct_change(), "b": J["yh"].pct_change()},
axis=1, join="inner").dropna()
d = (R["a"] - R["b"]).abs()
last_bps = abs(J["ib"].iloc[-1] / J["yh"].iloc[-1] - 1) * 1e4
ok = d.max() < 0.001 and last_bps < 20 # ret entro 10bps, last entro 20bps
print(f" {s:5} {len(J):>6} {d.max()*1e4:>8.1f}b {d.median()*1e4:>8.1f}b "
f"{J['ib'].iloc[-1]:>10.2f} {J['yh'].iloc[-1]:>10.2f} {last_bps:>6.1f}b "
f"{'CONCORDE' if ok else 'DIVERGENTE -> INDAGARE'}")
except Exception as e:
print(f" {s:5} ERRORE: {repr(e)[:80]}")
print(" -> confronto sui RENDIMENTI adjusted-vs-adjusted: devono combaciare a pochi bps. Una")
print(" divergenza non spiegata = feed sospetto, NON tradare prima di averla capita (v2.0.0).")
# ----------------------------- 2) MEAN-REVERSION "SOTTOQUOTATA" -----------------------------
def rsi(close: np.ndarray, n: int) -> np.ndarray:
d = np.diff(close, prepend=close[0])
up = pd.Series(np.where(d > 0, d, 0.0)).ewm(alpha=1 / n, adjust=False).mean().values
dn = pd.Series(np.where(d < 0, -d, 0.0)).ewm(alpha=1 / n, adjust=False).mean().values
rs = np.divide(up, dn, out=np.full_like(up, np.inf), where=dn > 0)
return 100 - 100 / (1 + rs)
def mr_target(close: np.ndarray, entry=10.0, exit_ma=5, trend=200) -> np.ndarray:
"""Posizione 0/1 DECISA a close[i] (Connors RSI2): entra se sottoquotata (RSI2<entry) in uptrend
(close>MA{trend}); esci quando close>MA{exit_ma}. Causale: usa solo dati <= close[i]."""
r2 = rsi(close, 2)
ma_t = pd.Series(close).rolling(trend).mean().values
ma_x = pd.Series(close).rolling(exit_ma).mean().values
pos = np.zeros(len(close)); inpos = False
for i in range(len(close)):
if not np.isfinite(ma_t[i]):
continue
if not inpos and close[i] > ma_t[i] and r2[i] < entry:
inpos = True
elif inpos and close[i] > ma_x[i]:
inpos = False
pos[i] = 1.0 if inpos else 0.0
return pos
def backtest(sym: str, fee_bps=3.0, **kw) -> pd.Series:
df = load_eq(sym); close = df["close"].astype(float).values
idx = df.index
tgt = mr_target(close, **kw)
r = np.zeros(len(close)); r[1:] = close[1:] / close[:-1] - 1.0
held = np.zeros(len(tgt)); held[1:] = tgt[:-1] # decisa a close[i-1], tenuta in i
turn = np.abs(np.diff(held, prepend=0.0))
net = held * r - (fee_bps / 1e4) * turn
return pd.Series(net, index=idx)
def metrics(daily: pd.Series, lo=None):
if lo is not None:
daily = daily[daily.index >= lo]
rr = daily.values
sh = float(np.mean(rr) / np.std(rr) * SQ) if np.std(rr) > 0 else 0.0
eq = np.cumprod(1 + rr); 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
expo = float((daily != 0).mean())
return dict(sharpe=sh, dd=dd, cagr=cagr, expo=expo)
def buyhold(sym, lo=None):
df = load_eq(sym); c = df["close"].astype(float)
r = c.pct_change().fillna(0.0)
return metrics(r, lo=lo)
def run_meanrev(syms, holdout="2015-01-01"):
HO = pd.Timestamp(holdout, tz="UTC")
print("\n" + "=" * 90)
print(f" 2) MEAN-REVERSION 'SOTTOQUOTATA' (RSI2<10 + filtro MA200, exit MA5). Hold-out {holdout}.")
print("=" * 90)
print(f" {'sym':5} | {'FULL Sh':>7} {'DD':>6} {'CAGR':>6} {'expo':>5} | "
f"{'HOLD Sh':>7} {'HOLD CAGR':>9} | {'B&H Sh':>6} {'B&H HOLD':>8}")
for s in syms:
net = backtest(s)
f = metrics(net); h = metrics(net, lo=HO)
bh = buyhold(s); bhh = buyhold(s, lo=HO)
print(f" {s:5} | {f['sharpe']:>+7.2f} {f['dd']*100:>5.0f}% {f['cagr']*100:>+5.0f}% "
f"{f['expo']*100:>4.0f}% | {h['sharpe']:>+7.2f} {h['cagr']*100:>+8.0f}% | "
f"{bh['sharpe']:>+6.2f} {bhh['sharpe']:>+7.2f}")
print(" expo = % giorni investito. B&H = buy&hold sullo stesso ETF (il benchmark onesto).")
print("\n Sweep fee (SPY) — quanto regge ai costi reali:")
for fb in (0.0, 3.0, 5.0, 10.0):
f = metrics(backtest("SPY", fee_bps=fb)); h = metrics(backtest("SPY", fee_bps=fb), lo=HO)
print(f" fee {fb:>4.1f}bps RT: FULL Sh {f['sharpe']:+.2f} HOLD Sh {h['sharpe']:+.2f}")
def main():
cross_check(["SPY", "QQQ", "IWM", "GLD", "TLT", "HYG"])
run_meanrev(["SPY", "QQQ", "IWM", "DIA", "EEM"])
print("\n" + "=" * 90)
print(" 3) ESEGUIBILITA' (il muro)")
print("=" * 90)
print(" - PDT RULE: day-trading azioni US < $25k = max 3 day-trade/5gg -> SCALPING BLOCCATO al")
print(" capitale del progetto. E' l'analogo equity del muro STAT-MODE a $600 sul crypto.")
print(" - Dati INTRADAY assenti (solo eq_*_1d) -> lo scalping non e' backtestabile, solo lo swing MR.")
print(" - IB Gateway instabile in questo ambiente (timeout ordini) -> niente HFT affidabile.")
if __name__ == "__main__":
main()
+159
View File
@@ -0,0 +1,159 @@
"""EQ-MOM01 — Momentum cross-sectional settoriale (SPDR), backtest onesto.
Prima ricerca del fronte equity (branch research/equities-ib). L'edge "noioso e robusto" piu'
plausibile in un mercato efficiente: ruotare nei settori a momentum forte. Domanda chiave (come nel
crypto col soffitto TP01): NON "fa soldi?" (un long-only equity cavalca il toro) ma **batte/ADDS a
SPY buy&hold?** il baseline vero in equity. Anche vs equal-weight 9 settori (isola il timing del
momentum dal tilt equal-weight).
DATI: cache su disco eq_*.parquet (ADJUSTED div+split), via eqlib (nessun IB). 9 settori CLASSICI
dal 1998 (27.5y) per il backtest lungo; 11 settori (2018+) come robustezza.
COSTRUZIONE (causale): ogni REB giorni, momentum = blend di lookback [63,126,252]g con SKIP recente
(12-1 classico), z-score cross-sectional mediato. long-only: full-invested nei top-k (confronto
like-for-like con SPY). long-short: dollar-neutral top-k vs bottom-k. Posizione decisa a <= i-1,
tenuta da i (W[:-1]*dret[1:]). Netto fee sul turnover. Opz. vol-target.
GIUDIZIO: standalone (FULL / pre-2015 / hold-out 2015+ / per-anno, CAGR, Sharpe, maxDD) vs SPY e
EW-settori; marginale vs SPY (corr, uplift blend full+hold, edge in-sample, persistenza multi-cut);
plateau su lookback/k/reb/skip; sweep fee.
"""
import sys
from pathlib import Path
import numpy as np, pandas as pd
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "scripts" / "research"))
import eqlib
from eqlib import panel, load_eq, SECTORS_CLASSIC, SECTORS
ANN = np.sqrt(252.0)
EQ_HOLDOUT = pd.Timestamp("2015-01-01", tz="UTC") # OOS lungo: ultimi ~11 anni (post-GFC, dove il momentum e' decaduto)
def _sh(r):
r = np.asarray(pd.Series(r).dropna(), float)
return float(np.mean(r) / np.std(r) * ANN) if len(r) > 2 and np.std(r) > 0 else 0.0
def _cagr(r, idx):
r = np.asarray(r, float); yrs = (idx[-1] - idx[0]).days / 365.25
return float(np.prod(1 + r) ** (1 / yrs) - 1) if yrs > 0 else 0.0
def _dd(r):
eq = np.cumprod(1 + np.asarray(r, float)); pk = np.maximum.accumulate(eq)
return float(np.max((pk - eq) / pk)) if len(eq) else 0.0
def momentum(universe=tuple(SECTORS_CLASSIC), lookbacks=(63, 126, 252), k=3, reb=21,
skip=21, mode="long", target_vol=None, fee_side=0.0002):
"""Serie netta daily del book momentum settoriale. mode='long' (top-k full-invested) o
'ls' (dollar-neutral top-k vs bottom-k)."""
P = panel(universe, how="inner")
idx = P.index; px = P.values; n, A = px.shape
dret = np.vstack([np.zeros(A), px[1:] / px[:-1] - 1.0])
mlb = max(lookbacks) + skip
W = np.zeros((n, A)); w = np.zeros(A)
for i in range(n):
if i >= mlb and i % reb == 0:
score = np.zeros(A); cnt = 0
for Lb in lookbacks:
a, b = i - skip - Lb, i - skip
rL = px[b] / px[a] - 1.0
sd = rL.std()
if sd > 0:
score += (rL - rL.mean()) / sd; cnt += 1
if cnt:
score /= cnt
order = np.argsort(score)
w = np.zeros(A)
if mode == "long":
w[order[-k:]] = 1.0 / k # full-invested nei top-k
else:
w[order[-k:]] = 0.5 / k; w[order[:k]] = -0.5 / k
W[i] = w
gross = np.zeros(n); gross[1:] = np.sum(W[:-1] * dret[1:], axis=1)
turn = np.zeros(n); turn[0] = np.abs(W[0]).sum(); turn[1:] = np.abs(np.diff(W, axis=0)).sum(axis=1)
net = gross - turn * fee_side
s = pd.Series(net, index=idx)
if target_vol:
rv = s.rolling(63, min_periods=20).std().shift(1) * ANN
scale = np.clip(np.nan_to_num(target_vol / rv.replace(0, np.nan).values, nan=0.0), 0, 3.0)
s = pd.Series(s.values * scale, index=idx)
return s
def spy_bh():
d = load_eq("SPY")["close"].astype(float)
return pd.Series(d.values[1:] / d.values[:-1] - 1.0, index=d.index[1:])
def ew_sectors_bh(universe=tuple(SECTORS_CLASSIC)):
P = panel(universe, how="inner"); dret = P.pct_change().dropna()
return dret.mean(axis=1)
def _line(name, r, idx=None, bench=None):
idx = idx if idx is not None else r.index
r = r.reindex(idx).fillna(0.0) if hasattr(r, "reindex") else pd.Series(r, index=idx)
h = r[r.index >= EQ_HOLDOUT]; isamp = r[r.index < EQ_HOLDOUT]
extra = ""
if bench is not None:
J = pd.concat({"r": r, "b": bench}, axis=1, join="inner").dropna()
extra = f" corr_SPY {J['r'].corr(J['b']):+.2f}"
print(f" {name:26} CAGR {_cagr(r.values, r.index)*100:>5.1f}% Sh {_sh(r):>5.2f} "
f"(pre15 {_sh(isamp):>5.2f} | OOS15+ {_sh(h):>5.2f}) maxDD {_dd(r.values)*100:>4.0f}%{extra}")
def main():
print("=" * 100)
print(" EQ-MOM01 — Momentum cross-sectional settoriale (9 SPDR classici, 1998+)")
print("=" * 100)
spy = spy_bh(); ew = ew_sectors_bh()
base = momentum() # long, lb[63,126,252], k=3, reb21, skip21
common = base.index
print(f" periodo {common[0].date()}..{common[-1].date()} ({len(common)}g) hold-out OOS = {EQ_HOLDOUT.date()}+\n")
print(" --- BASELINE da battere ---")
_line("SPY buy&hold", spy, common)
_line("EW 9 settori buy&hold", ew, common, bench=spy)
print("\n --- EQ-MOM01 ---")
_line("MOM long top-3", base, common, bench=spy)
_line("MOM long top-3 vt15%", momentum(target_vol=0.15), common, bench=spy)
_line("MOM long-short top-3", momentum(mode="ls"), common, bench=spy)
# MARGINALE vs SPY (il test che conta in equity)
print("\n --- MARGINALE vs SPY buy&hold (aggiunge al baseline?) ---")
J = pd.concat({"spy": spy, "c": base}, axis=1, join="inner").dropna()
JH = J[J.index >= EQ_HOLDOUT]
print(f" corr(MOM,SPY) full {J['spy'].corr(J['c']):+.3f} | OOS {JH['spy'].corr(JH['c']):+.3f}")
for wt in (0.25, 0.5):
bf = _sh((1-wt)*J["spy"]+wt*J["c"]) - _sh(J["spy"])
bh = _sh((1-wt)*JH["spy"]+wt*JH["c"]) - _sh(JH["spy"])
print(f" blend {int((1-wt)*100)}/{int(wt*100)} SPY/MOM: uplift Sharpe FULL {bf:+.3f} OOS {bh:+.3f}")
# per-decade (multi-cut onesto)
print(" Sharpe MOM per blocco: ", {f"{y}s": round(_sh(base[(base.index.year>=y)&(base.index.year<y+5)]),2)
for y in (1999,2004,2009,2014,2019,2024)})
# PLATEAU
print("\n --- PLATEAU (Sharpe FULL / pre15 / OOS15+) long-only ---")
print(f" {'cfg':24} {'FULL':>6} {'pre15':>6} {'OOS':>6} {'CAGR%':>6} {'DD%':>5}")
for lbs in [(126,), (63,126,252), (252,)]:
for k in (2, 3, 4):
for reb in (21,):
s = momentum(lookbacks=lbs, k=k, reb=reb)
tag = f"lb{'-'.join(map(str,lbs))} k{k}"
h=s[s.index>=EQ_HOLDOUT]; ii=s[s.index<EQ_HOLDOUT]
print(f" {tag:24} {_sh(s):>6.2f} {_sh(ii):>6.2f} {_sh(h):>6.2f} {_cagr(s.values,s.index)*100:>6.1f} {_dd(s.values)*100:>5.0f}")
# sweep fee + robustezza 11 settori (2018+)
print("\n --- ROBUSTEZZA ---")
for fee in (0.0, 0.0002, 0.0005, 0.001):
s = momentum(fee_side=fee); print(f" fee {fee*100:.2f}%/lato: Sh FULL {_sh(s):.2f} OOS {_sh(s[s.index>=EQ_HOLDOUT]):.2f}")
s11 = momentum(universe=tuple(SECTORS)); spy11 = spy.reindex(s11.index)
print(f" 11 settori (2018+): MOM Sh {_sh(s11):.2f} vs SPY {_sh(spy11):.2f} (periodo {s11.index[0].date()}+)")
if __name__ == "__main__":
main()
+146
View File
@@ -0,0 +1,146 @@
"""EQ-TREND01 — Trend DIFENSIVO time-series su SPY (analogo equity di TP01).
Il momentum cross-sectional settoriale e' morto (EQ-MOM01). Ma nel crypto l'unica cosa che ha retto
NON era un alpha relative-value: era TP01, un trend DIFENSIVO che taglia il drawdown restando vicino
al ritorno. L'equity ha lo stesso buco: SPY buy&hold fa Sharpe ~0.51 ma con maxDD 55% (due bear -50%:
2000-02 e 2008-09). Domanda: un trend long-flat su SPY ALZA il Sharpe e DIMEZZA il DD restando
investito nei tori? (NON cerchiamo di battere il CAGR cerchiamo il taglio del rischio, come TP01.)
DATI: cache su disco eq_spy/eq_tlt (ADJUSTED), via eqlib (nessun IB).
COSTRUZIONE (causale, stile TP01): TSMOM multi-orizzonte [21,63,126,252]g (1/3/6/12 mesi); target =
frazione di orizzonti in trend-up (0..1, allocazione graduale). Vol-target opz. Posizione decisa a
<=i-1, tenuta da i. Netto fee sul turnover. Varianti: long-flat (cash in risk-off), long-bonds
(TLT in risk-off, solo dal 2016), e SMA-200 binario (Faber) come riferimento classico.
GIUDIZIO: vs SPY buy&hold (CAGR/Sharpe full-pre15-OOS15+/maxDD/time-in-market), marginale vs SPY,
DD nei due bear storici, plateau (orizzonti/vol-target/cap leva), sweep fee.
"""
import sys
from pathlib import Path
import numpy as np, pandas as pd
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "scripts" / "research"))
import eqlib
from eqlib import load_eq
from eq_sector_momentum import _sh, _cagr, _dd, EQ_HOLDOUT, spy_bh
ANN = np.sqrt(252.0)
def _series(sym):
d = load_eq(sym)["close"].astype(float)
return pd.Series(d.values, index=d.index)
def tsmom_exposure(close: pd.Series, horizons=(21, 63, 126, 252), target_vol=None,
lev_cap=1.0) -> pd.Series:
"""Esposizione SPY in [0, lev_cap]: frazione di orizzonti in trend-up, opz. vol-targeted (causale)."""
px = close.values; n = len(px); tgt = np.zeros(n)
mh = max(horizons)
for i in range(mh, n):
tgt[i] = np.mean([1.0 if px[i] > px[i - H] else 0.0 for H in horizons])
s = pd.Series(tgt, index=close.index)
if target_vol:
ret = close.pct_change()
rv = ret.rolling(63, min_periods=20).std().shift(1) * ANN
scale = np.clip(np.nan_to_num(target_vol / rv.replace(0, np.nan).values, nan=0.0), 0, lev_cap / 0.0 if False else 10.0)
s = (s * scale).clip(0, lev_cap)
else:
s = s.clip(0, lev_cap)
return s
def sma_timing(close: pd.Series, win=200) -> pd.Series:
"""Faber: long se close > SMA(win), altrimenti flat. Binario {0,1}."""
sma = close.rolling(win, min_periods=win // 2).mean()
return (close > sma).astype(float)
def backtest(close: pd.Series, exposure: pd.Series, risk_off: pd.Series | None = None,
fee_side=0.0002) -> pd.Series:
"""Rendimenti netti: held = exposure ritardata di 1 (causale). La quota non in SPY (1-held, se
exposure<=1) va in risk_off (es. TLT) o cash (0). Fee sul turnover di SPY."""
ret = close.pct_change().fillna(0.0).values
exp = exposure.reindex(close.index).fillna(0.0).values
held = np.zeros(len(exp)); held[1:] = exp[:-1]
net = held * ret
if risk_off is not None:
ro = risk_off.reindex(close.index).pct_change().fillna(0.0).values
cash_w = np.clip(1.0 - held, 0.0, 1.0) # quota fuori da SPY -> bonds
net = net + cash_w * ro
net = net - fee_side * np.abs(np.diff(held, prepend=0.0))
return pd.Series(net, index=close.index)
def _row(name, r, common, bench=None):
r = r.reindex(common).fillna(0.0)
h = r[r.index >= EQ_HOLDOUT]; ii = r[r.index < EQ_HOLDOUT]
tim = float((r != 0).mean()) * 100
extra = ""
if bench is not None:
J = pd.concat({"r": r, "b": bench.reindex(common).fillna(0.0)}, axis=1).dropna()
extra = f" corr {J['r'].corr(J['b']):+.2f}"
print(f" {name:24} CAGR {_cagr(r.values, r.index)*100:>5.1f}% Sh {_sh(r):>5.2f} "
f"(pre15 {_sh(ii):>5.2f}|OOS {_sh(h):>5.2f}) maxDD {_dd(r.values)*100:>4.0f}% inMkt {tim:>3.0f}%{extra}")
def _bear_dd(r, common, lo, hi, label):
seg = r.reindex(common).fillna(0.0)
seg = seg[(seg.index >= pd.Timestamp(lo, tz="UTC")) & (seg.index <= pd.Timestamp(hi, tz="UTC"))]
return f"{label}: {_dd(seg.values)*100:.0f}%"
def main():
print("=" * 100)
print(" EQ-TREND01 — Trend DIFENSIVO time-series su SPY (analogo di TP01)")
print("=" * 100)
spy_px = _series("SPY"); spy = spy_bh()
common = spy_px.index[spy_px.index >= spy_px.index[252]] # warmup 1y
print(f" periodo {common[0].date()}..{common[-1].date()} ({len(common)}g) OOS = {EQ_HOLDOUT.date()}+\n")
print(" --- BASELINE ---")
_row("SPY buy&hold", spy, common)
print("\n --- TREND long-flat (cash in risk-off) ---")
_row("SMA-200 (Faber)", backtest(spy_px, sma_timing(spy_px)), common, bench=spy)
_row("TSMOM lf cap1.0", backtest(spy_px, tsmom_exposure(spy_px)), common, bench=spy)
_row("TSMOM lf vt15 cap1.0", backtest(spy_px, tsmom_exposure(spy_px, target_vol=0.15, lev_cap=1.0)), common, bench=spy)
_row("TSMOM lf vt15 cap1.5", backtest(spy_px, tsmom_exposure(spy_px, target_vol=0.15, lev_cap=1.5)), common, bench=spy)
print("\n --- TREND long-BONDS (TLT in risk-off, solo dove TLT esiste: 2016+) ---")
tlt = _series("TLT")
cb = spy_px.index[(spy_px.index >= tlt.index[0])]
_row("SPY b&h (2016+)", spy.reindex(cb), cb)
_row("TSMOM lf+TLT (2016+)", backtest(spy_px, tsmom_exposure(spy_px), risk_off=tlt), cb, bench=spy)
# MARGINALE vs SPY + DD nei bear
base = backtest(spy_px, tsmom_exposure(spy_px))
print("\n --- MARGINALE vs SPY (TSMOM lf cap1.0) ---")
J = pd.concat({"spy": spy, "c": base}, axis=1, join="inner").dropna(); JH = J[J.index >= EQ_HOLDOUT]
print(f" corr full {J['spy'].corr(J['c']):+.3f} | OOS {JH['spy'].corr(JH['c']):+.3f}")
for wt in (0.5, 1.0):
bf = _sh((1-wt)*J['spy']+wt*J['c']) - _sh(J['spy']); bh = _sh((1-wt)*JH['spy']+wt*JH['c']) - _sh(JH['spy'])
lbl = "100% TREND" if wt == 1.0 else f"{int((1-wt)*100)}/{int(wt*100)} SPY/TREND"
print(f" {lbl:16}: uplift Sharpe FULL {bf:+.3f} OOS {bh:+.3f}")
print(" DD nei bear storici (TSMOM vs SPY):")
for lo, hi, lbl in [("2000-03-01","2002-12-31","dot-com"), ("2007-10-01","2009-06-30","GFC"),
("2020-02-01","2020-04-30","COVID"), ("2022-01-01","2022-12-31","2022")]:
print(f" {lbl:8} TSMOM {_bear_dd(base,common,lo,hi,'')} | SPY {_bear_dd(spy,common,lo,hi,'')}")
# PLATEAU
print("\n --- PLATEAU (Sharpe FULL/pre15/OOS, maxDD, CAGR) long-flat cap1.0 ---")
print(f" {'horizons':22} {'FULL':>6} {'pre15':>6} {'OOS':>6} {'DD%':>5} {'CAGR%':>6}")
for hz in [(63,126,252),(21,63,126,252),(126,252),(50,200),(200,)]:
ex = sma_timing(spy_px, 200) if hz == (200,) else tsmom_exposure(spy_px, horizons=hz)
r = backtest(spy_px, ex); h=r[r.index>=EQ_HOLDOUT]; ii=r[r.index<EQ_HOLDOUT]
tag = "SMA-200" if hz==(200,) else "x".join(map(str,hz))
print(f" {tag:22} {_sh(r):>6.2f} {_sh(ii):>6.2f} {_sh(h):>6.2f} {_dd(r.values)*100:>5.0f} {_cagr(r.values,r.index)*100:>6.1f}")
print("\n --- SWEEP FEE (TSMOM lf cap1.0) ---")
for fee in (0.0, 0.0002, 0.0005, 0.001):
r = backtest(spy_px, tsmom_exposure(spy_px), fee_side=fee)
print(f" fee {fee*100:.2f}%/lato: Sh {_sh(r):.2f} maxDD {_dd(r.values)*100:.0f}%")
if __name__ == "__main__":
main()
+93
View File
@@ -0,0 +1,93 @@
"""COMBO DEPLOYABLE — TP01 (Deribit) + GTAA (IB): le DUE gambe realmente eseguibili a basso capitale.
Il combo crypto-pieno (TP01+XS01+VRP01)+GTAA diversificava (Sharpe 1.6->1.8), ma XS01/VRP01 sono
STAT-MODE (non eseguibili a $600). Qui il combo ONESTO/deployable: solo le gambe eseguibili
* TP01: TSMOM difensivo BTC/ETH, long-flat, gia' ARMATO live su Deribit;
* GTAA: trend difensivo multi-asset su ETF, eseguibile su IB (frazioni, switch mensile).
Domanda: due trend difensivi su mercati diversi, scorrelati, danno un blend migliore di ciascuno?
ALLINEAMENTO: TP01 e' calendario-giornaliero (crypto 7gg), GTAA giorni di borsa -> compoundo TP01 sul
grid dei giorni di borsa (cattura i weekend). Finestra = era TP01 (BTC/ETH dal 2019).
Confronto anche col combo crypto-pieno per quantificare il COSTO della deployability.
"""
import sys
from pathlib import Path
import numpy as np, pandas as pd
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT)); sys.path.insert(0, str(ROOT / "scripts" / "research"))
from eq_sector_momentum import _sh, _cagr, _dd, EQ_HOLDOUT
from eq_gtaa_trend import gtaa
from eq_crypto_combo import compound_to_grid, _ann_vol
from src.portfolio.sleeves import _tp01_returns, active_sleeves
from src.portfolio.portfolio import StrategyPortfolio
ANN = np.sqrt(252.0)
def _stat(nm, r):
print(f" {nm:26} Sh {_sh(r):>5.2f} CAGR {_cagr(r.values,r.index)*100:>5.1f}% "
f"volAnn {_ann_vol(r)*100:>4.1f}% maxDD {_dd(r.values)*100:>4.0f}%")
def main():
print("=" * 96)
print(" COMBO DEPLOYABLE — TP01 (Deribit) + GTAA (IB)")
print("=" * 96)
tp01 = _tp01_returns()
if tp01.index.tz is None:
tp01.index = tp01.index.tz_localize("UTC")
eq = gtaa(target_vol=0.12).dropna()
grid = eq.index[eq.index >= tp01.index[0]]
tp = compound_to_grid(tp01, grid)
J = pd.concat({"tp01": tp, "gtaa": eq.reindex(tp.index)}, axis=1).dropna()
print(f" finestra comune {J.index[0].date()}..{J.index[-1].date()} ({len(J)} giorni di borsa, ~{len(J)//252}y)\n")
t, g = J["tp01"], J["gtaa"]
print(" --- STANDALONE (finestra comune) ---")
_stat("TP01 (crypto, Deribit)", t)
_stat("GTAA vt12 (equity, IB)", g)
c = t.corr(g)
print(f"\n --- CORRELAZIONE TP01 <-> GTAA = {c:+.3f} ---")
print("\n --- BLEND (capitale) ---")
print(f" {'mix':20} {'Sharpe':>7} {'CAGR%':>6} {'volAnn%':>7} {'maxDD%':>6}")
best = (-9, None)
for wt in (1.0, 0.75, 0.6, 0.5, 0.4, 0.25, 0.0):
b = wt * t + (1 - wt) * g
if _sh(b) > best[0]:
best = (_sh(b), wt)
print(f" TP01 {int(wt*100):>3}/{int((1-wt)*100):<3} GTAA {_sh(b):>7.2f} {_cagr(b.values,b.index)*100:>6.1f} {_ann_vol(b)*100:>7.1f} {_dd(b.values)*100:>6.0f}")
vt, vg = _ann_vol(t), _ann_vol(g)
wt_rp = (1/vt) / (1/vt + 1/vg)
b_rp = wt_rp * t + (1 - wt_rp) * g
print(f"\n --- RISK-PARITY (inv-vol: TP01 {wt_rp*100:.0f}% / GTAA {(1-wt_rp)*100:.0f}%) ---")
_stat("risk-parity blend", b_rp)
# OOS (post-2023, per dare un taglio fuori dai primi anni TP01)
cut = pd.Timestamp("2023-01-01", tz="UTC")
print(f"\n --- robustezza: blend 50/50 per taglio ---")
b50 = 0.5 * t + 0.5 * g
for lbl, sub in (("full", b50), (">=2023", b50[b50.index >= cut])):
print(f" {lbl:10} Sh {_sh(sub):.2f} maxDD {_dd(sub.values)*100:.0f}%")
print(" Sharpe 50/50 per anno:", {y: round(_sh(b50[b50.index.year == y]), 2) for y in sorted(set(b50.index.year))})
# VERDETTO + costo della deployability
best_solo = max(_sh(t), _sh(g))
print(f"\n --- VERDETTO ---")
print(f" miglior standalone {best_solo:.2f} | blend 50/50 {_sh(b50):.2f} | risk-parity {_sh(b_rp):.2f} | "
f"miglior cap-mix {best[0]:.2f} (TP01 {int(best[1]*100)}%)")
print(f" -> {'DIVERSIFICA (blend > solo)' if max(_sh(b50),_sh(b_rp)) > best_solo + 0.03 else 'nessun guadagno netto'}")
# costo vs combo crypto-pieno
full = StrategyPortfolio(active_sleeves()).combined_daily()
if full.index.tz is None: full.index = full.index.tz_localize("UTC")
fc = compound_to_grid(full, J.index)
Jf = pd.concat({"f": fc, "g": g.reindex(fc.index)}, axis=1).dropna()
print(f" costo deployability: blend 50/50 con crypto-PIENO Sh {_sh(0.5*Jf['f']+0.5*Jf['g']):.2f} "
f"vs deployable {_sh(b50):.2f} (la differenza = cio' che lasciano XS01/VRP01 STAT-MODE)")
print(f" (cross-venue Deribit+IB; entrambe switch mensile/basso turnover; frazionabili a $0.5-2k)")
if __name__ == "__main__":
main()
+65
View File
@@ -0,0 +1,65 @@
"""EQLIB — harness di ricerca EQUITY/ETF (branch research/equities-ib).
Legge la CACHE su disco (data/raw/eq_*.parquet, ADJUSTED_LAST, scritta una volta da
fetch_ib_equities.py) MAI da IB. `lru_cache` -> ogni parquet si legge una sola volta per processo.
"Memorizza i dati per non rileggerli ogni volta": la persistenza e' il parquet su disco + questa cache.
Espone: universi (SECTORS/BROAD), load_eq(sym), panel(universe) allineato, e riusa lo scorer
indurito di altlib (_sh, _dd_ret, _to_daily, marginal_vs_tp01) per giudicare i candidati con la
stessa disciplina del lato crypto.
"""
from functools import lru_cache
from pathlib import Path
import numpy as np, pandas as pd
ROOT = Path(__file__).resolve().parents[2]
RAW = ROOT / "data" / "raw"
# 11 SPDR settoriali. I 9 "classici" (sotto SECTORS_CLASSIC) partono 1998; XLRE 2015, XLC 2018.
SECTORS = ["XLK", "XLF", "XLE", "XLV", "XLI", "XLP", "XLY", "XLU", "XLB", "XLRE", "XLC"]
SECTORS_CLASSIC = ["XLK", "XLF", "XLE", "XLV", "XLI", "XLP", "XLY", "XLU", "XLB"] # storia lunga (1998+)
BROAD = ["SPY", "QQQ", "IWM", "TLT", "GLD", "HYG"]
@lru_cache(maxsize=64)
def load_eq(sym: str) -> pd.DataFrame:
"""OHLCV aggiustato (dividendi+split) per `sym`, indicizzato datetime UTC. Cache su disco -> RAM."""
p = RAW / f"eq_{sym.lower()}_1d.parquet"
if not p.exists():
raise FileNotFoundError(f"{p} assente — gira: uv run --with ib_async python scripts/research/fetch_ib_equities.py")
d = pd.read_parquet(p).copy()
d.index = pd.to_datetime(d["timestamp"], unit="ms", utc=True)
return d[["open", "high", "low", "close", "volume"]]
@lru_cache(maxsize=16)
def _close_panel(universe: tuple) -> pd.DataFrame:
cols = {s: load_eq(s)["close"].astype(float) for s in universe}
return pd.concat(cols, axis=1).sort_index()
def panel(universe=tuple(SECTORS), how: str = "inner") -> pd.DataFrame:
"""Prezzi close aggiustati [date x asset]. how='inner' = date comuni a TUTTI (start = ETF piu' giovane);
'outer' = unione (NaN dove un ETF non esiste ancora)."""
P = _close_panel(tuple(universe))
return P.dropna(how="any") if how == "inner" else P
def describe(universe=None):
universe = universe or (SECTORS + BROAD)
print(f" {'sym':6} {'barre':>6} {'da':>11} {'a':>11} {'anni':>5}")
for s in universe:
try:
d = load_eq(s)
print(f" {s:6} {len(d):>6} {str(d.index[0].date()):>11} {str(d.index[-1].date()):>11} "
f"{(d.index[-1]-d.index[0]).days/365.25:>5.1f}")
except FileNotFoundError:
print(f" {s:6} (assente)")
Pc = panel(SECTORS_CLASSIC); Pa = panel(SECTORS)
print(f"\n panel 9 settori CLASSICI: {Pc.shape[1]}x{len(Pc)} start comune {Pc.index[0].date()}")
print(f" panel 11 settori : {Pa.shape[1]}x{len(Pa)} start comune {Pa.index[0].date()}")
if __name__ == "__main__":
print("=" * 70); print(" EQLIB — cache equity su disco (nessun IB)"); print("=" * 70)
describe()
+86
View File
@@ -0,0 +1,86 @@
"""DEEP-DIVE: crypto[sessione USA] -> indice EUROPEO[overnight successivo]. Reale o artefatto?
Lo sweep ha trovato un forte segnale: crypto[T-8h->T] (T~00:00 UTC, fine sessione USA) predice il
future europeo (ESTX50/DAX)[T->T+6h] con t_crypto incrementale ~8, Sharpe 2.5, 3/3 anni. Verifiche:
(1) ROBUSTEZZA su T (ora entrata): un effetto vero non e' a coltello su una sola ora.
(2) vs SEMPRE-LONG: l'overnight ha un drift? il crypto AGGIUNGE oltre il long incondizionato?
(3) GAP 1-2h tra fine-segnale e inizio-cattura: se sopravvive, niente contaminazione di bordo.
(4) vs FUTURE-OWN: il crypto batte il momentum proprio del future (gia' nel t incrementale).
Dati: cache (fut europei + crypto 1h). sqrt(252), net 2bps.
"""
import sys
from pathlib import Path
import numpy as np, pandas as pd
ROOT = Path(__file__).resolve().parents[2]; RAW = ROOT / "data" / "raw"
sys.path.insert(0, str(ROOT)); sys.path.insert(0, str(ROOT / "scripts" / "research"))
from crypto_lead_harness import crypto_hourly, at
COST = 0.0002
def fut_hourly(sym):
d = pd.read_parquet(RAW / f"fut_{sym.lower()}_1h.parquet")
return pd.Series(d["close"].astype(float).values, index=pd.to_datetime(d["timestamp"], unit="ms", utc=True)).sort_index()
def _sh(r):
r = np.asarray(r, float); r = r[np.isfinite(r)]
return float(np.mean(r) / np.std(r) * np.sqrt(252)) if len(r) > 5 and np.std(r) > 0 else 0.0
def series(fut, bc, T, S=8, H=6, gap=0):
days = pd.date_range(fut.index[0].normalize(), fut.index[-1].normalize(), freq="D", tz="UTC")
rows = []
for D in days:
te = D + pd.Timedelta(hours=T)
f0, f1 = at(fut, te - pd.Timedelta(hours=S)), at(fut, te)
fcs, fce = at(fut, te + pd.Timedelta(hours=gap)), at(fut, te + pd.Timedelta(hours=gap + H))
c0, c1 = at(bc, te - pd.Timedelta(hours=S)), at(bc, te)
if not all(np.isfinite(v) and v > 0 for v in (f0, f1, fcs, fce, c0, c1)):
continue
rows.append((D, c1/c0 - 1, f1/f0 - 1, fce/fcs - 1))
Dd = pd.DataFrame(rows, columns=["d", "csig", "fctrl", "cap"]).set_index("d")
return Dd[Dd["cap"].abs() > 1e-9]
def stats(Dd):
x, ctrl, y = Dd["csig"].values, Dd["fctrl"].values, Dd["cap"].values
def z(a):
s = a.std(); return (a - a.mean()) / s if s > 0 else a * 0
X = np.column_stack([np.ones(len(y)), z(x), z(ctrl)])
beta, *_ = np.linalg.lstsq(X, z(y), rcond=None)
resid = z(y) - X @ beta; dof = max(len(y) - 3, 1)
se = np.sqrt(np.sum(resid**2)/dof * np.diag(np.linalg.inv(X.T@X)))
t_c = float(beta[1]/se[1]) if se[1] > 0 else 0.0
crypto = np.sign(x)*y - COST
futown = np.sign(ctrl)*y - COST
always = y - COST
yrs = Dd.index.year.values
py = {int(v): round(_sh(crypto[yrs==v]),2) for v in sorted(set(yrs)) if (yrs==v).sum()>=30}
return dict(n=len(Dd), t_crypto=round(t_c,2), sh_crypto=round(_sh(crypto),2),
sh_futown=round(_sh(futown),2), sh_always=round(_sh(always),2),
ann_crypto=round(float(np.nanmean(crypto)*252*100),1), per_year=py)
def main():
print("="*96); print(" DEEP-DIVE crypto -> indice EUROPEO overnight (reale o artefatto?)"); print("="*96)
bcs = {l: crypto_hourly(l) for l in ("BTC","ETH")}
for sym in ("ESTX50","DAX"):
fut = fut_hourly(sym)
print(f"\n ===== {sym} =====")
print(f" (1) ROBUSTEZZA su T (lead=BTC, S=8h H=6h gap=0): t_crypto | Sh crypto vs futown vs always-long")
for T in (20,21,22,23,0,1,2,3,4):
s = stats(series(fut, bcs["BTC"], T))
print(f" T={T:>2}h: n={s['n']} t {s['t_crypto']:+.1f} | crypto {s['sh_crypto']:+.2f} futown {s['sh_futown']:+.2f} always {s['sh_always']:+.2f} ann {s['ann_crypto']:+.1f}% {s['per_year']}")
print(f" (3) GAP test (T=0h, cattura inizia +1h e +2h dopo il segnale):")
for g in (0,1,2):
s = stats(series(fut, bcs["BTC"], 0, gap=g))
print(f" gap={g}h: t {s['t_crypto']:+.1f} | crypto {s['sh_crypto']:+.2f} always {s['sh_always']:+.2f}")
print(f" (lead ETH, T=0h): ", end="")
s = stats(series(fut, bcs["ETH"], 0)); print(f"t {s['t_crypto']:+.1f} crypto {s['sh_crypto']:+.2f} always {s['sh_always']:+.2f} {s['per_year']}")
print("\n LETTURA: se 'crypto' >> 'always' E >> 'futown' E regge su T e col gap -> lead REALE (crypto")
print(" anticipa il catch-up europeo). Se crypto ~ always -> e' solo overnight-drift europeo.")
if __name__ == "__main__":
main()
+137
View File
@@ -0,0 +1,137 @@
"""FETCH + CERTIFY funding rate Hyperliquid (API pubblica, tokenless) — per ricerca CARRY cross-sectional.
CONTESTO (2026-06-22, onda "nuova ricerca mirata"). Le due grandi ondate (sweep 104-ipotesi +
ortho relative-value) hanno esaurito gli angoli DIREZIONALI e RELATIVE-VALUE sul *prezzo* BTC/ETH.
L'unico meccanismo con una fonte di ritorno DIVERSA non ancora testato su dati certi e' il CARRY da
funding (incassare il cashflow perp, delta-neutral). Scan di fattibilita':
* funding price-clock sul feed Deribit certificato -> gia' testato (agent_03 intraday) = FAIL.
* funding carry su Deribit (dove eseguiamo) -> ccxt fetch_funding_rate_history = 0 righe (bloccato).
* funding carry su Hyperliquid -> API pubblica /info {"type":"fundingHistory"} = DISPONIBILE,
cadenza ORARIA, tokenless, serie native dal 2023-05-12. HL e' gia' l'universo certificato di XS01.
DISCIPLINA (lezione v2.0.0): nessuna fiducia nel dato finche' non e' certificato. Qui certifichiamo:
(1) cadenza ~1h coerente, (2) gap interni, (3) copertura (giorni nativi reali per coin),
(4) plausibilita' magnitudine (|funding orario| tipico < ~0.06%/h = cap HL; outlier flaggati),
(5) il funding e' un CASHFLOW, non un prezzo -> niente cross-venue OHLC; il sanity check e'
che il funding medio sia ~positivo e piccolo (premio long-pays-short tipico dei perp crypto).
Universo = i 19 major di XS01 (quelli che la strategia live userebbe). Output:
data/raw/hlfund_<sym>_1h.parquet (namespace dedicato 'hlfund', NON tocca hl_<sym>_1d di XS01).
"""
import sys, time, datetime as dt
from pathlib import Path
import numpy as np, pandas as pd, requests
ROOT = Path(__file__).resolve().parents[2]
RAW = ROOT / "data" / "raw"
RAW.mkdir(parents=True, exist_ok=True)
# 19 major di XS01 (CLAUDE.md)
UNIVERSE = ["BTC","ETH","SOL","BNB","XRP","DOGE","AVAX","LINK","LTC","ADA",
"ARB","OP","SUI","APT","INJ","TIA","SEI","NEAR","AAVE"]
HL_INFO = "https://api.hyperliquid.xyz/info"
START = int(dt.datetime(2023, 1, 1, tzinfo=dt.timezone.utc).timestamp() * 1000)
HOUR_MS = 3600 * 1000
def _post(payload, max_retry=6):
"""POST con backoff esponenziale su 429/5xx (l'API pubblica HL throttla)."""
delay = 1.0
for attempt in range(max_retry):
r = requests.post(HL_INFO, json=payload, timeout=30)
if r.status_code == 429 or r.status_code >= 500:
time.sleep(delay)
delay = min(delay * 2, 20) # 1,2,4,8,16,20
continue
r.raise_for_status()
return r.json()
r.raise_for_status()
return r.json()
def fetch_funding(coin: str) -> pd.DataFrame:
"""Pagina fundingHistory (max 500/req, orario) avanzando startTime fino a oggi."""
rows, start = [], START
seen = set()
while True:
d = _post({"type": "fundingHistory", "coin": coin, "startTime": start})
if not d:
break
new = [x for x in d if x["time"] not in seen]
for x in new:
seen.add(x["time"])
rows.append((x["time"], float(x["fundingRate"]), float(x.get("premium", "nan"))))
last = d[-1]["time"]
if len(d) < 500: # ultima pagina
break
nxt = last + 1
if nxt <= start: # niente progresso -> stop
break
start = nxt
time.sleep(0.35) # gentile con l'API pubblica
if not rows:
return pd.DataFrame(columns=["ts", "funding", "premium"]).set_index("ts")
df = pd.DataFrame(rows, columns=["ts", "funding", "premium"]).drop_duplicates("ts").sort_values("ts")
df["ts"] = pd.to_datetime(df["ts"], unit="ms", utc=True)
return df.set_index("ts")
def certify(coin: str, df: pd.DataFrame) -> dict:
if df.empty:
return {"coin": coin, "n": 0, "status": "VUOTO"}
idx = df.index
span_days = (idx[-1] - idx[0]).total_seconds() / 86400
# cadenza: differenze in ore
deltas_h = np.diff(idx.view("int64")) / 1e9 / 3600
median_dt = float(np.median(deltas_h)) if len(deltas_h) else float("nan")
gaps = int((deltas_h > 1.5).sum()) # buchi > 1.5h
expected = int(round(span_days * 24)) + 1
coverage = len(df) / expected if expected else float("nan")
f = df["funding"].values
# statistiche funding (orario)
ann = float(np.nanmean(f)) * 24 * 365 # funding annualizzato (carry teorico per chi paga)
cap_hits = int((np.abs(f) > 0.0006).sum()) # cap HL ~0.06%/h (4%/8h clamp); fuori = sospetto
status = "OK"
if coverage < 0.97 or gaps > 50:
status = "GAP"
if span_days < 365:
status = "corto<365g"
return {"coin": coin, "n": len(df), "primo": idx[0].date(), "ultimo": idx[-1].date(),
"giorni": round(span_days), "cad_h": round(median_dt, 3), "gap>1.5h": gaps,
"cover%": round(coverage * 100, 1), "fund_med_bps": round(float(np.nanmedian(f)) * 1e4, 4),
"fund_ann%": round(ann * 100, 1), "cap_hit": cap_hits, "status": status}
def main():
print("=" * 100)
print(" FETCH + CERTIFY funding Hyperliquid (orario, tokenless) — 19 major XS01 -> data/raw/hlfund_*")
print("=" * 100)
rep = []
for sym in UNIVERSE:
try:
df = fetch_funding(sym)
except Exception as e:
print(f" {sym:5} ERR {repr(e)[:80]}")
rep.append({"coin": sym, "n": 0, "status": "ERR"})
continue
c = certify(sym, df)
rep.append(c)
if c.get("n", 0) > 0:
out = RAW / f"hlfund_{sym.lower()}_1h.parquet"
df.to_parquet(out)
print(f" {sym:5} n={c.get('n',0):>6} {str(c.get('primo','')):>10}->{str(c.get('ultimo','')):>10} "
f"cad={c.get('cad_h','?')}h gap={c.get('gap>1.5h','?')} cov={c.get('cover%','?')}% "
f"med={c.get('fund_med_bps','?')}bps ann={c.get('fund_ann%','?')}% cap_hit={c.get('cap_hit','?')} "
f"[{c['status']}]")
ok = [r["coin"] for r in rep if r.get("status") in ("OK",)]
short = [r["coin"] for r in rep if r.get("status") == "corto<365g"]
print("-" * 100)
print(f" CERTIFICATI OK ({len(ok)}): {ok}")
if short:
print(f" CORTI <365g ({len(short)}): {short}")
print(f" Scritti in data/raw/hlfund_<sym>_1h.parquet (funding ORARIO, serie nativa).")
if __name__ == "__main__":
main()
+134
View File
@@ -0,0 +1,134 @@
"""FETCH + CERTIFY universo azioni/ETF da IB (ADJUSTED_LAST) -> data/raw/eq_<sym>_1d.parquet.
Apre il fronte EQUITY (branch research/equities-ib). Disciplina v2.0.0: PRIMA il dato certificato,
POI la strategia. IB storia daily aggiustata per dividendi+split (ADJUSTED_LAST), profonda
(SPY dal 1996), sul conto paper. Namespace dedicato 'eq_' (NON tocca i parquet crypto).
UNIVERSO (prima ricerca = momentum cross-sectional settoriale, l'edge robusto plausibile in equity):
* 11 SPDR settoriali (XLK..XLC); * broad/macro SPY QQQ IWM TLT GLD HYG.
NB: i 9 settori "classici" partono 1998; XLRE 2015, XLC 2018 -> lo start COMUNE a 11 e' 2018.
Per backtest lunghi usare i 9 classici (1998+) o accettare lo start 2018 per gli 11.
CERTIFICAZIONE (gemello equity di certify_feed.py):
(1) integrità: barre, range, date monotone, duplicati, flat bars (close invariato);
(2) gap: run di giorni-lavorativi mancanti > 5 (festivi normali, buchi lunghi = sospetti);
(3) sanità ritorni: max |daily ret| (un >50% non-evento = errore di adjustment);
(4) sanità adjustment: primo close aggiustato << ultimo (i dividendi abbassano lo storico).
PREREQUISITO: gateway IB paper su 127.0.0.1:4002 (docker compose up -d ib-gateway).
uv run --with ib_async python scripts/research/fetch_ib_equities.py
"""
import sys, time
from pathlib import Path
import numpy as np, pandas as pd
ROOT = Path(__file__).resolve().parents[2]
RAW = ROOT / "data" / "raw"
RAW.mkdir(parents=True, exist_ok=True)
SECTORS = ["XLK", "XLF", "XLE", "XLV", "XLI", "XLP", "XLY", "XLU", "XLB", "XLRE", "XLC"]
BROAD = ["SPY", "QQQ", "IWM", "TLT", "GLD", "HYG"]
# espansione "diversi mercati" (intl / bond / credito / commodity / settori extra) per il lead-lag crypto
BROAD2 = ["DIA", "EFA", "EEM", "FXI", "EWJ", "AGG", "LQD", "IEF", "USO", "SLV", "DBC", "VNQ"]
UNIVERSE = SECTORS + BROAD + BROAD2
def certify(sym: str, df: pd.DataFrame) -> dict:
if df.empty:
return {"sym": sym, "n": 0, "status": "VUOTO"}
idx = df.index
dup = int(idx.duplicated().sum())
mono = bool(idx.is_monotonic_increasing)
c = df["close"].values.astype(float)
ret = np.diff(c) / c[:-1]
flat = int((ret == 0).sum())
maxret = float(np.max(np.abs(ret))) if len(ret) else 0.0
# gap: giorni lavorativi attesi vs presenti, run lunghi mancanti
bdays = pd.bdate_range(idx[0], idx[-1])
missing = len(bdays) - len(idx.intersection(bdays))
gaps = bdays.difference(idx)
longgap = 0
if len(gaps):
g = pd.Series(1, index=gaps).resample("1D").sum().fillna(0)
# conta run consecutivi di bday mancanti
s = (gaps.to_series().diff().dt.days.fillna(1) > 3).cumsum()
longgap = int((gaps.to_series().groupby(s).size() > 5).sum())
span_y = (idx[-1] - idx[0]).days / 365.25
adj_ratio = round(float(c[0] / c[-1]), 3) # primo/ultimo: <1 atteso (storico abbassato dai div)
status = "OK"
if dup or not mono:
status = "INTEGRITA'"
elif maxret > 0.5:
status = "SPIKE?"
elif longgap > 0:
status = "GAP-LUNGO"
elif span_y < 1:
status = "corto<1y"
return {"sym": sym, "n": len(df), "primo": idx[0].date(), "ultimo": idx[-1].date(),
"anni": round(span_y, 1), "dup": dup, "mono": mono, "flat": flat,
"maxret%": round(maxret * 100, 1), "miss_bd": missing, "gap_lunghi": longgap,
"adj_first/last": adj_ratio, "status": status}
def main():
try:
from ib_async import IB, Stock
except Exception:
print("ib_async assente. Esegui con: uv run --with ib_async python scripts/research/fetch_ib_equities.py")
sys.exit(2)
ib = IB()
try:
ib.connect("127.0.0.1", 4002, clientId=90, timeout=15)
except Exception as e:
print(f"[CONNESSIONE FALLITA] 127.0.0.1:4002 -> {repr(e)[:120]}\n Avvia: docker compose up -d ib-gateway")
sys.exit(1)
print("=" * 104)
print(f" FETCH + CERTIFY azioni/ETF (ADJUSTED_LAST) -> data/raw/eq_* | acct {ib.managedAccounts()}")
print("=" * 104)
rep, ok = [], []
force = "--force" in sys.argv[1:]
universe = UNIVERSE
if "--only" in sys.argv[1:]: # refresh mirato (es. solo i 6 ETF GTAA per il cron)
universe = sys.argv[sys.argv.index("--only") + 1].upper().split(",")
force = True # --only implica refresh dei simboli indicati
for sym in universe:
out_path = RAW / f"eq_{sym.lower()}_1d.parquet"
if out_path.exists() and not force:
print(f" {sym:5} GIA' SU DISCO -> skip (usa --force per riscaricare)")
ok.append(sym)
continue
con = Stock(sym, "SMART", "USD")
try:
bars = ib.reqHistoricalData(con, endDateTime="", durationStr="30 Y", barSizeSetting="1 day",
whatToShow="ADJUSTED_LAST", useRTH=True, formatDate=1, timeout=60)
except Exception as e:
print(f" {sym:5} ERR {repr(e)[:70]}"); rep.append({"sym": sym, "status": "ERR"}); time.sleep(1.2); continue
if not bars:
print(f" {sym:5} 0 barre (subscription?)"); rep.append({"sym": sym, "n": 0, "status": "VUOTO"}); time.sleep(1.2); continue
df = pd.DataFrame([(pd.Timestamp(str(b.date)), b.open, b.high, b.low, b.close, b.volume) for b in bars],
columns=["ts", "open", "high", "low", "close", "volume"]).set_index("ts").sort_index()
c = certify(sym, df)
rep.append(c)
if c.get("n", 0) > 0:
out = df.copy()
# ms epoch (come i parquet crypto), robusto alla risoluzione datetime64 (s/us/ns)
out["timestamp"] = out.index.astype("datetime64[ms]").astype("int64")
out.reset_index(drop=True).to_parquet(RAW / f"eq_{sym.lower()}_1d.parquet")
if c["status"] == "OK":
ok.append(sym)
print(f" {sym:5} n={c.get('n',0):>5} {str(c.get('primo','')):>10}->{str(c.get('ultimo',''))} "
f"{c.get('anni','?')}y flat={c.get('flat','?')} maxret={c.get('maxret%','?')}% "
f"miss_bd={c.get('miss_bd','?')} gapL={c.get('gap_lunghi','?')} adj={c.get('adj_first/last','?')} [{c['status']}]")
time.sleep(1.2) # pacing IB
print("-" * 104)
print(f" CERTIFICATI OK ({len(ok)}/{len(UNIVERSE)}): {ok}")
sec_ok = [s for s in SECTORS if s in ok]
print(f" settori OK: {len(sec_ok)}/11 {sec_ok}")
print(f" -> scritti in data/raw/eq_<sym>_1d.parquet (ADJUSTED_LAST, namespace dedicato).")
ib.disconnect()
if __name__ == "__main__":
main()
+64
View File
@@ -0,0 +1,64 @@
"""FETCH futures indice ORARI (ES/NQ/RTY) da IB -> data/raw/fut_<sym>_1h.parquet (UTC).
Per il test onesto dell'idea "monitor Deribit / trade IB": serve il path INTRADAY del future indice
(che si trada di notte) per misurare finestre overnight NON sovrapposte col segnale crypto.
ContFuture orario, in chunk da 1 anno (IB limita le durate intraday). Convertito in UTC.
Resumable (salta i parquet gia' scritti). Per RETURNS/lead-lag il back-adjust del ContFuture e' ok
(i ritorni infra-contratto sono preservati; i gap di roll ~4/anno sono trascurabili).
uv run --with ib_async python scripts/research/fetch_ib_futures.py
"""
import sys, time
from pathlib import Path
import numpy as np, pandas as pd
ROOT = Path(__file__).resolve().parents[2]
RAW = ROOT / "data" / "raw"
# sym -> exchange (indici US + esteri + commodity + bond per la ricerca cross-mercato oltre SP500)
SYMS = {"ES": "CME", "NQ": "CME", "RTY": "CME",
"GC": "COMEX", "CL": "NYMEX", "HG": "COMEX", "ZN": "CBOT",
"ESTX50": "EUREX", "DAX": "EUREX", "NKD": "CME"}
N_CHUNKS = 5 # (non usato: ContFuture non accetta endDateTime -> chiamata singola)
def main():
from ib_async import IB, ContFuture
ib = IB()
try:
ib.connect("127.0.0.1", 4002, clientId=144, timeout=15)
except Exception as e:
print(f"[CONNESSIONE FALLITA] {repr(e)[:100]}"); sys.exit(1)
print(f" acct {ib.managedAccounts()} | fetch futures orari -> data/raw/fut_*")
for sym, exc in SYMS.items():
out = RAW / f"fut_{sym.lower()}_1h.parquet"
if out.exists():
print(f" {sym}: gia' su disco -> skip"); continue
cf = ContFuture(sym, exchange=exc)
try:
ib.qualifyContracts(cf)
except Exception as e:
print(f" {sym}: qualify ERR {repr(e)[:60]}"); continue
# ContFuture NON accetta endDateTime (Error 10339) -> chiamata singola, durata massima (~3y orari)
try:
b = ib.reqHistoricalData(cf, endDateTime="", durationStr="4 Y", barSizeSetting="1 hour",
whatToShow="TRADES", useRTH=False, formatDate=1, timeout=150)
except Exception as e:
print(f" {sym}: ERR {repr(e)[:60]}"); continue
if not b:
print(f" {sym}: VUOTO"); continue
D = pd.DataFrame([(pd.Timestamp(x.date), x.open, x.high, x.low, x.close, x.volume) for x in b],
columns=["ts", "open", "high", "low", "close", "volume"]).drop_duplicates("ts").sort_values("ts").reset_index(drop=True)
# -> UTC ms (robusto alla risoluzione us/ns: naive-UTC -> datetime64[ms] -> int64)
ts = pd.to_datetime(D["ts"], utc=True).dt.tz_convert("UTC").dt.tz_localize(None)
D["timestamp"] = ts.values.astype("datetime64[ms]").astype("int64")
D = D.drop(columns=["ts"])
D.to_parquet(out)
u = pd.to_datetime(D["timestamp"], unit="ms", utc=True)
print(f" {sym}: SCRITTO {len(D)} barre {u.iloc[0]} .. {u.iloc[-1]} -> {out.name}")
time.sleep(1.5)
ib.disconnect()
print(" done.")
if __name__ == "__main__":
main()
+170
View File
@@ -0,0 +1,170 @@
"""FC01 — Funding-CARRY cross-sectional su Hyperliquid (backtest onesto, STAT-MODE).
IPOTESI. Il funding dei perp e' un CASHFLOW (long pagano short quando f>0). Un book
dollar-neutral che VENDE i perp ad alto funding e COMPRA quelli a basso funding INCASSA
il premio di funding (carry). E' una fonte di ritorno DIVERSA dal trend (TP01) e — forse —
dal momentum cross-sectional (XS01). Domanda chiave: e' un edge reale o solo XS01 travestito?
(gli asset ad alto funding sono spesso quelli pompati = forti = quelli che XS01 COMPRA; qui li
SHORTIAMO -> potenziale ANTI-correlazione con XS01, oppure il carry domina).
DATI (certificati). funding orario reale HL (scripts/research/fetch_hl_funding.py, dal 2023-05) +
prezzi HL 1d (data/raw/hl_*_1d.parquet, gli stessi di XS01). Universo = i 19 major di XS01.
COSTRUZIONE (causale, come XS01). Ogni H giorni: segnale = media causale del funding giornaliero
realizzato sugli ultimi L giorni (solo dati <= i-1). Rank cross-section; SHORT i k ad alto funding,
LONG i k a basso (dollar-neutral). Ritorno del perp per un LONG = price_ret - funding (chi e' long
paga il funding); quindi r_book[i] = sum_a w[i-1,a] * (price_ret[i,a] - funding_realizzato[i,a]),
meno fee sul turnover (0.05%/lato, come XS01), poi vol-target 20%.
GIUDIZIO (metodologia indurita). Standalone (FULL/in-sample/hold-out, DD, anni) + correlazione a
TP01/XS01/VRP01 + marginal_vs_tp01 (gate: edge in-sample>=0.5, persistenza multi-cut, hedge-vs-alpha,
noise-null) + UPLIFT vs XS01 (la domanda di overlap) + plateau su L/k/direzione.
CAVEAT ONESTO PRE-RISULTATO: e' market-neutral a 10 gambe -> NON eseguibile a $600 (STAT-MODE come
XS01/VRP01). Il deliverable e' "esiste un carry reale e ORTOGONALE a XS01?", non un deploy.
"""
import sys
from pathlib import Path
import numpy as np, 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, _xsec_returns, _vrp_combo_returns, _HL_DIR
import altlib as L
from altlib import _sh, _dd_ret, _to_daily, _uplift_series, HOLDOUT, marginal_vs_tp01
FEE_SIDE = 0.0005 # 0.10% RT / 2, come XS01
def _load_panel():
"""Ritorna (PX, FUND): due DataFrame daily allineati [date x asset] con prezzo e funding
giornaliero realizzato (somma oraria). Solo asset con ENTRAMBI i dati."""
px, fund = {}, {}
for sym in XS_UNIVERSE:
pp = _HL_DIR / f"hl_{sym.lower()}_1d.parquet"
fp = _HL_DIR / f"hlfund_{sym.lower()}_1h.parquet"
if not (pp.exists() and fp.exists()):
continue
dp = pd.read_parquet(pp)
px[sym] = pd.Series(dp["close"].values.astype(float),
index=pd.to_datetime(dp["timestamp"], unit="ms", utc=True)).resample("1D").last()
df = pd.read_parquet(fp) # index ts (orario, utc), col funding
fund[sym] = df["funding"].resample("1D").sum()
PX = pd.concat(px, axis=1).sort_index()
FUND = pd.concat(fund, axis=1).sort_index().reindex(PX.index)
# tieni solo le date con prezzi per tutti + funding noto (0 dove manca un giorno isolato)
common = PX.dropna(how="any").index.intersection(FUND.index)
return PX.loc[common], FUND.loc[common].fillna(0.0)
def carry_returns(L_lb=7, H=10, k=5, direction="carry", target_vol=0.20,
fee_side=FEE_SIDE) -> pd.Series:
"""Serie daily netta del book funding-carry cross-sectional. direction='carry' shorta l'alto
funding (incassa il premio); 'anti' lo compra."""
PX, FUND = _load_panel()
idx = PX.index
px = PX.values; fnd = FUND.values
n, A = px.shape
dret = np.vstack([np.zeros(A), px[1:] / px[:-1] - 1.0])
# segnale = media causale del funding giornaliero sugli ultimi L giorni (shiftato di 1)
sig = pd.DataFrame(fnd, index=idx).rolling(L_lb, min_periods=max(3, L_lb // 2)).mean().shift(1).values
W = np.zeros((n, A)); w = np.zeros(A)
for i in range(n):
if i >= L_lb + 1 and i % H == 0 and np.isfinite(sig[i]).sum() >= 2 * k:
s = sig[i].copy()
order = np.argsort(np.where(np.isfinite(s), s, np.nan))
order = order[np.isfinite(s[order])]
if len(order) >= 2 * k:
w = np.zeros(A)
lo, hi = order[:k], order[-k:] # lo=basso funding, hi=alto funding
if direction == "carry":
w[hi] = -0.5 / k; w[lo] = +0.5 / k # SHORT alto funding (incassa), LONG basso
else:
w[hi] = +0.5 / k; w[lo] = -0.5 / k
W[i] = w
# ritorno del perp per un LONG = price_ret - funding realizzato
perp_ret = dret - fnd
gross = np.zeros(n); gross[1:] = np.sum(W[:-1] * perp_ret[1:], axis=1)
turn = np.zeros(n); turn[0] = np.abs(W[0]).sum(); turn[1:] = np.abs(np.diff(W, axis=0)).sum(axis=1)
net = gross - turn * fee_side
s = pd.Series(net, index=idx)
rv = s.rolling(30, min_periods=15).std().shift(1) * np.sqrt(365.25)
scale = np.clip(np.nan_to_num(target_vol / rv.replace(0, np.nan).values, nan=0.0), 0, 3.0)
return pd.Series(s.values * scale, index=idx)
def _stats(s: pd.Series) -> dict:
s = _to_daily(s)
h = s[s.index >= HOLDOUT]; isamp = s[s.index < HOLDOUT]
yrs = {y: round(_sh(s[s.index.year == y]), 2) for y in sorted(set(s.index.year))}
return dict(n=len(s), full=round(_sh(s), 2), insample=round(_sh(isamp), 2) if len(isamp) > 30 else None,
hold=round(_sh(h), 2) if len(h) > 30 else None, dd=round(_dd_ret(s), 3),
ann_ret=round(float(s.mean() * 365.25), 3), yearly=yrs)
def main():
print("=" * 96)
print(" FC01 — FUNDING-CARRY cross-sectional su Hyperliquid (STAT-MODE)")
print("=" * 96)
PX, FUND = _load_panel()
print(f" panel: {PX.shape[1]} asset x {len(PX)} giorni [{PX.index[0].date()} .. {PX.index[-1].date()}]")
print(f" asset: {list(PX.columns)}")
# funding medio annualizzato per asset (dispersione = materia prima del carry)
fann = (FUND.mean() * 365.25 * 100).round(1).sort_values(ascending=False)
print(f" funding annualizzato% (carry teorico long-pays): "
f"max {fann.index[0]} {fann.iloc[0]:+.1f} min {fann.index[-1]} {fann.iloc[-1]:+.1f} "
f"mediana {fann.median():+.1f} spread {fann.iloc[0]-fann.iloc[-1]:.1f}")
base = carry_returns()
print("\n --- STANDALONE (config base L=7 H=10 k=5, direction=carry) ---")
st = _stats(base)
print(f" FULL Sh {st['full']} in-sample {st['insample']} HOLD {st['hold']} DD {st['dd']*100:.1f}% "
f"ann.ret {st['ann_ret']*100:+.1f}% ({st['n']}g)")
print(f" per anno: {st['yearly']}")
# correlazioni vs gli sleeve attivi
print("\n --- CORRELAZIONE vs sleeve attivi (daily, overlap comune) ---")
refs = {"TP01": L.tp01_baseline_daily(), "XS01": _to_daily(_xsec_returns()),
"VRP01": _to_daily(_vrp_combo_returns())}
bd = _to_daily(base)
for nm, r in refs.items():
J = pd.concat({"c": bd, "r": r}, axis=1, join="inner").dropna()
c = round(float(J["c"].corr(J["r"])), 3) if len(J) > 30 else None
print(f" corr(FC01, {nm}) = {c} (overlap {len(J)}g)")
# marginal vs TP01 (verdetto indurito completo)
print("\n --- MARGINAL vs TP01 (scorer indurito) ---")
m = marginal_vs_tp01(bd)
for key in ("marginal_verdict", "corr_full", "cand_full_sharpe", "cand_insample_sharpe",
"has_insample_edge", "multicut_persistent", "is_hedge", "null_pctl_insample"):
print(f" {key:22} = {m.get(key)}")
print(f" blend w25: {m.get('blends', {}).get('w25')}")
print(f" multicut_uplift: {m.get('multicut_uplift')}")
# UPLIFT vs XS01 (la domanda di overlap): XS01 da solo vs blend(XS01, FC01)
print("\n --- UPLIFT vs XS01 (aggiunge a XS01 o e' ridondante?) ---")
xs = refs["XS01"]
J = pd.concat({"xs": xs, "fc": bd}, axis=1, join="inner").dropna()
JH = J[J.index >= HOLDOUT]
for w in (0.25, 0.5):
bf = _sh((1 - w) * J["xs"] + w * J["fc"]) - _sh(J["xs"])
bh = (_sh((1 - w) * JH["xs"] + w * JH["fc"]) - _sh(JH["xs"])) if len(JH) > 30 else None
print(f" w={w}: uplift FULL {bf:+.3f} HOLD {bh:+.3f}" if bh is not None
else f" w={w}: uplift FULL {bf:+.3f}")
print(f" XS01 standalone: FULL {_sh(J['xs']):.2f} | FC01 standalone su overlap: {_sh(J['fc']):.2f}")
# PLATEAU su L, k, direzione
print("\n --- PLATEAU (FULL / in-sample / HOLD Sharpe) ---")
print(f" {'cfg':22} {'FULL':>6} {'in-s':>6} {'HOLD':>6} {'DD%':>6}")
for direction in ("carry", "anti"):
for Llb in (3, 7, 14, 30):
for k in (3, 5):
s = _stats(carry_returns(L_lb=Llb, k=k, direction=direction))
tag = f"{direction} L={Llb} k={k}"
print(f" {tag:22} {str(s['full']):>6} {str(s['insample']):>6} {str(s['hold']):>6} {s['dd']*100:>5.1f}")
if __name__ == "__main__":
main()
+108
View File
@@ -0,0 +1,108 @@
"""LEAD-LAG GENERICO non-sovrapposto: crypto[T-S -> T] predice future[T -> T+H]? (ogni mercato/fuso).
Market-agnostic. Per ogni giorno, entrata a ora T (UTC): segnale = crypto nella finestra [T-S, T]
(finisce all'entrata), cattura = future nella finestra SUCCESSIVA [T, T+H] (non sovrapposta).
Controllo = moto PROPRIO del future [T-S, T] -> isola se il crypto AGGIUNGE (anticipa) oltre il
momentum del future. Sweep su T (copre apertura Europa ~07h, USA ~13h, Asia ~00h) e H.
TRADE: sign(crypto[T-S,T]) * future[T,T+H] - costo. Sharpe sqrt(252), per-anno, OOS 2024+.
Dati: data/raw/fut_*_1h.parquet (UTC) + crypto 1h. Solo cache, nessun IB.
"""
import sys, glob
from pathlib import Path
import numpy as np, pandas as pd
ROOT = Path(__file__).resolve().parents[2]
RAW = ROOT / "data" / "raw"
sys.path.insert(0, str(ROOT)); sys.path.insert(0, str(ROOT / "scripts" / "research"))
from crypto_lead_harness import crypto_hourly, at
OOS = pd.Timestamp("2024-01-01", tz="UTC"); COST = 0.0002
def fut_hourly(sym):
d = pd.read_parquet(RAW / f"fut_{sym.lower()}_1h.parquet")
return pd.Series(d["close"].astype(float).values, index=pd.to_datetime(d["timestamp"], unit="ms", utc=True)).sort_index()
def _sh(r):
r = np.asarray(r, float); r = r[np.isfinite(r)]
return float(np.mean(r) / np.std(r) * np.sqrt(252)) if len(r) > 5 and np.std(r) > 0 else 0.0
def run(fut, bc, T, S, H):
days = pd.date_range(fut.index[0].normalize(), fut.index[-1].normalize(), freq="D", tz="UTC")
rows = []
for D in days:
te = D + pd.Timedelta(hours=T)
ts = te - pd.Timedelta(hours=S)
tx = te + pd.Timedelta(hours=H)
f0, f1 = at(fut, ts), at(fut, te); f2 = at(fut, tx)
c0, c1 = at(bc, ts), at(bc, te)
if not all(np.isfinite(v) and v > 0 for v in (f0, f1, f2, c0, c1)):
continue
rows.append((D, c1/c0 - 1, f1/f0 - 1, f2/f1 - 1))
if len(rows) < 120:
return None
Dd = pd.DataFrame(rows, columns=["d", "csig", "fctrl", "cap"]).set_index("d")
# filtra giorni in cui la cattura e' identicamente 0 (mercato chiuso, prezzo stale)
Dd = Dd[Dd["cap"].abs() > 1e-9]
if len(Dd) < 120:
return None
x, ctrl, y = Dd["csig"].values, Dd["fctrl"].values, Dd["cap"].values
def z(a):
s = a.std(); return (a - a.mean()) / s if s > 0 else a * 0
X = np.column_stack([np.ones(len(y)), z(x), z(ctrl)])
beta, *_ = np.linalg.lstsq(X, z(y), rcond=None)
resid = z(y) - X @ beta; dof = max(len(y) - 3, 1)
se = np.sqrt(np.sum(resid**2) / dof * np.diag(np.linalg.inv(X.T @ X)))
t_c = float(beta[1] / se[1]) if se[1] > 0 else 0.0
C = np.sign(x) * y - COST
m = Dd.index >= OOS
yrs = Dd.index.year.values
py = {int(v): round(_sh(C[yrs == v]), 2) for v in sorted(set(yrs)) if (yrs == v).sum() >= 30}
pos = sum(1 for v in py.values() if v > 0)
return dict(T=T, H=H, n=len(Dd), t_crypto=round(t_c, 2), sh_full=round(_sh(C), 2),
sh_oos=round(_sh(C[m]), 2), ann=round(float(np.nanmean(C) * 252 * 100), 1),
years_pos=pos, years_tot=len(py), per_year=py)
def main():
syms = sorted(p.name[4:-11].upper() for p in RAW.glob("fut_*_1h.parquet"))
print("=" * 98)
print(f" LEAD-LAG GENERICO non-sovrapposto — futures: {syms}")
print("=" * 98)
print(" crypto[T-8h -> T] -> future[T -> T+6h], controllo=moto proprio future. net 2bps, OOS2024+\n")
bcs = {l: crypto_hourly(l) for l in ("BTC", "ETH")}
winners = []
for sym in syms:
fut = fut_hourly(sym)
best = None
for lead in ("BTC", "ETH"):
for T in (0, 4, 8, 12, 16, 20):
r = run(fut, bcs[lead], T, 8, 6)
if not r:
continue
r["sym"] = sym; r["lead"] = lead
if best is None or r["sh_oos"] > best["sh_oos"]:
best = r
# raccogli i forti (crypto significativo E robusto)
if r["t_crypto"] >= 2.5 and r["sh_oos"] > 0.5 and r["years_pos"] == r["years_tot"]:
winners.append(r)
if best:
print(f" {sym:7} miglior: {best['lead']}->T{best['T']}h: t_crypto {best['t_crypto']:+.1f} "
f"Sh full {best['sh_full']:+.2f} OOS {best['sh_oos']:+.2f} ann {best['ann']:+.1f}% "
f"{best['years_pos']}/{best['years_tot']}y {best['per_year']}")
print("\n --- CANDIDATI FORTI (t_crypto>=2.5, OOS>0.5, tutti gli anni positivi) ---")
if not winners:
print(" NESSUNO. -> nessuna anticipazione crypto->future robusta oltre il rumore/beta.")
else:
winners.sort(key=lambda r: r["sh_oos"], reverse=True)
for w in winners[:10]:
print(f" {w['sym']:7} {w['lead']}->T{w['T']}h H{w['H']}: t_crypto {w['t_crypto']} "
f"Sh OOS {w['sh_oos']} full {w['sh_full']} ann {w['ann']}% {w['years_pos']}/{w['years_tot']}y {w['per_year']}")
if __name__ == "__main__":
main()
+116
View File
@@ -0,0 +1,116 @@
"""TEST ONESTO finestre NON sovrapposte: crypto[notte presto] -> future indice[notte tardi].
L'idea "monitor Deribit / trade IB" e' valida SOLO se il crypto anticipa il moto SUCCESSIVO del future
(non lo stesso intervallo = look-ahead). Qui:
entrata a T (ora UTC nella notte, equity cash chiuso): osservato crypto fino a T.
segnale = crypto[P 21:00 -> T] (info nota a T)
controllo= future[P 21:00 -> T] (il moto PROPRIO del future fino a T)
cattura = future[T -> D 13:00] (cio' che catturi entrando a T, fino a ~apertura cash)
TEST: il segnale crypto predice la cattura INCREMENTALMENTE al controllo? (crypto vede prima del future?)
TRADE: long/short future su sign(crypto[P21:00->T]), cattura future[T->open], net ~2bps. Sharpe/anno/OOS.
Confronto col baseline sign(future[P21:00->T]) (momentum proprio del future) per isolare il valore crypto.
Dati: data/raw/fut_<sym>_1h.parquet (UTC) + crypto 1h. sqrt(252).
"""
import sys, argparse, json
from pathlib import Path
import numpy as np, pandas as pd
ROOT = Path(__file__).resolve().parents[2]
RAW = ROOT / "data" / "raw"
sys.path.insert(0, str(ROOT)); sys.path.insert(0, str(ROOT / "scripts" / "research"))
from crypto_lead_harness import crypto_hourly, at
OOS = pd.Timestamp("2024-01-01", tz="UTC"); COST = 0.0002
CLOSE_H = 21; OPEN_H = 13
def fut_hourly(sym):
d = pd.read_parquet(RAW / f"fut_{sym.lower()}_1h.parquet")
return pd.Series(d["close"].astype(float).values, index=pd.to_datetime(d["timestamp"], unit="ms", utc=True)).sort_index()
def _sh(r):
r = np.asarray(r, float); r = r[np.isfinite(r)]
return float(np.mean(r) / np.std(r) * np.sqrt(252)) if len(r) > 5 and np.std(r) > 0 else 0.0
def run(sym="ES", lead="BTC", h_entry=6):
fut = fut_hourly(sym); bc = crypto_hourly(lead)
days = pd.date_range(fut.index[0].normalize(), fut.index[-1].normalize(), freq="B", tz="UTC")
rows = []
for k in range(1, len(days)):
D = days[k]; P = days[k-1]
t_pc = P + pd.Timedelta(hours=CLOSE_H) # P close 21:00
t_e = D + pd.Timedelta(hours=h_entry) # entrata T
t_o = D + pd.Timedelta(hours=OPEN_H) # ~apertura cash 13:00
fp, fe, fo = at(fut, t_pc), at(fut, t_e), at(fut, t_o)
cp, ce = at(bc, t_pc), at(bc, t_e)
if not all(np.isfinite(v) and v > 0 for v in (fp, fe, fo, cp, ce)):
continue
crypto_sig = ce / cp - 1.0 # crypto P21:00 -> T
fut_ctrl = fe / fp - 1.0 # future P21:00 -> T (controllo)
capture = fo / fe - 1.0 # future T -> open (cio' che catturi)
rows.append((D, crypto_sig, fut_ctrl, capture))
if len(rows) < 100:
return {"sym": sym, "lead": lead, "h_entry": h_entry, "err": f"n={len(rows)}"}
Dd = pd.DataFrame(rows, columns=["d", "csig", "fctrl", "cap"]).set_index("d")
x, ctrl, y = Dd["csig"].values, Dd["fctrl"].values, Dd["cap"].values
def z(a):
s = a.std(); return (a - a.mean()) / s if s > 0 else a * 0
# incrementale: cap ~ crypto + future_own_move
X = np.column_stack([np.ones(len(y)), z(x), z(ctrl)])
beta, *_ = np.linalg.lstsq(X, z(y), rcond=None)
resid = z(y) - X @ beta; dof = max(len(y) - 3, 1)
se = np.sqrt(np.sum(resid**2) / dof * np.diag(np.linalg.inv(X.T @ X)))
t_crypto = float(beta[1] / se[1]) if se[1] > 0 else 0.0
t_futown = float(beta[2] / se[2]) if se[2] > 0 else 0.0
# trade: crypto-signal vs future-own-signal
C_crypto = np.sign(x) * y - COST
C_futown = np.sign(ctrl) * y - COST
m = Dd.index >= OOS
yrs = Dd.index.year.values
py = {int(yv): round(_sh(C_crypto[yrs == yv]), 2) for yv in sorted(set(yrs)) if (yrs == yv).sum() >= 30}
return {"sym": sym, "lead": lead, "h_entry": h_entry, "n": len(Dd),
"corr_crypto_cap": round(float(np.corrcoef(x, y)[0, 1]), 3),
"t_crypto_incr": round(t_crypto, 2), "t_futown_incr": round(t_futown, 2),
"sh_crypto_full": round(_sh(C_crypto), 2), "sh_crypto_oos": round(_sh(C_crypto[m]), 2),
"sh_futown_full": round(_sh(C_futown), 2),
"ann_crypto_pct": round(float(np.nanmean(C_crypto) * 252 * 100), 1),
"per_year": py}
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--syms", default="ES,NQ,RTY")
ap.add_argument("--leads", default="BTC,ETH")
ap.add_argument("--entries", default="0,3,6,9")
args = ap.parse_args()
print("=" * 96)
print(" FUTURE OVERNIGHT — crypto[P21:00->T] predice future[T->open]? (finestre NON sovrapposte)")
print("=" * 96)
print(" cattura = future[T->open]; controllo = future[P21:00->T]; t_crypto_incr = crypto AL NETTO del future. net 2bps\n")
best = []
for sym in args.syms.split(","):
for lead in args.leads.split(","):
for h in [int(x) for x in args.entries.split(",")]:
r = run(sym, lead, h)
if "err" in r:
print(f" {sym}<-{lead} T={h}h: {r['err']}"); continue
print(f" {sym}<-{lead} T={h:>2}h UTC: n={r['n']} corr {r['corr_crypto_cap']:+.3f} | "
f"t_crypto(incr) {r['t_crypto_incr']:+.1f} t_futOwn {r['t_futown_incr']:+.1f} | "
f"trade crypto Sh {r['sh_crypto_full']:+.2f} (OOS {r['sh_crypto_oos']:+.2f}) ann {r['ann_crypto_pct']:+.1f}% | "
f"futOwn Sh {r['sh_futown_full']:+.2f}")
best.append(r)
best.sort(key=lambda r: r.get("sh_crypto_oos", -9), reverse=True)
print("\n --- migliori per Sharpe OOS (trade su segnale crypto) ---")
for r in best[:5]:
print(f" {r['sym']}<-{r['lead']} T={r['h_entry']}h: OOS {r['sh_crypto_oos']} full {r['sh_crypto_full']} "
f"t_crypto {r['t_crypto_incr']} per-anno {r['per_year']}")
print("\n NB: t_crypto_incr alto E sh_crypto > sh_futOwn => il crypto anticipa il future (idea valida).")
print(" t_crypto ~0 o sh_crypto ~ futOwn => e' solo momentum del future, il crypto non aggiunge.")
if __name__ == "__main__":
main()
+95
View File
@@ -0,0 +1,95 @@
"""IB EQUITIES/ETF DATA PROBE — certifica cosa il paper IB dà per la ricerca su azioni/ETF.
Gemello equity di certify_feed.py: PRIMA il dato (cosa c'è, quanto indietro, aggiustato per
dividendi/split?, cosa costa), POI la strategia. Disciplina v2.0.0.
Universo candidato per la prima ricerca equity (cross-sectional momentum / trend, l'edge "noioso e
robusto" più plausibile in un mercato efficiente):
* 11 SPDR settoriali (XLK..XLC) universo canonico del momentum cross-section settoriale;
* ETF broad / macro (SPY QQQ IWM TLT GLD HYG) per trend e risk-on/off;
* 2 azioni (AAPL MSFT) per tarare profondità/qualità.
Per ogni simbolo: profondità storica daily con whatToShow=ADJUSTED_LAST (split+dividendi, OBBLIGATORIO
per un backtest equity onesto) e TRADES (raw), + flag se scatta errore di subscription market-data.
uv run --with ib_async python scripts/research/ib_equities_probe.py
"""
import argparse, sys
SECTORS = ["XLK", "XLF", "XLE", "XLV", "XLI", "XLP", "XLY", "XLU", "XLB", "XLRE", "XLC"]
BROAD = ["SPY", "QQQ", "IWM", "TLT", "GLD", "HYG"]
STOCKS = ["AAPL", "MSFT"]
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--host", default="127.0.0.1")
ap.add_argument("--port", type=int, default=4002)
ap.add_argument("--client-id", type=int, default=88)
ap.add_argument("--years", default="20 Y", help="durata storica da richiedere")
args = ap.parse_args()
try:
from ib_async import IB, Stock
except Exception:
print("ib_async non importabile. Esegui con: uv run --with ib_async python ...")
sys.exit(2)
ib = IB()
try:
ib.connect(args.host, args.port, clientId=args.client_id, timeout=15)
except Exception as e:
print(f"[CONNESSIONE FALLITA] {args.host}:{args.port} -> {repr(e)[:140]}")
sys.exit(1)
print("=" * 96)
print(f" IB EQUITIES/ETF PROBE — {args.host}:{args.port} | acct {ib.managedAccounts()} | depth req {args.years}")
print("=" * 96)
universe = [("SECTOR", s) for s in SECTORS] + [("BROAD", s) for s in BROAD] + [("STOCK", s) for s in STOCKS]
print(f" {'sym':6} {'tipo':7} {'ADJUSTED_LAST':>26} {'TRADES':>22} note")
rows = []
for cat, sym in universe:
con = Stock(sym, "SMART", "USD")
try:
cds = ib.reqContractDetails(con)
if not cds:
print(f" {sym:6} {cat:7} {'-- no contract --':>26}")
continue
except Exception as e:
print(f" {sym:6} {cat:7} ERR resolve {repr(e)[:40]}")
continue
def hist(what):
try:
b = ib.reqHistoricalData(con, endDateTime="", durationStr=args.years,
barSizeSetting="1 day", whatToShow=what,
useRTH=True, formatDate=1, timeout=45)
if not b:
return "0 barre", None
return f"{len(b)}b {b[0].date}..{b[-1].date}", b
except Exception as e:
return f"ERR {repr(e)[:30]}", None
adj_s, adj_b = hist("ADJUSTED_LAST")
trd_s, _ = hist("TRADES")
note = ""
if "ERR" in adj_s or "0 barre" in adj_s:
note = "subscription? prova delayed"
print(f" {sym:6} {cat:7} {adj_s:>26} {trd_s:>22} {note}")
if adj_b:
rows.append((sym, len(adj_b), str(adj_b[0].date), str(adj_b[-1].date)))
print("-" * 96)
if rows:
depth = min(r[1] for r in rows); start = max(r[2] for r in rows)
print(f" CERTIFICABILI (ADJUSTED_LAST): {len(rows)}/{len(universe)} | profondità comune ~{depth}b | start comune {start}")
print(f" -> per un backtest cross-sectional servono date allineate: lo start comune e' il limite.")
else:
print(" NESSUN simbolo ha reso storia ADJUSTED — probabile mancanza market-data subscription.")
print(" Ripiego: whatToShow='TRADES' (raw, non adj) o dati 'delayed' / fonte esterna certificabile.")
ib.disconnect()
if __name__ == "__main__":
main()
+108
View File
@@ -0,0 +1,108 @@
"""IB DATA PROBE — enumera cosa un conto paper Interactive Brokers espone (dati storici).
NON e' una strategia: e' lo scan di FATTIBILITA' DATI, gemello di certify_feed.py per il mondo IB.
Disciplina v2.0.0: prima il dato (cosa c'e', quanto indietro, che qualita', cosa costa), poi la
strategia. "Solo dati, decido dopo".
PREREQUISITO: una sessione IB Gateway/TWS PAPER loggata e raggiungibile (default 127.0.0.1:4002).
Tipico: avvii IB Gateway (Paper) sul tuo PC con API abilitata su 4002, poi reverse-tunnel
SSH verso questo server: ssh -R 4002:localhost:4002 utente@server
ESECUZIONE (senza sporcare le dipendenze del progetto):
uv run --with ib_async python scripts/research/ib_probe.py
uv run --with ib_async python scripts/research/ib_probe.py --port 7497 # TWS paper
Cosa fa, in ordine, e si ferma con diagnosi chiara al primo errore:
(1) connette e stampa server version + account paper;
(2) risolve un set di contratti rilevanti per QUESTO progetto:
- CME crypto: BTC (5 BTC), MBT (micro 0.1 BTC), ETH, MET (micro); -> per il BASIS/carry
- spot crypto Paxos (se abilitato): BTC, ETH;
- 2 azioni/ETF di riferimento (SPY, AAPL) per tarare durate/qualita';
(3) per ogni contratto risolto: chiede un piccolo storico (durata breve, 1 day bars) e riporta
n barre, range, e se scatta un errore di SUBSCRIPTION mancante (codice 354/10089/10090...);
(4) sintesi: cosa e' scaricabile GRATIS su paper vs cosa richiede market-data a pagamento.
"""
import argparse, sys
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--host", default="127.0.0.1")
ap.add_argument("--port", type=int, default=7497, help="7497=TWS paper (default), 4002=GW paper, 4001/7496=live")
ap.add_argument("--client-id", type=int, default=77)
args = ap.parse_args()
try:
from ib_async import IB, Future, Stock, Crypto, util
except Exception:
print("ib_async non importabile. Esegui con: uv run --with ib_async python scripts/research/ib_probe.py")
sys.exit(2)
ib = IB()
try:
ib.connect(args.host, args.port, clientId=args.client_id, timeout=15)
except Exception as e:
print(f"[CONNESSIONE FALLITA] {args.host}:{args.port} -> {repr(e)[:160]}")
print(" Verifica: IB Gateway/TWS Paper acceso, API abilitata, porta giusta, tunnel attivo.")
sys.exit(1)
print("=" * 90)
print(f" IB DATA PROBE — connesso {args.host}:{args.port} | serverVersion={ib.client.serverVersion()}")
try:
accts = ib.managedAccounts()
print(f" account: {accts} (paper se inizia per 'D' tipicamente)")
except Exception as e:
print(f" account: ? ({repr(e)[:60]})")
print("=" * 90)
# (2) contratti rilevanti
candidates = []
# CME crypto futures: lasciamo che IB scelga il front-month (no expiry -> reqContractDetails)
candidates += [("CME BTC fut", Future("BTC", exchange="CME")),
("CME MBT micro", Future("MBT", exchange="CME")),
("CME ETH fut", Future("ETH", exchange="CME")),
("CME MET micro", Future("MET", exchange="CME"))]
# spot crypto Paxos (puo' non essere abilitato su paper)
candidates += [("Paxos BTC", Crypto("BTC", exchange="PAXOS", currency="USD")),
("Paxos ETH", Crypto("ETH", exchange="PAXOS", currency="USD"))]
# riferimenti equity
candidates += [("SPY ETF", Stock("SPY", "SMART", "USD")),
("AAPL", Stock("AAPL", "SMART", "USD"))]
resolved = []
print("\n (A) RISOLUZIONE CONTRATTI")
for label, c in candidates:
try:
cds = ib.reqContractDetails(c)
if not cds:
print(f" {label:16} -> NESSUN match")
continue
# per i futures prendi la scadenza piu' vicina disponibile
cd = sorted(cds, key=lambda d: getattr(d.contract, "lastTradeDateOrContractMonth", "") or "")[0]
con = cd.contract
extra = f" exp={con.lastTradeDateOrContractMonth}" if getattr(con, "lastTradeDateOrContractMonth", "") else ""
print(f" {label:16} -> OK {con.localSymbol or con.symbol} {con.exchange}{extra} (n match={len(cds)})")
resolved.append((label, con))
except Exception as e:
print(f" {label:16} -> ERR {repr(e)[:70]}")
# (3) prova storico breve
print("\n (B) STORICO DI PROVA (durata 10 D, barre 1 day)")
for label, con in resolved:
try:
bars = ib.reqHistoricalData(con, endDateTime="", durationStr="10 D",
barSizeSetting="1 day", whatToShow="TRADES",
useRTH=False, formatDate=1, timeout=30)
if not bars:
print(f" {label:16} -> 0 barre (forse serve subscription o whatToShow diverso)")
else:
print(f" {label:16} -> {len(bars)} barre {bars[0].date} .. {bars[-1].date} close={bars[-1].close}")
except Exception as e:
print(f" {label:16} -> ERR {repr(e)[:90]}")
print("\n (C) NOTE")
print(" - errori 354/10089/10090/10168 = market-data subscription mancante (paper la eredita dal live).")
print(" - per il BASIS/carry servono i MULTIPLI futures (front+next) -> poi reqContractDetails senza filtro expiry.")
ib.disconnect()
if __name__ == "__main__":
main()
+376
View File
@@ -0,0 +1,376 @@
"""intraday_regime.py — FILONE B: "INTRADAY REGIME BTC/ETH" (eseguibile) — 2026-06-29.
TESI. Cercare un meccanismo SUB-DAILY sui dati certificati 1h/4h/.../12h BTC/ETH che sia
ORTOGONALE sia a TP01 (TSMOM trend daily, long-flat) sia a SKH01 (regime BuzVola/BuzVolume
+ Donchian breakout a 230m). SKH01 prova che il sub-daily PUO' funzionare ed essere
quasi-ortogonale: qui si esplora un MECCANISMO DIVERSO, basato sulla QUALITA' del moto
intraday (efficiency-ratio / vol-expansion / thrust) come REGIME che condiziona una
posizione direzionale tenuta ~1 giorno.
Il killer ricorrente del progetto sotto le 12h e' il MURO-FEE (0.10% RT) + overfitting.
La ricetta che SKH01 usa per sopravvivere: DECISIONE sub-daily ma HOLD ~1 giorno -> pochi
trade -> la fee non uccide. Qui ogni meccanismo e' costruito per essere a basso turnover
(gate di regime che tiene flat la maggior parte del tempo, lookback non microscopici) e
viene giudicato col fee-sweep ALLA SUA FREQUENZA REALE. Se muore appena si mette la fee ->
SCARTATO e documentato (e' un risultato valido).
MECCANISMI (tutti come posizione CONTINUA decisa <= close[i], cosi' passano nativamente per
eval_weights / study_marginal / day_boundary_robust / eval_weights_smallcap di altlib):
ERM Efficiency-Ratio regime momentum. ER = |moto netto su L barre| / |percorso| (Kaufman):
alto = moto intraday "pulito"/direzionale, basso = chop. Prendi la direzione del moto
netto SOLO quando ER >= soglia (regime trendy intraday), altrimenti flat. Vol-target.
Storia economica: quando il prezzo intraday e' EFFICIENTE il momentum continua; quando
e' choppy non c'e' edge. DIVERSO da SKH01 (regime vol/volume) e da TP01 (TSMOM 1-6 mesi).
VEM Vol-Expansion Momentum. Direzione = segno del moto su Lmom barre, ATTIVA solo quando la
vol realizzata corta > vol realizzata lunga (espansione di volatilita'). Vol-target.
VBR Volatility/thrust breakout (Larry-Williams-style, ROLLING, no calendario). Segui solo i
movimenti significativi: posizione = segno(c[i]-c[i-1]) quando |Δ| > k*ATR, altrimenti
tieni la precedente. Momentum-continuation di thrust.
TOD Time-of-day seasonality (CONTROLLO calendario). Direzione per ora-del-giorno via media
espandente causale. Incluso APPOSTA per passarlo a day_boundary_robust: e' il tipo di
effetto che ha ucciso open_drive (artefatto di etichettatura del giorno UTC).
GATE (CLAUDE.md): causale/no-leak, NETTO fee 0.10% RT + sweep 0.00-0.20% a freq reale, OOS
hold-out + griglia + plateau, day_boundary_robust per effetti calendario, MARGINAL vs TP01
(earns_slot / has_insample_edge / multi-cut / non-hedge), corr con SKH01, haircut $600.
Esecuzione: uv run python scripts/research/intraday_regime.py
Idempotente, niente scritture su disco (solo report a stdout).
"""
from __future__ import annotations
import sys
from functools import lru_cache
import numpy as np
import pandas as pd
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt")
import altlib as al # noqa: E402
ASSETS = ("BTC", "ETH")
SCREEN_TFS = ("1h", "4h", "6h", "8h", "12h")
# ===========================================================================
# TARGET FACTORIES (ogni fattoria ritorna un target_fn(df) causale, posizione continua)
# ===========================================================================
def make_erm(tf: str, L_days: float, thr: float, long_flat: bool,
target_vol: float = 0.20):
"""Efficiency-Ratio regime momentum. L_days = lunghezza finestra in GIORNI (-> barre via bpd)."""
def fn(df):
c = df["close"].values.astype(float)
n = len(c)
L = max(2, round(L_days * al.bars_per_day(df)))
net = np.full(n, np.nan)
net[L:] = c[L:] - c[:-L]
step = np.abs(np.diff(c, prepend=c[0])) # |c[k]-c[k-1]|, causale
path = pd.Series(step).rolling(L, min_periods=L).sum().values
er = np.divide(np.abs(net), path, out=np.zeros(n), where=(path > 0))
active = (er >= thr) & np.isfinite(net)
raw = np.where(active, np.sign(net), 0.0)
if long_flat:
raw = np.clip(raw, 0.0, None)
return al.vol_target(raw, df, target_vol=target_vol, vol_win_days=30, leverage_cap=2.0)
return fn
def make_vem(tf: str, Lmom_days: float, Lshort_days: float, Llong_days: float,
long_flat: bool, target_vol: float = 0.20):
"""Vol-expansion momentum: momentum attivo solo quando rv_corta > rv_lunga (espansione)."""
def fn(df):
c = df["close"].values.astype(float)
n = len(c)
bpd = al.bars_per_day(df)
Lmom = max(2, round(Lmom_days * bpd))
ws, wl = max(2, round(Lshort_days * bpd)), max(3, round(Llong_days * bpd))
r = al.simple_returns(c)
rv_s = al.rolling_std(r, ws)
rv_l = al.rolling_std(r, wl)
expand = (rv_s > rv_l) & np.isfinite(rv_s) & np.isfinite(rv_l)
net = np.full(n, np.nan)
net[Lmom:] = c[Lmom:] - c[:-Lmom]
raw = np.where(expand & np.isfinite(net), np.sign(net), 0.0)
if long_flat:
raw = np.clip(raw, 0.0, None)
return al.vol_target(raw, df, target_vol=target_vol, vol_win_days=30, leverage_cap=2.0)
return fn
def make_vbr(tf: str, k: float, atr_win: int, long_flat: bool, target_vol: float = 0.20):
"""Thrust-breakout rolling: segui i moti significativi (|Δ| > k*ATR), altrimenti hold."""
def fn(df):
c = df["close"].values.astype(float)
a = al.atr(df, atr_win)
a_prev = np.roll(a, 1); a_prev[0] = a[0] # ATR noto a inizio barra (causale)
delta = np.diff(c, prepend=c[0])
sig = np.where(np.abs(delta) > k * a_prev, np.sign(delta), np.nan)
raw = pd.Series(sig).ffill().fillna(0.0).values
if long_flat:
raw = np.clip(raw, 0.0, None)
return al.vol_target(raw, df, target_vol=target_vol, vol_win_days=30, leverage_cap=2.0)
return fn
def make_tod(tf: str, long_flat: bool, target_vol: float = 0.20, min_obs: int = 20):
"""Time-of-day seasonality (controllo calendario). Direzione = segno della media espandente
causale del rendimento della stessa ora-del-giorno. Da passare a day_boundary_robust."""
def fn(df):
c = df["close"].values.astype(float)
r = al.simple_returns(c)
hour = pd.to_datetime(df["datetime"], utc=True).dt.hour.values
n = len(c)
sums = {}; cnts = {}
raw = np.zeros(n)
for i in range(1, n):
h_prev = int(hour[i - 1]) # aggiorna con la barra GIA' chiusa
sums[h_prev] = sums.get(h_prev, 0.0) + r[i - 1]
cnts[h_prev] = cnts.get(h_prev, 0) + 1
h = int(hour[i])
if cnts.get(h, 0) >= min_obs:
raw[i] = 1.0 if sums[h] >= 0 else -1.0
if long_flat:
raw = np.clip(raw, 0.0, None)
return al.vol_target(raw, df, target_vol=target_vol, vol_win_days=30, leverage_cap=2.0)
return fn
# ===========================================================================
# SCREENING — griglia leggera per (asset,tf,params) via eval_weights (vettoriale).
# ===========================================================================
def _screen_cell(fn, tf):
"""Min-asset full/hold Sharpe, fee@0.10 e @0.20 RT, turnover, time-in-market."""
fulls, holds, f10, f20, turn, tim = [], [], [], [], [], []
for a in ASSETS:
df = al.get(a, tf)
tgt = fn(df)
ev = al.eval_weights(df, tgt, fee_side=0.0005) # 0.10% RT
ev0 = al.eval_weights(df, tgt, fee_side=0.001) # 0.20% RT
fulls.append(ev["full"]["sharpe"]); holds.append(ev["holdout"].get("sharpe", 0.0))
f10.append(ev["full"]["sharpe"]); f20.append(ev0["full"]["sharpe"])
turn.append(ev["turnover_per_year"]); tim.append(ev["time_in_market"])
return dict(tf=tf, min_full=round(min(fulls), 3), min_hold=round(min(holds), 3),
min_f10=round(min(f10), 3), min_f20=round(min(f20), 3),
turnover=round(float(np.mean(turn)), 1), tim=round(float(np.mean(tim)), 2))
def screen_family(name, factory, grid, tfs=SCREEN_TFS):
"""Esegue la griglia, ritorna lista di dict ordinata per min_hold (solo fee-surviving in cima)."""
rows = []
for tf in tfs:
for params in grid:
fn = factory(tf=tf, **params)
m = _screen_cell(fn, tf)
m["params"] = params
m["fee_ok"] = bool(m["min_f20"] > 0)
rows.append(m)
rows.sort(key=lambda r: (r["fee_ok"], r["min_hold"]), reverse=True)
print(f"\n===== {name}: top celle (di {len(rows)}) =====")
print(f" {'tf':>4} {'minFull':>7} {'minHold':>7} {'f@.10':>6} {'f@.20':>6} "
f"{'turn/y':>7} {'tim':>5} feeOK params")
for r in rows[:10]:
print(f" {r['tf']:>4} {r['min_full']:+7.2f} {r['min_hold']:+7.2f} {r['min_f10']:+6.2f} "
f"{r['min_f20']:+6.2f} {r['turnover']:>7.0f} {r['tim']:>5.2f} "
f"{str(r['fee_ok']):>5} {r['params']}")
return rows
# ===========================================================================
# DEEP-DIVE sul vincitore: marginal vs TP01 + day_boundary + corr SKH01 + haircut $600.
# ===========================================================================
@lru_cache(maxsize=1)
def _skh_daily() -> pd.Series:
"""Rendimenti giornalieri SKH01-V2-DD (50/50 BTC+ETH) dallo sleeve di progetto (read-only)."""
from src.portfolio.sleeves import _skyhook_returns
s = _skyhook_returns()
if s.index.tz is None:
s.index = s.index.tz_localize("UTC")
return s
def corr_to_skh(fn, tf) -> dict:
cand = al.candidate_daily(fn, tf=tf)
skh = _skh_daily()
J = pd.concat({"C": cand, "S": skh}, axis=1, join="inner").dropna()
JH = J[J.index >= al.HOLDOUT]
return dict(n=int(len(J)),
corr_full=round(float(J["C"].corr(J["S"])), 3) if len(J) > 5 else None,
corr_hold=round(float(JH["C"].corr(JH["S"])), 3) if len(JH) > 5 else None)
def haircut_600(fn, tf) -> dict:
"""Sharpe onesto a $600: salta i ribilanci < $5 (eval_weights_smallcap), per asset + media."""
out = {}
for a in ASSETS:
df = al.get(a, tf)
sc = al.eval_weights_smallcap(df, fn(df), capital=600.0, min_order=5.0)
out[a] = dict(modeled=sc["modeled"]["sharpe"], real=sc["realistic"]["sharpe"],
haircut=sc["sharpe_haircut"], n_tr=sc["n_executed_trades"])
return out
def plateau_erm(tf="8h"):
"""Plateau fine L_days x thr al TF vincente (min-asset full/hold/f@.20). Un edge vero ha un
PLATEAU, non una cella isolata."""
print("\n" + "=" * 78)
print(f"PLATEAU ERM @ {tf} (min-asset; L_days righe, thr colonne) — full / hold / f@.20")
print("=" * 78)
Ls = (1.5, 2.0, 2.5, 3.0); thrs = (0.30, 0.35, 0.40, 0.45, 0.50)
print(" L\\thr " + "".join(f"{t:>16.2f}" for t in thrs))
for L in Ls:
cells = []
for t in thrs:
m = _screen_cell(make_erm(tf=tf, L_days=L, thr=t, long_flat=False), tf)
cells.append(f"{m['min_full']:+.2f}/{m['min_hold']:+.2f}/{m['min_f20']:+.2f}")
print(f" {L:>4.1f} " + "".join(f"{c:>16}" for c in cells))
def vs_book(fn, tf):
"""Il test decisivo del gate #5: ERM AGGIUNGE oltre il book esistente (TP01+SKH01), o e'
SKH01 travestito? Sharpe/DD full & hold dei blend incrementali su griglia giornaliera."""
cand = al.candidate_daily(fn, tf=tf)
tp = al.tp01_baseline_daily()
skh = _skh_daily()
J = pd.concat({"T": tp, "S": skh, "C": cand}, axis=1, join="inner").dropna()
JH = J[J.index >= al.HOLDOUT]
blends = [
("TP01", (1.0, 0.0, 0.0)),
("TP01+SKH 75/25", (0.75, 0.25, 0.0)),
("TP01+SKH+ERM 60/25/15", (0.60, 0.25, 0.15)),
("TP01+SKH+ERM 55/20/25", (0.55, 0.20, 0.25)),
]
print("\n" + "=" * 78)
print("vs BOOK ESISTENTE (TP01+SKH01) — ERM aggiunge oltre SKH? (gate #5)")
print("=" * 78)
print(f" {'blend':<26} {'FULL Sh':>8} {'FULL DD':>8} {'HOLD Sh':>8} {'HOLD DD':>8}")
for label, (wt, ws, wc) in blends:
bf = wt * J["T"] + ws * J["S"] + wc * J["C"]
bh = wt * JH["T"] + ws * JH["S"] + wc * JH["C"]
print(f" {label:<26} {al._sh(bf):>+8.2f} {al._dd_ret(bf) * 100:>7.1f}% "
f"{al._sh(bh):>+8.2f} {al._dd_ret(bh) * 100:>7.1f}%")
def deep_dive(name, fn, tf, calendar=False):
print("\n" + "#" * 78)
print(f"# DEEP-DIVE: {name} (tf={tf})")
print("#" * 78)
caus = al.causality_ok(fn, tf=tf)
print(f"\n[CAUSALITA'] ok={caus['ok']} max_tail_diff={caus['max_tail_diff']} "
f"(checked={caus['checked']})")
print("\n[FEE-SWEEP a frequenza reale] (study_weights su entrambi gli asset)")
sw = al.study_weights(name, fn, tfs=(tf,))
print(al.fmt(sw))
print("\n[MARGINAL vs TP01]")
sm = al.study_marginal(name, fn, tf=tf)
print(al.fmt_marginal(sm))
sk = corr_to_skh(fn, tf)
print(f"\n[CORR con SKH01] full={sk['corr_full']} hold={sk['corr_hold']} "
f"(n_giorni={sk['n']})")
if calendar:
print("\n[DAY-BOUNDARY ROBUST] (OBBLIGATORIO per effetti ora/sessione/giorno)")
else:
print("\n[DAY-BOUNDARY ROBUST] (sanity: un segnale di prezzo dev'essere ~INVARIANT)")
db = al.day_boundary_robust(fn, tf=tf)
print(f" verdict={db['verdict']} spread={db.get('spread')} "
f"min={db.get('min')} max={db.get('max')} per_offset={db.get('per_offset')}")
print("\n[HAIRCUT $600] (eval_weights_smallcap: salta ribilanci < $5)")
hc = haircut_600(fn, tf)
for a, d in hc.items():
print(f" {a}: modeled Sh {d['modeled']:+.2f} -> real Sh {d['real']:+.2f} "
f"(haircut {d['haircut']:+.2f}, trade eseguiti {d['n_tr']})")
return dict(name=name, tf=tf, causal=caus["ok"], earns_slot=sm["earns_slot"],
marginal=sm["marginal_verdict"], corr_skh=sk, day_boundary=db["verdict"],
haircut=hc)
# ===========================================================================
# MAIN
# ===========================================================================
def main():
print("=" * 78)
print("FILONE B — INTRADAY REGIME BTC/ETH (intraday_regime.py)")
print("=" * 78)
tp01 = al.tp01_baseline_daily()
print(f"Baseline TP01 (50/50) full Sharpe ~{al._sh(tp01):.2f} "
f"hold ~{al._sh(tp01[tp01.index >= al.HOLDOUT]):.2f} (riferimento marginale)")
# ---- Griglie (compatte: plateau leggibile, no overfit di griglia gigante) ----
erm_grid = [dict(L_days=L, thr=t, long_flat=lf)
for L in (1.0, 2.0, 3.0) for t in (0.35, 0.50) for lf in (False, True)]
vem_grid = [dict(Lmom_days=lm, Lshort_days=2.0, Llong_days=10.0, long_flat=lf)
for lm in (1.0, 3.0) for lf in (False, True)]
vbr_grid = [dict(k=k, atr_win=14, long_flat=lf)
for k in (0.5, 1.0, 1.5) for lf in (False, True)]
tod_grid = [dict(long_flat=lf) for lf in (False, True)]
fam = {
"ERM": (make_erm, erm_grid, SCREEN_TFS),
"VEM": (make_vem, vem_grid, ("4h", "6h", "8h", "12h")),
"VBR": (make_vbr, vbr_grid, ("4h", "6h", "8h", "12h")),
"TOD": (make_tod, tod_grid, ("1h",)),
}
screens = {}
for name, (factory, grid, tfs) in fam.items():
screens[name] = screen_family(name, factory, grid, tfs)
# ---- Vincitore per famiglia (best min_hold tra le fee-surviving con min_full>0) ----
print("\n" + "=" * 78)
print("VINCITORI PER FAMIGLIA (best min_hold tra fee-surviving, min_full>0)")
print("=" * 78)
winners = {}
for name, (factory, grid, tfs) in fam.items():
ok = [r for r in screens[name] if r["fee_ok"] and r["min_full"] > 0]
pool = ok if ok else screens[name]
w = max(pool, key=lambda r: r["min_hold"])
winners[name] = w
print(f" {name}: tf={w['tf']} {w['params']} minFull={w['min_full']:+.2f} "
f"minHold={w['min_hold']:+.2f} f@.20={w['min_f20']:+.2f} feeOK={w['fee_ok']}")
# ---- Deep-dive sui due meccanismi piu' promettenti (per min_hold) + il controllo TOD ----
ranked = sorted(["ERM", "VEM", "VBR"],
key=lambda n: winners[n]["min_hold"], reverse=True)
deep = []
for name in ranked[:2]:
w = winners[name]
factory = fam[name][0]
fn = factory(tf=w["tf"], **w["params"])
deep.append(deep_dive(f"{name} {w['params']}", fn, w["tf"], calendar=False))
# controllo calendario: TOD passa SEMPRE per day_boundary_robust
wt = winners["TOD"]
fn_tod = make_tod(tf=wt["tf"], **wt["params"])
deep.append(deep_dive(f"TOD {wt['params']}", fn_tod, wt["tf"], calendar=True))
# ---- Analisi extra sul vincitore ERM (plateau fine + vs book TP01+SKH01) ----
we = winners["ERM"]
fn_erm = make_erm(tf=we["tf"], **we["params"])
plateau_erm(we["tf"])
vs_book(fn_erm, we["tf"])
# ---- Verdetto sintetico ----
print("\n" + "=" * 78)
print("SINTESI")
print("=" * 78)
for d in deep:
print(f" {d['name']:<26} tf={d['tf']:>3} | marginal={d['marginal']:<9} "
f"earns_slot={d['earns_slot']!s:<5} corrSKH(full/hold)="
f"{d['corr_skh']['corr_full']}/{d['corr_skh']['corr_hold']} "
f"day_boundary={d['day_boundary']}")
any_slot = any(d["earns_slot"] for d in deep)
print(f"\n => earns_slot su qualche meccanismo? {any_slot}")
print(" (vedi diario docs/diary/2026-06-29-intraday-regime.md per il verdetto ragionato)")
if __name__ == "__main__":
main()
@@ -0,0 +1,267 @@
"""intraday_regime_analysis.py — ANALISI DI ROBUSTEZZA del LEAD ERM (filone B) — 2026-06-29.
Il lead di B (ERM 8h L=2.0 thr=0.35 L/S) fa earns_slot=True, ma con 2 caveat NON quantificati
dallo script di scoperta `intraday_regime.py`:
(1) il VINCITORE e' selezionato per min_hold MASSIMO su ~60 celle -> selezione-sull'hold-out;
(2) il plateau hold-out e' a UNA SOLA RIGA (positivo solo a L~2.0; L>=2.5 va negativo sull'hold).
Insieme = rischio multiple-testing / overfit della finestra recente, mai deflazionato (a
differenza del filone C che ha il deflated-Sharpe).
Questo script attacca esattamente quei nodi, SENZA toccare il live (read-only, branch separato):
A) DEFLATED SHARPE (Bailey & Lopez de Prado) del vincitore vs TUTTI i trial realmente
cercati (ERM+VEM+VBR+TOD, tutte le celle/TF). Se DSR << 0.95 lo Sharpe non e' significativo
dopo la correzione per multiple-testing.
B) SELEZIONE IN-SAMPLE-ONLY: ri-scelgo la cella ERM usando SOLO lo Sharpe PRE-2025 (mai
l'hold-out), poi ne valuto earns_slot sull'intera storia. Se una cella scelta SENZA vedere
l'hold-out continua ad ADDS, l'edge non e' hold-out-mined.
C) ENSEMBLE DEL PLATEAU: invece della singola cella migliore, media i pesi su tutto il
plateau ERM 8h (L x thr) -> un candidato unico "non-cherry-picked" -> earns_slot. Se la
famiglia regge senza scegliere L, il caveat (1)+(2) si attenua.
D) DOVE VIVE L'EDGE: Sharpe per-anno standalone + uplift per-anno del blend 3-way
(TP01+SKH+ERM) vs 2-way (TP01+SKH), e corr(ERM,SKH) per-anno (e' un hedge-di-SKH?).
Esecuzione: uv run python scripts/research/intraday_regime_analysis.py
Idempotente, niente scritture su disco (solo report a stdout).
"""
from __future__ import annotations
import sys
import numpy as np
import pandas as pd
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt")
import altlib as al # noqa: E402
from intraday_regime import make_erm, make_vem, make_vbr, make_tod, _skh_daily # noqa: E402
DPY = 365.25
HOLD = al.HOLDOUT
ASSETS = ("BTC", "ETH")
SCREEN_TFS = ("1h", "4h", "6h", "8h", "12h")
WIN = dict(tf="8h", L_days=2.0, thr=0.35, long_flat=False) # il lead di B
deflated_sharpe = al.deflated_sharpe # gate canonico (codificato in altlib da questo filone)
# ===========================================================================
def all_trials():
"""Ricostruisce la griglia COMPLETA realmente cercata in intraday_regime.py, ritorna una
lista di (tag, factory, tf, params) per pesare il multiple-testing onestamente."""
out = []
erm_grid = [dict(L_days=L, thr=t, long_flat=lf)
for L in (1.0, 2.0, 3.0) for t in (0.35, 0.50) for lf in (False, True)]
for tf in SCREEN_TFS:
for p in erm_grid:
out.append(("ERM", make_erm, tf, p))
# + il plateau fine a 8h (L x thr, lf=False) — anche quelle sono celle testate
for L in (1.5, 2.0, 2.5, 3.0):
for t in (0.30, 0.35, 0.40, 0.45, 0.50):
out.append(("ERM-plat", make_erm, "8h", dict(L_days=L, thr=t, long_flat=False)))
vem_grid = [dict(Lmom_days=lm, Lshort_days=2.0, Llong_days=10.0, long_flat=lf)
for lm in (1.0, 3.0) for lf in (False, True)]
for tf in ("4h", "6h", "8h", "12h"):
for p in vem_grid:
out.append(("VEM", make_vem, tf, p))
vbr_grid = [dict(k=k, atr_win=14, long_flat=lf) for k in (0.5, 1.0, 1.5) for lf in (False, True)]
for tf in ("4h", "6h", "8h", "12h"):
for p in vbr_grid:
out.append(("VBR", make_vbr, tf, p))
for lf in (False, True):
out.append(("TOD", make_tod, "1h", dict(long_flat=lf)))
return out
def cand_full_is_sharpe(factory, tf, params):
"""(daily, full Sharpe, in-sample<2025 Sharpe) del candidato 50/50 di quella cella."""
fn = factory(tf=tf, **params)
daily = al.candidate_daily(fn, tf=tf)
full = al._sh(daily)
ins = al._sh(daily[daily.index < HOLD]) if (daily.index < HOLD).sum() > 60 else float("nan")
return daily, full, ins
# ===========================================================================
def part_A_deflated():
print("=" * 78)
print("A) DEFLATED SHARPE del vincitore vs TUTTI i trial cercati (multiple-testing)")
print("=" * 78)
trials = all_trials()
sr_all, sr_no_tod, sr_erm = [], [], []
win_daily = win_full = None
for tag, factory, tf, params in trials:
try:
daily, full, _ = cand_full_is_sharpe(factory, tf, params)
except Exception:
continue
sr_all.append(full)
if tag != "TOD":
sr_no_tod.append(full)
if tag.startswith("ERM"):
sr_erm.append(full)
if tag == "ERM" and tf == WIN["tf"] and params.get("L_days") == WIN["L_days"] \
and params.get("thr") == WIN["thr"] and params.get("long_flat") is False:
win_daily, win_full = daily, full
sr_arr = np.array([s for s in sr_all if np.isfinite(s)])
print(f" N trial finiti : {len(sr_arr)}")
print(f" Sharpe winner (50/50) : {win_full:+.3f}")
print(f" Sharpe trial: mean {sr_arr.mean():+.2f} std {sr_arr.std():.2f} "
f"max {sr_arr.max():+.2f} >0: {int((sr_arr > 0).sum())}/{len(sr_arr)}")
dsr = None
for label, pool in (("TUTTI 122", sr_all), ("no-TOD", sr_no_tod), ("solo-ERM", sr_erm)):
d, sr0 = deflated_sharpe(win_full, pool, win_daily)
n = int(np.isfinite(np.array(pool)).sum())
print(f" DSR [{label:>9} N={n:>3}]: {d:.3f} (Sh-max null {sr0:+.2f}) -> "
f"{'PASS' if d >= 0.95 else 'FAIL'}")
if label == "TUTTI 122":
dsr = d
return dsr, win_full, None
# ===========================================================================
def part_B_insample_pick():
print("\n" + "=" * 78)
print("B) SELEZIONE IN-SAMPLE-ONLY (scelgo la cella ERM solo con Sharpe PRE-2025)")
print("=" * 78)
erm_grid = [dict(L_days=L, thr=t, long_flat=lf)
for L in (1.0, 1.5, 2.0, 2.5, 3.0) for t in (0.30, 0.35, 0.40, 0.50)
for lf in (False, True)]
rows = []
for tf in SCREEN_TFS:
for p in erm_grid:
try:
_, full, ins = cand_full_is_sharpe(make_erm, tf, p)
except Exception:
continue
rows.append((ins, full, tf, p))
rows = [r for r in rows if np.isfinite(r[0])]
rows.sort(key=lambda r: r[0], reverse=True) # ordina per Sharpe IN-SAMPLE (no hold-out)
print(f" Top 5 celle per Sharpe IN-SAMPLE (<2025):")
for ins, full, tf, p in rows[:5]:
print(f" IS {ins:+.2f} FULL {full:+.2f} tf={tf:>3} {p}")
ins, full, tf, p = rows[0]
print(f"\n -> cella scelta SENZA vedere l'hold-out: tf={tf} {p}")
sm = al.study_marginal(f"ERM-ISpick {p}", make_erm(tf=tf, **p), tf=tf)
m = sm["marginal"]
print(f" earns_slot={sm['earns_slot']} marginal={m['marginal_verdict']} "
f"abs={sm['absolute']['verdict']['grade']}")
print(f" corr->TP01 {m['corr_full']} has_insample_edge={m['has_insample_edge']} "
f"is_hedge={m['is_hedge']} robust_oos={m['robust_oos']}")
print(f" blend w25: full uplift {m['blends']['w25']['uplift_full']:+.3f} "
f"hold uplift {m['blends']['w25']['uplift_hold']:+.3f}")
same = (tf == WIN["tf"] and abs(p["L_days"] - WIN["L_days"]) < 1e-9
and abs(p["thr"] - WIN["thr"]) < 1e-9 and p["long_flat"] is False)
print(f" coincide col vincitore max-hold? {same}")
return sm["earns_slot"], (tf, p)
# ===========================================================================
def ensemble_target(tf, cells):
"""Media (equal-weight) dei pesi vol-targeted su piu' celle ERM -> un unico stream per asset.
Ritorna un target_fn(df) che ricostruisce l'ensemble per quel df."""
def fn(df):
ws = [make_erm(tf=tf, **c)(df) for c in cells]
return np.nanmean(np.vstack(ws), axis=0)
return fn
def part_C_plateau_ensemble():
print("\n" + "=" * 78)
print("C) ENSEMBLE DEL PLATEAU ERM 8h (media celle L x thr, NIENTE cherry-pick)")
print("=" * 78)
cells = [dict(L_days=L, thr=t, long_flat=False)
for L in (1.5, 2.0, 2.5, 3.0) for t in (0.30, 0.35, 0.40, 0.45, 0.50)]
fn = ensemble_target("8h", cells)
print(f" celle nell'ensemble: {len(cells)} (L 1.5-3.0 x thr 0.30-0.50, lf=False)")
caus = al.causality_ok(fn, tf="8h")
print(f" causale: ok={caus['ok']} max_tail_diff={caus['max_tail_diff']}")
sm = al.study_marginal("ERM-plateau-ens", fn, tf="8h")
m = sm["marginal"]
print(f" earns_slot={sm['earns_slot']} marginal={m['marginal_verdict']} "
f"abs={sm['absolute']['verdict']['grade']}")
print(f" standalone full {m['cand_full_sharpe']} hold {m['cand_hold_sharpe']} "
f"in-sample {m.get('cand_insample_sharpe')}")
print(f" corr->TP01 {m['corr_full']} has_insample_edge={m['has_insample_edge']} "
f"is_hedge={m['is_hedge']} robust_oos={m['robust_oos']}")
print(f" blend w25: full {m['blends']['w25']['full']} (uplift "
f"{m['blends']['w25']['uplift_full']:+.3f}) hold {m['blends']['w25']['hold']} "
f"(uplift {m['blends']['w25']['uplift_hold']:+.3f})")
print(f" multicut: {m['multicut_uplift']}")
return sm["earns_slot"]
# ===========================================================================
def part_D_where_edge():
print("\n" + "=" * 78)
print("D) DOVE VIVE L'EDGE — per-anno standalone + uplift 3-way vs 2-way + corr(ERM,SKH)")
print("=" * 78)
fn = make_erm(**WIN)
cand = al.candidate_daily(fn, tf=WIN["tf"])
tp = al.tp01_baseline_daily()
skh = _skh_daily()
J = pd.concat({"T": tp, "S": skh, "C": cand}, axis=1, join="inner").dropna()
two = 0.75 * J["T"] + 0.25 * J["S"]
three = 0.60 * J["T"] + 0.25 * J["S"] + 0.15 * J["C"]
print(f" {'anno':>5} {'ERM Sh':>7} {'TP+SKH':>7} {'+ERM':>7} {'Δuplift':>8} "
f"{'corr(ERM,SKH)':>14} {'corr(ERM,TP)':>13}")
for y in sorted(set(J.index.year)):
sub = J[J.index.year == y]
if len(sub) < 40:
continue
s2 = 0.75 * sub["T"] + 0.25 * sub["S"]
s3 = 0.60 * sub["T"] + 0.25 * sub["S"] + 0.15 * sub["C"]
print(f" {y:>5} {al._sh(sub['C']):>+7.2f} {al._sh(s2):>+7.2f} {al._sh(s3):>+7.2f} "
f"{al._sh(s3) - al._sh(s2):>+8.2f} {sub['C'].corr(sub['S']):>+14.2f} "
f"{sub['C'].corr(sub['T']):>+13.2f}")
print(f" {'FULL':>5} {al._sh(J['C']):>+7.2f} {al._sh(two):>+7.2f} {al._sh(three):>+7.2f} "
f"{al._sh(three) - al._sh(two):>+8.2f} {J['C'].corr(J['S']):>+14.2f} "
f"{J['C'].corr(J['T']):>+13.2f}")
JH = J[J.index >= HOLD]
h2 = 0.75 * JH["T"] + 0.25 * JH["S"]
h3 = 0.60 * JH["T"] + 0.25 * JH["S"] + 0.15 * JH["C"]
print(f" {'HOLD':>5} {al._sh(JH['C']):>+7.2f} {al._sh(h2):>+7.2f} {al._sh(h3):>+7.2f} "
f"{al._sh(h3) - al._sh(h2):>+8.2f} {JH['C'].corr(JH['S']):>+14.2f} "
f"{JH['C'].corr(JH['T']):>+13.2f}")
def part_E_codified_gate():
"""Validazione END-TO-END del gate appena codificato in altlib: study_family_honest sulla
famiglia ERM deve dare earns_slot_honest=False (sceglie in-sample-only + deflaziona)."""
print("\n" + "=" * 78)
print("E) GATE CODIFICATO (al.study_family_honest) sulla famiglia ERM — deve bocciare")
print("=" * 78)
grid = [dict(L_days=L, thr=t, long_flat=lf)
for L in (1.0, 1.5, 2.0, 2.5, 3.0) for t in (0.30, 0.35, 0.40, 0.50)
for lf in (False, True)]
rep = al.study_family_honest("ERM", make_erm, grid, SCREEN_TFS)
ch = rep["chosen"]
print(f" n_celle={rep['n_cells']} cella in-sample-best: tf={ch['tf']} {ch['params']}")
print(f" earns_slot_marginal={rep['earns_slot_marginal']} "
f"deflated_sharpe={rep['deflated_sharpe']} (dsr_pass={rep['dsr_pass']})")
print(f" => earns_slot_HONEST = {rep['earns_slot_honest']} "
f"(atteso False: slot bocciato dal gate)")
return rep["earns_slot_honest"]
def main():
print("ANALISI ROBUSTEZZA LEAD ERM (filone B) — read-only, nessun impatto live\n")
dsr, win_full, sr0 = part_A_deflated()
es_is, is_cell = part_B_insample_pick()
es_ens = part_C_plateau_ensemble()
part_D_where_edge()
es_honest = part_E_codified_gate()
print("\n" + "=" * 78)
print("SINTESI ANALISI B")
print("=" * 78)
print(f" A) deflated-Sharpe winner = {dsr:.3f} ({'PASS' if dsr >= 0.95 else 'FAIL'} vs 0.95)")
print(f" B) cella scelta in-sample-only earns_slot = {es_is} (cella {is_cell})")
print(f" C) ensemble del plateau earns_slot = {es_ens}")
print(f" E) gate codificato earns_slot_honest = {es_honest}")
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()
+458
View File
@@ -0,0 +1,458 @@
"""macro_regime_gate.py — Filone D: MACRO REGIME-GATE sul book crypto (eseguibile).
TESI
----
Usare segnali macro/cross-market equity (SPY/QQQ/IWM), credito (HYG/LQD), oro (GLD/SLV),
tassi (TLT/IEF) come GATE risk-on/risk-off applicato al book crypto (BTC/ETH) per migliorare
il TIMING del drawdown di TP01. Quando il regime macro e' risk-off (credito che cede, equity
sotto trend, fuga sui bond) -> riduci/azzera l'esposizione crypto; risk-on -> lascia agire TP01.
E' ESEGUIBILE perche' GATA solo BTC/ETH perp (non aggiunge gambe).
NON e' un lead-lag direzionale (gia' esplorato e morto: vedi 2026-06-22/-23 leadlag diaries).
L'angolo nuovo = un OVERLAY binario/continuo di DE-RISK sul book.
IL RISCHIO (da CLAUDE.md): il gate di de-risk rischia di essere RIDONDANTE col trend TP01 e'
gia' long-flat e va a 0 nei crash (lezione DVOL-spike "ridondante col trend, Delta 0.00"). Questo
script DEVE dimostrare che il gate aggiunge OLTRE quel che TP01 fa da solo, altrimenti SCARTATO.
CAUSALITA' (fusi orari, regola di prim'ordine)
----------------------------------------------
- Barre equity: open-labeled a 00:00 del giorno di trading; il CLOSE e' ~20:00-21:00 UTC dello
STESSO giorno (NYSE 16:00 ET).
- Barre crypto 1d: open-labeled a 00:00; il CLOSE e' a 00:00 UTC del giorno DOPO. TP01 decide la
posizione a close[i] e la TIENE durante la barra i+1 (eval_weights shift-a per te).
- Quindi: gate[i] allineato (merge_asof backward, equity-label <= crypto-label day i) usa il
close equity del giorno i (noto ~20:00 day i) per la posizione tenuta durante la barra i+1
(giorno i+1). Margine causale >= 4h. Leak-free. Variante STRICT (equity-label < crypto-label)
come margine extra. Verifica con al.causality_ok + day_boundary_robust.
USO: uv run python scripts/research/macro_regime_gate.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"))
import altlib as al # noqa: E402
from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio # noqa: E402
DATA = _ROOT / "data" / "raw"
HOLDOUT = al.HOLDOUT
ASSETS = ("BTC", "ETH")
# ===========================================================================
# MACRO FRAME — ETF daily, allineati causalmente sul calendario SPY (master).
# ===========================================================================
def _load_eq(sym: str) -> pd.DataFrame:
p = DATA / f"eq_{sym}_1d.parquet"
df = pd.read_parquet(p).sort_values("timestamp").reset_index(drop=True)
df["dt"] = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
return df[["timestamp", "dt", "close"]].rename(columns={"close": sym})
def macro_frame(syms=("spy", "qqq", "iwm", "hyg", "lqd", "gld", "slv", "tlt", "ief")) -> pd.DataFrame:
"""Frame macro sul calendario NYSE (master = SPY). Ogni colonna e' il close equity,
allineato causale (merge_asof backward) -> nessun valore futuro per riga."""
base = _load_eq("spy")[["timestamp", "dt"]].copy()
out = base
for s in syms:
e = _load_eq(s)[["timestamp", s]]
out = pd.merge_asof(out, e, on="timestamp", direction="backward")
return out
# ===========================================================================
# GATE BUILDERS — ognuno ritorna (timestamp_ms, gate in [0,1]) sul calendario equity.
# Tutti CAUSALI: la riga i usa solo close <= riga i (rolling/SMA, niente expanding-future).
# gate=1 => risk-on (TP01 pieno); gate=g_off (0 o 0.5) => risk-off (de-risk).
# ===========================================================================
def _sma(x: np.ndarray, n: int) -> np.ndarray:
return pd.Series(x).rolling(n, min_periods=n).mean().values
def _ret(x: np.ndarray, n: int) -> np.ndarray:
r = np.full(len(x), np.nan)
r[n:] = x[n:] / x[:-n] - 1.0
return r
def gate_trend(mf: pd.DataFrame, col: str, n: int, g_off: float) -> pd.DataFrame:
"""Risk-on se col_close > SMA(col, n). Filtro di trend classico (SPY200, HYG, ...)."""
c = mf[col].values.astype(float)
on = c > _sma(c, n)
g = np.where(on, 1.0, g_off)
g[~np.isfinite(c)] = np.nan
g[np.isnan(_sma(c, n))] = np.nan
return pd.DataFrame({"timestamp": mf["timestamp"].values, "gate": g})
def gate_ratio(mf: pd.DataFrame, num: str, den: str, n: int, g_off: float) -> pd.DataFrame:
"""Risk-on se ratio num/den (proxy spread di credito) > la sua SMA(n).
HYG/LQD o HYG/IEF in calo = spread che si allarga = risk-off."""
ratio = (mf[num].values.astype(float) / mf[den].values.astype(float))
on = ratio > _sma(ratio, n)
g = np.where(on, 1.0, g_off)
g[~np.isfinite(ratio)] = np.nan
g[np.isnan(_sma(ratio, n))] = np.nan
return pd.DataFrame({"timestamp": mf["timestamp"].values, "gate": g})
def gate_combo(mf: pd.DataFrame, n: int, g_off: float, thr: float = 0.5,
continuous: bool = False) -> pd.DataFrame:
"""Score di regime = media di 3 voti risk-on: SPY>SMA, HYG>SMA, HYG/LQD>SMA.
continuous=True -> gate = g_off + (1-g_off)*score (de-risk graduale).
continuous=False -> gate = 1 se score>=thr else g_off (binario su maggioranza)."""
spy = mf["spy"].values.astype(float)
hyg = mf["hyg"].values.astype(float)
ratio = hyg / mf["lqd"].values.astype(float)
votes = np.vstack([spy > _sma(spy, n), hyg > _sma(hyg, n), ratio > _sma(ratio, n)]).astype(float)
warm = np.isnan(_sma(spy, n)) | np.isnan(_sma(hyg, n)) | np.isnan(_sma(ratio, n))
score = votes.mean(axis=0)
if continuous:
g = g_off + (1.0 - g_off) * score
else:
g = np.where(score >= thr, 1.0, g_off)
g[warm] = np.nan
return pd.DataFrame({"timestamp": mf["timestamp"].values, "gate": g})
def gate_flight(mf: pd.DataFrame, safe: str, risk: str, n: int, g_off: float) -> pd.DataFrame:
"""Fuga verso la sicurezza: risk-off quando il safe-asset (TLT/GLD) sale MENTRE il risk
(SPY) scende sull'orizzonte n. Divergenza risk-off classica (flight-to-quality)."""
s = mf[safe].values.astype(float)
rk = mf[risk].values.astype(float)
off = (_ret(s, n) > 0) & (_ret(rk, n) < 0)
g = np.where(off, g_off, 1.0)
warm = np.isnan(_ret(s, n)) | np.isnan(_ret(rk, n))
g[warm] = np.nan
return pd.DataFrame({"timestamp": mf["timestamp"].values, "gate": g})
def gate_eqvol(mf: pd.DataFrame, n: int, win: int, z: float, g_off: float) -> pd.DataFrame:
"""Regime di vol equity: de-risk quando la vol realizzata SPY (win g) e' alta vs la sua
storia espandente-causale (z-score > z). Proxy 'VIX spike' senza VIX."""
spy = mf["spy"].values.astype(float)
r = np.zeros(len(spy)); r[1:] = spy[1:] / spy[:-1] - 1.0
rv = pd.Series(r).rolling(win, min_periods=win).std().values
zsc = al.zscore(rv, n)
off = zsc > z
g = np.where(off, g_off, 1.0)
g[~np.isfinite(zsc)] = np.nan
return pd.DataFrame({"timestamp": mf["timestamp"].values, "gate": g})
# ===========================================================================
# ALIGN GATE -> CRYPTO (causale) + GATED TARGET
# ===========================================================================
def align_gate(gate_df: pd.DataFrame, crypto_df: pd.DataFrame, strict: bool = False) -> np.ndarray:
"""Allinea il gate (calendario equity) alle barre crypto. merge_asof backward:
crypto-label day i -> ultimo gate equity con label <= day i (strict: < day i).
NaN pre-storia -> gate=1 (nessun de-risk quando non c'e' info)."""
left = pd.DataFrame({"timestamp": crypto_df["timestamp"].astype("int64").values})
g = gate_df.dropna(subset=["gate"]).sort_values("timestamp")
m = pd.merge_asof(left, g, on="timestamp", direction="backward",
allow_exact_matches=not strict)
return pd.Series(m["gate"].values).ffill().fillna(1.0).values
def tp01_pos(df: pd.DataFrame) -> np.ndarray:
return TrendPortfolio(**CANONICAL).target_series(df)
def make_target_fn(gate_builder, strict: bool = False):
"""Ritorna target_fn(df, asset) = posizione TP01 * gate macro allineato (causale).
gate_builder() costruisce il gate sul calendario equity una volta (cache esterna)."""
_MF = macro_frame()
gate_df = gate_builder(_MF)
def target_fn(df: pd.DataFrame, asset: str = "") -> np.ndarray:
pos = tp01_pos(df)
g = align_gate(gate_df, df, strict=strict)
return pos * g
return target_fn, gate_df
# ===========================================================================
# EVALUATION — solo vs gated, combo 50/50 + per-asset, FULL/HOLD/DD/CAGR.
# ===========================================================================
def _combo_daily(target_fn) -> pd.Series:
series = {}
for a in ASSETS:
df = al.get(a, "1d")
ev = al.eval_weights(df, target_fn(df, a))
series[a] = pd.Series(ev["net"], index=ev["idx"])
J = pd.concat(series, axis=1, join="inner").fillna(0.0)
return al._to_daily(0.5 * J[ASSETS[0]] + 0.5 * J[ASSETS[1]])
def _sh(s):
return al._sh(s)
def _metrics(s: pd.Series) -> dict:
sh = _sh(s)
eq = np.cumprod(1.0 + s.values)
pk = np.maximum.accumulate(eq)
dd = float(np.max((pk - eq) / pk)) if len(eq) else 0.0
yrs = max((s.index[-1] - s.index[0]).days / 365.25, 1e-6)
cagr = eq[-1] ** (1 / yrs) - 1 if len(eq) and eq[-1] > 0 else -1.0
return dict(sharpe=round(sh, 3), dd=round(dd, 4), cagr=round(cagr, 4))
def eval_solo_vs_gated(target_fn):
"""Combo 50/50: solo (gate=1) vs gated. Ritorna dict con FULL e HOLD per entrambi."""
solo_fn = lambda df, a="": tp01_pos(df)
solo = _combo_daily(solo_fn)
gated = _combo_daily(target_fn)
J = pd.concat({"solo": solo, "gated": gated}, axis=1, join="inner").dropna()
JH = J[J.index >= HOLDOUT]
return dict(
full_solo=_metrics(J["solo"]), full_gated=_metrics(J["gated"]),
hold_solo=_metrics(JH["solo"]), hold_gated=_metrics(JH["gated"]),
n=len(J), nh=len(JH), solo_series=J["solo"], gated_series=J["gated"])
def per_asset_table(target_fn) -> dict:
out = {}
for a in ASSETS:
df = al.get(a, "1d")
solo = al.eval_weights(df, tp01_pos(df))
gated = al.eval_weights(df, target_fn(df, a))
out[a] = dict(
full_solo=dict(sharpe=solo["full"]["sharpe"], dd=solo["full"]["maxdd"], cagr=solo["full"]["cagr"]),
full_gated=dict(sharpe=gated["full"]["sharpe"], dd=gated["full"]["maxdd"], cagr=gated["full"]["cagr"]),
hold_solo=dict(sharpe=solo["holdout"].get("sharpe", 0.0)),
hold_gated=dict(sharpe=gated["holdout"].get("sharpe", 0.0)))
return out
# ===========================================================================
# REDUNDANCY DIAGNOSTIC — il controllo DECISIVO contro il trend.
# ===========================================================================
def redundancy_diag(gate_df: pd.DataFrame) -> dict:
"""Quanto FA davvero il gate, dato che TP01 e' gia' flat nei crash?
- exposure TP01 nei giorni risk-off (gate<1) vs risk-on: se gia' ~0 -> ridondante.
- quota di giorni in cui il gate riduce una posizione NON gia' flat (lavoro effettivo).
- corr fra (1-gate) e (1-exposure_norm)."""
lev = CANONICAL["leverage"]
rows = []
for a in ASSETS:
df = al.get(a, "1d")
pos = tp01_pos(df)
expo = np.clip(np.abs(pos) / lev, 0, 1) # esposizione normalizzata 0..1
g = align_gate(gate_df, df)
dt = pd.to_datetime(df["datetime"], utc=True)
m = pd.DataFrame({"dt": dt, "expo": expo, "g": g})
m = m[m["dt"] >= dt.iloc[0]] # tutto
roff = m["g"] < 0.999
ron = ~roff
flat_thr = 0.05
# lavoro effettivo: gate<1 E TP01 non gia' flat
work = roff & (m["expo"] > flat_thr)
rows.append(dict(
asset=a,
expo_riskoff=round(float(m.loc[roff, "expo"].mean()) if roff.any() else 0.0, 3),
expo_riskon=round(float(m.loc[ron, "expo"].mean()) if ron.any() else 0.0, 3),
pct_days_riskoff=round(float(roff.mean()), 3),
pct_days_gate_works=round(float(work.mean()), 3),
corr_1mg_1mexpo=round(float(np.corrcoef(1 - m["g"], 1 - m["expo"])[0, 1])
if m["g"].std() > 0 else float("nan"), 3),
))
return {r["asset"]: r for r in rows}
# ===========================================================================
# RUNNER
# ===========================================================================
GATES = {
# binari classici
"SPY>MA200": lambda mf: gate_trend(mf, "spy", 200, 0.0),
"SPY>MA100": lambda mf: gate_trend(mf, "spy", 100, 0.0),
"SPY>MA50": lambda mf: gate_trend(mf, "spy", 50, 0.0),
"SPY>MA200_half": lambda mf: gate_trend(mf, "spy", 200, 0.5),
"HYG>MA100": lambda mf: gate_trend(mf, "hyg", 100, 0.0),
"HYG/LQD>MA50": lambda mf: gate_ratio(mf, "hyg", "lqd", 50, 0.0),
"HYG/LQD>MA100": lambda mf: gate_ratio(mf, "hyg", "lqd", 100, 0.0),
# combinati / score
"COMBO_maj100": lambda mf: gate_combo(mf, 100, 0.0, thr=0.5),
"COMBO_all100": lambda mf: gate_combo(mf, 100, 0.0, thr=0.99),
"COMBO_cont100": lambda mf: gate_combo(mf, 100, 0.0, continuous=True),
"COMBO_cont100h": lambda mf: gate_combo(mf, 100, 0.5, continuous=True),
# flight-to-quality / vol
"TLTup&SPYdn20": lambda mf: gate_flight(mf, "tlt", "spy", 20, 0.0),
"GLDup&SPYdn20": lambda mf: gate_flight(mf, "gld", "spy", 20, 0.0),
"SPYvol_z1": lambda mf: gate_eqvol(mf, 250, 20, 1.0, 0.0),
}
def delever_frontier(target_vols=(0.10, 0.12, 0.14, 0.16, 0.18, 0.20)) -> dict:
"""CONTROLLO DECISIVO (lezione DVOL-overlay): per meno DD la leva e' target_vol, non un
overlay. Frontiera DD/Sharpe di TP01 puro a target_vol decrescente. Se il miglior gate
sta SOPRA (DD piu' alto a parita' di Sharpe, o Sharpe piu' basso a parita' di DD) di questa
frontiera, il suo taglio di DD e' solo de-levering replicabile meglio senza macro."""
out = {}
for tv in target_vols:
cfg = {**CANONICAL, "target_vol": tv}
fn = lambda df, a="", cfg=cfg: TrendPortfolio(**cfg).target_series(df)
c = _combo_daily(fn)
out[tv] = _metrics(c)
return out
def fmt_cmp(label, m_solo, m_gated) -> str:
ds = m_gated["sharpe"] - m_solo["sharpe"]
dd = m_gated["dd"] - m_solo["dd"]
dc = m_gated.get("cagr", 0) - m_solo.get("cagr", 0)
return (f" {label:5s} Sh {m_solo['sharpe']:+.2f}->{m_gated['sharpe']:+.2f} (d{ds:+.2f}) "
f"DD {m_solo['dd']*100:4.1f}%->{m_gated['dd']*100:4.1f}% (d{dd*100:+.1f}pp) "
f"CAGR {m_solo.get('cagr',0)*100:+5.1f}%->{m_gated.get('cagr',0)*100:+5.1f}% (d{dc*100:+.1f}pp)")
def main():
pd.set_option("display.width", 160)
print("=" * 92)
print("MACRO REGIME-GATE sul book crypto (TP01 BTC/ETH) — Filone D")
print(f" TP01 CANONICAL = {CANONICAL}")
print(f" HOLD-OUT >= {HOLDOUT.date()} fee {al.FEE_SIDE*2*100:.2f}%RT")
mf = macro_frame()
print(f" Macro frame: {len(mf)} barre {mf['dt'].iloc[0].date()} -> {mf['dt'].iloc[-1].date()} "
f"cols={[c for c in mf.columns if c not in ('timestamp','dt')]}")
print("=" * 92)
# ---- 1) SWEEP DI TUTTI I GATE (combo 50/50) -------------------------------------
print("\n[1] SWEEP GATE — combo 50/50 BTC+ETH, FULL & HOLD-OUT, vs TP01-solo\n")
results = {}
for name, builder in GATES.items():
tf, gate_df = make_target_fn(builder)
cmp = eval_solo_vs_gated(tf)
results[name] = (cmp, gate_df, tf)
print(f"GATE {name}")
print(fmt_cmp("FULL", cmp["full_solo"], cmp["full_gated"]))
print(fmt_cmp("HOLD", cmp["hold_solo"], cmp["hold_gated"]))
# baseline (solo) numbers come from any cmp
any_cmp = next(iter(results.values()))[0]
print(f"\n [baseline TP01-solo] FULL Sh {any_cmp['full_solo']['sharpe']} DD {any_cmp['full_solo']['dd']*100:.1f}% "
f"CAGR {any_cmp['full_solo']['cagr']*100:.1f}% | HOLD Sh {any_cmp['hold_solo']['sharpe']} "
f"DD {any_cmp['hold_solo']['dd']*100:.1f}% CAGR {any_cmp['hold_solo']['cagr']*100:.1f}%")
# ---- 2) SELEZIONE: miglior gate per HOLD-OUT Sharpe, poi per riduzione DD --------
def score(name):
c = results[name][0]
return (c["hold_gated"]["sharpe"], -c["full_gated"]["dd"])
best = max(results, key=score)
# anche il "miglior DD-cutter" che non peggiora troppo lo Sharpe FULL
dd_best = min(results, key=lambda n: results[n][0]["full_gated"]["dd"])
print(f"\n[2] Miglior gate per HOLD-OUT Sharpe: {best}")
print(f" Miglior gate per DD FULL ridotto : {dd_best}")
# ---- CONTROLLO DECISIVO: de-lever frontier (target_vol) -------------------------
print("\n[2b] CONTROLLO DECISIVO — TP01 puro a target_vol piu' basso (de-lever) vs gate:")
fr = delever_frontier()
for tv, m in fr.items():
print(f" target_vol {tv:.2f}: FULL Sh {m['sharpe']:+.2f} DD {m['dd']*100:4.1f}% CAGR {m['cagr']*100:+5.1f}%")
print(" -> i gate de-leveranti (COMBO_cont, SPYvol) vanno confrontati con QUESTA frontiera:")
for n in ("COMBO_cont100", "COMBO_cont100h", "SPYvol_z1", "SPY>MA200_half"):
if n in results:
g = results[n][0]["full_gated"]
print(f" {n:16s}: FULL Sh {g['sharpe']:+.2f} DD {g['dd']*100:4.1f}% CAGR {g['cagr']*100:+5.1f}%")
for tag, name in [("BEST-HOLD", best), ("BEST-DD", dd_best)]:
if tag == "BEST-DD" and dd_best == best:
continue
deep_dive(tag, name, results)
def deep_dive(tag, name, results):
cmp, gate_df, tf = results[name]
print("\n" + "=" * 92)
print(f"[DEEP DIVE {tag}] GATE = {name}")
print("=" * 92)
# per-asset
print("\n Per-asset (TP01-solo -> TP01+gate):")
pa = per_asset_table(tf)
for a in ASSETS:
d = pa[a]
print(f" {a}: FULL Sh {d['full_solo']['sharpe']:+.2f}->{d['full_gated']['sharpe']:+.2f} "
f"DD {d['full_solo']['dd']*100:.0f}%->{d['full_gated']['dd']*100:.0f}% "
f"CAGR {d['full_solo']['cagr']*100:+.0f}%->{d['full_gated']['cagr']*100:+.0f}% | "
f"HOLD Sh {d['hold_solo']['sharpe']:+.2f}->{d['hold_gated']['sharpe']:+.2f}")
# ---- 3) CONTROLLO RIDONDANZA COL TREND ------------------------------------------
print("\n [3] CONTROLLO RIDONDANZA COL TREND (il test decisivo):")
rd = redundancy_diag(gate_df)
for a in ASSETS:
r = rd[a]
print(f" {a}: exposure TP01 nei giorni risk-off={r['expo_riskoff']} vs risk-on={r['expo_riskon']} "
f"| giorni risk-off {r['pct_days_riskoff']*100:.0f}% "
f"giorni in cui il gate LAVORA (riduce pos non-flat) {r['pct_days_gate_works']*100:.0f}% "
f"| corr(1-gate, 1-expo)={r['corr_1mg_1mexpo']}")
print(" -> se exposure-risk-off ~ exposure-risk-on e 'gate-lavora' e' basso => RIDONDANTE col trend.")
# ---- 4) MARGINAL SCORER -----------------------------------------------------------
print("\n [4] MARGINAL SCORER vs TP01 (gate come candidato-sleeve):")
rep = al.study_marginal(f"GATE[{name}]", tf, tf="1d")
print(al.fmt_marginal(rep))
# overlay-delta: lo STREAM incrementale del gate = gated - solo (e' alpha o hedge?)
delta = (cmp["gated_series"] - cmp["solo_series"]).dropna()
print("\n overlay-delta (gated - solo) come stream a se':")
md = al.marginal_vs_tp01(delta)
print(f" verdict={md.get('marginal_verdict')} corr->TP01 {md.get('corr_full')} "
f"is_hedge={md.get('is_hedge')} uplift TP01-up {md.get('uplift_tp01_up')} / "
f"TP01-down {md.get('uplift_tp01_down')} cand-Sh full {md.get('cand_full_sharpe')}")
# ---- 5) FEE SWEEP -----------------------------------------------------------------
print("\n [5] FEE SWEEP (combo 50/50 gated, FULL Sharpe):")
for f in al.FEE_SWEEP:
series = {}
for a in ASSETS:
df = al.get(a, "1d")
ev = al.eval_weights(df, tf(df, a), fee_side=f)
series[a] = pd.Series(ev["net"], index=ev["idx"])
J = pd.concat(series, axis=1, join="inner").fillna(0.0)
d = al._to_daily(0.5 * J[ASSETS[0]] + 0.5 * J[ASSETS[1]])
print(f" {2*f*100:.2f}%RT -> Sh {_sh(d):+.2f}")
# ---- 6) ESEGUIBILITA' a $600 ------------------------------------------------------
print("\n [6] ESEGUIBILITA' a $600 (eval_weights_smallcap, haircut reale):")
for a in ASSETS:
df = al.get(a, "1d")
sc = al.eval_weights_smallcap(df, tf(df, a), capital=600, min_order=5)
print(f" {a}: modeled Sh {sc['modeled']['sharpe']:+.2f} -> real Sh {sc['realistic']['sharpe']:+.2f} "
f"haircut {sc['sharpe_haircut']:+.2f} trade eseguiti {sc['n_executed_trades']}")
# ---- 7) LEAK / BOUNDARY -----------------------------------------------------------
print("\n [7] LEAK-FREE & BOUNDARY:")
cz = al.causality_ok(tf, tf="1d")
print(f" causality_ok={cz['ok']} (max_tail_diff {cz['max_tail_diff']}, checked {cz['checked']})")
# variante STRICT (equity-label < crypto-label): margine causale extra
tf_strict, _ = make_target_fn(GATES[name], strict=True)
cmp_s = eval_solo_vs_gated(tf_strict)
print(f" STRICT align (1 barra equity extra di lag): FULL Sh {cmp_s['full_gated']['sharpe']:+.2f} "
f"(vs {cmp['full_gated']['sharpe']:+.2f}) HOLD Sh {cmp_s['hold_gated']['sharpe']:+.2f} "
f"(vs {cmp['hold_gated']['sharpe']:+.2f}) -> robusto se ~uguale")
db = al.day_boundary_robust(tf, tf="1d")
print(f" day_boundary_robust={db['verdict']} (spread {db.get('spread')}, per-offset {db.get('per_offset')})")
# ---- 8) PLATEAU (solo per i trend MA) ---------------------------------------------
if name.startswith("SPY>MA") or name.startswith("HYG"):
print("\n [8] PLATEAU su finestra MA (SPY trend), g_off=0:")
for n in (50, 100, 150, 200, 250):
tfn, _ = make_target_fn(lambda mf, n=n: gate_trend(mf, "spy", n, 0.0))
cc = eval_solo_vs_gated(tfn)
print(f" SPY>MA{n:3d}: FULL Sh {cc['full_gated']['sharpe']:+.2f} DD {cc['full_gated']['dd']*100:.1f}% "
f"HOLD Sh {cc['hold_gated']['sharpe']:+.2f} DD {cc['hold_gated']['dd']*100:.1f}%")
if __name__ == "__main__":
main()
+410
View File
@@ -0,0 +1,410 @@
"""META-ALLOCATION — allocazione DINAMICA CAUSALE tra i 4 sleeve esistenti vs PESI FISSI.
TESI (angolo nuovo, NON un 5o sleeve): il portafoglio attivo combina TP01/XS01/VRP01/SKH01 a
PESO FISSO (41.25/18.75/15/25, rinormalizzati per-riga sugli sleeve attivi vedi
src/portfolio/portfolio.combined_daily). Domanda: una regola di allocazione DINAMICA e CAUSALE
fra gli stessi 4 sleeve batte i pesi fissi OUT-OF-SAMPLE? Cioe' c'e' meta-alpha di timing di
portafoglio, oltre ai pesi fissi?
MECCANISMI testati (tutti CAUSALI: decisione con dati <= t-1, peso applicato in t; ribilancio
SETTIMANALE con costo sul turnover dei pesi |Δw|*cost_rate, cosi' una regola che ribilancia di
continuo PAGA il suo attrito non si bara):
1. VOL-PARITY peso inverso alla vol realizzata rolling (risk-parity causale). Pure + tilt.
2. MOMENTUM-OF-SLEEVES sovrappesa gli sleeve con Sharpe rolling recente migliore (tilt capato).
3. DISPERSION-REGIME tilt verso XS01 quando la dispersione cross-section degli alt e' alta
(percentile ESPANDENTE causale), verso il resto altrimenti.
4. DRAWDOWN-CONTROL riduce l'esposizione aggregata (-> cash) o ribilancia verso VRP/SKH
quando il portafoglio e' in drawdown rolling (causale sull'equity propria).
GATE / ONESTA':
- FULL e HOLD-OUT (2025-01-01+) Sharpe + maxDD, per-anno, turnover dei pesi/anno.
- Confronto vs BASE pesi-fissi sulla STESSA finestra e con lo STESSO motore (entrambi pagano il
costo di ribilancio): il miglioramento deve esserci su HOLD-OUT, non solo FULL.
- MULTI-CUT: uplift dello Sharpe a piu' date di taglio (2022/23/24/25). Robusto solo se positivo
su piu' finestre, non su una sola fortunata.
- DE-LEVERING: lo Sharpe e' scale-invariant. Se uno schema ABBASSA DD/vol ma NON alza lo Sharpe,
il taglio di DD e' solo de-levering (replicabile abbassando la leva di BASE) -> NON e' alpha di
timing. Lo riportiamo esplicitamente confrontando BASE de-levered a pari vol.
VERDETTO per schema: BATTE-FISSO / solo-de-levering / RIDONDANTE / SCARTATO.
uv run python scripts/research/meta_allocation.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
import pandas as pd
from src.portfolio.sleeves import active_sleeves, XS_UNIVERSE, _HL_DIR
from src.portfolio.portfolio import metrics, yearly, HOLDOUT, DAYS_PER_YEAR
REBAL_DAYS = 7 # ribilancio settimanale
COST_RATE = 0.0005 # 5 bps per-lato sul turnover dei pesi (Deribit taker ~ questo ordine)
VOL_WIN = 60 # finestra vol realizzata (risk-parity)
MOM_WIN = 63 # finestra Sharpe rolling (momentum-of-sleeves, ~1 trimestre)
WARMUP = 90 # giorni di warm-up: prima -> fallback ai pesi fissi
# ----------------------------------------------------------------------------- data
def sleeve_matrix() -> tuple[pd.DatetimeIndex, np.ndarray, np.ndarray, list[str], np.ndarray]:
"""Matrice daily allineata dei 4 sleeve (outer-join). Ritorna (index, R, active, names, fixed_w).
R = rendimenti (0 dove inattivo), active = maschera bool di disponibilita'."""
base = active_sleeves()
names = [s.name for s in base]
fixed_w = np.array([s.weight for s in base], float)
cols = {s.name: s.daily() for s in base}
J = pd.concat(cols, axis=1, join="outer").sort_index()
J = J[J.notna().any(axis=1)]
active = J.notna().values
R = np.nan_to_num(J.values, nan=0.0)
return J.index, R, active, names, fixed_w
def dispersion_series(index: pd.DatetimeIndex) -> np.ndarray:
"""Dispersione cross-section dei rendimenti degli alt Hyperliquid (std cross-section dei ritorni
daily sull'universo XS01), allineata all'index del portafoglio. NaN dove non c'e' dato HL."""
cols = {}
for sym in XS_UNIVERSE:
p = _HL_DIR / f"hl_{sym.lower()}_1d.parquet"
if not p.exists():
continue
d = pd.read_parquet(p)
cols[sym] = pd.Series(d["close"].values.astype(float),
index=pd.to_datetime(d["timestamp"], unit="ms", utc=True))
C = pd.concat(cols, axis=1, join="inner").sort_index().dropna()
ret = C.pct_change()
disp = ret.std(axis=1) # dispersione cross-section per giorno
disp.index = disp.index.normalize()
return disp.reindex(index.normalize()).values
# ----------------------------------------------------------------------------- weight helpers
def _renorm_rows(W: np.ndarray, active: np.ndarray, expo: np.ndarray | None = None) -> np.ndarray:
"""Maschera inattivi -> 0, rinormalizza ogni riga alla esposizione `expo` (default 1)."""
Wm = W * active
rs = Wm.sum(axis=1, keepdims=True)
out = np.divide(Wm, rs, out=np.zeros_like(Wm), where=rs > 0)
if expo is not None:
out = out * expo[:, None]
return out
def base_weights(R, active, fixed_w) -> np.ndarray:
"""Pesi FISSI rinormalizzati per-riga sugli sleeve attivi (replica combined_daily)."""
n, A = R.shape
return _renorm_rows(np.tile(fixed_w, (n, 1)), active)
def add_cash(W: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
"""Appende una colonna CASH (rendimento 0) che assorbe 1 - somma-pesi (per schemi che de-levano).
Ritorna (W_aug, is_cash_active=True)."""
cash = np.clip(1.0 - W.sum(axis=1, keepdims=True), 0.0, 1.0)
return np.hstack([W, cash])
# ----------------------------------------------------------------------------- engine
def simulate(R: np.ndarray, active: np.ndarray, Wtgt: np.ndarray,
period_days: int = REBAL_DAYS, cost_rate: float = COST_RATE) -> dict:
"""Motore di ribilancio PERIODICO realistico, CAUSALE.
Wtgt[t] = pesi-bersaglio decisi con dati <= t-1 (vedi costruttori schemi), una colonna CASH in
coda (rend. 0). Fra un ribilancio e l'altro i pesi DERIVANO col rendimento; ogni `period_days`
si torna al target pagando cost_rate*|v-target|. Il costo grava sul rendimento del giorno.
period_days=1, cost=0 -> rebalance-continuo (= combined_daily)."""
n = R.shape[0]
Raug = np.hstack([R, np.zeros((n, 1))]) # colonna cash
v = Wtgt[0].copy() # equity iniziale = 1.0, allocata al target
out = np.zeros(n)
turn_tot = 0.0
n_rebal = 0
for t in range(n):
E_start = float(v.sum())
if t > 0 and (t % period_days == 0) and E_start > 0:
target = Wtgt[t] * E_start
turn = float(np.abs(v - target).sum())
v = Wtgt[t] * (E_start - cost_rate * turn)
turn_tot += turn / E_start
n_rebal += 1
v = v * (1.0 + Raug[t])
E_end = float(v.sum())
out[t] = E_end / E_start - 1.0 if E_start > 0 else 0.0
years = n / DAYS_PER_YEAR
return dict(daily=pd.Series(out),
turnover_per_year=turn_tot / years if years > 0 else 0.0,
n_rebalances=n_rebal)
# ----------------------------------------------------------------------------- schemes (causal Wtgt builders, with cash col)
def scheme_base(index, R, active, fixed_w, **_):
return add_cash(base_weights(R, active, fixed_w))
def _rolling_vol(R, active, win):
"""Vol realizzata rolling per-sleeve, SHIFTATA di 1 (causale: usa <= t-1)."""
df = pd.DataFrame(np.where(active, R, np.nan))
vol = df.rolling(win, min_periods=max(10, win // 2)).std().shift(1).values
return vol
def scheme_volpar_pure(index, R, active, fixed_w, win=VOL_WIN, **_):
"""Risk-parity puro: w_i ∝ 1/vol_i sugli sleeve attivi (causale). Warm-up -> BASE."""
vol = _rolling_vol(R, active, win)
inv = np.divide(1.0, vol, out=np.zeros_like(vol), where=(vol > 0) & np.isfinite(vol))
W = _renorm_rows(inv, active & np.isfinite(vol) & (vol > 0))
bw = base_weights(R, active, fixed_w)
bad = W.sum(axis=1) <= 0
W[bad] = bw[bad]
W[:WARMUP] = bw[:WARMUP]
return add_cash(W)
def scheme_volpar_tilt(index, R, active, fixed_w, win=VOL_WIN, **_):
"""Tilt dei pesi FISSI per inverso-vol: w_i ∝ fixed_i / vol_i (ancorato ai pesi fissi)."""
vol = _rolling_vol(R, active, win)
inv = np.divide(1.0, vol, out=np.zeros_like(vol), where=(vol > 0) & np.isfinite(vol))
W = _renorm_rows(fixed_w[None, :] * inv, active & np.isfinite(vol) & (vol > 0))
bw = base_weights(R, active, fixed_w)
bad = W.sum(axis=1) <= 0
W[bad] = bw[bad]
W[:WARMUP] = bw[:WARMUP]
return add_cash(W)
def scheme_momentum(index, R, active, fixed_w, win=MOM_WIN, tilt=0.5, cap=0.55, **_):
"""Momentum-of-sleeves: tilt dei pesi fissi per lo Sharpe rolling z-scored (causale), capato.
w_i fixed_i * (1 + tilt*z_i)+, z = standardizzazione cross-sleeve dello Sharpe rolling.
Cap per non concentrare. Warm-up / regime piatto -> BASE."""
df = pd.DataFrame(np.where(active, R, np.nan))
mu = df.rolling(win, min_periods=win // 2).mean().shift(1).values
sd = df.rolling(win, min_periods=win // 2).std().shift(1).values
sh = np.divide(mu, sd, out=np.full_like(mu, np.nan), where=(sd > 0)) * np.sqrt(DAYS_PER_YEAR)
n, A = R.shape
W = np.zeros((n, A))
bw = base_weights(R, active, fixed_w)
for t in range(n):
m = active[t] & np.isfinite(sh[t])
if m.sum() < 2 or t < WARMUP:
W[t] = bw[t]; continue
z = np.zeros(A); s = sh[t][m]
zsd = s.std()
if zsd > 0:
z[m] = (sh[t][m] - s.mean()) / zsd
raw = fixed_w * np.clip(1.0 + tilt * z, 0.0, None) * m
if raw.sum() <= 0:
W[t] = bw[t]; continue
w = raw / raw.sum()
for _ in range(3): # impone il cap iterando
over = w > cap
if not over.any():
break
excess = (w[over] - cap).sum()
w[over] = cap
room = m & ~over
if room.sum() == 0 or w[room].sum() == 0:
break
w[room] += excess * w[room] / w[room].sum()
W[t] = w / w.sum()
return add_cash(W)
def scheme_dispersion(index, R, active, fixed_w, pct=60, minhist=120, boost=2.0, **_):
"""Dispersion-regime: quando la dispersione cross-section degli alt supera il percentile
ESPANDENTE causale (pct), boost del peso XS01; sotto, XS01 -> 0 e redistribuito. Pesi fissi
altrove. XS01 attivo solo dal 2024 (prima: BASE)."""
disp = dispersion_series(index)
n, A = R.shape
names_idx = 1 # XS01 e' la colonna 1 (vedi active_sleeves)
bw = base_weights(R, active, fixed_w)
W = bw.copy()
hist = []
high = np.zeros(n, bool)
for t in range(n):
d = disp[t - 1] if t > 0 else np.nan # causale: dispersione <= t-1
if np.isfinite(d):
thr = np.percentile(hist, pct) if len(hist) >= minhist else np.inf
high[t] = d >= thr
hist.append(d)
for t in range(n):
if t < WARMUP or not active[t, names_idx]:
continue
raw = fixed_w.copy()
raw[names_idx] *= boost if high[t] else 0.05 # boost XS in regime disperso, quasi-spento altrove
W[t] = _renorm_rows(raw[None, :], active[t][None, :])[0]
return add_cash(W)
def scheme_dd_cash(index, R, active, fixed_w, dd_thr=0.05, floor=0.5, win=0, **_):
"""Drawdown-control (DE-LEVERING esplicito): traccia l'equity di BASE (causale, shiftata),
se il drawdown corrente > dd_thr riduce l'esposizione aggregata a `floor` (resto in CASH).
E' il caso-test del de-levering: ci aspettiamo DD piu' basso ma Sharpe NON piu' alto."""
bw = base_weights(R, active, fixed_w)
base_daily = simulate(R, active, add_cash(bw))["daily"].values
eq = np.cumprod(1.0 + base_daily)
pk = np.maximum.accumulate(eq)
dd = (pk - eq) / pk # drawdown realizzato
expo = np.ones(R.shape[0])
for t in range(R.shape[0]):
d = dd[t - 1] if t > 0 else 0.0 # causale
expo[t] = floor if d > dd_thr else 1.0
expo[:WARMUP] = 1.0
W = bw * expo[:, None]
return add_cash(W)
def scheme_dd_defensive(index, R, active, fixed_w, dd_thr=0.05, **_):
"""Drawdown-control DIFENSIVO: in drawdown ribilancia verso VRP01(2)/SKH01(3) (scorrelati),
via TP01(0)/XS01(1). Pienamente investito (no cash) -> isola il timing dal de-levering."""
bw = base_weights(R, active, fixed_w)
base_daily = simulate(R, active, add_cash(bw))["daily"].values
eq = np.cumprod(1.0 + base_daily)
pk = np.maximum.accumulate(eq)
dd = (pk - eq) / pk
n, A = R.shape
defensive = np.array([0.10, 0.10, 0.35, 0.45]) # VRP/SKH pesati in DD
W = bw.copy()
for t in range(n):
d = dd[t - 1] if t > 0 else 0.0
if t >= WARMUP and d > dd_thr:
W[t] = _renorm_rows(defensive[None, :], active[t][None, :])[0]
return add_cash(W)
SCHEMES = [
("BASE (pesi fissi)", scheme_base),
("VOLPAR pure (1/vol)", scheme_volpar_pure),
("VOLPAR tilt (fix/vol)", scheme_volpar_tilt),
("MOMENTUM-of-sleeves", scheme_momentum),
("DISPERSION-regime->XS", scheme_dispersion),
("DRAWDOWN-ctrl (cash)", scheme_dd_cash),
("DRAWDOWN-ctrl (defens.)", scheme_dd_defensive),
]
CUTS = ["2022-01-01", "2023-01-01", "2024-01-01", "2025-01-01"]
# ----------------------------------------------------------------------------- run
def run():
index, R, active, names, fixed_w = sleeve_matrix()
print("=" * 100)
print(" META-ALLOCATION — allocazione dinamica causale tra i 4 sleeve vs PESI FISSI")
print(f" sleeve: {names}")
print(f" pesi fissi: {dict(zip(names, np.round(fixed_w, 4)))}")
print(f" finestra {index.min().date()} -> {index.max().date()} | n={len(index)} giorni | "
f"hold-out {HOLDOUT.date()}+ | ribilancio {REBAL_DAYS}g | costo {COST_RATE*1e4:.0f}bps/lato")
print("=" * 100)
results = {}
for label, fn in SCHEMES:
Wtgt = fn(index, R, active, fixed_w)
sim = simulate(R, active, Wtgt)
d = pd.Series(sim["daily"].values, index=index)
results[label] = dict(daily=d, turnover=sim["turnover_per_year"], W=Wtgt)
base_d = results["BASE (pesi fissi)"]["daily"]
mb_full = metrics(base_d)
mb_hold = metrics(base_d[base_d.index >= HOLDOUT])
print(f"\n {'SCHEMA':<26s} | {'FULL Sh':>7s} {'CAGR':>7s} {'DD':>6s} | {'HOLD Sh':>7s} {'HOLD ret':>8s} {'DD':>6s} | turn/y")
print(" " + "-" * 96)
for label, _ in SCHEMES:
d = results[label]["daily"]
mf = metrics(d); mh = metrics(d[d.index >= HOLDOUT])
print(f" {label:<26s} | {mf['sharpe']:>7.2f} {mf['cagr']*100:>6.1f}% {mf['maxdd']*100:>5.1f}% | "
f"{mh['sharpe']:>7.2f} {mh['ret']*100:>+7.1f}% {mh['maxdd']*100:>5.1f}% | {results[label]['turnover']:>5.2f}")
print(f"\n delta vs BASE (FULL Sh {mb_full['sharpe']:.2f} / HOLD Sh {mb_hold['sharpe']:.2f}):")
print(f" {'SCHEMA':<26s} | {'ΔFULL Sh':>9s} {'ΔHOLD Sh':>9s} {'ΔFULL DD':>9s} {'ΔHOLD DD':>9s} | corr(BASE)")
print(" " + "-" * 96)
for label, _ in SCHEMES:
if label.startswith("BASE"):
continue
d = results[label]["daily"]
mf = metrics(d); mh = metrics(d[d.index >= HOLDOUT])
corr = float(np.corrcoef(d.values, base_d.values)[0, 1])
print(f" {label:<26s} | {mf['sharpe']-mb_full['sharpe']:>+9.2f} {mh['sharpe']-mb_hold['sharpe']:>+9.2f} "
f"{(mf['maxdd']-mb_full['maxdd'])*100:>+8.1f}% {(mh['maxdd']-mb_hold['maxdd'])*100:>+8.1f}% | {corr:>6.3f}")
# ---- MULTI-CUT: uplift Sharpe a piu' date di taglio (anti-overfit hold-out singolo) ----
print("\n MULTI-CUT — ΔSharpe (schema BASE) su finestre [cut, fine]:")
header = " " + f"{'SCHEMA':<26s} | " + " ".join(f"{c[:4]:>7s}" for c in CUTS)
print(header); print(" " + "-" * (len(header) - 2))
for label, _ in SCHEMES:
if label.startswith("BASE"):
continue
d = results[label]["daily"]
row = []
for c in CUTS:
lo = pd.Timestamp(c, tz="UTC")
sd = metrics(d[d.index >= lo])["sharpe"]
sb = metrics(base_d[base_d.index >= lo])["sharpe"]
row.append(f"{sd-sb:>+7.2f}")
print(f" {label:<26s} | " + " ".join(row))
# ---- DE-LEVERING check: BASE de-levered alla vol dello schema -> stesso DD? ----
print("\n DE-LEVERING check (Sharpe e' scale-invariant: DD-piu'-basso a pari-Sharpe = solo de-lever):")
print(f" {'SCHEMA':<26s} | {'vol/volBASE':>11s} | {'DD schema':>9s} {'DD BASE@volSchema':>18s}")
print(" " + "-" * 70)
vol_base = base_d.std()
dd_base = mb_full["maxdd"]
for label, _ in SCHEMES:
if label.startswith("BASE"):
continue
d = results[label]["daily"]
ratio = d.std() / vol_base if vol_base > 0 else 1.0
# BASE riscalato alla stessa vol dello schema -> il suo DD a quella leva
dd_base_scaled = metrics(base_d * ratio)["maxdd"]
print(f" {label:<26s} | {ratio:>11.3f} | {metrics(d)['maxdd']*100:>8.1f}% {dd_base_scaled*100:>17.1f}%")
# ---- PER-ANNO dei due piu' interessanti vs BASE ----
print("\n PER-ANNO ret% (BASE vs schemi):")
yb = yearly(base_d)
yrs = sorted(yb.keys())
print(" " + f"{'SCHEMA':<26s} | " + " ".join(f"{y:>7d}" for y in yrs))
print(" " + "-" * (28 + 8 * len(yrs)))
for label, _ in SCHEMES:
d = results[label]["daily"]; yd = yearly(d)
print(f" {label:<26s} | " + " ".join(f"{yd.get(y,{'ret':0})['ret']*100:>+6.1f}%" for y in yrs))
# ---- VERDETTI ----
print("\n VERDETTI (BATTE-FISSO richiede ΔHOLD Sh > +0.10 E multi-cut maggioritario positivo E"
" non solo de-levering):")
vol_base = base_d.std()
for label, _ in SCHEMES:
if label.startswith("BASE"):
continue
d = results[label]["daily"]
mf = metrics(d); mh = metrics(d[d.index >= HOLDOUT])
dfull = mf["sharpe"] - mb_full["sharpe"]
dhold = mh["sharpe"] - mb_hold["sharpe"]
cut_ups = []
for c in CUTS:
lo = pd.Timestamp(c, tz="UTC")
cut_ups.append(metrics(d[d.index >= lo])["sharpe"] - metrics(base_d[base_d.index >= lo])["sharpe"])
n_pos = sum(1 for x in cut_ups if x > 0.02)
vr = d.std() / vol_base if vol_base > 0 else 1.0
dd_lower = mf["maxdd"] < mb_full["maxdd"] - 0.005
is_delever = (vr < 0.97) and dd_lower and (dfull <= 0.03) # vol giu', DD giu', Sharpe non meglio
if dhold > 0.10 and dfull > -0.05 and n_pos >= 3:
verdict, why = "BATTE-FISSO", f"ΔHOLD {dhold:+.2f}, multi-cut {n_pos}/4 positivi, FULL non peggiore"
elif (dhold <= -0.10) or (n_pos == 0 and dfull < -0.07):
verdict, why = "SCARTATO", f"peggio OOS (ΔFULL {dfull:+.2f}, ΔHOLD {dhold:+.2f}, multi-cut {n_pos}/4 con turn/y {results[label]['turnover']:.1f})"
elif is_delever:
verdict, why = "solo-de-levering", f"vol {vr:.2f}×BASE, DD {mf['maxdd']*100:.1f}%<{mb_full['maxdd']*100:.1f}% ma Sharpe non meglio (ΔFULL {dfull:+.2f}) -> replicabile abbassando la leva"
else:
why = (f"≈BASE OOS (ΔHOLD {dhold:+.2f}); FULL ΔSh {dfull:+.2f}, ΔDD {(mf['maxdd']-mb_full['maxdd'])*100:+.1f}%"
+ (" [marginale in-sample, nullo su hold-out]" if abs(dfull) >= 0.03 else ""))
verdict = "RIDONDANTE"
print(f" {label:<26s} -> {verdict:<16s} {why}")
print("\n" + "=" * 100)
print(" CONCLUSIONE: vedi i verdetti sopra. Soglia BATTE-FISSO deliberatamente alta (anti-overfit):")
print(" l'allocazione dinamica deve battere i pesi fissi su HOLD-OUT *e* multi-cut, non su una")
print(" finestra fortunata, e non per solo de-levering (replicabile abbassando target_vol/leva).")
print("=" * 100)
if __name__ == "__main__":
run()
+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()
+604
View File
@@ -0,0 +1,604 @@
"""orthogonal_signals.py — SIGNAL-BASED, BY-CONSTRUCTION-ORTHOGONAL streams (2026-06-29).
TESI (richiesta utente). Il book attivo è TP01 (trend) + XS01 (cross-sectional) + VRP01
(short-vol) + SKH01 (regime L/S). Tutti hanno beta direzionale o vol crypto. Cerchiamo uno
stream a **beta di mercato ~0 e bassa corr al book** ma con un **EDGE DI SEGNALE reale**
NON la "diversification math" di uno stream a Sharpe~0 (il marginal scorer indurito la boccia:
serve has_insample_edge, Sharpe standalone PRE-2025 >= 0.5, deflated-Sharpe sull'intera griglia,
e selezione della cella IN-SAMPLE, mai sul max hold-out).
FOCUS PRIMARIO RELATIVE-VALUE ETH/BTC (dollar-neutral, 2 gambe). La posizione è sul SPREAD
long-ETH/short-BTC (o viceversa): r_spread[i] = pos[i-1]*(r_eth[i]-r_btc[i]) - fee*2*|Δpos|.
Per costruzione beta_mercato ~0 -> scorrelato a TP01/SKH. VANTAGGIO: un book a 2 gambe su
Deribit perp (BTC+ETH entrambi live) è MOLTO più vicino all'eseguibile a $600 di uno a 19 gambe
(XS01 è STAT-MODE). Segnali sul ratio log(ETH/BTC), tutti CAUSALI (decisione <= close[i]):
1. RATIO-MOM momentum del ratio (trend dello spread).
2. RATIO-REV reversal di breve del ratio.
3. RATIO-ACCEL accelerazione (2a differenza / curvatura) del ratio.
4. VOLSPREAD vol realizzata relativa BTC vs ETH -> verso l'asset col profilo giusto.
5. DVOLSPREAD vol IMPLICITA relativa (al.dvol BTC/ETH) -> re-valida l'ex-lead 'dvol_spread'.
6. STATARB-RESID residuo di ETH dopo beta*BTC (rolling OLS causale) -> mean-revert il residuo.
SECONDARIO CRYPTO vs MACRO (GLD/QQQ/TLT): long/short crypto vs hedge su momentum relativo,
merge_asof backward (equity 5gg/sett vs crypto 24/7), niente look-ahead. Probabile debole.
GATE (tutti obbligatori, replicano altlib indurito):
* CAUSALITÀ: prefix-check sul SPREAD (ricostruisci su prefisso, la coda deve combaciare).
* NETTO fee 0.10% RT su 2 gambe + SWEEP (0.00-0.30% RT).
* SELEZIONE CELLA IN-SAMPLE-ONLY (pre-2025), MAI sul max hold-out (punto cieco filone B).
* DEFLATED-SHARPE su TUTTE le celle cercate (multiple-testing).
* has_insample_edge: Sharpe standalone PRE-2025 >= 0.5 (no diversification-math).
* OOS hold-out 2025+, plateau su griglia, per-anno.
* corr vs BOOK 4-sleeve (|corr|<0.2 ideale) + beta vs mercato (50/50 BTC+ETH) ~0.
* marginal_vs_tp01 (ADDS / HEDGE / NOISE / NEUTRAL) -> earns_slot_honest.
* EXEC $600: haircut a 2 gambe (min $5/gamba, fee 0.10% RT) il punto FORTE del filone.
ONESTÀ BRUTALE: bassa-corr da sola NON basta. dvol_spread era forward-monitor (storia DVOL
corta + multiple-testing); ratio_accel era lead debole. Se è diversification-math o
hold-out-fitting -> NOISE/SCARTATO con numeri.
Esecuzione: cd /opt/docker/PythagorasGoal && uv run python scripts/research/orthogonal_signals.py
Idempotente, solo stdout.
"""
from __future__ import annotations
import sys
import math
import warnings
from pathlib import Path
import numpy as np
import pandas as pd
warnings.filterwarnings("ignore")
_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(_ROOT / "scripts" / "research" / "alt"))
sys.path.insert(0, str(_ROOT))
import altlib as al # noqa: E402
FEE_SIDE = al.FEE_SIDE # 0.0005 = 0.05%/side
FEE_SWEEP = (0.0, 0.00025, 0.0005, 0.001, 0.0015) # per-side; ×2 legs ×2 (RT) for RT%
HOLDOUT = al.HOLDOUT
ANN = 365.25
# ===========================================================================
# JOINT FRAME + DOLLAR-NEUTRAL EVALUATOR (custom — eval_weights is single-asset)
# ===========================================================================
def build_joint(tf: str = "1d") -> pd.DataFrame:
"""BTC/ETH allineati sull'indice comune (inner join su timestamp). Ritorna un frame con
r_btc/r_eth (simple), close_*, log_ratio = log(ETH/BTC), datetime, timestamp."""
b = al.get("BTC", tf)[["timestamp", "datetime", "close"]].rename(columns={"close": "cb"})
e = al.get("ETH", tf)[["timestamp", "close"]].rename(columns={"close": "ce"})
j = b.merge(e, on="timestamp", how="inner").sort_values("timestamp").reset_index(drop=True)
j["r_btc"] = al.simple_returns(j["cb"].values)
j["r_eth"] = al.simple_returns(j["ce"].values)
j["log_ratio"] = np.log(j["ce"].values / j["cb"].values)
return j
def spread_ret(j: pd.DataFrame) -> np.ndarray:
"""Ritorno dollar-neutral per unità di gross-per-gamba: long $1 ETH, short $1 BTC."""
return j["r_eth"].values - j["r_btc"].values
def vol_target_spread(direction: np.ndarray, j: pd.DataFrame, target_vol: float = 0.20,
win_days: int = 30, cap: float = 2.0) -> np.ndarray:
"""Scala una direzione in [-1,1] a vol-target sullo SPREAD (causale: vol realizzata <= i)."""
s = spread_ret(j)
bpd = al.bars_per_day(j)
bpy = bpd * ANN
vol = al.realized_vol(s, max(2, win_days * bpd), bpy)
scal = np.where((vol > 0) & np.isfinite(vol), target_vol / vol, 0.0)
pos = np.clip(np.nan_to_num(direction) * scal, -cap, cap)
pos[~np.isfinite(pos)] = 0.0
return pos
def eval_spread(j: pd.DataFrame, pos: np.ndarray, fee_side: float = FEE_SIDE) -> dict:
"""Backtest ONESTO del SPREAD. pos[i] decisa <= close[i], TENUTA durante la barra i+1
(lo shift è qui -> niente leak). Fee su 2 GAMBE: ogni Δpos muove ETH e BTC -> 2×|Δpos|."""
pos = np.nan_to_num(np.asarray(pos, float))
s = spread_ret(j)
held = np.zeros(len(pos)); held[1:] = pos[:-1]
gross = held * s
turn = np.abs(np.diff(held, prepend=0.0)) # turnover per-gamba
net = gross - fee_side * 2.0 * turn # 2 gambe
net[0] = 0.0
idx = pd.DatetimeIndex(pd.to_datetime(j["datetime"], utc=True))
full = al._metrics_from_net(net, idx)
hmask = idx >= HOLDOUT
hold = al._metrics_from_net(net[hmask], idx[hmask]) if hmask.sum() > 3 else dict(sharpe=0.0, n=0)
return dict(full=full, holdout=hold, yearly=al._yearly(net, idx),
tim=round(float(np.mean(held != 0)), 3),
turnover=round(float(turn.sum() / (len(turn) / (al.bars_per_day(j) * ANN))), 1),
net=net, idx=idx)
def spread_daily(j: pd.DataFrame, pos: np.ndarray, fee_side: float = FEE_SIDE) -> pd.Series:
"""Serie NET giornaliera del candidato spread (compounded a 1d se TF sub-daily)."""
ev = eval_spread(j, pos, fee_side=fee_side)
s = pd.Series(ev["net"], index=ev["idx"])
return al._to_daily(s)
def eval_spread_smallcap(j: pd.DataFrame, pos: np.ndarray, capital: float = 600.0,
min_order: float = 5.0, fee_side: float = FEE_SIDE) -> dict:
"""Net REALISTICO a $600 su 2 GAMBE. Un Δpos il cui nozionale PER-GAMBA |Δpos|*capital < $5
NON si esegue (held). Le due gambe cambiano dello stesso |Δ| -> il vincolo binding è
|Δpos|*capital >= min_order. Riporta Sharpe modellato vs realistico + haircut + n trade."""
pos = np.clip(np.nan_to_num(np.asarray(pos, float)), -10, 10)
held = np.empty(len(pos)); cur = 0.0; n_tr = 0
for i in range(len(pos)):
if abs(pos[i] - cur) * capital >= min_order:
cur = pos[i]; n_tr += 1
held[i] = cur
s = spread_ret(j)
p = np.zeros(len(held)); p[1:] = held[:-1]
turn = np.abs(np.diff(p, prepend=0.0))
net = p * s - fee_side * 2.0 * turn; net[0] = 0.0
idx = pd.DatetimeIndex(pd.to_datetime(j["datetime"], utc=True))
real = al._metrics_from_net(net, idx)
modeled = eval_spread(j, pos, fee_side=fee_side)["full"]
return dict(realistic=real, modeled=modeled,
sharpe_haircut=round(modeled["sharpe"] - real["sharpe"], 3),
n_executed_trades=int(n_tr))
# ===========================================================================
# CAUSALITY — prefix-check sul SPREAD (la coda del prefisso deve combaciare col full).
# ===========================================================================
def causality_spread(make_pos, tf: str = "1d", tail: int = 80, tol: float = 1e-6) -> dict:
"""make_pos(j) -> pos sull'intero frame. Ricostruisce su prefissi; la coda deve combaciare."""
j = build_joint(tf)
full = np.nan_to_num(make_pos(j))
n = len(j)
worst = 0.0; bad = False; checked = 0
for cut in (int(n * 0.80), int(n * 0.92)):
if cut <= tail + 5 or cut >= n:
continue
sub = j.iloc[:cut].reset_index(drop=True)
s = np.nan_to_num(make_pos(sub))
if len(s) != cut:
bad = True; continue
d = np.abs(s[cut - tail:cut] - full[cut - tail:cut])
worst = max(worst, float(np.max(d)) if len(d) else 0.0)
checked += 1
return dict(ok=bool((not bad) and worst <= tol), max_tail_diff=round(worst, 10), checked=checked)
# ===========================================================================
# SIGNAL FACTORIES — factory(tf, **p) -> make_pos(j) -> vol-targeted position on the spread
# (direzione in [-1,1] internamente, poi vol_target_spread). 'sgn' (+/-1) testa il verso.
# ===========================================================================
def f_ratio_mom(tf="1d", L=30, sgn=1, tv=0.20, vw=30, cap=2.0):
def make(j):
lr = j["log_ratio"].values
mom = lr - pd.Series(lr).shift(L).values # momentum L-barre del ratio
z = al.zscore(mom, max(20, L))
d = sgn * np.tanh(np.nan_to_num(z))
return vol_target_spread(d, j, tv, vw, cap)
return make
def f_ratio_rev(tf="1d", L=5, sgn=-1, tv=0.20, vw=30, cap=2.0):
def make(j):
lr = j["log_ratio"].values
z = al.zscore(lr, L) # deviazione di breve dal trend locale
d = sgn * np.tanh(np.nan_to_num(z))
return vol_target_spread(d, j, tv, vw, cap)
return make
def f_ratio_accel(tf="1d", L=20, sgn=-1, tv=0.20, vw=30, cap=2.0):
def make(j):
lr = pd.Series(j["log_ratio"].values)
accel = lr - 2 * lr.shift(L) + lr.shift(2 * L) # 2a differenza (curvatura)
z = al.zscore(np.nan_to_num(accel.values), max(20, L))
d = sgn * np.tanh(np.nan_to_num(z))
return vol_target_spread(d, j, tv, vw, cap)
return make
def f_volspread(tf="1d", W=30, sgn=1, tv=0.20, vw=30, cap=2.0):
def make(j):
bpd = al.bars_per_day(j); bpy = bpd * ANN
vb = al.realized_vol(j["r_btc"].values, max(2, W * bpd), bpy)
ve = al.realized_vol(j["r_eth"].values, max(2, W * bpd), bpy)
z = al.zscore(np.nan_to_num(vb - ve), max(30, W)) # BTC più volatile di ETH?
d = sgn * np.tanh(np.nan_to_num(z))
return vol_target_spread(d, j, tv, vw, cap)
return make
def f_dvolspread(tf="1d", W=30, sgn=1, tv=0.20, vw=30, cap=2.0):
def make(j):
db = al.dvol(j, "BTC"); de = al.dvol(j, "ETH") # vol IMPLICITA causale (merge_asof)
sp = np.nan_to_num(db - de)
z = al.zscore(sp, max(30, W))
d = sgn * np.tanh(np.nan_to_num(z))
return vol_target_spread(d, j, tv, vw, cap)
return make
def f_statarb_resid(tf="1d", W=60, sgn=-1, tv=0.20, vw=30, cap=2.0):
def make(j):
x = np.log(j["cb"].values); y = np.log(j["ce"].values) # OLS rolling causale y~a+b x
sx = pd.Series(x); sy = pd.Series(y)
mx = sx.rolling(W, min_periods=W).mean()
my = sy.rolling(W, min_periods=W).mean()
cov = (sx * sy).rolling(W, min_periods=W).mean() - mx * my
var = (sx * sx).rolling(W, min_periods=W).mean() - mx * mx
beta = (cov / var.replace(0, np.nan))
resid = (sy - (my - beta * mx) - beta * sx).values # resid al tempo i (usa <= i)
z = al.zscore(np.nan_to_num(resid), W)
d = sgn * np.tanh(np.nan_to_num(z)) # mean-revert il residuo
return vol_target_spread(d, j, tv, vw, cap)
return make
FAMILIES = {
"RATIO-MOM": (f_ratio_mom, [dict(L=L, sgn=s) for L in (15, 30, 45, 60, 90) for s in (1, -1)], ("1d",)),
"RATIO-REV": (f_ratio_rev, [dict(L=L, sgn=s) for L in (3, 5, 8, 12, 20) for s in (1, -1)], ("1d",)),
"RATIO-ACCEL": (f_ratio_accel, [dict(L=L, sgn=s) for L in (10, 20, 30, 45) for s in (1, -1)], ("1d",)),
"VOLSPREAD": (f_volspread, [dict(W=W, sgn=s) for W in (10, 20, 30, 60) for s in (1, -1)], ("1d",)),
"DVOLSPREAD": (f_dvolspread, [dict(W=W, sgn=s) for W in (15, 30, 45, 60) for s in (1, -1)], ("1d",)),
"STATARB-RESID": (f_statarb_resid, [dict(W=W, sgn=s) for W in (30, 45, 60, 90, 120) for s in (1, -1)], ("1d",)),
}
# ===========================================================================
# BOOK + MARKET references (for corr / beta)
# ===========================================================================
def book_daily() -> pd.Series:
"""Serie NET giornaliera del BOOK attivo a 4 sleeve (TP01+XS01+VRP01+SKH01)."""
from src.portfolio.sleeves import active_sleeves
from src.portfolio.portfolio import StrategyPortfolio
pf = StrategyPortfolio(active_sleeves()); pf.backtest()
s = pf.combined_daily()
if not isinstance(s.index, pd.DatetimeIndex):
s.index = pd.to_datetime(s.index, utc=True)
if s.index.tz is None:
s.index = s.index.tz_localize("UTC")
return s.dropna()
def market_daily() -> pd.Series:
"""Mercato di riferimento: 50/50 BTC+ETH ritorni semplici giornalieri (per beta ~0)."""
series = {}
for a in ("BTC", "ETH"):
df = al.get(a, "1d")
series[a] = pd.Series(al.simple_returns(df["close"].values),
index=pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)))
J = pd.concat(series, axis=1, join="inner").fillna(0.0)
return (0.5 * J["BTC"] + 0.5 * J["ETH"]).dropna()
def beta_to(cand: pd.Series, ref: pd.Series) -> tuple[float, float]:
J = pd.concat({"c": cand, "r": ref}, axis=1, join="inner").dropna()
if len(J) < 30 or J["r"].var() == 0:
return float("nan"), float("nan")
c, r = J["c"].values, J["r"].values
beta = float(np.cov(c, r)[0, 1] / np.var(r))
return round(beta, 3), round(float(J["c"].corr(J["r"])), 3)
# ===========================================================================
# HONEST FAMILY STUDY for the SPREAD (replica study_family_honest a mano):
# (1) cella scelta su Sharpe IN-SAMPLE (pre-HOLDOUT), MAI sul max hold-out;
# (2) deflated-Sharpe su TUTTE le celle; (3) marginal_vs_tp01 sulla cella scelta.
# ===========================================================================
def _sh(s) -> float:
return al._sh(s)
def study_spread_family(name, factory, grid, tfs, refs, fee_side=FEE_SIDE, dsr_min=0.95) -> dict:
rows = []
for tf in tfs:
for p in grid:
try:
j = build_joint(tf)
pos = factory(tf=tf, **p)(j)
daily = spread_daily(j, pos, fee_side=fee_side)
except Exception as ex: # pragma: no cover
rows.append(dict(tf=tf, params=p, err=str(ex)[:60])); continue
ins = daily[daily.index < HOLDOUT]
is_sh = _sh(ins) if len(ins) > 60 else float("nan")
rows.append(dict(tf=tf, params=p, daily=daily, insample_sharpe=round(is_sh, 3),
full_sharpe=round(_sh(daily), 3)))
valid = [r for r in rows if r.get("insample_sharpe") is not None
and np.isfinite(r.get("insample_sharpe", np.nan))]
if not valid:
return dict(name=name, chosen=None, earns_slot_honest=False, reason="no valid in-sample cell")
chosen = max(valid, key=lambda r: r["insample_sharpe"])
all_full = [r["full_sharpe"] for r in rows if r.get("full_sharpe") is not None]
j = build_joint(chosen["tf"])
pos = factory(tf=chosen["tf"], **chosen["params"])(j)
daily = chosen["daily"]
ev = eval_spread(j, pos, fee_side=fee_side)
# fee sweep (RT% = per-side ×2 legs? No: RT per leg = 2×side; on 2 legs the cost already
# ×2 in eval_spread. Report per-side grid -> "X%RT/leg".)
sweep = {}
for f in FEE_SWEEP:
sweep[f"{2*f*100:.2f}%RT/leg"] = round(eval_spread(j, pos, fee_side=f)["full"]["sharpe"], 3)
fee_survives = sweep.get(f"{2*0.0015*100:.2f}%RT/leg", -9) > 0
# marginal vs TP01
marg = al.marginal_vs_tp01(daily)
# deflated Sharpe over the WHOLE grid (come da istruzione)
dsr, sr0 = al.deflated_sharpe(_sh(daily), all_full, daily)
dsr_pass = bool(np.isfinite(dsr) and dsr >= dsr_min)
# SENSIBILITÀ: la griglia include sgn=+1 E sgn=-1 (specchi: +Sh e -Sh) -> raddoppia la
# dispersione dei trial e gonfia il null-max. DSR "same-sign" = deflazione sul SOLO verso
# scelto (conta i soli lookback davvero in competizione). È il limite ottimistico onesto.
same = [r["full_sharpe"] for r in rows
if r.get("full_sharpe") is not None and r["params"].get("sgn") == chosen["params"].get("sgn")]
dsr_ss, sr0_ss = al.deflated_sharpe(_sh(daily), same if len(same) >= 2 else all_full, daily)
dsr_ss_pass = bool(np.isfinite(dsr_ss) and dsr_ss >= dsr_min)
# corr/beta vs BOOK & MARKET
bbeta, bcorr = beta_to(daily, refs["book"])
mbeta, mcorr = beta_to(daily, refs["market"])
btcbeta, btccorr = beta_to(daily, refs["btc"])
ethbeta, ethcorr = beta_to(daily, refs["eth"])
# $600 executability (2 legs)
sc = eval_spread_smallcap(j, pos)
earns = bool(marg.get("marginal_verdict") == "ADDS" and marg.get("robust_oos", False)
and marg.get("has_insample_edge", False) and not marg.get("is_hedge", False)
and dsr_pass and fee_survives)
return dict(name=name, n_cells=len(all_full), chosen=chosen, rows=valid,
ev=ev, sweep=sweep, fee_survives=fee_survives, marginal=marg,
deflated_sharpe=round(dsr, 3) if np.isfinite(dsr) else None,
expected_null_max=round(sr0, 3) if np.isfinite(sr0) else None, dsr_pass=dsr_pass,
dsr_samesign=round(dsr_ss, 3) if np.isfinite(dsr_ss) else None,
expected_null_max_ss=round(sr0_ss, 3) if np.isfinite(sr0_ss) else None,
dsr_ss_pass=dsr_ss_pass, n_cells_samesign=len(same),
book=dict(beta=bbeta, corr=bcorr), market=dict(beta=mbeta, corr=mcorr),
btc=dict(beta=btcbeta, corr=btccorr), eth=dict(beta=ethbeta, corr=ethcorr),
smallcap=sc, earns_slot_honest=earns)
def verdict_for(rep: dict) -> tuple[str, str]:
if rep.get("chosen") is None:
return "SCARTATO", "nessuna cella valida"
m = rep["marginal"]
is_edge = m.get("has_insample_edge"); is_sh = m.get("cand_insample_sharpe")
dsr = rep.get("deflated_sharpe"); mv = m.get("marginal_verdict")
ss = rep.get("dsr_samesign"); ss_tag = f"same-sign {ss} {'pass' if rep.get('dsr_ss_pass') else 'fail'}"
if rep["earns_slot_honest"]:
return "SLEEVE-CANDIDATE-eseguibile", "ADDS + robust_oos + edge in-sample + DSR pass + fee/exec ok"
if mv == "DILUTES":
return "SCARTATO", "DILUTES (abbassa il blend del book)"
if m.get("is_hedge"):
return "SCARTATO", "HEDGE (paga solo quando TP01 è debole, non alpha)"
if not is_edge:
return "NOISE", f"no edge in-sample (Sharpe<2025 {is_sh} < 0.5) -> diversification-math"
# ha edge in-sample reale: il discriminante è se MIGLIORA il book (marginal ADDS)
if mv == "ADDS":
if not rep.get("dsr_pass"):
return "LEAD-forward-monitor", (f"ADDS + edge in-sample {is_sh} + executable, MA deflated-Sharpe "
f"full-grid {dsr} < 0.95 ({ss_tag}) -> multiple-testing")
if not m.get("robust_oos"):
return "LEAD-forward-monitor", "ADDS + edge ma non robust_oos (single-window / multi-cut debole)"
return "LEAD-forward-monitor", "ADDS ma blocco fee/exec"
# edge standalone reale ma marginal NEUTRAL/REDUNDANT: NON migliora il book -> niente slot
return "NEUTRAL-standalone", (f"edge in-sample {is_sh} reale ma marginal={mv}: non migliora il book "
f"(corr~0 senza uplift = diversification-math; hold-out debole)")
def print_family(rep: dict):
print("=" * 100)
print(f"### {rep['name']}")
if rep.get("chosen") is None:
print(" SCARTATO:", rep.get("reason")); return
ch = rep["chosen"]; ev = rep["ev"]; m = rep["marginal"]
print(f" best cell (IN-SAMPLE pick): tf={ch['tf']} params={ch['params']} "
f"[cercate {rep['n_cells']} celle]")
print(f" in-sample Sharpe (pre-2025) {ch['insample_sharpe']} | FULL Sharpe {ev['full']['sharpe']} "
f"DD {ev['full']['maxdd']*100:.1f}% ret {ev['full']['ret']*100:+.0f}% | "
f"HOLD Sharpe {ev['holdout'].get('sharpe')} ret {ev['holdout'].get('ret',0)*100:+.0f}%")
print(f" time-in-mkt {ev['tim']} turnover/yr {ev['turnover']}")
print(f" per-anno: " + " ".join(f"{y}:{d['ret']*100:+.0f}%(dd{d['dd']*100:.0f})"
for y, d in ev["yearly"].items()))
print(f" fee sweep (per-leg RT): {rep['sweep']} fee_survives={rep['fee_survives']}")
print(f" corr vs BOOK {rep['book']['corr']} (beta {rep['book']['beta']}) | "
f"beta vs MERCATO(50/50) {rep['market']['beta']} (corr {rep['market']['corr']})")
print(f" corr/beta vs BTC {rep['btc']['corr']}/{rep['btc']['beta']} "
f"vs ETH {rep['eth']['corr']}/{rep['eth']['beta']}")
print(f" MARGINAL vs TP01: verdict={m.get('marginal_verdict')} corr_full={m.get('corr_full')} "
f"corr_hold={m.get('corr_hold')}")
print(f" has_insample_edge={m.get('has_insample_edge')} (standalone Sh<2025 {m.get('cand_insample_sharpe')}) "
f"robust_oos={m.get('robust_oos')} multicut={m.get('multicut_persistent')} {m.get('multicut_uplift')}")
print(f" blend w25: full {m['blends']['w25']['full']} (uplift {m['blends']['w25']['uplift_full']:+.3f}) "
f"hold {m['blends']['w25']['hold']} (uplift {m['blends']['w25']['uplift_hold']}) is_hedge={m.get('is_hedge')}")
print(f" DEFLATED-Sharpe full-grid {rep['deflated_sharpe']} (null-max {rep['expected_null_max']}, "
f"{rep['n_cells']} celle) pass={rep['dsr_pass']} | same-sign {rep['dsr_samesign']} "
f"(null-max {rep['expected_null_max_ss']}, {rep['n_cells_samesign']} celle) pass={rep['dsr_ss_pass']}")
sc = rep["smallcap"]
print(f" EXEC $600 (2 gambe): modeled Sh {sc['modeled']['sharpe']} -> realistic {sc['realistic']['sharpe']} "
f"(haircut {sc['sharpe_haircut']}) trade eseguiti {sc['n_executed_trades']}")
v, why = verdict_for(rep)
print(f" >>> VERDETTO: {v}{why}")
print(f" earns_slot_honest = {rep['earns_slot_honest']}")
# ===========================================================================
# SECONDARIO — CRYPTO vs MACRO (GLD/QQQ/TLT) momentum relativo, merge_asof backward.
# ===========================================================================
def _eq_daily(sym: str) -> pd.Series:
p = _ROOT / "data" / "raw" / f"eq_{sym}_1d.parquet"
d = pd.read_parquet(p).sort_values("timestamp").reset_index(drop=True)
idx = pd.DatetimeIndex(pd.to_datetime(d["timestamp"], unit="ms", utc=True))
return pd.Series(d["close"].values, index=idx)
def study_macro_rv(hedge: str, L: int = 60, sgn: int = 1, fee_side: float = FEE_SIDE,
refs: dict | None = None) -> dict:
"""Long crypto (50/50 BTC+ETH) / short hedge ETF su momentum relativo L-giorni.
Allineamento: calendario dell'EQUITY (trading days); crypto allineata backward (no look-ahead).
Decisione <= close[i] del giorno di trading."""
eqc = _eq_daily(hedge)
# crypto close 50/50 (geometric proxy: media dei log-prezzi normalizzati)
cb = al.get("BTC", "1d"); ce = al.get("ETH", "1d")
cbi = pd.Series(cb["close"].values, index=pd.DatetimeIndex(pd.to_datetime(cb["datetime"], utc=True)))
cei = pd.Series(ce["close"].values, index=pd.DatetimeIndex(pd.to_datetime(ce["datetime"], utc=True)))
cj = pd.concat({"b": cbi, "e": cei}, axis=1, join="inner").dropna()
crypto = np.exp(0.5 * np.log(cj["b"]) + 0.5 * np.log(cj["e"])) # crypto index level
# merge_asof backward: per ogni giorno equity, l'ultimo close crypto <= quel giorno
L_ = pd.DataFrame({"ts": eqc.index, "eq": eqc.values}).sort_values("ts")
R_ = pd.DataFrame({"ts": crypto.index, "cr": crypto.values}).sort_values("ts")
mg = pd.merge_asof(L_, R_, on="ts", direction="backward").dropna()
eqv = mg["eq"].values; crv = mg["cr"].values; idx = pd.DatetimeIndex(mg["ts"])
r_eq = al.simple_returns(eqv); r_cr = al.simple_returns(crv)
s = r_cr - r_eq # long crypto / short hedge
lr = np.log(crv) - np.log(eqv) # log relative level
mom = lr - pd.Series(lr).shift(L).values
z = al.zscore(np.nan_to_num(mom), max(20, L))
d = sgn * np.tanh(np.nan_to_num(z))
# vol target on the macro spread
vol = al.realized_vol(s, 30, ANN)
scal = np.where((vol > 0) & np.isfinite(vol), 0.20 / vol, 0.0)
pos = np.clip(np.nan_to_num(d) * scal, -2.0, 2.0)
held = np.zeros(len(pos)); held[1:] = pos[:-1]
net = held * s - fee_side * 2.0 * np.abs(np.diff(held, prepend=0.0)); net[0] = 0.0
full = al._metrics_from_net(net, idx)
hmask = idx >= HOLDOUT
hold = al._metrics_from_net(net[hmask], idx[hmask]) if hmask.sum() > 3 else dict(sharpe=0.0, n=0)
daily = al._to_daily(pd.Series(net, index=idx))
ins = daily[daily.index < HOLDOUT]
is_sh = round(_sh(ins), 3) if len(ins) > 60 else None
bcorr = beta_to(daily, refs["book"])[1] if refs else None
mbeta, mcorr = beta_to(daily, refs["market"]) if refs else (None, None)
btcb = beta_to(daily, refs["btc"])[0] if refs else None
return dict(hedge=hedge, L=L, sgn=sgn, full=full, holdout=hold,
insample_sharpe=is_sh, book_corr=bcorr, mkt_beta=mbeta, mkt_corr=mcorr,
btc_beta=btcb, yearly=al._yearly(net, idx), daily=daily)
# ===========================================================================
# MAIN
# ===========================================================================
def main():
print("#" * 100)
print("# ORTHOGONAL SIGNALS — RELATIVE-VALUE ETH/BTC (dollar-neutral) + CRYPTO-vs-MACRO")
print("#" * 100)
j = build_joint("1d")
print(f"Joint BTC/ETH 1d: {len(j)} barre {j['datetime'].min().date()} -> {j['datetime'].max().date()} "
f"(hold-out {HOLDOUT.date()}+)")
s = spread_ret(j)
print(f"Spread r_eth-r_btc: vol annua {np.std(s)*math.sqrt(ANN)*100:.0f}% "
f"corr(r_eth,r_btc)={np.corrcoef(j['r_eth'].values[1:], j['r_btc'].values[1:])[0,1]:.2f}")
# references
print("\nCarico riferimenti BOOK / MERCATO ...")
refs = dict(book=book_daily(), market=market_daily())
cb = al.get("BTC", "1d"); ce = al.get("ETH", "1d")
refs["btc"] = pd.Series(al.simple_returns(cb["close"].values),
index=pd.DatetimeIndex(pd.to_datetime(cb["datetime"], utc=True)))
refs["eth"] = pd.Series(al.simple_returns(ce["close"].values),
index=pd.DatetimeIndex(pd.to_datetime(ce["datetime"], utc=True)))
print(f" BOOK 4-sleeve: {len(refs['book'])} giorni, Sharpe {_sh(refs['book']):.2f}")
# causality smoke-test on one cell per family
print("\n--- CAUSALITÀ (prefix-check sullo SPREAD, tol 1e-6) ---")
for name, (fac, grid, tfs) in FAMILIES.items():
p0 = grid[0]
ck = causality_spread(fac(tf=tfs[0], **p0), tf=tfs[0])
print(f" {name:14s} ok={ck['ok']} max_tail_diff={ck['max_tail_diff']} checked={ck['checked']}")
# study each family
print("\n" + "#" * 100)
print("# RELATIVE-VALUE ETH/BTC — selezione cella IN-SAMPLE, deflated-Sharpe su tutta la griglia")
print("#" * 100)
summary = []
for name, (fac, grid, tfs) in FAMILIES.items():
rep = study_spread_family(name, fac, grid, tfs, refs)
print_family(rep)
v, why = verdict_for(rep)
ch = rep.get("chosen")
summary.append(dict(name=name, verdict=v,
cell=(ch["params"] if ch else None),
full=rep["ev"]["full"]["sharpe"] if ch else None,
hold=rep["ev"]["holdout"].get("sharpe") if ch else None,
is_sh=rep["marginal"].get("cand_insample_sharpe") if ch else None,
book_corr=rep["book"]["corr"] if ch else None,
mkt_beta=rep["market"]["beta"] if ch else None,
dsr=rep.get("deflated_sharpe") if ch else None,
marginal=rep["marginal"].get("marginal_verdict") if ch else None,
earns=rep.get("earns_slot_honest") if ch else False,
haircut=rep["smallcap"]["sharpe_haircut"] if ch else None))
# secondary: crypto vs macro
print("\n" + "#" * 100)
print("# SECONDARIO — CRYPTO vs MACRO (relative momentum, merge_asof backward)")
print("#" * 100)
macro_rows = []
for hedge in ("gld", "qqq", "tlt"):
best = None
for L in (30, 60, 90):
for sgn in (1, -1):
r = study_macro_rv(hedge, L=L, sgn=sgn, refs=refs)
if best is None or (r["insample_sharpe"] or -9) > (best["insample_sharpe"] or -9):
best = r
macro_rows.append(best)
print(f" {hedge.upper():4s} bestIS L={best['L']} sgn={best['sgn']}: "
f"FULL Sh {best['full']['sharpe']} DD {best['full']['maxdd']*100:.0f}% "
f"HOLD Sh {best['holdout'].get('sharpe')} in-sample Sh {best['insample_sharpe']} "
f"corr->BOOK {best['book_corr']} beta->MERCATO {best['mkt_beta']} (corr {best['mkt_corr']}) "
f"beta->BTC {best['btc_beta']}")
print(" NOTA: la vol crypto (~47%) domina GLD/TLT/QQQ (~15%) -> r_cr-r_hedge ≈ r_cr: la 'relative"
" momentum' è di fatto MOMENTUM CRYPTO (la gamba hedge è troppo poco volatile per neutralizzare).")
print(" Beta-mercato ~0 solo perché il momentum entra/esce dall'esposizione; ma la corr->BOOK"
" 0.17-0.20 (vs ~0.02 degli spread ETH/BTC) tradisce l'overlap col trend di TP01 -> NON ortogonale.")
# marginal gate onesto sul migliore macro (per smentire il numero di Sharpe tentatore)
bm = max(macro_rows, key=lambda r: (r["insample_sharpe"] or -9))
mg = al.marginal_vs_tp01(bm["daily"])
print(f" MARGINAL gate sul migliore ({bm['hedge'].upper()}): verdict={mg.get('marginal_verdict')} "
f"corr_full={mg.get('corr_full')} blend-uplift w25 full {mg['blends']['w25']['uplift_full']:+.3f} "
f"hold {mg['blends']['w25']['uplift_hold']} robust_oos={mg.get('robust_oos')} "
f"-> {'overlap col trend (no slot)' if mg.get('marginal_verdict') in ('NEUTRAL','REDUNDANT','DILUTES') else 'da gate completo'}")
# final table
print("\n" + "#" * 100)
print("# SINTESI")
print("#" * 100)
print(f"{'segnale':14s} {'verdetto':28s} {'FULL':>5s} {'HOLD':>5s} {'IS-Sh':>5s} "
f"{'corrBK':>6s} {'':>5s} {'DSR':>5s} {'marg':>9s} {'cut':>6s} earns")
for r in summary:
print(f"{r['name']:14s} {r['verdict']:28s} "
f"{str(r['full']):>5s} {str(r['hold']):>5s} {str(r['is_sh']):>5s} "
f"{str(r['book_corr']):>6s} {str(r['mkt_beta']):>5s} {str(r['dsr']):>5s} "
f"{str(r['marginal']):>9s} {str(r['haircut']):>6s} {r['earns']}")
print("\nMACRO (secondario): " + " | ".join(
f"{m['hedge'].upper()} IS={m['insample_sharpe']} HOLD={m['holdout'].get('sharpe')} bkcorr={m['book_corr']}"
for m in macro_rows))
winners = [r for r in summary if r["earns"]]
leads = [r for r in summary if r["verdict"].startswith("LEAD")]
standalone = [r for r in summary if r["verdict"].startswith("NEUTRAL")]
print("\n" + "=" * 100)
print("CONCLUSIONE — c'è uno stream scorrelato CON edge reale ED eseguibile a 2 gambe?")
print("=" * 100)
if winners:
print("SI -> SLEEVE-CANDIDATE eseguibile a 2 gambe:", ", ".join(r["name"] for r in winners))
else:
print("NO sleeve pronto. L'ORTOGONALITÀ c'è (corr->BOOK ~0.02, beta-mercato ~0.01: il filone")
print("relative-value ETH/BTC è scorrelato per costruzione, ed è ESEGUIBILE a $600 — haircut ~0,")
print("fee-surviving a 0.30%RT/gamba). Ma l'EDGE non passa il deflated-Sharpe:")
if leads:
print(" LEAD (forward-monitor, ADDS + edge in-sample ma DSR<0.95):",
", ".join(r["name"] for r in leads))
if standalone:
print(" NEUTRAL-standalone (edge reale, NON migliora il book):",
", ".join(r["name"] for r in standalone))
print("Dettaglio nei verdetti per-segnale sopra.")
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()
+383
View File
@@ -0,0 +1,383 @@
"""r0701_breadth_internals.py — BREADTH / MARKET-INTERNALS del mercato ALT come segnale su BTC/ETH.
TESI (filone 2026-07-01)
------------------------
Gli "internals" del mercato crypto la partecipazione degli alt come segnale direzionale o gate
di de-risk su BTC/ETH perp (2 gambe, eseguibile a ~$600):
FAM-MA : % di alt sopra la propria SMA(N) (breadth classica)
FAM-AD : advance/decline frazione di advancers, SMA(N) (partecipazione giornaliera)
FAM-RS : % di alt che BATTONO BTC sul ritorno a N giorni (risk-appetite relativo, ~mkt-neutral)
FAM-TH : breadth-THRUST delta della breadth MA20 su N giorni (thrust/collapse)
Forme: LS (long/short), LF (long/flat), GATE (TP01 * gate binario). Tutte vol-target 20% cap 2x
(LS/LF) o ereditano il sizing TP01 (GATE).
RISCHI NOTI IN PARTENZA (CLAUDE.md, prior art)
----------------------------------------------
1. MACRO regime-gate (2026-06-29) = SCARTATO: corr->TP01 0.989, il gate lavorava nel 2-3% dei
giorni (TP01 e' gia' flat nei crash). Un breadth-gate rischia di essere LO STESSO artefatto:
TP01 travestito. Qui DOBBIAMO riportare corr->TP01 + verdetto marginale + "giorni in cui il
gate lavora" (gate off E TP01 non gia' flat).
2. trend-multiasset = ridondante (corr 0.74): la breadth degli alt e' correlata alla direzione
del mercato -> il rischio che breadth>soglia == "BTC sopra trend" e' concreto.
3. is_hedge: un segnale che paga solo quando TP01 soffre e' un hedge, non alpha.
4. STORIA: l'universo HL parte dal 2024-01 -> ~2.2 anni utili post-warmup. In-sample (pre-HOLDOUT
2025-01-01) = SOLO ~8 mesi del 2024. Limite strutturale DICHIARATO: qualunque esito e' al
massimo un LEAD, la selezione in-sample poggia su una finestra corta.
METODO (obbligatorio, CLAUDE.md)
--------------------------------
- Dati: 51 parquet certificati data/raw/hl_*_1d.parquet; PANEL = 49 alt (esclusi hl_btc/hl_eth
dalla breadth; hl_btc usato solo come riferimento per FAM-RS). Barre a volume<=0 = sintetiche
-> close mascherato NaN. Breadth definita solo con >= MIN_VALID(20) asset validi alla data.
- Causalita': barre HL 1d e barre BTC/ETH 1d (altlib.get, resample Deribit) sono entrambe
open-labeled 00:00 UTC -> il close del giorno D e' noto allo stesso istante su entrambe.
Allineamento merge_asof backward (allow_exact) sul timestamp; eval_weights shifta la posizione
(decisa a close[i], tenuta in i+1). Verifica al.causality_ok sul target end-to-end.
- Selezione ONESTA (lezione SELECTION-ON-HOLDOUT 2026-06-29): la cella si sceglie con il SOLO
Sharpe in-sample (pre-2025) sul candidato 50/50, MAI sull'hold-out; deflated Sharpe (Bailey &
Lopez de Prado) su TUTTE le celle cercate; poi al.marginal_vs_tp01 (multi-cut, has_insample_edge,
is_hedge) sulla cella scelta. NB: non si usa al.study_family_honest stock perche' la breadth non
esiste pre-2024 e il padding (LS/LF=flat, GATE=TP01 pieno) contaminerebbe il ranking in-sample
full-history (le celle GATE erediterebbero lo Sharpe 2019-2024 di TP01); la procedura qui sotto
e' il MIRROR esatto di select_cell_insample + deflated_sharpe + study_marginal sulla FINESTRA
COMUNE 2024-05+ (stessa libreria, stessi gate).
- Fee 0.10% RT + sweep 0-0.30% RT; eval_weights_smallcap a $600.
USO: uv run python scripts/research/r0701_breadth_internals.py
"""
from __future__ import annotations
import glob
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"))
import altlib as al # noqa: E402
from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio # noqa: E402
RAW = _ROOT / "data" / "raw"
HOLDOUT = al.HOLDOUT
ASSETS = ("BTC", "ETH")
MIN_VALID = 20 # asset validi minimi perche' la breadth esista
START = pd.Timestamp("2026-01-01", tz="UTC") # placeholder, ridefinito sotto
# finestra comune: HL parte 2024-01-01; warmup max = FAM-TH N=100 (20g MA + 100g delta) ~ 120g
START = pd.Timestamp("2024-05-05", tz="UTC")
MA_GRID = (20, 50, 100)
THR_GRID = (0.3, 0.5, 0.7)
FORMS = ("ls", "lf", "gate")
FAMS = ("ma", "ad", "rs", "th")
# ===========================================================================
# PANEL ALT (49 asset, vol=0 mascherato) + riferimento BTC (hl_btc)
# ===========================================================================
def load_panel():
px, vol = {}, {}
for f in sorted(glob.glob(str(RAW / "hl_*_1d.parquet"))):
sym = Path(f).stem.replace("hl_", "").replace("_1d", "").upper()
d = pd.read_parquet(f)
idx = pd.to_datetime(d["timestamp"], unit="ms", utc=True)
px[sym] = pd.Series(d["close"].values.astype(float), index=idx)
vol[sym] = pd.Series(d["volume"].values.astype(float), index=idx)
PX = pd.concat(px, axis=1).sort_index()
VOL = pd.concat(vol, axis=1).sort_index().reindex_like(PX)
PX = PX.mask(VOL <= 0) # barre sintetiche (vol=0) -> NaN (lezione 2026-06-20)
btc_ref = PX["BTC"].copy() # riferimento FAM-RS (stessa venue/stesso close time)
ALTS = PX.drop(columns=["BTC", "ETH"]) # breadth = SOLO alt
return ALTS, btc_ref
def _mask_min_valid(score: pd.Series, n_valid: pd.Series) -> pd.Series:
s = score.copy()
s[n_valid < MIN_VALID] = np.nan
return s
def breadth_ma(ALTS: pd.DataFrame, N: int) -> pd.Series:
"""% di alt validi con close > SMA(N). Causale (SMA su dati <= i)."""
sma = ALTS.rolling(N, min_periods=N).mean()
valid = ALTS.notna() & sma.notna()
above = (ALTS > sma) & valid
n_valid = valid.sum(axis=1)
return _mask_min_valid(above.sum(axis=1) / n_valid.replace(0, np.nan), n_valid)
def breadth_ad(ALTS: pd.DataFrame, N: int) -> pd.Series:
"""Advance/decline: frazione di advancers (ret 1g > 0) tra i validi, SMA(N)."""
dr = ALTS.pct_change(fill_method=None)
valid = dr.notna()
adv = ((dr > 0) & valid).sum(axis=1)
n_valid = valid.sum(axis=1)
frac = adv / n_valid.replace(0, np.nan)
frac[n_valid < MIN_VALID] = np.nan
return frac.rolling(N, min_periods=N).mean()
def breadth_rs(ALTS: pd.DataFrame, btc_ref: pd.Series, N: int) -> pd.Series:
"""% di alt che battono BTC sul ritorno a N giorni (risk-appetite relativo)."""
altret = ALTS / ALTS.shift(N) - 1.0
btcret = (btc_ref / btc_ref.shift(N) - 1.0)
valid = altret.notna() & btcret.notna().values[:, None]
beat = altret.gt(btcret, axis=0) & valid
n_valid = valid.sum(axis=1)
return _mask_min_valid(beat.sum(axis=1) / n_valid.replace(0, np.nan), n_valid)
def breadth_th(ALTS: pd.DataFrame, N: int) -> pd.Series:
"""Breadth-THRUST: 0.5 + delta a N giorni della breadth MA20 (thrust>0.5, collapse<0.5)."""
b20 = breadth_ma(ALTS, 20)
return 0.5 + (b20 - b20.shift(N))
# ===========================================================================
# FACTORY — target_fn(df, asset) per una cella (fam, N, thr, form)
# ===========================================================================
_ALTS, _BTC_REF = load_panel()
_BREADTH: dict[tuple, pd.Series] = {}
for _N in MA_GRID:
_BREADTH[("ma", _N)] = breadth_ma(_ALTS, _N)
_BREADTH[("ad", _N)] = breadth_ad(_ALTS, _N)
_BREADTH[("rs", _N)] = breadth_rs(_ALTS, _BTC_REF, _N)
_BREADTH[("th", _N)] = breadth_th(_ALTS, _N)
_TP01_POS: dict[str, np.ndarray] = {}
def tp01_pos(df: pd.DataFrame, asset: str) -> np.ndarray:
if asset not in _TP01_POS:
_TP01_POS[asset] = TrendPortfolio(**CANONICAL).target_series(df)
return _TP01_POS[asset]
_EPOCH = pd.Timestamp("1970-01-01", tz="UTC")
def _align(b: pd.Series, df: pd.DataFrame) -> np.ndarray:
"""Breadth (calendario HL) -> barre BTC/ETH. merge_asof backward, exact ok (stesso
istante di close 00:00 UTC). NaN dove la breadth non esiste.
NB timestamp via epoca esplicita: .view('int64') su DatetimeIndex tz-aware a risoluzione
non-ns (pandas 2.x) da' la SCALA SBAGLIATA -> merge_asof matchava tutto all'ULTIMO valore
(broadcast del futuro su tutta la storia = look-ahead che causality_ok non vede, perche'
la serie breadth e' un input esterno fisso). Bug trovato e corretto in questa ricerca."""
ts_ms = ((b.index - _EPOCH) // pd.Timedelta(milliseconds=1)).astype("int64")
g = pd.DataFrame({"timestamp": ts_ms, "b": b.values})
g = g.dropna(subset=["b"]).sort_values("timestamp")
left = pd.DataFrame({"timestamp": df["timestamp"].astype("int64").values})
m = pd.merge_asof(left, g, on="timestamp", direction="backward")
return m["b"].values.astype(float)
def factory(tf: str = "1d", fam: str = "ma", N: int = 50, thr: float = 0.5, form: str = "ls"):
b_series = _BREADTH[(fam, N)]
def target_fn(df: pd.DataFrame, asset: str = "") -> np.ndarray:
b = _align(b_series, df)
if form == "gate":
g = np.where(np.isfinite(b), np.where(b >= thr, 1.0, 0.0), 1.0) # no info -> no de-risk
return tp01_pos(df, asset) * g
if form == "ls":
d = np.where(np.isfinite(b), np.where(b >= thr, 1.0, -1.0), 0.0)
else: # lf
d = np.where(np.isfinite(b), np.where(b >= thr, 1.0, 0.0), 0.0)
return al.vol_target(d, df, 0.20, 30, 2.0)
return target_fn
GRID = [dict(fam=f, N=n, thr=t, form=fo)
for f in FAMS for n in MA_GRID for t in THR_GRID for fo in FORMS]
# ===========================================================================
# DRIVER ONESTO sulla finestra comune (mirror di study_family_honest, stessi gate altlib)
# ===========================================================================
def cand_trim(fn) -> pd.Series:
return al.candidate_daily(fn, tf="1d")[lambda s: s.index >= START]
def abs_verdict_trimmed(fn) -> dict:
"""study_weights-equivalente sulla finestra comune 2024-05+ (fee sweep incluso)."""
per_asset = {}
fee_ok_all = True
for a in ASSETS:
df = al.get(a, "1d")
tgt = np.asarray(fn(df, a), float)
mask = (pd.to_datetime(df["datetime"], utc=True) >= START).values
dft = df[mask].reset_index(drop=True)
tgtt = tgt[mask]
base = al.eval_weights(dft, tgtt, fee_side=al.FEE_SIDE)
sweep = {f"{2*f*100:.2f}%RT": al.eval_weights(dft, tgtt, fee_side=f)["full"]["sharpe"]
for f in al.FEE_SWEEP}
fee_ok_all = fee_ok_all and sweep.get("0.20%RT", -9) > 0
per_asset[a] = dict(full=base["full"], holdout=base["holdout"],
tim=base["time_in_market"], turnover=base["turnover_per_year"],
fee_sweep=sweep, yearly=base["yearly"])
cell = dict(tf="1d", per_asset=per_asset,
min_asset_full_sharpe=round(min(per_asset[a]["full"]["sharpe"] for a in ASSETS), 3),
min_asset_holdout_sharpe=round(min(per_asset[a]["holdout"].get("sharpe", 0.0) for a in ASSETS), 3),
full_sharpe=round(float(np.mean([per_asset[a]["full"]["sharpe"] for a in ASSETS])), 3),
fee_survives=fee_ok_all)
return dict(cells=[cell], verdict=al._verdict([cell]))
def gate_work_diag(fn, thr_flat: float = 1e-6) -> dict:
"""Deep-dive ridondanza (lezione macro-gate): nei giorni in cui il segnale vorrebbe stare
fuori/short, TP01 e' gia' flat da solo? 'lavora' = segnale off/short E TP01 non flat."""
out = {}
for a in ASSETS:
df = al.get(a, "1d")
mask = (pd.to_datetime(df["datetime"], utc=True) >= START).values
tgt = np.asarray(fn(df, a), float)[mask]
tp = tp01_pos(df, a)[mask]
off = tgt <= thr_flat # segnale fuori (o short per LS: qui solo "non long")
works = off & (tp > thr_flat)
out[a] = dict(days_off=round(float(off.mean()), 3),
days_gate_works=round(float(works.mean()), 3),
tp01_flat_days=round(float((tp <= thr_flat).mean()), 3),
corr_pos=round(float(np.corrcoef(tgt, tp)[0, 1]), 3)
if np.std(tgt) > 0 and np.std(tp) > 0 else None)
return out
def cell_activity(p: dict) -> float:
"""Frazione di giorni nello STATO DI MINORANZA del segnale (criterio STRUTTURALE, non di
performance: una cella sempre-on e' buy&hold/TP01 travestito, non un segnale di breadth).
ls: min(on, off); lf: frazione on... comunque = minoranza; gate: frazione off."""
b = _BREADTH[(p["fam"], p["N"])]
bb = b[(b.index >= START)].dropna()
on = float((bb >= p["thr"]).mean())
return round(min(on, 1.0 - on) if p["form"] == "ls" else
(on if p["form"] == "lf" else 1.0 - on), 3)
def full_report(label: str, chosen: dict, all_full: list, n_trials: int) -> dict:
p = chosen["params"]
fn = factory(**p)
daily = cand_trim(fn)
dsr, sr0 = al.deflated_sharpe(al._sh(daily), all_full, daily)
print(f"\n==== {label}: {p} (attivita' {chosen['act']}) ====")
print(f" standalone (finestra comune): IS {chosen['insample_sharpe']:+.2f} "
f"FULL {chosen['full_sharpe']:+.2f} HOLD {chosen['hold_sharpe']:+.2f}")
print(f" deflated Sharpe (su TUTTI i {n_trials} trial cercati): DSR={dsr:.3f} "
f"(null-max atteso {sr0:.2f}) PASS>=0.95: {dsr >= 0.95}")
marg = al.marginal_vs_tp01(daily)
absr = abs_verdict_trimmed(fn)
abs_grade = absr["verdict"]["grade"]
earns_slot = (abs_grade != "FAIL" and marg.get("marginal_verdict") == "ADDS"
and marg.get("robust_oos", False) and marg.get("beats_noise_null", False)
and not marg.get("is_hedge", False))
rep = dict(name=f"BREADTH {p}", marginal=marg, abs_grade=abs_grade,
marginal_verdict=marg.get("marginal_verdict"), earns_slot=earns_slot)
print("\n" + al.fmt_marginal(rep))
honest = earns_slot and dsr >= 0.95
print(f" earns_slot_honest = earns_slot({earns_slot}) AND DSR>=0.95({dsr >= 0.95}) "
f"=> {honest}")
# assoluto trimmed + fee sweep
c = absr["cells"][0]
print(f"\n---- ASSOLUTO (finestra comune, verdetto {abs_grade}): "
f"minFull {c['min_asset_full_sharpe']:+.2f} minHold {c['min_asset_holdout_sharpe']:+.2f} "
f"feeOK={c['fee_survives']}")
for a in ASSETS:
pa = c["per_asset"][a]
yr = " ".join(f"{y}:{d['ret']*100:+.0f}%" for y, d in pa["yearly"].items())
print(f" {a}: full Sh {pa['full']['sharpe']:+.2f} DD {pa['full']['maxdd']*100:.0f}% "
f"hold Sh {pa['holdout'].get('sharpe', 0):+.2f} tim {pa['tim']} "
f"turn/y {pa['turnover']} | {yr}")
print(f" fee sweep: {pa['fee_sweep']}")
# causalita' + smallcap $600
ca = al.causality_ok(fn, tf="1d")
print(f"\n---- causality_ok: {ca['ok']} (max_tail_diff {ca['max_tail_diff']}, checked {ca['checked']})")
for a in ASSETS:
df = al.get(a, "1d")
mask = (pd.to_datetime(df["datetime"], utc=True) >= START).values
dft = df[mask].reset_index(drop=True)
tgtt = np.asarray(fn(df, a), float)[mask]
sc = al.eval_weights_smallcap(dft, tgtt, capital=600.0, min_order=5.0)
print(f" smallcap $600 {a}: modeled Sh {sc['modeled']['sharpe']:+.2f} -> "
f"real {sc['realistic']['sharpe']:+.2f} (haircut {sc['sharpe_haircut']:+.2f}, "
f"{sc['n_executed_trades']} trade eseguiti)")
# deep-dive ridondanza col trend (lezione macro-gate)
print("---- RIDONDANZA COL TREND (il rischio n.1):")
for a, d in gate_work_diag(fn).items():
print(f" {a}: corr(pos, TP01pos) {d['corr_pos']} giorni segnale-off {d['days_off']} "
f"TP01-gia'-flat {d['tp01_flat_days']} GIORNI IN CUI LAVORA {d['days_gate_works']}")
return dict(params=p, dsr=round(float(dsr), 3), earns_slot=earns_slot,
earns_slot_honest=honest, marginal_verdict=marg.get("marginal_verdict"),
abs_grade=abs_grade, corr_tp01=marg.get("corr_full"),
is_hedge=marg.get("is_hedge"))
def main():
print(f"=== r0701 BREADTH/INTERNALS — panel: {_ALTS.shape[1]} alt, "
f"{_ALTS.index[0].date()} -> {_ALTS.index[-1].date()} | finestra analisi {START.date()}+ "
f"(in-sample {START.date()} -> {HOLDOUT.date()} = ~8 mesi; storia ~2.2y: LIMITE DICHIARATO)")
nv = _ALTS.notna().sum(axis=1)
print(f" asset validi/D: min {int(nv.min())} med {int(nv.median())} max {int(nv.max())} "
f"(MIN_VALID={MIN_VALID})")
# ---- 1. tutte le celle: Sharpe in-sample (selezione) + full (DSR) sulla finestra comune
rows = []
for p in GRID:
fn = factory(**p)
daily = cand_trim(fn)
ins = daily[daily.index < HOLDOUT]
if len(ins) < 60 or daily.std() == 0:
continue
rows.append(dict(params=p, act=cell_activity(p),
insample_sharpe=round(al._sh(ins), 3),
full_sharpe=round(al._sh(daily), 3),
hold_sharpe=round(al._sh(daily[daily.index >= HOLDOUT]), 3)))
rows.sort(key=lambda r: r["insample_sharpe"], reverse=True)
all_full = [r["full_sharpe"] for r in rows]
print(f"\n---- GRIGLIA: {len(GRID)} celle, {len(rows)} valutabili; "
f"full>0: {sum(1 for s in all_full if s > 0)}/{len(all_full)}")
print("---- TOP-15 per Sharpe IN-SAMPLE (selezione onesta: MAI sull'hold-out) "
"[hold mostrato solo per trasparenza; act = frazione stato di minoranza]")
for r in rows[:15]:
p = r["params"]
print(f" {p['fam']:>2s} N={p['N']:>3d} thr={p['thr']} {p['form']:>4s} act={r['act']:.2f} | "
f"IS {r['insample_sharpe']:+.2f} full {r['full_sharpe']:+.2f} hold {r['hold_sharpe']:+.2f}")
# ---- 2. PRIMARIO: cella scelta in-sample su TUTTA la griglia (procedura onesta pura)
out1 = full_report("CELLA SCELTA IN-SAMPLE (tutta la griglia)", rows[0], all_full, len(rows))
# ---- 3. SECONDARIO (dichiarato): sole celle ATTIVE (minoranza >=10% — criterio strutturale
# deciso a priori, NON di performance; DSR sempre deflazionato su TUTTI i trial).
active = [r for r in rows if r["act"] >= 0.10]
print(f"\n---- CELLE ATTIVE (act>=0.10): {len(active)}/{len(rows)}")
out2 = None
if active and active[0]["params"] != rows[0]["params"]:
out2 = full_report("SECONDARIO: best cella ATTIVA in-sample", active[0], all_full, len(rows))
# ---- 4. marginal sui best per-forma tra le ATTIVE (trasparenza: il verdetto per forma)
print("\n---- VERDETTO MARGINALE dei best-IN-SAMPLE ATTIVI per forma (contesto):")
for fo in FORMS:
sub = [r for r in active if r["params"]["form"] == fo]
if not sub:
continue
rp = sub[0]["params"]
m = al.marginal_vs_tp01(cand_trim(factory(**rp)))
uh = m["blends"]["w25"]["uplift_hold"]
print(f" {fo:>4s} {rp} act={sub[0]['act']:.2f}: {m.get('marginal_verdict')} "
f"corr {m.get('corr_full')} IS-edge {m.get('cand_insample_sharpe')} "
f"is_hedge {m.get('is_hedge')} uplift w25 full {m['blends']['w25']['uplift_full']:+.3f} "
f"hold {uh if uh is None else format(uh, '+.3f')} multicut {m.get('multicut_uplift')}")
print("\n==== SINTESI ====")
print(f" primario: {out1}")
print(f" secondario (attive): {out2}")
print("==== FINE r0701 ====")
if __name__ == "__main__":
main()
+387
View File
@@ -0,0 +1,387 @@
"""r0701_funding_ts — FUNDING RATE come segnale TIME-SERIES direzionale su BTC/ETH (NON carry).
2026-07-01. Ipotesi: il funding orario Hyperliquid (proxy di POSIZIONAMENTO/sentiment dei perp)
contiene informazione direzionale a orizzonte giornaliero su BTC/ETH. Famiglia (griglia modesta):
- FADE : z-score del funding estremo-positivo = affollamento long -> SHORT (e viceversa)
- FOLLOW : funding in espansione = domanda long persistente -> LONG (sentiment momentum)
- GATE : trend TP01-like long-flat, FLAT quando il funding e' affollato (z>=thr, de-risk)
- DIVERGE : momentum prezzo 20d con funding NON affollato -> follow; affollato -> fade
Griglia: 4 forme x lookback z {7,14,30,60}g x soglia {0.5,1.0,1.5} = 48 celle, solo 1d.
PRIOR ART (non ripetuto): FC01 carry cross-sectional delta-neutral -> SCARTATO
(docs/diary/2026-06-22-funding-carry-hl.md); funding price-clock intraday -> FAIL (onda intraday).
Qui il funding e' un SEGNALE time-series direzionale su BTC/ETH perp (2 gambe, eseguibile a ~$600),
non un cashflow da incassare.
DATI: data/raw/hlfund_{btc,eth}_1h.parquet (funding orario HL: 2023-05-12 -> 2026-06-22; primi
~27 giorni a cadenza 8h, poi oraria, 0 gap; certificato nel diario 2026-06-22). Prezzi certificati
Deribit via altlib.get (1d resampled leak-free).
CAUSALITA' (il punto delicato): le barre 1d sono OPEN-LABELED (datetime = 00:00 UTC del giorno D;
il close della barra D e' noto alle 00:00 di D+1). Il feature-day D aggrega i SOLI stamp funding
in [D 00:00, D+24h) l'ultimo alle 23:00 — quindi tutto e' noto PRIMA della decisione al close
della barra D. eval_weights poi shifta: target[D] e' tenuto durante la barra D+1. Nessun leak
strutturale; in piu' prefix-check esplicito.
VALUTAZIONE su finestra TRONCATA alla copertura funding (2023-05-12..2026-06-21), NON sul frame
prezzi 2018+: fuori copertura il target sarebbe zero per costruzione e i giorni-zero (a) GONFIANO
il T del deflated-Sharpe (anti-conservativo) e (b) DILUISCONO cand_insample_sharpe (gate
has_insample_edge scatterebbe a vuoto). La logica di study_family_honest e' replicata ESATTAMENTE
sui frame troncati coi primitivi altlib: selezione cella IN-SAMPLE-ONLY (mai sul hold-out) ->
study_marginal gates (ADDS + robust_oos + has_insample_edge + not is_hedge) -> deflated-Sharpe
>= 0.95 sull'INTERA griglia. Cross-check con study_marginal non-troncato riportato in coda.
CAVEAT STORIA: funding solo dal 2023-05 (~3.1 anni). In-sample pre-HOLDOUT ~1.6 anni (meno il
warmup z), hold-out 2025-01+ ~1.5 anni. Finestra corta: qualunque PASS andrebbe comunque in
forward-monitor, e un FAIL su questa finestra non e' appellabile a "regime sfortunato".
Run: cd /opt/docker/PythagorasGoal && uv run python scripts/research/r0701_funding_ts.py
"""
from __future__ import annotations
import json
import sys
from functools import lru_cache
from pathlib import Path
import numpy as np
import pandas as pd
sys.path.insert(0, str(Path(__file__).resolve().parent / "alt"))
import altlib as al # noqa: E402
ASSETS = ("BTC", "ETH")
FORMS = ("fade", "follow", "gate", "diverge")
LOOKBACKS = (7, 14, 30, 60)
THRESHOLDS = (0.5, 1.0, 1.5)
EARLY_8H_END = pd.Timestamp("2023-06-11", tz="UTC") # fino a qui cadenza 8h (3 stamp/giorno)
# ===========================================================================
# DATI FUNDING — aggregazione giornaliera causale
# ===========================================================================
@lru_cache(maxsize=16)
def daily_funding(asset: str, back_h: int = 0) -> pd.DataFrame:
"""Funding giornaliero = SOMMA degli stamp orari nella finestra [D-back_h, D+24h-back_h).
back_h=0 (default) = giorno UTC pieno [D, D+24h): tutti gli stamp (ultimo 23:00) sono noti
al close della barra open-labeled D (= 00:00 di D+1) -> causale. back_h>0 sposta la finestra
INDIETRO (sempre causale) usato solo dal boundary-shift check.
'valid' = giorno con copertura piena (>=20 stamp orari; >=3 nell'era 8h iniziale)."""
p = al.DATA_DIR / f"hlfund_{asset.lower()}_1h.parquet"
d = pd.read_parquet(p)
idx = pd.DatetimeIndex(pd.to_datetime(d.index, utc=True)) + pd.Timedelta(hours=back_h)
day = idx.floor("1D")
g = pd.Series(d["funding"].values.astype(float), index=day).groupby(level=0)
out = pd.DataFrame({"fday": g.sum(), "n": g.count()})
early = out.index <= EARLY_8H_END
out["valid"] = np.where(early, out["n"] >= 3, out["n"] >= 20)
return out
@lru_cache(maxsize=1)
def fund_window() -> tuple:
"""Intersezione BTC/ETH dei giorni funding validi (a back_h=0)."""
los, his = [], []
for a in ASSETS:
v = daily_funding(a)
vd = v.index[v["valid"].values]
los.append(vd.min()); his.append(vd.max())
return max(los), min(his)
@lru_cache(maxsize=8)
def get_trunc(asset: str, tf: str = "1d") -> pd.DataFrame:
"""Prezzi certificati troncati alla copertura funding (vedi docstring modulo)."""
lo, hi = fund_window()
df = al.get(asset, tf)
day = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)).floor("1D")
m = (day >= lo) & (day <= hi)
return df.loc[m].reset_index(drop=True)
def aligned_fday(df: pd.DataFrame, asset: str, back_h: int = 0) -> np.ndarray:
"""Funding giornaliero allineato alle barre di df (NaN dove manca/incompleto)."""
fd = daily_funding(asset, back_h)
day = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)).floor("1D")
return fd["fday"].where(fd["valid"]).reindex(day).values.astype(float)
# ===========================================================================
# FAMIGLIA DI SEGNALI — factory(tf, form, lb, thr) -> target_fn(df, asset)
# ===========================================================================
def make_target(tf: str = "1d", form: str = "fade", lb: int = 30, thr: float = 1.0,
back_h: int = 0):
def target(df: pd.DataFrame, asset: str) -> np.ndarray:
f = aligned_fday(df, asset, back_h)
z = al.zscore(f, lb) # causale: rolling fino a i incluso
c = pd.Series(df["close"].values.astype(float))
if form == "fade": # affollamento long -> short (e viceversa)
d = np.where(z >= thr, -1.0, np.where(z <= -thr, 1.0, 0.0))
elif form == "follow": # funding come sentiment momentum
d = np.where(z >= thr, 1.0, np.where(z <= -thr, -1.0, 0.0))
elif form == "gate": # trend long-flat, flat se affollato
m30 = np.nan_to_num(np.sign(c.pct_change(30).values))
m90 = np.nan_to_num(np.sign(c.pct_change(90).values))
trend = ((m30 + m90) > 0).astype(float)
zz = np.where(np.isfinite(z), z, np.inf) # z ignoto -> conservativo: flat
d = trend * (zz < thr).astype(float)
elif form == "diverge": # mossa non affollata -> follow; affollata -> fade
mom = np.nan_to_num(np.sign(c.pct_change(20).values))
d = np.where(z >= thr, -mom, np.where(z <= -thr, mom, 0.0))
else:
raise ValueError(form)
return al.vol_target(np.nan_to_num(d), df, 0.20, 30, 2.0)
return target
# ===========================================================================
# VALUTAZIONE (replica study_family_honest su frame troncati)
# ===========================================================================
def cell_daily(target_fn, fee_side: float = al.FEE_SIDE) -> pd.Series:
"""Serie daily netta 50/50 BTC+ETH del candidato (convenzione candidate_daily)."""
series = {}
for a in ASSETS:
df = get_trunc(a)
ev = al.eval_weights(df, target_fn(df, a), fee_side=fee_side)
series[a] = pd.Series(ev["net"], index=ev["idx"])
J = pd.concat(series, axis=1, join="inner").fillna(0.0)
return al._to_daily(0.5 * J["BTC"] + 0.5 * J["ETH"])
def scan_family() -> list[dict]:
rows = []
for form in FORMS:
for lb in LOOKBACKS:
for thr in THRESHOLDS:
daily = cell_daily(make_target(form=form, lb=lb, thr=thr))
ins = daily[daily.index < al.HOLDOUT]
hold = daily[daily.index >= al.HOLDOUT]
rows.append(dict(
form=form, lb=lb, thr=thr,
insample_sharpe=round(al._sh(ins), 3) if len(ins) > 60 else float("nan"),
full_sharpe=round(al._sh(daily), 3),
hold_sharpe=round(al._sh(hold), 3) if len(hold) > 60 else float("nan")))
return rows
def absolute_study(target_fn, name: str) -> dict:
"""study_weights-equivalente sui frame troncati (fee sweep 0.00-0.30% RT incluso)."""
per_asset = {}
fee_ok_all = True
for a in ASSETS:
df = get_trunc(a)
tgt = target_fn(df, a)
base = al.eval_weights(df, tgt, fee_side=al.FEE_SIDE)
sweep = {f"{2 * f * 100:.2f}%RT": al.eval_weights(df, tgt, fee_side=f)["full"]["sharpe"]
for f in al.FEE_SWEEP}
fee_ok_all = fee_ok_all and sweep.get("0.20%RT", -9) > 0
per_asset[a] = dict(full=base["full"], holdout=base["holdout"],
tim=base["time_in_market"], turnover=base["turnover_per_year"],
fee_sweep=sweep, yearly=base["yearly"])
cells = [dict(tf="1d", per_asset=per_asset,
min_asset_full_sharpe=round(min(per_asset[a]["full"]["sharpe"] for a in ASSETS), 3),
min_asset_holdout_sharpe=round(min(per_asset[a]["holdout"].get("sharpe", 0.0) for a in ASSETS), 3),
full_sharpe=round(float(np.mean([per_asset[a]["full"]["sharpe"] for a in ASSETS])), 3),
fee_survives=fee_ok_all)]
return dict(name=name, kind="weights", cells=cells, verdict=al._verdict(cells))
def prefix_check(target_fn, tail: int = 60) -> float:
"""Consistenza online (guardia look-ahead): il target ricalcolato su un prefisso troncato
deve coincidere col target(full) sugli stessi indici. Ritorna il max scostamento."""
worst = 0.0
for a in ASSETS:
df = get_trunc(a)
full = np.nan_to_num(np.asarray(target_fn(df, a), float))
n = len(df)
for cut in (int(n * 0.80), int(n * 0.92)):
sub = df.iloc[:cut].reset_index(drop=True)
s = np.nan_to_num(np.asarray(target_fn(sub, a), float))
worst = max(worst, float(np.max(np.abs(s[cut - tail:cut] - full[cut - tail:cut]))))
return worst
def boundary_check(form: str, lb: int, thr: float, offsets=(0, 3, 6, 9, 12)) -> dict:
"""Lezione day_boundary: sposto INDIETRO di back_h ore la finestra di aggregazione del
funding (sempre causale). Un effetto di posizionamento reale non cambia segno."""
B = al.tp01_baseline_daily()
out = {}
for off in offsets:
daily = cell_daily(make_target(form=form, lb=lb, thr=thr, back_h=off))
J = pd.concat({"B": B, "C": daily}, axis=1, join="inner").dropna()
up = al._sh(0.75 * J["B"] + 0.25 * J["C"]) - al._sh(J["B"]) if len(J) > 30 else float("nan")
out[off] = dict(full_sharpe=round(al._sh(daily), 3), uplift_w25=round(up, 3))
ups = [v["uplift_w25"] for v in out.values() if np.isfinite(v["uplift_w25"])]
shs = [v["full_sharpe"] for v in out.values()]
return dict(per_offset=out,
sharpe_sign_stable=bool(min(shs) * max(shs) >= 0 or max(map(abs, shs)) < 0.1),
uplift_spread=round(max(ups) - min(ups), 3) if ups else None)
def trend_only_target(df: pd.DataFrame, asset: str = "") -> np.ndarray:
"""CONTROLLO DECISIVO (lezione TP01-DVOL-overlay): lo STESSO trend long-flat della forma
'gate' ma SENZA il gate funding. Se fa uguale/meglio, il funding non aggiunge nulla."""
c = pd.Series(df["close"].values.astype(float))
m30 = np.nan_to_num(np.sign(c.pct_change(30).values))
m90 = np.nan_to_num(np.sign(c.pct_change(90).values))
trend = ((m30 + m90) > 0).astype(float)
return al.vol_target(trend, df, 0.20, 30, 2.0)
def smallcap_check(target_fn) -> dict:
out = {}
for a in ASSETS:
df = get_trunc(a)
sc = al.eval_weights_smallcap(df, target_fn(df, a), capital=600.0, min_order=5.0)
out[a] = dict(modeled_sh=sc["modeled"]["sharpe"], realistic_sh=sc["realistic"]["sharpe"],
haircut=sc["sharpe_haircut"], n_trades=sc["n_executed_trades"])
return out
# ===========================================================================
def main():
print("=" * 88)
print("r0701_funding_ts — funding HL come segnale TS direzionale su BTC/ETH (non carry)")
print("=" * 88)
# --- 1. data-first: qualita'/copertura --------------------------------------------
lo, hi = fund_window()
print("\n[1] DATI FUNDING")
for a in ASSETS:
fd = daily_funding(a)
v = fd["valid"]
ann = fd.loc[v, "fday"].mean() * 365.25 * 100
print(f" {a}: giorni validi {int(v.sum())}/{len(fd)} "
f"finestra {fd.index[0].date()} -> {fd.index[-1].date()} "
f"funding medio {ann:+.1f}%/anno "
f"std daily {fd.loc[v, 'fday'].std() * 1e4:.2f} bps")
print(f" finestra comune valida: {lo.date()} -> {hi.date()} "
f"({(hi - lo).days} giorni, ~{(hi - lo).days / 365.25:.1f} anni)")
n_ins = (al.HOLDOUT - lo).days
n_hold = (hi - al.HOLDOUT).days
print(f" in-sample pre-HOLDOUT ~{n_ins}g ({n_ins / 365.25:.1f}a), "
f"hold-out ~{n_hold}g ({n_hold / 365.25:.1f}a) <-- STORIA CORTA, caveat")
# --- 2. scan famiglia (48 celle, selezione IN-SAMPLE-ONLY) -------------------------
print("\n[2] SCAN FAMIGLIA (4 forme x lb{7,14,30,60} x thr{0.5,1.0,1.5} = 48 celle, 1d)")
rows = scan_family()
valid = [r for r in rows if np.isfinite(r["insample_sharpe"])]
valid.sort(key=lambda r: r["insample_sharpe"], reverse=True)
print(f" celle valide {len(valid)}/{len(rows)}; top-8 per Sharpe IN-SAMPLE "
f"(hold-out mostrato SOLO per trasparenza, mai per selezione):")
print(f" {'form':8s} {'lb':>3s} {'thr':>4s} {'IS':>7s} {'FULL':>7s} {'HOLD':>7s}")
for r in valid[:8]:
print(f" {r['form']:8s} {r['lb']:3d} {r['thr']:4.1f} {r['insample_sharpe']:7.2f} "
f"{r['full_sharpe']:7.2f} {r['hold_sharpe']:7.2f}")
per_form = {f: max((r["insample_sharpe"] for r in valid if r["form"] == f), default=float("nan"))
for f in FORMS}
print(f" best IS per forma: {per_form}")
chosen = valid[0]
print(f"\n CELLA SCELTA (in-sample-only): {chosen['form']} lb={chosen['lb']} thr={chosen['thr']} "
f"(IS {chosen['insample_sharpe']}, FULL {chosen['full_sharpe']}, HOLD {chosen['hold_sharpe']})")
fn = make_target(form=chosen["form"], lb=chosen["lb"], thr=chosen["thr"])
daily = cell_daily(fn)
# --- 3. deflated Sharpe sull'INTERA griglia ----------------------------------------
all_full = [r["full_sharpe"] for r in rows]
dsr, sr0 = al.deflated_sharpe(al._sh(daily), all_full, daily)
dsr_pass = bool(np.isfinite(dsr) and dsr >= 0.95)
print(f"\n[3] DEFLATED SHARPE (griglia {len(rows)} celle): "
f"DSR={dsr:.3f} (null-max atteso {sr0:.2f}) PASS(>=0.95)={dsr_pass}")
# --- 4. assoluto + marginale (gates study_marginal) --------------------------------
print("\n[4] ASSOLUTO (frame troncati, fee sweep 0.00-0.30% RT)")
absolute = absolute_study(fn, f"R0701-FUND-{chosen['form'].upper()}")
print(al.fmt(absolute))
c0 = absolute["cells"][0]
for a in ASSETS:
pa = c0["per_asset"][a]
print(f" {a}: TIM={pa['tim']} turnover/anno={pa['turnover']} fee_sweep={pa['fee_sweep']}")
print("\n[5] MARGINALE vs TP01 (finestra comune col baseline)")
marg = al.marginal_vs_tp01(daily)
abs_grade = absolute["verdict"]["grade"]
earns_slot = (abs_grade != "FAIL" and marg.get("marginal_verdict") == "ADDS"
and marg.get("robust_oos", False) and marg.get("beats_noise_null", False)
and not marg.get("is_hedge", False))
rep = dict(name=f"R0701-FUND {chosen['form']} lb{chosen['lb']} thr{chosen['thr']}",
absolute=absolute, marginal=marg, abs_grade=abs_grade,
marginal_verdict=marg.get("marginal_verdict"), earns_slot=earns_slot)
print(al.fmt_marginal(rep))
earns_honest = bool(earns_slot and dsr_pass)
print(f"\n EARNS_SLOT (marginal) = {earns_slot} EARNS_SLOT_HONEST (con DSR) = {earns_honest}")
# --- 5bis. controllo di attribuzione: il funding aggiunge qualcosa al trend nudo? ----
print("\n[5bis] CONTROLLO DECISIVO — trend long-flat IDENTICO ma SENZA gate funding")
d_tr = cell_daily(trend_only_target)
tr_ins, tr_hold = d_tr[d_tr.index < al.HOLDOUT], d_tr[d_tr.index >= al.HOLDOUT]
JJ = pd.concat({"G": daily, "T": d_tr}, axis=1, join="inner").dropna()
print(f" trend NUDO: IS {al._sh(tr_ins):.2f} FULL {al._sh(d_tr):.2f} HOLD {al._sh(tr_hold):.2f}")
print(f" trend+GATE: IS {chosen['insample_sharpe']:.2f} FULL {chosen['full_sharpe']:.2f} "
f"HOLD {chosen['hold_sharpe']:.2f}")
print(f" corr(gated, nudo) = {JJ['G'].corr(JJ['T']):.3f} "
f"delta FULL = {al._sh(JJ['G']) - al._sh(JJ['T']):+.3f} "
f"delta HOLD = {al._sh(JJ['G'][JJ.index >= al.HOLDOUT]) - al._sh(JJ['T'][JJ.index >= al.HOLDOUT]):+.3f}")
attribution = dict(trend_nudo=dict(IS=round(al._sh(tr_ins), 3), FULL=round(al._sh(d_tr), 3),
HOLD=round(al._sh(tr_hold), 3)),
corr_gated_nudo=round(float(JJ["G"].corr(JJ["T"])), 3),
delta_full=round(al._sh(JJ["G"]) - al._sh(JJ["T"]), 3),
delta_hold=round(al._sh(JJ["G"][JJ.index >= al.HOLDOUT])
- al._sh(JJ["T"][JJ.index >= al.HOLDOUT]), 3))
# --- 5ter. la migliore cella PURO-funding (fade/follow/diverge, senza trend) ---------
pure = [r for r in valid if r["form"] != "gate"]
bp = pure[0] if pure else None
if bp:
print(f"\n[5ter] MIGLIOR CELLA PURO-FUNDING (no trend): {bp['form']} lb={bp['lb']} "
f"thr={bp['thr']} IS {bp['insample_sharpe']} FULL {bp['full_sharpe']} "
f"HOLD {bp['hold_sharpe']}")
d_bp = cell_daily(make_target(form=bp["form"], lb=bp["lb"], thr=bp["thr"]))
m_bp = al.marginal_vs_tp01(d_bp)
print(f" marginale vs TP01: {m_bp.get('marginal_verdict')} corr {m_bp.get('corr_full')} "
f"IS-edge {m_bp.get('cand_insample_sharpe')} "
f"uplift w25 full {m_bp['blends']['w25']['uplift_full']:+.3f} / "
f"hold {m_bp['blends']['w25']['uplift_hold']:+.3f}")
# --- 6. realism: prefix / boundary / smallcap ---------------------------------------
print("\n[6] REALISM CHECKS (cella scelta)")
worst = prefix_check(fn)
print(f" prefix-consistency (guardia look-ahead): max diff = {worst:.2e} "
f"({'OK' if worst < 1e-9 else 'ATTENZIONE'})")
bnd = boundary_check(chosen["form"], chosen["lb"], chosen["thr"])
print(f" boundary-shift (finestra funding -0/3/6/9/12h): {bnd['per_offset']}")
print(f" sharpe_sign_stable={bnd['sharpe_sign_stable']} uplift_spread={bnd['uplift_spread']}")
sc = smallcap_check(fn)
print(f" smallcap $600 (min order $5): {sc}")
# --- 7. cross-check non troncato (footnote) -----------------------------------------
print("\n[7] CROSS-CHECK study_marginal NON troncato (frame 2018+, diluito dagli zeri "
"pre-copertura: footnote, non il giudizio primario)")
sm_full = al.study_marginal(f"R0701-FUND-XCHK {chosen['form']}", fn, tf="1d")
print(f" abs={sm_full['abs_grade']} marginal={sm_full['marginal_verdict']} "
f"earns_slot={sm_full['earns_slot']}")
# --- 8. verdetto --------------------------------------------------------------------
summary = dict(
chosen=dict(form=chosen["form"], lb=chosen["lb"], thr=chosen["thr"]),
insample_sharpe=chosen["insample_sharpe"], full_sharpe=chosen["full_sharpe"],
hold_sharpe=chosen["hold_sharpe"], dsr=round(float(dsr), 3), dsr_pass=dsr_pass,
abs_grade=abs_grade, marginal_verdict=marg.get("marginal_verdict"),
corr_tp01_full=marg.get("corr_full"), cand_insample_sharpe=marg.get("cand_insample_sharpe"),
has_insample_edge=marg.get("has_insample_edge"), is_hedge=marg.get("is_hedge"),
robust_oos=marg.get("robust_oos"), multicut=marg.get("multicut_uplift"),
earns_slot=earns_slot, earns_slot_honest=earns_honest,
smallcap=sc, boundary_uplift_spread=bnd["uplift_spread"],
attribution_vs_trend_nudo=attribution,
best_pure_funding=(dict(form=bp["form"], lb=bp["lb"], thr=bp["thr"],
IS=bp["insample_sharpe"], FULL=bp["full_sharpe"],
HOLD=bp["hold_sharpe"]) if bp else None),
n_cells=len(rows), history_years=round((hi - lo).days / 365.25, 1))
print("\n[8] SUMMARY JSON")
print(json.dumps(summary, default=str))
return summary
if __name__ == "__main__":
main()
+103
View File
@@ -0,0 +1,103 @@
"""r0701_gtaa_5th_sleeve — EQ-GTAA01 come 5° sleeve del portafoglio attivo? Valutazione ONESTA.
Contesto (diari 2026-06-22/23): GTAA (trend difensivo equity, 6 ETF, vt12%, ~30y di storia, netto
fee IB 2bps/lato) fu validato come diversificatore strutturale del book crypto (corr ~0.17-0.21,
combo Sharpe 1.60->1.81) ma lasciato in paper cross-venue (paper_combo) e MAI valutato come 5°
sleeve del portafoglio attivo (il giorno dopo arrivo' SKH01). Qui: gate onesti per lo slot.
Metodo:
* pannello = 4 sleeve attivi + GTAA (to_daily), outer-join rinormalizzato (combine_outer);
* finestra di valutazione ancorata all'inizio del book crypto (TP01 start) — NO gonfiaggio
da 23 anni di solo-GTAA pre-2019;
* peso GTAA selezionato IN-SAMPLE (Sharpe pre-2025) su griglia {10..30%}, gli altri scalati
(1-w) nessuna scelta sull'hold-out;
* verifica: hold-out 2025+, multi-cut (2023-01/2024-01/2024-07/2025-01), finestre DISGIUNTE,
per-anno, corr, is_hedge (corr tra Sharpe-anno del book e uplift-anno), plateau in w.
"""
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.portfolio.portfolio import HOLDOUT, combine_outer, metrics, to_daily
from src.portfolio.sleeves import active_sleeves
from src.portfolio.gtaa import gtaa_returns
GRID_W = [0.10, 0.15, 0.20, 0.25, 0.30]
CUTS = [pd.Timestamp(c, tz="UTC") for c in ("2023-01-01", "2024-01-01", "2024-07-01", "2025-01-01")]
DISJOINT = [("2019-03-01", "2023-01-01"), ("2023-01-01", "2025-01-01"), ("2025-01-01", "2027-01-01")]
def sh(s: pd.Series, lo=None, hi=None) -> float:
v = s
if lo is not None:
v = v[v.index >= lo]
if hi is not None:
v = v[v.index < hi]
r = np.asarray(v.dropna().values, float)
return float(r.mean() / r.std() * np.sqrt(365.25)) if len(r) > 1 and r.std() > 0 else 0.0
def main() -> None:
sl = active_sleeves()
cols = {s.name: s.daily() for s in sl}
w_cur = {s.name: s.weight for s in sl}
g = to_daily(gtaa_returns())
lo_book = min(c.index.min() for c in cols.values()) # inizio era-crypto del book
cols5 = dict(cols, GTAA01=g)
base = combine_outer(cols, w_cur, lo=lo_book)
print(f"book start {lo_book.date()} | GTAA storia {g.index.min().date()}->{g.index.max().date()}"
f" | GTAA standalone: FULL {sh(g):.2f}, pre-2025 {sh(g, hi=HOLDOUT):.2f}, "
f"era-crypto {sh(g[g.index >= lo_book]):.2f}, HOLD {sh(g, lo=HOLDOUT):.2f}")
print(f"corr(GTAA, book) era-crypto: {g.reindex(base.index).corr(base):+.3f} | "
f"hold-out: {g.reindex(base.index).loc[HOLDOUT:].corr(base.loc[HOLDOUT:]):+.3f}")
print(f"\nBASE 4-sleeve : FULL {sh(base):.3f} IS {sh(base, hi=HOLDOUT):.3f} "
f"HOLD {sh(base, lo=HOLDOUT):.3f} DD {metrics(base)['maxdd']:.1%} "
f"CAGR {metrics(base)['cagr']:+.1%}")
# griglia peso GTAA — selezione IN-SAMPLE
rows = []
for w in GRID_W:
w5 = {k: v * (1 - w) for k, v in w_cur.items()} | {"GTAA01": w}
c5 = combine_outer(cols5, w5, lo=lo_book)
rows.append(dict(w=w, IS=sh(c5, hi=HOLDOUT), FULL=sh(c5), HOLD=sh(c5, lo=HOLDOUT),
DD=metrics(c5)["maxdd"], CAGR=metrics(c5)["cagr"]))
print(f" +GTAA {w:.0%}: IS {rows[-1]['IS']:.3f} FULL {rows[-1]['FULL']:.3f} "
f"HOLD {rows[-1]['HOLD']:.3f} DD {rows[-1]['DD']:.1%} CAGR {rows[-1]['CAGR']:+.1%}")
best = max(rows, key=lambda r: r["IS"])
w = best["w"]
print(f"\ncella IN-SAMPLE: w={w:.0%} (IS {best['IS']:.3f}); plateau IS: "
+ ", ".join(f"{r['w']:.0%}:{r['IS']:.2f}" for r in rows))
w5 = {k: v * (1 - w) for k, v in w_cur.items()} | {"GTAA01": w}
c5 = combine_outer(cols5, w5, lo=lo_book)
print("\nmulti-cut (Sharpe OOS dal taglio, book+GTAA vs book):")
for cut in CUTS:
print(f" {cut.date()}: {sh(c5, lo=cut):.3f} vs {sh(base, lo=cut):.3f} "
f"delta {sh(c5, lo=cut) - sh(base, lo=cut):+.3f}")
print("finestre DISGIUNTE:")
for a, b in DISJOINT:
la, lb = pd.Timestamp(a, tz="UTC"), pd.Timestamp(b, tz="UTC")
print(f" {a[:7]}..{b[:7]}: delta {sh(c5, lo=la, hi=lb) - sh(base, lo=la, hi=lb):+.3f}")
print("per-anno (ret book -> ret book+GTAA):")
ups, shs = [], []
for y in sorted(set(base.index.year)):
by, cy = base[base.index.year == y], c5[c5.index.year == y]
rb, rc = float((1 + by).prod() - 1), float((1 + cy).prod() - 1)
ups.append(sh(cy) - sh(by)); shs.append(sh(by))
print(f" {y}: {rb:+7.1%} -> {rc:+7.1%} dSh {sh(cy) - sh(by):+.2f}")
ih = float(np.corrcoef(shs, ups)[0, 1]) if len(ups) > 2 else float("nan")
print(f"is_hedge check — corr(Sharpe-anno book, uplift-anno): {ih:+.2f} "
f"(molto negativa = hedge, non alpha)")
if __name__ == "__main__":
main()
+472
View File
@@ -0,0 +1,472 @@
"""r0701_portfolio_opt — MIGLIORARE IL PORTAFOGLIO ATTIVO SENZA NUOVE STRATEGIE (2026-07-01).
(A) RI-OTTIMIZZAZIONE DEI PESI STATICI dei 4 sleeve (TP01/XS01/VRP01/SKH01).
I pesi correnti (41.25/18.75/15/25) sono ad-hoc. Test onesto:
- ottimizza SOLO in-sample (pre-cut), valuta OOS (post-cut), multi-cut {2024-01, 2024-07, 2025-01};
- criteri: MAXSH (max-Sharpe simulato), RP (inverse-vol), ERC (equal-risk-contribution),
MINVAR-R (min-varianza con vincolo di ritorno >= ritorno dei pesi correnti in-sample);
- null di riferimento: EW (25x4) e CURRENT (pesi correnti);
- vincoli: 0.05 <= w_i <= 0.60, sum(w)=1;
- sleeve con < MIN_IS_DAYS giorni in-sample al cut (XS01 al 2024-01) -> PINNED al peso corrente
(l'ottimizzatore non ha dati per giudicarli), gli altri ottimizzati nel budget residuo;
- il combine replica ESATTAMENTE src/portfolio/portfolio.combined_daily (outer-join,
pesi rinormalizzati per-riga sugli sleeve attivi).
Verdetto: MIGLIORA solo se l'uplift OOS vs CURRENT e' PERSISTENTE (positivo a ogni cut),
non su una finestra sola.
(B) GUARDIA-DRAWDOWN a livello portafoglio (replica del soft-guard vincente del tail-hedge lab
2026-06-23, portato sul 4-sleeve). CAUSALE: l'esposizione del giorno D usa l'equity fino a D-1.
Trigger/re-entry sul NAV OMBRA (equity non-guardata: lezione stops_lab sull'equity congelata
a expo=0 il DD non rientra mai). Isteresi: de-risk quando DD>X, re-risk quando DD<0.4*X.
Griglia modesta X in {3,4,5,6}%, derisk in {0, 0.5}. Selezione cella IN-SAMPLE (pre-cut,
criterio Calmar = CAGR/MaxDD), verifica OOS multi-cut.
NULL DECISIVO (lezione tp01-dvol-overlay): esposizione COSTANTE c calibrata in-sample per
pareggiare il MaxDD della guardia lo Sharpe di c*r e' INVARIANTE, quindi la guardia "vale"
solo se a pari MaxDD trattiene piu' CAGR/Sharpe del semplice de-levering.
(C) Combinazione A+B se entrambe reggono.
NB (contesto): il thread meta-allocazione (2026-06-29) ha gia' mostrato che l'allocazione DINAMICA
fra sleeve perde vs pesi fissi qui NON la rifacciamo: (A) e' statico, (B) e' un overlay di
esposizione aggregata, non riallocazione fra sleeve.
Solo ricerca: NON tocca src/ ne' scripts/live/. Propone pesi, non li applica.
uv run python scripts/research/r0701_portfolio_opt.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
import pandas as pd
from scipy.optimize import minimize
from src.portfolio.sleeves import active_sleeves
from src.portfolio.portfolio import HOLDOUT, DAYS_PER_YEAR
CUTS = [pd.Timestamp(c, tz="UTC") for c in ("2024-01-01", "2024-07-01", "2025-01-01")]
LO_W, HI_W = 0.05, 0.60 # vincoli pesi
MIN_IS_DAYS = 120 # sotto -> sleeve PINNED al peso corrente (niente dati per giudicarlo)
GUARD_TRIGGERS = (0.03, 0.04, 0.05, 0.06)
GUARD_DERISK = (0.0, 0.5)
GUARD_REC_FRAC = 0.4 # re-risk quando DD < 0.4*trigger (isteresi, come tail-hedge lab)
# --------------------------------------------------------------------------- dati
def load_matrix() -> tuple[pd.DataFrame, list[str], np.ndarray]:
"""Matrice daily OUTER-join dei 4 sleeve (NaN dove lo sleeve non e' ancora partito)."""
sl = active_sleeves()
names = [s.name for s in sl]
w_cur = np.array([s.weight for s in sl], float)
w_cur = w_cur / w_cur.sum()
J = pd.concat({s.name: s.daily() for s in sl}, axis=1, join="outer").sort_index()
J = J[J.notna().any(axis=1)]
return J, names, w_cur
def combo(J: pd.DataFrame, w: np.ndarray) -> pd.Series:
"""Replica ESATTA di portfolio.combined_daily: pesi rinormalizzati per-riga sugli attivi."""
active = J.notna().values * w
rowsum = active.sum(axis=1, keepdims=True)
wnorm = np.divide(active, rowsum, out=np.zeros_like(active), where=rowsum > 0)
return pd.Series(np.nansum(np.nan_to_num(J.values) * wnorm, axis=1), index=J.index)
def met(r: pd.Series | np.ndarray) -> dict:
v = np.asarray(pd.Series(r).dropna().values, float)
if len(v) < 20 or v.std() == 0:
return dict(sh=0.0, cagr=0.0, dd=0.0, calmar=0.0, n=len(v))
eq = np.cumprod(1 + v); pk = np.maximum.accumulate(eq)
yrs = len(v) / DAYS_PER_YEAR
cagr = float(eq[-1] ** (1 / yrs) - 1) if eq[-1] > 0 else -1.0
dd = float(np.max((pk - eq) / pk))
return dict(sh=float(v.mean() / v.std() * np.sqrt(DAYS_PER_YEAR)), cagr=cagr, dd=dd,
calmar=(cagr / dd if dd > 0 else 0.0), n=len(v))
def fmt_w(names, w):
return " ".join(f"{n.split('_')[0]} {x*100:.1f}%" for n, x in zip(names, w))
# --------------------------------------------------------------------------- (A) ottimizzatori
def _pinned_mask(J_is: pd.DataFrame, w_cur: np.ndarray) -> np.ndarray:
"""True = sleeve senza abbastanza storia in-sample -> peso fissato al corrente."""
return np.array([J_is[c].notna().sum() < MIN_IS_DAYS for c in J_is.columns])
def _solve(J_is, w_cur, objective, extra_cons=None, hi_bounds=None) -> np.ndarray:
"""SLSQP su pesi liberi (i pinned restano al corrente), bounds+somma, multi-start.
hi_bounds: array di upper-bound per-sleeve (default HI_W) per i cap STRUTTURALI."""
k = len(w_cur)
hi = np.full(k, HI_W) if hi_bounds is None else np.asarray(hi_bounds, float)
pin = _pinned_mask(J_is, w_cur)
free = ~pin
budget = 1.0 - w_cur[pin].sum()
nf = int(free.sum())
def full_w(wf):
w = w_cur.copy(); w[free] = wf
return w
cons = [dict(type="eq", fun=lambda wf: wf.sum() - budget)]
if extra_cons:
cons += [dict(type="ineq", fun=lambda wf, f=f: f(full_w(wf))) for f in extra_cons]
bounds = [(LO_W, float(h)) for h in hi[free]]
hif = hi[free]
starts = [w_cur[free], np.full(nf, budget / nf)]
rng = np.random.default_rng(7)
for _ in range(4):
x = rng.uniform(LO_W, hif); starts.append(x / x.sum() * budget)
best, bval = w_cur.copy(), np.inf
for x0 in starts:
x0 = np.clip(x0, LO_W, hif); x0 = x0 / x0.sum() * budget
res = minimize(lambda wf: objective(full_w(wf)), x0, method="SLSQP",
bounds=bounds, constraints=cons,
options=dict(maxiter=300, ftol=1e-10))
if res.success and res.fun < bval:
bval, best = res.fun, full_w(res.x)
best = np.clip(best, LO_W, hi)
return best / best.sum()
def opt_maxsh(J_is, w_cur):
return _solve(J_is, w_cur, lambda w: -met(combo(J_is, w))["sh"])
def opt_minvar_ret(J_is, w_cur):
"""Min-varianza con vincolo: media in-sample >= media dei pesi correnti in-sample."""
mu_cur = float(combo(J_is, w_cur).mean())
return _solve(J_is, w_cur, lambda w: float(combo(J_is, w).var()),
extra_cons=[lambda w: float(combo(J_is, w).mean()) - mu_cur])
def opt_rp(J_is, w_cur):
"""Inverse-vol (risk-parity naive) sulla vol in-sample di ogni sleeve (giorni attivi propri)."""
pin = _pinned_mask(J_is, w_cur); free = ~pin
vol = np.array([J_is[c].dropna().std() for c in J_is.columns])
w = w_cur.copy()
iv = 1.0 / np.where(vol[free] > 0, vol[free], np.inf)
w[free] = iv / iv.sum() * (1.0 - w_cur[pin].sum())
for _ in range(50): # clip ai bounds + rinormalizza
w = np.clip(w, LO_W, HI_W)
if abs(w.sum() - 1.0) < 1e-9:
break
adj = ~np.isclose(w, LO_W) & ~np.isclose(w, HI_W)
if not adj.any():
break
w[adj] += (1.0 - w.sum()) * (w[adj] / w[adj].sum())
return w / w.sum()
def opt_erc(J_is, w_cur):
"""Equal-risk-contribution sulla cov in-sample pairwise-complete (submatrice dei non-pinned)."""
pin = _pinned_mask(J_is, w_cur)
cov = J_is.cov().values # pairwise-complete
idx = np.where(~pin)[0]
S = cov[np.ix_(idx, idx)]
S = np.nan_to_num(S, nan=0.0)
def obj(w):
wf = w[idx]
port = wf @ S @ wf
if port <= 0:
return 1e6
rc = wf * (S @ wf) / port
return float(np.sum((rc - 1.0 / len(idx)) ** 2))
return _solve(J_is, w_cur, obj)
def _struct_caps(J_is) -> np.ndarray:
"""Cap STRUTTURALI (prudenza, non statistica): VRP01 <= 15% (sleeve MODELLATO su DVOL ATM,
stress-f non catturato 'niente short-vol da modello in deploy'), XS01 <= 25% (STAT-MODE,
storia ~2.5 anni). Gli ottimizzatori naive caricano VRP01 (vol 7.7% ma e' la vol del MODELLO)."""
return np.array([0.15 if c.startswith("VRP") else (0.25 if c.startswith("XS") else HI_W)
for c in J_is.columns])
def opt_maxsh_struct(J_is, w_cur):
return _solve(J_is, w_cur, lambda w: -met(combo(J_is, w))["sh"], hi_bounds=_struct_caps(J_is))
CRITERIA = [("MAXSH", opt_maxsh), ("RP-invvol", opt_rp), ("ERC", opt_erc), ("MINVAR-R", opt_minvar_ret),
("MAXSH-STR", opt_maxsh_struct)]
# --------------------------------------------------------------------------- (B) guardia-DD
def dd_guard(r: pd.Series, trigger: float, derisk: float, rec_frac: float = GUARD_REC_FRAC):
"""Soft-guard causale: expo[t] decisa dal DD del NAV OMBRA (non-guardato) fino a t-1.
DD>trigger -> expo=derisk; DD<rec_frac*trigger -> expo=1 (isteresi). Ritorna (serie, expo)."""
v = r.values
eq = np.cumprod(1 + v); pk = np.maximum.accumulate(eq)
n = len(v); expo = np.ones(n); on = True
for i in range(1, n):
ddi = (pk[i - 1] - eq[i - 1]) / pk[i - 1]
if ddi > trigger:
on = False
elif ddi < trigger * rec_frac:
on = True
expo[i] = 1.0 if on else derisk
return pd.Series(expo * v, index=r.index), expo
def const_expo_match_dd(r: pd.Series, target_dd: float) -> float:
"""Esposizione costante c (in-sample) che pareggia il MaxDD target — il null de-levering."""
lo, hi = 0.05, 1.0
if met(r)["dd"] <= target_dd:
return 1.0
for _ in range(40):
mid = 0.5 * (lo + hi)
if met(pd.Series(mid * r.values, index=r.index))["dd"] > target_dd:
hi = mid
else:
lo = mid
return lo
# --------------------------------------------------------------------------- report
def section_A(J, names, w_cur):
print("\n" + "=" * 108)
print(" (A) RI-OTTIMIZZAZIONE PESI STATICI — ottimizza IN-SAMPLE (pre-cut), valuta OOS (post-cut), multi-cut")
print("=" * 108)
# correlazioni e vol per contesto (full, pairwise)
C = J.corr()
print("\n corr pairwise (full):")
for i, a in enumerate(names):
print(" " + a.split("_")[0].ljust(6) + " ".join(f"{C.iloc[i, j]:+.2f}" for j in range(len(names))))
print(" vol annua per-sleeve (full): " +
" ".join(f"{n.split('_')[0]} {J[n].dropna().std()*np.sqrt(DAYS_PER_YEAR)*100:.1f}%" for n in names))
candidates = {} # nome -> pesi ottimizzati su pre-HOLDOUT (headline)
J_is_main = J[J.index < HOLDOUT]
for cname, fn in CRITERIA:
candidates[cname] = fn(J_is_main, w_cur)
candidates["EW"] = np.full(len(names), 1.0 / len(names))
caps = _struct_caps(J)
w_ewstr = np.full(len(names), 1.0 / len(names)) # EW sotto i cap strutturali (null senza fit)
for _ in range(20):
over = w_ewstr > caps + 1e-12
if not over.any():
break
excess = float((w_ewstr[over] - caps[over]).sum())
w_ewstr[over] = caps[over]
room = w_ewstr < caps - 1e-12 # redistribuisci SOLO a chi ha spazio
w_ewstr[room] += excess / max(int(room.sum()), 1)
candidates["EW-STR"] = w_ewstr
candidates["CURRENT"] = w_cur
print(f"\n PESI (ottimizzati su in-sample pre-{HOLDOUT.date()}):")
for cname, w in candidates.items():
print(f" {cname:<10s} {fmt_w(names, w)}")
# headline: FULL + HOLD-OUT
print(f"\n {'criterio':<10s} {'FULL Sh':>8s} {'FULL DD':>8s} {'FULL CAGR':>9s} | {'HOLD Sh':>8s} {'HOLD DD':>8s} {'HOLD CAGR':>9s}")
rows = {}
for cname, w in candidates.items():
f = met(combo(J, w)); h = met(combo(J[J.index >= HOLDOUT], w))
rows[cname] = (f, h)
print(f" {cname:<10s} {f['sh']:>8.2f} {f['dd']*100:>7.1f}% {f['cagr']*100:>+8.1f}% |"
f" {h['sh']:>8.2f} {h['dd']*100:>7.1f}% {h['cagr']*100:>+8.1f}%")
# multi-cut: ottimizza su <cut, valuta su >=cut, uplift vs CURRENT sulla stessa finestra OOS
print("\n MULTI-CUT (pesi ri-ottimizzati a ogni cut sul solo pre-cut; ΔSh OOS vs CURRENT):")
header = f" {'criterio':<10s}" + "".join(f" | {c.date()} Sh_oos ΔSh " for c in CUTS)
print(header)
persist = {}
fixed = {"EW": candidates["EW"], "EW-STR": candidates["EW-STR"]}
for cname, fn in CRITERIA + [("EW", None), ("EW-STR", None)]:
cells = []
ok = True
for cut in CUTS:
J_is, J_oos = J[J.index < cut], J[J.index >= cut]
w = fixed[cname] if cname in fixed else fn(J_is, w_cur)
sh_o = met(combo(J_oos, w))["sh"]
sh_c = met(combo(J_oos, w_cur))["sh"]
d = sh_o - sh_c
ok &= d > 0
cells.append(f" | {sh_o:>10.2f} {d:>+5.2f}")
persist[cname] = ok
print(f" {cname:<10s}" + "".join(cells) + (" <-- PERSISTENTE" if ok else ""))
print(" ⚠ caveat: le 3 finestre OOS sono ANNIDATE (tutte contengono il 2025-26) — non 3 conferme")
print(" indipendenti. Il test disgiunto vero e' il per-anno qui sotto.")
# per-anno (finestre DISGIUNTE): ritorno annuo del candidato vs CURRENT, pesi fissi pre-2025
print("\n PER-ANNO (pesi headline fissi; Δret vs CURRENT, finestre disgiunte):")
sel = ["MAXSH", "MINVAR-R", "MAXSH-STR", "EW", "EW-STR"]
years = sorted(set(J.index.year))
print(" " + "anno".ljust(6) + "".join(f"{c:>12s}" for c in ["CURRENT"] + sel))
wins = {c: 0 for c in sel}
for y in years:
Jy = J[J.index.year == y]
base_ret = float(np.prod(1 + combo(Jy, w_cur).values) - 1)
row = f" {y} {base_ret*100:>+10.1f}%"
for c in sel:
ry = float(np.prod(1 + combo(Jy, candidates[c]).values) - 1)
wins[c] += ry > base_ret
row += f"{(ry-base_ret)*100:>+11.1f}p"
print(row)
print(" anni vinti vs CURRENT: " + " ".join(f"{c} {wins[c]}/{len(years)}" for c in sel))
# verdetto: la persistenza statistica NON basta — i pesi devono rispettare i cap STRUTTURALI
# (VRP01<=15% perche' MODELLATO, XS01<=25% perche' STAT-MODE/storia corta), altrimenti l'uplift
# sta comprando rischio-modello che il backtest non vede (punto cieco tipo CC01).
winners = [c for c, ok in persist.items() if ok]
respects = {c: bool(np.all(candidates[c] <= caps + 1e-3)) for c in candidates} # tol 0.1pp (jitter clip/renorm)
struct_winners = [c for c in winners if respects[c]]
print("\n cap strutturali: " + fmt_w(names, caps))
if winners:
print(" persistenti: " + ", ".join(f"{c} ({'dentro i cap' if respects[c] else 'VIOLA i cap'})"
for c in winners))
if struct_winners:
wbest = candidates[struct_winners[0]]
fb, fc = met(combo(J, wbest)), met(combo(J, w_cur))
hb, hc = met(combo(J[J.index >= HOLDOUT], wbest)), met(combo(J[J.index >= HOLDOUT], w_cur))
verdict = (f"MIGLIORA (OOS) — {struct_winners} persistente E dentro i cap strutturali. "
f"Pesi candidati ({struct_winners[0]}): {fmt_w(names, wbest)}.\n"
f" TRADE-OFF onesto: HOLD Sh {hc['sh']:.2f}->{hb['sh']:.2f}, HOLD CAGR "
f"{hc['cagr']*100:+.1f}%->{hb['cagr']*100:+.1f}%, MA FULL Sh {fc['sh']:.2f}->{fb['sh']:.2f} "
f"e FULL DD {fc['dd']*100:.1f}%->{fb['dd']*100:.1f}% — l'uplift e' un TILT di ritorno verso "
f"gli sleeve research-grade (piu' SKH01/XS01, meno TP01), non un free lunch risk-adjusted. "
f"NB: nessun OTTIMIZZATORE aggiunge nulla oltre questo null parameter-free "
f"(MAXSH in-sample vince 1/8 anni: l'ottimizzazione qui overfitta, la de-concentrazione no).")
elif winners:
verdict = ("NON ADOTTABILE — l'uplift persistente esiste SOLO violando i cap strutturali "
"(tutti i vincitori caricano VRP01>15%, sleeve MODELLATO il cui rischio di coda "
"il backtest non cattura). Dentro i cap l'ottimizzatore converge ~ai pesi correnti "
"e non e' persistente -> per il book reale I PESI CORRENTI REGGONO.")
else:
verdict = "NON MIGLIORA — nessun criterio batte i pesi CORRENTI in modo persistente. I pesi correnti reggono."
print("\n VERDETTO (A): " + verdict)
return candidates, persist, struct_winners
def section_B(J, names, w_cur, label="CURRENT"):
print("\n" + "=" * 108)
print(f" (B) GUARDIA-DRAWDOWN a livello portafoglio (pesi {label}) — trigger X, de-risk d, isteresi 0.4X")
print("=" * 108)
r_full = combo(J, w_cur)
# griglia completa su FULL/IS/HOLD per trasparenza (selezione = solo in-sample)
r_is = r_full[r_full.index < HOLDOUT]
r_ho = r_full[r_full.index >= HOLDOUT]
b_is, b_ho, b_f = met(r_is), met(r_ho), met(r_full)
print(f"\n baseline IS Sh {b_is['sh']:.2f} DD {b_is['dd']*100:.1f}% CAGR {b_is['cagr']*100:+.1f}%"
f" | HOLD Sh {b_ho['sh']:.2f} DD {b_ho['dd']*100:.1f}% CAGR {b_ho['cagr']*100:+.1f}%"
f" | FULL Sh {b_f['sh']:.2f} DD {b_f['dd']*100:.1f}%")
print(f"\n {'cella':<16s} {'IS Sh':>6s} {'IS DD':>6s} {'IS CAGR':>8s} {'IS Calmar':>9s} {'%off':>5s} |"
f" {'HOLD Sh':>7s} {'HOLD DD':>7s} {'HOLD CAGR':>9s}")
grid = {}
for trig in GUARD_TRIGGERS:
for dr in GUARD_DERISK:
g_full, expo = dd_guard(r_full, trig, dr)
g_is = g_full[g_full.index < HOLDOUT]; g_ho = g_full[g_full.index >= HOLDOUT]
mi, mh = met(g_is), met(g_ho)
off = float((expo[: len(r_is)] < 1.0).mean())
grid[(trig, dr)] = dict(is_=mi, ho=mh, full=met(g_full))
print(f" X={trig*100:.0f}% d={dr:<4.1f} {mi['sh']:>6.2f} {mi['dd']*100:>5.1f}% {mi['cagr']*100:>+7.1f}%"
f" {mi['calmar']:>9.2f} {off*100:>4.0f}% | {mh['sh']:>7.2f} {mh['dd']*100:>6.1f}% {mh['cagr']*100:>+8.1f}%")
# selezione IN-SAMPLE (Calmar), poi null de-levering a pari MaxDD in-sample
best_cell = max(grid, key=lambda k: grid[k]["is_"]["calmar"])
trig, dr = best_cell
print(f"\n cella selezionata IN-SAMPLE (max Calmar): X={trig*100:.0f}% derisk={dr}")
tgt_dd = grid[best_cell]["is_"]["dd"]
c = const_expo_match_dd(r_is, tgt_dd)
cnull_full = pd.Series(c * r_full.values, index=r_full.index)
n_is, n_ho = met(cnull_full[cnull_full.index < HOLDOUT]), met(cnull_full[cnull_full.index >= HOLDOUT])
g = grid[best_cell]
print(f" NULL de-levering: expo costante c={c:.2f} pareggia il MaxDD IS della guardia ({tgt_dd*100:.1f}%)")
print(f" {'':<14s} {'IS Sh':>6s} {'IS CAGR':>8s} | {'HOLD Sh':>7s} {'HOLD DD':>7s} {'HOLD CAGR':>9s}")
print(f" guardia {g['is_']['sh']:>6.2f} {g['is_']['cagr']*100:>+7.1f}% | {g['ho']['sh']:>7.2f}"
f" {g['ho']['dd']*100:>6.1f}% {g['ho']['cagr']*100:>+8.1f}%")
print(f" const c={c:.2f} {n_is['sh']:>6.2f} {n_is['cagr']*100:>+7.1f}% | {n_ho['sh']:>7.2f}"
f" {n_ho['dd']*100:>6.1f}% {n_ho['cagr']*100:>+8.1f}%")
# multi-cut: selezione sul pre-cut, valutazione OOS post-cut (guardia vs baseline vs null)
inert = True
print("\n MULTI-CUT (cella riscelta a ogni cut sul solo pre-cut; OOS post-cut):")
print(f" {'cut':<12s} {'cella':<14s} {'OOS Sh(g)':>9s} {'OOS Sh(b)':>9s} {'ΔSh':>6s} {'OOS DD(g)':>9s}"
f" {'OOS DD(b)':>9s} {'ΔCAGR':>7s} {'null c: ΔSh':>11s}")
persist_sh, persist_dd = True, True
for cut in CUTS:
r_pre = r_full[r_full.index < cut]
cells = {}
for t in GUARD_TRIGGERS:
for d in GUARD_DERISK:
gpre, _ = dd_guard(r_pre, t, d)
cells[(t, d)] = met(gpre)["calmar"]
(t_, d_) = max(cells, key=cells.get)
g_full, _ = dd_guard(r_full, t_, d_) # guard sul path completo (stato causale)
g_oos = g_full[g_full.index >= cut]
b_oos = r_full[r_full.index >= cut]
gm, bm = met(g_oos), met(b_oos)
cc = const_expo_match_dd(r_pre, met(dd_guard(r_pre, t_, d_)[0])["dd"])
nm = met(pd.Series(cc * b_oos.values, index=b_oos.index))
dsh = gm["sh"] - bm["sh"]; ddd = gm["dd"] - bm["dd"]; dcagr = gm["cagr"] - bm["cagr"]
persist_sh &= dsh >= 0; persist_dd &= ddd < 0
inert &= abs(dsh) < 0.005 and abs(ddd) < 1e-4
print(f" {str(cut.date()):<12s} X={t_*100:.0f}% d={d_:<6.1f} {gm['sh']:>9.2f} {bm['sh']:>9.2f} {dsh:>+6.2f}"
f" {gm['dd']*100:>8.1f}% {bm['dd']*100:>8.1f}% {dcagr*100:>+6.1f}pp {gm['sh']-nm['sh']:>+11.2f}")
if inert:
verdict = ("NON MIGLIORA — INERTE OOS: la cella scelta in-sample non scatta MAI post-cut "
"(il DD OOS del 4-sleeve resta sotto il trigger: la diversificazione fa gia' il lavoro "
"della guardia). Trigger piu' stretti (X=3-4%) scattano ma COSTANO Sharpe su HOLD. "
"Coerente col thread meta-allocazione 2026-06-29 (drawdown-control ridondante).")
elif persist_sh and persist_dd:
verdict = f"MIGLIORA — la guardia X={trig*100:.0f}%/d={dr} taglia il DD OOS senza costo Sharpe persistente"
elif persist_dd:
verdict = ("TAGLIA IL DD MA COSTA — DD ridotto a ogni cut ma Sharpe/CAGR inferiori: "
"trade-off assicurativo, confrontare col null de-levering prima di adottarla")
else:
verdict = "NON MIGLIORA — la guardia non riduce il DD OOS in modo persistente (o e' solo de-levering)"
print("\n VERDETTO (B): " + verdict)
return grid, best_cell
def section_C(J, names, w_cur, candidates, struct_winners, grid, best_cell):
print("\n" + "=" * 108)
print(" (C) COMBINAZIONE A+B")
print("=" * 108)
wA = candidates[struct_winners[0]] if struct_winners else w_cur
labA = struct_winners[0] if struct_winners else "CURRENT (A non adottabile)"
trig, dr = best_cell
r = combo(J, wA)
g, _ = dd_guard(r, trig, dr)
for lbl, s in [("baseline CURRENT", combo(J, w_cur)),
(f"pesi {labA}", r),
(f"pesi {labA} + guard X={trig*100:.0f}%/d={dr}", g)]:
f = met(s); h = met(s[s.index >= HOLDOUT])
print(f" {lbl:<52s} FULL Sh {f['sh']:>5.2f} DD {f['dd']*100:>4.1f}% CAGR {f['cagr']*100:>+5.1f}%"
f" | HOLD Sh {h['sh']:>5.2f} DD {h['dd']*100:>4.1f}% CAGR {h['cagr']*100:>+5.1f}%")
if not struct_winners:
print(" -> (A) non adottabile e (B) inerte OOS: la combinazione non ha una cella da proporre.")
def main():
print("=" * 108)
print(" r0701_portfolio_opt — pesi statici + guardia-DD sul portafoglio attivo a 4 sleeve")
print(f" hold-out bloccato {HOLDOUT.date()}+ | vincoli pesi [{LO_W},{HI_W}] | cuts {[str(c.date()) for c in CUTS]}")
print("=" * 108)
J, names, w_cur = load_matrix()
print(f"\n sleeve: {names}")
print(f" pesi CORRENTI: {fmt_w(names, w_cur)}")
print(f" finestra: {J.index[0].date()} -> {J.index[-1].date()} ({len(J)} giorni)")
for n in names:
s = J[n].dropna()
print(f" {n:<16s} dal {s.index[0].date()} ({len(s)} giorni)")
b_f = met(combo(J, w_cur)); b_h = met(combo(J[J.index >= HOLDOUT], w_cur))
print(f"\n BASELINE (pesi correnti): FULL Sh {b_f['sh']:.2f} DD {b_f['dd']*100:.1f}% CAGR {b_f['cagr']*100:+.1f}%"
f" | HOLD Sh {b_h['sh']:.2f} DD {b_h['dd']*100:.1f}% CAGR {b_h['cagr']*100:+.1f}%")
candidates, persistA, struct_winners = section_A(J, names, w_cur)
grid, best_cell = section_B(J, names, w_cur)
section_C(J, names, w_cur, candidates, struct_winners, grid, best_cell)
if __name__ == "__main__":
main()
+304
View File
@@ -0,0 +1,304 @@
"""r0701_portfolio_skeptic — VERIFICA AVVERSARIALE del tilt EW-STR (TP30/XS25/VRP15/SKH30).
Oggetto: la proposta di r0701_portfolio_opt (diario 2026-07-01-portfolio-weights-ddguard.md) di
spostare i pesi del portafoglio a 4 sleeve da CURRENT (41.25/18.75/15/25) a EW-STR (30/25/15/30).
Claim: HOLD Sh 2.21->2.35, HOLD CAGR +16.0->+19.7%, multi-cut +0.06/+0.13/+0.14, 7/8 anni vinti.
Linee di attacco (agente scettico):
1. RIPRODUZIONE indipendente via src.portfolio.StrategyPortfolio (path di PRODUZIONE, non lo
script dell'agente).
2. SELEZIONE-SULL'HOLD-OUT DI 2° ORDINE via SKH01/XS01: SKH01 fu ammesso (2026-06-23) perche'
alzava l'hold-out 2025-26; XS01 fu affinato (blend+gate) conoscendo l'hold-out. Un tilt verso
di loro eredita quella selezione? Test: (a) per-anno SOLO 2019-2024; (b) tilt scomposti
(solo-XS con SKH pinned 25, solo-SKH con XS pinned); (c) finestre OOS DISGIUNTE (i 3 cut del
diario sono ANNIDATI); (d) contributo per-sleeve all'uplift hold-out.
3. "7/8 ANNI VINTI": quale metrica (ritorno, non Sharpe), margini per anno, Sharpe per-anno.
4. PLATEAU: griglia 2.5pp fra CURRENT e EW-STR (TP x SKH, VRP=15, XS=residuo cap 25).
5. REALISMO: pesi RINORMALIZZATI per era (outer-join: nel 2019-20 attivi solo TP+SKH!), haircut
d'esecuzione su SKH01/XS01 come DRAG (r' = r - h*mean(r), modello-costi, non de-levering).
6. FORKING PATHS: quante config viste sull'hold-out; null di tilt CASUALI cap-respecting — se
quasi ogni tilt anti-TP01 batte CURRENT sull'hold-out, il claim e' generico, non informativo.
Solo lettura/analisi: NON tocca src/ ne' scripts/live/.
uv run python scripts/research/r0701_portfolio_skeptic.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
import pandas as pd
from src.portfolio.sleeves import active_sleeves
from src.portfolio.portfolio import StrategyPortfolio, HOLDOUT, DAYS_PER_YEAR, metrics
CUTS = [pd.Timestamp(c, tz="UTC") for c in ("2024-01-01", "2024-07-01", "2025-01-01")]
LO_W = 0.05
# pesi per PREFISSO sleeve (ordine risolto sui nomi del registry)
W_CURRENT = {"TP01": 0.4125, "XS01": 0.1875, "VRP01": 0.15, "SKH01": 0.25}
W_EWSTR = {"TP01": 0.30, "XS01": 0.25, "VRP01": 0.15, "SKH01": 0.30}
W_ONLY_XS = {"TP01": 0.35, "XS01": 0.25, "VRP01": 0.15, "SKH01": 0.25} # SKH pinned al corrente
W_ONLY_SKH = {"TP01": 0.3625, "XS01": 0.1875, "VRP01": 0.15, "SKH01": 0.30} # XS pinned al corrente
CAPS = {"TP01": 0.60, "XS01": 0.25, "VRP01": 0.15, "SKH01": 0.60}
def prefix(name: str) -> str:
return name.split("_")[0]
def wvec(names: list[str], wd: dict) -> np.ndarray:
v = np.array([wd[prefix(n)] for n in names], float)
return v / v.sum()
def combo(J: pd.DataFrame, w: np.ndarray) -> pd.Series:
"""Combine outer-join con pesi rinormalizzati per-riga (stessa semantica di combined_daily;
verificata sotto contro il path di produzione)."""
act = J.notna().values * w
rs = act.sum(axis=1, keepdims=True)
wn = np.divide(act, rs, out=np.zeros_like(act), where=rs > 0)
return pd.Series(np.nansum(np.nan_to_num(J.values) * wn, axis=1), index=J.index)
def met(s) -> dict:
return metrics(pd.Series(s).dropna()) # metriche di PRODUZIONE (src.portfolio)
def sh(s) -> float:
return met(s)["sharpe"]
def yr_ret(s) -> float:
v = pd.Series(s).dropna().values
return float(np.prod(1 + v) - 1)
def main():
print("=" * 110)
print(" r0701_portfolio_skeptic — verifica avversariale EW-STR (30/25/15/30) vs CURRENT (41.25/18.75/15/25)")
print("=" * 110)
sl = active_sleeves()
names = [s.name for s in sl]
J = pd.concat({s.name: s.daily() for s in sl}, axis=1, join="outer").sort_index()
J = J[J.notna().any(axis=1)]
w_cur = wvec(names, W_CURRENT)
w_ew = wvec(names, W_EWSTR)
print(f"\n finestra {J.index[0].date()} -> {J.index[-1].date()} ({len(J)} giorni)")
for n in names:
s = J[n].dropna()
print(f" {n:<16s} dal {s.index[0].date()} ({len(s)} giorni)")
# ------------------------------------------------ 1) RIPRODUZIONE (path di produzione)
print("\n" + "-" * 110)
print(" [1] RIPRODUZIONE INDIPENDENTE — StrategyPortfolio.combined_daily (produzione), non lo script agente")
print("-" * 110)
for lbl, wd in [("CURRENT", W_CURRENT), ("EW-STR", W_EWSTR)]:
for s, sleeve_w in zip(sl, [wd[prefix(n)] for n in names]):
s.weight = sleeve_w
port = StrategyPortfolio(sl)
full = port.combined_daily()
hold = port.combined_daily(lo=HOLDOUT)
# check che il mio combine coincida col path di produzione
mine = combo(J, wvec(names, wd))
dmax = float(np.max(np.abs(mine.values - full.values)))
f, h = met(full), met(hold)
print(f" {lbl:<8s} FULL Sh {f['sharpe']:.2f} DD {f['maxdd']*100:.1f}% CAGR {f['cagr']*100:+.1f}%"
f" | HOLD Sh {h['sharpe']:.2f} DD {h['maxdd']*100:.1f}% CAGR {h['cagr']*100:+.1f}%"
f" (|combine mio - produzione|max = {dmax:.2e})")
print(" multi-cut ΔSh OOS EW-STR vs CURRENT (finestre ANNIDATE, come nel diario):")
for cut in CUTS:
Jo = J[J.index >= cut]
d = sh(combo(Jo, w_ew)) - sh(combo(Jo, w_cur))
print(f" cut {cut.date()} ΔSh {d:+.2f}")
# ------------------------------------------------ 2) SELEZIONE-SULL'HOLD-OUT DI 2° ORDINE
print("\n" + "-" * 110)
print(" [2] SELEZIONE HOLD-OUT DI 2° ORDINE — SKH01 ammesso PERCHE' alzava l'hold-out; XS01 affinato idem")
print("-" * 110)
# (d prima: contesto) Sharpe standalone per-sleeve, pre-2025 vs hold-out
print(" Sharpe standalone per sleeve (pre-2025 | hold-out 2025+):")
for n in names:
s = J[n].dropna()
print(f" {n:<16s} IS {sh(s[s.index < HOLDOUT]):>5.2f} | HOLD {sh(s[s.index >= HOLDOUT]):>5.2f}")
# (a) per-anno SOLO 2019-2024 (pre-selezione di SKH01/holdout)
print("\n (a) per-anno SOLO 2019-2024 (finestra NON toccata dalla selezione hold-out):")
print(f" {'anno':<6s} {'ret CUR':>9s} {'ret EW-STR':>10s} {'Δret':>8s} | {'Sh CUR':>7s} {'Sh EW-STR':>9s} {'ΔSh':>6s}")
wins_ret_pre = wins_sh_pre = 0
years_pre = [y for y in sorted(set(J.index.year)) if y < 2025]
for y in years_pre:
Jy = J[J.index.year == y]
rc, re = combo(Jy, w_cur), combo(Jy, w_ew)
dr = yr_ret(re) - yr_ret(rc); dsh = sh(re) - sh(rc)
wins_ret_pre += dr > 0; wins_sh_pre += dsh > 0
print(f" {y:<6d} {yr_ret(rc)*100:>+8.1f}% {yr_ret(re)*100:>+9.1f}% {dr*100:>+7.1f}p |"
f" {sh(rc):>7.2f} {sh(re):>9.2f} {dsh:>+6.2f}")
print(f" -> vittorie EW-STR 2019-2024: ritorno {wins_ret_pre}/{len(years_pre)}, Sharpe {wins_sh_pre}/{len(years_pre)}")
Jpre = J[J.index < HOLDOUT]
print(f" pre-2025 aggregato: Sh CUR {sh(combo(Jpre, w_cur)):.2f} vs EW-STR {sh(combo(Jpre, w_ew)):.2f}"
f" (ΔSh {sh(combo(Jpre, w_ew)) - sh(combo(Jpre, w_cur)):+.2f})")
# (b) tilt scomposti
print("\n (b) tilt SCOMPOSTI (chi porta l'uplift?): HOLD Sh + multi-cut ΔSh vs CURRENT")
for lbl, wd in [("solo-XS (SKH pinned 25): TP35 /XS25/VRP15/SKH25", W_ONLY_XS),
("solo-SKH (XS pinned 18.75): TP36.25/XS18.75/VRP15/SKH30", W_ONLY_SKH),
("EW-STR: TP30 /XS25/VRP15/SKH30", W_EWSTR)]:
w = wvec(names, wd)
hold_sh = sh(combo(J[J.index >= HOLDOUT], w))
cuts_d = [sh(combo(J[J.index >= c], w)) - sh(combo(J[J.index >= c], w_cur)) for c in CUTS]
print(f" {lbl:<58s} HOLD Sh {hold_sh:.2f} multi-cut " +
" ".join(f"{d:+.2f}" for d in cuts_d))
# (c) finestre OOS DISGIUNTE (i cut del diario sono annidati)
print("\n (c) finestre OOS DISGIUNTE (ΔSh e Δret EW-STR vs CURRENT):")
windows = [("2024-01 -> 2024-07", "2024-01-01", "2024-07-01"),
("2024-07 -> 2025-01", "2024-07-01", "2025-01-01"),
("2025-01 -> fine ", "2025-01-01", None)]
for lbl, lo, hi in windows:
m = (J.index >= pd.Timestamp(lo, tz="UTC"))
if hi:
m &= (J.index < pd.Timestamp(hi, tz="UTC"))
Jw = J[m]
rc, re = combo(Jw, w_cur), combo(Jw, w_ew)
print(f" {lbl} ΔSh {sh(re) - sh(rc):+.2f} Δret {(yr_ret(re) - yr_ret(rc))*100:+.1f}pp")
# (d) contributo per-sleeve all'uplift hold-out (pesi statici: sul hold-out tutti attivi)
print("\n (d) contributo per-sleeve al Δritorno annualizzato sull'hold-out (Δw_i x mean_i x 365):")
Jh = J[J.index >= HOLDOUT]
for n in names:
dw = W_EWSTR[prefix(n)] - W_CURRENT[prefix(n)]
mu = float(Jh[n].dropna().mean()) * DAYS_PER_YEAR
print(f" {n:<16s} Δw {dw*100:>+6.2f}pp x ret_ann {mu*100:>+6.1f}% = {dw*mu*100:>+5.2f}pp/anno")
# ------------------------------------------------ 3) "7/8 ANNI VINTI"
print("\n" + "-" * 110)
print(" [3] '7/8 ANNI VINTI' — metrica = RITORNO composto per anno (non Sharpe). Tabella completa + margini")
print("-" * 110)
print(f" {'anno':<6s} {'ret CUR':>9s} {'ret EW-STR':>10s} {'Δret':>8s} | {'Sh CUR':>7s} {'Sh EW-STR':>9s} {'ΔSh':>6s} |"
f" {'DD CUR':>7s} {'DD EW':>7s}")
wins_ret = wins_sh = 0
years = sorted(set(J.index.year))
for y in years:
Jy = J[J.index.year == y]
rc, re = combo(Jy, w_cur), combo(Jy, w_ew)
mc, me = met(rc), met(re)
dr = yr_ret(re) - yr_ret(rc); dsh = sh(re) - sh(rc)
wins_ret += dr > 0; wins_sh += dsh > 0
print(f" {y:<6d} {yr_ret(rc)*100:>+8.1f}% {yr_ret(re)*100:>+9.1f}% {dr*100:>+7.1f}p |"
f" {sh(rc):>7.2f} {sh(re):>9.2f} {dsh:>+6.2f} | {mc['maxdd']*100:>6.1f}% {me['maxdd']*100:>6.1f}%")
print(f" -> vittorie EW-STR: RITORNO {wins_ret}/{len(years)} | SHARPE {wins_sh}/{len(years)}"
f" (2026 = anno parziale)")
# ------------------------------------------------ 4) PLATEAU
print("\n" + "-" * 110)
print(" [4] PLATEAU — griglia 2.5pp: TP x SKH, VRP=15 fisso, XS = residuo (cap 25). HOLD Sh | min multi-cut ΔSh")
print("-" * 110)
tps = [0.30, 0.325, 0.35, 0.375, 0.40, 0.4125]
skhs = [0.25, 0.275, 0.30]
Jh = J[J.index >= HOLDOUT]
hold_cur = sh(combo(Jh, w_cur))
print(f" (HOLD Sh CURRENT = {hold_cur:.2f}; celle con XS>25% = viola cap, marcate *)")
hdr = " TP\\SKH " + "".join(f"{s*100:>18.1f}%" for s in skhs)
print(hdr)
for tp in tps:
row = f" {tp*100:>6.2f}%"
for skh in skhs:
xs = 1.0 - 0.15 - tp - skh
tag = "*" if xs > CAPS["XS01"] + 1e-9 else " "
if xs < LO_W - 1e-9:
row += f"{'':>19s}"
continue
w = np.array([{"TP01": tp, "XS01": xs, "VRP01": 0.15, "SKH01": skh}[prefix(n)] for n in names])
hs = sh(combo(Jh, w))
dmin = min(sh(combo(J[J.index >= c], w)) - sh(combo(J[J.index >= c], w_cur)) for c in CUTS)
row += f" {hs:>5.2f}|{dmin:+.2f}{tag}"
print(row)
# ------------------------------------------------ 5) REALISMO
print("\n" + "-" * 110)
print(" [5] REALISMO — pesi EFFETTIVI per era (outer-join rinormalizza!) + haircut d'esecuzione")
print("-" * 110)
# pesi rinormalizzati per era
eras = [("2019-2020 (TP+SKH)", "2019-06-01"), ("2021-2023 (TP+VRP+SKH)", "2022-06-01"),
("2024+ (tutti)", "2025-06-01")]
print(" pesi EFFETTIVI (rinormalizzati sugli sleeve attivi) per era:")
for lbl, probe in eras:
t = pd.Timestamp(probe, tz="UTC")
i = J.index.get_indexer([t], method="nearest")[0]
act = J.iloc[i].notna().values
for wl, w in [("CURRENT", w_cur), ("EW-STR ", w_ew)]:
wn = w * act; wn = wn / wn.sum()
s = " ".join(f"{prefix(n)} {x*100:.0f}%" for n, x in zip(names, wn) if act[list(names).index(n)])
print(f" {lbl:<24s} {wl}: {s}")
print(" -> nel 2019-23 EW-STR tiene SKH01 al 40-50% effettivo (research-grade, book 230m).")
# haircut d'esecuzione come DRAG (r' = r - h*mean_full(r)): modello-costi, non de-levering
print("\n haircut esecuzione (drag costante = h x mean ritorno full dello sleeve):")
print(f" {'scenario':<34s} {'CUR HOLD Sh':>11s} {'EW HOLD Sh':>11s} {'ΔSh':>6s} | {'CUR HOLD CAGR':>13s} {'EW HOLD CAGR':>13s}")
scen = [("nessun haircut", {}, 0.0),
("SKH01 -20%", {"SKH01"}, 0.20), ("SKH01 -30%", {"SKH01"}, 0.30),
("SKH01+XS01 -20%", {"SKH01", "XS01"}, 0.20), ("SKH01+XS01 -30%", {"SKH01", "XS01"}, 0.30)]
for lbl, targets, h in scen:
Jx = J.copy()
for n in names:
if prefix(n) in targets:
mu = float(J[n].dropna().mean())
Jx[n] = J[n] - h * mu # drag solo dove lo sleeve e' attivo (NaN restano NaN)
Jxh = Jx[Jx.index >= HOLDOUT]
rc, re = combo(Jxh, w_cur), combo(Jxh, w_ew)
mc, me = met(rc), met(re)
print(f" {lbl:<34s} {mc['sharpe']:>11.2f} {me['sharpe']:>11.2f} {me['sharpe']-mc['sharpe']:>+6.2f} |"
f" {mc['cagr']*100:>+12.1f}% {me['cagr']*100:>+12.1f}%")
# ------------------------------------------------ 6) FORKING PATHS
print("\n" + "-" * 110)
print(" [6] FORKING PATHS — config viste sull'hold-out + null di tilt casuali cap-respecting")
print("-" * 110)
n_configs = 7 + 8 # 7 vettori pesi (MAXSH,RP,ERC,MINVAR-R,MAXSH-STR,EW,EW-STR) + 8 celle guardia
print(f" config valutate sull'hold-out nello script agente: >= {n_configs} (7 pesi + 8 celle guardia),")
print(" EW-STR costruito DOPO aver visto EW vincere (ammesso nel diario).")
rng = np.random.default_rng(42)
caps = np.array([CAPS[prefix(n)] for n in names])
N = 500
dsh_hold, dsh_full, all_cuts_pos = [], [], 0
Jh = J[J.index >= HOLDOUT]
hold_c = sh(combo(Jh, w_cur)); full_c = sh(combo(J, w_cur))
cuts_J = {c: J[J.index >= c] for c in CUTS}
cuts_base = {c: sh(combo(cuts_J[c], w_cur)) for c in CUTS}
for _ in range(N):
w = rng.dirichlet(np.ones(len(names)))
for _ in range(60): # proiezione su [LO_W, caps], somma 1
w = np.clip(w, LO_W, caps)
d = 1.0 - w.sum()
if abs(d) < 1e-9:
break
if d > 0:
room = caps - w
w += d * room / max(room.sum(), 1e-12)
else:
room = w - LO_W
w += d * room / max(room.sum(), 1e-12)
dh = sh(combo(Jh, w)) - hold_c
dsh_hold.append(dh)
dsh_full.append(sh(combo(J, w)) - full_c)
all_cuts_pos += all(sh(combo(cuts_J[c], w)) - cuts_base[c] > 0 for c in CUTS)
dsh_hold = np.array(dsh_hold); dsh_full = np.array(dsh_full)
dew = sh(combo(Jh, w_ew)) - hold_c
print(f" {N} tilt casuali dentro i cap (VRP<=15, XS<=25, w>=5): quota che batte CURRENT")
print(f" su HOLD-OUT: {float((dsh_hold > 0).mean())*100:.0f}% (mediana ΔSh {np.median(dsh_hold):+.2f})")
print(f" su FULL: {float((dsh_full > 0).mean())*100:.0f}% (mediana ΔSh {np.median(dsh_full):+.2f})")
print(f" su TUTTI e 3 cut: {all_cuts_pos/N*100:.0f}%")
print(f" percentile di EW-STR (ΔSh HOLD {dew:+.2f}) fra i tilt casuali: {float((dsh_hold < dew).mean())*100:.0f}°")
print(" lettura: se quasi ogni tilt cap-respecting batte CURRENT sull'hold-out, il 'vince OOS'")
print(" e' una proprieta' del PERIODO (hold-out pro-SKH/XS per costruzione), non del vettore EW-STR.")
print("\n" + "=" * 110)
print(" fine verifica — vedi addendum nel diario 2026-07-01-portfolio-weights-ddguard.md")
print("=" * 110)
if __name__ == "__main__":
main()
+369
View File
@@ -0,0 +1,369 @@
"""r0701_vrp_refine — AFFINAMENTO VRP01 (gate/sizing) dentro i limiti del modello (2026-07-01).
Baseline = VRP01 combo (sleeves._vrp_combo_returns): put credit spread settimanale -0.28/-0.10,
f=1.0, tenor 7d, gate VRP>0 (DVOL>RV30 causale) AND IV-rank>0.30 AND crash-skip IV-rank>0.90,
fee 12.5% del premio. FULL Sh ~1.10 / HOLD ~0.60 / DD ~12%.
Celle NUOVE (mai provate verificato nei diari; l'active management intra-trade e' gia'
SCARTATO in 2026-06-20-vrp-active-management.md e NON si ripete):
1. SIZING sul gap IV-RV (il carry atteso): size lineare clip(vrp/scale,0,1) o percentile
espandente causale del VRP, invece del (o in aggiunta al) gate binario IV-rank.
NB: il gate composito "IV-rank>0.30 AND IV-RV>0" e' GIA' il baseline (gate_vrp=True).
2. Filtro DVOL-MOMENTUM: non vendere vol mentre DVOL sta salendo (dv[i]-dv[i-k] > thr).
(Diverso da dvol_directional 2026-06-29: la' il DVOL-mom era segnale DIREZIONALE sul perp.)
3. Gate di REGIME da TP01: de-risk (skip o half-size) quando TP01 e' flat su BTC e ETH
(risk-off). Rischio ridondanza col trend -> riporto la frequenza d'intervento REALE.
4. Croce completa delle manopole (griglia contenuta, 105 celle, TUTTE contate nel DSR).
Metodo: stessa pipeline di options_vrp_v2 (pricing BS su DVOL reale, payoff sul path
certificato, stesse fee) cambiano SOLO gate/sizing. Selezione cella IN-SAMPLE (pre-2025),
hold-out 2025-26, multi-cut (5 tagli), deflated-Sharpe su tutti i trial, effetto a livello
portafoglio 4-sleeve (TP01 41.25 / XS01 18.75 / VRP 15 / SKH01 25).
ONESTA': il premio resta MODELLATO su DVOL ATM (no skew), book 1d, f di stress non catturato.
Il verdetto massimo possibile e' "sleeve modellato migliorato", MAI deploy pieno.
uv run python scripts/research/r0701_vrp_refine.py [--skip-portfolio]
"""
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"))
from collections import Counter
from functools import lru_cache
import numpy as np
import pandas as pd
from scripts.research.options_vrp_lab import bs_put, strike_from_delta, load_series, m_weekly, per_year
from altlib import deflated_sharpe
HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC")
WK_PER_YEAR = 365.25 / 7.0
CUTS = [pd.Timestamp(c, tz="UTC") for c in
("2023-01-01", "2023-07-01", "2024-01-01", "2024-07-01", "2025-01-01")]
MIN_IS_ACTIVE = 0.20 # attivita' minima in-sample per candidarsi (baseline ~41%)
# --- parametri FISSI del baseline VRP01 (NON toccati: cambia solo gate/sizing) ---
SHORT_DELTA, LONG_DELTA, F, TENOR_D = -0.28, -0.10, 1.0, 7
CRASH_SKIP, FEE_FRAC = 0.90, 0.125
# ----------------------------- pre-compute per asset (causale) -----------------------------
@lru_cache(maxsize=None)
def prep(asset: str):
"""px/dvol allineati + VRP causale (DVOL - RV30) e IV-rank espandente per OGNI giorno.
vrp[i] usa i 30 log-ret che finiscono a close[i]; ivr[i] = percentile di dv[i] in dv[:i]."""
J = load_series(asset)
px = J["px"].values.astype(float)
dv = J["dvol"].values.astype(float) / 100.0
idx = J.index
n = len(px)
lr = np.diff(np.log(px)) # lr[k] = log(px[k+1]/px[k])
vrp = np.full(n, np.nan)
for i in range(31, n):
vrp[i] = dv[i] - float(np.std(lr[i - 30:i]) * np.sqrt(365.25)) # come baseline (ddof=0)
ivr = np.full(n, np.nan)
for i in range(60, n):
ivr[i] = float((dv[:i] < dv[i]).mean())
return px, dv, idx, vrp, ivr
@lru_cache(maxsize=None)
def tp01_avg_target():
"""Serie giornaliera del target medio TP01 (BTC+ETH)/2. target[i] usa solo dati <= close[i]
-> noto alla sell-date del VRP (stessa close). Long-flat: 0.0 = risk-off pieno."""
from src.data.downloader import load_data
from src.strategies.trend_portfolio import TrendPortfolio, CANONICAL, resample_1d
tp = TrendPortfolio(**CANONICAL)
cols = {}
for a in ("BTC", "ETH"):
df = resample_1d(load_data(a, "1h"))
t = pd.Series(np.nan_to_num(tp.target_series(df), nan=0.0),
index=pd.to_datetime(df["datetime"]))
if t.index.tz is None:
t.index = t.index.tz_localize("UTC")
cols[a] = t
J = pd.concat(cols, axis=1, join="inner")
return J.mean(axis=1)
# ----------------------------- motore settimanale (unica differenza: gate/sizing) -----------------------------
def vrp_weekly(asset: str, sizing="bin", prop_scale=0.10, ivr_gate=0.30,
mom_k=0, mom_thr=0.0, tp_mode="off") -> tuple[pd.Series, Counter]:
"""Put credit spread settimanale come VRP01, con gate/sizing parametrici. CAUSALE:
strike/premio/gate/size usano solo dati <= sell-date; payoff a scadenza sul path certificato.
Ordine gate: prima i gate BASELINE (vrp/crash/ivr), poi i NUOVI (mom, tp) -> i counter dei
nuovi gate contano l'intervento MARGINALE (settimane altrimenti tradabili)."""
px, dv, idx, vrp_a, ivr_a = prep(asset)
n = len(px); T = TENOR_D / 365.25
tpv = None
if tp_mode != "off":
tpv = tp01_avg_target().reindex(idx, method="ffill").values
rets = {}; st = Counter()
i = 60
while i + TENOR_D < n:
st["weeks"] += 1
S0 = px[i]; sig = dv[i]; vrp = vrp_a[i]; ivr = ivr_a[i]
blocked = None
# --- gate BASELINE (identici a VRP01) ---
if np.isnan(vrp) or vrp <= 0:
blocked = "vrp"
elif not np.isnan(ivr) and ivr > CRASH_SKIP:
blocked = "crash"
elif ivr_gate > 0 and not np.isnan(ivr) and ivr < ivr_gate:
blocked = "ivr"
# --- gate NUOVI (contati sul residuo tradabile) ---
if blocked is None and mom_k > 0 and i >= mom_k:
if (dv[i] - dv[i - mom_k]) > mom_thr:
blocked = "mom"
size = 1.0
if blocked is None and tp_mode != "off" and tpv is not None and tpv[i] <= 1e-12:
if tp_mode == "skip":
blocked = "tp"
else: # half-size in risk-off
size *= 0.5; st["tp_half"] += 1
if blocked is None and sizing != "bin":
if sizing == "lin": # size ∝ gap IV-RV (carry atteso)
size *= float(np.clip(vrp / prop_scale, 0.0, 1.0))
elif sizing == "rank": # percentile espandente causale del VRP
hist = vrp_a[31:i]; hist = hist[~np.isnan(hist)]
size *= float((hist < vrp).mean()) if len(hist) >= 30 else 0.5
if blocked is not None:
st[f"blk_{blocked}"] += 1
rets[idx[i + TENOR_D]] = 0.0
i += TENOR_D
continue
st["traded"] += 1; st["size_sum"] += size
Ks = strike_from_delta(S0, T, sig, SHORT_DELTA)
Kl = strike_from_delta(S0, T, sig, LONG_DELTA)
net_prem = (bs_put(S0, Ks, T, sig) - bs_put(S0, Kl, T, sig)) * F
S1 = px[i + TENOR_D]
payoff = max(0.0, Ks - S1) - max(0.0, Kl - S1)
pnl = net_prem - payoff - FEE_FRAC * abs(net_prem)
rets[idx[i + TENOR_D]] = size * pnl / Ks # cash-secured su strike corto
i += TENOR_D
return pd.Series(rets), st
def book(**kw) -> tuple[pd.Series, Counter]:
rB, sB = vrp_weekly("BTC", **kw)
rE, sE = vrp_weekly("ETH", **kw)
b = pd.concat({"B": rB, "E": rE}, axis=1, join="inner").mean(axis=1).sort_index()
return b, sB + sE
# ----------------------------- metriche -----------------------------
def sh_wk(r: pd.Series) -> float:
r = r.dropna()
if len(r) < 8 or r.std() == 0:
return float("nan")
return float(r.mean() / r.std() * np.sqrt(WK_PER_YEAR))
def cell_metrics(b: pd.Series) -> dict:
is_ = b[b.index < HOLDOUT]; ho = b[b.index >= HOLDOUT]
full = m_weekly(b)
return dict(full_sh=full["sh"], full_dd=full["dd"], full_cagr=full["cagr"],
is_sh=sh_wk(is_), hold_sh=sh_wk(ho), worst=float(b.min()),
active=float((b != 0).mean()), is_active=float((is_ != 0).mean()))
def multicut(cand: pd.Series, base: pd.Series) -> list[tuple[str, float, float, float]]:
out = []
for c in CUTS:
sc, sb = sh_wk(cand[cand.index >= c]), sh_wk(base[base.index >= c])
out.append((str(c.date()), sc, sb, sc - sb))
return out
# ----------------------------- griglia -----------------------------
def grid_cells():
sizings = [("bin", 0.0, 0.30), ("lin", 0.08, 0.30), ("lin", 0.08, 0.0),
("lin", 0.12, 0.30), ("lin", 0.12, 0.0), ("rank", 0.0, 0.30), ("rank", 0.0, 0.0)]
moms = [(0, 0.0), (5, 0.0), (5, 0.05), (10, 0.0), (10, 0.05)]
tps = ["off", "skip", "half"]
cells = []
for sz, scale, ivr in sizings:
for mk, mth in moms:
for tp in tps:
name = (f"{sz}{f'{scale:g}' if sz == 'lin' else ''}"
f"|ivr{ivr:g}|mom{mk}k{mth:g}|tp-{tp}")
cells.append(dict(name=name, sizing=sz, prop_scale=scale, ivr_gate=ivr,
mom_k=mk, mom_thr=mth, tp_mode=tp))
return cells
BASELINE_NAME = "bin|ivr0.3|mom0k0|tp-off"
# ----------------------------- portafoglio 4-sleeve -----------------------------
def weekly_to_daily_lump(wk: pd.Series) -> pd.Series:
"""Come sleeves._vrp_combo_returns: rendimento settimanale sul giorno di scadenza, 0 altrove."""
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
return daily
def portfolio_compare(base_wk: pd.Series, cand_wk: pd.Series, cand_name: str):
"""4-sleeve con VRP baseline vs VRP variante (stessi TP01/XS01/SKH01, cache condivisa)."""
from src.portfolio.sleeves import tp01_sleeve, xsec_sleeve, skyhook_sleeve
from src.portfolio.portfolio import Sleeve, StrategyPortfolio, metrics
tp, xs, sk = tp01_sleeve(weight=0.4125), xsec_sleeve(weight=0.1875), skyhook_sleeve(weight=0.25)
rows = []
for tag, wk in (("VRP01 baseline", base_wk), (f"VRP variante [{cand_name}]", cand_wk)):
daily = weekly_to_daily_lump(wk)
vrp = Sleeve("VRP01_shortvol", 0.15, lambda d=daily: d)
port = StrategyPortfolio([tp, xs, vrp, sk])
full = metrics(port.combined_daily())
hold = metrics(port.combined_daily(lo=HOLDOUT))
rows.append((tag, full, hold))
print(f" {tag:<38} FULL Sh {full['sharpe']:>5.2f} DD {full['maxdd']*100:>4.1f}% "
f"CAGR {full['cagr']*100:>+5.1f}% | HOLD Sh {hold['sharpe']:>5.2f} DD {hold['maxdd']*100:>4.1f}%")
return rows
# ----------------------------- main -----------------------------
def main():
skip_port = "--skip-portfolio" in sys.argv
print("=" * 110)
print(" r0701 VRP REFINE — sizing IV-RV / filtro DVOL-momentum / gate TP01 (griglia onesta, sel. in-sample)")
print("=" * 110)
cells = grid_cells()
print(f" griglia: {len(cells)} celle (TUTTE contate nel deflated-Sharpe). "
f"IS = pre-2025, HOLD = 2025-01-01+.\n")
results = {}
for c in cells:
b, st = book(sizing=c["sizing"], prop_scale=c["prop_scale"], ivr_gate=c["ivr_gate"],
mom_k=c["mom_k"], mom_thr=c["mom_thr"], tp_mode=c["tp_mode"])
results[c["name"]] = dict(cfg=c, b=b, st=st, **cell_metrics(b))
base = results[BASELINE_NAME]
print(f" (0) BASELINE riprodotto [{BASELINE_NAME}]:")
print(f" FULL Sh {base['full_sh']:.2f} DD {base['full_dd']*100:.0f}% CAGR {base['full_cagr']*100:+.0f}% "
f"worst {base['worst']*100:+.1f}% IS Sh {base['is_sh']:.2f} HOLD Sh {base['hold_sh']:.2f} "
f"attivo {base['active']*100:.0f}% (atteso ~ FULL 1.10 / HOLD 0.60 / DD 12%)")
# ---- frequenza d'intervento dei gate NUOVI (sul baseline + singola manopola) ----
print("\n (1) FREQUENZA D'INTERVENTO dei gate nuovi (settimane altrimenti tradabili, book BTC+ETH):")
probes = [("mom k=5 thr=0", dict(mom_k=5, mom_thr=0.0)),
("mom k=5 thr=5pt", dict(mom_k=5, mom_thr=0.05)),
("mom k=10 thr=0", dict(mom_k=10, mom_thr=0.0)),
("mom k=10 thr=5pt", dict(mom_k=10, mom_thr=0.05)),
("tp01-skip", dict(tp_mode="skip")),
("tp01-half", dict(tp_mode="half"))]
base_traded = base["st"]["traded"]
for label, kw in probes:
_, st = book(**kw)
blk = st.get("blk_mom", 0) + st.get("blk_tp", 0)
half = st.get("tp_half", 0)
extra = f" (+{half} sett. a mezza size)" if half else ""
print(f" {label:<18} blocca {blk:>3} / {base_traded} settimane-trade del baseline "
f"({100*blk/max(base_traded,1):>4.1f}%){extra}")
tgt = tp01_avg_target()
pxB, _, idxB, _, _ = prep("BTC")
tp_on_grid = tgt.reindex(idxB, method="ffill")
print(f" [contesto] TP01 flat (BTC+ETH entrambi 0): {100*float((tp_on_grid <= 1e-12).mean()):.0f}% dei giorni della finestra DVOL")
# ---- classifica IN-SAMPLE (selezione onesta: nessuno sguardo all'hold-out) ----
ranked = sorted((r for r in results.values() if r["is_active"] >= MIN_IS_ACTIVE),
key=lambda r: r["is_sh"], reverse=True)
print(f"\n (2) TOP-10 per Sharpe IN-SAMPLE (pre-2025; filtro attivita' IS >= {MIN_IS_ACTIVE:.0%}):")
print(f" {'cella':<34}{'IS Sh':>7}{'FULL':>7}{'HOLD':>7}{'DD':>6}{'worst':>8}{'att.':>6}")
for r in ranked[:10]:
print(f" {r['cfg']['name']:<34}{r['is_sh']:>7.2f}{r['full_sh']:>7.2f}{r['hold_sh']:>7.2f}"
f"{r['full_dd']*100:>5.0f}%{r['worst']*100:>+7.1f}%{r['active']*100:>5.0f}%")
# ---- varianti a SINGOLA manopola vs baseline (tabella diario) ----
print("\n (2b) VARIANTI A SINGOLA MANOPOLA vs baseline (stessa tabella, nessuna selezione):")
singles = ["lin0.08|ivr0.3|mom0k0|tp-off", "lin0.12|ivr0.3|mom0k0|tp-off",
"rank|ivr0.3|mom0k0|tp-off", "lin0.08|ivr0|mom0k0|tp-off",
"rank|ivr0|mom0k0|tp-off",
"bin|ivr0.3|mom5k0.05|tp-off", "bin|ivr0.3|mom10k0.05|tp-off",
"bin|ivr0.3|mom5k0|tp-off", "bin|ivr0.3|mom10k0|tp-off",
"bin|ivr0.3|mom0k0|tp-skip", "bin|ivr0.3|mom0k0|tp-half"]
print(f" {'cella':<34}{'IS Sh':>7}{'FULL':>7}{'HOLD':>7}{'DD':>6}{'worst':>8}{'att.':>6}")
r = base
print(f" {'BASELINE ' + BASELINE_NAME:<34}{r['is_sh']:>7.2f}{r['full_sh']:>7.2f}{r['hold_sh']:>7.2f}"
f"{r['full_dd']*100:>5.0f}%{r['worst']*100:>+7.1f}%{r['active']*100:>5.0f}%")
for nm in singles:
r = results[nm]
print(f" {nm:<34}{r['is_sh']:>7.2f}{r['full_sh']:>7.2f}{r['hold_sh']:>7.2f}"
f"{r['full_dd']*100:>5.0f}%{r['worst']*100:>+7.1f}%{r['active']*100:>5.0f}%")
n_beat_hold = sum(1 for r in results.values() if r["hold_sh"] > base["hold_sh"])
print(f" [onesta'] celle che battono l'HOLD-OUT del baseline: {n_beat_hold}/{len(results)}"
f"NON selezionabili (sarebbe selection-on-holdout, gate 2026-06-29).")
cand = ranked[0]
is_baseline_best = cand["cfg"]["name"] == BASELINE_NAME
print(f"\n -> cella scelta IN-SAMPLE: [{cand['cfg']['name']}] IS Sh {cand['is_sh']:.2f} "
f"(baseline IS {base['is_sh']:.2f}, Δ {cand['is_sh']-base['is_sh']:+.2f})")
# ---- hold-out multi-cut vs baseline ----
print("\n (3) MULTI-CUT hold-out (Sharpe da ogni taglio a fine storia; uplift = cand - baseline):")
mc = multicut(cand["b"], base["b"])
pos = sum(1 for _, _, _, u in mc if u > 0)
for cut, sc, sb, u in mc:
print(f" cut {cut}: cand {sc:>5.2f} base {sb:>5.2f} uplift {u:>+5.2f}")
print(f" uplift positivo in {pos}/{len(mc)} tagli (richiesti >= 4/5)")
# ---- deflated Sharpe (tutti i trial della griglia) ----
all_sh = [r["full_sh"] for r in results.values()]
dsr_c, null_max = deflated_sharpe(cand["full_sh"], all_sh, cand["b"].values, dpy=WK_PER_YEAR)
dsr_b, _ = deflated_sharpe(base["full_sh"], all_sh, base["b"].values, dpy=WK_PER_YEAR)
print(f"\n (4) DEFLATED SHARPE (N={len(all_sh)} trial di questa griglia; PASS >= 0.95):")
print(f" cand DSR {dsr_c:.3f} (null-max Sh {null_max:.2f}) | baseline DSR {dsr_b:.3f}")
print(" NB: le celle della griglia sono fortemente correlate fra loro (stesso trade sottostante)")
print(" -> il DSR qui e' anti-conservativo sul multiple-testing; in piu' VRP01 stesso viene da")
print(" ~20 config precedenti (options_vrp_lab/_v2). Leggere il DSR come limite SUPERIORE.")
# ---- per-anno cand vs base ----
print("\n (5) PER-ANNO (ritorno composto):")
pyc, pyb = per_year(cand["b"]), per_year(base["b"])
print(" base: " + " ".join(f"{y}:{v*100:+.0f}%" for y, v in pyb.items()))
print(" cand: " + " ".join(f"{y}:{v*100:+.0f}%" for y, v in pyc.items()))
# ---- portafoglio 4-sleeve ----
if not skip_port:
print("\n (6) PORTAFOGLIO 4-SLEEVE (TP01 41.25 / XS01 18.75 / VRP 15 / SKH01 25), VRP base vs variante:")
try:
portfolio_compare(base["b"], cand["b"], cand["cfg"]["name"])
except Exception as e: # dati HL/5m mancanti in qualche ambiente
print(f" [saltato: {type(e).__name__}: {e}]")
else:
print("\n (6) portafoglio: saltato (--skip-portfolio)")
# ---- verdetto ----
print("\n" + "=" * 110)
improves = (not is_baseline_best
and cand["is_sh"] > base["is_sh"]
and pos >= 4
and (cand["hold_sh"] > base["hold_sh"])
and dsr_c >= 0.95)
if is_baseline_best:
print(" VERDETTO: NON MIGLIORA — il baseline VRP01 vince gia' la selezione in-sample.")
elif improves:
print(f" VERDETTO: MIGLIORA (variante {cand['cfg']['name']}) — batte il baseline in-sample,")
print(f" su hold-out multi-cut ({pos}/{len(mc)}) e DSR {dsr_c:.2f}>=0.95. Resta SLEEVE MODELLATO")
print(" (premio DVOL ATM, book 1d, f di stress non catturato): NON deploy pieno.")
else:
why = []
if cand["is_sh"] <= base["is_sh"]:
why.append("non batte il baseline in-sample")
if pos < 4:
why.append(f"multi-cut {pos}/{len(mc)} (<4)")
if cand["hold_sh"] <= base["hold_sh"]:
why.append("hold-out non migliore")
if dsr_c < 0.95:
why.append(f"DSR {dsr_c:.2f}<0.95")
print(f" VERDETTO: NON MIGLIORA — cella IS-best [{cand['cfg']['name']}] bocciata: " + "; ".join(why) + ".")
print("=" * 110)
if __name__ == "__main__":
main()
+439
View File
@@ -0,0 +1,439 @@
"""r0701_xs — RESIDUAL (IDIOSYNCRATIC) MOMENTUM cross-sectional sui 19 major Hyperliquid.
TESI (2026-07-01). STATARB-RESID (thread 4, 2026-06-29) ha mostrato che il MOMENTUM del residuo
ETHβ·BTC (β OLS rolling, sgn=+1: le dislocazioni CONTINUANO a 1d) passa quasi tutti i gate su
2 gambe, fallendo SOLO il deflated-Sharpe (0.929<0.95). Angolo nuovo: lo stesso meccanismo
CROSS-SECTIONAL sui 19 major di XS01 per ogni alt, residuo vs β·BTC (β OLS rolling B giorni),
momentum del residuo su lookback (blend z-score [30,90] come XS01, o singolo), rank cross-section,
long top-k / short bottom-k, vol-target 20%. Ipotesi: la breadth (18 stream invece di 1) alza il
DSR dove il 2-gambe falliva.
DISTINZIONE da quanto gia' testato:
* IREV (xsec_v2_nonmom, idio-REVERSAL, sgn=-1): FALLITO. Qui sgn=+1 (idio-MOMENTUM).
* IMOM (xsec_v2_nonmom): residuo vs mercato EQUAL-WEIGHT, B=60 fisso, no blend, era solo
"riferimento momentum". Qui: fattore = BTC (come STATARB-RESID), B in griglia, blend z-score
[30,90] + probe con gate di dispersione (parita' strutturale con XS01), selezione IN-SAMPLE.
IL BAR (fondamentale): una variante di XS01 e' utile SOLO se (a) SOSTITUISCE XS01 (meglio
standalone E nel portafoglio 4-sleeve) oppure (b) AGGIUNGE come 5o sleeve (corr bassa a XS01 E
TP01, uplift del PORTAFOGLIO). corr>0.6 a XS01 senza batterlo -> REDUNDANT/SCARTATO.
Baseline XS01: standalone FULL Sh ~1.50 / HOLD ~1.71 / DD ~11%.
GATE (CLAUDE.md, metodologia obbligatoria):
1. CAUSALE: score a close[i], peso tenuto in i+1 (engine shifta W[i-1]*dret[i]); barre vol=0
escluse; prefix-check di causalita' sulla cella scelta.
2. NETTO fee 0.10% RT per gamba per ribilancio + sweep {0.05, 0.10, 0.20, 0.30}% RT/gamba.
3. Selezione cella IN-SAMPLE-ONLY (pre-2025, anti selection-on-holdout), poi hold-out bloccato.
4. DEFLATED Sharpe su TUTTI i trial della griglia (serve >=0.95).
5. Confronto PORTAFOGLIO: sostituzione di XS01 a parita' di peso + aggiunta 5o sleeve @10/15%
(riusa StrategyPortfolio/active_sleeves senza modificarli) + marginal vs il BOOK a 4 sleeve.
6. CAVEAT IMMUTABILI: storia HL ~2.5 anni; book L/S a ~2k gambe -> STAT-MODE a $600 (dichiaro
comunque l'haircut small-cap $600/min$5).
uv run python scripts/research/r0701_xs_residmom.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 / "scripts" / "research"))
sys.path.insert(0, str(PROJECT_ROOT))
import numpy as np
import pandas as pd
import xsec_v2_nonmom as xv # harness collaudato (load_matrix, deflated_sharpe, portfolio, ...)
from src.portfolio.sleeves import XS_UNIVERSE
DPY, TV, FEE, HOLDOUT = xv.DPY, xv.TV, xv.FEE, xv.HOLDOUT
FACTOR = "BTC" # il fattore del residuo (come STATARB-RESID), NON tradato
BLEND = (30, 90) # blend z-score come XS01
# griglia (modesta, da mandato): beta-window x lookback x k x H (gate=off)
BETAS = (60, 90, 120)
LOOKS = ("blend", 30, 90)
KS = (3, 5)
HS = (5, 10, 20)
# probe a parita' STRUTTURALE con XS01 (blend + gate dispersione p30, H10 k5) — contano nei trial
GATED_PROBES = [dict(B=B, L="blend", k=5, H=10, gate=30) for B in BETAS]
# ===========================================================================
# SCORE BUILDER — residual momentum vs beta*BTC. CAUSALE (dati <= i).
# Ritorna score_at(i) -> (score_blend_z[A], valid[A], disp_raw_i) + warmup.
# disp_raw_i = dispersione cross-section del momentum RESIDUO grezzo (per il gate: lo z-score
# blended ha std ~1 per costruzione, quindi la dispersione va misurata sul grezzo).
# ===========================================================================
def make_residmom(PX: pd.DataFrame, B: int, L):
lookbacks = BLEND if L == "blend" else (int(L),)
px = PX.values
n, A = px.shape
fi = list(PX.columns).index(FACTOR)
DR = PX.pct_change()
m = DR[FACTOR]
beta, _ = xv._rolling_beta(DR, m, B) # beta_j vs BTC su finestra B (<= i)
SDR = {Lk: DR.rolling(Lk, min_periods=int(0.8 * Lk)).sum().values for Lk in lookbacks}
SM = {Lk: m.rolling(Lk, min_periods=int(0.8 * Lk)).sum().values for Lk in lookbacks}
CNT = {Lk: DR.rolling(Lk, min_periods=1).count().values for Lk in lookbacks}
def score_at(i):
b = beta[i]
valid = np.isfinite(px[i]) & np.isfinite(b)
valid[fi] = False # BTC = fattore, fuori dal cross-section
resids = []
for Lk in lookbacks:
resid = SDR[Lk][i] - b * SM[Lk][i] # momentum del residuo r_j - beta_j*r_btc
valid = valid & np.isfinite(resid) & (CNT[Lk][i] >= 0.8 * Lk)
resids.append(resid)
score = np.full(A, np.nan)
disp = np.nan
nv = int(valid.sum())
if nv >= 2:
acc = np.zeros(nv)
cnt = 0
stds = []
for resid in resids:
r = resid[valid]
sd = float(r.std())
stds.append(sd)
if sd > 0:
acc += (r - r.mean()) / sd
cnt += 1
if cnt:
score[valid] = acc / cnt # blend: media z-score cross-sectional
disp = float(np.mean(stds)) # dispersione del momentum residuo GREZZO
return score, valid, disp
return score_at, max(max(lookbacks), B) + 1
# ===========================================================================
# ENGINE locale (= xv.xs_engine + ritorno di W/scale + gate di dispersione opzionale).
# L'uguaglianza con xv.xs_engine sulle celle non-gated e' VERIFICATA in main().
# ===========================================================================
def xs_engine_w(PX, VOL, score_at, H, k, target_vol=TV, fee=FEE, min_assets=10, warmup=0,
disp_pct=0, disp_minhist=20):
px = PX.values
vol = VOL.values
n, A = px.shape
dret = np.full((n, A), np.nan)
dret[1:] = px[1:] / px[:-1] - 1.0
W = np.zeros((n, A))
w = np.zeros(A)
disp_hist = []
for i in range(n):
if i >= warmup and i % H == 0:
score, valid, disp = score_at(i)
valid = valid & np.isfinite(score) & (vol[i] > 0)
idxv = np.where(valid)[0]
if len(idxv) >= min_assets:
thr = (np.percentile(disp_hist, disp_pct)
if (disp_pct > 0 and len(disp_hist) >= disp_minhist) else -np.inf)
if not (disp_pct > 0) or (np.isfinite(disp) and disp >= thr):
kk = min(k, len(idxv) // 2)
order = idxv[np.argsort(score[idxv])]
lo, hi = order[:kk], order[-kk:]
w = np.zeros(A)
w[hi] = 0.5 / kk # long alto residual-momentum (sgn=+1)
w[lo] = -0.5 / kk # short basso
else:
w = np.zeros(A) # regime compatto -> flat (gate)
if disp_pct > 0 and np.isfinite(disp):
disp_hist.append(disp)
else:
w = np.zeros(A)
W[i] = w
gross = np.zeros(n)
gross[1:] = np.nansum(W[:-1] * np.nan_to_num(dret[1:]), axis=1)
turn = np.zeros(n)
turn[0] = np.abs(W[0]).sum()
turn[1:] = np.abs(np.diff(W, axis=0)).sum(axis=1)
net = gross - turn * (fee / 2.0)
s = pd.Series(net, index=PX.index)
rv = s.rolling(30, min_periods=15).std().shift(1) * np.sqrt(DPY)
scale = np.clip(np.nan_to_num(target_vol / rv.replace(0, np.nan).values, nan=0.0), 0, 3.0)
turn_py = float(turn.sum() / (n / DPY)) if n else 0.0
return pd.Series(s.values * scale, index=PX.index), turn_py, W, scale
def run_cell(PX, VOL, cfg, fee=FEE):
score_at, warm = make_residmom(PX, cfg["B"], cfg["L"])
daily, turn, W, scale = xs_engine_w(PX, VOL, score_at, cfg["H"], cfg["k"], fee=fee,
warmup=warm, disp_pct=cfg.get("gate", 0))
return xv.to_daily(daily), turn, W, scale
# ===========================================================================
# CAUSALITA' (prefix-check, pattern di xv.causality_prefix_check sul nostro engine)
# ===========================================================================
def causality_prefix_check(PX, VOL, cfg, frac=0.85, tail=60, tol=1e-9):
score_full, warm = make_residmom(PX, cfg["B"], cfg["L"])
full, *_ = xs_engine_w(PX, VOL, score_full, cfg["H"], cfg["k"], warmup=warm,
disp_pct=cfg.get("gate", 0))
cut = int(len(PX) * frac)
PXc, VOLc = PX.iloc[:cut], VOL.iloc[:cut]
score_pre, warm2 = make_residmom(PXc, cfg["B"], cfg["L"])
pre, *_ = xs_engine_w(PXc, VOLc, score_pre, cfg["H"], cfg["k"], warmup=warm2,
disp_pct=cfg.get("gate", 0))
lo = max(0, cut - tail)
a, b = full.values[lo:cut], pre.values[lo:cut]
worst = float(np.max(np.abs(a - b))) if len(a) else float("nan")
return dict(ok=bool(worst <= tol), max_tail_diff=worst, cut=cut, tail=len(a))
# ===========================================================================
# SMALL-CAP $600 (dichiarativo: il book resta STAT-MODE comunque, come XS01).
# Pesi EFFETTIVI = W[i] * scale[i+1] (scale[i+1] usa net fino a i via shift(1) -> noto a close i).
# Un cambio-gamba con |dw|*capital < min_order NON si esegue. Confronto realistico vs modeled
# sulla STESSA simulazione a pesi (coerente internamente).
# ===========================================================================
def smallcap_check(PX, W, scale, fee=FEE, capital=600.0, min_order=5.0):
px = PX.values
n, A = px.shape
dret = np.nan_to_num(np.vstack([np.zeros(A), px[1:] / px[:-1] - 1.0]))
sc = np.roll(scale, -1)
sc[-1] = scale[-1]
T = W * sc[:, None] # target effettivo deciso a close i
def sim(min_ord):
held = np.zeros((n, A))
cur = np.zeros(A)
n_tr = 0
for i in range(n):
d = np.abs(T[i] - cur) * capital
ex = d >= min_ord
n_tr += int(ex.sum())
cur = np.where(ex, T[i], cur)
held[i] = cur
pos = np.zeros((n, A))
pos[1:] = held[:-1]
turn = np.abs(np.diff(held, axis=0, prepend=np.zeros((1, A)))).sum(axis=1)
net = (pos * dret).sum(axis=1) - turn * (fee / 2.0)
r = net[np.isfinite(net)]
sh = float(r.mean() / r.std() * np.sqrt(DPY)) if r.std() > 0 else 0.0
return sh, n_tr, float(turn.sum() / (n / DPY))
sh_real, ntr_real, turn_real = sim(min_order)
sh_mod, ntr_mod, turn_mod = sim(0.0)
return dict(sharpe_modeled=round(sh_mod, 3), sharpe_realistic=round(sh_real, 3),
haircut=round(sh_mod - sh_real, 3), n_executed=ntr_real, n_modeled=ntr_mod,
turnover_real=round(turn_real, 1), turnover_modeled=round(turn_mod, 1))
# ===========================================================================
# PORTAFOGLIO — sostituzione XS01 + aggiunta 5o sleeve (pattern di xsec_v3_momstruct)
# ===========================================================================
_BASE = None
_BASE_M = None
def _base():
global _BASE, _BASE_M
if _BASE is None:
_BASE = xv.active_sleeves()
pf = xv.StrategyPortfolio(_BASE)
pf.backtest()
_BASE_M = (xv.metrics(pf.combined_daily()), xv.metrics(pf.combined_daily(lo=HOLDOUT)))
return _BASE, _BASE_M
def add_uplift(daily, fr):
base, _ = _base()
wraw = fr / (1.0 - fr)
cand = xv.Sleeve("R0701_cand", wraw, lambda d=daily: d)
pf = xv.StrategyPortfolio(base + [cand])
return (xv.metrics(pf.combined_daily()), xv.metrics(pf.combined_daily(lo=HOLDOUT)),
pf.weights().get("R0701_cand", 0.0))
def substitute_xs01(daily):
base, _ = _base()
sub = [xv.Sleeve("R0701_sub", s.weight, lambda d=daily: d) if s.name == "XS01_xsec_hl" else s
for s in base]
pf = xv.StrategyPortfolio(sub)
return xv.metrics(pf.combined_daily()), xv.metrics(pf.combined_daily(lo=HOLDOUT))
def marginal_vs_book(daily):
"""Corr + uplift del blend 0.9*BOOK+0.1*cand vs il BOOK a 4 sleeve (full/hold + multi-cut)."""
base, _ = _base()
book = xv.StrategyPortfolio(base).combined_daily()
J = pd.concat({"B": book, "C": daily}, axis=1, join="inner").dropna()
def _sh(s):
r = np.asarray(s.dropna().values, float)
return float(r.mean() / r.std() * np.sqrt(DPY)) if len(r) > 2 and r.std() > 0 else 0.0
def _up(sub):
return _sh(0.9 * sub["B"] + 0.1 * sub["C"]) - _sh(sub["B"])
JH = J[J.index >= HOLDOUT]
cuts = {}
for y in sorted(set(J.index.year))[1:]:
sub = J[J.index >= pd.Timestamp(f"{y}-01-01", tz="UTC")]
if len(sub) >= 120:
cuts[int(y)] = round(_up(sub), 3)
return dict(corr_book=round(float(J["B"].corr(J["C"])), 3),
uplift_full=round(_up(J), 3),
uplift_hold=round(_up(JH), 3) if len(JH) > 30 else None,
multicut=cuts)
# ===========================================================================
def per_year(daily):
return [(int(y), round(float((1 + g).prod() - 1), 3)) for y, g in daily.groupby(daily.index.year)]
def tag(cfg):
g = f" gate{cfg['gate']}" if cfg.get("gate") else ""
return f"B{cfg['B']} L{cfg['L']} k{cfg['k']} H{cfg['H']}{g}"
def main():
print("=" * 104)
print(" r0701_xs — RESIDUAL MOMENTUM cross-sectional (residuo vs beta*BTC) sui 19 major HL — STAT-MODE")
print("=" * 104)
PX, VOL = xv.load_matrix(XS_UNIVERSE)
print(f" universo 19-major: {PX.shape[1]} asset, {PX.shape[0]} giorni "
f"[{PX.index[0].date()} -> {PX.index[-1].date()}] fattore={FACTOR} (escluso dal cross-section)")
tp_daily = xv.tp01_sleeve().daily()
xs_daily = xv.xsec_sleeve().daily()
xs_f = xv.metrics(xs_daily)
xs_h = xv.metrics(xs_daily[xs_daily.index >= HOLDOUT])
print(f" baseline XS01 (sleeve attivo): FULL Sh {xs_f['sharpe']:.2f} DD {xs_f['maxdd']*100:.0f}%"
f" | HOLD Sh {xs_h['sharpe']:.2f}")
# --- sanity: engine locale == xv.xs_engine sulle celle non-gated -----------------
chk_cfg = dict(B=90, L=30, k=5, H=10)
score_at, warm = make_residmom(PX, chk_cfg["B"], chk_cfg["L"])
mine, *_ = xs_engine_w(PX, VOL, score_at, chk_cfg["H"], chk_cfg["k"], warmup=warm)
ref, _ = xv.xs_engine(PX, VOL, lambda i: score_at(i)[:2], chk_cfg["H"], chk_cfg["k"], warmup=warm)
dmax = float(np.nanmax(np.abs(mine.values - ref.values)))
assert dmax < 1e-12, f"engine locale diverge da xv.xs_engine: {dmax}"
print(f" [sanity] engine locale == xv.xs_engine (maxdiff {dmax:.1e})")
# --- griglia -----------------------------------------------------------------------
grid = [dict(B=B, L=L, k=k, H=H) for B in BETAS for L in LOOKS for k in KS for H in HS]
grid += GATED_PROBES
rows = []
for cfg in grid:
daily, turn, W, scale = run_cell(PX, VOL, cfg)
if daily.std() == 0 or len(daily) < 60:
continue
f, h, pct = xv.evalcfg(daily)
ins = daily[daily.index < HOLDOUT]
is_sh = xv.metrics(ins)["sharpe"] if len(ins) > 60 else float("nan")
rows.append(dict(cfg=cfg, daily=daily, W=W, scale=scale, turn=turn,
full=f["sharpe"], hold=h["sharpe"], dd=f["maxdd"], ret=f["ret"],
pct=pct, is_sh=is_sh,
corrXS=xv._corr(daily, xs_daily), corrTP=xv._corr(daily, tp_daily)))
all_sr = [r["full"] for r in rows]
print(f"\n griglia: {len(rows)} celle valide su {len(grid)} "
f"(trial per deflated-Sharpe = {len(all_sr)}; il conteggio VERO del programma e' >>)")
hdr = f" {'cfg':<26}{'IS':>7}{'FULL':>7}{'HOLD':>7}{'DD%':>6}{'anni+':>7}{'corrXS':>8}{'corrTP':>8}{'turn/y':>8}"
valid = [r for r in rows if np.isfinite(r["is_sh"])]
print("\n TOP-5 per Sharpe IN-SAMPLE (pre-2025) — la selezione ONESTA:")
print(hdr)
for r in sorted(valid, key=lambda r: -r["is_sh"])[:5]:
print(f" {tag(r['cfg']):<26}{r['is_sh']:>7.2f}{r['full']:>7.2f}{r['hold']:>7.2f}"
f"{r['dd']*100:>6.0f}{r['pct']*100:>6.0f}%{r['corrXS']:>+8.2f}{r['corrTP']:>+8.2f}{r['turn']:>8.0f}")
print("\n TOP-5 per HOLD (solo trasparenza — selezionare qui = selection-on-holdout):")
print(hdr)
for r in sorted(rows, key=lambda r: -r["hold"])[:5]:
iss = f"{r['is_sh']:.2f}" if np.isfinite(r["is_sh"]) else "n/a"
print(f" {tag(r['cfg']):<26}{iss:>7}{r['full']:>7.2f}{r['hold']:>7.2f}"
f"{r['dd']*100:>6.0f}{r['pct']*100:>6.0f}%{r['corrXS']:>+8.2f}{r['corrTP']:>+8.2f}{r['turn']:>8.0f}")
if not valid:
print("\n >>> nessuna cella con in-sample valutabile. SCARTATO.")
return
pick = max(valid, key=lambda r: r["is_sh"])
daily = pick["daily"]
print("\n" + "=" * 104)
print(f" CELLA SCELTA (in-sample-only): {tag(pick['cfg'])}")
print("=" * 104)
print(f" IS Sh {pick['is_sh']:.2f} | FULL {pick['full']:.2f} | HOLD {pick['hold']:.2f}"
f" | DD {pick['dd']*100:.0f}% | ret {pick['ret']*100:+.0f}% | anni+ {pick['pct']*100:.0f}%"
f" | turnover/y {pick['turn']:.0f}")
print(f" corr vs XS01 {pick['corrXS']:+.2f} | corr vs TP01 {pick['corrTP']:+.2f}"
f" | per-anno {per_year(daily)}")
caus = causality_prefix_check(PX, VOL, pick["cfg"])
print(f" CAUSALITA' (prefix-check): ok={caus['ok']} max_tail_diff={caus['max_tail_diff']:.2e}")
dsr, sr0 = xv.deflated_sharpe(pick["full"], all_sr, daily)
print(f" DEFLATED Sharpe (N={len(all_sr)} trial di QUESTA griglia): {dsr:.3f}"
f" | soglia null-max annualizz. {sr0:.2f} (serve >=0.95)")
print(" fee sweep (RT per gamba):", end=" ")
for fee in (0.0005, 0.001, 0.002, 0.003):
d_f, *_ = run_cell(PX, VOL, pick["cfg"], fee=fee)
ff = xv.metrics(d_f)
hh = xv.metrics(d_f[d_f.index >= HOLDOUT])
print(f"{fee*100:.2f}%: F{ff['sharpe']:+.2f}/H{hh['sharpe']:+.2f}", end=" ")
print()
sc = smallcap_check(PX, pick["W"], pick["scale"])
print(f" SMALL-CAP $600/min$5 (dichiarativo, resta STAT-MODE): modeled Sh {sc['sharpe_modeled']:.2f}"
f" -> realistic {sc['sharpe_realistic']:.2f} (haircut {sc['haircut']:+.2f});"
f" fill eseguiti {sc['n_executed']}/{sc['n_modeled']}"
f" turn/y {sc['turnover_real']:.0f} vs {sc['turnover_modeled']:.0f}")
# --- confronto PORTAFOGLIO -----------------------------------------------------------
print("\n PORTAFOGLIO (TP01+XS01+VRP01+SKH01, pesi canonici):")
_, (bf, bh) = _base()
print(f" BASE 4-sleeve FULL Sh {bf['sharpe']:.2f} DD {bf['maxdd']*100:.1f}%"
f" | HOLD Sh {bh['sharpe']:.2f} DD {bh['maxdd']*100:.1f}%")
sf, sh_ = substitute_xs01(daily)
sub_full_d, sub_hold_d = sf["sharpe"] - bf["sharpe"], sh_["sharpe"] - bh["sharpe"]
print(f" SOSTITUZIONE XS01 -> cand FULL Sh {sf['sharpe']:.2f} ({sub_full_d:+.2f}) DD {sf['maxdd']*100:.1f}%"
f" | HOLD Sh {sh_['sharpe']:.2f} ({sub_hold_d:+.2f}) DD {sh_['maxdd']*100:.1f}%")
up_best = (-9.0, -9.0)
for fr in (0.10, 0.15):
cf, ch, wgt = add_uplift(daily, fr)
d_f, d_h = cf["sharpe"] - bf["sharpe"], ch["sharpe"] - bh["sharpe"]
if d_h > up_best[1]:
up_best = (d_f, d_h)
print(f" AGGIUNTA 5o sleeve @{wgt*100:>4.1f}% FULL Sh {cf['sharpe']:.2f} ({d_f:+.2f}) DD {cf['maxdd']*100:.1f}%"
f" | HOLD Sh {ch['sharpe']:.2f} ({d_h:+.2f}) DD {ch['maxdd']*100:.1f}%")
mb = marginal_vs_book(daily)
print(f" MARGINAL vs BOOK: corr {mb['corr_book']:+.2f} | uplift@10% full {mb['uplift_full']:+.3f}"
f" hold {mb['uplift_hold']:+.3f} | multi-cut {mb['multicut']}")
# --- VERDETTO -------------------------------------------------------------------------
print("\n" + "=" * 104)
beats_xs_standalone = (pick["full"] > xs_f["sharpe"] and pick["hold"] > xs_h["sharpe"])
dominates = sub_full_d > 0.02 and sub_hold_d > 0.05 and beats_xs_standalone
diversifies = (abs(pick["corrXS"]) < 0.6 and abs(pick["corrTP"]) < 0.5
and up_best[1] > 0.05 and mb["uplift_hold"] is not None and mb["uplift_hold"] > 0
and all(u > 0 for u in mb["multicut"].values()))
dsr_ok = np.isfinite(dsr) and dsr >= 0.95
if not caus["ok"]:
verdict, why = "SCARTATO", "prefix-check di causalita' fallito"
elif pick["full"] <= 0.3 or pick["hold"] <= 0:
verdict, why = "SCARTATO", f"standalone debole (FULL {pick['full']:+.2f}, HOLD {pick['hold']:+.2f})"
elif abs(pick["corrXS"]) > 0.6 and not dominates:
verdict, why = "REDUNDANT", f"corrXS {pick['corrXS']:+.2f}>0.6 e non batte XS01 (sub HOLD {sub_hold_d:+.2f})"
elif dominates and dsr_ok:
verdict, why = "CANDIDATO-SLEEVE (sostituto)", "batte XS01 standalone E nel book, DSR>=0.95"
elif diversifies and dsr_ok:
verdict, why = "CANDIDATO-SLEEVE (5o)", "scorrelato, uplift book persistente, DSR>=0.95"
elif dominates or diversifies:
verdict, why = "LEAD-forward", f"profilo utile ma DSR {dsr:.2f}<0.95 (storia ~2.5a, multiple-testing)"
else:
verdict, why = "SCARTATO", (f"ne' sostituto (sub HOLD {sub_hold_d:+.2f}) ne' additivo"
f" (uplift HOLD {up_best[1]:+.2f}, corrXS {pick['corrXS']:+.2f})")
print(f" VERDETTO: {verdict}{why}")
print(" CAVEAT immutabili: storia HL ~2.5 anni; in-sample = solo 2024 (selezione su finestra corta);")
print(" book L/S multi-gamba -> STAT-MODE a $600 (come XS01), mai deploy a questo capitale.")
print("=" * 104)
if __name__ == "__main__":
main()
+295
View File
@@ -0,0 +1,295 @@
"""r0701_xs_seasonal — STAGIONALITÀ CROSS-SECTIONAL sull'universo Hyperliquid (2026-07-01).
DOMANDA: esistono effetti calendario RELATIVI tra i 19 alt major HL (weekday tilt,
turn-of-month, pattern weekend->lunedì nel cross-section)? Essendo long/short
market-neutral (demeaned cross-section), il "buy&hold travestito" che ha ucciso la
seasonality trackF su BTC/ETH è strutturalmente escluso.
METODO (ordine obbligatorio, dal mandato):
1. TEST STATISTICO PRIMA DELLA STRATEGIA persistenza split-half: per ogni giorno
della settimana, il tilt cross-sectional per-asset (media del ritorno relativo
demeaned in quel giorno, al netto del tilt incondizionato dell'asset) della PRIMA
metà del campione correla (Spearman rank) con quello della SECONDA metà?
Null: permutazione delle etichette-giorno (2000 draw) entro ciascuna metà
distribuzione del max-su-7 rank-corr. Se il max reale non batte il 95° pctl del
null SCARTATO senza backtest. Idem per weekend-bucket e turn-of-month.
La permutazione controlla automaticamente il confound "alpha persistente
dell'asset su entrambe le metà" (momentum), perché anche le etichette permutate
lo mostrerebbero.
2. (solo se persiste) strategia L/S market-neutral vol-target, fee 0.10% RT,
breakeven fee, selezione IN-SAMPLE (pre-2025), hold-out, deflated_sharpe.
3. day_boundary_robust OBBLIGATORIO per ogni effetto calendario NB: i dati HL
locali sono SOLO 1d il confine giorno NON è ri-tagliabile localmente sui 19
alt. Qualunque lead weekday su HL 1d resta NON-VERIFICABILE al boundary-shift
finché non esistono barre orarie HL: il verdetto massimo possibile qui è
LEAD-forward *condizionato*, mai sleeve. (Su BTC/ETH 1h il test esiste in
altlib, ma il cross-section a 2 asset non riproduce l'effetto a 19.)
DATI: data/raw/hl_*_1d.parquet 19 major XS01, 913 giorni (2024-01-01 2026-07-01),
0 barre vol=0 (verificato). LIMITE DICHIARATO: ~2.5 anni ~130 osservazioni per
weekday, ~65 per metà alto rischio rumore; soglie severe (p<0.05 sul max-statistic
permutato, non per-weekday).
Esecuzione: cd /opt/docker/PythagorasGoal && uv run python scripts/research/r0701_xs_seasonal.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 / "scripts" / "research" / "alt"))
sys.path.insert(0, str(_ROOT))
import altlib as al # noqa: E402 (HOLDOUT, deflated_sharpe — riuso convenzioni)
RNG = np.random.default_rng(20260701)
N_PERM = 2000
UNIVERSE = ["BTC", "ETH", "SOL", "BNB", "XRP", "DOGE", "AVAX", "LINK", "LTC", "ADA",
"ARB", "OP", "SUI", "APT", "INJ", "TIA", "SEI", "NEAR", "AAVE"]
FEE_SIDE = 0.0005 # 0.10% RT
HOLDOUT = al.HOLDOUT # 2025-01-01 UTC (convenzione di progetto)
WD_NAMES = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
# ---------------------------------------------------------------- dati
def load_relative_returns():
"""Matrice (date × asset) dei ritorni GIORNALIERI RELATIVI (demeaned cross-section).
Esclude barre vol=0 ( NaN). x[t,a] = r[t,a] mean_a r[t,a] market-neutral."""
cols = {}
for s in UNIVERSE:
d = pd.read_parquet(_ROOT / "data" / "raw" / f"hl_{s.lower()}_1d.parquet")
idx = pd.to_datetime(d["timestamp"], unit="ms", utc=True)
c = pd.Series(d["close"].values.astype(float), index=idx)
c[d["volume"].values <= 0] = np.nan # guardrail backfill sintetico
cols[s] = c
C = pd.concat(cols, axis=1).sort_index()
R = C.pct_change()
R = R.iloc[1:] # prima riga NaN
X = R.sub(R.mean(axis=1), axis=0) # demean cross-section per data
return R, X
# ---------------------------------------------------------------- statistica
def _rank(v):
return pd.Series(v).rank().values
def spearman(a, b):
ra, rb = _rank(a), _rank(b)
if np.std(ra) == 0 or np.std(rb) == 0:
return 0.0
return float(np.corrcoef(ra, rb)[0, 1])
def bucket_tilts(X: pd.DataFrame, labels: np.ndarray, n_buckets: int) -> np.ndarray:
"""tilt[b, a] = media di x nei giorni con label==b, MENO il tilt incondizionato
dell'asset (isola l'effetto calendario dal drift relativo generico)."""
base = np.nanmean(X.values, axis=0)
out = np.full((n_buckets, X.shape[1]), np.nan)
for b in range(n_buckets):
m = labels == b
if m.sum() >= 10:
out[b] = np.nanmean(X.values[m], axis=0) - base
return out
def split_half_persistence(X: pd.DataFrame, labels: np.ndarray, n_buckets: int,
n_perm: int = N_PERM):
"""Rank-corr H1 vs H2 dei tilt per bucket + null permutando le etichette entro
ciascuna metà. Ritorna (rho per bucket, max reale, p-value del max, null 95° pctl)."""
half = len(X) // 2
X1, X2 = X.iloc[:half], X.iloc[half:]
l1, l2 = labels[:half], labels[half:]
def rhos(la, lb):
t1, t2 = bucket_tilts(X1, la, n_buckets), bucket_tilts(X2, lb, n_buckets)
return np.array([spearman(t1[b], t2[b])
if np.isfinite(t1[b]).all() and np.isfinite(t2[b]).all() else np.nan
for b in range(n_buckets)])
real = rhos(l1, l2)
real_max = float(np.nanmax(real))
null_max = np.empty(n_perm)
for i in range(n_perm):
null_max[i] = np.nanmax(rhos(RNG.permutation(l1), RNG.permutation(l2)))
pval = float(np.mean(null_max >= real_max))
return real, real_max, pval, float(np.percentile(null_max, 95))
def weekend_monday_ic(X: pd.DataFrame):
"""Pattern pre/post weekend: il ritorno relativo cumulato Sab+Dom predice
(cross-sectionalmente) il ritorno relativo del lunedì? IC = Spearman giornaliero;
riporta mean IC e t-stat su ciascuna metà (persistenza del segno)."""
wd = X.index.dayofweek.values
ics, dates = [], []
for i in np.where(wd == 0)[0]: # lunedì
if i < 2 or wd[i - 1] != 6 or wd[i - 2] != 5:
continue
wkend = np.nansum(X.values[i - 2:i], axis=0) # Sab+Dom relativo
mon = X.values[i]
ok = np.isfinite(wkend) & np.isfinite(mon)
if ok.sum() >= 10:
ics.append(spearman(wkend[ok], mon[ok])); dates.append(X.index[i])
s = pd.Series(ics, index=pd.DatetimeIndex(dates))
half = len(s) // 2
out = {}
for name, seg in (("H1", s.iloc[:half]), ("H2", s.iloc[half:]), ("FULL", s)):
t = float(seg.mean() / seg.std() * np.sqrt(len(seg))) if len(seg) > 3 and seg.std() > 0 else 0.0
out[name] = dict(mean_ic=round(float(seg.mean()), 4), t=round(t, 2), n=len(seg))
return out
# ---------------------------------------------------------------- strategia (solo se persiste)
def sharpe(r):
r = np.asarray(pd.Series(r).dropna().values, float)
return float(np.mean(r) / np.std(r) * np.sqrt(365.25)) if len(r) > 2 and np.std(r) > 0 else 0.0
def weekday_ls(R: pd.DataFrame, X: pd.DataFrame, k: int = 3, est_win: int = 0,
min_obs: int = 20, target_vol: float = 0.20):
"""L/S market-neutral: al close di t (dati ≤ t) stima il tilt per-asset del weekday
di DOMANI (expanding se est_win=0, altrimenti rolling est_win gg) e va long top-k /
short bottom-k per il giorno t+1. Fee su |Δw|. Ritorna (net Series, gross Series,
turnover medio/anno, breakeven fee %RT)."""
wd = X.index.dayofweek.values
xv = X.values
n, A = xv.shape
W = np.zeros((n, A))
# tilt causale: per ogni weekday d, media (expanding o rolling) dei soli giorni con wd==d fino a t
sums = np.zeros((7, A)); cnts = np.zeros((7, A))
hist: list[list[np.ndarray]] = [[] for _ in range(7)] # per rolling
for t in range(n - 1):
row = np.nan_to_num(xv[t], nan=0.0)
fin = np.isfinite(xv[t]).astype(float)
d = wd[t]
sums[d] += row; cnts[d] += fin
if est_win > 0:
hist[d].append(np.where(fin > 0, row, np.nan))
if len(hist[d]) > est_win:
hist[d].pop(0)
dn = wd[t + 1] # weekday di domani: noto (calendario)
if est_win > 0:
hh = np.array(hist[dn]) if hist[dn] else np.empty((0, A))
cnt = np.isfinite(hh).sum(axis=0) if len(hh) else np.zeros(A)
tilt = np.where(cnt >= min_obs, np.nanmean(hh, axis=0) if len(hh) else 0.0, np.nan)
else:
tilt = np.where(cnts[dn] >= min_obs, sums[dn] / np.maximum(cnts[dn], 1), np.nan)
ok = np.isfinite(tilt)
if ok.sum() >= 2 * k:
order = np.argsort(np.where(ok, tilt, -np.inf))
w = np.zeros(A)
w[order[-k:]] = 0.5 / k # long tilt positivi
lo = np.argsort(np.where(ok, tilt, np.inf))[:k]
w[lo] = -0.5 / k # short tilt negativi
W[t] = w
dret = np.nan_to_num(R.values, nan=0.0)
gross = np.zeros(n); gross[1:] = np.sum(W[:-1] * dret[1:], axis=1)
turn = np.abs(np.diff(W, axis=0, prepend=np.zeros((1, A)))).sum(axis=1)
net = gross - FEE_SIDE * turn
g, tn = pd.Series(gross, index=X.index), pd.Series(net, index=X.index)
rv = tn.rolling(30, min_periods=15).std().shift(1) * np.sqrt(365.25)
scale = np.clip(np.nan_to_num(target_vol / rv.replace(0, np.nan).values, nan=0.0), 0, 3.0)
net_vt = pd.Series(tn.values * scale, index=X.index)
mean_turn = float(turn.mean())
be_rt = 2 * float(gross.mean() / mean_turn) * 100 if mean_turn > 0 else np.nan # %RT a Sharpe 0
turn_yr = round(mean_turn * 365.25, 1)
return net_vt, g, turn_yr, be_rt
def run_strategy_branch(R, X):
"""Selezione IN-SAMPLE (pre-2025) su griglia piccola, hold-out, DSR su tutti i trial."""
grid = [dict(k=k, est_win=w) for k in (3, 5) for w in (0, 180)]
rows, all_full = [], []
for g in grid:
net, _, turn_yr, be = weekday_ls(R, X, **g)
ins, hold = net[net.index < HOLDOUT], net[net.index >= HOLDOUT]
rows.append(dict(params=g, ins=round(sharpe(ins), 2), hold=round(sharpe(hold), 2),
full=round(sharpe(net), 2), turn_yr=turn_yr,
be_rt=round(be, 3) if np.isfinite(be) else None, net=net))
all_full.append(sharpe(net))
chosen = max(rows, key=lambda r: r["ins"]) # selezione SOLO in-sample
dsr, sr0 = al.deflated_sharpe(chosen["full"], all_full, chosen["net"].dropna().values)
return rows, chosen, dsr, sr0
# ---------------------------------------------------------------- main
def main():
R, X = load_relative_returns()
print(f"Universo: {len(UNIVERSE)} major HL | {X.index[0].date()} -> {X.index[-1].date()} "
f"({len(X)} giorni, ~{len(X) / 365.25:.1f} anni) | barre vol=0 escluse: "
f"{int((~np.isfinite(X.values)).sum())} celle NaN")
print(f"LIMITE: ~{len(X) // 7 // 2} osservazioni per weekday per metà campione — "
f"soglia severa: max-statistic permutato, p<0.05\n")
wd = X.index.dayofweek.values
print("=" * 78)
print("STEP 1 — PERSISTENZA SPLIT-HALF (test statistico PRIMA della strategia)")
print("=" * 78)
# --- (a) weekday (7 bucket)
rho, mx, p, null95 = split_half_persistence(X, wd, 7)
print("\n[A] WEEKDAY TILT (tilt weekday-specifico, al netto del tilt incondizionato)")
for d in range(7):
print(f" {WD_NAMES[d]}: rank-corr H1 vs H2 = {rho[d]:+.3f}")
print(f" max reale = {mx:+.3f} | null 95° pctl (perm max-su-7) = {null95:+.3f} | p = {p:.3f}")
pass_wd = p < 0.05
# --- (b) weekend vs feriali (2 bucket)
wk_lab = (wd >= 5).astype(int)
rho_w, mx_w, p_w, null95_w = split_half_persistence(X, wk_lab, 2)
print(f"\n[B] WEEKEND-vs-FERIALI: rho weekday={rho_w[0]:+.3f} weekend={rho_w[1]:+.3f} "
f"| max={mx_w:+.3f} null95={null95_w:+.3f} p={p_w:.3f}")
pass_we = p_w < 0.05
# --- (c) turn-of-month (ultimi 2 + primi 2 gg del mese vs resto)
day = X.index.day.values
dim = X.index.days_in_month.values
tom_lab = ((day <= 2) | (day >= dim - 1)).astype(int)
rho_t, mx_t, p_t, null95_t = split_half_persistence(X, tom_lab, 2)
print(f"[C] TURN-OF-MONTH (±2gg): rho non-TOM={rho_t[0]:+.3f} TOM={rho_t[1]:+.3f} "
f"| max={mx_t:+.3f} null95={null95_t:+.3f} p={p_t:.3f}")
pass_tom = p_t < 0.05
# --- (d) pattern weekend->lunedì (IC cross-serial)
ic = weekend_monday_ic(X)
print(f"[D] WEEKEND->LUNEDÌ IC (Spearman x_weekend vs x_lunedì): "
f"H1 {ic['H1']['mean_ic']:+.3f} (t={ic['H1']['t']}) | "
f"H2 {ic['H2']['mean_ic']:+.3f} (t={ic['H2']['t']}) | "
f"FULL {ic['FULL']['mean_ic']:+.3f} (t={ic['FULL']['t']}, n={ic['FULL']['n']})")
pass_ic = (abs(ic["FULL"]["t"]) > 2.5
and np.sign(ic["H1"]["mean_ic"]) == np.sign(ic["H2"]["mean_ic"])
and abs(ic["H1"]["t"]) > 1.5 and abs(ic["H2"]["t"]) > 1.5)
print(f" persistenza segno + |t|>2.5 FULL + |t|>1.5 su entrambe le metà: {pass_ic}")
any_pass = pass_wd or pass_we or pass_tom or pass_ic
print("\n" + "=" * 78)
if not any_pass:
print("VERDETTO: SCARTATO — nessuna persistenza cross-sectional calendario.")
print("Nessun backtest eseguito (regola: test statistico prima della strategia).")
print("NB: il day_boundary_robust resta comunque NON eseguibile sui 19 alt (dati")
print("HL solo 1d) → anche un pass qui sarebbe stato al massimo LEAD condizionato.")
print("=" * 78)
return
# ---------------- branch strategia (si arriva qui solo con persistenza reale)
print("STEP 2 — persistenza rilevata: strategia L/S weekday-tilt (selezione IN-SAMPLE)")
print("=" * 78)
rows, chosen, dsr, sr0 = run_strategy_branch(R, X)
for r in rows:
print(f" k={r['params']['k']} est={'exp' if r['params']['est_win'] == 0 else r['params']['est_win']}: "
f"IS {r['ins']:+.2f} | HOLD {r['hold']:+.2f} | FULL {r['full']:+.2f} | "
f"turn/yr {r['turn_yr']} | breakeven fee {r['be_rt']}%RT")
print(f"\n CELLA IN-SAMPLE: {chosen['params']} -> FULL {chosen['full']:+.2f} "
f"HOLD {chosen['hold']:+.2f} | DSR={dsr:.3f} (null max {sr0:.2f}) "
f"{'PASS' if dsr >= 0.95 else 'FAIL'}")
print("\n ⚠ day_boundary_robust NON eseguibile su HL 1d (serve orario) → qualunque")
print(" esito qui è al massimo LEAD-forward CONDIZIONATO, mai sleeve.")
print("=" * 78)
if __name__ == "__main__":
main()
+662
View File
@@ -0,0 +1,662 @@
"""r0702_alb_claims — Audit STATISTICO delle claim "strategia Albimarini" (video didattici).
CLAIM DICHIARATE (input, dal video):
- Struttura: double diagonal DEEP OTM su SPX/SPY (short strangle ~6DTE + long strangle
a scadenza successiva, strike piu' lontani), durata trade 3-6 giorni di calendario.
- Track record: n=28 trade, win-rate 82% (23W/5L), P&L medio +$113/trade,
perdite totali $765 (loss medio $153), profit factor 5.16, capitale ~$10k,
estrapolazione "420% annuo", sizing a compounding 1->2->3->4 contratti.
Derivati: totale +$3.164; gross win $3.929 (avg win $171); PF = 3929/765 = 5.14 ok.
OBIETTIVO: QUANTIFICARE la critica (win-rate strutturale, coda non campionata, PF non
significativo su n=28), non riformularla. 6 test, ognuno con numeri.
DATI: SOLO locali gia' certificati/presenti nel progetto:
- data/raw/eq_spy_1d.parquet (SPY daily 1996-2026, fetch IB del filone GTAA)
- BTC/ETH 1d via altlib (Deribit mainnet certificato)
Nessuna rete. Nessun file scritto fuori dallo scratchpad. Nessun DatetimeIndex.view.
Convenzioni oneste:
- 6 giorni di calendario ~= 4 trading day (h=4 primario; h=2..5 come robustezza).
- Le probabilita' empiriche usano finestre rolling OVERLAPPING (stima della prob.
per-finestra); i test di significativita' usano le 28 finestre NON sovrapposte
del track record e cicli non sovrapposti nel replay.
- Il replay del Test 3 e' CLAIM-ANCHORED: usa l'economia per-trade dichiarata dal
video (massimo beneficio al claim); l'unico parametro nostro e' la severita'
della coda, calibrata in due modi (cap BS della diagonale / EV=0).
- BS senza skew nei Test 4-5: sottostima il premio degli OTM (a favore del claim).
Uso: uv run python scripts/research/r0702_alb_claims.py
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
import numpy as np
import pandas as pd
from scipy.stats import norm, t as student_t
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "research" / "alt"))
import altlib as al # noqa: E402
RNG = np.random.default_rng(20260702)
ROOT = Path(__file__).resolve().parents[2]
# ---- claim dichiarate ------------------------------------------------------
N_TRADES = 28
WINRATE_CLAIM = 23 / 28 # 0.821
AVG_PNL = 113.0 # $/trade
TOT_LOSS = 765.0 # $ totali (5 loss -> avg $153)
AVG_WIN = (N_TRADES * AVG_PNL + TOT_LOSS) / 23 # $171
AVG_LOSS = TOT_LOSS / 5 # $153
PF_CLAIM = (23 * AVG_WIN) / TOT_LOSS # 5.14
CAPITAL = 10_000.0
H_TD = 4 # 6 giorni calendario ~ 4 trading day
OUT: dict = {}
def p(msg=""):
print(msg, flush=True)
# ===========================================================================
# BS helpers (no dividendi/tassi — orizzonte 6-13 giorni)
# ===========================================================================
def bs_put(S, K, T, sigma):
T = max(T, 1e-9)
d1 = (np.log(S / K) + 0.5 * sigma**2 * T) / (sigma * np.sqrt(T))
d2 = d1 - sigma * np.sqrt(T)
return K * norm.cdf(-d2) - S * norm.cdf(-d1)
def bs_call(S, K, T, sigma):
T = max(T, 1e-9)
d1 = (np.log(S / K) + 0.5 * sigma**2 * T) / (sigma * np.sqrt(T))
d2 = d1 - sigma * np.sqrt(T)
return S * norm.cdf(d1) - K * norm.cdf(d2)
# ===========================================================================
# DATA
# ===========================================================================
def load_spy() -> pd.DataFrame:
df = pd.read_parquet(ROOT / "data" / "raw" / "eq_spy_1d.parquet")
df = df.sort_values("timestamp").reset_index(drop=True)
df["datetime"] = pd.to_datetime(df["timestamp"].astype("int64"), unit="ms", utc=True)
return df
def hmoves(close: np.ndarray, h: int) -> np.ndarray:
return close[h:] / close[:-h] - 1.0
def cal_moves(df: pd.DataFrame, days: int = 6) -> np.ndarray:
"""Move su ESATTAMENTE `days` giorni di calendario (ultimo close <= t+days).
Epoca ms esplicita (mai .view su DatetimeIndex)."""
ts = df["timestamp"].to_numpy(dtype="int64") # ms
c = df["close"].to_numpy(dtype=float)
tgt = ts + days * 86_400_000
j = np.searchsorted(ts, tgt, side="right") - 1 # ultimo close <= t+days
ok = j > np.arange(len(ts))
return c[j[ok]] / c[ok] - 1.0
# ===========================================================================
# TEST 1 — WIN-RATE STRUTTURALE
# ===========================================================================
def test1():
p("=" * 88)
p("TEST 1 — WIN-RATE STRUTTURALE: P(move >9% in ~6 giorni) e strike impliciti dall'82%")
p("=" * 88)
spy = load_spy()
c, lo, hi = spy["close"].values, spy["low"].values, spy["high"].values
r1 = np.diff(np.log(c))
vol_ann = r1.std() * np.sqrt(252)
res = {}
for h in (2, 3, 4, 5):
m = hmoves(c, h)
res[h] = dict(p_abs9=float(np.mean(np.abs(m) > 0.09)),
p_dn9=float(np.mean(m < -0.09)),
sigma=float(m.std()),
p_2sig=float(np.mean(np.abs(m) > 2 * m.std())))
m6c = cal_moves(spy, 6)
m4 = hmoves(c, H_TD)
# touch (management intra-finestra): min low / max high nei prossimi h giorni
n = len(c) - H_TD
lo_w = np.array([lo[i + 1:i + 1 + H_TD].min() for i in range(n)])
hi_w = np.array([hi[i + 1:i + 1 + H_TD].max() for i in range(n)])
p_touch9 = float(np.mean((lo_w < c[:n] * 0.91) | (hi_w > c[:n] * 1.09)))
# analitico: normale a vol 16% e alla vol campione; t-Student fittata sui move 4td
sig6_16 = 0.16 * np.sqrt(6 / 365)
p_norm16 = 2 * (1 - norm.cdf(0.09 / sig6_16))
sig4 = res[4]["sigma"]
p_norm_emp = 2 * (1 - norm.cdf(0.09 / sig4))
tdf, tloc, tscale = student_t.fit(m4)
p_t = float(2 * student_t.sf((0.09 - tloc) / tscale, tdf))
# strike implicito dall'82% di win (quantile 82% di |move|) e win-rate a 9% OTM
q82_4 = float(np.quantile(np.abs(m4), WINRATE_CLAIM))
q82_6c = float(np.quantile(np.abs(m6c), WINRATE_CLAIM))
win_at_9 = 1 - res[4]["p_abs9"]
p(f"SPY 1996-2026 ({len(spy)} barre, vol ann {vol_ann:.1%})")
for h in (2, 3, 4, 5):
r = res[h]
p(f" h={h}td: P(|m|>9%)={r['p_abs9']:.4%} P(m<-9%)={r['p_dn9']:.4%} "
f"sigma={r['sigma']:.2%} P(|m|>2sig)={r['p_2sig']:.2%}")
p(f" 6 giorni CALENDARIO esatti: P(|m|>9%)={np.mean(np.abs(m6c) > 0.09):.4%} "
f"sigma={m6c.std():.2%}")
p(f" TOUCH intra-finestra (h=4td, strike a +-9%): P={p_touch9:.4%}")
p(f" Analitico: normale vol16% -> {p_norm16:.2e} | normale vol camp. -> {p_norm_emp:.2e} | "
f"t-Student fit (df={tdf:.2f}) -> {p_t:.4%} | empirico {res[4]['p_abs9']:.4%}")
p(f" -> fat-tail factor empirico/normale = {res[4]['p_abs9'] / p_norm_emp:,.0f}x")
p()
p(f" INCONSISTENZA QUANTIFICATA delle claim:")
p(f" (a) se gli strike sono DAVVERO ~9% OTM: win-rate strutturale a scadenza = "
f"{win_at_9:.2%} (>=99%), non 82% -> le 5 perdite non sono breach, sono gestione;")
p(f" (b) se il win-rate 82% e' letterale (breach a scadenza): gli strike distano "
f"q82(|m4|) = {q82_4:.2%} (6gg cal: {q82_6c:.2%}) ~= 1.2 sigma, NON 'deep OTM'.")
p(f" In entrambi i casi il win-rate e' il quantile della distribuzione (il delta "
f"venduto), non skill: qualunque venditore agli stessi strike ottiene lo stesso numero.")
# BTC/ETH dai NOSTRI dati certificati
crypto = {}
for a in ("BTC", "ETH"):
d = al.get(a, "1d")
cc = d["close"].values
m6 = hmoves(cc, 6)
crypto[a] = dict(p_abs9=float(np.mean(np.abs(m6) > 0.09)),
sigma=float(m6.std()),
p_2sig=float(np.mean(np.abs(m6) > 2 * m6.std())),
q82=float(np.quantile(np.abs(m6), WINRATE_CLAIM)),
span=f"{d['datetime'].iloc[0].date()}->{d['datetime'].iloc[-1].date()}",
last=float(cc[-1]))
p(f" {a} (certificato, {crypto[a]['span']}): P(|m6d|>9%)={crypto[a]['p_abs9']:.1%} "
f"sigma6d={crypto[a]['sigma']:.1%} P(>2sig)={crypto[a]['p_2sig']:.1%} "
f"strike per win82% = {crypto[a]['q82']:.1%} OTM")
p(f" -> la stessa struttura trasposta su crypto: strike a 9% OTM = win-rate "
f"{1 - crypto['BTC']['p_abs9']:.0%} (BTC) / {1 - crypto['ETH']['p_abs9']:.0%} (ETH), "
f"cioe' il 9% che su SPY e' 'deep' su crypto e' ~1 sigma.")
OUT["test1"] = dict(spy=res, spy_6cal_p9=float(np.mean(np.abs(m6c) > 0.09)),
p_touch9=p_touch9, p_norm16=float(p_norm16),
p_norm_emp=float(p_norm_emp), p_t=p_t, t_df=float(tdf),
q82_4td=q82_4, q82_6cal=q82_6c, win_at_9otm=float(win_at_9),
crypto=crypto)
return spy, m4, q82_4
# ===========================================================================
# TEST 2 — SIGNIFICATIVITA' DEL PF 5.16 SU n=28 (null EV=0)
# ===========================================================================
def test2(spy: pd.DataFrame, q82: float):
p("\n" + "=" * 88)
p("TEST 2 — PF 5.16 su n=28: distribuzione sotto il null 'vendita premio a EV=0 netto'")
p("=" * 88)
c = spy["close"].values
m4 = hmoves(c, H_TD)
NSIM = 200_000
W = 126 # 6 mesi di trading day
def branch(name: str, thr: float) -> dict:
"""Null EV=0: win +$171 (p=0.821), small loss -$153, tail loss -L con
p_tail = P(|m4|>thr) empirica; L calibrata perche' EV=0 netto."""
p_loss = 1 - WINRATE_CLAIM
p_tail = float(np.mean(np.abs(m4) > thr))
p_small = max(p_loss - p_tail, 0.0)
L_tail = (WINRATE_CLAIM * AVG_WIN - p_small * AVG_LOSS) / p_tail
wins = RNG.normal(AVG_WIN, 60, (NSIM, N_TRADES)).clip(20, None)
smalls = RNG.normal(AVG_LOSS, 60, (NSIM, N_TRADES)).clip(30, None)
u = RNG.random((NSIM, N_TRADES))
is_win = u < min(WINRATE_CLAIM, 1 - p_tail)
is_tail = u > 1 - p_tail
pnl = np.where(is_win, wins, np.where(is_tail, -L_tail, -smalls))
gp = np.where(pnl > 0, pnl, 0).sum(1)
gl = -np.where(pnl < 0, pnl, 0).sum(1)
pf = gp / np.maximum(gl, 1e-9)
wr = is_win.mean(1)
p_joint = float(np.mean((pf >= PF_CLAIM) & (wr >= WINRATE_CLAIM - 1e-9)))
p_notail = float(np.mean(~is_tail.any(1)))
tail_day = np.abs(m4) > thr
frac_clean = float(np.mean([not tail_day[i:i + W].any()
for i in range(0, len(tail_day) - W)]))
r = dict(name=name, thr=thr, p_tail=p_tail, p_small=p_small,
L_tail=float(L_tail), ev_check=float(pnl.mean()),
pf_median=float(np.median(pf)),
p_pf_ge=float(np.mean(pf >= PF_CLAIM)), p_joint=p_joint,
p_no_tail_28=p_notail,
p_no_tail_analytic=float((1 - p_tail) ** N_TRADES),
frac_6m_clean=frac_clean)
p(f" Branch {name}:")
p(f" tail = |m4| > {thr:.2%}: P(tail/trade)={p_tail:.2%} -> L_tail per EV=0 = "
f"${L_tail:,.0f} ({L_tail / CAPITAL:.0%} del capitale per unita' di size); "
f"EV MC {pnl.mean():+.1f}$/trade")
p(f" PF su 28 trade: mediana {r['pf_median']:.2f} | P(PF>=5.16)={r['p_pf_ge']:.1%} | "
f"P(PF>=5.16 E win>=82%)={p_joint:.1%}")
p(f" P(zero tail in 28 trade)={p_notail:.1%} (analitico {r['p_no_tail_analytic']:.1%}); "
f"quota storica finestre 6 mesi SENZA tail = {frac_clean:.1%}")
return r
p(f"Null 'venditore di premio a EV=0 netto' (tanti +$171, -$153 gestite, code rare):")
b_obs = branch(f"OBS (strike {q82:.2%} OTM, breach oltre il long +2%)", q82 + 0.02)
b_deep = branch("DEEP (strike 9% OTM come dichiarato)", 0.09)
p(f" -> Il track record dichiarato (PF 5.16, win 82%, zero code) e' un esito da "
f"P={b_obs['p_joint']:.0%} (lettura OBS) a P={b_deep['p_joint']:.0%} (lettura DEEP) "
f"sotto ZERO skill: tra 'comune' e 'esito mediano'.")
p(f" Il PF senza coda campionata misura solo 23/5 * avg_win/avg_smallloss = "
f"{(23 * AVG_WIN) / (5 * AVG_LOSS):.2f}: e' un rapporto STRUTTURALE, non una statistica "
f"di edge (n=28 non tocca mai la coda che paga tutto).")
OUT["test2"] = dict(obs=b_obs, deep=b_deep)
return b_obs["L_tail"]
# ===========================================================================
# TEST 3 — IL "420% ANNUO" ATTRAVERSO UNA CODA REALE (replay claim-anchored)
# ===========================================================================
def _replay(dates, moves, q82: float, width: float, L_tail: float,
win_amt: float, small_amt: float,
sizing: str = "video", start: float = CAPITAL, label: str = ""):
"""Replay CLAIM-ANCHORED sulla sequenza REALE di move (cicli non sovrapposti).
PER CONTRATTO:
|m| <= q82 -> +win_amt (win, calibrato sui numeri del video)
q82 < |m| <= q82+w -> -small_amt (perdita gestita, come le 5 del video)
|m| > q82+w -> -L_tail (breach oltre il long: coda)
sizing: 'video' = +1 contratto ogni +$1000 di profitto (1->2->3->4 come
dichiarato), cap margine equity/$2500; 'prop' = leva costante iniziale
(n = equity//10k); 'fixed' = 1 contratto. Ruin se equity <= 0."""
eq = start
path, pnl_hist, npos = [], [], []
ruined = None
for i, m in enumerate(moves):
if abs(m) <= q82:
out = win_amt
elif abs(m) <= q82 + width:
out = -small_amt
else:
out = -L_tail
if sizing == "video":
n = int(np.clip(1 + max(eq - start, 0) // 1000, 1, max(1, eq // 2500)))
elif sizing == "prop":
n = int(max(1, eq // 10_000))
else:
n = 1
pnl = n * out
eq += pnl
pnl_hist.append(pnl)
npos.append(n)
path.append(eq)
if eq <= 0:
ruined = dates[i]
break
path = np.asarray(path, dtype=float)
peak = np.maximum.accumulate(np.maximum(path, 1e-9))
maxdd = float((path / peak - 1.0).min()) if len(path) else 0.0
last_i = len(path) - 1
yrs = max(1e-9, (pd.Timestamp(dates[last_i]) - pd.Timestamp(dates[0])).days / 365.25)
cagr = float((max(path[-1], 1e-9) / start) ** (1 / yrs) - 1) if len(path) else 0.0
wi = int(np.argmin(pnl_hist)) if pnl_hist else 0
return dict(label=label, final=float(path[-1]) if len(path) else start,
cagr=cagr, maxdd=maxdd,
ruined=str(pd.Timestamp(ruined).date()) if ruined is not None else None,
worst_cycle=float(min(pnl_hist)) if pnl_hist else 0.0,
worst_date=str(pd.Timestamp(dates[wi]).date()) if pnl_hist else None,
n_at_worst=int(npos[wi]) if npos else 0,
n_cycles=len(pnl_hist),
n_tails=int(np.sum(np.abs(np.asarray(moves[:len(pnl_hist)])) > q82 + width)),
avg_pnl_cycle=float(np.mean(pnl_hist)) if pnl_hist else 0.0,
winrate=float(np.mean(np.asarray(pnl_hist) > 0)) if pnl_hist else 0.0,
years=float(yrs))
def _cycles(df: pd.DataFrame, h: int):
c = df["close"].values
idx = np.arange(0, len(c) - h, h)
moves = c[idx + h] / c[idx] - 1.0
dates = df["datetime"].values[idx + h]
return dates, moves
def _calib_win_per_contract() -> float:
"""Calibra il win PER CONTRATTO cosi' che 28 cicli TUTTI win con il sizing
video (+1 contratto/$1000, 1->2->3->4) riproducano il totale dichiarato
(+$3.164): i +$171/+$113 del video sono medie a livello CONTO su 1-4
contratti, non per contratto."""
lo, hi = 10.0, 171.0
for _ in range(60):
w = 0.5 * (lo + hi)
eq, tot = CAPITAL, 0.0
for _k in range(N_TRADES):
n = int(np.clip(1 + max(eq - CAPITAL, 0) // 1000, 1, max(1, eq // 2500)))
eq += n * w
tot += n * w
if tot > N_TRADES * AVG_PNL:
hi = w
else:
lo = w
return round(0.5 * (lo + hi), 1)
def test3(spy: pd.DataFrame, q82: float):
p("\n" + "=" * 88)
p("TEST 3 — '420% ANNUO': la stessa strategia attraverso le code reali, con compounding")
p("=" * 88)
# Replay CLAIM-ANCHORED: economia PER CONTRATTO calibrata sui numeri del video,
# guidata dalla sequenza REALE dei move ~6gg. Unico parametro nostro: la
# severita' della coda per contratto, boxata da due letture convergenti:
# cap fisico = width 2% x notional $60k = $1.200 (cap della diagonale, Test 4)
# EV=0 = la severita' che rende il gioco fair alle p empiriche
w_c = _calib_win_per_contract()
s_c = round(w_c * AVG_LOSS / AVG_WIN, 1)
m4 = hmoves(spy["close"].values, H_TD)
p_tail = float(np.mean(np.abs(m4) > q82 + 0.02))
p_small = max(1 - WINRATE_CLAIM - p_tail, 0.0)
L_ev0 = round((WINRATE_CLAIM * w_c - p_small * s_c) / p_tail, 0)
L_bs = 1200.0
p(f"Replay claim-anchored (cicli 4td non sovrapposti, strike q82={q82:.2%}, long +2%):")
p(f"PER CONTRATTO: win +${w_c} (|m|<=q82), gestita -${s_c} (<=q82+2%), coda -$L")
p(f"(|m|>q82+2%). Calibrazione: 28 cicli senza coda col sizing video 1->4 = +$3.164")
p(f"(riproduce il video PER COSTRUZIONE: win 82%, PF 5.1). Severita' coda boxata:")
p(f"cap fisico width2%x$60k = ${L_bs:,.0f} | EV=0 = ${L_ev0:,.0f} (convergenti).")
p(f"Sizing 'video' = +1 contratto/$1000 profitto (cap eq/$2500); 'prop' = eq//10k.")
windows = [("FULL 1996-2026", None, None),
("2019-2026 (era book)", "2019-01-01", None),
("H2-2023 (finestra tipo video)", "2023-07-01", "2023-12-31"),
("2020 (COVID)", "2020-01-01", "2020-12-31"),
("2022 (bear)", "2022-01-01", "2022-12-31"),
("2008 (GFC)", "2008-01-01", "2008-12-31")]
rows = []
for sizing, L, tag in (("video", L_bs, "video, cap fisico $1.200"),
("video", L_ev0, f"video, EV=0 ${L_ev0:,.0f}"),
("prop", L_bs, "leva costante, cap fisico")):
p(f"\n -- sizing {tag} --")
for label, a, b in windows:
d = spy
if a:
d = d[d["datetime"] >= pd.Timestamp(a, tz="UTC")]
if b:
d = d[d["datetime"] <= pd.Timestamp(b, tz="UTC")]
dates, moves = _cycles(d.reset_index(drop=True), H_TD)
out = _replay(dates, moves, q82, 0.02, L, w_c, s_c, sizing=sizing,
label=f"{label} [{tag}]")
rows.append(out)
ann = f"{out['cagr']:+.0%}" if not out["ruined"] else "RUIN"
p(f" {label:30s} finale ${out['final']:>8,.0f} CAGR {ann:>7s} "
f"maxDD {out['maxdd']:>5.0%} win {out['winrate']:.0%} "
f"code {out['n_tails']:>2d} worst ${out['worst_cycle']:>7,.0f} "
f"({out['worst_date']}, {out['n_at_worst']}c)"
+ (f" ** ROVINA {out['ruined']} **" if out["ruined"] else ""))
# controllo: size FISSA 1 contratto, e scenario 'premio generoso' (+20% sui win)
dates, moves = _cycles(spy, H_TD)
fx = _replay(dates, moves, q82, 0.02, L_ev0, w_c, s_c, sizing="fixed",
label="FULL fixed-1c EV=0")
gen_fx = _replay(dates, moves, q82, 0.02, L_ev0, w_c * 1.2, s_c, sizing="fixed",
label="FULL fixed-1c premio+20%")
gen = _replay(dates, moves, q82, 0.02, L_ev0, w_c * 1.2, s_c, sizing="video",
label="FULL video premio+20%")
p(f"\n Controlli (FULL 1996-2026, coda EV=0):")
p(f" - size FISSA 1 contratto: finale ${fx['final']:>8,.0f} CAGR {fx['cagr']:+.1%} "
f"maxDD {fx['maxdd']:.0%}" + (f" ROVINA {fx['ruined']}" if fx['ruined'] else ""))
p(f" - fixed-1c, premio +20%: finale ${gen_fx['final']:>8,.0f} CAGR "
f"{gen_fx['cagr']:+.1%} maxDD {gen_fx['maxdd']:.0%}"
+ (f" ROVINA {gen_fx['ruined']}" if gen_fx['ruined'] else ""))
p(f" - sizing video, premio +20%: finale ${gen['final']:>8,.0f} CAGR "
f"{gen['cagr']:+.1%} maxDD {gen['maxdd']:.0%}"
+ (f" ROVINA {gen['ruined']}" if gen['ruined'] else ""))
p(f" -> anche regalando un VRP persistente del +20%, il CAGR onesto multi-anno e'")
p(f" a una cifra, NON 420%; e il sizing del video lo converte comunque in rovina")
p(f" (scala dopo i win -> la coda colpisce sempre vicino alla size massima).")
rows += [fx, gen_fx, gen]
# BTC/ETH dai NOSTRI dati: stesso 82% strutturale (q82 proprio), width vol-scalata,
# severita' EV=0 ricalibrata sulla p_tail dell'asset.
p()
for aname in ("BTC", "ETH"):
d = al.get(aname, "1d")
m6 = hmoves(d["close"].values, 6)
q82a = float(np.quantile(np.abs(m6), WINRATE_CLAIM))
wa = float(0.9 * m6.std())
p_tail_a = float(np.mean(np.abs(m6) > q82a + wa))
La = round((WINRATE_CLAIM * w_c
- max(1 - WINRATE_CLAIM - p_tail_a, 0) * s_c) / p_tail_a, 0)
dates_a, moves_a = _cycles(d, 6)
out = _replay(dates_a, moves_a, q82a, wa, La, w_c, s_c, sizing="video",
label=f"{aname} strike {q82a:.0%} width {wa:.0%} EV=0")
rows.append(out)
ann = f"{out['cagr']:+.0%}" if not out["ruined"] else "RUIN"
p(f" {out['label']:38s} P(coda)={p_tail_a:.1%} L=${La:,.0f} -> finale "
f"${out['final']:>8,.0f} CAGR {ann:>6s} maxDD {out['maxdd']:>5.0%} "
f"code {out['n_tails']}"
+ (f" ** ROVINA {out['ruined']} **" if out["ruined"] else ""))
p()
p(" -> Il '420%' e' l'annualizzazione della finestra senza coda con il sizing gia'")
p(" scalato. Sulla storia intera, con la STESSA economia dichiarata dal video e")
p(" una coda fair, il compounding del video incontra la coda alla size massima.")
OUT["test3"] = dict(win_per_contract=w_c, small_per_contract=s_c,
L_bs=L_bs, L_ev0=L_ev0, p_tail=p_tail, rows=rows)
# ===========================================================================
# TEST 4 — QUANTO COPRE LA DIAGONALE IL GIORNO DEL CRASH
# ===========================================================================
def test4(q82: float):
p("\n" + "=" * 88)
p("TEST 4 — DIAGONALE COME PROTEZIONE: loss_con_diagonale / loss_naked (BS, gap+vol spike)")
p("=" * 88)
S0 = 100.0
iv0 = 0.16
def scenario(d_short, width, gap, iv1):
"""Ritorna (loss_naked, loss_diag, ratio) in frazione di S0 (positivi=perdita)."""
Kp_s = S0 * (1 - d_short)
Kp_l = S0 * (1 - d_short - width)
v_s0 = bs_put(S0, Kp_s, 6 / 365, iv0)
v_l0 = bs_put(S0, Kp_l, 13 / 365, iv0)
S1 = S0 * (1 + gap)
v_s1 = bs_put(S1, Kp_s, 5 / 365, iv1)
v_l1 = bs_put(S1, Kp_l, 12 / 365, iv1)
loss_naked = float(v_s1 - v_s0) # mark contro lo short
loss_diag = float((v_s1 - v_s0) - (v_l1 - v_l0)) # long compensa
ratio = loss_diag / loss_naked if loss_naked > 1e-9 else np.nan
return loss_naked, loss_diag, ratio
rows = []
p(f"Struttura put-side branch OBS: short K={100 * (1 - q82):.1f} T=6g, long T=13g piu' OTM.")
p(f"Scenario: gap overnight (resta 1g di vita in meno), IV 16% -> IV_crash. Perdite % notional.")
p(f" {'gap':>5s} {'IV->':>5s} {'width':>6s} | {'naked':>7s} {'diag':>7s} {'ratio':>6s} "
f"{'equity hit @6x':>14s}")
for width in (0.02, 0.03):
for gap in (-0.05, -0.10, -0.15):
for iv1 in (0.16, 0.40, 0.50, 0.70):
ln, ld, ratio = scenario(q82, width, gap, iv1)
rows.append(dict(d=q82, width=width, gap=gap, iv1=iv1,
naked=ln, diag=ld, ratio=ratio))
p(f" {gap:>5.0%} {iv1:>5.0%} {width:>6.0%} | {ln / S0:>7.2%} "
f"{ld / S0:>7.2%} {ratio:>6.0%} {6 * ld / S0:>13.1%}")
# branch DEEP (9% OTM come dichiarato), width 3%
deep = []
for gap in (-0.10, -0.15):
for iv1 in (0.40, 0.50, 0.70):
ln, ld, ratio = scenario(0.09, 0.03, gap, iv1)
deep.append(dict(d=0.09, width=0.03, gap=gap, iv1=iv1,
naked=ln, diag=ld, ratio=ratio))
p()
p(" Branch DEEP (short 9% OTM, long 12% OTM, width 3%):")
for r in deep:
p(f" {r['gap']:>5.0%} IV->{r['iv1']:.0%} | naked {r['naked'] / S0:>6.2%} "
f"diag {r['diag'] / S0:>6.2%} ratio {r['ratio']:>4.0%} "
f"equity hit @6x {6 * r['diag'] / S0:>6.1%}")
r10 = [r for r in rows if r["gap"] == -0.10 and r["iv1"] == 0.50]
r10_novol = [r for r in rows if r["gap"] == -0.10 and r["iv1"] == 0.16
and r["width"] == 0.02][0]
p()
p(f" -> La diagonale NON e' un hedge pieno e NON e' gratis: a gap -10%/IV 50 lascia")
p(f" passare il {r10[0]['ratio']:.0%} (width 2%) / {r10[1]['ratio']:.0%} (width 3%) "
f"della perdita naked; SENZA vol spike (IV ferma) il {r10_novol['ratio']:.0%}.")
p(f" A 6x notional un -10% costa {6 * r10[0]['diag'] / S0:.0%}-"
f"{6 * r10_novol['diag'] / S0:.0%} di equity PER UNITA' (x4 unita' post-scaling = "
f"{24 * r10[0]['diag'] / S0:.0%}-{24 * r10_novol['diag'] / S0:.0%}).")
p(f" La protezione dipende dal vol-spike che gonfia il long (vega del T+7g): e'")
p(f" un cap alla ROVINA, non alla perdita — e il costo del cap (long ricomprato")
p(f" ogni ciclo) e' il motivo per cui l'EV netto resta ~0 (Test 2/3).")
OUT["test4"] = dict(obs=rows, deep=deep)
# ===========================================================================
# TEST 5 — ESEGUIBILITA' PER NOI (Deribit BTC/ETH, $600)
# ===========================================================================
def test5():
p("\n" + "=" * 88)
p("TEST 5 — ESEGUIBILITA' su Deribit BTC/ETH al NOSTRO capitale ($600)")
p("=" * 88)
res = {}
for a, minsz in (("BTC", 0.1), ("ETH", 0.1)):
d = al.get(a, "1d")
S = float(d["close"].values[-1])
m6 = hmoves(d["close"].values, 6)
q82a = float(np.quantile(np.abs(m6), WINRATE_CLAIM))
leg_notional = minsz * S
sig = 0.55 if a == "BTC" else 0.70
w = float(0.9 * m6.std()) # width vol-scalata (come il 2% su SPY)
short0 = bs_put(S, S * (1 - q82a), 6 / 365, sig) + bs_call(S, S * (1 + q82a), 6 / 365, sig)
long0 = (bs_put(S, S * (1 - q82a - w), 13 / 365, sig)
+ bs_call(S, S * (1 + q82a + w), 13 / 365, sig))
long7 = (bs_put(S, S * (1 - q82a - w), 7 / 365, sig)
+ bs_call(S, S * (1 + q82a + w), 7 / 365, sig))
# raccolto theta atteso per ciclo a mercato FERMO (spread 5%/gamba)
harvest = (0.95 * short0 - 1.05 * long0 + 0.95 * long7) / S * leg_notional
# fee Deribit opzioni: min(0.0003*S, 12.5% premio) per gamba; 8 esecuzioni/ciclo
avg_leg = (short0 / 2 + long0 / 2) / 2 / S * leg_notional
fee = 8 * min(0.0003 * S * minsz, 0.125 * max(avg_leg, 1e-9))
# margine short (standard margin, senza netting sulle diagonali):
# ~max(0.15-OTM, 0.10)*S*size + mark, x2 gambe short
margin = 2 * (max(0.15 - q82a, 0.10) * S * minsz + short0 / 2 * minsz)
# tail netta: full-breach del cap (gap = -(q82+w), IV x1.5), lato put
gap_full = -(q82a + w)
Sc = S * (1 + gap_full)
vs = bs_put(Sc, S * (1 - q82a), 1 / 365, 1.5 * sig)
vl = bs_put(Sc, S * (1 - q82a - w), 8 / 365, 1.5 * sig)
vs0 = bs_put(S, S * (1 - q82a), 6 / 365, sig)
vl0 = bs_put(S, S * (1 - q82a - w), 13 / 365, sig)
tail = float(((vs - vs0) - (vl - vl0)) / S * leg_notional)
net_cycle = harvest - fee
fee_pct = f"{fee / harvest:.0%} del raccolto" if harvest > 0 else "raccolto <=0!"
res[a] = dict(S=S, q82=q82a, width=w, leg_notional=leg_notional,
harvest_cycle=float(harvest), fee_rt=float(fee),
net_cycle=float(net_cycle), margin=float(margin), tail_net=tail,
gap_full=float(gap_full))
p(f" {a} (close {S:,.0f}, strike {q82a:.0%} OTM per win 82%, width {w:.0%}): "
f"min {minsz}/gamba -> {leg_notional:,.0f}$/gamba, 4 gambe = "
f"{4 * leg_notional:,.0f}$ notional")
p(f" raccolto theta/ciclo (mercato fermo, netto spread 5%) ~${harvest:.2f}, "
f"fee Deribit 8 gambe ~${fee:.2f} ({fee_pct}) -> NETTO ${net_cycle:+.2f}/ciclo;")
p(f" margine short ~${margin:,.0f}; tail netta (full-breach {gap_full:.0%} 6d, "
f"IV x1.5) ~${tail:,.0f}")
p()
b, e = res["BTC"], res["ETH"]
p(f" Fee strutturale: le fee Deribit sono PROPORZIONALI alla size -> il rapporto")
p(f" fee/raccolto ({b['fee_rt'] / max(b['harvest_cycle'], 1e-9):.0%} BTC, "
f"{e['fee_rt'] / max(e['harvest_cycle'], 1e-9):.0%} ETH) NON migliora scalando:")
p(f" a questi strike la DOUBLE DIAGONAL e' fee-negativa su Deribit A QUALSIASI size")
p(f" (netto {b['net_cycle']:+.2f}$/ciclo BTC, {e['net_cycle']:+.2f}$/ciclo ETH per "
f"unita' minima). Il capitale sposta il muro di margine, non questo.")
p()
# ---- soglie di capitale: $600 oggi, 2k/3.5k/5k prospettici --------------
# Granularita' sensata = >=3 posizioni concorrenti + scalabile 1->2 contratti
# (>=6 slot di margine), budget margine 25% equity, rischio/posizione <=5%.
# ETH CREDIT SPREAD: numeri del filone gemello ALB-A (margine ~$107/spread =
# max-loss defined-risk, credito ~$2/spread) + fee 2 gambe.
sp_margin, sp_credit = 107.0, 2.0
sp_fee = 2 * min(0.0003 * e["S"] * 0.1, 0.125 * sp_credit) # entry 2 gambe, 0.1 ETH
p(f" SOGLIE DI CAPITALE (budget margine 25%, rischio/pos <=5%, granularita' sensata")
p(f" = 3 posizioni concorrenti scalabili 1->2 = 6 slot):")
p(f" {'conto':>7s} | {'DD BTC (m.$' + format(b['margin'], ',.0f') + ')':>22s} | "
f"{'DD ETH min (m.$' + format(e['margin'], ',.0f') + ')':>22s} | "
f"{'spread ETH (m.$107)':>20s}")
tiers = {}
for cap in (600.0, 2000.0, 3500.0, 5000.0):
mbud = 0.25 * cap
n_btc = int(mbud // b["margin"])
n_eth = int(mbud // e["margin"])
n_sp = int(mbud // sp_margin)
risk_ok_sp = sp_margin <= 0.05 * cap # max-loss spread <= 5% equity
risk_ok_eth = e["tail_net"] <= 0.05 * cap
tiers[int(cap)] = dict(margin_budget=mbud, n_btc=n_btc, n_eth_dd=n_eth,
n_spread=n_sp, spread_risk_ok=bool(risk_ok_sp),
eth_dd_risk_ok=bool(risk_ok_eth),
spread_pct_per_pos=sp_margin / cap,
spread_income_yr=n_sp * (sp_credit - sp_fee) * 52)
p(f" {cap:>7,.0f} | {n_btc:>4d} unita' ({b['margin'] / cap:>4.0%}/u) | "
f"{n_eth:>4d} unita' ({e['margin'] / cap:>4.0%}/u) | "
f"{n_sp:>3d} spread ({sp_margin / cap:>4.0%}/u, risk5% "
f"{'OK' if risk_ok_sp else 'NO'})")
p()
p(f" Lettura (con upgrade conto 2-5k):")
p(f" - DOUBLE DIAGONAL BTC: margine ${b['margin']:,.0f}/unita' -> 1a unita' dentro il")
p(f" budget 25% solo da ~${4 * b['margin']:,.0f}; 'granularita' sensata' (6 slot) da "
f"~${24 * b['margin'] / 1000:,.0f}k. Nemmeno 5k basta: resta il muro gamma-scalp")
p(f" (diario 2026-06-26: 'opzione BTC min $5.968 >> $600'), che si sposta a ~10-30k.")
p(f" - DOUBLE DIAGONAL ETH min 0.1: gia' a 2k il margine non e' il vincolo "
f"({int(0.25 * 2000 / e['margin'])} unita' nel budget), ma resta FEE-NEGATIVA "
f"({e['net_cycle']:+.2f}$/ciclo): eseguibile != conveniente, a ogni soglia.")
p(f" - CREDIT SPREAD ETH (ALB-A): il PRIMO oggetto con soglia reale. A $600: 1 spread")
p(f" = {sp_margin / 600:.0%} del conto (max-loss {sp_margin / 600:.0%} > 5% -> NO). "
f"A 2k: {int(0.25 * 2000 // sp_margin)} spread nel budget, max-loss {sp_margin / 2000:.1%}")
p(f" ~ok ma niente scalata 1->2 su 3 posizioni; a 3.5k: {int(0.25 * 3500 // sp_margin)} slot "
f"(3 pos x2 non ancora); a 5k: {int(0.25 * 5000 // sp_margin)} slot -> granularita' OK")
p(f" (3 concorrenti scalabili 1->2, {6 * sp_margin / 5000:.0%} del conto impegnato).")
p(f" SOGLIA ~${int(np.ceil(6 * sp_margin / 0.25 / 100) * 100):,} per il set completo; "
f"~$2.1k per 5 slot senza scalata. Income lordo a 5k: "
f"{tiers[5000]['spread_income_yr']:.0f}$/anno ({tiers[5000]['spread_income_yr'] / 5000:.1%}) "
f"PRIMA delle code (Test 2-3: EV~0).")
p(f" - Confronto scala video: 10k su SPX = 1 contratto SPY (~$60k notional, 6x leva).")
p(f" L'equivalente Deribit a parita' di prudenza (tail 5%) e' ~${20 * e['tail_net']:,.0f}+")
p(f" su ETH e ~${20 * b['tail_net']:,.0f}+ su BTC: anche a 5k il conto e' SOTTO la")
p(f" scala minima del gioco del video su BTC, e ci sta su ETH solo in versione")
p(f" credit-spread (che e' VRP01, non 'Albimarini').")
OUT["test5"] = dict(assets=res, tiers=tiers,
spread=dict(margin=sp_margin, credit=sp_credit, fee=float(sp_fee)))
# ===========================================================================
# TEST 6 — CONFRONTO CON VRP01 (perche' un theta-harvest sopravvive)
# ===========================================================================
def test6():
p("\n" + "=" * 88)
p("TEST 6 — Che cosa distingue un theta-harvest che sopravvive (VRP01) da uno che no")
p("=" * 88)
p(" VRP01 (audit superato, diario 2026-06-20): (1) DEFINED-RISK come struttura, non")
p(" come accessorio: il long wing cappa worst-week -16.6% -> -7.4% e DD 33% -> 21%;")
p(" (2) GATE di regime IV-rank>0.30 (vende solo vol RICCA): ribalta l'HOLD-OUT da")
p(" -0.25 a +0.28 — l'alpha e' il filtro, non il theta; (3) SIZING: sleeve al 12%")
p(" del book, niente compounding del numero di contratti; positivo/flat anche 2022.")
p(" La 'Albimarini' e' l'opposto quantificato: vende SEMPRE (nessun gate: incassa il")
p(" premio anche quando IV<RV), il rischio e' cappato solo dalla width della diagonale")
p(" (a 6x notional un gap -10% = ~-5/-12% equity PER UNITA', x4 post-scaling, Test 4),")
p(" e il sizing SCALA sui profitti proprio mentre la coda si avvicina (Test 3).")
p(" Win-rate 82% e PF 5.16 su 28 trade sono la FIRMA STRUTTURALE del venditore di")
p(" quantile a EV=0 in una finestra senza code (P 20-45%, Test 2), non un edge.")
# ===========================================================================
def main():
spy, m4, q82 = test1()
test2(spy, q82)
test3(spy, q82)
test4(q82)
test5()
test6()
p("\n" + "=" * 88)
p("JSON")
p("=" * 88)
p(json.dumps(OUT, indent=1, default=float))
if __name__ == "__main__":
main()
+333
View File
@@ -0,0 +1,333 @@
"""R0702 ALB-STRUCTURE — double diagonal deep-OTM "Albimarini" vs struttura VRP01.
FILONE: video didattici claimano su SPY una vendita sistematica di DOUBLE DIAGONAL a credito
(2 short scadenza T a ~9% di distanza su ~6 giorni + 2 long scadenza T+1g, entrambi i lati) con
82% win, PF 5.16, "420% annuo". Qui la STRUTTURA (non il gate lezione 2026-07-01: il gate
IV-rank>0.30 canonico NON si riottimizza) viene portata su BTC/ETH Deribit (che ha scadenze
giornaliere) e modellata onestamente sul nostro stack DVOL, contro il VRP01 canonico
(put credit spread settimanale -0.28/-0.10, gate vrp>0 + ivr>=0.30 + crash-skip 0.90).
CAVEAT SKEW IN TESTA, NON IN FONDO: il pricing e' Black-Scholes FLAT sulla DVOL (IV ATM 30g).
Il deep-OTM (qui 1.5-3.0 sigma ~ 8-25% di distanza) e' ESATTAMENTE dove il flat-vol sbaglia di piu':
su crypto lo smile e' ripido su ENTRAMBE le ali (su equity solo put). Il premio reale delle ali e'
probabilmente > modello (f>1) per le put e variabile per le call; la calibrazione reale che abbiamo
(f~1.0) e' ATM-ish delta -0.28 su finestra calma, NON copre il deep-OTM. Quindi OGNI numero va letto
come BANDA sul fattore premio f in {0.6, 0.8, 1.0, 1.3} + uno scenario skew asimmetrico
(f_put=1.3 / f_call=0.7), MAI come stima puntuale. In piu' la DVOL e' IV a 30g usata per tenor 3-6g
(term structure ignorata; snapshot vol_term 2026-06: iv_7d vs iv_30d entro ~±3pt in calma, ma in
stress il front-end esplode -> il modello SOTTOSTIMA il mark-to-market avverso in crash).
Distanza in unita' di VOL, non il 9% fisso di SPY: 9%/6g su SPY (IV~16%) = ~3.5-4 sigma; BTC si
muove del 9% in 6 giorni spesso. Cella centrale dichiarata A PRIORI: z=2.0 sigma, ali dz=+1.0 sigma,
tenor 5g (centro del range 3-6g del video). z=3.0 in griglia = cella "fedele al video".
Riusa: options_vrp_lab (bs_put, load_series, per_year), options_vrp_v2 (vrp_spread_weekly = VRP01,
_ivrank, _rv30), altlib.marginal_vs_tp01. Fee Deribit opzioni per gamba: 0.03% del notional cap
12.5% del premio (+ delivery 0.015% cap 12.5% sull'ITM a scadenza; il diagonale paga anche la fee
di USCITA per vendere le long residue a T). NON deploy: regola standing "niente short-vol da
modello in deploy" — l'esito massimo e' conoscenza sulla struttura.
uv run python scripts/research/r0702_alb_structure.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))
sys.path.insert(0, str(PROJECT_ROOT / "scripts" / "research" / "alt"))
import numpy as np, pandas as pd
from scipy.stats import norm
from scripts.research.options_vrp_lab import bs_put, load_series, per_year
from scripts.research.options_vrp_v2 import vrp_spread_weekly, _ivrank, _rv30
HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC")
DAY = 1.0 / 365.25
# cella centrale DICHIARATA A PRIORI (prima di guardare qualsiasi risultato)
CENTRAL = dict(z=2.0, dz=1.0, tenor_d=5)
F_SWEEP = (0.6, 0.8, 1.0, 1.3) # banda skew simmetrica
F_SKEW = dict(f_put=1.3, f_call=0.7) # scenario skew crypto-shaped (put ricche, call povere)
def bs_call(S, K, T, sig):
if T <= 0 or sig <= 0:
return max(S - K, 0.0)
d1 = (np.log(S / K) + 0.5 * sig ** 2 * T) / (sig * np.sqrt(T))
return S * norm.cdf(d1) - K * norm.cdf(d1 - sig * np.sqrt(T)) # r=0
def _fee_frac(prem_frac, notional_ratio=1.0, rate=0.0003):
"""Fee Deribit per gamba come frazione di S0: rate*notional cap 12.5% del premio."""
return min(rate * notional_ratio, 0.125 * max(prem_frac, 0.0))
def run_structure(asset, kind, z=2.0, dz=1.0, tenor_d=5, f_put=1.0, f_call=1.0,
gated=False, collect=None):
"""Vendita sistematica non-overlapping della struttura, cadenza = tenor_d.
kind: 'diag' = double diagonal Albimarini (short T entrambi i lati, long T+1g piu' OTM)
'condor' = iron condor STESSA scadenza (controllo del claim '+1 giorno')
'vert' = vertical put credit spread deep-OTM (solo lato put, stessa scadenza)
Strike: K = S0*exp(±z·σT) (z in sigma dell'orizzonte SHORT); ali a z+dz.
gated=True -> gate CANONICO VRP01 (vrp>0 AND ivr>=0.30 AND ivr<=0.90), NON riottimizzato.
Ritorna Series di rendimenti per-periodo su capitale = S0 (spot a entry), indice = scadenza.
collect (dict) accumula diagnostica per la decomposizione diag-vs-condor."""
J = load_series(asset)
px = J["px"].values; dv = J["dvol"].values / 100.0; idx = J.index
n = len(px); T = tenor_d / 365.25
T_long = T + DAY if kind == "diag" else T
has_call = kind in ("diag", "condor")
rets = {}
i = 60
while i + tenor_d < n:
S0 = px[i]; sig = dv[i]
if gated:
rv = _rv30(px, i); ivr = _ivrank(dv, i)
skip = ((not np.isnan(rv) and (sig - rv) <= 0)
or (not np.isnan(ivr) and (ivr < 0.30 or ivr > 0.90)))
if skip:
rets[idx[i + tenor_d]] = 0.0
i += tenor_d
continue
m = sig * np.sqrt(T)
Kp_s = S0 * np.exp(-z * m); Kp_l = S0 * np.exp(-(z + dz) * m)
Kc_s = S0 * np.exp(+z * m); Kc_l = S0 * np.exp(+(z + dz) * m)
# premi a entry (frazione di S0), f per lato
ps = bs_put(S0, Kp_s, T, sig) / S0 * f_put # short put
pl = bs_put(S0, Kp_l, T_long, sig) / S0 * f_put # long put (T o T+1g)
legs = [ps, pl]
cs = cl = 0.0
if has_call:
cs = bs_call(S0, Kc_s, T, sig) / S0 * f_call
cl = bs_call(S0, Kc_l, T_long, sig) / S0 * f_call
legs += [cs, cl]
credit = (ps + cs) - (pl + cl)
# exit a scadenza degli short
j = i + tenor_d
S1 = px[j]; sig1 = dv[j]
short_pay = max(0.0, Kp_s - S1) / S0 + (max(0.0, S1 - Kc_s) / S0 if has_call else 0.0)
if kind == "diag": # long con 1 giorno residuo: mark BS alla DVOL di uscita (vega!)
lp = bs_put(S1, Kp_l, DAY, sig1) / S0 * f_put
lc = bs_call(S1, Kc_l, DAY, sig1) / S0 * f_call
long_val = lp + lc
exit_fee = sum(_fee_frac(v, notional_ratio=S1 / S0) for v in (lp, lc))
else: # stessa scadenza: valore = solo intrinseco
long_val = max(0.0, Kp_l - S1) / S0 + (max(0.0, S1 - Kc_l) / S0 if has_call else 0.0)
exit_fee = 0.0
entry_fee = sum(_fee_frac(v) for v in legs)
# delivery fee su gambe short ITM a scadenza (0.015% cap 12.5%)
deliv = _fee_frac(max(0.0, Kp_s - S1) / S0, rate=0.00015)
if has_call:
deliv += _fee_frac(max(0.0, S1 - Kc_s) / S0, rate=0.00015)
pnl = credit - short_pay + long_val - entry_fee - exit_fee - deliv
rets[idx[j]] = pnl
if collect is not None:
# costo extra del T+1 a entry e valore residuo recuperato a exit (vs intrinseco)
pl_T = bs_put(S0, Kp_l, T, sig) / S0 * f_put
cl_T = (bs_call(S0, Kc_l, T, sig) / S0 * f_call) if has_call else 0.0
intr = max(0.0, Kp_l - S1) / S0 + (max(0.0, S1 - Kc_l) / S0 if has_call else 0.0)
collect.setdefault("extra_cost", []).append((pl + cl) - (pl_T + cl_T))
collect.setdefault("recovered", []).append((long_val - intr) if kind == "diag" else 0.0)
collect.setdefault("short_pay", []).append(short_pay)
collect.setdefault("credit", []).append(credit)
i += tenor_d
return pd.Series(rets)
def book(kind, **kw):
rB = run_structure("BTC", kind, **kw); rE = run_structure("ETH", kind, **kw)
return pd.concat({"B": rB, "E": rE}, axis=1, join="inner").mean(axis=1)
def metrics(r, tenor_d):
"""Metriche su rendimenti per-periodo (cadenza tenor_d). win/PF solo sui periodi ATTIVI."""
r = r.dropna()
ppy = 365.25 / tenor_d
if len(r) < 3 or r.std() == 0:
return dict(sh=0.0, sh_h=0.0, cagr=0.0, dd=0.0, worst=0.0, win=0.0, pf=0.0, act=0.0)
def _sh(x):
return float(x.mean() / x.std() * np.sqrt(ppy)) if len(x) > 2 and x.std() > 0 else 0.0
eq = np.cumprod(1 + r.values); pk = np.maximum.accumulate(eq)
yrs = len(r) / ppy
act = r[r != 0.0]
pos = act[act > 0].sum(); neg = -act[act < 0].sum()
return dict(
sh=_sh(r), sh_h=_sh(r[r.index >= HOLDOUT]),
cagr=float(eq[-1] ** (1 / yrs) - 1) if yrs > 0 and eq[-1] > 0 else -1.0,
dd=float(np.max((pk - eq) / pk)), worst=float(r.min()),
win=float((act > 0).mean()) if len(act) else 0.0,
pf=float(pos / neg) if neg > 0 else float("inf"),
act=float((r != 0.0).mean()), n_act=int(len(act)))
def row(label, r, tenor_d):
mm = metrics(r, tenor_d)
pf = f"{mm['pf']:5.2f}" if np.isfinite(mm["pf"]) else " inf"
print(f" {label:<40} {mm['sh']:>6.2f} {mm['sh_h']:>6.2f} {mm['cagr']*100:>+6.1f}% "
f"{mm['dd']*100:>5.1f}% {mm['worst']*100:>+6.2f}% {pf} {mm['win']*100:>4.0f}% {mm['act']*100:>4.0f}%")
return mm
def to_daily_lumped(wk):
"""Rendimenti per-periodo -> griglia giornaliera con lump alla scadenza (convenzione
_vrp_combo_returns: preserva lo Sharpe annualizzato, niente smoothing)."""
wk = wk.sort_index()
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
return daily
HDR = f" {'struttura':<40} {'ShF':>6} {'ShH':>6} {'CAGR':>7} {'maxDD':>6} {'worst':>7} {'PF':>5} {'win%':>5} {'att%':>5}"
def main():
print("=" * 110)
print(" R0702 ALB-STRUCTURE — double diagonal deep-OTM (Albimarini) vs vertical vs condor vs VRP01")
print(" Capitale = SPOT a entry (S0) per le strutture nuove; VRP01 canonico = strike corto (sua convenzione).")
print(" ⚠️ SKEW: pricing BS FLAT su DVOL-30g; il deep-OTM e' banda-f, non stima puntuale. Term structure ignorata.")
print("=" * 110)
z, dz, tn = CENTRAL["z"], CENTRAL["dz"], CENTRAL["tenor_d"]
for a in ("BTC", "ETH"):
J = load_series(a)
sig = J["dvol"].mean() / 100.0
d5 = sig * np.sqrt(tn / 365.25)
print(f" {a}: DVOL media {sig*100:.0f}% -> 1σ su {tn}g = {d5*100:.1f}% | z=2.0 = {2*d5*100:.1f}% "
f"| z=3.0 = {3*d5*100:.1f}% (il '9% su SPY/6g' ≈ 3.5-4σ equity)")
# ------------------------------------------------------------------ (1) VRP01 canonico
print(f"\n (1) VRP01 CANONICO (riproduzione options_vrp_v2 COMBO: spread -0.28/-0.10 7g, vrp>0+ivr30+cs90)")
print(HDR)
vrp = {}
for f in F_SWEEP:
vrp[f] = pd.concat({"B": vrp_spread_weekly("BTC", defined_risk=True, f=f, gate_vrp=True,
gate_ivr=0.30, crash_skip=0.90),
"E": vrp_spread_weekly("ETH", defined_risk=True, f=f, gate_vrp=True,
gate_ivr=0.30, crash_skip=0.90)},
axis=1, join="inner").mean(axis=1)
row(f"VRP01 gated f={f}", vrp[f], 7)
vrp_nog = pd.concat({"B": vrp_spread_weekly("BTC", defined_risk=True, f=1.0),
"E": vrp_spread_weekly("ETH", defined_risk=True, f=1.0)},
axis=1, join="inner").mean(axis=1)
row("VRP01 NO-gate f=1.0", vrp_nog, 7)
# bridge di validazione del motore: vertical -0.28-equivalente (z~0.58, dz~0.70, 7g) ~ VRP01
zb = float(-norm.ppf(0.28)); dzb = float(-norm.ppf(0.10)) - zb
br = book("vert", z=zb, dz=dzb, tenor_d=7, f_put=1.0, f_call=1.0, gated=True)
row(f"[bridge] vert z={zb:.2f} dz={dzb:.2f} 7g gated", br, 7)
print(" (bridge ~ VRP01 a meno di convenzione strike/capitale: valida il motore nuovo)")
# ------------------------------------------------------------------ (2) tabella principale
print(f"\n (2) STRUTTURE ALLA CELLA CENTRALE A PRIORI (z={z}σ, ali +{dz}σ, tenor {tn}g) — banda f")
scen = [(f, f, f"f={f}") for f in F_SWEEP] + [(F_SKEW["f_put"], F_SKEW["f_call"], "SKEW fp=1.3/fc=0.7")]
streams = {}
for gated, gtag in ((False, "NO-GATE"), (True, "GATE canonico (vrp>0+ivr30+cs90)")):
print(f"\n --- {gtag} ---")
print(HDR)
for kind, ktag in (("diag", "DIAG double-diagonal T+1g"),
("condor", "CONDOR iron condor stessa T"),
("vert", "VERT put spread stessa T")):
for fp, fc, ftag in scen:
r = book(kind, z=z, dz=dz, tenor_d=tn, f_put=fp, f_call=fc, gated=gated)
row(f"{ktag} {ftag}", r, tn)
streams[(kind, gated, ftag)] = r
# distanze alternative (sweep trasparente, selezione SOLO in-sample; f=1.0 gated)
print(f"\n (3) SWEEP DISTANZA/TENOR (gated, f=1.0) — selezione cella SOLO su Sharpe PRE-holdout")
print(f" {'cella':<40} {'ShF-IS':>7} {'ShH':>6} {'DD':>6} {'worst':>7} {'PF':>5} {'win%':>5}")
best = None
for kind in ("diag", "condor", "vert"):
for zz in (1.5, 2.0, 2.5, 3.0):
for tt in (3, 5):
r = book(kind, z=zz, dz=dz, tenor_d=tt, f_put=1.0, f_call=1.0, gated=True)
ris = r[r.index < HOLDOUT]
mm = metrics(r, tt); mi = metrics(ris, tt)
pf = f"{mm['pf']:5.2f}" if np.isfinite(mm["pf"]) else " inf"
act = r[r != 0.0]
nloss = int((act < 0).sum())
tag = " <- video ~3.5σ" if zz == 3.0 and tt == 5 and kind == "diag" else ""
if nloss == 0:
tag += " ⚠️ 0 perdite su tutta la storia = coda MAI campionata (lezione CC01: Sharpe implausibile -> rischio nascosto)"
print(f" {kind:<7} z={zz} dz={dz} tenor={tt}g{'':<14} {mi['sh']:>7.2f} {mm['sh_h']:>6.2f} "
f"{mm['dd']*100:>5.1f}% {mm['worst']*100:>+6.2f}% {pf} {mm['win']*100:>4.0f}% n={len(act):>3}{tag}")
if best is None or mi["sh"] > best[1]:
best = ((kind, zz, tt), mi["sh"], mm["sh_h"])
print(f" -> cella best IN-SAMPLE: {best[0]} (ShF-IS {best[1]:.2f}) | suo hold-out ShH {best[2]:.2f}")
print(" Le celle z>=2.5/5g vendono un evento ~1% mai occorso nel subsample gated (~140 trade):")
print(" Sharpe 'inf/5.9' = premio senza coda osservata, NON edge. E' il punto cieco CC01 in forma opzioni.")
# ------------------------------------------------------------------ (4) claim del video
print(f"\n (4) TEST CLAIM VIDEO: la long a T+1g domina la long a STESSA T? (z={z}, dz={dz}, {tn}g, f=1.0)")
for gated in (False, True):
colD, colC = {}, {}
rD = pd.concat({a[0]: run_structure(a, "diag", z=z, dz=dz, tenor_d=tn, gated=gated, collect=colD)
for a in ("BTC", "ETH")}, axis=1, join="inner").mean(axis=1)
rC = pd.concat({a[0]: run_structure(a, "condor", z=z, dz=dz, tenor_d=tn, gated=gated, collect=colC)
for a in ("BTC", "ETH")}, axis=1, join="inner").mean(axis=1)
mD, mC = metrics(rD, tn), metrics(rC, tn)
ec = np.array(colD["extra_cost"]); rec = np.array(colD["recovered"])
sp = np.array(colD["short_pay"]); crd = np.array(colD["credit"])
crash = sp > np.quantile(sp, 0.95)
gt = "GATE" if gated else "NO-GATE"
print(f" [{gt}] DIAG ShF {mD['sh']:+.2f}/ShH {mD['sh_h']:+.2f} worst {mD['worst']*100:+.2f}% | "
f"CONDOR ShF {mC['sh']:+.2f}/ShH {mC['sh_h']:+.2f} worst {mC['worst']*100:+.2f}%")
print(f" costo extra T+1 a entry: {ec.mean()*1e4:+.1f} bps/trade | residuo recuperato a exit: "
f"{rec.mean()*1e4:+.1f} bps (nei 5% peggiori: {rec[crash].mean()*1e4:+.1f} bps vs extra {ec[crash].mean()*1e4:+.1f})")
print(f" trade a CREDITO netto: {(crd>0).mean()*100:.0f}% (credito medio {crd.mean()*1e4:+.1f} bps di S0)")
dY = per_year(rD); cY = per_year(rC)
print(" Δ(diag-condor) per anno: " + " ".join(f"{y}:{(dY[y]-cY.get(y,0))*100:+.2f}%" for y in sorted(dY)))
# ------------------------------------------------------------------ (5) per-anno
print(f"\n (5) PER-ANNO (gated, f=1.0) — 2022 = LUNA+FTX e' il banco di prova")
for tag, r, tt in (("DIAG", streams[("diag", True, "f=1.0")], tn),
("CONDOR", streams[("condor", True, "f=1.0")], tn),
("VERT", streams[("vert", True, "f=1.0")], tn),
("VRP01", vrp[1.0], 7)):
py = per_year(r)
print(f" {tag:<7} " + " ".join(f"{y}:{v*100:+5.1f}%" for y, v in sorted(py.items())))
# ------------------------------------------------------------------ (6) marginale
print(f"\n (6) MARGINALE vs TP01 e vs VRP01 (daily-lumped; corr su griglia settimanale)")
import altlib as al
tp = al.tp01_baseline_daily()
dv_daily = to_daily_lumped(streams[("diag", True, "f=1.0")])
vr_daily = to_daily_lumped(vrp[1.0])
tp_w = (1 + tp).resample("W").prod() - 1
di_w = (1 + dv_daily).resample("W").prod() - 1
vr_w = (1 + vr_daily).resample("W").prod() - 1
Jw = pd.concat({"tp": tp_w, "di": di_w, "vr": vr_w}, axis=1, join="inner").dropna()
print(f" corr settimanale: DIAG~TP01 {Jw['tp'].corr(Jw['di']):+.2f} | DIAG~VRP01 {Jw['di'].corr(Jw['vr']):+.2f} "
f"| VRP01~TP01 {Jw['tp'].corr(Jw['vr']):+.2f}")
for name, dd in (("DIAG gated f=1.0", dv_daily), ("VRP01 gated f=1.0 (riferimento)", vr_daily)):
mv = al.marginal_vs_tp01(dd)
print(f" marginal_vs_tp01[{name}]: verdict={mv.get('marginal_verdict')} corr={mv.get('corr_full')} "
f"uplift w25 full/hold={mv['blends']['w25']['uplift_full']}/{mv['blends']['w25']['uplift_hold']} "
f"IS-Sh={mv.get('cand_insample_sharpe')} insample_edge={mv.get('has_insample_edge')} "
f"hedge={mv.get('is_hedge')} robust_oos={mv.get('robust_oos')} multicut={mv.get('multicut_uplift')}")
print(" ⚠️ Un verdetto ADDS qui NON promuove: lo stream vende coda che nel subsample gated non ha mai")
print(" colpito (hold-out Sh 3+ = assenza di eventi, non alpha) — vale la lezione CC01, e vale la regola")
print(" standing 'niente short-vol da modello in deploy'.")
# ------------------------------------------------------------------ (7) eseguibilita'
print(f"\n (7) ESEGUIBILITA' DERIBIT (min 0.1 BTC / 1 ETH per gamba; diag = 4 gambe/asset, book = 8)")
for a, minc in (("BTC", 0.1), ("ETH", 1.0)):
J = load_series(a); S = float(J["px"].iloc[-1]); sig = float(J["dvol"].iloc[-1]) / 100.0
w = dz * sig * np.sqrt(tn / 365.25) # larghezza ala in frazione di S
notional = minc * S
maxloss = w * notional # margine ~ max loss defined-risk (per lato)
col = {}
run_structure(a, "diag", z=z, dz=dz, tenor_d=tn, gated=True, collect=col)
cbps = np.mean(col["credit"]) * 1e4 if col.get("credit") else float("nan")
print(f" {a}: spot ~${S:,.0f} -> notional min {minc} = ${notional:,.0f}/gamba | ala {w*100:.1f}% "
f"-> margine/max-loss min ~${maxloss:,.0f} | credito tipico {cbps:+.0f} bps = ${notional*cbps/1e4:,.0f}/trade")
print(" -> a $600: UN diagonale BTC min-size impegna >50% del capitale su un trade 5g = NON eseguibile.")
print(" Scala minima: sleeve opzioni al ~12% con margine <= peso richiede >~$3-5k per il solo BTC")
print(" min-size; book 50/50 con granularita' (>=3-5 step di size) ~= $15-25k. STAT-MODE, come VRP01.")
print("\n NB ONESTO: win-rate alto e' STRUTTURALE nel deep-OTM (vendi eventi rari), non e' edge. Il verdetto")
print(" sta in Sharpe/PF/coda attraverso la banda f e il 2022. Regola standing: niente short-vol da modello")
print(" in deploy — esito massimo = aggiornamento di conoscenza sulla STRUTTURA.")
if __name__ == "__main__":
main()

Some files were not shown because too many files have changed in this diff Show More