Compare commits
12 Commits
7534b08be0
...
05d069f336
| Author | SHA1 | Date | |
|---|---|---|---|
| 05d069f336 | |||
| 636be89b2b | |||
| 00996640fb | |||
| 4c1961d77b | |||
| b65f067f69 | |||
| 2320fa9cec | |||
| 5c48a8322a | |||
| 2e7178487d | |||
| acfba63420 | |||
| 870428e559 | |||
| 7ebb1b8c37 | |||
| ddc980a097 |
@@ -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/
|
||||
|
||||
@@ -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`.
|
||||
@@ -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).
|
||||
@@ -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.20–0.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 = (1−w)·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.30–0.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: barclose−hourly Δ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
|
||||
0–50 min del cron orario è **rumore, non un costo sistematico**. Il vero gap è il
|
||||
**fill-al-livello** (canonical−barclose: Δ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.20–0.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.
|
||||
@@ -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)/(A−1) è 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.
|
||||
@@ -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.
|
||||
|
||||
Executable
+16
@@ -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
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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())
|
||||
@@ -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}")
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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)/(A−1) 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()
|
||||
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user