Compare commits

12 Commits

Author SHA1 Message Date
Adriano Dal Pastro 05d069f336 chore(cron): cabla i monitor read-only decisi alla merge — cc01_regime_watch + snapshot stablecoin in cron_daily; snapshot opzioni 0DTE 2x/g (07:55+08:05 UTC)
Crontab utente aggiornato (2 righe nuove per cron_opt_snapshot.sh). Tutti read-only:
nessun ordine, book live invariato. Smoke-test OK (0DTE: 160 righe; CC01: QUIET
BTC +8.4%/ETH +7.5% ann. 30g).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 23:19:47 +00:00
Adriano Dal Pastro 636be89b2b research(live): follow-up SKH chiuso — peso 0.25 e cron orario CONFERMATI de-luckati sul path live; + snapshot point-in-time stablecoin (sblocco WATCH a 12 mesi)
- r0724_skh_live_weight.py: sweep w 0-0.50 sul path hourly x 23 offset; w*=0.25
  (argmax mediana-IS di banda, plateau 0.20-0.30); w=0.30 passa weights_tilt_null
  solo a off0 (ancora fortunata), fallisce a offset mediano -> INVARIATO.
  Cadenza 230m = rumore (+0.01/+0.02 Sh med); il degrado live e' il fill-al-livello
  (~+0.35 Sh) che nessun cron recupera. Book e cron INVARIATI.
- r0724_stable_snapshot.py: cattura giornaliera point-in-time supply stablecoin
  (DefiLlama, tokenless, idempotente) -> data/external/stable_snapshots/ (gitignored).
  Criterio di rivisita del lead STABLE: >=12 mesi di serie propria.
- CLAUDE.md: bullet SKH01 aggiornato (follow-up chiuso).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 23:08:23 +00:00
Adriano Dal Pastro 00996640fb research(cases): 3 agenti live sui 3 case — PM<->Deribit SKIP (gap=spec non alpha), HLP SKIP/WATCH (carry ex-evento 0.2%/a), 0DTE capture avviata
PM: 34 binarie riconciliate vs daily options; il gap 11pp si decompone in basis USDT
(+4-5pp ATM), tempo 8h (-+5pp) e replica su book morti; residuo tail 2-3pp sotto costi;
trade coperto reale: lock /bin/bash.1-5, P(conflitto gambe) 2-22%, margine SM domina -> SKIP.
HLP: serie daily trovata (wHLP): Sharpe 0.69 non ~2, +12% 2026 = 1 evento, ex-evento
+0.2%/a, coda JELLY = inventario ereditato troncato da voto discrezionale, Kelly f*=0
-> SKIP/WATCH con trigger meccanici. 0DTE: fee cap binding = drag 2-3x weekly, IV<RV
al front stanotte; snapshot pipeline scritta e primo capture su disco; decisione
pre-registrata a 90g di serie. Book INVARIATO.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 22:46:59 +00:00
Adriano Dal Pastro 4c1961d77b research(hunt-web): sintesi caccia strategie via web — 10 famiglie nuove 0 slot; lead-lag JEDC = reversal travestito (rank-corr -1.000), MAX DSR 0.41
3 agenti web (~50 fonti) + 3 ondate empiriche in una notte. Convergenza indipendente
ricerca<->empirica: il factor zoo crypto si riduce a liquidity/momentum (gia' nel book).
XS su HL: lead-lag all-others e' matematicamente l'inverso del momentum proprio (check
di degenerazione da riusare); MAX segno instabile L7<->L30. HLP probe: crash-long reale
(corr book -0.19, +10.4% nel crash ott-25) ma decay 79->12%/anno + coda JELLY fuori
serie -> ALLOCATION-WATCH. Pista nuova prossima ondata: Polymarket<->Deribit (gap 11pp,
arXiv 2606.19517, spec-mismatch da riconciliare prima di credere). Book INVARIATO.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 22:24:57 +00:00
Adriano Dal Pastro b65f067f69 research(premium): ondata premi cross-venue auto-calcolati — CBPREM morto, KIMCHI d30 ucciso dallo scettico (spike+lag)
CBPREM (Coinbase vs feed cert, 2015->): HEDGE, hold -0.46. KIMCHI (Upbit/ECB, 2017->,
ancora 00:00 UTC verificata): EARNS_SLOT=True al marginal scorer (ADDS, robust_oos,
uplift + ogni anno) ma DSR 0.891 -> scettico obbligatorio: niente plateau
(d15/30/45/60 = 0.44/1.07/0.51/0.24) e lag +1g azzera (hold 0.74->0.14) = parameter
luck. Lezione codificata: EARNS_SLOT con DSR<0.95 -> sempre plateau+lag skeptic.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 22:23:16 +00:00
Adriano Dal Pastro 2320fa9cec research(onchain): prima ondata on-chain/sentiment — 0/6 slot; l'on-chain tradabile e' prezzo travestito (corr TP01 0.5-0.8)
6 famiglie mai testate (NET Liu-Tsyvinski, MVRV, exchange-supply, hash ribbons, F&G,
stablecoin-supply-growth) su segnali CoinMetrics community + alternative.me + DefiLlama,
ritorni SOLO dal feed certificato, study_family_honest su 32 celle. EXS hold-out -0.58
(claim outflow=bullish decaduto), HASH=HEDGE, FNG corr 0.82 (trend travestito), MVRV
DILUTES. Unico lead: STABLE thr=10% (DSR 0.998, ADDS persistente) ma robust_oos=False
+ caveat VINTAGE (storia DefiLlama ricostruita) -> WATCH, no paper. Regola nuova:
classificare il rischio-vintage di ogni fonte esterna prima del backtest.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 22:15:05 +00:00
Adriano Dal Pastro 5c48a8322a research(goal50): MC prop INTRADAY wick-aware — leva eval 1.5x refutata, funded ottimo a 0.75x, P(perdere fee) ~56%
Recon MTM 1h del book Deribit (TP01 pesi daily + SKH01 230m exit-al-livello SL-first,
fee incluse). Validata: leg TP corr 0.9997 / log-total +0.833 vs 0.827; leg SKH +0.548
vs +0.513; Sharpe MTM 1.62 vs 1.77 sleeve (delta = attribuzione a scalino di SKH).
I wick tagliano 6-37pp di P(pass): de-luck 1.0x HYRO 44.7% / FTMO 59.9% / Breakout 26.6%
(declassata). Funded sweep: leva ottima 0.75x (HYRO P(vivo) 58% vs 20.5% a 1x), atteso
~EUR 14.5/g a 100k. EV biglietto resta positivo ma sottile (de-luck +491/+1048/+2197 USD).
Bug tz-naive/aware nel reindex catturato dalla sanity (TP01 spariva, corr 0.43).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 21:55:23 +00:00
Adriano Dal Pastro 2e7178487d research(goal50): R2 prop-firm + roadmap finale — 50eur/g in mesi NON esiste via legale; stack onesto 18-36 mesi
- MC aggiornato alle regole VERE (HYRO 10/6/4, BREAKOUT 10/6/3, FTMO 10/10/5):
  de-luck P(pass) 55-68% a leva 1-1.5x, mediana 68-137g; funded 100k de-luck
  P(vivo) 69%, atteso blended onesto 250-450 eur/MESE (attriti fuori-MC: carry,
  recycling bust, controparte non regolata, cap industria 200k).
- Roadmap: prop = unico canale che cambia scala in mesi (8-15 eur/g);
  stack 24 mesi top-decile 30-60 eur/g, mediana 10-25; fisco 33% = muro x1.5.
- Book live INVARIATO.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 21:02:45 +00:00
Adriano Dal Pastro acfba63420 fix(test): test_paper_advance bug latente — factor divideva via (1+tail[0]), passava solo con combo[-500]==0
Pre-esistente su main (fallisce anche li'), emerso con l'avanzare dei dati.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 20:58:52 +00:00
Adriano Dal Pastro 870428e559 docs(goal50): diario R3 — monetizzazione track record: 0 in mesi, stack legale 18-36 mesi (Darwinex/eToro/OKX EU); family&friends remunerato = art.166 TUF
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 20:57:48 +00:00
Adriano Dal Pastro 7ebb1b8c37 feat(goal50): CC01 regime watch (funding 30g HL, trigger 10/15% ann.) + diario R1 yield/basis
R1 (web): carry al fondo del ciclo (BTC 0-4%, ETH negativo), Aave 4-5% = pavimento,
0k -> 2.5-4 eur/g; MiCA lascia a IT esattamente Deribit+HL; fisco 33% dal 2026
(muro capitale x1.5). Watcher read-only, oggi: BTC +8.4% / ETH +7.4% 30g = QUIET.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 20:57:13 +00:00
Adriano Dal Pastro ddc980a097 research(goal50): matematica onesta del target 50eur/g + gate deploy pre-registrato statarb
- r0724_goal50_math: muro capitale (73-228k), leva=rovina, Monte Carlo prop-firm
  sui ritorni reali del book (P(pass) 56-86%, funded 100k mediana 31-42eur/g),
  vault TVL 1-2M. Unica via 'da mesi' = moltiplicare nozionale gestito.
- r0724_statarb_deploy_gate: regola di deploy PRE-REGISTRATA a fwd-day 25
  (decisione 2026-09-27, Sh>=0.5, haircut<0.5pp, 1 proroga max) — anti
  selection-on-forward.
- diario roadmap (sezioni web-research da completare).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 20:55:23 +00:00
23 changed files with 3534 additions and 4 deletions
+4
View File
@@ -70,3 +70,7 @@ data/paper_statarb/
# log esecuzioni del book live (stato runtime, contiene fill/fee del conto reale)
data/live/
# dati esterni di ricerca (on-chain CoinMetrics community, F&G) — non certificati, non in git
data/external/
data/options_daily/
+6 -1
View File
@@ -106,7 +106,12 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis
(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.
INVARIATI (ogni cambio passa weights_tilt_null). **Follow-up CHIUSO 2026-07-24**
(`scripts/research/r0724_skh_live_weight.py`): sul path live (lente hourly, 23 offset) il peso
ottimale de-luckato È 0.25 (argmax mediana-IS di banda, plateau 0.20-0.30; w=0.30 passa il gate
solo a off0 = ancora fortunata, fallisce a offset mediano) e la cadenza 230m vale ~+0.01/+0.02 Sh
mediano (rumore: il degrado live è il fill-al-livello, ~+0.35 Sh, che nessun cron recupera) →
**book 75/25 e cron orario CONFERMATI**; diario `2026-07-24-skh-live-weight.md`.
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
@@ -0,0 +1,76 @@
# 2026-07-24 — Dossier 0DTE: economia LIVE delle daily options Deribit (BTC/ETH)
**Goal.** Quantificare stanotte, da quote pubbliche LIVE Deribit, l'economia del premio a
scadenza DAILY (0-1DTE) — il dossier 0DTE parcheggiato. VRP01 resta deploy-bloccato dalla regola
"niente short-vol da modello"; le daily (365 expiry/anno) accumulerebbero una serie di premi
REALI 7x più in fretta delle weekly. Domanda: il tenor daily è quotato/eseguibile/conveniente,
o muore di fee (lezione Albimarini)?
**Misura (3 snapshot ~22:35 UTC, ven 24 lug):** catena 0DTE (settle 25JUL 08:00, DTE 9.4h),
daily "fresca" (26JUL, DTE 33.4h), weekly (31JUL, DTE 153h). Fee verificate: taker=maker
0.0003 base/contratto, **cap 12.5% del premio**, daily ESENTI da delivery fee. Min size:
BTC 0.1 contratto, ETH 1.0. Spot 64.1k / 1859.
## Numeri chiave (put credit spread -0.28d/-0.10d, prezzi al tocco, fee taker 2 gambe)
| tenor | credito lordo | cross-spread (vs mid) | fee (% credito) | NETTO (% width) | %width/giorno |
|---|---|---|---|---|---|
| BTC 9.4h | $38.5 | 25% | 25.0% | 5.8% | — |
| BTC 33h | $96.2 (0.1 lot: $9.6) | 12% | 20.8% | 7.6% | 5.5%/g |
| BTC weekly | $224.6 | 16% | 17.1% | 9.3% | 1.5%/g |
| ETH 9.4h | $1.12 | 29% | 29.2% | 3.2% | — |
| ETH 33h | $3.53 | 14% | 23.0% | 10.9% | 7.8%/g |
| ETH weekly | $13.39 | 8% | **8.3%** | 12.3% | 1.9%/g |
- **Il cap 12.5% è quasi sempre BINDING a tenor daily** (premi < 0.0024 base) → fee ≈ 10-12.5%
del premio PER GAMBA, sempre. A weekly la fissa 0.0003 = 3-5% del premio. Il drag fee del
daily è **2-3x** quello weekly per unità di premio: la legge di scala Albimarini vista live
(premio ~ √T, fee ~ costante).
- **Haircut totale mid→netto**: daily 33h ≈ 30-35% del credito mid; 0DTE 9.4h ≈ 45-50%;
weekly ETH ≈ 16%. Richezza di pareggio a tenor daily ≈ **IV/RV ~1.4x**; a weekly ~1.1x.
- **IV vs RV stanotte**: ATM daily mark_iv BTC **12.8%** / ETH **21.5%** ann. contro RV20
29.6% / 39.8% → **IV_daily/RV20 = 0.43x / 0.54x: il premio daily era SOTTO la realized**
(VRP negativo al front). Weekly: 33.9% / 44.9% = 1.13-1.14x RV (il VRP vive lì, coerente
con VRP01). DVOL-30d 37.0 / 50.2. Caveat onesto: 1 snapshot, finestra weekend (ven→dom) =
IV bassa in parte razionale; NON generalizzabile — esattamente il perché della cattura.
- **EV check ETH 33h**: credito netto $2.72 < fair-value mid ~$4.1 e < E[payout] alla STESSA
IV implicita (~$4.9) → EV negativo *anche al pricing del mercato*, prima di ogni view di vol.
- **Quote sulle ali**: sì, 2-sided su tutto il ladder anche a 9h; depth MM ETH 400-1150
contratti al tocco, BTC più sottile (0.4-49). MA la griglia strike daily è GROSSA (step
1.3% ETH): i delta -0.28/-0.10 non esistono, si salta da -0.36 a -0.12; smile ripidissimo
(vendi bid_iv 13-20, compri ask_iv 22-38); le ali <5d hanno spread mediano 200% del mid =
di fatto non prezzate. Tick 0.0001 base = 3-10% del premio daily: anche la granularità quota.
## Margine = la realtà che uccide la taglia
Default Deribit = **standard margin SENZA netting fra gambe** (verificato docs): lo spread
"defined-risk" è margginato come naked short + long pagata intera. Il netting (margine ≈ width)
richiede Portfolio Margin, **min equity ~0.5 BTC ≈ $32k**. Quindi:
- ETH 1-lot PCS daily: margine SM ~$276, credito netto $2.72, max loss ~$22 → *meccanicamente*
eseguibile a $600 (1-2 lot), rendimento su margine ~1%/g SE zero perdite (e l'EV è negativo).
- BTC 0.1-lot: margine SM ~$963 → FUORI a $600, dentro a $2.6k.
- La soglia "$2.6k ETH" vale per strutture long/covered; per credit spread le soglie oneste
sono: $600 = ETH 1-lot (SM), $2.6k = entrambi sottili, **$32k = defined-risk vero (PM)**.
## Pipeline di cattura — FATTA (non in cron)
`scripts/research/r0724_daily_opt_snapshot.py`: pubblico/read-only, appende 1 riga JSON per
strumento (+1 meta con spot e DVOL) a `data/options_daily/snapshots.jsonl`; scadenze ≤50h,
moneyness 75-125%, ~160 righe/run, ~40 MB/anno a 2 run/g. Cadenza raccomandata quando la si
cabla: **08:05 UTC** (daily appena listata, ~24h DTE = premio vendibile) + **07:55 UTC**
(pre-settle = chiude il ciclo payoff). Primo snapshot già nel file. Esisteva solo
`options_real_quote_check.py` (weekly, one-shot, dipende da EXT path) e `fetch_dvol.py` (IV
30d, non quote): nessuna cattura daily preesistente.
## Verdetto: **CAPTURE-AND-WAIT con prior negativo sul daily**
Il tenor daily NON è morto-di-quotazione (mercato 2-sided, depth reale) ma stanotte era
**morto-di-economia**: haircut fee+spread 2-3x il weekly E IV<RV al front → servono entrambe
le gambe della condizione (IV/RV>~1.4x al daily) che oggi non c'è. Nessun deploy, nessun
cambio a book/pesi. Regola di decisione futura: dopo **≥90 giorni di cattura 2x/g**, calcolare
mediana(credito_netto/fair_mid) e mediana(IV_daily/RV_forward); il dossier si riapre SOLO se
il VRP daily mediano netto-haircut > 0 (e poi passa i gate standard). Il f di stress reale
arriva gratis se un crash cade nella finestra di cattura — lo stesso prerequisito che sblocca
VRP01. Fino ad allora: le daily servono da **strumento di misura**, non da strategia.
Script: `scripts/research/r0724_daily_opt_snapshot.py`. Dati: `data/options_daily/snapshots.jsonl`.
+292
View File
@@ -0,0 +1,292 @@
# 2026-07-24 — Goal "50 EUR/giorno in qualche mese": analisi onesta e roadmap
**Contesto.** Goal dichiarato: rendita aggiuntiva che arrivi a **50 EUR/giorno entro qualche
mese**, senza precludere alcuna modalità. Branch `goal/50eur-day`. Questo diario è la sintesi
di: matematica del target sui ritorni reali del book, Monte Carlo prop-firm, 3 ricerche web
parallele (prop firm 2026, yield/basis a piccola scala, monetizzazione del track record),
e audit delle migliorie interne possibili.
**Verdetto in una riga:** 50 EUR/g = **EUR 18.250/anno**; ai CAGR onesti del book (10-20%)
servono **EUR 73k-183k di capitale**. Da $600 in "qualche mese" per via di trading sul
capitale proprio **non esiste** — l'unica famiglia di vie compatibile con "mesi" è
**moltiplicare il nozionale gestito** (conto funded / capitale di terzi), ognuna con
probabilità e rischi quantificati sotto.
---
## A) Il muro del capitale (script `r0724_goal50_math.py`, parte A)
| CAGR netto | capitale per 50 EUR/g |
|---|---|
| 8% | ~EUR 228k |
| 10% | ~EUR 182k |
| 15% | ~EUR 122k |
| 20% | ~EUR 91k |
| 25% | ~EUR 73k |
Tempo da $600 (CAGR 15%, compounding + depositi mensili): **0/mese → 38.7 anni; 500/mese →
9.6 anni; 1.000/mese → 6.3 anni; 2.000/mese → 3.8 anni; 5.000/mese → 1.8 anni.**
## B) La leva non è la scorciatoia (parte B)
Book Deribit live (TP01 .75 + SKH01 .25, modellato): Sharpe 1.77, **vol nativa ~11%**,
CAGR 20.5%, maxDD 9.4%. Il target su capitale piccolo implica: $600 → 3.315%/anno
(leva ~160x); $5k → 398%/anno (~19x, vol ~212%); $20k → 99%/anno (~4.8x, vol ~53%).
Tutte fuori dalla zona di sopravvivenza (Kelly documentato: il book gira a ~0.01-0.27 Kelly).
La frontiera 2026-07-03 ha già stabilito che il book gira alla sua vol nativa — alzare
`target_vol` scala DD in proporzione, non crea rendimento.
## C) Prop-firm / conto funded — l'unica via "da mesi" quantificabile (parte C)
Monte Carlo (block bootstrap 20g dei ritorni giornalieri reali del book, 20k path,
regole tipiche: target 8-10%, maxDD 10% statico/trailing, daily loss 4-5%):
| scenario | leva eval | P(pass entro 1a) | mediana giorni |
|---|---|---|---|
| book modellato | 1.0x | 81-86% | 111-132 g |
| book modellato | 1.5x | 78-85% | **72-89 g** |
| book modellato | 2.0x | 72-81% | 46-62 g |
| de-luck ×0.6 | 1.0x | 63-72% | 113-137 g |
| de-luck ×0.6 | 1.5x | 62-71% | **70-87 g** |
| de-luck ×0.6 | 2.0x | 56-65% | 43-58 g |
Fase funded ($100k nozionale, split 80%, 1 anno): **modellato** P(conto vivo) 88.9%,
payout p10/p50/p90 = EUR 8.9/42.3/93.4 al giorno; **de-luck ×0.6** P(vivo) 67.7%,
p10/p50/p90 = EUR 1.3/**31.1**/77.9 al giorno. A $200k nozionale i payout raddoppiano
(mediana de-luck ~EUR 62/g) — il target dei 50 EUR/g sta tra $150k e $200k funded.
**Caveat onesti:** (1) daily-loss simulato su barre daily → i wick intraday rendono le
P(pass) OTTIMISTE; (2) le regole reali (consistency, min trading days, divieto bot/API,
weekend) possono uccidere lo stile low-frequency → vedi ricerca firm 2026 sotto;
(3) payout = reddito VOLATILE, non "rendita" (p10 de-luck ≈ EUR 1/g).
## D) Vault / copy-trading (parte D)
A perf-share 10% sopra HWM servono **$1-2M di TVL altrui** per 50 EUR/g → richiede anni
di track record pubblico, non mesi. Vincolo legale per residente italiano (gestione di
capitale altrui = art. 166 TUF) approfondito nella ricerca dedicata sotto.
---
## Migliorie interne implementate/decise oggi
1. **Gate di deploy PRE-REGISTRATO per STATARB-RESID** (`r0724_statarb_deploy_gate.py`).
Forward a oggi: 25 barre, +4.42%, Sharpe fwd +5.84 (caldissimo → regredirà), DD 1.4%,
haircut 0.01pp. Regola fissata OGGI (day 25) per evitare selection-on-forward:
**decisione il 2026-09-27 (90g)**; Sh≥0.5 → candidato deploy (pesi {10,15,20}% via
`weights_tilt_null`, maxDD combinato <15%); 0≤Sh<0.5 → UNA proroga di 90g; Sh<0 → ritiro.
Guardie: haircut <0.5pp, config congelata (W=45, sgn=+1), DD fwd <10%.
2. **Audit infra:** i messaggi "open/completed orders request timed out" nel cron daily sono
warning benigni di `ib_async` al connect col Gateway IB (conto paper DU) — cosmetici,
nessuna azione. Book live 743/743 giri completati; errori cerbero-mcp solo storici.
3. **Conferme di sistema (non ri-litigate):** soffitto direzionale ~1.3 (10+ ondate);
book già quasi-risk-parity; alzare vol = de-levering inverso (frontiera 2026-07-03);
il vincolo binding è il CAPITALE, non la ricerca. Il cap dinamico equity/2 è già
pronto ad assorbire depositi.
## Ricerche web (3 agenti paralleli) — RISULTATI
### R1 — Yield / basis / carry a $600-20k (luglio 2026): NIENTE di rilevante ora
- **Il carry è al fondo del ciclo, CC01 confermato live**: funding BTC annualizzato
+70% (gen 2026, euforia) → **3.4%** (mag) → **+4.2%** (metà lug); spot 22-24 lug ≈ 0.
**ETH funding NEGATIVO** (flip 3-4 giu). CME basis 2.35% dic 2025 (peggior
backwardation post-FTX). Carry netto BTC oggi: **0-2%/anno < Aave**, con più tail risk.
- **Menu onesto**: Aave v3 USDC **4-5%** = pavimento income reale; sUSDe 4-7% in
compressione (= il premio CC01 tokenizzato, riserva 1.18% del TVL, TVL fuggito
$14B→$5.9B, DeFi-only per EEA); **HLP ~15-25%** ma è inventory di market-making
travestito da yield (tail plausibile 20/50%, cooldown sui prelievi; nel crash
ott 2025 fece +10% in un weekend — profitta nei cascade, sanguina nei trend).
- **Tier di capitale**: $600 → ~€0.07/g; $5k → ~€0.6-0.8/g; $20k → **€2.5-4/g**
(aggressivo HLP-heavy ~€5-6 con la sua coda). Nessuno step-change tra 5k e 20k;
quelli veri sono ~$20k (basis multi-leg eseguibile QUANDO il funding torna) e $100k+
(fee tier). 50 EUR/g a 5-8% onesto = **€250-350k** — peggio del muro trading.
- **Regola operativa cablabile**: CC01 resta LEAD **regime-triggered** — riaprire il
dossier solo con **funding medio 1-mese > 10-15% annualizzato sostenuto** (la serie
funding HL ce l'abbiamo già). Gli spike stile gen-2026 (70% ann., spariti in
settimane) NON sono income raccoglibile.
- **MiCA day-one 2026-07-01 — mappa venue per residenti IT**: **Binance OUT** (no
licenza, restrizioni dal 1/7); Bybit globale bloccato EEA, bybit.eu senza perp;
**Deribit OK** (Coinbase ha licenza MiCA Luxembourg) ma perp-EU-retail in zona grigia
MiFID-II da monitorare; **Hyperliquid ancora accessibile** senza KYC ma è il
test-case del perimetro EU → rischio geoblock futuro reale. In pratica: i nostri
due venue sono esattamente ciò che resta.
- ⚠️ **FISCO ITALIA 2026**: capital gain crypto **33% dal 1/1/2026** (L.199/2025),
esenzione €2.000 ABOLITA, IVAFE 0.2%, DAC8 auto-reporting, crypto nell'ISEE.
→ ogni numero di questo diario è LORDO: **50 EUR/g netti ≈ 75 EUR/g lordi**, il
muro di capitale sale di ~1.5x (~EUR 180k a CAGR 15%). Da verificare col
commercialista il trattamento di yield/staking.
_(FX: i rendimenti sono USD-denominati; ~2pp del 4.5% stable è solo il differenziale
tassi USD-EUR — hedged in EUR il "4.5%" reale è ~2.5%.)_
### R2 — Prop firm crypto 2026: via CREDIBILE ma payoff modesto — €8-15/g a $100k, cap $200k
- **Vincolo vero = geometria del drawdown**: le eval crypto-native danno **DD max 3-6%
statico** (non 10%) contro il maxDD storico 9.4% del book → si gira a 0.5-1.0x vol.
FTMO Swing è l'unica con DD 10% ma è crypto-via-CFD (swap ~10-25%/anno).
- **Ranking per QUESTO stile** (verificato su fonti primarie, non affiliate-SEO):
1. **HyroTrader** 1-step $100k ($579): unica con **API reale** (sub-account Bybit) da
funded, funding = mercato (stessa economia del book Deribit), no time limit, weekend
ok, consistency 40% solo in eval, fee rimborsata al pass, payout ~12h. Cap $200k.
2. **Breakout/Kraken Prop** ($800): miglior reputazione (Trustpilot 4.8, Kraken-backed),
regole pulite, MA **carry nascosto 0.033%/giorno ≈ 12%/anno sul nozionale** = ostile
ai multi-week hold; niente API supportata (a cadenza 1d l'esecuzione manuale è ~5
min/g, non-blocker). Evitare 2-step (DD trailing = veleno per un trend book) e Turbo (DD 3%).
3. **FTMO Swing** (~€540): controparte più solvibile del settore ($450M+ pagati),
DD 10%, EA propri su MT5; ma crypto non è il suo core.
- **Crypto Fund Trader: EVITARE** (Trustpilot soppresso per manipolazione, pattern di
payout negati, SL obbligatorio che rompe la meccanica TSMOM).
- **Monte Carlo aggiornato alle regole VERE** (parte C di `r0724_goal50_math.py`, bootstrap
dei ritorni reali del book): de-luck ×0.6 a leva 1.0-1.5x → **P(pass) 55-68%, mediana
68-137 giorni**; funded $100k HYRO de-luck → P(vivo 1a) 69%, payout mediano €31/g SE
vivo. La MC dell'agente (Sharpe 0.5-1.0, code t(4)) converge: P(pass) 40-72%.
⚠️ **SUPERATO da C-bis (MC intraday, sera stessa)**: questi numeri sono close-only e
OTTIMISTI — coi wick intraday P(pass) de-luck ~44% a 1.0x, funded a 1.0x P(vivo) crolla
a 20.5% (leva ottima funded = 0.75x). Vedi sezione C-bis sotto.
- **Attriti fuori dal MC** (che tagliano l'atteso): carry/swap, recycling dei bust
(~40-55%/anno di P(bust) al limite 6%), consistency in eval, **controparte non regolata**
(il settore ha perso il 13% delle firm nel solo 2024, $50M+ di payout stranded; profitti
non ritirati = credito chirografario) → **atteso blended onesto: €250-450/MESE a $100k
funded; €15-30/g al cap $200k nei buoni scenari.** Consob/ESMA stanno tirando il
modello verso MiFID II → rischio regolatorio EU 2026-27.
- **Disciplina se si apre il fronte**: entry sensata = $25k ($249) o $50k ($379) come
test economico che può autofinanziare il tentativo $100k; prelievi SETTIMANALI
(mai lasciare credito alla firm); è un side-bet EV+ ~10-50x il rendimento della stessa
strategia sui nostri $600 — **non** la soluzione del capitale.
### R3 — Monetizzazione track record / vault / legale IT: NIENTE in mesi, stack lento legale in 18-36 mesi
- **Aritmetica sovrana**: ogni canale profit-share paga ~10-15% dei profitti generati sul
capitale altrui → a CAGR onesto 15-20%, 50 EUR/g richiede **€600k-1.2M di capitale
follower**. Nessun meccanismo sposta quel capitale su un track record di mesi con $600
di skin-in-the-game.
- **Hyperliquid vault: MORTO a $600** — creazione ora costa **$10k** + leader deve tenere
≥5% del vault; TVL mediano dei vault utente ≈ $0 (solo ~$35M su 2.400 vault non-HLP);
la discovery è ordinata per APR → un 15-20% difensivo è carta da parati.
- **Copy trading post-MiCA (1/7/2026)**: Binance/Bybit/Bitget CHIUSI per residenti EEA.
Restano: **OKX EU** (Malta MiCA+MiFID II, lead trader legale con KYC, 8-13% profit
share) e **eToro Popular Investor** (Champion: $400-800/mese FISSI = €13-26/g, ma
servono $50k di copier AUM + $5k equity propri + 6 mesi risk-score ≤7 → 12-24 mesi).
- **Darwinex Zero = miglior fit strutturale**: l'engine normalizza tutti alla stessa vol
**l'allocazione si vince con lo SHARPE, non col CAGR flashy** — l'unico canale dove
la virtù del book è la metrica classificata. FCA-regolato, 15% perf fee sopra HWM,
~$35-50/mese di costo. Primi fee piccoli a 3-6 mesi, >€10/g a 18-36 mesi con
qualche centinaio di k€ di allocazione cumulata. Caveat: crypto solo via MT5 CFD →
conto parallelo, non monetizza direttamente il book Deribit.
- **Collective2**: mediana ≈ $0 lifetime, C2 trattiene ~50%, il retail compra emozioni
non Sharpe difensivi → non vale il listing fee.
- ⚠️ **LEGALE (pattern regolatorio, non parere)**: gestione remunerata di conti di
amici/famiglia = **art. 166 TUF, 1-8 anni** (+ D.Lgs. 129/2024 art. 30 per i
crypto-asset, 6m-4a) — è l'unico canale "veloce" ed è esattamente quello penale;
la denuncia arriva con le perdite. Safe harbor = lead trader su venue licenziata
(la licenza è della piattaforma). Newsletter GENERICA con disclaimer = lecita;
personalizzazione/auto-esecuzione/soldi in mano = attività riservata.
- **Stack realistico a 24 mesi** (Darwinex + eToro/OKX): top-decile **€15-45/g**,
mediana €5-15/g. In "qualche mese": ~€0. Il vincolo non è la strategia, è il
**trust-capital** — e si accumula sullo stesso orologio del capitale.
### C-bis — MC INTRADAY wick-aware (addendum serale): chiuso il caveat n.1 — i wick
### tagliano 6-37pp di P(pass) e RIBALTANO la config consigliata
Script `scripts/research/r0724_goal50_intraday_mc.py`. Il MC di parte C applicava le
regole alle CHIUSURE daily; le regole vere scattano sull'equity intraday. Qui l'equity
del book è ricostruita MTM a 1h sul feed certificato: TP01 = pesi daily (fee di
ribilanciamento incluse), SKH01 = trade 230m con exit AL LIVELLO SL/TP nell'ora del
trigger (SL prioritario, stessa convenzione di `backtest_signals`), wick per-barra
firmati col segno del peso.
**Validazione della ricostruzione** (obbligatoria prima di credere ai numeri):
leg TP01 corr daily 0.9997 e log-total +0.833 vs sleeve×0.75 +0.827 (esatto); leg SKH
log-total +0.548 vs sleeve×0.25 +0.513 (Δ≈0.5%/anno, bordi orari); book Sharpe MTM 1.62
vs 1.77 dello sleeve — il delta è l'ATTRIBUZIONE: lo sleeve SKH è equity-a-scalino
per-trade (P&L intero sulla barra d'ingresso), mentre un conto prop è marcato MTM →
la lens MTM è quella giusta per questo uso. ⚠️ Bug catturato dalla sanity al primo run:
`reindex` con indice tz-naive contro tz-aware → NaN→0 SILENZIOSI, TP01 sparito dal recon
(corr 0.43) — ennesima conferma della famiglia di lezioni pandas-tz del progetto.
**Geometria dei wick del book**: gap (min intraday chiusura) p50 0.17pp, p10 1.03pp,
p1 2.7pp, worst 6pp; 2.6% dei giorni con wick oltre 2pp. Piccolo in mediana, ma le
regole prop sono sensibili proprio alla coda.
**Eval (de-luck ×0.6, P(pass) close→intraday):** a leva 1.0x HYRO 58.8%→**44.7%**,
BREAKOUT 57.1%→**26.6%**, FTMO Swing 65.7%→**59.9%**. Tre conseguenze:
1. **La leva 1.5x consigliata dal MC close-only è REFUTATA**: intraday fa PEGGIO di 1.0x
(HYRO 36.0% vs 44.7%) — i wick amplificati bruciano il daily-loss prima che il drift
paghi. Config onesta: **eval a leva 1.0x**, mediana pass ~99g (HYRO) / ~125g (FTMO).
2. **Breakout Classic declassata**: il daily-loss 3% coi wick del book è una tagliola
(P(pass) 26.6%) — fuori dalla shortlist.
3. **FTMO Swing riabilitata dal lato regole**: il DD 10% assorbe i wick (haircut solo
5.8pp) → miglior P(pass); resta il caveat swap CFD ~10-25%/anno NON nel MC, che
colpisce esattamente i multi-week hold del book.
**Fase funded — sweep di leva (novità decisiva): la leva ottima è 0.75x, non 1x.**
A 1.0x col max-loss 6% HYRO l'account de-luck sopravvive 1 anno solo il **20.5%** delle
volte (il close-only diceva 69% — era il numero più gonfiato di tutta la parte C).
A **0.75x**: HYRO P(vivo) 58.1%, E[payout] $5.8k/anno (~€14.5/g ATTESO, mediana €20.6/g
se vivo); FTMO 0.75x P(vivo) 82.2%, E[payout] $7.4k lordo swap. Equivalente operativo:
girare il funded a target_vol ~15% invece del 20% canonico.
**EV del biglietto aggiornato (eval 1.0x, funded 0.75x, de-luck):** P(pass) ~44%,
**P(perdere la fee) ~56%** (peggio del 35-45% stimato close-only), EV a 1 anno
+$491/$1.048/$2.197 per i conti 25/50/100k → il fronte prop resta EV-positivo ma più
sottile; il costo atteso per arrivare funded è ~$570-1.325 a seconda della taglia.
Limiti dichiarati: wick a risoluzione 1h (i 5m sarebbero più severi → le P(pass) restano
un TETTO); leva applicata linearmente al giorno; bust funded = payout 0 (i prelievi
settimanali reali migliorano l'atteso). Test suite invariata (172 pass).
## Roadmap — sintesi e decision tree
### Verdetto finale (nessuna modalità esclusa, tutte quantificate)
**50 EUR/giorno entro "qualche mese": NON esiste via legale che ci arrivi.** I massimi
onesti per canale nell'orizzonte di mesi: prop funded ~€8-15/g (a $100k, P~55-68%,
3-6 mesi); yield ~€0-4/g (vincolato dal capitale, e il carry è al fondo del ciclo);
monetizzazione del track record ~€0 (i canali pagano in anni). L'unico canale "veloce"
è gestire a compenso i soldi di terzi senza autorizzazione = **reato** (art. 166 TUF,
1-8 anni) → chiuso. Il fisco 2026 (33%) alza ogni muro di un ulteriore ~1.5x.
**50 EUR/g resta raggiungibile solo come STACK su 18-36 mesi**, con esecuzione buona e
un po' di fortuna: prop al cap $200k (€15-30/g) + Darwinex/eToro (€5-20/g) + capitale
proprio cresciuto con depositi (a €20k il book fa ~€5-8/g). Top-decile ≈ €30-60/g a
24 mesi; mediana ≈ €10-25/g. Senza depositi sostanziosi, il target scivola oltre.
### Cosa è stato cablato oggi (branch `goal/50eur-day`)
| # | Cosa | File |
|---|---|---|
| 1 | Matematica del target + MC prop con regole vere | `scripts/research/r0724_goal50_math.py` |
| 2 | Gate deploy PRE-REGISTRATO statarb (decisione 27/09) | `scripts/research/r0724_statarb_deploy_gate.py` |
| 3 | Trigger regime CC01 (funding 30g, WARN 10%/ALERT 15%) | `scripts/live/cc01_regime_watch.py` |
| 4 | Fix test latente paper-advance (pre-esistente su main) | `tests/test_trend_portfolio.py` |
| 5 | Questo diario | `docs/diary/2026-07-24-goal-50eur-roadmap.md` |
| 6 | MC prop INTRADAY wick-aware (recon MTM 1h del book) | `scripts/research/r0724_goal50_intraday_mc.py` |
### Decisioni che spettano all'operatore (non automatizzabili)
1. **Fronte prop** (l'unico che cambia la scala in mesi): sì/no. Se sì (numeri C-bis,
MC intraday): HyroTrader $25-50k come test (€230-350), poi $100k; **eval a leva 1.0x
(NON 1.5x — refutata dai wick), funded a 0.75x** (≈ target_vol 15%); prelievi
settimanali; budget mentale = perdere l'intera fee con **P~56%** de-luck; atteso
funded ~€14.5/g a $100k. Alternativa con P(pass) migliore: FTMO Swing (DD 10%
assorbe i wick) MA swap CFD 10-25%/anno sui multi-week hold. Breakout Classic NO
(daily-loss 3% + wick = P(pass) 27%). Il book si esegue a mano (5 min/g)
dal segnale di `paper_portfolio` finché non c'è l'API funded.
2. **Track record pubblico** (canale 18-36 mesi): aprire Darwinex Zero (~€40-50/mese,
l'unico venue dove lo Sharpe difensivo compete sul merito) e/o candidarsi lead trader
OKX EU / eToro PI. Costo basso, payoff lento ma legale e cumulativo.
3. **Depositi**: il moltiplicatore più affidabile resta il capitale proprio — il sistema
è già pronto (cap dinamico equity/2, aspettativa onesta ~€0.6-0.8/g a 2k,
~€1.4-2/g a 5k, ~€5-8/g a 20k lordi).
4. **Alla merge in main**: aggiungere `cc01_regime_watch.py` a `cron_daily.sh` (riga
read-only) così il trigger CC01 lavora da solo.
### Cosa NON fare (confermato da questa ondata)
- Niente leva per "accelerare" (parte B: rovina quasi certa a ogni tier di capitale).
- Niente gestione remunerata di conti altrui, nemmeno family&friends (penale).
- Niente yield-chasing sopra il pavimento Aave 4-5% (HLP = trading risk travestito).
- Niente vault HL a questo capitale ($10k di creazione + 5% skin + discovery per APR).
- Il book live Deribit resta INVARIATO (pesi, config, esecuzione) — questa ondata non
ha toccato nulla del path live.
@@ -0,0 +1,72 @@
# 2026-07-24 — Ondata ON-CHAIN / SENTIMENT: la famiglia era vergine, il verdetto è 0/6 slot
**Goal**: "trova altre strategie, sfruttiamo anche internet". Prima parte (questa): la
famiglia on-chain/sentiment — l'unica grande famiglia di INFORMAZIONE (non di prezzo)
mai testata dal progetto — meccanizzata onestamente su dati gratuiti scaricati stasera.
Script `scripts/research/r0724_onchain_wave.py`. In parallelo girano 3 agenti di ricerca
web (letteratura accademica, segnali on-chain/fonti dati, pratica non convenzionale) —
sintesi in un diario/addendum separato quando completano.
## Setup
- **Segnali**: CoinMetrics Community (CSV daily dal genesis, gratuito, tokenless:
AdrActCnt, TxTfrCnt, CapMVRVCur, SplyExNtv, HashRate) + Fear&Greed alternative.me
(2018-02→oggi) + supply stablecoin totale DefiLlama (2017-11→oggi). Staging in
`data/external/coinmetrics/` (gitignored — NON certificato, mai usato come prezzo).
- **Ritorni**: SOLO feed Deribit certificato (lezione v2.0.0). Harness: `altlib`,
`study_family_honest` (selezione cella IN-SAMPLE, deflated-Sharpe sull'intera griglia,
marginal scorer indurito vs TP01). 32 celle totali su 6 famiglie, tutte contate.
- **Causalità**: doppio lag — segnale by-day shiftato di 1 giorno (la riga CM del giorno
d si completa a EOD d + processing) + lo shift di 1 barra di `eval_weights` ⇒ lag
attività→posizione = 2 giorni. tz-aware su entrambi i lati del reindex (la trappola
naive/aware ha già colpito stasera nel MC intraday).
## Verdetti (0/6 earns_slot_honest)
| Famiglia | Cella IS | DSR | Marginale | Perché muore |
|---|---|---|---|---|
| **STABLE**-supply-growth | L=30 LF thr=10% | **0.998** | **ADDS** | robust_oos=False (uplift concentrato, 2026 domina) |
| **NET**-growth (Liu-Tsyvinski) | TxTfrCnt L=30 LF | 0.916 | ADDS | DSR<0.95 E jackknife 0.12 |
| **MVRV**-valuation | long-salvo-top p90 | 0.968 | DILUTES | hold-out 0.08, blend peggiora sempre |
| **EXS**-exchange-supply | L=90 LF | 0.903 | NEUTRAL | hold-out **0.58**: il claim "outflow=bullish" è DECADUTO |
| **HASH**-ribbons (BTC-only) | 30/60 hold120 | 0.984 | **HEDGE** | paga solo quando TP01 è debole (corr 0.67) |
| **FNG**-fear-greed | regime media7>50 | 0.955 | NEUTRAL | corr→TP01 **0.82**: è trend travestito |
## Il finding strutturale
**L'on-chain tradabile è in larga parte PREZZO TRAVESTITO**: corr→TP01 0.50-0.82 su
tutte le famiglie. L'attività di rete, il sentiment e la supply su exchange *seguono*
il prezzo; quando ne estrai un segnale direzionale daily ottieni un trend-follower
peggiore di TP01 con un vocabolario diverso. È la stessa firma delle onde macro-gate
(corr 0.989) e funding: la barra per un nuovo sleeve non è "informazione nuova" ma
"informazione nuova NON già incorporata nel prezzo che TP01 segue".
## Il lead (debole): STABLE-supply-growth
L'unico sopra la linea su quasi tutti i gate: la crescita 30g della supply stablecoin
totale (soglia 10% annualizzato) come gate risk-on. Standalone FULL 1.25/HOLD 0.42,
DSR 0.998, ADDS persistente multi-cut, non-hedge, blend w25 hold +0.12. Muore SOLO su
robust_oos (drop-best-month 0.06; l'uplift 2026 = 1.32 domina la persistenza).
⚠️ **Caveat VINTAGE che declassa il lead a WATCH**: la storia DefiLlama è RICOSTRUITA
retroattivamente (chain e coin aggiunte nel tempo) — il "totale supply" visto oggi per
il 2019-2021 NON era osservabile allora. Un backtest su serie vintage-contaminate può
inventare edge. Idem (più mite) per le metriche exchange-flow CM (labeling dei wallet
rivisto ex-post). **Regola nuova da questa ondata: ogni segnale da aggregatori esterni
va classificato per rischio-vintage prima del backtest; un PASS su serie ricostruita
vale al massimo WATCH.**
Decisione: **WATCH, niente paper-monitor** (corr 0.61-0.67 a TP01 = il beneficio
marginale reale è piccolo; il vintage-risk non è sanabile retroattivamente). Criterio
di rivisita: se una fonte point-in-time della supply stablecoin (snapshot propri da
oggi, o fonte con vintage garantito) accumula ≥12 mesi, ritestare il gate thr=10%.
## Note dati (per il futuro)
- CM community: fermo al **2026-05-24** (~2 mesi di lag) → inutilizzabile live senza
fonte fresca; per backtest ok.
- ETH HashRate muore al Merge (2022-09) → hash-famiglia solo BTC.
- F&G è DERIVATO da vol+momentum di prezzo → la ridondanza col trend era attesa e
lo scorer l'ha confermata.
Book/pesi INVARIATI. Test suite invariata (172 pass).
+132
View File
@@ -0,0 +1,132 @@
# 2026-07-24 — SKH01 sul PATH LIVE: peso del book Deribit a 2 sleeve + valore della cadenza 230m
**Chiude il follow-up aperto dall'audit anchor 2026-07-02** (`2026-07-02-anchor-audit-xs01-skh01.md`:
"cadenza 230m, peso live da rivedere"). Il book live Deribit è TP01 0.75 + SKH01 0.25
(`sleeves.deribit_book_sleeves`); l'audit aveva mostrato che il path live reale (cron ORARIO
`0 * * * *` + exit software, non on-book al livello) degrada SKH01 (50/50: FULL 1.46→1.19,
HOLD 1.64→1.15, DD 18→25%) e che l'offset 0 della griglia 230m/690m è best-of-23. Domanda:
**dato il path live vero, qual è il peso SKH ottimale, e quanto vale allineare il cron a 230m?**
**Script:** `scripts/research/r0724_skh_live_weight.py` — riusa la macchineria di
`r0702_anchor_skh01.py` (import diretto di `run_asset`/`sim_equity`/`get5m`/`sanity`, non
riscritta). Nessun file di produzione toccato (config/sleeves/live/cron INVARIATI).
## Verdetto in una riga
**INVARIATO su entrambi i fronti**: il peso SKH ottimale sul path live, scelto onestamente
(mediana de-luckata sui 23 offset, IN-SAMPLE, mai su hold-out), è **0.25 = il peso corrente**
(plateau 0.200.30); e **allineare il cron ai confini 230m non vale nulla** (Δmediana di banda
~+0.01/+0.02 Sharpe): il degrado live è quasi tutto **fill-al-livello vs exit software**,
che nessuna cadenza recupera.
## Setup (onestà dichiarata a priori)
- **Serie SKH live-path**: tutti i **23 offset** a priori della griglia (ogni 30m su [0,690)) ×
3 modi di `sim_equity`: `canonical` (fill al livello sl/tp = backtest), `barclose` (fill al
close della barra 230m = ciò che otterrebbe un cron allineato ai confini), `hourly` (fill al
close 5m del prossimo multiplo orario = **path del cron reale**). Sanity replica bit-exact
del canonico prima di ogni numero.
- **Sweep peso**: book = (1w)·TP01 + w·SKH_live, w ∈ {0, .10, .15, .20, .25, .30, .40, .50}
(**8 valori = multiple-testing leggero, dichiarato**; nel gate k_seen=8). TP01 =
`al.tp01_baseline_daily()` canonico (come nell'audit).
- **Regola di selezione dichiarata PRIMA di vedere i numeri**: w* = argmax della **mediana
sui 23 offset dello Sharpe IN-SAMPLE** (pre-2025), lente `hourly`; a parità (<0.03) vince
il w più vicino al corrente 0.25. **L'hold-out serve solo a verificare, mai a scegliere.**
- **Gate**: `weights_tilt_null` (src/portfolio/portfolio.py) adattato al book a 2 sleeve
(null = 500 tilt casuali floor 0.05 / cap SKH 0.50 attorno al 75/25, sui daily del path
live hourly).
## Sanity — riconciliazione con l'audit r0702
Off0 == `_skyhook_returns()` **bit-exact** (max|Δ|=0.0 su 2688 giorni; l'audit ne aveva 2666 —
22 giorni di feed in più, deriva-dati attesa). DD harness BTC 21.4% / ETH 27.4% e n. trade
identici al diario di ammissione. Book 50/50 off0:
| modo | ShFULL | ShIS | ShHOLD | maxDD | audit r0702 |
|---|---|---|---|---|---|
| canonical | 1.447 | 1.418 | 1.578 | 18.1% | 1.46 / — / 1.64 / 18.1% |
| barclose | 1.100 | 1.080 | 1.228 | 23.0% | (non riportato) |
| hourly | 1.179 | 1.201 | 1.103 | 24.6% | 1.19 / — / 1.15 / 24.6% |
TP01 baseline: FULL 1.290 / IS 1.488 / HOLD 0.297 / DD 14.3% (audit ~1.30/~0.31). Blend 75/25
canonical off0 HOLD 1.125 (audit ~1.16). **Riconcilia** (Δ ≈ deriva-dati).
## Sweep peso — lente HOURLY (path live, quella della decisione)
| w | off0 FULL/IS/HOLD/DD | mediana banda FULL/IS/HOLD/DD | HOLD min..max | DD min..max |
|---|---|---|---|---|
| 0.00 | 1.29 / 1.49 / 0.30 / 14.3% | 1.29 / 1.49 / 0.30 / 14.3% | — | — |
| 0.10 | 1.48 / 1.67 / 0.54 / 11.8% | 1.42 / 1.63 / 0.43 / 11.9% | 0.33..0.56 | 11.3..13.1% |
| 0.15 | 1.56 / 1.74 / 0.66 / 11.6% | 1.47 / 1.68 / 0.50 / 11.6% | 0.35..0.70 | 10.7..12.6% |
| 0.20 | 1.61 / 1.79 / 0.77 / 11.3% | 1.51 / 1.71 / 0.56 / 11.3% | 0.36..0.82 | 10.1..12.7% |
| **0.25** | **1.65 / 1.82 / 0.86 / 11.1%** | **1.53 / 1.72 / 0.62 / 11.2%** | 0.36..0.94 | 9.6..13.6% |
| 0.30 | 1.67 / 1.82 / 0.94 / 11.0% | 1.53 / 1.71 / 0.67 / 11.0% | 0.37..1.05 | 9.4..14.6% |
| 0.40 | 1.65 / 1.78 / 1.04 / 11.9% | 1.48 / 1.65 / 0.73 / 11.9% | 0.35..1.20 | 10.4..17.3% |
| 0.50 | 1.58 / 1.69 / 1.09 / 12.9% | 1.39 / 1.53 / 0.76 / 13.4% | 0.33..1.30 | 11.2..19.9% |
(La tabella `canonical` di riferimento è nell'output dello script: stessa forma, picco a
0.300.40 su off0 — l'off0 canonico resta la lente gonfiata già nota.)
- **Mediana-IS (hourly) per w**: 0.00:1.488 · 0.10:1.627 · 0.15:1.678 · 0.20:1.710 ·
**0.25:1.721** · 0.30:1.712 · 0.40:1.645 · 0.50:1.534 → argmax = **0.25**, plateau
{0.20, 0.25, 0.30} dentro la tolleranza 0.03 → **w\* = 0.25 = corrente**.
- Anche sul path live onesto la gamba SKH **paga**: a w=0.25 la mediana di banda migliora
TP01-solo su tutto (FULL 1.29→1.53, IS 1.49→1.72, HOLD 0.30→0.62, DD 14.3%→11.2%) —
la diversificazione sopravvive al path orario. Il sizing 25%, deciso a suo tempo su numeri
best-of-23, **risulta a posteriori difendibile anche de-luckato sul path vero**.
- **Tentazione rifiutata per regola**: su off0 l'HOLD cresce monotono fino a w=0.50 (1.09).
Sceglierlo sarebbe selezione-sull'hold-out + fortuna d'ancora (off0 = 98° pctl HOLD
nell'audit). La banda IS dice 0.25 e il DD di banda peggiora oltre 0.30.
## Gate `weights_tilt_null`
w\* == corrente → **nessun cambio proposto: INVARIATO** (gate non necessario). Eseguito
comunque, a titolo informativo, sul miglior w ≠ corrente (0.30):
- **off0 (griglia live)**: delta_insample +0.007, delta_hold +0.076, pctl_hold 53.6 (<88.9
best-of-8) → gate_pass **True ma marginale** (frac_random_beat_hold 0.562: più di metà dei
tilt casuali batte il 75/25 sull'hold-out — il claim "SKH aiuta l'hold-out" è generico,
non specifico di 0.30).
- **off30 (offset IS-mediano)**: delta_insample **0.009** → gate_pass **False**. Il pass di
off0 è (di nuovo) figlio dell'ancora fortunata: il tick +0.01 IS a 0.30 non è robusto
alla banda. → 0.30 NON supera la barra onesta; **75/25 resta**.
## Cadenza 230m — barclose vs hourly (a w=0.25)
- off0: ΔShFULL **0.040**, ΔShHOLD +0.068, ΔDD 0.9pt. Banda 23 offset: ΔShFULL med
**+0.010** [0.042,+0.105], ΔShHOLD med **+0.020** [0.109,+0.162], ΔDD med 0.3pt.
- SKH standalone 50/50: barclosehourly ΔShFULL off0 0.080 (med +0.015), ΔShHOLD off0
+0.125 (med +0.041), ΔDD off0 1.5pt (med 1.3pt).
- **Sorpresa onesta**: a off0 il barclose è perfino PEGGIO dell'hourly su FULL — il ritardo
050 min del cron orario è **rumore, non un costo sistematico**. Il vero gap è il
**fill-al-livello** (canonicalbarclose: ΔShFULL off0 +0.347, ΔShHOLD +0.350, ΔDD 4.9pt),
cioè l'assunzione del backtest che l'exit avvenga esattamente a sl/tp — e quello **nessun
cron lo recupera** (l'exit software in una barra che gappa prende il gap comunque; già
misurato dall'audit: sl2% → 11/23% realizzato nei crash).
- Fattibilità (SOLO stima, non cablata): 230=23×10 → i confini 230m cadono sempre su multipli
di 10 min dall'epoch, quindi un cron `*/10` che esegue solo quando `now % 230min == 0`
colpirebbe ogni confine con ritardo ~0. **Ma non vale la pena**: guadagno mediano ~+0.01/+0.02
Sharpe, dentro il rumore, contro 6× invocazioni cron e complessità sul path live certificato.
## Caveat
- Exit software nei crash = gap reale (la sim `hourly` è già la stima pessimistica giusta);
niente slippage/parziali modellati; equity daily-step (lens Sharpe) come canonico e audit.
- 8 valori di w provati e dichiarati (k_seen=8 nel gate); selezione mai sull'hold-out.
- La banda 23-offset è la lente de-luckata; live gira UNA sola griglia (off 0).
- Deriva-dati vs audit (22 giorni di feed in più): riconciliazione "circa", riportata sopra.
- A $600 la granularità del peso non vincola (gamba SKH a w=0.10 ≈ $60/trade ≫ min-order $5).
## Raccomandazione finale (decisione utente per ogni cambio config)
1. **Peso SKH del book live Deribit: INVARIATO a 0.25.** È l'ottimo di banda sul path live
(plateau 0.200.30); nessun w alternativo passa il gate in modo robusto.
2. **Cadenza cron: INVARIATA (oraria).** L'allineamento 230m vale ~+0.01/+0.02 Sharpe mediano
(rumore); il degrado live è strutturale (fill-al-livello), non di cadenza. Non cablare il
cron `*/10`.
3. L'aspettativa onesta della gamba SKH live resta quella dell'audit: blend ~fase-mediana +
path orario (HOLD di banda ~0.62 a w=0.25, non 1.17), con gap-through-stop nei crash.
Il **follow-up dell'audit 2026-07-02 è CHIUSO** (entrambi i punti, esito nullo).
**Stato:** config/live.json, sleeves.py, scripts/live, cron **INVARIATI** — questo è ricerca;
ogni eventuale cambio resta una decisione esplicita dell'utente. Script committabile dopo review.
+161
View File
@@ -0,0 +1,161 @@
# 2026-07-24 — Caccia strategie via web (3 agenti + 3 ondate empiriche): 0 slot, 2 lead operativi, 1 pista infrastrutturale
**Goal**: "trova altre strategie, sfruttiamo anche internet". Seconda metà della serata
(la prima è l'ondata on-chain, diario `2026-07-24-onchain-sentiment-wave.md`). Tre agenti
di ricerca web in parallelo (accademia 2022-26, on-chain/fonti dati, pratica non
convenzionale) + meccanizzazione immediata di tutto ciò che era testabile stanotte.
## Convergenza ricerca ↔ empirica (il quadro)
La letteratura di replicazione 2024-26 dice: dei 36 fattori crypto pubblicati ne
sopravvivono **2-3** (tutti liquidity/momentum — Mercik/Zaremba IRFA 2026); le anomalie
sono micro-cap e bull-conditional (Fieberg IRFA 2024); attention/volume falliscono OOS;
i flussi opzioni predicono la VOL, non la direzione (Alexander JFM 2022). Le prime 5
raccomandazioni degli agenti (MVRV, stablecoin, exchange-flow, F&G, hash ribbons)
**coincidono con le famiglie uccise stanotte dall'ondata on-chain** — la convergenza
indipendente ricerca/empirica è essa stessa il finding: il soffitto ~1.3 direzionale
BTC/ETH non si aggira con "informazione nuova" che è funzione del prezzo.
## Ondata PREMI CROSS-VENUE (empirica, stanotte) — 0/2, con lezione
`scripts/research/r0724_premium_wave.py` — unica famiglia flow con dati 100%
auto-calcolati da candele raw (zero rischio-vintage): Coinbase premium (2015→) e
kimchi premium Upbit/ECB (2017→), allineamento 00:00 UTC verificato (le daily Upbit
ancorano a 09:00 KST = mezzanotte UTC).
- **CBPREM**: medio 1bp (il feed certificato è un indice che include Coinbase →
premio smorzato, strutturale); miglior cella = HEDGE, hold 0.46 → MORTO.
- **KIMCHI d30-follow**: il candidato più forte della serata — **EARNS_SLOT=True al
marginal scorer** (ADDS, robust_oos, uplift positivo OGNI anno 2020-26, non-hedge,
hold standalone 0.74) ma DSR 0.891<0.95. Lo scettico (`r0724_kimchi_skeptic.py`) lo
uccide in due mosse: **niente plateau** (d15 0.44 / d30 1.07 / d45 0.51 / d60 0.24 =
spike isolato) e **lag +1g lo azzera** (hold 0.74→0.14, uplift blend negativo).
→ SCARTATO, parameter-luck. **Lezione: un EARNS_SLOT con DSR<0.95 va SEMPRE allo
scettico plateau+lag prima di qualsiasi entusiasmo** — il DSR aveva ragione.
## Probe HLP vault (dati reali API HL, 89 periodi ~14g, 2023-05→oggi)
L'unico stream **crash-long** mai visto dal progetto: corr col book **0.19**, nei
periodi in cui il book perde HLP fa in media **+2.5%**; crash ott-2025 **+10.4%**,
liquidazione feb-2026 +7.0%. Sharpe grezzo 1.98 (su barre 14g — il daily vero è più
brutto), maxDD 5.8%. MA: **decay netto** (+60%/+79% 2023/24 → +19% 2025 → ~+12%
run-rate 2026, TVL diluisce) e la coda di protocollo (JELLY mar-2025, $12M
quasi-socializzati; contract/bridge risk) NON è nella serie → lo Sharpe è
event-luck + short-tail-risk travestito da income. **Verdetto: ALLOCATION-WATCH**
(non è una strategia: è un deposito USDC con lockup 4g). A $600 la materialità è
~€10-30/anno → decisione operatore, non urgente. Dati: `data/external/hlp_vault.json`.
## LEAD infrastrutturale nuovo: Polymarket ↔ Deribit (probabilità implicite)
Il best-find dell'agente pratica: le binarie BTC/ETH su Polymarket vs le probabilità
implicite della chain opzioni Deribit — **gap medio 11pp vs Deribit** (arXiv 2606.19517),
half-life ~4h (= retail-compatible), delta-hedge col perp che già tradiamo. Unico
candidato 2026 che è insieme: meccanismo NUOVO (non prezzo), eseguibile a $600 (PM min
$1, no fee), e costruibile su infra ESISTENTE (motore BS/DVOL di VRP01 + cerbero-bite).
Scettici seri dichiarati: campione accademico piccolo e clusterizzato; parte del gap è
probabile **spec-mismatch** (fonte/orario di risoluzione PM vs settle Deribit) da
riconciliare contratto-per-contratto PRIMA di credere a un bp; book PM sottili vicino a
risoluzione; l'arb veloce è dei bot (73% sub-100ms) — l'angolo onesto è la convergenza
lenta a limit-order. **→ Prossima ondata di ricerca: studio di riconciliazione** (dati:
PM CLOB API gratuita + DB storico pubblicato arXiv 2606.04217). Nessun codice stasera.
## Altre azioni a costo zero raccomandate dagli agenti (per l'operatore)
1. **Puntare cerbero-bite anche sulle scadenze DAILY Deribit** da subito: 365 expiry/anno
= una serie di premi REALI si accumula 7× più veloce delle weekly → in 6 mesi decide
il dossier 0DTE/calendar (che resta bloccato dalla regola no-short-vol-da-modello).
2. Dataset Reservoir S3 (tape completo HL: fill, liquidazioni, ADL, L2) = primo dataset
non-di-prezzo del progetto, requester-pays (serve account AWS) → abilita studio
liquidation-exhaustion + listing-effects. Parcheggiato: richiede setup infra.
3. ETF flows: storia 2.6 anni → NON testabile onestamente fino al ~2028 (parcheggiato
con data di rivisita).
## Test cross-sectional su HL (lead-lag JEDC 2024 + salience/MAX) — DEAD 2/2
`scripts/research/r0724_xs_leadlag_max.py` (agente, 19 major HL, 2024→2026-07, 936g,
0 backfill verificato a runtime, convenzioni XS01: fee 0.10% RT/gamba, vol-target 20%,
holdout 2025).
- **LEAD-LAG (Guo et al. JEDC 2024)**: finding strutturale — con predictor equal-weight
"all-others", S_i = (Σr r_i)/(A1) è affine DECRESCENTE nel ritorno proprio
(rank-corr empirica **1.000**): il "lead-lag accademico" sull'universo È lo
short-term reversal, già morto (3ª conferma FOLLOW>FADE). Le 8 celle hanno IS Sharpe
TUTTE negative (best 0.11); le celle large5 "buone" in hold-out (1.18) non sono
selezionabili in-sample (trappola selection-on-holdout) E sono degenerate (14/19
asset a segnale identico). DSR 0.237. **DEAD.**
- **MAX/lottery (Cai&Zhao JBF 2024)**: cella IS L30/H10 FULL 0.59 / HOLD 0.47,
DSR **0.412**, il segno si INVERTE tra L7 e L30 (plateau assente), 34 ribilanci IS
(small-n), fee-zero non salva (edge lordo piccolo, non morte-per-fee). Sotto lo
standard STATARB (0.84/DSR 0.929) → **DEAD**, non lead. Direzione coerente con la
letteratura ma indistinguibile dal rumore su ~2.5 anni.
Nota metodologica in omaggio: la degenerazione "predictor affine nel ritorno proprio"
è un check di 3 righe (rank-corr segnale↔momentum proprio) che smaschera i lead-lag
cross-section travestiti — da riusare su ogni futura proposta XS.
## Bilancio del goal "trova altre strategie"
Su tutta la serata: **10 famiglie nuove meccanizzate e giudicate coi gate completi + 1
probe** (6 on-chain/sentiment + 2 premi cross-venue + 2 cross-sectional HL + probe HLP),
3 report di ricerca web con ~50 fonti primarie, **0 slot nuovi** — coerente con la
storia del progetto (il book attuale resta il massimo onesto raggiunto) — ma:
1 pista genuinamente nuova con meccanismo non-di-prezzo (Polymarket↔Deribit, prossima
ondata), 1 allocation-watch crash-long (HLP), 2 azioni a costo zero (daily options
capture, rivisita ETF 2028), e 2 regole metodologiche nuove a diario (vintage-risk
delle fonti esterne; scettico plateau+lag obbligatorio sugli EARNS_SLOT con DSR<0.95).
Book/pesi INVARIATI.
---
## Addendum (sera, goal "un agente per ogni case") — i 3 verdetti
Tre agenti dedicati, uno per case, tutti su DATI LIVE (nessun claim di seconda mano).
Script: `r0724_pm_deribit_probe.py`, `r0724_hlp_deepdive.py`, `r0724_daily_opt_snapshot.py`.
### 1. Polymarket ↔ Deribit (con opzioni in copertura) — **SKIP come trade; il lead "11pp" è REFUTATO dalla misura**
34 binarie PM matchate alle daily options Deribit, spec verificata su 66 mercati (PM
risolve su candela 1m Binance USDT alle 16:00 UTC; Deribit TWAP 30m indice USD alle
08:00). Il gap si decompone in: **fonte USDT +9,3bps di basis = +4-5pp di probabilità
ATM**, tempo 8h ∓5pp, e replica ingenua su book ITM morti (la banda sub/super-replica
ATM è [39%,91%] con strike ogni $500 su orizzonti a 1σ≈$350). Residuo spec-adjusted:
ATM 5…+6pp a segno instabile DENTRO la banda di replica; tail +1,8-3,1pp
(favorite-longshot) sotto i costi. Trade coperto reale a size minima: lock netto
$0,1-5, capitale $200-1.000 (il margine SM del leg corto domina: Deribit non netta i
vertical), **P(gambe in conflitto 08→16 UTC) 2-22%** → non è un arb. Più geoblock/ToS
PM per residenti IT. Facoltativo: WATCH a costo zero (cron dello script per la serie
del gap tail). Lezione: anche il "best lead" accademico muore alla riconciliazione di
spec — la parte vera del gap era spec, non alpha.
### 2. HLP vault — **SKIP come income, WATCH con trigger meccanici; il carry 2026 ex-evento è +0,2%/anno**
Trovata la serie daily vera (wrapper wHLP via CoinGecko, 364 giorni + thunderhead per
i tail di marzo 2025): il daily smaschera la lens 14g — Sharpe 0,69 (non ~2), maxDD
7,9%, worst day 5,96%; e l'uscita-in-stress via wrapper costa ~5% di sconto NAV.
**Il finding killer: il +12% headline 2026 è UN SOLO periodo evento (feb-2026 +7%);
gli altri 15 periodi sommano +0,10%** → carry ex-evento ~0,2%/anno, PnL-$ piatto vs
TVL (ritorno ∝ 1/TVL, e i depositanti lo sanno: TVL 59% dal picco). Coda JELLY
ricostruita: 4,7% MtM su inventario EREDITATO illimitato, troncato SOLO da un voto
discrezionale dei validator in 2 minuti — il crash-long può morire NEL crash
(pattern is_hedge-che-fallisce). Kelly onesto con code: **f*=0 al carry corrente**.
Trigger di ri-arma meccanici: 90g return-on-AUM ≥8% ann. AND TVL<$350M AND nessun
evento di socializzazione in 180g. Legale: HL accessibile dall'IT ma non autorizzato
MiCA → rischio geoblock non copribile con lock 4g.
### 3. Opzioni DAILY Deribit (0DTE) — **CAPTURE-AND-WAIT con prior negativo; cattura AVVIATA**
Misura live (22:35 UTC): mercato 2-sided reale anche sulle ali, MA il cap fee 12,5%
è quasi sempre binding a tenor daily → **drag fee 2-3× il weekly** (legge Albimarini
dal vivo: premio ~√T, fee ~costante; haircut mid→netto 0DTE ~45-50%) e stanotte
**IV_daily/RV = 0,43-0,54× (VRP NEGATIVO al front)** mentre il weekly fa 1,13×
(coerente con VRP01: il premio vive lì). Capitale minimo per defined-risk vero
(Portfolio Margin) ≈ $32k; a $600 = 1 lotto ETH a EV negativo. **Pipeline scritta e
attiva**: `r0724_daily_opt_snapshot.py``data/options_daily/snapshots.jsonl`
(primo snapshot 160 righe già su disco; raccomandate 2 run/g 08:05+07:55 UTC — da
cablare in cron alla merge, read-only). Regola pre-registrata: riaprire il dossier
solo dopo ≥90g di serie E mediana(IV_daily/RV_forward) netto-haircut > 0.
Diario dedicato: `2026-07-24-daily-options-0dte.md`.
### Bilancio dei 3 case
0 trade nuovi (ed è il punto: due lead famosi refutati coi numeri, non con le
opinioni), 1 pipeline di cattura reale avviata che tra 90 giorni decide da sola,
regole di ri-arma meccaniche per HLP, e il metodo confermato: ogni claim muore o
sopravvive alla RICONCILIAZIONE DI SPEC prima che al backtest. Book/pesi INVARIATI.
+2
View File
@@ -11,6 +11,8 @@ mkdir -p logs
uv run python scripts/live/paper_portfolio.py # avanza paper TP01+XS01
uv run python scripts/live/paper_prevday.py # forward-monitor lead prevday-breakout (PAPER, non deploy)
uv run python scripts/live/paper_statarb.py # forward-monitor lead STATARB-RESID ETH/BTC ortogonale (PAPER, non deploy)
uv run python scripts/live/cc01_regime_watch.py # trigger regime CC01 (read-only: WARN>=10%/ALERT>=15% funding 30g)
uv run python scripts/research/r0724_stable_snapshot.py # snapshot point-in-time supply stablecoin (sblocca WATCH STABLE a 12 mesi)
# 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.
+16
View File
@@ -0,0 +1,16 @@
#!/bin/bash
# SNAPSHOT catena opzioni DAILY (0-1DTE) Deribit — cadenza 2x/giorno. v2.0.0+.
# Cattura quote REALI per il dossier 0DTE (diario 2026-07-24-daily-options-0dte.md):
# 07:55 UTC = subito prima del settle (08:00) -> chiude il ciclo (payoff realizzato)
# 08:05 UTC = subito dopo il listing della nuova daily -> il premio "vendibile"
# Read-only (API pubblica tokenless), append su data/options_daily/snapshots.jsonl
# (gitignored). Decisione pre-registrata: il dossier si riapre solo dopo >=90g di serie
# con mediana IV_daily/RV_forward netto-haircut > 0. NON tocca il book live.
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_opt_snapshot ====="
uv run python scripts/research/r0724_daily_opt_snapshot.py
echo "===== done $(date -u '+%H:%M:%SZ') ====="
} >> logs/cron_opt_snapshot.log 2>&1
+97
View File
@@ -0,0 +1,97 @@
"""cc01_regime_watch — trigger di regime per riaprire il dossier CASH-AND-CARRY (2026-07-24).
CONTESTO. CC01 (basis/funding carry) e' un LEAD archiviato: premio REALE ma procyclico
(diario 2026-06-26-cash-carry-hl.md), non uno sleeve. La ricerca 2026-07-24 (goal50, R1)
ha confermato il ciclo live: funding BTC +70% ann. (gen 2026) -> -3.4% (mag) -> ~0-4% (lug).
Regola decisa: il dossier si riapre SOLO quando il funding medio 1-MESE e' sostenutamente
ricco — non sugli spike (gen-2026 style, spariti in settimane).
Questo watcher legge gli ultimi ~35g di funding orario BTC/ETH dall'API pubblica
Hyperliquid (tokenless, stessa fonte certificata di fetch_hl_funding.py) e stampa:
- media 30g e 7g annualizzate per BTC/ETH
- stato trigger: QUIET (<10% ann.) | WARN (>=10%) | ALERT (>=15% -> riaprire CC01,
che resta comunque vincolato a ~$20k+ di capitale e venue con funding eseguibile).
Uso: `uv run python scripts/live/cc01_regime_watch.py`
(candidato a una riga in cron_daily.sh: e' read-only, nessun ordine, nessuno stato.)
"""
from __future__ import annotations
import datetime as dt
import time
import numpy as np
import requests
HL_INFO = "https://api.hyperliquid.xyz/info"
HOURS_PER_YEAR = 24 * 365
WARN_ANN = 0.10 # >=10% ann. su media 30g -> WARN
ALERT_ANN = 0.15 # >=15% ann. su media 30g -> ALERT: riaprire il dossier CC01
def _post(payload: dict, max_retry: int = 6):
delay = 1.0
for _ 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)
continue
r.raise_for_status()
return r.json()
r.raise_for_status()
return r.json()
def fetch_recent_funding(coin: str, days: int = 35) -> np.ndarray:
"""Funding orario degli ultimi `days` giorni (paginato, max 500/req)."""
start = int((dt.datetime.now(dt.timezone.utc)
- dt.timedelta(days=days)).timestamp() * 1000)
rows, 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]
if not new:
break
for x in new:
seen.add(x["time"])
rows.append((x["time"], float(x["fundingRate"])))
start = new[-1]["time"] + 1
if len(d) < 500:
break
rows.sort()
return np.array([r for _, r in rows], dtype=float)
def main() -> None:
print("=" * 78)
print(" CC01 REGIME WATCH — funding medio 1-mese (Hyperliquid, orario, tokenless)")
print("=" * 78)
worst = "QUIET"
for coin in ("BTC", "ETH"):
f = fetch_recent_funding(coin)
if len(f) < 24 * 7:
print(f" {coin}: dati insufficienti ({len(f)} ore) — check API")
continue
ann30 = float(f[-24 * 30:].mean()) * HOURS_PER_YEAR
ann7 = float(f[-24 * 7:].mean()) * HOURS_PER_YEAR
state = ("ALERT" if ann30 >= ALERT_ANN else
"WARN" if ann30 >= WARN_ANN else "QUIET")
if state == "ALERT" or (state == "WARN" and worst == "QUIET"):
worst = state
print(f" {coin}: media 30g {ann30:+7.1%} ann. media 7g {ann7:+7.1%} ann. -> {state}")
print("-" * 78)
if worst == "ALERT":
print(" >>> ALERT CC01: funding 30g >= 15% ann. — regime ricco SOSTENUTO.")
print(" Riaprire il dossier cash-and-carry (vincoli invariati: ~$20k+,")
print(" venue con funding eseguibile, tail risk NON nel dataset).")
elif worst == "WARN":
print(" >> WARN: funding 30g >= 10% ann. — osservare, non agire.")
else:
print(" regime QUIET: carry non raccoglibile (coerente con luglio 2026).")
if __name__ == "__main__":
main()
@@ -0,0 +1,150 @@
"""SNAPSHOT catena opzioni DAILY (0-1DTE) Deribit BTC/ETH — cattura quote REALI per il dossier 0DTE.
CONTESTO (2026-07-24). VRP01 (put credit spread settimanale) e' deploy-bloccato dalla regola
"niente short-vol da modello": serve una serie di premi REALI, non BS-su-DVOL. Le scadenze DAILY
accumulano 365 expiry/anno (7x le weekly): in ~6 mesi di cattura si ottiene una serie di ~180
premi reali a tenor giornaliero + ~26 weekly, abbastanza per stimare il VRP daily NETTO di
bid/ask e fee (misura live 2026-07-24: haircut mid->netto ~30-50% a tenor daily, fee cap 12.5%
quasi sempre binding perche' i premi daily sono < 0.0024 base ccy).
COSA FA (sola lettura, API pubblica, nessun ordine, nessun token):
- per BTC e ETH: public/get_instruments (kind=option) -> tutte le scadenze entro --max-dte-h
(default 50h = la daily 0DTE + la daily 1DTE appena listata);
- per ogni strumento nel ladder di moneyness (default 75-125%): public/ticker ->
bid/ask/depth, mark, mark_iv, bid_iv/ask_iv, greche, OI, volume, underlying;
- 1 riga JSON per strumento ("rec":"chain") + 1 riga meta per valuta ("rec":"meta", con
index price e DVOL corrente) APPESE a data/options_daily/snapshots.jsonl.
CADENZA RACCOMANDATA (quando/se si decide di cablarla — NON e' in cron adesso):
- 08:05 UTC: subito dopo il listing della nuova daily (~24h DTE) = il premio "vendibile";
- 07:55 UTC: subito prima del settle (08:00 UTC) = chiude il ciclo (payoff realizzato).
Con 2 run/giorno: ~160 strumenti/run, ~350 byte/riga -> ~120 KB/giorno, ~40 MB/anno. Banale.
Ogni run extra (es. 12:00/20:00) aggiunge la dimensione intraday dello spread: opzionale.
USO:
uv run python scripts/research/r0724_daily_opt_snapshot.py
uv run python scripts/research/r0724_daily_opt_snapshot.py --currencies BTC --max-dte-h 30
NB ONESTO: questo script MISURA, non decide. La serie che produce serve a rispondere fra ~6 mesi
a: (1) IV daily vs RV daily netto haircut, (2) quanto spesso il gate IV-rank aprirebbe a tenor
daily, (3) f di stress reale quando capita un crash dentro la finestra di cattura.
"""
from __future__ import annotations
import argparse
import json
import sys
import time
from pathlib import Path
import requests
PROJECT_ROOT = Path(__file__).resolve().parents[2]
OUT_DIR = PROJECT_ROOT / "data" / "options_daily"
OUT_FILE = OUT_DIR / "snapshots.jsonl"
API = "https://www.deribit.com/api/v2/public"
SESSION = requests.Session()
def api(endpoint: str, **params):
"""GET pubblico con retry breve. Ritorna result o solleva."""
last = None
for _ in range(4):
try:
r = SESSION.get(f"{API}/{endpoint}", params=params, timeout=20)
j = r.json()
if "result" in j:
return j["result"]
last = j.get("error")
except Exception as e: # rete/JSON: ritenta
last = str(e)
time.sleep(0.7)
raise RuntimeError(f"Deribit API fail {endpoint} {params}: {last}")
def snapshot_currency(cur: str, max_dte_h: float, mny_lo: float, mny_hi: float) -> list[dict]:
now_ms = int(time.time() * 1000)
snap_ts = now_ms
rows: list[dict] = []
instruments = api("get_instruments", currency=cur, kind="option", expired="false")
spot = api("get_index_price", index_name=f"{cur.lower()}_usd")["index_price"]
try:
dvol_data = api("get_volatility_index_data", currency=cur,
start_timestamp=now_ms - 3_600_000, end_timestamp=now_ms,
resolution=3600).get("data", [])
dvol = float(dvol_data[-1][4]) if dvol_data else None
except Exception:
dvol = None
chain = [i for i in instruments
if (i["expiration_timestamp"] - now_ms) / 3.6e6 <= max_dte_h
and mny_lo * spot <= i["strike"] <= mny_hi * spot]
chain.sort(key=lambda i: (i["expiration_timestamp"], i["strike"], i["option_type"]))
expiries = sorted({i["expiration_timestamp"] for i in chain})
rows.append({
"rec": "meta", "snap_ts": snap_ts, "currency": cur, "index_price": spot,
"dvol": dvol, "n_instruments": len(chain), "expiries": expiries,
"max_dte_h": max_dte_h, "moneyness": [mny_lo, mny_hi],
})
for inst in chain:
name = inst["instrument_name"]
try:
t = api("ticker", instrument_name=name)
except RuntimeError as e:
print(f" WARN ticker {name}: {e}", file=sys.stderr)
continue
g = t.get("greeks") or {}
st = t.get("stats") or {}
rows.append({
"rec": "chain", "snap_ts": snap_ts, "currency": cur, "instrument": name,
"expiry_ts": inst["expiration_timestamp"],
"dte_h": round((inst["expiration_timestamp"] - snap_ts) / 3.6e6, 3),
"strike": inst["strike"], "type": inst["option_type"],
"settlement_period": inst.get("settlement_period"),
"min_trade_amount": inst.get("min_trade_amount"),
"taker_comm": inst.get("taker_commission"),
"bid": t.get("best_bid_price"), "ask": t.get("best_ask_price"),
"bid_amount": t.get("best_bid_amount"), "ask_amount": t.get("best_ask_amount"),
"mark": t.get("mark_price"), "mark_iv": t.get("mark_iv"),
"bid_iv": t.get("bid_iv"), "ask_iv": t.get("ask_iv"),
"delta": g.get("delta"), "gamma": g.get("gamma"),
"vega": g.get("vega"), "theta": g.get("theta"),
"oi": t.get("open_interest"), "volume_24h": st.get("volume"),
"underlying": t.get("underlying_price"), "index_price": t.get("index_price"),
})
time.sleep(0.05) # rate-limit gentile (pubblico: 20 req/s, stiamo larghi)
return rows
def main():
ap = argparse.ArgumentParser(description="Snapshot catena daily-expiry Deribit -> JSONL")
ap.add_argument("--currencies", nargs="+", default=["BTC", "ETH"])
ap.add_argument("--max-dte-h", type=float, default=50.0,
help="cattura tutte le scadenze entro N ore (default 50 = 0DTE+1DTE)")
ap.add_argument("--moneyness", nargs=2, type=float, default=[0.75, 1.25],
metavar=("LO", "HI"), help="ladder strike in frazione dello spot")
args = ap.parse_args()
OUT_DIR.mkdir(parents=True, exist_ok=True)
total = 0
with OUT_FILE.open("a") as f:
for cur in args.currencies:
try:
rows = snapshot_currency(cur, args.max_dte_h, *args.moneyness)
except Exception as e:
print(f"ERRORE {cur}: {e}", file=sys.stderr)
continue
for r in rows:
f.write(json.dumps(r, separators=(",", ":")) + "\n")
total += len(rows)
n_chain = sum(1 for r in rows if r["rec"] == "chain")
print(f"{cur}: {n_chain} strumenti (+1 meta) appesi")
print(f"OK: {total} righe -> {OUT_FILE}")
if __name__ == "__main__":
main()
@@ -0,0 +1,358 @@
"""r0724_goal50_intraday_mc — MC prop-firm con regole sui WICK INTRADAY (2026-07-24).
CHIUDE il caveat n.1 di r0724_goal50_math.py parte C: quel MC applicava daily-loss e
max-drawdown alle CHIUSURE giornaliere del book, ma le regole vere delle firm scattano
sull'equity INTRADAY mark-to-market (il wick conta). Qui:
1) ricostruzione MTM a 1h dell'equity del book Deribit (TP01 75% pesi daily + SKH01 25%
posizioni 230m con exit AL LIVELLO SL/TP nell'ora del trigger, SL prioritario — stessa
convenzione di backtest_signals), wick per-barra inclusi (low/high firmati col segno
del peso), fee incluse (TP01 fee_side*|Dheld|, SKH 0.10% RT a fine trade);
2) per ogni giorno UTC: R_d (chiusura MTM) e m_d (minimo intraday cumulato). Il MC gira
su TUPLE (R_d, m_d) della STESSA ricostruzione -> chiusure e wick coerenti tra loro.
NB: il daily MTM e' la lens GIUSTA per un conto prop (l'equity del conto e' marcata
intraday); lo sleeve SKH ufficiale ha attribuzione a scalino per-trade (P&L intero
sulla barra d'ingresso, caveat noto "equity daily-step") -> la corr daily recon-vs-
sleeve su SKH e' strutturalmente bassa; la sanity giusta su SKH e' il TOTALE.
3) block-bootstrap sulle tuple: stesse regole/leve/de-luck del MC daily-close; breach
valutato su m. Confronto diretto close-only vs intraday = "wick haircut" in pp;
4) fase funded intraday-aware + EV del biglietto per taglia ($25k/$50k/$100k HYRO).
APPROSSIMAZIONI DICHIARATE: (a) wick a risoluzione 1h (i 5m sarebbero piu' profondi ->
le P(pass) restano un TETTO, ma molto piu' oneste del close-only); (b) confini barra
230m non allineati all'ora -> piccoli edge sub-orari; (c) leva applicata linearmente al
giorno (ok per lev <= 2); (d) trigger SL/TP ricercato sulle H/L orarie dentro la barra
230m d'uscita, fallback = cap all'ultima ora della barra.
Uso: `uv run python scripts/research/r0724_goal50_intraday_mc.py` (~1-2 min: SKH 5m full)
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pandas as pd
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from src.data.downloader import load_data # noqa: E402
from src.portfolio.portfolio import StrategyPortfolio, metrics # noqa: E402
from src.portfolio.sleeves import deribit_book_sleeves # noqa: E402
from src.strategies.skyhook import SKH01_V2_DD, build_frames, skyhook_entries # noqa: E402
from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio, resample_1d # noqa: E402
EURUSD = 1.09
RNG = np.random.default_rng(724)
ASSETS = ("BTC", "ETH")
W_TP, W_SKH = 0.75, 0.25 # pesi del book live Deribit (deribit_book_sleeves)
SKH_FEE_RT = 0.001
# ------------------------------------------------------------------ esposizioni
def tp01_daily_frame(asset: str) -> pd.DataFrame:
"""Per giorno UTC: peso TP01 tenuto (deciso a close[d-1]) e fee del ribilanciamento,
gia' scalati al leg (0.5 del book TP; il peso di book W_TP si applica fuori)."""
tp = TrendPortfolio(**CANONICAL)
df = resample_1d(load_data(asset, "1h"))
tgt = tp.target_series(df)
held = np.zeros(len(tgt))
held[1:] = tgt[:-1]
fee = tp.fee_side * np.abs(np.diff(held, prepend=0.0))
# tz-aware UTC come l'indice 1h del chiamante: un mismatch naive/aware nel reindex
# darebbe NaN->0 silenziosi (TP01 sparirebbe dal recon — visto al primo run, corr 0.43)
days = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True).dt.floor("D"))
return pd.DataFrame({"w": held * 0.5, "fee": fee * 0.5}, index=days)
def skh_hourly_contrib(asset: str, idx1h: pd.DatetimeIndex, close: np.ndarray,
low: np.ndarray, high: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
"""Contributo del leg SKH01 (peso W_SKH*0.5) a ritorno e wick di ogni barra 1h.
Replica trade-per-trade la logica di sleeves._skyhook_positions/backtest_signals:
entry a close 230m [i], exit alla prima barra 230m che tocca SL (prioritario) o TP
— AL LIVELLO — oppure a close dopo max_bars. Qui il trade e' marcato a mercato ora
per ora; nell'ora del trigger il ritorno e' cappato al livello e la posizione muore.
Fee RT sottratta nell'ora d'uscita."""
ltf, htf = build_frames(load_data(asset, "5m"))
ent = skyhook_entries(ltf, htf, SKH01_V2_DD)
H = ltf["high"].values
L = ltf["low"].values
times = pd.DatetimeIndex(pd.to_datetime(ltf["datetime"], utc=True))
bar_td = times[1] - times[0]
n = len(ltf)
w = W_SKH * 0.5
ret = np.zeros(len(idx1h))
wick = np.zeros(len(idx1h))
i = 0
while i < n:
e = ent[i]
if e is None:
i += 1
continue
d, sl, tp_, mb = e["dir"], e["sl"], e["tp"], e["max_bars"]
exit_idx, exit_mode = None, None
for s in range(1, mb + 1):
j = i + s
if j >= n:
break
hit_sl = (L[j] <= sl) if d == 1 else (H[j] >= sl)
hit_tp = (H[j] >= tp_) if d == 1 else (L[j] <= tp_)
if hit_sl or hit_tp or s == mb:
exit_idx = j
exit_mode = "sl" if hit_sl else ("tp" if hit_tp else "time")
break
if exit_idx is None:
exit_idx, exit_mode = n - 1, "open" # trade ancora aperto a fine dati
t0 = times[i] + bar_td # entry a close della barra i
t1 = times[exit_idx] + bar_td # fine della barra d'uscita
h0 = int(idx1h.searchsorted(t0, side="left"))
h1 = int(idx1h.searchsorted(t1, side="left"))
trig_from = int(idx1h.searchsorted(times[exit_idx], side="left"))
done = False
last_h = None
for h in range(max(h0, 1), h1):
prev_c = close[h - 1]
r_h = close[h] / prev_c - 1.0
wk_h = (low[h] / prev_c - 1.0) if d == 1 else (high[h] / prev_c - 1.0)
if exit_mode in ("sl", "tp") and h >= trig_from:
hit_sl_h = (low[h] <= sl) if d == 1 else (high[h] >= sl)
hit_tp_h = (high[h] >= tp_) if d == 1 else (low[h] <= tp_)
if hit_sl_h: # SL prioritario, come backtest_signals
lvl = sl
wk_h = sl / prev_c - 1.0 # il modello esce al livello: wick cappato
elif hit_tp_h:
lvl = tp_ # wick avverso dell'ora resta (adverse-first)
else:
lvl = None
if lvl is not None:
ret[h] += w * d * (lvl / prev_c - 1.0) - w * SKH_FEE_RT
wick[h] += w * d * wk_h
done = True
break
ret[h] += w * d * r_h
wick[h] += w * d * wk_h
last_h = h
if exit_mode in ("sl", "tp") and not done and last_h is not None:
# trigger non trovato sulle H/L orarie (bordi sub-orari): forza il cap
# all'ultima ora -> P&L coerente col prezzo d'uscita modellato
prev_c = close[last_h - 1]
lvl = sl if exit_mode == "sl" else tp_
ret[last_h] += w * d * (lvl / prev_c - 1.0) - w * d * (close[last_h] / prev_c - 1.0) \
- w * SKH_FEE_RT
elif exit_mode == "time" and last_h is not None:
ret[last_h] -= w * SKH_FEE_RT
if exit_mode == "open":
break
i = exit_idx + 1
return ret, wick
def book_intraday_days() -> tuple[pd.DataFrame, dict]:
"""Per giorno UTC: R_d (chiusura MTM netta del book ricostruito) e m_d (min intraday).
Ritorna anche le serie daily per-leg per la sanity."""
book_r = None
book_wk = None
legs: dict[str, pd.Series] = {}
for a in ASSETS:
df = load_data(a, "1h")
idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True))
close = df["close"].values.astype(float)
low = df["low"].values.astype(float)
high = df["high"].values.astype(float)
prev = np.concatenate(([np.nan], close[:-1]))
r_close = close / prev - 1.0
# --- TP01: peso costante nel giorno, fee alla prima ora del giorno
tpf = tp01_daily_frame(a)
day_of = idx.floor("D")
w_tp = tpf["w"].reindex(day_of).values * W_TP
w_tp = np.nan_to_num(w_tp)
fee_tp = tpf["fee"].reindex(day_of).values * W_TP
fee_tp = np.nan_to_num(fee_tp)
first_of_day = np.concatenate(([True], day_of[1:] != day_of[:-1]))
r_wick_tp = np.where(w_tp >= 0, low / prev - 1.0, high / prev - 1.0)
tp_ret = w_tp * r_close - np.where(first_of_day, fee_tp, 0.0)
tp_wick = w_tp * r_wick_tp - np.where(first_of_day, fee_tp, 0.0)
# --- SKH01: MTM per-trade con exit al livello
skh_ret, skh_wick = skh_hourly_contrib(a, idx, close, low, high)
tot_r = pd.Series(tp_ret + skh_ret, index=idx)
tot_wk = pd.Series(tp_wick + skh_wick, index=idx)
legs[f"tp_{a}"] = pd.Series(tp_ret, index=idx)
legs[f"skh_{a}"] = pd.Series(skh_ret, index=idx)
book_r = tot_r if book_r is None else book_r.add(tot_r, fill_value=np.nan)
book_wk = tot_wk if book_wk is None else book_wk.add(tot_wk, fill_value=np.nan)
book_r = book_r.dropna()
book_wk = book_wk.reindex(book_r.index)
rows = []
for day, g in book_r.groupby(book_r.index.floor("D")):
r = g.values
wk = book_wk.loc[g.index].values
cum = np.cumprod(1 + r)
cum_prev = np.concatenate(([1.0], cum[:-1]))
m = min(cum.min(), (cum_prev * (1 + wk)).min()) - 1.0
rows.append((day, cum[-1] - 1.0, min(m, cum[-1] - 1.0)))
out = pd.DataFrame(rows, columns=["day", "R", "m"]).set_index("day")
return out, legs
# ------------------------------------------------------------------ Monte Carlo
def _paths(R: np.ndarray, gap: np.ndarray, n_days: int, n_paths: int, block: int = 20,
drift_scale: float = 1.0) -> tuple[np.ndarray, np.ndarray]:
"""Bootstrap a blocchi sulle TUPLE (R, gap) — il wick resta accoppiato al suo giorno."""
mu = R.mean()
R_adj = (R - mu) + mu * drift_scale
n = len(R_adj)
n_blocks = int(np.ceil(n_days / block))
starts = RNG.integers(0, n - block, size=(n_paths, n_blocks))
idx = (starts[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1)[:, :n_days]
return R_adj[idx], gap[idx]
def _sim_eval(R: np.ndarray, m: np.ndarray, target: float, max_dd: float,
daily_loss: float, max_days: int, intraday: bool) -> dict:
"""Eval: pass se la chiusura tocca 1+target prima di un breach. Breach su minimo
intraday (m) se intraday=True, altrimenti su chiusura (R) come nel MC vecchio.
Pareggio stesso giorno -> vince il breach (il minimo precede quasi sempre la chiusura)."""
worst = m if intraday else R
eq = np.cumprod(1 + R, axis=1)
eq_start = eq / (1 + R)
dd_breach = eq_start * (1 + worst) < (1 - max_dd)
dl_breach = worst < -daily_loss
fail = dd_breach | dl_breach
passed = eq >= 1 + target
first_fail = np.where(fail.any(axis=1), fail.argmax(axis=1), max_days + 1)
first_pass = np.where(passed.any(axis=1), passed.argmax(axis=1), max_days + 1)
ok = first_pass < first_fail
return {"p_pass": ok.mean(),
"median_days": float(np.median(first_pass[ok])) if ok.any() else np.nan}
RULES = [
("HYRO 1-step: tgt 10% / DD 6% st / daily 4%", 0.10, 0.06, 0.04),
("BREAKOUT Classic: tgt 10% / DD 6% st / dl 3%", 0.10, 0.06, 0.03),
("FTMO Swing: tgt 10% / DD 10% st / daily 5%", 0.10, 0.10, 0.05),
]
FUNDED_RULES = [("HYRO (max loss 6% st, daily 4%)", 0.06, 0.04),
("FTMO (max loss 10% st, daily 5%)", 0.10, 0.05)]
# biglietti HyroTrader 1-step (ricerca R2): fee rimborsata al primo payout se funded
TICKETS = [(25_000, 249.0), (50_000, 379.0), (100_000, 579.0)]
def main() -> None:
print("costruzione MTM intraday del book (TP01 daily + SKH01 230m exit-al-livello, 1h)...")
intr, legs = book_intraday_days()
intr = intr[intr.index >= "2019-03-01"]
R = intr["R"].to_numpy()
m = intr["m"].to_numpy()
gap = m - R
# ---- sanity vs sleeve ufficiali
port = StrategyPortfolio(deribit_book_sleeves())
book = port.combined_daily().dropna()
book.index = pd.to_datetime(book.index, utc=True).floor("D")
book = book[book.index >= "2019-03-01"]
both = intr.index.intersection(book.index)
corr = float(np.corrcoef(R[intr.index.isin(both)], book.loc[both].to_numpy())[0, 1])
mm_rec = metrics(intr.loc[both, "R"])
mm_book = metrics(book.loc[both])
tp_daily = ((legs["tp_BTC"] + legs["tp_ETH"]).groupby(
(legs["tp_BTC"] + legs["tp_ETH"]).index.floor("D")).apply(lambda g: float(np.prod(1 + g) - 1)))
skh_daily = ((legs["skh_BTC"] + legs["skh_ETH"]).groupby(
(legs["skh_BTC"] + legs["skh_ETH"]).index.floor("D")).apply(lambda g: float(np.prod(1 + g) - 1)))
print("=" * 100)
print(" RICOSTRUZIONE MTM — sanity e geometria dei wick")
print("=" * 100)
print(f" giorni: {len(both)} ({both[0].date()} -> {both[-1].date()})")
print(f" corr daily recon vs book certificato: {corr:.3f} "
"(SKH sleeve = equity a scalino per-trade -> <1 atteso)")
print(f" Sharpe/maxDD recon MTM {mm_rec['sharpe']:.2f}/{mm_rec['maxdd']:.1%} "
f"vs book certificato {mm_book['sharpe']:.2f}/{mm_book['maxdd']:.1%}")
sl_tot = {}
for s in port.sleeves:
sd = s.daily()
sd.index = pd.to_datetime(sd.index, utc=True).floor("D")
sl_tot[s.name] = float(np.log1p(sd[sd.index >= "2019-03-01"]).sum())
print(f" log-total leg TP: recon {np.log1p(tp_daily).sum():+.3f} vs sleeve x{W_TP} "
f"{sl_tot['TP01_trend_1d'] * W_TP:+.3f} | leg SKH: recon {np.log1p(skh_daily).sum():+.3f} "
f"vs sleeve x{W_SKH} {sl_tot['SKH01_skyhook'] * W_SKH:+.3f}")
q = np.percentile(gap, [50, 10, 1])
print(f" gap wick (min intraday - chiusura): p50 {q[0]*100:.2f}pp p10 {q[1]*100:.2f}pp "
f"p1 {q[2]*100:.2f}pp worst {gap.min()*100:.2f}pp")
print(f" giorni con wick oltre -2pp sotto la chiusura: {(gap < -0.02).mean():.1%}")
if corr < 0.80 or abs(mm_rec["sharpe"] - mm_book["sharpe"]) > 0.5:
print(" !! recon troppo lontana dal book certificato: NON credere ai numeri sotto")
n_paths, max_days = 20_000, 365
print("\n" + "=" * 100)
print(" EVAL MC — regole sui WICK INTRADAY vs sole chiusure (stesse path, stesso seed)")
print("=" * 100)
for drift_scale, lab in ((1.0, "book modellato"), (0.6, "de-luck x0.6 (onesto)")):
Rp, Gp = _paths(R, gap, max_days, n_paths, drift_scale=drift_scale)
for lev in (1.0, 1.5, 2.0):
R_l, m_l = Rp * lev, (Rp + Gp) * lev
print(f"\n --- {lab}, leva x{lev:.1f} ---")
for name, tgt, dd, dl in RULES:
a = _sim_eval(R_l, m_l, tgt, dd, dl, max_days, intraday=False)
b = _sim_eval(R_l, m_l, tgt, dd, dl, max_days, intraday=True)
d = f"{b['median_days']:.0f}g" if np.isfinite(b["median_days"]) else "n/a"
print(f" {name:<44} close {a['p_pass']:>5.1%} -> intraday {b['p_pass']:>5.1%} "
f"(wick {100*(b['p_pass']-a['p_pass']):+.1f}pp) mediana {d}")
print("\n" + "=" * 100)
print(" FASE FUNDED intraday-aware — $100k, split 80%, 1 anno, SWEEP DI LEVA")
print(" (bust=0 payout: conservativo; con DD 6% la domanda giusta e' se girare SOTTO 1x)")
print("=" * 100)
ev_inputs = {}
for drift_scale, lab in ((1.0, "book modellato"), (0.6, "de-luck x0.6")):
Rp, Gp = _paths(R, gap, 365, n_paths, drift_scale=drift_scale)
for fname, dd_lim, dl_lim in FUNDED_RULES:
best = (None, -1.0)
for lev in (0.5, 0.75, 1.0, 1.25):
Rl, ml = Rp * lev, (Rp + Gp) * lev
eq = np.cumprod(1 + Rl, axis=1)
eq_start = eq / (1 + Rl)
blown = (eq_start * (1 + ml) < 1 - dd_lim) | (ml < -dl_lim)
alive = ~blown.any(axis=1)
pnl = (eq[:, -1] - 1) * 100_000 * 0.80
qq = np.percentile(pnl[alive], [10, 50, 90]) if alive.any() else [np.nan] * 3
e_payout = float(np.where(alive, np.maximum(pnl, 0.0), 0.0).mean())
if e_payout > best[1]:
best = (lev, e_payout)
print(f" {lab:<16} {fname:<32} lev {lev:.2f} P(vivo 1a) {alive.mean():>5.1%} "
f"p10/p50/p90 EUR {qq[0]/EURUSD/365:>5.1f} / {qq[1]/EURUSD/365:>5.1f} / "
f"{qq[2]/EURUSD/365:>5.1f} /g E[payout] ${e_payout:,.0f}")
print(f" {'':<16} {fname:<32} -> lev ottima {best[0]:.2f} (E[payout] ${best[1]:,.0f})")
if fname.startswith("HYRO"):
ev_inputs[lab] = best[1]
print("\n" + "=" * 100)
print(" EV DEL BIGLIETTO (HYRO: eval a leva x1.0, funded a leva ottima; regole intraday;")
print(" fee rimborsata al pass)")
print("=" * 100)
for drift_scale, lab in ((1.0, "book modellato"), (0.6, "de-luck x0.6")):
Rp, Gp = _paths(R, gap, max_days, n_paths, drift_scale=drift_scale)
R_l, m_l = Rp * 1.0, (Rp + Gp) * 1.0
res = _sim_eval(R_l, m_l, 0.10, 0.06, 0.04, max_days, intraday=True)
p = res["p_pass"]
e100 = ev_inputs[lab]
print(f"\n {lab}: P(pass) {p:.1%}")
for size, fee in TICKETS:
e_pay = e100 * size / 100_000
ev = p * (e_pay + fee) - fee
cost_funded = fee / p if p > 0 else np.nan
print(f" ${size//1000}k (fee ${fee:.0f}): EV 1a = ${ev:>8,.0f} "
f"costo atteso per arrivare funded ${cost_funded:,.0f} "
f"P(perdere la fee) {1-p:.0%}")
print("\n NB: EV lordo di tasse (33%) e attriti fuori-MC (consistency, recycling bust,")
print(" controparte non regolata). Bust funded = payout 0 (prelievi settimanali reali")
print(" lo migliorano). I wick sono a risoluzione 1h: i 5m sarebbero ~piu' severi.")
if __name__ == "__main__":
main()
+206
View File
@@ -0,0 +1,206 @@
"""r0724_goal50_math — la matematica ONESTA del target 50 EUR/giorno (2026-07-24).
Domanda del goal: "rendita aggiuntiva che entro qualche mese arrivi a 50 EUR/giorno".
Questo script NON cerca un edge nuovo: quantifica le vie possibili col book che ABBIAMO,
usando i rendimenti giornalieri REALI del book Deribit (TP01 0.75 + SKH01 0.25) e
scenari de-luckati. Quattro parti:
A) IL MURO — capitale richiesto per 50 EUR/g ai CAGR onesti; tempo da 600$ con depositi.
B) LA LEVA NON E' LA SCORCIATOIA — vol/DD implicati dal target su capitale piccolo.
C) PROP-FIRM EVAL — Monte Carlo (block bootstrap dei ritorni reali del book) di una
evaluation tipo crypto-prop: P(pass), giorni mediani, poi fase funded su 100k$:
distribuzione EUR/giorno e P(perdere il conto entro 1 anno).
D) VAULT / COPY — TVL necessario per 50 EUR/g a perf-share 10%.
Onesta': i ritorni del book sono MODELLATI (il path live SKH01 e' peggiore: FULL 1.19/
HOLD 1.15/DD~25% vs modellato ~1.4-1.6) -> ogni scenario gira anche con haircut sul
drift (x0.6) per il de-luck. EURUSD assunto 1.09 (parametro).
"""
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 StrategyPortfolio, metrics # noqa: E402
from src.portfolio.sleeves import deribit_book_sleeves # noqa: E402
EURUSD = 1.09 # 1 EUR = 1.09 USD (assunzione dichiarata)
TARGET_EUR_DAY = 50.0
TARGET_EUR_YR = TARGET_EUR_DAY * 365
TARGET_USD_YR = TARGET_EUR_YR * EURUSD
RNG = np.random.default_rng(724)
def eur_day(usd_yr: float) -> float:
return usd_yr / EURUSD / 365.0
# ----------------------------------------------------------------------------- A
def part_a() -> None:
print("=" * 100)
print(f" A) IL MURO — 50 EUR/g = {TARGET_EUR_YR:,.0f} EUR/anno = ${TARGET_USD_YR:,.0f}/anno")
print("=" * 100)
print(" capitale richiesto (rendita = capitale x CAGR netto):")
for cagr in (0.08, 0.10, 0.15, 0.20, 0.25):
cap = TARGET_USD_YR / cagr
print(f" CAGR {cagr:>4.0%} -> capitale ${cap:>10,.0f} (~EUR {cap / EURUSD:,.0f})")
print("\n tempo per arrivarci da $600 (compounding mensile + deposito mensile, CAGR 15%):")
for dep in (0, 500, 1000, 2000, 5000):
eq, months = 600.0, 0
target_cap = TARGET_USD_YR / 0.15
while eq < target_cap and months < 12 * 60:
eq = eq * (1 + 0.15) ** (1 / 12) + dep * EURUSD
months += 1
yrs = months / 12
lab = f"{yrs:.1f} anni" if months < 720 else ">60 anni"
print(f" deposito EUR {dep:>5}/mese -> {lab}")
print("\n VERDETTO A: da $600, 'qualche mese' per compounding NON esiste. Il capitale")
print(" proprio arriva al target solo con depositi da migliaia/mese per molti anni.")
# ----------------------------------------------------------------------------- B
def part_b(book: pd.Series) -> None:
m = metrics(book)
print("\n" + "=" * 100)
print(" B) LA LEVA NON E' LA SCORCIATOIA")
print("=" * 100)
sh = m["sharpe"]
vol = book.std() * np.sqrt(365)
cagr = (1 + book).prod() ** (365 / len(book)) - 1
print(f" book Deribit (TP01 .75 + SKH01 .25) modellato: Sharpe {sh:.2f} vol {vol:.0%} CAGR {cagr:.1%}")
for cap_usd in (600, 2000, 5000, 20000):
need = TARGET_USD_YR / cap_usd # rendimento annuo richiesto
lev = need / max(cagr, 1e-9) # multiplo di leva sul book
vol_imp = vol * lev
print(f" su ${cap_usd:>6,}: servono {need:>7.0%}/anno -> leva ~{lev:>5.1f}x il book "
f"-> vol implicita ~{vol_imp:>6.0%} -> rovina quasi certa" if lev > 3 else
f" su ${cap_usd:>6,}: servono {need:>7.0%}/anno -> leva ~{lev:>5.1f}x il book "
f"-> vol implicita ~{vol_imp:>6.0%}")
print(" VERDETTO B: il target su capitale piccolo implica vol a 3 cifre = rovina.")
print(" (Kelly gia' documentato: il book gira a ~0.01-0.27 Kelly a seconda dello sleeve.)")
# ----------------------------------------------------------------------------- C
def _block_bootstrap_paths(r: np.ndarray, n_days: int, n_paths: int, block: int = 20,
drift_scale: float = 1.0) -> np.ndarray:
"""Percorsi bootstrap a blocchi dai ritorni giornalieri reali del book.
drift_scale < 1 = haircut de-luck: riduce la media conservando la vol
(r' = (r - mu) + mu*scale).
"""
mu = r.mean()
r_adj = (r - mu) + mu * drift_scale
n = len(r_adj)
n_blocks = int(np.ceil(n_days / block))
starts = RNG.integers(0, n - block, size=(n_paths, n_blocks))
idx = (starts[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1)[:, :n_days]
return r_adj[idx]
def _simulate_eval(paths: np.ndarray, target: float, max_dd: float, daily_loss: float,
trailing: bool, max_days: int) -> dict:
"""Simula una evaluation: passa se equity tocca (1+target) prima di violare
il max drawdown (statico da 1.0 o trailing dal max) o il daily loss limit."""
n_paths, n_days = paths.shape
eq = np.cumprod(1 + paths, axis=1)
hwm = np.maximum.accumulate(np.maximum(eq, 1.0), axis=1) if trailing else 1.0
dd_breach = eq < (hwm * (1 - max_dd) if trailing else (1 - max_dd))
dl_breach = paths < -daily_loss
fail = dd_breach | dl_breach
passed = eq >= 1 + target
first_fail = np.where(fail.any(axis=1), fail.argmax(axis=1), max_days + 1)
first_pass = np.where(passed.any(axis=1), passed.argmax(axis=1), max_days + 1)
ok = first_pass < first_fail
p_pass = ok.mean()
days = np.median(first_pass[ok]) if ok.any() else np.nan
return {"p_pass": p_pass, "median_days": days,
"p_fail": (first_fail <= np.minimum(first_pass, max_days)).mean()}
def part_c(book: pd.Series) -> None:
print("\n" + "=" * 100)
print(" C) PROP-FIRM EVALUATION — Monte Carlo sui ritorni REALI del book (block bootstrap 20g)")
print("=" * 100)
r = book.to_numpy()
n_paths, max_days = 20_000, 365
# regole VERE verificate (ricerca web R2, 2026-07-24): le firm crypto-native danno
# DD 3-6%, NON 10% — e' il vincolo binding per un book con maxDD storico 9.4%.
rules = [
("HYRO 1-step: tgt 10% / DD 6% st / daily 4%", 0.10, 0.06, 0.04, False),
("BREAKOUT Classic: tgt 10% / DD 6% st / dl 3%", 0.10, 0.06, 0.03, False),
("FTMO Swing: tgt 10% / DD 10% st / daily 5%", 0.10, 0.10, 0.05, False),
]
for drift_scale, lab in ((1.0, "book modellato"), (0.6, "de-luck x0.6 (onesto)")):
base = _block_bootstrap_paths(r, max_days, n_paths, drift_scale=drift_scale)
for lev in (1.0, 1.5, 2.0):
paths = base * lev
print(f"\n --- {lab}, leva eval x{lev:.1f} (vol ~{book.std() * np.sqrt(365) * lev:.0%}) ---")
for name, tgt, dd, dl, tr in rules:
res = _simulate_eval(paths, tgt, dd, dl, tr, max_days)
d = f"{res['median_days']:.0f}g" if np.isfinite(res["median_days"]) else "n/a"
print(f" {name:<44} P(pass entro 1a) {res['p_pass']:>5.1%} mediana {d}")
# fase funded: 100k, split 80%, anno di ritorni; regole di bust VERE (R2):
# HYRO = max loss 6% statico + daily 4% (vol nativa book 11% -> gia' nel range
# 10-12% raccomandato); FTMO = 10% statico + daily 5%.
print("\n --- fase FUNDED: $100k nozionale, split 80%, orizzonte 1 anno ---")
funded_rules = [("HYRO (max loss 6% st, daily 4%)", 0.06, 0.04),
("FTMO (max loss 10% st, daily 5%)", 0.10, 0.05)]
for drift_scale, lab in ((1.0, "book modellato"), (0.6, "de-luck x0.6")):
paths = _block_bootstrap_paths(r, 365, n_paths, drift_scale=drift_scale)
eq = np.cumprod(1 + paths, axis=1)
for fname, dd_lim, dl_lim in funded_rules:
blown = (eq < 1 - dd_lim) | (paths < -dl_lim)
alive = ~blown.any(axis=1)
pnl = (eq[:, -1] - 1) * 100_000 * 0.80
pnl_alive = pnl[alive]
q = np.percentile(pnl_alive, [10, 50, 90]) if alive.any() else [np.nan] * 3
print(f" {lab:<16} {fname:<32} P(vivo 1a) {alive.mean():>5.1%} "
f"payout p10/p50/p90: EUR {eur_day(q[0]):>5.1f} / {eur_day(q[1]):>5.1f} / "
f"{eur_day(q[2]):>5.1f} /g")
print("\n NB (ricerca R2 completata 2026-07-24): HYRO = API reale Bybit da funded, consistency")
print(" 40% solo in eval; BREAKOUT = carry nascosto 0.033%/g (~12%/anno) ostile ai multi-week")
print(" hold, esecuzione manuale ok a cadenza 1d; FTMO = CFD con swap ~10-25%/anno. Questi")
print(" attriti + recycling bust + controparte non regolata NON sono nel MC: l'atteso blended")
print(" onesto a $100k funded e' ~250-450 EUR/MESE (8-15/g), 15-30/g al cap $200k.")
# ----------------------------------------------------------------------------- D
def part_d() -> None:
print("\n" + "=" * 100)
print(" D) VAULT / COPY-TRADING — perf-share 10% sopra HWM")
print("=" * 100)
for cagr in (0.10, 0.15, 0.20):
tvl = TARGET_USD_YR / (cagr * 0.10)
print(f" CAGR {cagr:.0%}: per 50 EUR/g servono ${tvl / 1e6:,.1f}M di TVL altrui")
print(" VERDETTO D: TVL a 7 cifre richiede anni di track record pubblico, non mesi.")
def main() -> None:
port = StrategyPortfolio(deribit_book_sleeves())
book = port.combined_daily().dropna()
book = book[book.index >= "2019-03-01"]
m = metrics(book)
print(f"book Deribit live (modellato): {len(book)} giorni {book.index[0].date()} -> "
f"{book.index[-1].date()} Sharpe {m['sharpe']:.2f} maxDD {m['maxdd']:.1%}")
part_a()
part_b(book)
part_c(book)
part_d()
print("\n" + "=" * 100)
print(" SINTESI ONESTA: 50 EUR/g in qualche mese e' fuori portata del capitale proprio.")
print(" L'unica via quantificabile in MESI e' moltiplicare il nozionale gestito (prop/")
print(" funded), con P(successo) e regole da verificare; tutto il resto e' anni+capitale.")
print("=" * 100)
if __name__ == "__main__":
main()
+291
View File
@@ -0,0 +1,291 @@
"""r0724_hlp_deepdive — Deep-dive ALLOCATION sul vault HLP di Hyperliquid.
Contesto (2026-07-24): il probe HLP (89-95 punti ~14g dall'API nativa, vedi
`data/external/hlp_vault.json`) ha mostrato l'unico stream crash-long mai visto dal
progetto (corr col book -0.19, +10.4% nel cascade ott-2025). Questo script risponde
alle domande aperte con dati reali:
1. GRANULARITA' DAILY — l'API nativa (`vaultDetails`/`portfolio`) e' downsampled:
allTime ~14g (95 pti), month ~10h ma SOLO ultimi 30g, week ~2.4h ultimi 7g
→ nessun daily storico ufficiale. Fonti esterne trovate:
- CoinGecko `wrapped-hlp` (wHLP di Hyperbeat, redimibile a NAV): prezzo DAILY
dal 2025-07-25 → oggi. E' un prezzo di mercato (puo' fare sconto sotto stress),
ma e' l'unico mark giornaliero di lungo periodo liberamente accessibile.
- DefiLlama `protocol/hyperliquid-hlp`: TVL DAILY dal 2024-12 (per il modello
di diluizione return-vs-TVL).
- Thunderhead (cloudfront d2v1fiwobg9w6): `hlp_liquidator_pnl` DAILY, ma solo
2025-03-05 → 2025-07-12 (stantio; copre pero' il 12-mar e JELLY 26-mar-2025)
e `hlp_positions` (esposizione daily per coin dal 2023-06, per la leva).
- MORTI: ASXN api-hyperliquid.asxn.xyz `/hlp_pnl` (daily completo, dietro
Turnstile anti-bot); DefiLlama yields (HLP non ha token → non e' un pool);
stats-data.hyperliquid.xyz espone solo `Mainnet/vaults` (stessi dati downsampled).
2. CODA — ricostruzione numerica 12-mar-2025 (whale ETH, -$4.28M realizzati in
1 giorno dal liquidator) e JELLY 26-mar-2025 (unrealized peak ~-$13.5M su TVL
~$240M ≈ -5.6%, salvato da delist+settlement dei validator a $0.0095 → +$703k
realizzati). Vedi diario per il worst-case ragionato.
3-5. Fisco/accesso e allocation math: nel diario. Qui i numeri.
Uso:
uv run python scripts/research/r0724_hlp_deepdive.py # usa cache se c'e'
uv run python scripts/research/r0724_hlp_deepdive.py --refresh # ri-scarica
I fetch sono salvati in data/external/hlp_deepdive/ (gitignored di fatto: non
committare i dati). Nessuna azione live: SOLO analisi.
"""
from __future__ import annotations
import argparse
import json
import sys
import urllib.request
from datetime import datetime, timezone
from pathlib import Path
import numpy as np
import pandas as pd
ROOT = Path(__file__).resolve().parents[2]
EXT = ROOT / "data" / "external"
CACHE = EXT / "hlp_deepdive"
CACHE.mkdir(parents=True, exist_ok=True)
VAULT = "0xdfc24b077bc1425ad1dea75bcb6f8158e10df303"
SOURCES = {
# prezzo daily wHLP (Hyperbeat Wrapped HLP) — mark di mercato del NAV HLP
"whlp_coingecko.json": (
"https://api.coingecko.com/api/v3/coins/wrapped-hlp/market_chart"
"?vs_currency=usd&days=365&interval=daily"
),
# TVL daily del vault HLP
"llama_hlp_tvl.json": "https://api.llama.fi/protocol/hyperliquid-hlp",
# PnL daily del liquidator HLP (stantio: 2025-03-05 → 2025-07-12, copre JELLY)
"thunderhead_liq_pnl.json": "https://d2v1fiwobg9w6.cloudfront.net/hlp_liquidator_pnl",
}
def fetch(name: str, url: str, refresh: bool) -> dict | list | None:
p = CACHE / name
if p.exists() and not refresh:
return json.loads(p.read_text())
try:
req = urllib.request.Request(url, headers={"User-Agent": "pythagoras-research/1.0"})
raw = urllib.request.urlopen(req, timeout=60).read()
p.write_bytes(raw)
return json.loads(raw)
except Exception as e: # noqa: BLE001
print(f" [WARN] fetch {name} fallito ({e}); uso cache se esiste")
return json.loads(p.read_text()) if p.exists() else None
def refresh_vault_details(refresh: bool) -> dict:
"""Snapshot vaultDetails dall'API nativa (stesso formato del probe originale)."""
p = EXT / "hlp_vault.json"
if p.exists() and not refresh:
return json.loads(p.read_text())
body = json.dumps({"type": "vaultDetails", "vaultAddress": VAULT}).encode()
req = urllib.request.Request(
"https://api.hyperliquid.xyz/info", data=body,
headers={"Content-Type": "application/json"},
)
raw = urllib.request.urlopen(req, timeout=60).read()
p.write_bytes(raw)
return json.loads(raw)
def series_from_period(d: dict, period: str) -> pd.DataFrame:
for per, pdata in d["portfolio"]:
if per == period:
av = pd.DataFrame(pdata["accountValueHistory"], columns=["ts", "av"])
pnl = pd.DataFrame(pdata["pnlHistory"], columns=["ts", "pnl"])
df = av.merge(pnl, on="ts")
df["t"] = pd.to_datetime(df["ts"], unit="ms", utc=True)
df["av"] = df["av"].astype(float)
df["pnl"] = df["pnl"].astype(float) # PnL CUMULATIVO
return df.set_index("t")[["av", "pnl"]]
raise KeyError(period)
def ret_stats(r: pd.Series, periods_per_year: float, label: str) -> dict:
r = r.replace([np.inf, -np.inf], np.nan).dropna()
mu, sd = r.mean(), r.std()
sharpe = mu / sd * np.sqrt(periods_per_year) if sd > 0 else np.nan
eq = (1 + r).cumprod()
dd = (eq / eq.cummax() - 1).min()
ann = (1 + mu) ** periods_per_year - 1
out = dict(label=label, n=len(r), ann_ret=ann, sharpe=sharpe, maxdd=dd,
worst=r.min(), worst_t=str(r.idxmin())[:10], best=r.max())
print(f" {label:28s} n={out['n']:4d} ann~{ann*100:6.1f}% Sh={sharpe:5.2f} "
f"maxDD={dd*100:5.1f}% worst={r.min()*100:6.2f}% ({out['worst_t']})")
return out
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--refresh", action="store_true")
args = ap.parse_args()
print("=" * 78)
print("HLP DEEP-DIVE — dati al", datetime.now(timezone.utc).isoformat()[:16])
print("=" * 78)
d = refresh_vault_details(args.refresh)
print(f"\nVault: {d['name']} APR corrente dichiarato: {d['apr']*100:.3f}%")
print(f"isClosed={d['isClosed']} allowDeposits={d['allowDeposits']} "
f"maxDistributable=${d['maxDistributable']/1e6:.0f}M")
# ---------------- A. serie nativa 14g (allTime) ----------------
print("\n[A] Serie NATIVA allTime (~14g/punto) — return su AUM di inizio periodo")
at = series_from_period(d, "allTime")
at["dpnl"] = at["pnl"].diff()
at["ret"] = at["dpnl"] / at["av"].shift(1)
at.loc[at["av"].shift(1) < 5e6, "ret"] = np.nan # AUM<5M: return non significativo
ret14 = at["ret"].iloc[1:]
ret_stats(ret14, 365.25 / 14, "HLP 14g FULL (AUM>5M)")
for y in (2023, 2024, 2025, 2026):
sub = ret14[ret14.index.year == y]
if len(sub) > 3:
ret_stats(sub, 365.25 / 14, f" anno {y}")
print(" Traiettoria 2026 (per periodo 14g):",
" ".join(f"{x*100:+.1f}" for x in ret14[ret14.index.year == 2026].dropna()))
worst5 = ret14.dropna().nsmallest(5)
print(" 5 peggiori periodi 14g:", [(str(i.date()), f"{v*100:+.2f}%") for i, v in worst5.items()])
# month nativo (~10h granularita', ultimi 30g)
mo = series_from_period(d, "month")
mo_ret = mo["pnl"].diff() / mo["av"].shift(1)
tot30 = (1 + mo_ret.dropna()).prod() - 1
print(f" Ultimi 30g (periodo 'month', {len(mo)} pti ~10h): tot {tot30*100:+.2f}%")
# ---------------- B. wHLP daily (CoinGecko) ----------------
print("\n[B] wHLP (CoinGecko, prezzo DAILY di mercato ~ NAV; sconto possibile)")
cg = fetch("whlp_coingecko.json", SOURCES["whlp_coingecko.json"], args.refresh)
if cg and cg.get("prices"):
pr = pd.DataFrame(cg["prices"], columns=["ts", "px"])
pr["t"] = pd.to_datetime(pr["ts"], unit="ms", utc=True).dt.normalize()
pr = pr.drop_duplicates("t").set_index("t")["px"].astype(float)
r1d = pr.pct_change()
ret_stats(r1d, 365.25, "wHLP daily FULL (12 mesi)")
for lbl, a, b in [
("cascade 2025-10-09→13", "2025-10-09", "2025-10-13"),
("feb-2026 (liq event)", "2026-02-01", "2026-02-28"),
("ultimi 45g", str(pr.index[-1] - pd.Timedelta(days=45))[:10], None),
]:
w = pr.loc[a:b] if b else pr.loc[a:]
if len(w) > 1:
print(f" {lbl:26s} {w.iloc[0]:.4f}{w.iloc[-1]:.4f} "
f"({(w.iloc[-1]/w.iloc[0]-1)*100:+.2f}%) min {w.min():.4f}")
peak = pr.cummax()
cur_dd = pr.iloc[-1] / peak.iloc[-1] - 1
print(f" Drawdown CORRENTE dal max ({pr.idxmax().date()} {pr.max():.4f}): "
f"{cur_dd*100:+.2f}%")
# ---------------- C. TVL daily (DefiLlama) → diluizione ----------------
print("\n[C] TVL daily (DefiLlama hyperliquid-hlp) + fit return-vs-TVL")
ll = fetch("llama_hlp_tvl.json", SOURCES["llama_hlp_tvl.json"], args.refresh)
if ll and ll.get("tvl"):
tvl = pd.DataFrame(ll["tvl"])
tvl["t"] = pd.to_datetime(tvl["date"], unit="s", utc=True).dt.normalize()
tvl = tvl.drop_duplicates("t").set_index("t")["totalLiquidityUSD"].astype(float)
print(f" TVL {tvl.index[0].date()} ${tvl.iloc[0]/1e6:.0f}M → picco "
f"{tvl.idxmax().date()} ${tvl.max()/1e6:.0f}M → oggi ${tvl.iloc[-1]/1e6:.0f}M "
f"({(tvl.iloc[-1]/tvl.max()-1)*100:+.0f}% dal picco)")
# fit sul nativo: PnL$ 14g vs AUM inizio periodo (tutta la storia)
x = at["av"].shift(1).iloc[1:] / 1e6
y = at["dpnl"].iloc[1:] / 1e6
ok = x.notna() & y.notna()
b1, b0 = np.polyfit(x[ok], y[ok], 1)
corr_xy = np.corrcoef(x[ok], y[ok])[0, 1]
print(f" Fit PnL$_14g = {b0:+.2f}M {b1:+.4f}·TVL(M) corr={corr_xy:+.2f} "
f"(b1≈0 ⇒ PnL$ NON scala col TVL ⇒ return-on-AUM ∝ 1/TVL = diluizione)")
for lo, hi in [(0, 150), (150, 300), (300, 450), (450, 700)]:
m = (x >= lo) & (x < hi)
if m.sum() >= 4:
impl = (y[m].mean() / x[m].mean()) * (365.25 / 14) * 100
print(f" TVL {lo:3d}-{hi:3d}M: n={m.sum():3d} PnL medio "
f"${y[m].mean()*1000:+7.0f}k/14g → ~{impl:+.1f}%/anno su AUM")
# ---------------- D. liquidator daily (thunderhead) — la coda ----------------
print("\n[D] Liquidator PnL DAILY (thunderhead, 2025-03-05→2025-07-12 — copre JELLY)")
th = fetch("thunderhead_liq_pnl.json", SOURCES["thunderhead_liq_pnl.json"], args.refresh)
if th and th.get("chart_data"):
liq = pd.DataFrame(th["chart_data"]).dropna()
liq["t"] = pd.to_datetime(liq["time"], utc=True)
liq = liq.set_index("t")["total_pnl"].astype(float)
w = liq.sort_values()
print(" 5 peggiori giorni:", [(str(i.date()), f"{v/1e6:+.2f}M") for i, v in w.head(5).items()])
print(" 5 migliori giorni:", [(str(i.date()), f"{v/1e6:+.2f}M") for i, v in w.tail(5).items()])
for day, note in [("2025-03-12", "whale ETH 50x"), ("2025-03-26", "JELLY settle")]:
if day in liq.index.strftime("%Y-%m-%d").tolist():
v = liq[liq.index.strftime("%Y-%m-%d") == day].iloc[0]
print(f" {day} ({note}): {v/1e6:+.2f}M")
# ---------------- F. decomposizioni oneste ----------------
print("\n[F] Decomposizioni oneste")
# carry 2026 ex-evento: togli il singolo periodo +7% (evento liquidazione feb-2026)
r26 = ret14[ret14.index.year == 2026].dropna()
if len(r26):
ev = r26.idxmax()
ex = r26.drop(ev)
cum_ex = (1 + ex).prod() - 1
ann_ex = (1 + cum_ex) ** (365.25 / (14 * len(ex))) - 1
print(f" 2026: evento {ev.date()} {r26.max()*100:+.1f}% | ex-evento cum "
f"{cum_ex*100:+.2f}% su {len(ex)} periodi ≈ {ann_ex*100:+.1f}%/anno "
f"→ IL CARRY E' MORTO, resta solo il crash-alpha")
# TVL al giorno JELLY (per scalare il -13.5M unrealized)
if ll and ll.get("tvl"):
for day in ("2025-03-12", "2025-03-26", "2025-10-10", "2026-02-08"):
ts = pd.Timestamp(day, tz="UTC")
i = tvl.index.get_indexer([ts], method="nearest")[0]
print(f" TVL {day}: ${tvl.iloc[i]/1e6:.0f}M "
f"(-13.5M JELLY = {-13.5e6/tvl.iloc[i]*100:.1f}% MtM)" if day == "2025-03-26"
else f" TVL {day}: ${tvl.iloc[i]/1e6:.0f}M")
# sconto wrapper: wHLP mercato vs NAV nativo su finestra comune (da max wHLP a oggi)
if cg and cg.get("prices"):
w_from = pr.idxmax()
nav_win = ret14[ret14.index >= w_from].dropna()
nav_chg = (1 + nav_win).prod() - 1
whlp_chg = pr.iloc[-1] / pr.max() - 1
print(f" Da {w_from.date()}: wHLP mercato {whlp_chg*100:+.2f}% vs NAV nativo "
f"{nav_chg*100:+.2f}% → sconto wrapper ≈ {(whlp_chg-nav_chg)*100:+.1f}pt "
f"(costo di un exit-in-stress via wrapper)")
# ---------------- E. allocation math (Kelly log-utility, outcome discreti) ----------------
print("\n[E] Allocation math — Kelly log-utility su outcome annui discreti")
# Outcome annui dell'ALLOCAZIONE (non del bankroll): base 2 scenari di coda:
# - evento socializzazione stile-JELLY-senza-salvataggio: -20% dell'allocazione
# - morte protocollo/bridge (hack, insolvenza, socializzazione totale): -100%
# mu base = run-rate onesto 2026 (decay in corso: usare 6%, non il 19% del 2025)
scenarios = [
("base mu=6%", [(0.06, 0.83), (-0.20, 0.15), (-1.00, 0.02)]),
("ottimista mu=12%", [(0.12, 0.87), (-0.20, 0.10), (-1.00, 0.03)]),
("pessimista mu=3%", [(0.03, 0.80), (-0.25, 0.17), (-1.00, 0.03)]),
]
for name, outs in scenarios:
mu_adj = sum(x * p for x, p in outs)
fgrid = np.linspace(0.001, 0.999, 999)
util = [sum(p * np.log(1 + f * x) for x, p in outs) for f in fgrid]
f_star = float(fgrid[int(np.argmax(util))])
print(f" {name:20s} mu_adj={mu_adj:+.1%} Kelly pieno f*={f_star:.2f} "
f"0.25·Kelly={0.25*f_star:.1%} del bankroll")
f_use = 0.10 # ~0.25·Kelly dello scenario base, cap operativo
print(f" → allocazione difendibile ~{f_use:.0%} del bankroll (VRP01 fu ~0.27 Kelly):")
for cap in (600, 2000, 5000, 20000):
a = f_use * cap
print(f" capitale ${cap:>6,}: ~${a:,.0f} → atteso ~${a*0.06:,.0f}/anno; "
f"coda -${a*0.20:,.0f} (JELLY-like) / -${a:,.0f} (protocollo)")
print("\nFonti dati:")
for k, v in SOURCES.items():
print(f" {k:28s} {v}")
print(" hlp_vault.json https://api.hyperliquid.xyz/info "
'{"type":"vaultDetails","vaultAddress":"' + VAULT + '"}')
print(" MORTI: ASXN /api/hlp_pnl (Turnstile), DefiLlama yields (HLP non-token), "
"stats-data.hyperliquid.xyz (downsampled)")
if __name__ == "__main__":
sys.exit(main())
+50
View File
@@ -0,0 +1,50 @@
"""r0724_kimchi_skeptic — scettico sul candidato KIMCHI d30-folLF (2026-07-24).
Il candidato usciva da r0724_premium_wave con EARNS_SLOT=True al marginal scorer
(ADDS, robust_oos, persistente ogni anno) ma DSR 0.891<0.95. Due check obbligatori:
(1) PLATEAU: d15/d30/d45/d60 — un edge vero degrada dolcemente, uno spike no;
(2) LAG ESECUZIONE +1g: un segnale di flusso a 30g deve sopravvivere a 24h di ritardo.
ESITO (run 2026-07-24): d30 e' uno SPIKE isolato (0.44/1.07/0.51/0.24) e il lag +1g
lo azzera (hold 0.74->0.14, uplift blend negativo) -> SCARTATO, parameter-luck.
"""
import sys
import pathlib
ROOT = str(pathlib.Path(__file__).resolve().parents[2])
sys.path.insert(0, ROOT)
sys.path.insert(0, ROOT + "/scripts/research/alt")
import numpy as np, pandas as pd
import altlib
from altlib import candidate_daily, tp01_baseline_daily, HOLDOUT
sys.path.insert(0, ROOT + "/scripts/research")
from r0724_premium_wave import _PREM
def _sh(s):
s = s.dropna()
return float(s.mean()/s.std()*np.sqrt(365.25)) if s.std() > 0 else 0.0
def factory(D, extra_lag=0):
def fn(df, asset):
p = _PREM[asset]["kimchi"]
z = (p - p.shift(D)).shift(extra_lag)
pos = (np.sign(z) > 0).astype(float)
days = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)).floor("D")
return np.nan_to_num(pos.reindex(days).values.astype(float))
return fn
B = tp01_baseline_daily()
def blend_uplift(C, w=0.25):
J = pd.concat({"b": B, "c": C}, axis=1, join="inner").fillna(0.0)
bl = (1-w)*J["b"] + w*J["c"]
hb, hbl = J["b"][J.index >= HOLDOUT], bl[bl.index >= HOLDOUT]
return _sh(bl)-_sh(J["b"]), _sh(hbl)-_sh(hb)
print(f"{'cella':<18}{'Sh full':>9}{'Sh hold':>9}{'upl full':>10}{'upl hold':>10}")
for D in (15, 30, 45, 60):
C = candidate_daily(factory(D), tf="1d")
uf, uh = blend_uplift(C)
hold = C[C.index >= HOLDOUT]
print(f"d{D:<3} lag0 {_sh(C):>9.2f}{_sh(hold):>9.2f}{uf:>+10.3f}{uh:>+10.3f}")
C = candidate_daily(factory(30, extra_lag=1), tf="1d")
uf, uh = blend_uplift(C)
hold = C[C.index >= HOLDOUT]
print(f"d30 lag+1g {_sh(C):>9.2f}{_sh(hold):>9.2f}{uf:>+10.3f}{uh:>+10.3f}")
+256
View File
@@ -0,0 +1,256 @@
"""r0724_onchain_wave — prima ondata ON-CHAIN / SENTIMENT (famiglia MAI testata) (2026-07-24).
Goal "trova altre strategie": la famiglia on-chain e' l'unica grande famiglia di
INFORMAZIONE (non di prezzo) mai toccata dal progetto. Fonte segnali: CoinMetrics
Community (github.com/coinmetrics/data, CSV daily dal genesis, gratuito) + Fear&Greed
(alternative.me, dal 2018-02). I RITORNI restano SOLO dal feed certificato Deribit
(lezione v2.0.0: i dati esterni sono segnale, mai prezzo).
5 famiglie, ognuna giudicata con `study_family_honest` (selezione cella IN-SAMPLE,
deflated-Sharpe sull'INTERA griglia, marginal scorer indurito vs TP01):
NET — network-growth momentum (AdrActCnt / TxTfrCnt, stile Liu-Tsyvinski)
MVRV — valuation gate su CapMVRVCur (percentile causale espandente)
EXS — supply su exchange (SplyExNtv: accumulo = coin che LASCIANO gli exchange)
HASH — hash ribbons BTC-only (capitolazione/recovery miner; ETH post-Merge = 0)
FNG — Fear&Greed contrarian (long dopo paura estrema)
CAUSALITA' (doppio lag): la riga CoinMetrics del giorno d si completa a fine giorno d
(+ ore di processing) -> al close del bar daily d (=00:00 UTC di d+1) l'ultima riga
SICURAMENTE nota e' d-1 => segnale shiftato di 1 GIORNO prima del mapping sui bar;
eval_weights shifta di un altro bar (decisione a close[i], hold i+1) => lag totale
attivita'->posizione = 2 giorni. F&G: pubblicato ~00:00 UTC del giorno stesso ->
stesso trattamento conservativo.
CAVEAT DATI (dichiarati): CM community aggiornato al 2026-05-24 (~2 mesi di lag: ok
per ricerca, NON per un deploy senza fonte fresca); metriche exchange-flow = stima
CM dei wallet exchange (proxy, non verita'); F&G e' in parte DERIVATO dal prezzo
(vol+momentum) -> rischio ridondanza col trend, il marginal scorer lo vede.
Uso: `uv run python scripts/research/r0724_onchain_wave.py`
Dati attesi in data/external/coinmetrics/ (cm_btc.csv, cm_eth.csv, fng.json);
per aggiornare: scaricare di nuovo dalle fonti (URL nei commenti di _load_cm/_load_fng).
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
import numpy as np
import pandas as pd
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt"))
import altlib # noqa: E402
from altlib import study_family_honest, fmt_marginal # noqa: E402
EXT = ROOT / "data" / "external" / "coinmetrics"
AVAIL_LAG_D = 1 # giorni di lag di disponibilita' del segnale PRIMA del mapping sui bar
# ------------------------------------------------------------------ dati segnale
def _load_cm(asset: str) -> pd.DataFrame:
"""CoinMetrics community: https://raw.githubusercontent.com/coinmetrics/data/master/csv/{btc,eth}.csv"""
df = pd.read_csv(EXT / f"cm_{asset.lower()}.csv", low_memory=False)
idx = pd.DatetimeIndex(pd.to_datetime(df["time"], utc=True)).floor("D")
out = df.drop(columns=["time"]).apply(pd.to_numeric, errors="coerce")
out.index = idx
return out
def _load_fng() -> pd.Series:
"""alternative.me: https://api.alternative.me/fng/?limit=0&format=json"""
d = json.loads((EXT / "fng.json").read_text())["data"]
ts = pd.to_datetime([int(x["timestamp"]) for x in d], unit="s", utc=True).floor("D")
return pd.Series([float(x["value"]) for x in d], index=ts).sort_index()
def _load_stables() -> pd.Series:
"""DefiLlama: https://stablecoins.llama.fi/stablecoincharts/all -> supply USD totale
stablecoin per giorno (dal 2017-11). Liquidita' 'dry powder' NON derivata dal prezzo BTC."""
d = json.loads((EXT / "stables.json").read_text())
ts = pd.to_datetime([int(x["date"]) for x in d], unit="s", utc=True).floor("D")
v = [float(x.get("totalCirculating", {}).get("peggedUSD", np.nan)) for x in d]
return pd.Series(v, index=ts).sort_index()
_CM = {a: _load_cm(a) for a in ("BTC", "ETH")}
_FNG = _load_fng()
_STB = _load_stables()
def _to_target(df: pd.DataFrame, sig_by_day: pd.Series) -> np.ndarray:
"""Mappa un segnale by-day sui bar del df certificato con lag di disponibilita'.
tz-aware su entrambi i lati (lezione della sera: naive-vs-aware nel reindex = NaN->0
silenziosi)."""
days = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)).floor("D")
known = sig_by_day.shift(AVAIL_LAG_D)
return np.nan_to_num(known.reindex(days).values.astype(float))
def _expanding_pctl(x: pd.Series, minp: int = 365) -> pd.Series:
"""Percentile causale espandente di x[t] nella storia fino a t (incluso)."""
v = x.values.astype(float)
out = np.full(len(v), np.nan)
order: list[float] = []
import bisect
for i, xi in enumerate(v):
if np.isfinite(xi):
bisect.insort(order, xi)
if len(order) >= minp:
out[i] = bisect.bisect_left(order, xi) / len(order)
return pd.Series(out, index=x.index)
# ------------------------------------------------------------------ famiglie
def net_factory(tf: str, metric: str = "AdrActCnt", L: int = 30, mode: str = "LF"):
def fn(df, asset):
m = _CM[asset][metric]
sm = np.log(m.rolling(7, min_periods=4).mean())
sig = sm - sm.shift(L)
pos = np.sign(sig) if mode == "LS" else (sig > 0).astype(float)
return _to_target(df, pos)
return fn
def mvrv_factory(tf: str, lo: float = 0.3, hi: float = 1.1, mode: str = "LF"):
"""LF lo: long solo se MVRV-pctl < lo (compra paura di valuation).
LF hi: long salvo top (pctl < hi). LS: +1 sotto lo, -1 sopra hi."""
def fn(df, asset):
p = _expanding_pctl(_CM[asset]["CapMVRVCur"])
if mode == "LS":
pos = pd.Series(np.where(p < lo, 1.0, np.where(p > hi, -1.0, 0.0)), index=p.index)
else:
pos = (p < (lo if hi > 1.0 else hi)).astype(float) if hi > 1.0 else (p < hi).astype(float)
return _to_target(df, pos)
return fn
def exs_factory(tf: str, L: int = 30, mode: str = "LF"):
def fn(df, asset):
s = np.log(_CM[asset]["SplyExNtv"].where(_CM[asset]["SplyExNtv"] > 0))
sig = -(s - s.shift(L)) # supply che LASCIA gli exchange = accumulo = +
pos = np.sign(sig) if mode == "LS" else (sig > 0).astype(float)
return _to_target(df, pos)
return fn
def hash_factory(tf: str, fast: int = 30, slow: int = 60, mode: str = "ribbon"):
"""BTC-only. ribbon: long da recovery-cross (fast riattraversa sopra slow) fino alla
prossima capitolazione (fast sotto slow). holdH: long per H giorni dal recovery."""
def fn(df, asset):
if asset != "BTC":
return np.zeros(len(df))
h = _CM[asset]["HashRate"]
f, s = h.rolling(fast).mean(), h.rolling(slow).mean()
above = (f > s).astype(float)
if mode == "ribbon":
pos = above # long quando il ribbon e' sano
else: # recovery: long H giorni dal cross-up
H = int(mode[4:])
cross_up = (above.diff() > 0)
pos = cross_up.rolling(H, min_periods=1).max().fillna(0.0)
return _to_target(df, pos)
return fn
def fng_factory(tf: str, lo: int = 20, H: int = 30, mode: str = "fear"):
"""fear: long H giorni dopo F&G < lo (contrarian). regime: long quando media7 > 50."""
def fn(df, asset):
g = _FNG
if mode == "regime":
pos = (g.rolling(7, min_periods=4).mean() > 50).astype(float)
else:
trig = (g < lo)
pos = trig.rolling(H, min_periods=1).max().fillna(0.0)
return _to_target(df, pos)
return fn
def stable_factory(tf: str, L: int = 30, mode: str = "LF", thr: float = 0.0):
"""Crescita della supply stablecoin totale (liquidita' in ingresso nel sistema).
LF: long se crescita L-giorni > thr (annualizzata), flat altrimenti. LS: segno."""
def fn(df, asset):
s = np.log(_STB.where(_STB > 0))
sig = (s - s.shift(L)) * (365.0 / L) - thr
pos = np.sign(sig) if mode == "LS" else (sig > 0).astype(float)
return _to_target(df, pos)
return fn
FAMILIES = [
("STABLE-supply-growth", stable_factory, [
dict(L=30, mode="LF", thr=0.0), dict(L=90, mode="LF", thr=0.0),
dict(L=30, mode="LF", thr=0.10), dict(L=90, mode="LF", thr=0.10),
dict(L=30, mode="LS", thr=0.0), dict(L=90, mode="LS", thr=0.0),
]),
("NET-growth", net_factory, [
dict(metric="AdrActCnt", L=30, mode="LF"), dict(metric="AdrActCnt", L=90, mode="LF"),
dict(metric="AdrActCnt", L=30, mode="LS"), dict(metric="AdrActCnt", L=90, mode="LS"),
dict(metric="TxTfrCnt", L=30, mode="LF"), dict(metric="TxTfrCnt", L=90, mode="LF"),
dict(metric="TxTfrCnt", L=30, mode="LS"), dict(metric="TxTfrCnt", L=90, mode="LS"),
]),
("MVRV-valuation", mvrv_factory, [
dict(lo=0.2, hi=9.9, mode="LF"), dict(lo=0.3, hi=9.9, mode="LF"),
dict(lo=0.0, hi=0.8, mode="LF"), dict(lo=0.0, hi=0.9, mode="LF"),
dict(lo=0.2, hi=0.8, mode="LS"), dict(lo=0.3, hi=0.9, mode="LS"),
]),
("EXS-exchange-supply", exs_factory, [
dict(L=30, mode="LF"), dict(L=90, mode="LF"),
dict(L=30, mode="LS"), dict(L=90, mode="LS"),
]),
("HASH-ribbons-BTC", hash_factory, [
dict(fast=30, slow=60, mode="ribbon"),
dict(fast=30, slow=60, mode="hold60"), dict(fast=30, slow=60, mode="hold120"),
]),
("FNG-fear-greed", fng_factory, [
dict(lo=15, H=10, mode="fear"), dict(lo=15, H=30, mode="fear"),
dict(lo=25, H=10, mode="fear"), dict(lo=25, H=30, mode="fear"),
dict(mode="regime"),
]),
]
def main() -> None:
print("=" * 100)
print(" ONDATA ON-CHAIN / SENTIMENT — 5 famiglie via study_family_honest (gate completi)")
print(f" CM: BTC {_CM['BTC'].index[0].date()}->{_CM['BTC'].index[-1].date()}, "
f"ETH {_CM['ETH'].index[0].date()}->{_CM['ETH'].index[-1].date()} | "
f"F&G {_FNG.index[0].date()}->{_FNG.index[-1].date()}")
print("=" * 100)
n_cells_tot = sum(len(g) for _, _, g in FAMILIES)
print(f" trial totali dichiarati: {n_cells_tot} celle su 5 famiglie "
"(tutte contate nel deflated-Sharpe di famiglia)\n")
results = []
for name, factory, grid in FAMILIES:
print("-" * 100)
rep = study_family_honest(name, factory, grid, tfs=("1d",))
results.append(rep)
if rep.get("chosen") is None:
print(f"=== {name}: nessuna cella valida in-sample")
continue
ch = rep["chosen"]
print(f"=== {name}: cella IS {ch['params']} (IS Sh {ch['insample_sharpe']}, "
f"full {ch['full_sharpe']}) su {rep['n_cells']} celle")
print(f" deflated-Sharpe {rep['deflated_sharpe']} (null-max atteso "
f"{rep['expected_null_max']}) dsr_pass={rep['dsr_pass']}")
print(fmt_marginal(rep["marginal"]) if isinstance(rep["marginal"], str)
else fmt_marginal(rep["marginal"]))
print(f" >>> EARNS_SLOT_HONEST = {rep['earns_slot_honest']}")
print("\n" + "=" * 100)
print(" SINTESI")
print("=" * 100)
for rep in results:
ch = rep.get("chosen")
lab = "no-cell" if ch is None else (
f"IS{ch['params']} dsr={rep.get('deflated_sharpe')} "
f"marg={rep['marginal'].get('marginal_verdict') if isinstance(rep.get('marginal'), dict) else rep['marginal']['marginal_verdict']}")
print(f" {rep['name']:<24} earns_slot_honest={rep.get('earns_slot_honest')} {lab}")
if __name__ == "__main__":
main()
+473
View File
@@ -0,0 +1,473 @@
"""r0724_pm_deribit_probe — Riconciliazione LIVE Polymarket <-> Deribit su digitali BTC/ETH.
Domanda: quanto del "gap ~11pp" (arXiv 2606.19517, PM vs prob implicite Deribit) sopravvive
OGGI a una riconciliazione onesta che aggiusta i mismatch di SPEC, e il trade hedged
(lato cheap su PM + digitale opposta via VERTICAL di opzioni Deribit) ha senso a ~$600?
Mismatch di spec quantificati (non assunti):
1. TEMPO: i PM "above $K on <date>" risolvono sulla candela 1m Binance delle 12:00 ET
(=16:00 UTC in estate); le daily Deribit scadono 08:00 UTC (settlement TWAP 30m
dell'indice) -> 8h di varianza extra lato PM. dP_time = N(d2;T_pm) - N(d2;T_der).
2. FONTE: PM risolve su Binance BTC/USDT; Deribit su indice BTC-USD. Basis misurato
live (= sconto USDT/USD): pochi bps di spot MA a orizzonte daily con IV ~15%
vale svariati pp di probabilita' ATM. dP_src = N(d2; S*(1+basis)) - N(d2; S).
3. REPLICA: la digitale via vertical ha bound sub/super-replicanti:
call-spread [K,K+w] <= 1{S>K} <= call-spread [K-w,K] (identico coi put OTM).
Lato ITM i book Deribit sono MORTI -> si replica SEMPRE dal lato OTM
(K>=S: call; K<S: put, P_above = 1 - put_spread/w). Stima centrale = spread
centrato [K-w,K+w] sui mark; banda = [sub_mark, super_mark]; eseguibile =
sub-replica incrociando bid/ask REALI.
4. COSTI: PM fee 0 ma spread+depth CLOB; Deribit taker 0.0003 ccy/contratto cap
12.5% del premio per gamba + delivery 0.00015 (cap 12.5%) sul leg ITM; margine
del leg corto in standard margin (nessun netting del vertical in SM).
5. RISCHIO NON HEDGIABILE: P(cross) = probabilita' che S stia da lati OPPOSTI di K
alle 08:00 e alle 16:00 UTC (le due gambe risolvono in conflitto -> payoff 0 o 2):
MC bivariato con sigma implicita. Vicino all'ATM e' grande -> il "lock" non esiste.
Tutto live, API pubbliche tokenless, zero storage; se un endpoint e' vuoto lo dichiara.
Riproducibile: uv run python scripts/research/r0724_pm_deribit_probe.py
"""
from __future__ import annotations
import json
import math
import random
import time
import urllib.request
from datetime import datetime, timezone
UA = {"User-Agent": "Mozilla/5.0 (research probe)"}
def get(url: str, timeout: int = 20, retries: int = 2):
for k in range(retries + 1):
try:
req = urllib.request.Request(url, headers=UA)
with urllib.request.urlopen(req, timeout=timeout) as r:
return json.load(r)
except Exception as e: # noqa: BLE001
if k == retries:
print(f" [FETCH FAIL] {url[:100]} -> {e}")
return None
time.sleep(1.0)
def ncdf(x: float) -> float:
return 0.5 * (1.0 + math.erf(x / math.sqrt(2.0)))
def bs_digital(S: float, K: float, sig: float, T_yr: float) -> float:
if T_yr <= 0 or sig <= 0:
return 1.0 if S > K else 0.0
d2 = (math.log(S / K) - 0.5 * sig * sig * T_yr) / (sig * math.sqrt(T_yr))
return ncdf(d2)
# ----------------------------------------------------------------------------- Polymarket
PM_EVENTS = [
("BTC", "bitcoin-above-on-july-25-2026", "25JUL26"),
("ETH", "ethereum-above-on-july-25-2026", "25JUL26"),
("BTC", "bitcoin-above-on-july-26-2026", "26JUL26"),
("ETH", "ethereum-above-on-july-26-2026", "26JUL26"),
("BTC", "bitcoin-above-on-july-27-2026", "27JUL26"),
("ETH", "ethereum-above-on-july-27-2026", "27JUL26"),
]
def parse_strike(question: str) -> float | None:
import re
m = re.search(r"\$([\d,]+(?:\.\d+)?)", question)
return float(m.group(1).replace(",", "")) if m else None
def fetch_pm():
out = []
for asset, slug, dexp in PM_EVENTS:
ev = get(f"https://gamma-api.polymarket.com/events?slug={slug}")
if not ev:
print(f"[PM] evento {slug}: VUOTO/bloccato")
continue
ev = ev[0]
for m in ev["markets"]:
K = parse_strike(m["question"])
if K is None:
continue
desc = m.get("description", "")
tok = json.loads(m.get("clobTokenIds", "[]"))
out.append(
dict(
asset=asset,
dexp=dexp,
end=ev["endDate"],
K=K,
yes_bid=float(m["bestBid"]) if m.get("bestBid") else None,
yes_ask=float(m["bestAsk"]) if m.get("bestAsk") else None,
vol24=float(m.get("volume24hr") or 0),
spec_ok=("Binance" in desc and "12:00 in the ET" in desc),
yes_token=tok[0] if tok else None,
)
)
return out
def pm_book_depth(token_id: str):
b = get(f"https://clob.polymarket.com/book?token_id={token_id}")
if not b or not b.get("bids") or not b.get("asks"):
return None
bids = [(float(x["price"]), float(x["size"])) for x in b["bids"]]
asks = [(float(x["price"]), float(x["size"])) for x in b["asks"]]
bb, ba = max(p for p, _ in bids), min(p for p, _ in asks)
mid = 0.5 * (bb + ba)
d = {}
for w in (0.01, 0.02):
d[w] = (
sum(s * p for p, s in bids if p >= mid - w),
sum(s * p for p, s in asks if p <= mid + w),
)
return dict(bb=bb, ba=ba, depth=d)
# ------------------------------------------------------------------------------- Deribit
def fetch_deribit(ccy: str):
bs = get(
"https://www.deribit.com/api/v2/public/get_book_summary_by_currency"
f"?currency={ccy}&kind=option"
)
idx = get(
f"https://www.deribit.com/api/v2/public/get_index_price?index_name={ccy.lower()}_usd"
)
ins = get(
f"https://www.deribit.com/api/v2/public/get_instruments?currency={ccy}"
"&kind=option&expired=false"
)
if not bs or not idx or not ins:
return None
books = {row["instrument_name"]: row for row in bs["result"]}
expts = {}
for i in ins["result"]:
expts[i["instrument_name"].split("-")[1]] = i["expiration_timestamp"] / 1000.0
return dict(books=books, index=idx["result"]["index_price"], expts=expts)
def _q(books, ccy, dexp, strike, typ, index):
"""(bid_usd, ask_usd, mark_usd) di uno strumento; None se assente."""
n = f"{ccy}-{dexp}-{int(strike)}-{typ}"
r = books.get(n)
if not r:
return None
return dict(
n=n,
bid=(r["bid_price"] or 0.0) * index,
ask=(r["ask_price"] * index) if r["ask_price"] else None,
mark=(r.get("mark_price") or 0.0) * index,
)
def deribit_digital(books, ccy, dexp, K, index):
"""Stima onesta di P(S_der > K) via vertical OTM-side.
Ritorna: mark centrato, banda [sub_mark, super_mark], eseguibili
(exec_buy = costo per COMPRARE la sub-replica dell'above-digitale ai prezzi reali;
exec_sell = incasso per VENDERLA), gambe usate.
"""
typ = "C" if K >= index else "P"
strikes = sorted(
float(n.split("-")[2])
for n in books
if n.split("-")[1] == dexp and n.endswith(f"-{typ}")
)
if K not in strikes:
return None # tutti gli strike PM qui coincidono con strike listati
i = strikes.index(K)
if i == 0 or i == len(strikes) - 1:
return None
Km, Kp = strikes[i - 1], strikes[i + 1]
qm, q0, qp = (
_q(books, ccy, dexp, Km, typ, index),
_q(books, ccy, dexp, K, typ, index),
_q(books, ccy, dexp, Kp, typ, index),
)
if not (qm and q0 and qp):
return None
if typ == "C":
centered = (qm["mark"] - qp["mark"]) / (Kp - Km)
sub = (q0["mark"] - qp["mark"]) / (Kp - K) # <= P
sup = (qm["mark"] - q0["mark"]) / (K - Km) # >= P
# comprare sub-replica: buy C(K), sell C(Kp)
exec_buy = (
(q0["ask"] - qp["bid"]) / (Kp - K) if q0["ask"] is not None else None
)
exec_sell = (
(q0["bid"] - qp["ask"]) / (Kp - K) if qp["ask"] is not None else None
)
legs = (q0, qp)
else:
# P_above = 1 - put_spread/w ; [K,Kp] put spread super-replica il below
centered = 1.0 - (qp["mark"] - qm["mark"]) / (Kp - Km)
sub = 1.0 - (qp["mark"] - q0["mark"]) / (Kp - K) # <= P_above
sup = 1.0 - (q0["mark"] - qm["mark"]) / (K - Km) # >= P_above
# comprare (sinteticamente) l'above = VENDERE il put spread [K,Kp]:
# incasso bid(Kp)-ask(K); prezzo implicito pagato = 1 - incasso/w
exec_buy = (
1.0 - (qp["bid"] - q0["ask"]) / (Kp - K) if q0["ask"] is not None else None
)
exec_sell = (
1.0 - (qp["ask"] - q0["bid"]) / (Kp - K) if qp["ask"] is not None else None
)
legs = (q0, qp)
return dict(
typ=typ,
Km=Km,
Kp=Kp,
w=Kp - K,
centered=centered,
sub=sub,
sup=sup,
exec_buy=exec_buy,
exec_sell=exec_sell,
legs=legs,
)
IV_CACHE: dict = {}
def deribit_iv(ccy: str, dexp: str, name: str) -> float | None:
if name in IV_CACHE:
return IV_CACHE[name]
t = get(f"https://www.deribit.com/api/v2/public/ticker?instrument_name={name}")
iv = None
if t:
iv = t["result"].get("mark_iv")
iv = iv / 100.0 if iv else None
IV_CACHE[name] = iv
return iv
def p_cross(S, K, sig, T1_yr, T2_yr, n=200_000, seed=7):
"""MC: P(lati opposti di K a T1 e T2) e P(sopra a T2 ma sotto a T1) ecc."""
rng = random.Random(seed)
lo_hi = hi_lo = 0
lnK = math.log(K / S)
s1 = sig * math.sqrt(T1_yr)
s2x = sig * math.sqrt(max(T2_yr - T1_yr, 1e-12))
for _ in range(n):
x1 = -0.5 * s1 * s1 + s1 * rng.gauss(0, 1)
x2 = x1 - 0.5 * s2x * s2x + s2x * rng.gauss(0, 1)
a, b = x1 > lnK, x2 > lnK
if not a and b:
lo_hi += 1
elif a and not b:
hi_lo += 1
return lo_hi / n, hi_lo / n
# ----------------------------------------------------------------------------------- run
def main():
now = datetime.now(timezone.utc)
print(f"=== PROBE PM<->DERIBIT {now.isoformat(timespec='seconds')} ===\n")
pm = fetch_pm()
nbad = sum(1 for m in pm if not m["spec_ok"])
print(f"[PM] {len(pm)} mercati above/below; spec Binance/12:00ET non confermata su {nbad}")
der = {c: fetch_deribit(c) for c in ("BTC", "ETH")}
for c, d in der.items():
if d:
print(f"[Deribit] {c} index={d['index']:.2f}")
def binance_spot(sym):
for host in ("https://api.binance.com", "https://data-api.binance.vision"):
r = get(f"{host}/api/v3/ticker/price?symbol={sym}", timeout=10, retries=0)
if r and "price" in r:
return float(r["price"])
return None
binance = {"BTC": binance_spot("BTCUSDT"), "ETH": binance_spot("ETHUSDT")}
kr = get("https://api.kraken.com/0/public/Ticker?pair=USDTZUSD", timeout=10, retries=1)
usdtusd = None
try:
usdtusd = float(list(kr["result"].values())[0]["c"][0])
except Exception: # noqa: BLE001
pass
print(f"[Binance] BTCUSDT={binance['BTC']} ETHUSDT={binance['ETH']} | USDT/USD={usdtusd}")
basis = {}
for c in ("BTC", "ETH"):
basis[c] = (binance[c] / der[c]["index"] - 1.0) if (der[c] and binance[c]) else 0.0
print(f" basis {c} BinanceUSDT vs indice Deribit: {basis[c]*1e4:+.1f} bps")
rows = []
for m in pm:
c, d = m["asset"], der[m["asset"]]
if not d or m["dexp"] not in d["expts"]:
continue
S = d["index"]
dig = deribit_digital(d["books"], c, m["dexp"], m["K"], S)
if dig is None:
continue
T_der = max(d["expts"][m["dexp"]] - now.timestamp(), 0) / (365.25 * 86400)
t_pm = datetime.fromisoformat(m["end"].replace("Z", "+00:00")).timestamp()
T_pm = max(t_pm - now.timestamp(), 0) / (365.25 * 86400)
near = abs(math.log(m["K"] / S)) < 0.09
sig = deribit_iv(c, m["dexp"], dig["legs"][0]["n"]) if near else None
if sig is None:
sig = 0.45
dP_time = bs_digital(S, m["K"], sig, T_pm) - bs_digital(S, m["K"], sig, T_der)
dP_src = bs_digital(S * (1 + basis[c]), m["K"], sig, T_pm) - bs_digital(
S, m["K"], sig, T_pm
)
pm_mid = (
0.5 * (m["yes_bid"] + m["yes_ask"])
if (m["yes_bid"] is not None and m["yes_ask"] is not None)
else (m["yes_ask"] or m["yes_bid"])
)
raw = pm_mid - dig["centered"] if pm_mid is not None else None
resid = raw - dP_time - dP_src if raw is not None else None
depth = pm_book_depth(m["yes_token"]) if (m["yes_token"] and near) else None
rows.append(
dict(
m=m,
dig=dig,
sig=sig,
T_der=T_der,
T_pm=T_pm,
dP_time=dP_time,
dP_src=dP_src,
pm_mid=pm_mid,
raw=raw,
resid=resid,
depth=depth,
S=S,
near=near,
)
)
print("\n=== RICONCILIAZIONE (prob %, gap pp; digitale = vertical OTM-side) ===")
hdr = (
f"{'mkt':<22}{'PM b/a':>12}{'Der mark[sub,sup]':>20}{'exec b/s':>13}"
f"{'raw':>7}{'dT':>6}{'dSrc':>6}{'resid':>7}{'iv%':>5}"
)
print(hdr)
print("-" * len(hdr))
for r in rows:
m, g = r["m"], r["dig"]
name = f"{m['asset']} >{int(m['K'])} {m['dexp'][:5]}"
pmba = (
f"{(m['yes_bid'] or 0)*100:.1f}/{(m['yes_ask'] or 0)*100:.1f}"
if (m["yes_bid"] is not None or m["yes_ask"] is not None)
else "n/a"
)
ex = (
f"{g['exec_buy']*100:.0f}/{g['exec_sell']*100:.0f}"
if (g["exec_buy"] is not None and g["exec_sell"] is not None)
else "n/q"
)
print(
f"{name:<22}{pmba:>12}"
f"{g['centered']*100:>8.1f}[{g['sub']*100:.0f},{g['sup']*100:.0f}]".ljust(42)
+ f"{ex:>13}"
f"{(r['raw'] or 0)*100:>+7.1f}{r['dP_time']*100:>+6.1f}"
f"{r['dP_src']*100:>+6.1f}{(r['resid'] or 0)*100:>+7.1f}"
f"{r['sig']*100:>5.0f}"
)
print(
"\n Der mark = spread centrato sui mark, [sub,sup] = bound di replica;"
" exec b/s = comprare/vendere la sub-replica ai bid/ask REALI."
"\n raw = PM_mid - Der_mark; resid = raw - dT - dSrc"
" (>0: PM sovraprezza il lato above vs Deribit spec-adjusted)."
)
# sistematicita' per bucket di moneyness
print("\n=== RESIDUO PER BUCKET (solo quote PM a doppio lato) ===")
buckets = {"ITM(K<S-1.5%)": [], "ATM(|1.5%|)": [], "OTM(K>S+1.5%)": []}
for r in rows:
if r["resid"] is None or r["m"]["yes_bid"] is None or r["m"]["yes_ask"] is None:
continue
lm = math.log(r["m"]["K"] / r["S"])
b = "ITM(K<S-1.5%)" if lm < -0.015 else ("OTM(K>S+1.5%)" if lm > 0.015 else "ATM(|1.5%|)")
buckets[b].append(r["resid"])
for b, v in buckets.items():
if v:
pos = sum(1 for x in v if x > 0)
print(
f" {b:<15} n={len(v):>2} resid medio {sum(v)/len(v)*100:+.1f}pp"
f" mediana {sorted(v)[len(v)//2]*100:+.1f}pp >0: {pos}/{len(v)}"
)
print("\n=== DEPTH CLOB PM (vicino allo spot) ===")
for r in rows:
if r["depth"]:
m, dp = r["m"], r["depth"]
d1, d2 = dp["depth"][0.01], dp["depth"][0.02]
print(
f" {m['asset']} >{int(m['K'])} {m['dexp']}: book {dp['bb']:.3f}/{dp['ba']:.3f}"
f" depth±1c ${d1[0]:,.0f}/${d1[1]:,.0f} ±2c ${d2[0]:,.0f}/${d2[1]:,.0f}"
f" vol24h ${m['vol24']:,.0f}"
)
# ------------------------------------------------------------------ trade hedged demo
print("\n=== TRADE HEDGED a size minima (0.1 BTC / 1 ETH) — numeri VERI ===")
min_amt = {"BTC": 0.1, "ETH": 1.0}
fee_rate, dlv_rate = 0.0003, 0.00015
for r in rows:
m, g = r["m"], r["dig"]
if r["resid"] is None or not r["near"] or abs(r["resid"]) < 0.01:
continue
if g["exec_buy"] is None or g["exec_sell"] is None:
continue
c, S = m["asset"], r["S"]
amt = min_amt[c]
W = g["w"] * amt # notional digitale $
if r["resid"] > 0:
# PM ricco sul lato above: buy PM NO al (1-yes_bid) + buy digitale-above Deribit
pm_px = 1 - (m["yes_bid"] or 0)
der_px = g["exec_buy"]
side = "BUY PM NO + LONG vertical OTM (sub-replica above)"
else:
pm_px = m["yes_ask"] or 1
der_px = 1 - g["exec_sell"]
side = "BUY PM YES + SHORT vertical OTM (sub-replica below)"
cost = (pm_px + der_px) * W
lock = W - cost
fees = 0.0
for leg in g["legs"]:
fees += min(fee_rate * amt * S, 0.125 * leg["mark"] * amt)
dlv = min(dlv_rate * amt * S, 0.125 * max(le["mark"] for le in g["legs"]) * amt)
otm_frac = abs(g["Kp"] - S) / S
mark_short = g["legs"][1]["mark"] / S
im = (max(0.15 - otm_frac, 0.10) + mark_short) * amt
pc = p_cross(S, m["K"], r["sig"], r["T_der"], r["T_pm"])
capital = cost + im * S
net = lock - fees - dlv
days = r["T_pm"] * 365.25
print(f"\n {m['asset']} >{int(m['K'])} {m['dexp']} resid {r['resid']*100:+.1f}pp -> {side}")
print(
f" gambe Deribit {g['legs'][0]['n']}/{g['legs'][1]['n']} amount {amt} {c}"
f" -> payout digitale ${W:,.0f}"
)
print(
f" costo PM ${pm_px*W:,.2f} + Deribit ${der_px*W:,.2f} = ${cost:,.2f};"
f" lock lordo ${lock:,.2f}; fee entry ${fees:.2f} + delivery ${dlv:.2f}"
f" -> netto ${net:,.2f}"
)
print(
f" margine leg corto (SM, no netting) ~${im*S:,.0f};"
f" capitale impegnato ~${capital:,.0f};"
f" ritorno se lock regge: {net/capital*100:.2f}% in {days:.1f}g"
f" (~{net/capital*365.25/days*100:.0f}%/anno)"
)
print(
f" RISCHIO CROSS 08->16 UTC (MC, iv {r['sig']*100:.0f}%):"
f" P(sotto@08,sopra@16)={pc[0]*100:.1f}% P(sopra@08,sotto@16)={pc[1]*100:.1f}%"
f" -> P(gambe in conflitto)={sum(pc)*100:.1f}%"
)
print("\n[NOTE] Deribit settle = TWAP 30m pre-08:00 UTC vs PM candela 1m 16:00 UTC;")
print("la sub-replica paga <1 nella rampa [K,K+w] -> il 'lock' e' un bound inferiore")
print("solo FUORI dalla rampa; ATM con w=500 (BTC) la rampa e' ~1-1.5 sigma daily.")
if __name__ == "__main__":
main()
+141
View File
@@ -0,0 +1,141 @@
"""r0724_premium_wave — premi cross-venue AUTO-CALCOLATI: Coinbase premium + Kimchi (2026-07-24).
Seconda parte dell'ondata "trova altre strategie" (dopo r0724_onchain_wave): la ricerca
web (agente on-chain/dati) indica i premi regionali come UNICA famiglia flow con dati
100% auto-calcolabili da candele raw -> zero rischio-revisione/vintage del vendor
(coerente con la dottrina dati del progetto). Evidenza accademica: kimchi = anomalia
documentata (violazione persistente della legge del prezzo unico, capital controls);
lead-lag ASIMMETRICO e TEMPO-VARIANTE (MDPI 2026) -> nessuna regola pubblicata onesta,
qui si meccanizza da zero.
SEGNALI (mai il prezzo): per asset a e giorno d
CBPREM_a(d) = close Coinbase USD (00:00 UTC) / close feed certificato - 1
KIMCHI_a(d) = close Upbit KRW (00:00 UTC, ancora 09:00 KST) / (USDKRW_ECB x close cert) - 1
Le candele Upbit daily sono ancorate a mezzanotte UTC (=09:00 KST) -> stesso istante di
chiusura del feed certificato, nessun premio finto da mismatch orario. FX = fixing ECB
del giorno (ffill weekend; il KRW si muove ~nulla vs la vol crypto — caveat dichiarato).
NB: il feed certificato Deribit e' esso stesso un indice multi-exchange che include
Coinbase -> il CBPREM misurato e' SMORZATO (caveat strutturale).
Griglie (piccole, tutte contate nel deflated-Sharpe): z-score rolling 180g del livello
(follow / contrarian) e segno della variazione 30g (follow), LF e LS -> 6 celle/famiglia.
Gate: study_family_honest (cella in-sample, DSR, marginal scorer vs TP01).
Dati: data/external/premium/ (fetch: scratchpad/fetch_premium.py — Coinbase Exchange
public candles, Upbit public candles, frankfurter.app ECB; tutti tokenless).
Uso: `uv run python scripts/research/r0724_premium_wave.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 # noqa: E402
from altlib import study_family_honest, fmt_marginal, get # noqa: E402
EXT = ROOT / "data" / "external" / "premium"
def _cert_close_by_day(asset: str) -> pd.Series:
df = get(asset, "1d")
days = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)).floor("D")
return pd.Series(df["close"].values.astype(float), index=days)
def _cb_close(asset: str) -> pd.Series:
d = pd.read_csv(EXT / f"cb_{asset.lower()}.csv")
idx = pd.DatetimeIndex(pd.to_datetime(d["ts"], unit="s", utc=True)).floor("D")
return pd.Series(d["close"].values.astype(float), index=idx)
def _upbit_close(asset: str) -> pd.Series:
d = pd.read_csv(EXT / f"upbit_{asset.lower()}.csv")
idx = pd.DatetimeIndex(pd.to_datetime(d["utc"], utc=True)).floor("D")
return pd.Series(d["close_krw"].values.astype(float), index=idx)
def _fx() -> pd.Series:
d = pd.read_csv(EXT / "usdkrw.csv")
idx = pd.DatetimeIndex(pd.to_datetime(d["date"], utc=True))
s = pd.Series(d["usdkrw"].values.astype(float), index=idx)
full = pd.date_range(s.index[0], s.index[-1] + pd.Timedelta(days=3), freq="D", tz="UTC")
return s.reindex(full).ffill()
def _premia() -> dict:
fx = _fx()
out = {}
for a in ("BTC", "ETH"):
cert = _cert_close_by_day(a)
cb = _cb_close(a).reindex(cert.index)
up = _upbit_close(a).reindex(cert.index)
out[a] = pd.DataFrame({
"cbprem": cb / cert - 1.0,
"kimchi": up / (fx.reindex(cert.index) * cert) - 1.0,
}, index=cert.index)
return out
_PREM = _premia()
def prem_factory_maker(col: str):
def factory(tf: str, sig: str = "z180", mode: str = "folLF"):
def fn(df, asset):
p = _PREM[asset][col]
if sig == "z180":
mu = p.rolling(180, min_periods=90).mean()
sd = p.rolling(180, min_periods=90).std()
z = (p - mu) / sd
else: # d30: variazione 30g del premio
z = p - p.shift(30)
s = np.sign(z) if mode.startswith("fol") else -np.sign(z)
pos = s if mode.endswith("LS") else (s > 0).astype(float)
days = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)).floor("D")
return np.nan_to_num(pos.reindex(days).values.astype(float))
return fn
return factory
GRID = [
dict(sig="z180", mode="folLF"), dict(sig="z180", mode="conLF"),
dict(sig="z180", mode="folLS"), dict(sig="z180", mode="conLS"),
dict(sig="d30", mode="folLF"), dict(sig="d30", mode="folLS"),
]
def main() -> None:
print("=" * 100)
print(" ONDATA PREMI CROSS-VENUE — CBPREM + KIMCHI via study_family_honest")
for a in ("BTC", "ETH"):
P = _PREM[a].dropna()
print(f" {a}: {len(P)} giorni | cbprem medio {P['cbprem'].mean()*1e4:+.1f}bps "
f"(p1/p99 {P['cbprem'].quantile(0.01)*1e4:+.0f}/{P['cbprem'].quantile(0.99)*1e4:+.0f}) | "
f"kimchi medio {P['kimchi'].mean()*1e4:+.1f}bps "
f"(p1/p99 {P['kimchi'].quantile(0.01)*1e4:+.0f}/{P['kimchi'].quantile(0.99)*1e4:+.0f})")
print("=" * 100)
for fam, col in (("CBPREM-coinbase", "cbprem"), ("KIMCHI-korea", "kimchi")):
print("-" * 100)
rep = study_family_honest(fam, prem_factory_maker(col), GRID, tfs=("1d",))
if rep.get("chosen") is None:
print(f"=== {fam}: nessuna cella valida in-sample")
continue
ch = rep["chosen"]
print(f"=== {fam}: cella IS {ch['params']} (IS Sh {ch['insample_sharpe']}, "
f"full {ch['full_sharpe']}) su {rep['n_cells']} celle")
print(f" deflated-Sharpe {rep['deflated_sharpe']} (null-max {rep['expected_null_max']})"
f" dsr_pass={rep['dsr_pass']}")
print(fmt_marginal(rep["marginal"]))
print(f" >>> EARNS_SLOT_HONEST = {rep['earns_slot_honest']}")
if __name__ == "__main__":
main()
+281
View File
@@ -0,0 +1,281 @@
#!/usr/bin/env python
"""r0724_skh_live_weight.py — FOLLOW-UP dell'audit anchor 2026-07-02 su SKH01:
"cadenza 230m, peso live da rivedere" (diario 2026-07-02-anchor-audit-xs01-skh01.md).
CONTESTO. Il book live Deribit e' TP01 0.75 + SKH01 0.25 (sleeves.deribit_book_sleeves).
L'audit r0702 ha mostrato che il PATH LIVE reale (cron ORARIO `0 * * * *` + exit software,
non on-book al livello) degrada SKH01: alla lente 50/50 il book passa FULL 1.46->1.19,
HOLD 1.64->1.15, DD 18->25%; nei crash gap-through-stop (sl 2% modellato -> -11/-23%
realizzato). In piu' l'offset 0 della griglia 230m/690m e' il 93-98 pctl dei 23 offset a
priori (best-of-23). DOMANDA: dato il path live vero, qual e' il peso SKH ottimale del book
a 2 sleeve, e quanto vale allineare il cron alla cadenza 230m?
COSA FA (riusa la macchineria di r0702_anchor_skh01 — import diretto, NON reinventata):
1. SANITY: r02.sanity() (off0 == _skyhook_returns bit-exact) + riconciliazione con i
numeri dell'audit (50/50 canonical off0 FULL ~1.46 / HOLD ~1.64 / DD ~18.1%; hourly
~1.19 / ~1.15 / ~24.6%; blend 75/25 canonical HOLD ~1.16). NB: il feed live aggiunge
barre ogni giorno -> attesa una piccola deriva-dati vs i numeri del 2026-07-02.
2. Serie SKH live-path: per TUTTI i 23 offset a priori x {canonical, barclose, hourly}
(sim_equity di r0702; 'hourly' = fill al close 5m del prossimo multiplo orario = path
del cron reale; 'barclose' = fill al close della barra 230m = cio' che otterrebbe un
cron allineato ai confini 230m; 'canonical' = fill-al-livello del backtest, riferimento).
3. SWEEP PESO: book = (1-w)*TP01 + w*SKH_live, w in {0,.10,.15,.20,.25,.30,.40,.50}
(8 valori = multiple-testing leggero, DICHIARATO; nel gate k_seen=8). TP01 =
al.tp01_baseline_daily() (canonico, come nell'audit). Per ogni w: Sharpe FULL / IS
(pre-2025) / HOLD (2025+) / maxDD, su off0 E min/med/max sui 23 offset.
REGOLA DI SELEZIONE (dichiarata A PRIORI, prima di vedere i numeri): w* = argmax
della MEDIANA sugli offset dello Sharpe IN-SAMPLE (pre-holdout, lente hourly =
path live, de-luckata). A parita' (<0.03 di Sharpe) si preferisce il w piu' VICINO
al corrente 0.25 (conservativo). L'hold-out serve SOLO a verificare, non a scegliere.
4. GATE weights_tilt_null (src/portfolio/portfolio.py) adattato al book a 2 sleeve:
null = tilt casuali cap-respecting (floor 0.05, cap SKH 0.50 = estremo dello sweep)
attorno al 75/25 corrente, sui daily del PATH LIVE (hourly, off0 = griglia che gira
davvero). k_seen=8 (i w guardati sull'hold-out in questo studio). Se il w* non passa
il gate -> verdetto onesto "INVARIATO".
5. CADENZA 230m: valore di allineare l'esecuzione ai confini 230m = barclose vs hourly
al peso corrente 0.25 (dSharpe FULL/HOLD, dDD), off0 + banda sugli offset. Fattibilita'
SOLO STIMATA (non cablata): 230 = 23x10 -> i confini 230m cadono SEMPRE su multipli di
10 minuti dall'epoch => un cron a passo 10m che triggera solo quando
(now % 230min) == 0 colpisce ogni confine con ritardo ~0 (vs 0-50 min, media 25, del
cron orario) -> 'barclose' e' esattamente il suo modello (al netto di runtime/slippage).
CAVEAT DICHIARATI (onesta'):
- l'exit software nei crash prende il GAP (gia' misurato dall'audit: sl2% -> -11/-23%
realizzato); la sim 'hourly' e' gia' la stima pessimistica giusta del path live e la
cadenza 230m NON recupera il gap canonical->barclose (fill-al-livello: nessun cron
riporta l'exit software sul livello SL dentro una barra che gappa);
- equity daily-step (lens Sharpe), stessa convenzione del canonico e dell'audit;
- nessuna selezione sull'hold-out: w* scelto sull'IS mediano-di-banda, hold-out solo
verifica; 8 valori di w provati e dichiarati (k_seen=8 nel gate);
- la banda sugli offset e' la lente de-luckata; live gira UNA sola griglia (off 0);
- deriva-dati vs audit (il feed cresce ogni giorno): riconciliazione "circa", riportata.
VINCOLI: nessun file di produzione toccato (config/live.json, sleeves.py, scripts/live,
cron INVARIATI); niente commit; dati SOLO dal feed certificato (get5m di r0702 -> load_data).
Runtime ~4-8 min (46 run skyhook cachati + 138 sim_equity leggere + sweep + gate).
"""
from __future__ import annotations
import sys
import time
from pathlib import Path
import numpy as np
import pandas as pd
ROOT = Path("/opt/docker/PythagorasGoal")
sys.path.insert(0, str(ROOT / "scripts" / "research"))
sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt"))
sys.path.insert(0, str(ROOT))
import r0702_anchor_skh01 as r02 # noqa: E402 (macchineria audit: run_asset/sim_equity/get5m/sanity)
import altlib as al # noqa: E402
from src.portfolio.portfolio import combine_outer, weights_tilt_null # noqa: E402
HOLDOUT = r02.HOLDOUT
ASSETS = r02.ASSETS
OFFSETS = r02.OFFSETS # 23 offset a priori (ogni 30m su [0,690)), 0 = canonico
MODES = ("canonical", "barclose", "hourly")
W_GRID = (0.0, 0.10, 0.15, 0.20, 0.25, 0.30, 0.40, 0.50) # 8 valori — dichiarati (k_seen=8)
W_CURRENT = 0.25
TIE_TOL = 0.03 # regola a priori: parita' se |dSh IS| < 0.03 -> w più vicino a 0.25
# ---------------------------------------------------------------------------
# Serie daily per (asset, offset, mode) — riusa run_asset + sim_equity di r0702
# ---------------------------------------------------------------------------
_D: dict = {}
def skh_daily_asset(a: str, off: int, mode: str) -> pd.Series:
key = (a, off, mode)
if key in _D:
return _D[key]
_, m0, ltf, ent = r02.run_asset(a, off)
df5 = r02.get5m(a)
ts5 = df5["timestamp"].values.astype(np.int64) + r02.MS5
c5 = df5["close"].values.astype(float)
eq = r02.sim_equity(ltf, ent, mode, ts5_close=ts5, c5=c5)
if mode == "canonical": # sanity strutturale: re-sim == harness bit-exact
dmax = float(np.max(np.abs(eq - m0.equity)))
assert dmax < 1e-6, f"re-sim canonica != harness ({a}, off={off}): max|dif|={dmax:.2e}"
s = pd.Series(eq, index=pd.DatetimeIndex(pd.to_datetime(ltf["datetime"], utc=True)))
_D[key] = s.resample("1D").last().ffill().pct_change().dropna()
return _D[key]
def skh_book(off: int, mode: str) -> pd.Series:
"""Book 50/50 BTC+ETH daily-step (convenzione di _skyhook_returns / audit)."""
J = pd.concat({a: skh_daily_asset(a, off, mode) for a in ASSETS},
axis=1, join="inner").fillna(0.0)
return pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index)
def stats4(s: pd.Series) -> tuple[float, float, float, float]:
f, i_, h = r02.sh3(s)
return f, i_, h, al._dd_ret(s)
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main() -> None:
t0 = time.time()
print("=" * 100)
print("r0724 — SKH01 sul PATH LIVE (cron orario): peso ottimale del book Deribit a 2 sleeve")
print(f"+ valore della cadenza 230m. 23 offset a priori, fee 0.10% RT, HOLD-OUT >= {HOLDOUT.date()}")
print("=" * 100)
# ---- (1) SANITY + riconciliazione con l'audit r0702 --------------------
r02.sanity()
b0 = {m: skh_book(0, m) for m in MODES}
print("\n[SANITY] riconciliazione col r0702 (2026-07-02; attesa deriva-dati: il feed cresce):")
print(f"{'modo':<10} {'ShFULL':>7} {'ShIS':>7} {'ShHOLD':>7} {'maxDD':>7} audit r0702")
ref = {"canonical": "FULL 1.46 / HOLD 1.64 / DD 18.1%",
"barclose": "(intermedio, isola il fill-al-livello)",
"hourly": "FULL 1.19 / HOLD 1.15 / DD 24.6%"}
for m in MODES:
f, i_, h, dd = stats4(b0[m])
print(f"{m:<10} {f:>7.3f} {i_:>7.3f} {h:>7.3f} {dd:>6.1%} {ref[m]}")
B = al.tp01_baseline_daily()
fB, iB, hB, ddB = stats4(B)
print(f"\nTP01 baseline canonico (al.tp01_baseline_daily): FULL {fB:.3f} / IS {iB:.3f} / "
f"HOLD {hB:.3f} / DD {ddB:.1%} (audit: FULL ~1.30, HOLD ~0.31)")
def blend(skh: pd.Series, w: float) -> pd.Series:
if w == 0.0:
return B
bl = combine_outer({"TP": B, "SKH": skh}, {"TP": 1.0 - w, "SKH": w})
return bl[bl.index >= B.index.min()]
f, i_, h, dd = stats4(blend(b0["canonical"], 0.25))
print(f"blend 75/25 canonical off0: FULL {f:.3f} / HOLD {h:.3f} (audit: HOLD ~1.16)")
# ---- (2) tutte le serie: 23 offset x 3 modi ----------------------------
print(f"\n[{time.time()-t0:5.0f}s] genero i 23 offset x 3 modi (run skyhook cachati)...")
SKB: dict[str, dict[int, pd.Series]] = {m: {} for m in MODES}
for off in OFFSETS:
for m in MODES:
SKB[m][off] = b0[m] if off == 0 else skh_book(off, m)
fh, ih, hh, ddh = stats4(SKB["hourly"][off])
print(f" [{time.time()-t0:5.0f}s] off {off:>3}m: hourly FULL {fh:+.2f} IS {ih:+.2f} "
f"HOLD {hh:+.2f} DD {ddh:.0%}")
# ---- (3) SWEEP PESO ----------------------------------------------------
print("\n" + "=" * 100)
print(f"(3) SWEEP PESO — book (1-w)*TP01 + w*SKH_live, w in {list(W_GRID)}")
print(" (8 valori = multiple-testing leggero, dichiarato; selezione su IS mediano, lente hourly)")
print("=" * 100)
ST: dict[str, dict[float, dict[int, tuple]]] = {m: {w: {} for w in W_GRID} for m in MODES}
for m in MODES:
for w in W_GRID:
for off in OFFSETS:
ST[m][w][off] = stats4(blend(SKB[m][off], w))
def band(m: str, w: float, k: int) -> tuple[float, float, float]:
v = np.array([ST[m][w][off][k] for off in OFFSETS])
return float(v.min()), float(np.median(v)), float(v.max())
for m in ("hourly", "canonical"):
lens = "PATH LIVE (lente della decisione)" if m == "hourly" else "riferimento backtest"
print(f"\n--- modo {m.upper()}{lens} ---")
print(f"{'w':>5} | {'off0:':<6}{'FULL':>6} {'IS':>6} {'HOLD':>6} {'DD':>6} | "
f"{'med:':<5}{'FULL':>6} {'IS':>6} {'HOLD':>6} {'DD':>6} | "
f"{'HOLD min..max':>14} | {'DD min..max':>13}")
for w in W_GRID:
f0, i0, h0, d0 = ST[m][w][0]
fm = band(m, w, 0)[1]; im = band(m, w, 1)[1]
hmn, hmd, hmx = band(m, w, 2)
dmn, dmd, dmx = band(m, w, 3)
cur = " <- corrente" if w == W_CURRENT else ""
print(f"{w:>5.2f} | {'':<6}{f0:>6.2f} {i0:>6.2f} {h0:>6.2f} {d0:>5.1%} | "
f"{'':<5}{fm:>6.2f} {im:>6.2f} {hmd:>6.2f} {dmd:>5.1%} | "
f"{hmn:>6.2f}..{hmx:<6.2f} | {dmn:>5.1%}..{dmx:<5.1%}{cur}")
# ---- (4) selezione (regola a priori) -----------------------------------
print("\n--- (4) SELEZIONE del w* (regola dichiarata a priori) ---")
med_is = {w: band("hourly", w, 1)[1] for w in W_GRID}
best_val = max(med_is.values())
ties = [w for w in W_GRID if best_val - med_is[w] < TIE_TOL]
wstar = min(ties, key=lambda w: (abs(w - W_CURRENT), w))
print("mediana-IS (hourly) per w: " +
", ".join(f"{w:.2f}:{med_is[w]:.3f}" for w in W_GRID))
print(f"argmax = {max(med_is, key=med_is.get):.2f} ({best_val:.3f}); "
f"in parita' (<{TIE_TOL}): {[f'{w:.2f}' for w in ties]} -> "
f"w* = {wstar:.2f} (il piu' vicino al corrente {W_CURRENT:.2f})")
h_star_med = band("hourly", wstar, 2)[1]; h_cur_med = band("hourly", W_CURRENT, 2)[1]
d_star_med = band("hourly", wstar, 3)[1]; d_cur_med = band("hourly", W_CURRENT, 3)[1]
print(f"verifica hold-out (mediana banda): w*={wstar:.2f} HOLD {h_star_med:.2f} / DD {d_star_med:.1%}"
f" vs corrente 0.25: HOLD {h_cur_med:.2f} / DD {d_cur_med:.1%}")
# ---- (5) GATE weights_tilt_null ---------------------------------------
print("\n--- (5) GATE weights_tilt_null (book a 2 sleeve, daily del PATH LIVE hourly) ---")
print("null = tilt casuali floor 0.05 / cap SKH 0.50 attorno al 75/25 corrente; k_seen=8")
if wstar == W_CURRENT:
print(f"w* == corrente ({W_CURRENT:.2f}) -> nessun cambio proposto: verdetto INVARIATO "
f"(gate non necessario). Lo eseguo comunque sul miglior w != corrente, a titolo informativo.")
alt = [w for w in W_GRID if w != W_CURRENT and w >= 0.05]
w_gate = max(alt, key=lambda w: med_is[w])
else:
w_gate = wstar
# primario: off0 (la griglia che gira davvero live); robustezza: offset con IS mediano a w corrente
is_cur = {off: ST["hourly"][W_CURRENT][off][1] for off in OFFSETS}
off_med = sorted(OFFSETS, key=lambda o: is_cur[o])[len(OFFSETS) // 2]
for tag, off in (("off0 (griglia live)", 0), (f"off{off_med} (offset IS-mediano)", off_med)):
cols = {"TP01": B, "SKH01": SKB["hourly"][off]}
res = weights_tilt_null(cols, {"TP01": 1 - W_CURRENT, "SKH01": W_CURRENT},
{"TP01": 1 - w_gate, "SKH01": w_gate},
caps={"SKH01": 0.50}, floor=0.05, n=500, k_seen=8)
res.pop("samples", None)
print(f" [{tag}] proposto SKH {w_gate:.2f}: " +
", ".join(f"{k}={v}" for k, v in res.items()))
# ---- (6) CADENZA 230m --------------------------------------------------
print("\n--- (6) CADENZA 230m — barclose (cron allineato ai confini) vs hourly (cron 0 * * * *) ---")
print(f"al peso corrente w={W_CURRENT:.2f}:")
rows = []
for off in OFFSETS:
fb, ib, hb, db = ST["barclose"][W_CURRENT][off]
fh, ih, hh, dh = ST["hourly"][W_CURRENT][off]
rows.append((fb - fh, hb - hh, db - dh))
A = np.array(rows)
print(f" off0: dShFULL {A[0,0]:+.3f}, dShHOLD {A[0,1]:+.3f}, dDD {A[0,2]:+.1%}")
print(f" banda: dShFULL med {np.median(A[:,0]):+.3f} [{A[:,0].min():+.3f},{A[:,0].max():+.3f}], "
f"dShHOLD med {np.median(A[:,1]):+.3f} [{A[:,1].min():+.3f},{A[:,1].max():+.3f}], "
f"dDD med {np.median(A[:,2]):+.1%} [{A[:,2].min():+.1%},{A[:,2].max():+.1%}]")
# riferimento: SKH standalone 50/50 e quota NON recuperabile (canonical - barclose)
rows_s = []
for off in OFFSETS:
fb, _, hb, db = stats4(SKB["barclose"][off])
fh, _, hh, dh = stats4(SKB["hourly"][off])
fc, _, hc, dc = stats4(SKB["canonical"][off])
rows_s.append((fb - fh, hb - hh, db - dh, fc - fb, hc - hb, dc - db))
S = np.array(rows_s)
print(f" SKH standalone 50/50: barclose-hourly dShFULL off0 {S[0,0]:+.3f} "
f"(med {np.median(S[:,0]):+.3f}), dShHOLD off0 {S[0,1]:+.3f} (med {np.median(S[:,1]):+.3f}), "
f"dDD off0 {S[0,2]:+.1%} (med {np.median(S[:,2]):+.1%})")
print(f" quota NON recuperabile da nessun cron (canonical-barclose = fill-al-livello): "
f"dShFULL off0 {S[0,3]:+.3f} (med {np.median(S[:,3]):+.3f}), "
f"dShHOLD off0 {S[0,4]:+.3f} (med {np.median(S[:,4]):+.3f}), "
f"dDD off0 {S[0,5]:+.1%} (med {np.median(S[:,5]):+.1%})")
print(" fattibilita' (SOLO STIMA, non cablata): 230 = 23x10 -> i confini 230m cadono sempre su")
print(" multipli di 10 min dall'epoch => un cron `*/10 * * * *` che esegue solo quando")
print(" (now % 230min)==0 colpisce ogni confine con ritardo ~0 (vs 0-50 min, media 25, del cron")
print(" orario): 'barclose' e' esattamente il suo modello (al netto di runtime job e slippage).")
print(" NB: la cadenza NON recupera il gap fill-al-livello (exit software in barre che gappano).")
# ---- (7) caveat --------------------------------------------------------
print("\n--- (7) CAVEAT (onesta') ---")
print("- exit software nei crash = GAP reale (audit: sl2% -> -11/-23% realizzato); 'hourly' e'")
print(" la stima pessimistica giusta del path live; niente slippage/parziali modellati;")
print("- equity daily-step (lens Sharpe), convenzione identica a canonico e audit r0702;")
print("- selezione: w* su IS mediano-di-banda (mai su hold-out); 8 w provati, k_seen=8 nel gate;")
print("- banda 23-offset = lente de-luckata; live gira UNA griglia sola (off 0);")
print("- deriva-dati vs audit del 2026-07-02 (il feed cresce ogni giorno): riconciliazione 'circa';")
print("- a $600 la gamba SKH a w=0.10 vale ~$60/trade (>> min-order $5): la granularita' non vincola.")
print(f"\nFatto in {time.time()-t0:.0f}s.")
if __name__ == "__main__":
main()
+102
View File
@@ -0,0 +1,102 @@
"""SNAPSHOT point-in-time supply stablecoin (DefiLlama) — sblocca il lead STABLE fra 12 mesi.
CONTESTO (2026-07-24, ondata on-chain). STABLE-supply-growth (gate risk-on su crescita 30g
della supply stablecoin totale, thr 10% ann.) e' stato l'unico candidato dell'ondata sopra
quasi tutti i gate (DSR 0.998, marginale ADDS, non-hedge) ma e' declassato a WATCH perche'
la storia DefiLlama e' RICOSTRUITA retroattivamente (chain/coin aggiunte nel tempo): il
"totale" visto oggi per il 2019-21 non era osservabile allora -> vintage-risk non sanabile.
Criterio di rivisita scritto a diario (2026-07-24-onchain-sentiment-wave.md): quando una
fonte POINT-IN-TIME della supply accumula >=12 mesi, ritestare il gate thr=10%. Questo
script E' quella fonte: cattura oggi cio' che e' osservabile oggi.
COSA FA (sola lettura, API pubblica tokenless, nessun ordine):
- GET https://stablecoins.llama.fi/stablecoins (lista coin + circolante corrente);
- 1 riga JSON per run APPESA a data/external/stable_snapshots/snapshots.jsonl:
totale USD-peg, totale all-peg, breakdown top-15 per simbolo, n_coins.
~1 KB/riga -> ~400 KB/anno. Idempotente per giorno: se esiste gia' una riga con la
stessa data UTC esce senza scrivere (safe da ri-run in cron).
CADENZA RACCOMANDATA (quando/se si decide di cablarla — NON e' in cron adesso):
1 run/giorno dopo mezzanotte UTC (es. 00:15), accanto agli altri snapshot.
USO:
uv run python scripts/research/r0724_stable_snapshot.py
NB ONESTO: questo script MISURA, non decide. Fra >=12 mesi la serie risponde a: il segnale
crescita-30g calcolato su dati point-in-time replica quello calcolato sulla storia
ricostruita? Se no, il lead STABLE era un artefatto di vintage e muore li'.
"""
from __future__ import annotations
import json
import sys
import time
from datetime import datetime, timezone
from pathlib import Path
import requests
PROJECT_ROOT = Path(__file__).resolve().parents[2]
OUT_DIR = PROJECT_ROOT / "data" / "external" / "stable_snapshots"
OUT_FILE = OUT_DIR / "snapshots.jsonl"
API = "https://stablecoins.llama.fi/stablecoins?includePrices=true"
def fetch() -> dict:
last = None
for _ in range(4):
try:
r = requests.get(API, timeout=30)
j = r.json()
if "peggedAssets" in j:
return j
last = str(j)[:200]
except Exception as e:
last = str(e)
time.sleep(2.0)
raise RuntimeError(f"DefiLlama fail: {last}")
def circ_usd(asset: dict) -> float:
c = asset.get("circulating") or {}
return float(sum(v for v in c.values() if isinstance(v, (int, float))))
def main() -> None:
today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
if OUT_FILE.exists():
for line in OUT_FILE.read_text().splitlines():
try:
if json.loads(line).get("date") == today:
print(f"SKIP: snapshot per {today} gia' presente")
return
except json.JSONDecodeError:
continue
data = fetch()
assets = data["peggedAssets"]
usd_peg = [a for a in assets if a.get("pegType") == "peggedUSD"]
tot_usd = sum(circ_usd(a) for a in usd_peg)
tot_all = sum(circ_usd(a) for a in assets)
top = sorted(usd_peg, key=circ_usd, reverse=True)[:15]
row = {
"date": today,
"snap_ts": int(time.time() * 1000),
"total_usd_peg": round(tot_usd, 2),
"total_all_peg": round(tot_all, 2),
"n_coins": len(assets),
"n_usd_peg": len(usd_peg),
"top15": {a.get("symbol", "?"): round(circ_usd(a), 2) for a in top},
"source": "stablecoins.llama.fi/stablecoins",
}
OUT_DIR.mkdir(parents=True, exist_ok=True)
with OUT_FILE.open("a") as f:
f.write(json.dumps(row, separators=(",", ":")) + "\n")
print(f"OK {today}: totale USD-peg ${tot_usd/1e9:.2f}B ({len(usd_peg)} coin, "
f"{len(assets)} totali) -> {OUT_FILE}")
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,79 @@
"""r0724_statarb_deploy_gate — gate di deploy PRE-REGISTRATO per STATARB-RESID (2026-07-24).
PERCHE' PRE-REGISTRATO: la regola si fissa a forward-day 25 (oggi), PRIMA di vedere
il giorno 90. Decidere "a occhio" dopo aver visto il numero = selection-on-forward,
lo stesso bias che il progetto ha gia' pagato (gate SELECTION-ON-HOLDOUT, 2026-06-29).
REGOLA (immutabile — ogni modifica va motivata nel diario come violazione):
- Data decisione: 2026-09-27 (90 giorni forward dal 2026-06-29). Non prima.
- Metrica: Sharpe annualizzato di net_modeled su TUTTA la finestra forward
(nessuna sottofinestra), da data/paper_statarb/returns.jsonl.
- Esiti:
Sharpe >= 0.5 -> CANDIDATO AL DEPLOY: proporre peso {10,15,20}% del book
Deribit, giudicato con weights_tilt_null + stima maxDD
combinata < 15%. Deploy solo se il tilt-null passa.
0 <= Sh < 0.5 -> ESTENDI una sola volta di altri 90g (decisione 2026-12-26).
Alla seconda scadenza: o >=0.5 o RITIRO. Niente terza proroga.
Sharpe < 0 -> RITIRO dal forward-monitor (l'edge DSR 0.929 non ha confermato).
- Guardie accessorie (tutte richieste per il deploy):
* fill-haircut cumulato |MODELED - REAL| < 0.5 pp (eseguibilita' reale a $600);
* config invariata (W=45, sgn=+1) — ogni ritocco azzera la finestra;
* maxDD forward < 10% (coerenza col profilo di ricerca, DD backtest ~11%).
Uso: `uv run python scripts/research/r0724_statarb_deploy_gate.py`
(si puo' lanciare in qualsiasi momento: ricorda la data-decisione e valuta lo stato).
"""
from __future__ import annotations
import json
from datetime import date
from pathlib import Path
import numpy as np
ROOT = Path(__file__).resolve().parents[2]
RETURNS = ROOT / "data" / "paper_statarb" / "returns.jsonl"
START = date(2026, 6, 29) # inizio forward (config congelata)
DECISION = date(2026, 9, 27) # 90g forward — PRIMA data utile di decisione
DECISION_EXT = date(2026, 12, 26) # unica proroga ammessa
SH_DEPLOY = 0.5
HAIRCUT_MAX_PP = 0.5
DD_MAX = 0.10
def main() -> None:
rows = [json.loads(x) for x in RETURNS.read_text().splitlines() if x.strip()]
rm = np.array([r["net_modeled"] for r in rows])
rr = np.array([r["net_real"] for r in rows])
n = len(rm)
sh = float(rm.mean() / rm.std() * np.sqrt(365)) if rm.std() > 0 else 0.0
eq = np.cumprod(1 + rm)
dd = float(np.max((np.maximum.accumulate(eq) - eq) / np.maximum.accumulate(eq)))
haircut_pp = abs(float((np.prod(1 + rm) - np.prod(1 + rr)) * 100))
today = date.today()
print("=" * 88)
print(" STATARB-RESID — gate di deploy PRE-REGISTRATO (fissato 2026-07-24, forward-day 25)")
print("=" * 88)
print(f" finestra forward : {rows[0]['dt'][:10]} -> {rows[-1]['dt'][:10]} ({n} barre)")
print(f" Sharpe fwd (mod) : {sh:+.2f} (soglia deploy >= {SH_DEPLOY})")
print(f" maxDD fwd : {dd:.1%} (guardia < {DD_MAX:.0%})")
print(f" fill-haircut cum : {haircut_pp:.2f} pp (guardia < {HAIRCUT_MAX_PP} pp)")
print(f" data decisione : {DECISION} (proroga unica: {DECISION_EXT})")
if today < DECISION:
print(f"\n -> NESSUNA DECISIONE OGGI ({today}): mancano {(DECISION - today).days} giorni.")
print(" Il numero corrente NON autorizza deploy anticipato (regola pre-registrata).")
else:
if sh >= SH_DEPLOY and dd < DD_MAX and haircut_pp < HAIRCUT_MAX_PP:
print("\n -> GATE SUPERATO: procedere con weights_tilt_null sui pesi candidati "
"{10,15,20}% e stima maxDD combinata (<15%) prima del deploy.")
elif sh >= 0:
print("\n -> SOTTO SOGLIA ma >=0: estensione unica fino al "
f"{DECISION_EXT} (se gia' estesa: RITIRO).")
else:
print("\n -> Sharpe forward NEGATIVO: RITIRO dal forward-monitor.")
if __name__ == "__main__":
main()
+283
View File
@@ -0,0 +1,283 @@
"""r0724_xs — LEAD-LAG cross-crypto (Guo/Sang/Tu/Wang JEDC 2024) + SALIENCE/MAX (Cai&Zhao
JBF 2024) sui 19 major Hyperliquid certificati (1d, 2024-2026).
TESI (2026-07-24). Due famiglie cross-sectional dalla letteratura accademica, mai meccanizzate
qui, sullo stesso universo/harness di XS01:
FAMIGLIA A — LEAD-LAG "cross-cryptocurrency return predictability" (diffusione lenta
dell'informazione: i ritardi degli ALTRI coin predicono il focale). Segnale
S_i(t) = media equal-weight dei ritorni a k giorni degli ALTRI asset (mai il proprio);
variante predictor-set = solo large-cap (BTC/ETH/SOL/BNB/XRP, leave-one-out se il focale
e' large). Book L/S top-5 / bottom-5 per S_i, hold H, come XS01.
Griglia: k in {3,7} x predictor in {all-others, large5} x H in {3,7} = 8 celle.
⚠️ IDENTITA' MATEMATICA (dichiarata PRIMA di guardare i numeri): con predictor
all-others equal-weight, S_i = (SommaTotale r_i)/(A1) e' AFFINE DECRESCENTE nel
ritorno proprio r_i → il rank cross-section di S e' ESATTAMENTE il rank inverso del
momentum proprio a k giorni: la cella "all-others" E' la short-term reversal (gia'
negativa nella griglia rev di xsec_research). La verifico empiricamente (rank-corr 1).
⚠️ DEGENERAZIONE large5: i 14 asset non-large hanno segnale IDENTICO (media dei 5
large) → la composizione del book fra i pari e' decisa dal tie-break dell'argsort
(stabile = ordine colonne), economicamente arbitraria. Dichiarata, non nascosta.
FAMIGLIA B — SALIENCE/MAX (lottery demand: i coin col massimo ritorno giornaliero
recente vengono sovracomprati → overpriced). MAX_i(t) = max ritorno 1d negli ultimi L
giorni; book LONG bottom-quintile MAX (k=4) / SHORT top-quintile, hold H.
Griglia: L in {7,30} x H in {7,10} = 4 celle.
GATE (CLAUDE.md, metodologia obbligatoria):
1. CAUSALE: segnale da close<=t, peso tenuto da t+1 (l'engine shifta W[i-1]*dret[i]).
2. NETTO fee 0.10% RT per gamba (FEE=0.001, addebitata FEE/2 per lato sul turnover,
identica a xsec_research/sleeves) + probe fee-zero sulla cella scelta (morte-per-fee
vs assenza di edge lordo).
3. Selezione cella IN-SAMPLE-ONLY (pre-2025-01-01), poi hold-out bloccato.
4. DEFLATED Sharpe (Bailey & Lopez de Prado, altlib) su TUTTE le celle della famiglia.
5. RIDONDANZA: corr giornaliera vs XS01 (sleeves._xsec_returns) e TP01
(altlib.tp01_baseline_daily) + uplift del blend 0.75*XS01+0.25*candidato (full e
hold-out). corr>0.5 a XS01 o uplift negativo -> REDUNDANT.
6. BANDA DI FASE del ciclo H sulla cella scelta (lezione anchor-luck 2026-07-02: i
numeri di strategie a ribilanciamento ancorato si citano con la banda).
CAVEAT IMMUTABILI: storia HL nativa ~2.5 anni (join 19 major = 2024-01-01+, 0 barre
sintetiche/vol=0 — verificato a runtime); IS = solo il 2024 (~330 giorni utili, ~48
ribilanci a H=7 → small-n); book L/S a 8-10 gambe NON eseguibile a $600 → STAT-MODE
come XS01 (eventuale esito positivo = forward-monitor, non deploy).
uv run python scripts/research/r0724_xs_leadlag_max.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" / "alt"))
sys.path.insert(0, str(PROJECT_ROOT))
import numpy as np
import pandas as pd
from altlib import deflated_sharpe, tp01_baseline_daily # noqa: E402
from src.portfolio.portfolio import HOLDOUT, metrics, to_daily # noqa: E402
from src.portfolio.sleeves import XS_UNIVERSE, _xsec_returns # noqa: E402
RAW = PROJECT_ROOT / "data" / "raw"
FEE = 0.001 # 0.10% RT per gamba (come xsec_research: FEE/2 per lato sul turnover)
TV = 0.20 # vol-target come XS01
WARMUP = 30 # warmup comune a tutte le celle (max lookback della griglia) -> stessi giorni
DPY = 365.25
LARGE5 = ["BTC", "ETH", "SOL", "BNB", "XRP"]
def load_universe():
"""Chiusure (e guardia volume) dei 19 major XS01. Inner-join = 2024+ nativo (0 backfill)."""
cols, vols = {}, {}
for s in XS_UNIVERSE:
d = pd.read_parquet(RAW / f"hl_{s.lower()}_1d.parquet")
idx = pd.to_datetime(d["timestamp"], unit="ms", utc=True)
cols[s] = pd.Series(d["close"].values.astype(float), index=idx)
vols[s] = pd.Series(d["volume"].values.astype(float), index=idx)
C = pd.concat(cols, axis=1, join="inner").sort_index().dropna()
V = pd.concat(vols, axis=1, join="inner").sort_index().reindex(C.index)
nz = int((V.values == 0).sum())
assert nz == 0, f"barre vol=0 nella finestra joined ({nz}): possibile backfill sintetico"
return C
def kret_matrix(px: np.ndarray, k: int) -> np.ndarray:
"""Ritorno a k giorni fino a close[t] (causale). NaN nelle prime k righe."""
R = np.full_like(px, np.nan, dtype=float)
R[k:] = px[k:] / px[:-k] - 1.0
return R
def sig_leadlag(px: np.ndarray, cols: list[str], k: int, pred: str) -> np.ndarray:
"""S_i(t) = media dei ritorni k-giorni degli ALTRI asset (mai il proprio).
pred='all' : tutti gli altri 18 (⚠️ affine decrescente nel proprio ritorno).
pred='large5': solo BTC/ETH/SOL/BNB/XRP (leave-one-out se il focale e' large)."""
R = kret_matrix(px, k)
n, A = R.shape
S = np.full((n, A), np.nan)
if pred == "all":
tot = R.sum(axis=1)
for i in range(A):
S[:, i] = (tot - R[:, i]) / (A - 1)
else:
li = [cols.index(s) for s in LARGE5]
totL = R[:, li].sum(axis=1)
for i in range(A):
if i in li:
S[:, i] = (totL - R[:, i]) / (len(li) - 1)
else:
S[:, i] = totL / len(li)
return S
def sig_neg_max(px: np.ndarray, L: int) -> np.ndarray:
"""score = MAX_i(t) (max ritorno 1d negli ultimi L giorni): rank alto = low-MAX = LONG."""
n, A = px.shape
dret = np.vstack([np.full((1, A), np.nan), px[1:] / px[:-1] - 1.0])
mx = pd.DataFrame(dret).rolling(L, min_periods=L).max().values
return -mx
def xs_book(C: pd.DataFrame, S: np.ndarray, H: int, k: int,
phase: int = 0, fee: float = FEE) -> tuple[pd.Series, dict]:
"""Book L/S market-neutral da matrice di segnale (rank a close[i], tenuto da i+1),
fee sul turnover, vol-target 20% causale — convenzioni identiche a XS01."""
px = C.values
n, A = px.shape
dret = np.vstack([np.zeros((1, A)), px[1:] / px[:-1] - 1.0])
W = np.zeros((n, A))
w = np.zeros(A)
nreb = nreb_is = 0
is_mask = C.index < HOLDOUT
for i in range(n):
if i >= WARMUP and (i - WARMUP - phase) % H == 0 and np.isfinite(S[i]).all():
order = np.argsort(S[i], kind="stable")
w = np.zeros(A)
w[order[-k:]] = 0.5 / k
w[order[:k]] = -0.5 / k
nreb += 1
nreb_is += int(is_mask[i])
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 / 2.0)
s = pd.Series(net, index=C.index)
rv = s.rolling(30, min_periods=15).std().shift(1) * np.sqrt(DPY)
scale = np.clip(np.nan_to_num(TV / rv.replace(0, np.nan).values, nan=0.0), 0, 3.0)
out = pd.Series(s.values * scale, index=C.index)
diag = dict(time_in_market=float((W != 0).any(axis=1).mean()),
turnover_yr=float(turn.sum() / (n / DPY)),
n_reb=nreb, n_reb_is=nreb_is)
return out, diag
def cell_row(d: pd.Series) -> dict:
f = metrics(d)
i = metrics(d[d.index < HOLDOUT])
h = metrics(d[d.index >= HOLDOUT])
return dict(full=f, ins=i, hold=h)
def redundancy(cand: pd.Series) -> dict:
"""Corr vs XS01/TP01 + uplift del blend 0.75*XS01+0.25*cand (finestra comune)."""
xs = to_daily(_xsec_returns())
tp = tp01_baseline_daily()
Jx = pd.concat({"xs": xs, "c": cand}, axis=1, join="inner").dropna()
Jt = pd.concat({"tp": tp, "c": cand}, axis=1, join="inner").dropna()
corr_xs = float(Jx["xs"].corr(Jx["c"])) if len(Jx) > 5 else float("nan")
corr_tp = float(Jt["tp"].corr(Jt["c"])) if len(Jt) > 5 else float("nan")
blend = 0.75 * Jx["xs"] + 0.25 * Jx["c"]
up_f = metrics(blend)["sharpe"] - metrics(Jx["xs"])["sharpe"]
bh = blend[blend.index >= HOLDOUT]
xh = Jx["xs"][Jx["xs"].index >= HOLDOUT]
up_h = metrics(bh)["sharpe"] - metrics(xh)["sharpe"]
return dict(corr_xs=corr_xs, corr_tp=corr_tp, uplift_full=up_f, uplift_hold=up_h,
xs_full=metrics(Jx["xs"])["sharpe"], xs_hold=metrics(xh)["sharpe"])
def phase_band(C, S, H, k) -> list[float]:
"""Sharpe FULL a ogni fase del ciclo H (lezione anchor-luck: cita la banda, non la fase 0)."""
return [metrics(xs_book(C, S, H, k, phase=p)[0])["sharpe"] for p in range(H)]
def run_family(name: str, C: pd.DataFrame, cells: list[tuple[str, np.ndarray, int, int]]):
"""cells = [(tag, S, H, k)]. Selezione IS-only, DSR su tutte le celle, ridondanza + fasi."""
print("\n" + "=" * 100)
print(f" FAMIGLIA {name}")
print("=" * 100)
print(f" {'cella':<30}{'IS Sh':>8}{'FULL Sh':>9}{'HOLD Sh':>9}{'DD%':>7}{'TiM':>6}{'to/yr':>7}{'reb':>5}")
rows = []
for tag, S, H, k in cells:
d, diag = xs_book(C, S, H, k)
m = cell_row(d)
rows.append((tag, S, H, k, d, diag, m))
print(f" {tag:<30}{m['ins']['sharpe']:>8.2f}{m['full']['sharpe']:>9.2f}"
f"{m['hold']['sharpe']:>9.2f}{m['full']['maxdd'] * 100:>7.1f}"
f"{diag['time_in_market']:>6.2f}{diag['turnover_yr']:>7.1f}{diag['n_reb']:>5}")
# selezione IN-SAMPLE-ONLY
best = max(rows, key=lambda r: r[6]["ins"]["sharpe"])
tag, S, H, k, d, diag, m = best
all_full = [r[6]["full"]["sharpe"] for r in rows]
dsr, sr0 = deflated_sharpe(m["full"]["sharpe"], all_full, d)
red = redundancy(d)
d0, _ = xs_book(C, S, H, k, fee=0.0)
m0 = cell_row(d0)
band = phase_band(C, S, H, k)
print(f"\n CELLA IS-BEST: {tag} (scelta sul solo IS Sharpe {m['ins']['sharpe']:.2f})")
print(f" FULL Sh {m['full']['sharpe']:.2f} DD {m['full']['maxdd'] * 100:.1f}% | "
f"HOLD Sh {m['hold']['sharpe']:.2f} DD {m['hold']['maxdd'] * 100:.1f}%")
print(f" deflated-Sharpe (N={len(rows)} celle): DSR={dsr:.3f} (null-max ~{sr0:.2f}) "
f"{'PASS' if dsr >= 0.95 else 'FAIL'}")
print(f" fee-zero probe: IS {m0['ins']['sharpe']:.2f} / FULL {m0['full']['sharpe']:.2f} / "
f"HOLD {m0['hold']['sharpe']:.2f} (edge lordo vs morte-per-fee)")
print(f" corr vs XS01 {red['corr_xs']:+.2f} | corr vs TP01 {red['corr_tp']:+.2f}")
print(f" blend 0.75*XS01+0.25*cand: uplift FULL {red['uplift_full']:+.2f} "
f"(XS01 solo {red['xs_full']:.2f}) | HOLD {red['uplift_hold']:+.2f} (XS01 solo {red['xs_hold']:.2f})")
print(f" banda di fase H={H} (Sh FULL): min {min(band):.2f} / med {np.median(band):.2f} / "
f"max {max(band):.2f} [fase0 = {band[0]:.2f}]")
print(f" ribilanci: {diag['n_reb']} totali, {diag['n_reb_is']} in IS"
+ (" ⚠️ SMALL-N (<50 in IS)" if diag['n_reb_is'] < 50 else ""))
return dict(tag=tag, m=m, dsr=dsr, red=red, band=band, diag=diag, fee0=m0)
def main():
C = load_universe()
cols = list(C.columns)
px = C.values
print("=" * 100)
print(f" r0724 LEAD-LAG + MAX — {len(cols)} major HL, {len(C)} giorni "
f"[{C.index[0].date()} -> {C.index[-1].date()}], fee {FEE * 100:.2f}% RT/gamba, "
f"holdout {HOLDOUT.date()}, IS = {int((C.index < HOLDOUT).sum())} giorni")
print("=" * 100)
# verifica empirica dell'identita' (Famiglia A, all-others): rank-corr(S, r_proprio) = -1
Sa = sig_leadlag(px, cols, 7, "all")
Ra = kret_matrix(px, 7)
i = len(px) // 2
rc = pd.Series(Sa[i]).corr(pd.Series(Ra[i]), method="spearman")
print(f"\n [check identita' A/all-others] rank-corr(S_i, ritorno proprio 7g) al giorno "
f"{C.index[i].date()}: {rc:+.3f} (atteso -1: la cella 'all' E' reversal del proprio momentum)")
# FAMIGLIA A — lead-lag: k x predictor x H = 8 celle (k lati book = 5 come XS01)
cells_a = []
for kk in (3, 7):
for pred in ("all", "large5"):
S = sig_leadlag(px, cols, kk, pred)
for H in (3, 7):
cells_a.append((f"LL k{kk} {pred:<7} H{H}", S, H, 5))
res_a = run_family("A — LEAD-LAG cross-crypto (8 celle)", C, cells_a)
# FAMIGLIA B — MAX/salience: L x H = 4 celle (quintile: 4 gambe/lato)
cells_b = []
for L in (7, 30):
S = sig_neg_max(px, L)
for H in (7, 10):
cells_b.append((f"MAX L{L} H{H}", S, H, 4))
res_b = run_family("B — SALIENCE/MAX lottery-demand (4 celle)", C, cells_b)
print("\n" + "=" * 100)
print(" VERDETTO (bar: DSR>=0.95, corr XS01<0.5, uplift blend>0, hold-out>0, small-n onesto)")
print("=" * 100)
for nm, r in (("A LEAD-LAG", res_a), ("B MAX", res_b)):
flags = []
if r["dsr"] < 0.95:
flags.append(f"DSR {r['dsr']:.2f}<0.95")
if abs(r["red"]["corr_xs"]) > 0.5:
flags.append(f"corrXS01 {r['red']['corr_xs']:+.2f}")
if r["red"]["uplift_full"] < 0 or r["red"]["uplift_hold"] < 0:
flags.append("uplift blend negativo")
if r["m"]["hold"]["sharpe"] <= 0:
flags.append("hold-out <=0")
if r["diag"]["n_reb_is"] < 50:
flags.append("small-n IS")
print(f" {nm}: cella {r['tag']}" + ("; ".join(flags) if flags else "tutti i gate passati"))
print("\n NB: STAT-MODE (8-10 gambe, non eseguibile a $600); storia ~2.5 anni; IS = solo 2024.")
if __name__ == "__main__":
main()
+6 -3
View File
@@ -60,11 +60,14 @@ def test_paper_advance_matches_backtest_slice():
# equity sull'ultimo tratto (skip warmup)
tail = combo[-500:]
eq_ref = np.cumprod(1.0 + np.clip(tail, -0.99, None))
# ricostruzione "alla paper" deve dare lo stesso fattore
factor = float(eq_ref[-1] / eq_ref[0])
# ricostruzione "alla paper" deve dare lo stesso fattore. NB fix 2026-07-24:
# l'equity parte da 1.0 PRIMA della fetta -> il fattore della fetta e' eq_ref[-1].
# (Il vecchio eq_ref[-1]/eq_ref[0] divideva via (1+tail[0]): passava solo finche'
# combo[-500] era esattamente 0.0 — bug latente emerso con l'avanzare dei dati.)
factor = float(eq_ref[-1])
assert factor > 0
# sanity: il fattore equivale al prodotto dei (1+combo)
assert np.isclose(factor, np.prod(1.0 + np.clip(tail, -0.99, None)) / (1.0), rtol=1e-9)
assert np.isclose(factor, np.prod(1.0 + np.clip(tail, -0.99, None)), rtol=1e-9)
def test_tsmom_blend_range():