research(anchor-audit): timing-luck confermato su XS01 e SKH01 — 3/3 sleeve ancorati, book de-luckato HOLD ~2.0

Chiude il pendente dell'ondata timing 2026-07-02. Due audit indipendenti
(sanity replica bit-exact, ancore a priori, zero tuning per-fase, bootstrap):

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

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-07-02 22:34:35 +00:00
parent 73d74c5e53
commit a74cc69583
5 changed files with 1054 additions and 1 deletions
+25 -1
View File
@@ -57,6 +57,13 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis
regime compatto). Standalone FULL Sh **1.50** / HOLD 1.71 / DD 11%, plateau robusto (lookback, gate regime compatto). Standalone FULL Sh **1.50** / HOLD 1.71 / DD 11%, plateau robusto (lookback, gate
p15-35). **Caveat:** storia ~2.5 anni; STAT-MODE (book a 19 gambe non eseguibile a 2k, serve ~20k) → p15-35). **Caveat:** storia ~2.5 anni; STAT-MODE (book a 19 gambe non eseguibile a 2k, serve ~20k) →
monitor forward. NB il gate concentra XS nei regimi dispersi (2025-26 = hold-out alta-dispersione). monitor forward. NB il gate concentra XS nei regimi dispersi (2025-26 = hold-out alta-dispersione).
⚠️ **PHASE TIMING-LUCK (2026-07-02):** i numeri headline sono sulla fase 0 del ciclo H=10, che è
al **15° pctl di DD** (10.8% vs ~15.5% fase tipica, 29% peggiore) e 85° di FULL fra le 10 fasi
(HOLD solo 65°, non estremo); P(spike per caso)≈0.91-0.94. Lens onesta = **ensemble di fase:
FULL 1.25 / HOLD 1.31 / DD 10.9%**; a fase mediana FULL 1.08/HOLD 1.10/DD 21%. La decisione di
ammissione @15% regge (0 fasi negative, 8/10 FULL≥1.0), i numeri 1.50/1.71/11% no → citarli con
banda di fase. Ora-del-giorno NON testabile (solo 1d HL). Script `scripts/research/r0702_anchor_xs01.py`;
diario `2026-07-02-anchor-audit-xs01-skh01.md`.
Ricerca `scripts/portfolio/{xsec_research,xsec_blend,xsec_dispgate}.py`. Diari `2026-06-19-hyperliquid-xsec` Ricerca `scripts/portfolio/{xsec_research,xsec_blend,xsec_dispgate}.py`. Diari `2026-06-19-hyperliquid-xsec`
/ `-xsec-blend` / `-xsec-dispgate` / `-xsec-universe-expansion` / `-trend-multiasset`. / `-xsec-blend` / `-xsec-dispgate` / `-xsec-universe-expansion` / `-trend-multiasset`.
- **PORTAFOGLIO ATTIVO = TP01 (33%) + XS01 (15%) + VRP01 (12%) + SKH01 (20%) + GTAA01 (20%)** - **PORTAFOGLIO ATTIVO = TP01 (33%) + XS01 (15%) + VRP01 (12%) + SKH01 (20%) + GTAA01 (20%)**
@@ -75,6 +82,11 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis
Test `tests/test_gtaa_sleeve.py`; diario `2026-07-01-strategy-wave-6threads.md` (addendum GTAA). Test `tests/test_gtaa_sleeve.py`; diario `2026-07-01-strategy-wave-6threads.md` (addendum GTAA).
Report `scripts/portfolio/run_portfolio.py`. Sleeve a date d'inizio diverse → outer-join con pesi Report `scripts/portfolio/run_portfolio.py`. Sleeve a date d'inizio diverse → outer-join con pesi
rinormalizzati (TP01/SKH01/GTAA dal 2019*, VRP dal 2021, XS dal 2024; *GTAA troncato all'era book). rinormalizzati (TP01/SKH01/GTAA dal 2019*, VRP dal 2021, XS dal 2024; *GTAA troncato all'era book).
⚠️ **ANCHOR-LUCK del book (2026-07-02):** l'HOLD 2.46 è calcolato con TUTTI gli sleeve ancorati
alla loro ancora canonica, che per TP01/XS01/SKH01 è al top della rispettiva banda (eredità di
fortuna ~+0.10/+0.17/+0.5 HOLD). **Stima de-luckata onesta: HOLD ~1.9-2.1, FULL ~2.0-2.2, DD ~6%
invariato** — il book resta positivo e diversificato a ogni ancora testata, ma 2.46 è un massimo
di configurazioni d'ancora, non la stima centrale. Diario `2026-07-02-anchor-audit-xs01-skh01.md`.
- **SKH01-V2-DD "Skyhook" — DIVERSIFICATORE quasi-ortogonale (research)** — `src/strategies/skyhook.SKH01_V2_DD`, - **SKH01-V2-DD "Skyhook" — DIVERSIFICATORE quasi-ortogonale (research)** — `src/strategies/skyhook.SKH01_V2_DD`,
sleeve `src/portfolio/sleeves._skyhook_returns`. Sistema dual-TF (segnale 690m / exec 230m) regime sleeve `src/portfolio/sleeves._skyhook_returns`. Sistema dual-TF (segnale 690m / exec 230m) regime
(BuzVola/BuzVolume tipo-Chande) AND pattern (Donchian breakout), NON trend-follower, L/S. Vincitrice (BuzVola/BuzVolume tipo-Chande) AND pattern (Donchian breakout), NON trend-follower, L/S. Vincitrice
@@ -84,6 +96,16 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis
robust_oos multicut 7/7, is_hedge=False); blend 0.75·TP01+0.25·SKH **hold-out 0.31→1.17**. Verificato robust_oos multicut 7/7, is_hedge=False); blend 0.75·TP01+0.25·SKH **hold-out 0.31→1.17**. Verificato
leak-free + 2 scettici. **CAVEAT:** equity daily-step (Sharpe lens), ETH DD margine sottile, book 230m leak-free + 2 scettici. **CAVEAT:** equity daily-step (Sharpe lens), ETH DD margine sottile, book 230m
(costi ribilanciamento da verificare a deploy) → research win, forward-monitor. Diario `2026-06-23-skyhook.md`. (costi ribilanciamento da verificare a deploy) → research win, forward-monitor. Diario `2026-06-23-skyhook.md`.
⚠️ **GRID TIMING-LUCK (2026-07-02, più forte di TP01):** i numeri headline sono sull'offset 0 della
griglia 230m/690m, al **93-98° pctl dei 23 offset a priori** — minHold +1.26, blend 1.17 e book
HOLD 2.44 sono il MASSIMO dei 23 (mediane: minHold +0.39, blend 0.72, book 1.96); spike, non
plateau (±30m crolla); P(spike)≈0.70. **Il gate DD<30% (criterio di selezione di V2-DD) fallisce
in 15/23 offset** (mediana ETH 29.2%). Regge de-luckato: uplift blend positivo a TUTTE le 23 fasi
(min +0.18, med +0.42) + corr 0.05-0.11 → ADDS sopravvive ridimensionato. **LIVE (SKH=25% del book
Deribit):** path reale cron orario + exit software → book 50/50 FULL 1.46→1.19 / HOLD 1.64→1.15 /
DD 18→25%; nei crash gap-through-stop reale (sl2% modellato → 11/23% realizzato). Pesi/book
INVARIATI (ogni cambio passa weights_tilt_null); follow-up: cadenza 230m, peso live da rivedere.
Script `scripts/research/r0702_anchor_skh01.py`; diario `2026-07-02-anchor-audit-xs01-skh01.md`.
- **VRP01 Options Short-Vol — DIVERSIFICATORE da FinanceOld/OptionsAgent** — `src/portfolio/sleeves._vrp_combo_returns`. - **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 Put credit spread settimanale (vendi put -0.28, compra put -0.10) gated su IV-rank. Idee portate da
`../FinanceOld/OptionsAgent` (Bear Call Spread + gate d'ingresso). Migliora il lead VRP nudo `../FinanceOld/OptionsAgent` (Bear Call Spread + gate d'ingresso). Migliora il lead VRP nudo
@@ -209,7 +231,9 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis
netto ~0 → SCARTATO. (3) **Anchor timing-luck TP01 + tranching**: finding confermato (dettagli nel netto ~0 → SCARTATO. (3) **Anchor timing-luck TP01 + tranching**: finding confermato (dettagli nel
bullet TP01); tranching K=2/4 = sola riduzione della varianza della STIMA (ΔSharpe n.s., ΔDD ~0.5pt), bullet TP01); tranching K=2/4 = sola riduzione della varianza della STIMA (ΔSharpe n.s., ΔDD ~0.5pt),
NO deploy a $600 (il min-order lo degenera in K=1; serve feed intraday fuori path certificato) — NO deploy a $600 (il min-order lo degenera in K=1; serve feed intraday fuori path certificato) —
rivalutare a ≥5-10k; ⚠️ audit d'ancora PENDENTE su XS01 (rebalance 10g) e SKH01 (fase 230m/690m). rivalutare a ≥5-10k. **Audit d'ancora ESEGUITO su XS01 e SKH01 (stesso giorno): il finding si
replica su 3/3 sleeve ancorati** — vedi ⚠️ nei rispettivi bullet e la stima de-luckata del book
nel bullet portafoglio; diario `2026-07-02-anchor-audit-xs01-skh01.md`.
(4) **Clock lenti (2-7g) + bande isteresi**: fee drag di TP01 = ~0.4%/anno = tetto di ogni risparmio; (4) **Clock lenti (2-7g) + bande isteresi**: fee drag di TP01 = ~0.4%/anno = tetto di ogni risparmio;
il lag costa più del risparmio (HOLD ensemble 0.34→0.11 da N=2 a 7); a $600 **il min-order $5 è GIÀ la il lag costa più del risparmio (HOLD ensemble 0.34→0.11 da N=2 a 7); a $600 **il min-order $5 è GIÀ la
banda ottimale** (ordini 74% a costo ~0) → nessun cambio al book. (5) **Velocità trend banda ottimale** (ordini 74% a costo ~0) → nessun cambio al book. (5) **Velocità trend
@@ -0,0 +1,132 @@
# 2026-07-02 — Audit anchor timing-luck su XS01 e SKH01 (chiusura del pendente dell'ondata TIMING)
**Contesto.** L'ondata TIMING+CRT dello stesso giorno (`2026-07-02-timing-crt-wave.md`) ha stabilito
che l'hold-out di TP01 (0.31) è la **migliore delle 24 ancore daily possibili** (mediana 0.04,
P~0.86 di uno spike così per caso). Lo scettico ha flaggato l'audit come **pendente** sugli altri
due sleeve a ribilanciamento/griglia ancorata: **XS01** (fase del ciclo di ribilanciamento H=10
giorni) e **SKH01** (offset della griglia dual-TF 230m/690m, `origin='epoch'`). Questo diario
chiude entrambi. Due agenti indipendenti, metodologia identica allo scettico TP01
(`r0702_skeptic_offset.py`): ancore dichiarate **a priori**, parametri **identici** in ogni
fase/offset (zero tuning per-ancora), **sanity replica bit-exact** della sleeve canonica prima di
qualsiasi numero, block-bootstrap congiunto per P(spike).
**Script:** `scripts/research/r0702_anchor_xs01.py`, `scripts/research/r0702_anchor_skh01.py`.
Nessun file di produzione toccato dagli agenti (solo docstring/narrativa aggiornate a valle, qui).
---
## Verdetto in una riga
**Il finding TP01 si replica su TUTTI e tre gli sleeve ancorati — su SKH01 più forte che su TP01 —
e i numeri headline del book (HOLD 2.46) sono in parte best-of-anchors.** Ciò che sopravvive
de-luckato: XS01 resta un diversificatore ammissibile (ensemble di fase FULL 1.25 / HOLD 1.31 /
DD 11%); SKH01 resta ADDS (uplift del blend positivo a **tutte** le 23 fasi, corr ~0.08) ma i suoi
numeri headline e — cruciale — **il gate DD<30% che l'ha selezionata** non reggono alla fase
mediana. Stima onesta de-luckata del book 5-sleeve: **HOLD ~1.92.1, FULL ~2.02.2** (vs 2.46/2.24
dichiarati).
---
## XS01 — fase del ciclo di ribilanciamento H=10 (10 fasi)
Sanity: replica di `_xsec_returns` con parametro `phase`**max|Δ|=0.0 bit-exact** vs la canonica.
NB: la canonica sui dati di oggi fa FULL 1.367/HOLD 1.479/DD 10.8% (i dichiarati 1.50/1.71/11%
erano del 2026-06-19; deriva dati, confronto interno coerente).
| metrica | min | mediana | max | canonica (f0) | pctl f0 |
|---|---|---|---|---|---|
| Sh FULL | 0.41 | 1.21 | 1.64 | 1.37 | 85° |
| Sh HOLD | 0.15 | 1.17 | 1.71 | 1.48 | 65° |
| maxDD | 10.0% | 15.5% | 29.1% | 10.8% | **15°** |
- **Firma diversa da TP01: la fortuna NON è nell'hold-out** (65° pctl, non estremo) **ma nel
DD** (15° pctl: il "DD 11%" dichiarato è uno dei più bassi delle 10 fasi; la fase tipica fa
~15.5%, la peggiore 29%) e nel FULL (85°).
- **Bootstrap**: spike osservato +0.38 su hold-out; **P(una fase qualsiasi ≥ +0.38) = 0.910.94**
(block 10/20/40, B=4000) → la canonica è il massimo atteso di 10 stime correlate (corr mediana
fra fasi 0.71).
- **Ensemble 10 fasi** (lens di reporting onesta): **FULL 1.25 / HOLD 1.31 / DD 10.9%** — la
diversificazione di fase riporta il DD al livello canonico con Sharpe ≈ mediana.
- **Book 5-sleeve**: HOLD 2.44 (canonica) → 2.26 (fase mediana) → 1.75 (peggiore, marginalmente
PEGGIO del book senza XS01 = 1.77) → 2.35 (ensemble). **Eredità di fortuna ~+0.17 HOLD.**
FULL quasi insensibile (2.102.28).
- **Gate di ammissione a fase mediana**: FULL 1.08 / HOLD 1.10 / DD 20.7% — **i numeri dichiarati
non reggono** (Sharpe 0.3/0.6, DD raddoppia). **La decisione però regge**: nessuna fase
negativa, 8/10 con FULL≥1.0, book migliore a ogni fase tranne la peggiore (dove è neutro) →
slot da diversificatore @15% difendibile; cambia la NARRATIVA (banda/ensemble, non 1.50/1.71/11%).
- **Caveat**: storia ~2.5 anni (CI95 Sh HOLD canonica **[0.04, +2.72]** — nessuna stima puntuale
affidabile); dimensione **ora-del-giorno non testabile** (solo barre 1d HL: resta luck residua
10 fasi × 24 ore non certificabile); XS01 è STAT-MODE → impatto su reporting/gate, non sul book
live Deribit.
## SKH01-V2-DD — offset della griglia dual-TF 230m/690m (23 offset × 30m)
Sanity: offset 0 riproduce `_skyhook_returns()` **bit-exact** (max|Δ|=0.0 su 2666 giorni); maxDD
harness BTC 21.4%/ETH 27.4% = identici al diario di ammissione; riuso diretto di
`skyhook_entries`/`htf_features`/`merge_htf_to_ltf` con `resample(origin='epoch', offset=...)`,
confini 690 ⊂ 230 verificati a ogni offset.
| metrica (book 50/50) | min | mediana | max | off0 (canonico) | pctl off0 |
|---|---|---|---|---|---|
| Sh FULL | 0.78 | 1.05 | 1.48 | 1.46 | 93° |
| Sh HOLD | 0.44 | 0.93 | 1.64 | 1.64 | **98° (max)** |
| minFull (2 asset) | 0.35 | 0.67 | 1.01 | 0.99 | 93° |
| minHold | 0.10 | 0.39 | 1.25 | 1.25 | **98° (max)** |
| blend 0.75·TP01+0.25·SKH, HOLD | 0.49 | 0.72 | 1.16 | 1.16 | **98° (max)** |
- **È uno spike, non un plateau**: gli offset adiacenti ±30m crollano (book HOLD 1.64 → 0.88 a
off30). Firma identica all'ancora TP01, ma **più forte**: il canonico è al 9398° pctl di OGNI
metrica; minHold +1.26 e blend "0.31→1.17" sono il **massimo dei 23**.
- **Gate DD<30% — il criterio per cui V2-DD fu selezionata — FALLISCE in 15/23 offset**
(mediana DD: BTC 23.5%, ETH **29.2%** — pass per 0.8pt; peggiori 35.9%/42.1%). Il margine ETH
"già sottile" (27.4%) è in realtà **sotto la mediana di fase**: la proprietà DD<30% è in larga
parte fortuna di fase.
- **Ciò che regge de-luckato**: **uplift del blend positivo a TUTTE le 23 fasi** (min +0.18,
mediana +0.42, canonico +0.86) e corr→TP01 in [0.05, 0.11] a ogni fase → il verdetto
ADDS/diversificatore sopravvive, ridimensionato. Alla fase peggiore invece non regge
(minHold 0.10, DD fail).
- **Bootstrap**: spike blend +0.44; **P(un offset qualsiasi ≥ +0.44) ≈ 0.70** (block 10/20/40);
P(uplift≤0) = 0.030.04 → l'uplift è reale, l'ampiezza è best-of-23.
- **Book 5-sleeve**: HOLD 2.44 (off0 = max dei 23) → mediana **1.96** → ensemble 2.12; FULL 2.24 →
mediana 2.01. **Eredità di fortuna ~+0.5 HOLD** — molto più dei ~+0.10 dell'ancora TP01.
- **Rilevanza LIVE (SKH = 25% del book Deribit reale)**: il cron è orario, i confini 230m no →
ritardo uniforme {0..50} min (media 25). Re-sim del path reale (fill al close 5m del prossimo
run orario, exit SOFTWARE come da `book.py`): book 50/50 **FULL 1.46→1.19, HOLD 1.64→1.15,
DD 18.1%→24.6%**. La maggior parte del gap è l'assunzione fill-al-livello del backtest, non il
ritardo orario in sé: nei crash le barre 230m **gappano attraverso lo SL software** (short
sl2% modellato 2% → realizzato **11/23%**: ETH 2021-05-19, 2021-01-04, COVID 2020-03-13).
Il disaster-SL on-book 30% copre solo la coda estrema della posizione netta.
- **Caveat**: equity daily-step (lens Sharpe, come il canonico); l'ensemble di offset NON è
eseguibile live (gira una sola griglia) → serve solo come stima de-luckata; sim hourly senza
slippage/parziali.
## Book 5-sleeve — stima de-luckata congiunta (approssimata)
Ogni audit varia UN solo sleeve (gli altri restano canonici), quindi gli effetti non si sommano
esattamente; ordine di grandezza dell'eredità di fortuna sull'HOLD 2.46: TP01-ancora ~+0.10,
XS01-fase ~+0.17, SKH01-offset ~+0.5. **Stima onesta: HOLD ~1.92.1, FULL ~2.02.2, DD ~6%
(invariato)** — il book resta largamente positivo e diversificato a ogni combinazione testata,
ma il 2.46 è un massimo di ~24×10×23 configurazioni di ancora, non la stima centrale.
## Regole codificate in narrativa (CLAUDE.md aggiornato)
1. **I numeri headline di OGNI sleeve ancorato si citano con la banda d'ancora** (o come ensemble
di fase): vale già per TP01, ora anche per XS01 e SKH01 e per l'HOLD del book.
2. **Un gate di selezione (es. DD<30%) va verificato sulla banda di fase, non sull'ancora
canonica** — SKH01-V2-DD fu selezionata per una proprietà che tiene in 8/23 fasi.
3. Il candidato gate `anchor_luck_band()` in altlib sale di priorità: tre sleeve su tre ancorati
mostrano la stessa firma, e il deflated-Sharpe non conta questo multiple-testing.
## Follow-up (non eseguiti, da valutare)
- **Rivalutare il peso 25% di SKH01 nel book live Deribit**: l'aspettativa onesta della gamba SKH
è ≈ fase-mediana + path orario (blend HOLD ~0.72, non 1.17) con gap-through-stop reale nei
crash. L'uplift resta positivo a ogni fase → non c'è motivo di rimuoverla, ma il sizing fu
deciso su numeri best-of-23.
- Cadenza cron a 230m (o trigger sui confini di barra) per chiudere il gap di path orario.
- `anchor_luck_band()` in `altlib.py` come gate standard per candidati ancorati.
- Audit ora-del-giorno di XS01 quando esisterà un feed HL intraday certificato.
**Stato:** XS01/SKH01/pesi/book live INVARIATI (nessun cambio operativo da questo audit — è un
audit di reporting e di gate, e ogni cambio pesi dovrà passare `weights_tilt_null` +
selection-on-holdout). Test suite 168 verdi. Script audit committati.
+513
View File
@@ -0,0 +1,513 @@
#!/usr/bin/env python
"""r0702_anchor_skh01.py — AUDIT timing-luck della FASE della griglia dual-TF di SKH01-V2-DD.
CONTESTO (diario 2026-07-02-timing-crt-wave.md): l'hold-out di TP01 (Sharpe 0.31) si e'
rivelato la MIGLIORE delle 24 ancore orarie possibili (mediana 0.04) — timing-luck
dell'ancora, dimensione di multiple-testing non contata dal deflated-Sharpe. SKH01 e' nel
BOOK LIVE Deribit (TP01+SKH01 75/25) -> audit analogo, con rilevanza operativa diretta.
SPAZIO DI LUCK: l'origine della griglia. SKH01 resampla il 5m certificato a 230m (exec) e
690m (segnale) con origin='epoch'. Shiftando l'origin di k x 5m si spostano COERENTEMENTE
entrambe le griglie (690 = 3 x 230 -> i confini HTF restano sottoinsieme dei LTF); la
struttura congiunta si ripete con periodo 690m = 138 step da 5m. Campioniamo 23 OFFSET
UNIFORMI (ogni 30m) su [0, 690), dichiarati A PRIORI — offset 0 = canonico. Parametri
IDENTICI (SKH01_V2_DD) su tutti gli offset; nessuna selezione.
NB strutturale: 230m e 690m NON dividono 24h -> la griglia MIGRA attraverso la giornata
(nessun offset "possiede" un'ora del giorno) — l'ancora e' meno "speciale" a priori di
quella daily di TP01, ma va misurato.
COSA FA:
1. SANITY: a offset 0 riproduce ESATTAMENTE la serie daily di _skyhook_returns()
(max|dif| ~0) e i confini HTF c LTF per ogni offset.
2. Per 23 offset x {BTC,ETH}: Sharpe FULL/IS/HOLD (equity daily-step, convenzione
canonica), maxDD (equity harness), n trade -> tabella + min/med/max + pctl di off 0.
3. GATE ammissione ri-valutati alla mediana/peggiore: (a) maxDD<30% entrambi gli asset,
(b) minFull ~0.99 / minHold ~1.26, (c) blend 0.75*TP01+0.25*SKH hold-out (baseline
al.tp01_baseline_daily; claim 0.31->1.17), (d) corr a TP01 (~0.09).
4. BOOK 5-sleeve 33/15/12/20/20 (combine_outer, outer-join rinormalizzato, era crypto):
HOLD/FULL con SKH a ogni offset + ensemble degli offset (lens de-luckato, NON eseguibile).
5. BOOTSTRAP (block ~20g, maniera scettico r0702_skeptic_offset): P(un offset qualsiasi
mostri uno spike >= quello del canonico sull'hold-out del blend).
6. RILEVANZA LIVE: il cron del book e' ORARIO (crontab 0 * * * * -> scripts/cron_book.sh)
ma i confini 230m NON sono allineati all'ora (230 mod 60 = 50 -> ciclo di 6 barre,
ritardo confine->prossima-ora in {0,10,20,30,40,50} min). Quantifica la distribuzione
e l'impatto: re-sim con fill al close 5m del PROSSIMO multiplo orario dopo la chiusura
della barra di segnale/exit (entry E exit software ritardati; detection dei trade
INVARIATA -> isola il puro effetto prezzo-esecuzione). Sanity: la modalita' canonica
della re-sim riproduce backtest_signals bit-exact.
CAVEAT DICHIARATI:
- equity daily-step (lens Sharpe) come il canonico — non e' mark-to-market intrabar;
- costi di ribilanciamento del book 230m gia' flaggati a deploy (diario skyhook);
- l'ensemble di offset NON e' eseguibile live (una sola griglia gira): serve SOLO come
stima de-luckata;
- la sim 'hourly' modella fill a close 5m del multiplo orario (cron a minuto 0, runtime
del job trascurato) e NON modella slippage/parziali.
TECNICA: mai .view('int64') su tz-aware (epoca esplicita in ms); htf_features/merge_htf_to_ltf
RIUSATE via skyhook_entries (importate, non riscritte). Vincoli: nessun file toccato fuori da
questo script; niente commit. Runtime ~3-6 min (46 run skyhook + book + bootstrap).
"""
from __future__ import annotations
import sys
import time
from functools import lru_cache
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" / "alt"))
sys.path.insert(0, str(ROOT))
import altlib as al # noqa: E402
from src.backtest.harness import backtest_signals # noqa: E402
from src.data.downloader import load_data # noqa: E402
from src.portfolio.portfolio import combine_outer, to_daily # noqa: E402
from src.strategies.skyhook import ( # noqa: E402
HTF_MIN, LTF_MIN, SKH01_V2_DD, skyhook_entries)
HOLDOUT = al.HOLDOUT
ASSETS = ("BTC", "ETH")
OFFSETS = tuple(range(0, 690, 30)) # 23 offset a priori (ogni 30m su [0,690)), 0 = canonico
MS5 = 300_000
MSH = 3_600_000
MS_LTF = LTF_MIN * 60_000
BLEND_W = {"TP": 0.75, "SKH": 0.25}
BOOK_W = {"TP": 0.33, "XS": 0.15, "VRP": 0.12, "SKH": 0.20, "GTAA": 0.20}
B_BOOT = 4000
# ---------------------------------------------------------------------------
# Dati + resample con fase spostata (identico a skyhook.resample_5m + offset)
# ---------------------------------------------------------------------------
@lru_cache(maxsize=4)
def get5m(asset: str) -> pd.DataFrame:
df = load_data(asset, "5m").reset_index(drop=True)
if "datetime" not in df.columns:
df["datetime"] = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
return df
def resample_off(df5: pd.DataFrame, minutes: int, off: int) -> pd.DataFrame:
"""Identico a skyhook.resample_5m ma con griglia spostata di `off` minuti:
origin='epoch' + offset -> confini a epoch + off + n*minutes. Con `off` comune a
230m e 690m (690=3x230) i confini HTF restano sottoinsieme dei confini LTF."""
g = df5[["timestamp", "open", "high", "low", "close", "volume"]].copy()
g.index = pd.to_datetime(g["timestamp"], unit="ms", utc=True)
out = (g.resample(f"{minutes}min", label="left", closed="left", origin="epoch",
offset=pd.Timedelta(minutes=off))
.agg({"open": "first", "high": "max", "low": "min",
"close": "last", "volume": "sum"})
.dropna(subset=["open"]))
out["datetime"] = out.index
epoch = pd.Timestamp("1970-01-01", tz="UTC")
out["timestamp"] = ((out.index - epoch) // pd.Timedelta(milliseconds=1)).astype("int64")
return out.reset_index(drop=True)[
["timestamp", "open", "high", "low", "close", "volume", "datetime"]]
# ---------------------------------------------------------------------------
# Run per (asset, offset) — pipeline IDENTICA a sleeves._skyhook_returns
# ---------------------------------------------------------------------------
_CACHE: dict = {}
def run_asset(asset: str, off: int):
"""(daily equity-step series, Metrics, ltf, entries) per un asset a fase `off`."""
key = (asset, off)
if key in _CACHE:
return _CACHE[key]
df5 = get5m(asset)
ltf = resample_off(df5, LTF_MIN, off)
htf = resample_off(df5, HTF_MIN, off)
# confini HTF c confini LTF: vale per TUTTE le barre tranne al piu' la PRIMA (parziale:
# se il feed parte a meta' di un bin 690m il sub-bin 230m con la stessa label puo' essere
# vuoto). Identico al comportamento del canonico build_frames (origin='epoch').
assert np.isin(htf["timestamp"].values[1:], ltf["timestamp"].values).all(), \
f"confini HTF NON sottoinsieme dei LTF (asset={asset}, off={off})"
ent = skyhook_entries(ltf, htf, SKH01_V2_DD) # riusa htf_features/merge_htf_to_ltf
m = backtest_signals(ltf, ent, fee_rt=0.001, leverage=1.0, asset=asset, tf="230m")
s = pd.Series(m.equity, index=pd.DatetimeIndex(pd.to_datetime(m.eq_index, utc=True)))
daily = s.resample("1D").last().ffill().pct_change().dropna()
_CACHE[key] = (daily, m, ltf, ent)
return _CACHE[key]
@lru_cache(maxsize=32)
def skh_port(off: int) -> pd.Series:
"""Book 50/50 BTC+ETH daily-step alla fase `off` (convenzione di _skyhook_returns)."""
series = {a: run_asset(a, off)[0] for a in ASSETS}
J = pd.concat(series, axis=1, join="inner").fillna(0.0)
return pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index)
def sh3(s: pd.Series) -> tuple[float, float, float]:
return (al._sh(s), al._sh(s[s.index < HOLDOUT]), al._sh(s[s.index >= HOLDOUT]))
def pctl_of_first(v: np.ndarray) -> float:
return float((v < v[0]).mean() + 0.5 * (v == v[0]).mean()) * 100
# ---------------------------------------------------------------------------
# (1) SANITY
# ---------------------------------------------------------------------------
def sanity() -> None:
from src.portfolio.sleeves import _skyhook_returns
mine = skh_port(0)
ref = _skyhook_returns()
assert len(mine) == len(ref), f"sanity len: {len(mine)} vs {len(ref)}"
dmax = float(np.max(np.abs(mine.values - ref.values)))
assert dmax < 1e-12, f"sanity offset 0: max|dif| = {dmax:.2e}"
f, i, h = sh3(mine)
print(f"[SANITY] offset 0 == _skyhook_returns(): max|dif| = {dmax:.2e} "
f"su {len(mine)} giorni (FULL {f:.3f} / IS {i:.3f} / HOLD {h:.3f})")
for a in ASSETS:
_, m, _, _ = run_asset(a, 0)
print(f"[SANITY] {a} off=0: maxDD harness {m.max_dd:.1%}, trade {m.n_trades} "
f"(diario: BTC 21.4% / ETH 27.4%)")
# ---------------------------------------------------------------------------
# (6) LIVE — re-sim con fill al prossimo multiplo orario (path del cron)
# ---------------------------------------------------------------------------
def sim_equity(ltf: pd.DataFrame, ent: list, mode: str,
ts5_close: np.ndarray | None = None,
c5: np.ndarray | None = None) -> np.ndarray:
"""Replica del loop di backtest_signals con prezzi d'esecuzione iniettabili.
mode='canonical': entry a close[i], exit AL LIVELLO sl/tp (SL prioritario) o close
-> DEVE riprodurre backtest_signals bit-exact (sanity della re-sim).
mode='barclose': stessi trade (stessa detection), fill entry/exit al CLOSE della barra
230m di segnale/trigger -> quota dell'ottimismo 'fill al livello' senza cron.
mode='hourly': fill entry/exit al close 5m del PROSSIMO multiplo orario dopo la
chiusura della barra di segnale/trigger (path reale del cron 0 * * * *).
In tutti i modi le TRADE BOUNDARIES (barra entry, barra trigger, non-overlap) sono
identiche al canonico: cambia solo il prezzo d'esecuzione."""
c = ltf["close"].values.astype(float)
h = ltf["high"].values.astype(float)
l = ltf["low"].values.astype(float)
n = len(c)
close_ts = ltf["timestamp"].values.astype(np.int64) + MS_LTF
def px_hour(t: int) -> float:
hb = ((t + MSH - 1) // MSH) * MSH # prossimo multiplo orario >= t
j = np.searchsorted(ts5_close, hb, side="left")
return c5[min(j, len(c5) - 1)]
initial = 1000.0
capital = initial
equity = np.full(n, capital, dtype=float)
busy_until = -1
for i in range(n):
e = ent[i] if i < len(ent) else None
if e is None or e.get("dir", 0) == 0:
equity[i] = capital
continue
if i <= busy_until:
equity[i] = capital
continue
direction = int(e["dir"])
tp = e.get("tp"); sl = e.get("sl")
max_bars = int(e.get("max_bars") or 24)
# entry price per modalita'
if mode == "canonical":
entry = c[i]
elif mode == "barclose":
entry = c[i]
else: # hourly
entry = px_hour(close_ts[i])
exit_idx = min(i + max_bars, n - 1)
exit_lvl = c[exit_idx] # default: time exit a close
hit_kind = "time"
for j in range(i + 1, min(i + max_bars + 1, n)):
hit_sl = sl is not None and (
(direction == 1 and l[j] <= sl) or (direction == -1 and h[j] >= sl))
hit_tp = tp is not None and (
(direction == 1 and h[j] >= tp) or (direction == -1 and l[j] <= tp))
if hit_sl:
exit_lvl, exit_idx, hit_kind = sl, j, "sl"
break
if hit_tp:
exit_lvl, exit_idx, hit_kind = tp, j, "tp"
break
exit_lvl, exit_idx = c[j], j
if mode == "canonical":
exit_price = exit_lvl # fill al livello (come harness)
elif mode == "barclose":
exit_price = c[exit_idx] # fill al close della barra trigger
else:
exit_price = px_hour(close_ts[exit_idx]) # fill al prossimo multiplo orario
gross = (exit_price - entry) / entry * direction
net = gross - 0.001 # fee_rt 0.10%, leverage 1
capital += capital * net
capital = max(capital, 1.0)
equity[i:exit_idx + 1] = capital
busy_until = exit_idx
_ = hit_kind
# stessa forward-fill robusta del harness
last = initial
for k in range(n):
if equity[k] != last and equity[k] != initial:
last = equity[k]
else:
equity[k] = last
return equity
def live_delay_section() -> None:
print("\n" + "=" * 100)
print("(6) RILEVANZA LIVE — cron ORARIO (0 * * * *) vs confini 230m non allineati all'ora")
print("=" * 100)
# distribuzione del ritardo confine-230m -> prossima ora (struttura: 230 mod 60 = 50)
_, _, ltf0, _ = run_asset("BTC", 0)
close_ts = ltf0["timestamp"].values.astype(np.int64) + MS_LTF
delay_min = ((MSH - (close_ts % MSH)) % MSH) // 60_000
vals, cnts = np.unique(delay_min, return_counts=True)
tot = cnts.sum()
print("ritardo chiusura-barra-230m -> prossimo run orario del cron:")
for v, cn in zip(vals, cnts):
print(f" {int(v):>3} min : {cn / tot:>6.1%}")
print(f" media {float(delay_min.mean()):.1f} min, max {int(delay_min.max())} min "
f"(ciclo di 6 barre = 23h: la griglia migra attraverso la giornata)")
# impatto: re-sim canonico (sanity bit-exact) / barclose / hourly
print(f"\nimpatto sull'equity daily-step (offset 0, canonico; fill hourly = close 5m del "
f"prossimo multiplo orario dopo la chiusura della barra di segnale/exit):")
print(f"{'asset':<5} {'modo':<10} {'ShFULL':>7} {'ShIS':>7} {'ShHOLD':>7} {'maxDD':>7}")
dailies: dict[str, dict[str, pd.Series]] = {m: {} for m in ("canonical", "barclose", "hourly")}
for a in ASSETS:
_, m0, ltf, ent = run_asset(a, 0)
df5 = get5m(a)
ts5_close = df5["timestamp"].values.astype(np.int64) + MS5
c5 = df5["close"].values.astype(float)
for mode in ("canonical", "barclose", "hourly"):
eq = sim_equity(ltf, ent, mode, ts5_close=ts5_close, c5=c5)
if mode == "canonical":
dmax = float(np.max(np.abs(eq - m0.equity)))
assert dmax < 1e-6, f"re-sim canonica != harness ({a}): max|dif|={dmax:.2e}"
print(f" [sanity] {a}: re-sim canonica == backtest_signals "
f"(max|dif equity| = {dmax:.2e})")
s = pd.Series(eq, index=pd.DatetimeIndex(pd.to_datetime(ltf["datetime"], utc=True)))
d = s.resample("1D").last().ffill().pct_change().dropna()
dailies[mode][a] = d
f, i_, h = sh3(d)
dd = al._dd_ret(d)
print(f"{a:<5} {mode:<10} {f:>7.3f} {i_:>7.3f} {h:>7.3f} {dd:>6.1%}")
print("\nbook 50/50 BTC+ETH per modo:")
print(f"{'modo':<10} {'ShFULL':>7} {'ShIS':>7} {'ShHOLD':>7} {'maxDD':>7}")
port_mode = {}
for mode in ("canonical", "barclose", "hourly"):
J = pd.concat(dailies[mode], axis=1, join="inner").fillna(0.0)
p = pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index)
port_mode[mode] = p
f, i_, h = sh3(p)
print(f"{mode:<10} {f:>7.3f} {i_:>7.3f} {h:>7.3f} {al._dd_ret(p):>6.1%}")
dfull = al._sh(port_mode['hourly']) - al._sh(port_mode['canonical'])
dhold = (al._sh(port_mode['hourly'][port_mode['hourly'].index >= HOLDOUT])
- al._sh(port_mode['canonical'][port_mode['canonical'].index >= HOLDOUT]))
print(f"\n-> delta hourly vs canonico: FULL {dfull:+.3f}, HOLD {dhold:+.3f} "
f"(barclose isola il fill-al-livello; hourly aggiunge il ritardo 0-50 min)")
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main() -> None:
t0 = time.time()
print("=" * 100)
print("r0702 — SKH01-V2-DD: timing-luck della FASE della griglia dual-TF 230m/690m")
print(f"23 offset a priori (ogni 30m su [0,690)), parametri IDENTICI, fee 0.10% RT, "
f"HOLD-OUT >= {HOLDOUT.date()}")
print("=" * 100)
sanity()
# ---- (2) per-offset x asset -------------------------------------------
rows = []
for off in OFFSETS:
rec = {"off": off}
for a in ASSETS:
d, m, _, _ = run_asset(a, off)
f, i_, h = sh3(d)
rec[f"{a}_full"] = f; rec[f"{a}_is"] = i_; rec[f"{a}_hold"] = h
rec[f"{a}_dd"] = m.max_dd; rec[f"{a}_ntr"] = m.n_trades
p = skh_port(off)
f, i_, h = sh3(p)
rec["P_full"] = f; rec["P_is"] = i_; rec["P_hold"] = h
rec["P_dd"] = al._dd_ret(p)
rec["minFull"] = min(rec["BTC_full"], rec["ETH_full"])
rec["minHold"] = min(rec["BTC_hold"], rec["ETH_hold"])
rows.append(rec)
print(f" [{time.time()-t0:5.0f}s] offset {off:>3}m fatto "
f"(minFull {rec['minFull']:+.2f}, minHold {rec['minHold']:+.2f}, "
f"DD {rec['BTC_dd']:.0%}/{rec['ETH_dd']:.0%})")
T = pd.DataFrame(rows).set_index("off")
print("\n--- (2) PER-OFFSET (equity daily-step, come il canonico) ---")
for a in ASSETS:
print(f"\n{a}:")
print(f"{'off':>4} {'ShFULL':>7} {'ShIS':>7} {'ShHOLD':>7} {'maxDD':>7} {'trade':>6}")
for off, r in T.iterrows():
tag = " <- canonico" if off == 0 else ""
print(f"{off:>4} {r[f'{a}_full']:>7.3f} {r[f'{a}_is']:>7.3f} "
f"{r[f'{a}_hold']:>7.3f} {r[f'{a}_dd']:>6.1%} {int(r[f'{a}_ntr']):>6}{tag}")
print(" min/med/max [pctl canonico]:")
for col, lbl in ((f"{a}_full", "ShFULL"), (f"{a}_is", "ShIS"),
(f"{a}_hold", "ShHOLD"), (f"{a}_dd", "maxDD")):
v = T[col].values
print(f" {lbl:<7} {v.min():>7.3f} / {np.median(v):>7.3f} / {v.max():>7.3f} "
f" [off0 = {v[0]:.3f}, {pctl_of_first(v):.0f} pctl]")
print(f"\nBOOK 50/50 (portafoglio SKH01 standalone):")
print(f"{'off':>4} {'ShFULL':>7} {'ShIS':>7} {'ShHOLD':>7} {'maxDD':>7}")
for off, r in T.iterrows():
tag = " <- canonico" if off == 0 else ""
print(f"{off:>4} {r.P_full:>7.3f} {r.P_is:>7.3f} {r.P_hold:>7.3f} {r.P_dd:>6.1%}{tag}")
for col, lbl in (("P_full", "ShFULL"), ("P_is", "ShIS"), ("P_hold", "ShHOLD"),
("P_dd", "maxDD"), ("minFull", "minFull"), ("minHold", "minHold")):
v = T[col].values
print(f" {lbl:<8} min {v.min():>7.3f} / med {np.median(v):>7.3f} / max {v.max():>7.3f} "
f" [off0 = {v[0]:.3f}, {pctl_of_first(v):.0f} pctl]")
# ---- (3) gate di ammissione ------------------------------------------
print("\n--- (3) GATE DI AMMISSIONE ri-valutati sui 23 offset ---")
viol_b = T["BTC_dd"] >= 0.30
viol_e = T["ETH_dd"] >= 0.30
viol = viol_b | viol_e
print(f"(a) maxDD<30%: violazioni su 23 offset: BTC {int(viol_b.sum())}, "
f"ETH {int(viol_e.sum())}, almeno-un-asset {int(viol.sum())} "
f"({[int(o) for o in T.index[viol]]})")
print(f" mediana DD: BTC {T['BTC_dd'].median():.1%}, ETH {T['ETH_dd'].median():.1%}; "
f"peggiore: BTC {T['BTC_dd'].max():.1%} (off {int(T['BTC_dd'].idxmax())}), "
f"ETH {T['ETH_dd'].max():.1%} (off {int(T['ETH_dd'].idxmax())})")
print(f"(b) minFull (canonico +0.99): mediana {T['minFull'].median():+.2f}, "
f"peggiore {T['minFull'].min():+.2f} (off {int(T['minFull'].idxmin())})")
print(f" minHold (canonico +1.26): mediana {T['minHold'].median():+.2f}, "
f"peggiore {T['minHold'].min():+.2f} (off {int(T['minHold'].idxmin())})")
# (c) blend 0.75 TP01 + 0.25 SKH — TP01 baseline canonico FISSO
B = al.tp01_baseline_daily()
b_hold = al._sh(B[B.index >= HOLDOUT])
blends_hold, blends_full, corrs = [], [], []
blend_series_hold = {}
for off in OFFSETS:
s = skh_port(off)
bl = combine_outer({"TP": B, "SKH": s}, BLEND_W)
bl = bl[bl.index >= B.index.min()]
blends_hold.append(al._sh(bl[bl.index >= HOLDOUT]))
blends_full.append(al._sh(bl))
blend_series_hold[off] = bl[bl.index >= HOLDOUT]
J = pd.concat({"TP": B, "SKH": s}, axis=1, join="inner").dropna()
corrs.append(float(J["TP"].corr(J["SKH"])))
bh = np.array(blends_hold); bf = np.array(blends_full); co = np.array(corrs)
print(f"(c) blend 0.75*TP01+0.25*SKH (TP01 h=0 canonico; suo HOLD da solo = {b_hold:.2f}):")
print(f" Sharpe HOLD blend: off0 {bh[0]:.2f} | min {bh.min():.2f} / med "
f"{np.median(bh):.2f} / max {bh.max():.2f} [off0 al {pctl_of_first(bh):.0f} pctl]"
f" (claim: 0.31 -> 1.17)")
print(f" uplift HOLD vs TP01 solo: off0 {bh[0]-b_hold:+.2f} | min {bh.min()-b_hold:+.2f} "
f"/ med {np.median(bh)-b_hold:+.2f} / max {bh.max()-b_hold:+.2f}")
print(f" Sharpe FULL blend: off0 {bf[0]:.2f} | min {bf.min():.2f} / med "
f"{np.median(bf):.2f} / max {bf.max():.2f}")
print(f"(d) corr(SKH, TP01) full: off0 {co[0]:+.3f} | min {co.min():+.3f} / med "
f"{np.median(co):+.3f} / max {co.max():+.3f} (claim ~0.09)")
med_minfull = T["minFull"].median(); med_minhold = T["minHold"].median()
med_uplift = float(np.median(bh) - b_hold)
med_dd_ok = T["BTC_dd"].median() < 0.30 and T["ETH_dd"].median() < 0.30
worst_ok = (not viol.any()) and T["minFull"].min() > 0 and (bh.min() - b_hold) > 0
print("\nVERDETTO ammissione alla fase MEDIANA: "
f"DD<30% {'PASS' if med_dd_ok else 'FAIL'} alla mediana"
f" (violazioni puntuali {int(viol.sum())}/23), minFull {med_minfull:+.2f}, "
f"minHold {med_minhold:+.2f}, uplift blend HOLD {med_uplift:+.2f}, "
f"corr {np.median(co):+.2f}")
print(f" al PEGGIORE dei 23: DD {'PASS tutti' if not viol.any() else 'FAIL su ' + str(int(viol.sum())) + ' offset'}, "
f"minFull {T['minFull'].min():+.2f}, minHold {T['minHold'].min():+.2f}, "
f"uplift blend HOLD {bh.min()-b_hold:+.2f} "
f"-> {'regge anche al peggiore' if worst_ok else 'NON regge al peggiore'}")
# ---- (4) book 5-sleeve -------------------------------------------------
print("\n--- (4) BOOK 5-SLEEVE (TP 33 / XS 15 / VRP 12 / SKH 20 / GTAA 20) ---")
from src.portfolio.sleeves import (_gtaa_daily_returns, _tp01_returns,
_vrp_combo_returns, _xsec_returns)
tp_d = to_daily(_tp01_returns())
cols_fixed = dict(TP=tp_d, XS=to_daily(_xsec_returns()),
VRP=to_daily(_vrp_combo_returns()), GTAA=to_daily(_gtaa_daily_returns()))
lo = tp_d.index.min()
bkh, bkf = [], []
for off in OFFSETS:
s = combine_outer(dict(SKH=skh_port(off), **cols_fixed), BOOK_W, lo=lo)
bkh.append(al._sh(s[s.index >= HOLDOUT])); bkf.append(al._sh(s))
bkh = np.array(bkh); bkf = np.array(bkf)
ens = pd.concat({o: skh_port(o) for o in OFFSETS}, axis=1).mean(axis=1)
s_ens = combine_outer(dict(SKH=ens, **cols_fixed), BOOK_W, lo=lo)
i_med_h = int(np.argsort(bkh)[len(bkh) // 2]); i_worst_h = int(np.argmin(bkh))
print(f"HOLD: off0 {bkh[0]:.2f} | min {bkh.min():.2f} (off {OFFSETS[i_worst_h]}) / med "
f"{np.median(bkh):.2f} (off {OFFSETS[i_med_h]}) / max {bkh.max():.2f} "
f"[off0 al {pctl_of_first(bkh):.0f} pctl] | ENSEMBLE "
f"{al._sh(s_ens[s_ens.index >= HOLDOUT]):.2f}")
print(f"FULL: off0 {bkf[0]:.2f} | min {bkf.min():.2f} / med {np.median(bkf):.2f} / max "
f"{bkf.max():.2f} [off0 al {pctl_of_first(bkf):.0f} pctl] | ENSEMBLE {al._sh(s_ens):.2f}")
print("(l'ensemble di offset NON e' eseguibile live — una sola griglia gira: e' solo la "
"stima de-luckata)")
# ---- (5) bootstrap alla maniera dello scettico -------------------------
print("\n--- (5) BOOTSTRAP (block) — lo spike del canonico sull'hold-out del blend ---")
Mdf = pd.concat(blend_series_hold, axis=1, join="inner").dropna()
M = Mdf.values
def _sh_cols(R: np.ndarray) -> np.ndarray:
mu = R.mean(axis=1); sd = R.std(axis=1)
return np.where(sd > 0, mu / sd, 0.0) * np.sqrt(365.25)
sh_obs = np.array([al._sh(Mdf[c]) for c in Mdf.columns])
g0_obs = float(sh_obs[0] - np.median(sh_obs[1:]))
corrM = np.corrcoef(M.T); iu = np.triu_indices(M.shape[1], 1)
print(f"hold-out: {M.shape[0]} giorni x {M.shape[1]} offset; Sh blend off0 {sh_obs[0]:.3f}, "
f"mediana altri {np.median(sh_obs[1:]):.3f}, spike osservato g0 = {g0_obs:+.3f}")
print(f"corr daily fra i 23 blend (hold-out): mediana {np.median(corrM[iu]):.3f}, "
f"min {corrM[iu].min():.3f}")
n, K = M.shape
for blk in (10, 20, 40):
rng = np.random.default_rng(42 + blk)
nblocks = int(np.ceil(n / blk))
gmaxs, g0s = [], []
done = 0
while done < B_BOOT:
b = min(500, B_BOOT - done)
starts = rng.integers(0, n, size=(b, nblocks))
idx = (starts[:, :, None] + np.arange(blk)[None, None, :]) % n
idx = idx.reshape(b, -1)[:, :n]
R = M[idx] # (b, n, K)
Sh = np.stack([_sh_cols(R[:, :, k]) for k in range(K)], axis=1)
med_others = np.empty_like(Sh)
for k in range(K):
med_others[:, k] = np.median(np.delete(Sh, k, axis=1), axis=1)
g = Sh - med_others
gmaxs.append(g.max(axis=1)); g0s.append(g[:, 0])
done += b
gmax = np.concatenate(gmaxs); g0 = np.concatenate(g0s)
print(f" block={blk:>2}: P(spike di UN offset qualsiasi >= {g0_obs:+.2f}) = "
f"{float(np.mean(gmax >= g0_obs)):.3f} | P(g0<=0) = {float(np.mean(g0 <= 0)):.3f} "
f"| CI95 g0 [{np.percentile(g0, 2.5):+.2f},{np.percentile(g0, 97.5):+.2f}]")
# ---- (6) live ----------------------------------------------------------
live_delay_section()
# ---- (7) caveat --------------------------------------------------------
print("\n--- (7) CAVEAT ---")
print("- equity daily-step (lens Sharpe), stessa convenzione del canonico;")
print("- costi di ribilanciamento del book 230m gia' flaggati a deploy (diario skyhook);")
print("- ensemble di offset NON eseguibile live (una sola griglia gira) -> solo stima de-luckata;")
print("- sim 'hourly': fill a close 5m del multiplo orario (cron minuto 0), niente slippage/parziali;")
print("- nessuna selezione: 23 offset uniformi dichiarati a priori, parametri identici ovunque.")
print(f"\nFatto in {time.time()-t0:.0f}s.")
if __name__ == "__main__":
main()
+378
View File
@@ -0,0 +1,378 @@
#!/usr/bin/env python
"""r0702_anchor_xs01.py — TIMING-LUCK della FASE del ciclo di ribilanciamento di XS01.
XS01 (`src/portfolio/sleeves._xsec_returns`, XS_CFG H=10) ribilancia ogni 10 giorni quando
`i % H == 0` con `i` = indice di riga della matrice prezzi inner-joined (start 2024-01-01).
La FASE del ciclo (quale dei 10 giorni possibili) e' quindi un artefatto della prima riga
dei dati — esattamente come l'ancora oraria di TP01 (vedi r0702_tp01_offset.py +
r0702_skeptic_offset.py, diario 2026-07-02-timing-crt-wave.md). Spazio di luck: 10 fasi.
Questo script (metodologia identica all'audit TP01, ZERO tuning per-fase):
1. REPLICA `_xsec_returns` con parametro `phase` (i % H == phase). SANITY OBBLIGATORIO:
phase=0 deve riprodurre ESATTAMENTE la serie di `_xsec_returns()` (max|dif| ~ 0).
Il gate di dispersione (percentile ESPANDENTE causale) e' ricalcolato PER FASE:
disp_hist accumula solo nei giorni di ribilanciamento di QUELLA fase (replica fedele).
2. Tabella delle 10 fasi: Sharpe/CAGR/maxDD FULL (serie dal 2024) e HOLD-OUT 2025-26;
min/mediana/max + percentile della fase canonica (phase=0).
3. ENSEMBLE delle 10 fasi (media dei ritorni = 1/10 del capitale per fase) vs canonica.
4. IMPATTO SUL BOOK 5-sleeve (TP 33 / XS 15 / VRP 12 / SKH 20 / GTAA 20, combine_outer
con pesi rinormalizzati, attivazione era crypto): XS canonica vs fase mediana vs fase
peggiore vs ensemble vs senza-XS -> HOLD e FULL del book.
5. GATE DI AMMISSIONE: plateau (lookback singoli/blend, disp_pct 15-35) ricalcolato alla
fase MEDIANA e alla canonica: i numeri di ammissione (FULL 1.50/HOLD 1.71/DD 11%) reggono?
6. BOOTSTRAP alla maniera dello scettico: block-bootstrap congiunto delle 10 fasi
sull'hold-out -> P(una fase qualsiasi mostri uno spike >= quello della canonica)
+ CI95 (storia ~2.5y -> quantifica l'ampiezza).
Tecnica: mai .view su tz-aware (epoca via pd.to_datetime unit='ms', come l'originale);
posizioni shiftate come nell'originale (gross[1:] = W[:-1]*dret[1:]); vol-target rolling
ricalcolato per fase sulla PROPRIA serie netta. Nessun file toccato fuori da questo script.
Runtime ~2-4 min (il grosso e' SKH01 dal 5m per il book).
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pandas as pd
ROOT = Path("/opt/docker/PythagorasGoal")
sys.path.insert(0, str(ROOT))
from src.portfolio.portfolio import HOLDOUT, combine_outer, to_daily # noqa: E402
from src.portfolio.sleeves import (_HL_DIR, XS_CFG, XS_UNIVERSE, # noqa: E402
_xsec_returns)
RNG_SEED = 42
B_BOOT = 4000
H = XS_CFG["H"] # 10 -> 10 fasi possibili
FULL_START = pd.Timestamp("2024-01-01", tz="UTC")
# ---------------------------------------------------------------------------
# Metriche (convenzioni identiche ad altlib._sh/_dd_ret e a r0702_tp01_offset.dmetrics)
# ---------------------------------------------------------------------------
def _sh(s) -> float:
r = np.asarray(pd.Series(s).dropna().values, float)
return float(np.mean(r) / np.std(r) * np.sqrt(365.25)) if len(r) > 2 and np.std(r) > 0 else 0.0
def _dd(s) -> float:
eq = np.cumprod(1.0 + np.asarray(pd.Series(s).dropna().values, float))
pk = np.maximum.accumulate(eq)
return float(np.max((pk - eq) / pk)) if len(eq) else 0.0
def _cagr(s) -> float:
r = np.asarray(pd.Series(s).dropna().values, float)
eq = float(np.prod(1.0 + r))
yrs = len(r) / 365.25
return eq ** (1 / yrs) - 1 if eq > 0 and yrs > 0 else -1.0
def dmetrics(s: pd.Series) -> dict:
s = s.dropna()
ho = s[s.index >= HOLDOUT]
return dict(sh_full=_sh(s), cagr_full=_cagr(s), dd_full=_dd(s),
sh_hold=_sh(ho), cagr_hold=_cagr(ho), dd_hold=_dd(ho), n=len(s))
def pctl_of(v: np.ndarray, x: float) -> float:
return float((v < x).mean() + 0.5 * (v == x).mean()) * 100.0
# ---------------------------------------------------------------------------
# Prezzi: caricamento IDENTICO a _xsec_returns (stesso ordine di operazioni)
# ---------------------------------------------------------------------------
_PX_CACHE: pd.DataFrame | None = None
def load_prices() -> pd.DataFrame:
global _PX_CACHE
if _PX_CACHE is not None:
return _PX_CACHE
cols = {}
for sym in XS_UNIVERSE:
p = _HL_DIR / f"hl_{sym.lower()}_1d.parquet"
if not p.exists():
continue
d = pd.read_parquet(p)
cols[sym] = pd.Series(d["close"].values.astype(float),
index=pd.to_datetime(d["timestamp"], unit="ms", utc=True))
if len(cols) < 10:
raise FileNotFoundError("universo Hyperliquid XS01 incompleto")
_PX_CACHE = pd.concat(cols, axis=1, join="inner").sort_index().dropna()
return _PX_CACHE
# ---------------------------------------------------------------------------
# REPLICA di _xsec_returns con parametro `phase` (+ override cfg per il plateau).
# Codice copiato 1:1 da src/portfolio/sleeves._xsec_returns; UNICA differenza:
# `i % H == 0` -> `i % H == phase`. Il disp_hist (percentile espandente causale)
# accumula SOLO nei giorni di ribilanciamento della fase -> ricalcolato per fase.
# ---------------------------------------------------------------------------
def xs_phase(phase: int, lookbacks=None, disp_pct=None) -> pd.Series:
cfg = dict(XS_CFG)
if lookbacks is not None:
cfg["lookbacks"] = lookbacks
if disp_pct is not None:
cfg["disp_pct"] = disp_pct
C = load_prices()
px = C.values
n, A = px.shape
lookbacks_, Hc, k, mode, tv = (cfg["lookbacks"], cfg["H"], cfg["k"],
cfg["mode"], cfg["target_vol"])
disp_pct_ = cfg.get("disp_pct", 0)
minhist = cfg.get("disp_minhist", 20)
mlb = max(lookbacks_)
dret = np.vstack([np.zeros(A), px[1:] / px[:-1] - 1.0])
W = np.zeros((n, A))
w = np.zeros(A)
disp_hist = []
for i in range(n):
if i >= mlb and i % Hc == phase:
rLs = [px[i] / px[i - L] - 1.0 for L in lookbacks_]
disp_i = float(np.mean([r.std() for r in rLs]))
thr = (np.percentile(disp_hist, disp_pct_)
if (disp_pct_ > 0 and len(disp_hist) >= minhist) else -np.inf)
if disp_i >= thr:
score = np.zeros(A)
cnt = 0
for rL in rLs:
sd = rL.std()
if sd > 0:
score += (rL - rL.mean()) / sd
cnt += 1
if cnt:
score /= cnt
order = np.argsort(score)
w = np.zeros(A)
lo, hi = order[:k], order[-k:]
if mode == "mom":
w[hi] = 0.5 / k
w[lo] = -0.5 / k
else:
w[lo] = 0.5 / k
w[hi] = -0.5 / k
else:
w = np.zeros(A)
disp_hist.append(disp_i)
W[i] = w
gross = np.zeros(n)
gross[1:] = np.sum(W[:-1] * dret[1:], axis=1) # posizioni SHIFTATE (held t+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 * (0.001 / 2.0)
s = pd.Series(net, index=C.index)
rv = s.rolling(30, min_periods=15).std().shift(1) * np.sqrt(365.25)
scale = np.clip(np.nan_to_num(tv / rv.replace(0, np.nan).values, nan=0.0), 0, 3.0)
return pd.Series(s.values * scale, index=C.index)
# ---------------------------------------------------------------------------
# 1. SANITY: replica a fase canonica == _xsec_returns() (bit-exact)
# ---------------------------------------------------------------------------
def sanity() -> None:
ref = _xsec_returns()
mine = xs_phase(0)
assert len(mine) == len(ref) and mine.index.equals(ref.index), "SANITY index mismatch"
dmax = float(np.max(np.abs(mine.values - ref.values)))
assert dmax == 0.0, f"SANITY FAIL: replica phase=0 != _xsec_returns (max|dif|={dmax:.3e})"
m = dmetrics(ref)
print(f"[SANITY] replica phase=0 == _xsec_returns(): OK (max|dif| = {dmax:.1e}, "
f"n={len(ref)}, start {ref.index.min().date()})")
print(f"[SANITY] canonica oggi: FULL Sh {m['sh_full']:.3f} / HOLD {m['sh_hold']:.3f} / "
f"DD {m['dd_full']:.1%} (dichiarati all'ammissione: 1.50 / 1.71 / 11% — dati mossi da allora)")
# ---------------------------------------------------------------------------
# 6. BOOTSTRAP alla maniera dello scettico (r0702_skeptic_offset.block_boot_stats)
# ---------------------------------------------------------------------------
def _sh_mat(R: np.ndarray) -> np.ndarray:
mu = R.mean(axis=1)
sd = R.std(axis=1)
return np.where(sd > 0, mu / sd, 0.0) * np.sqrt(365.25)
def block_boot(M: np.ndarray, B: int, block: int, rng) -> dict:
n, K = M.shape
nblocks = int(np.ceil(n / block))
g0s, gmaxs, meds, sh0s = [], [], [], []
done = 0
while done < B:
b = min(500, B - done)
starts = rng.integers(0, n, size=(b, nblocks))
idx = (starts[:, :, None] + np.arange(block)[None, None, :]) % n
idx = idx.reshape(b, -1)[:, :n]
R = M[idx] # (b, n, K)
Sh = np.stack([_sh_mat(R[:, :, kk]) for kk in range(K)], axis=1)
gg = np.empty_like(Sh)
for h in range(K):
gg[:, h] = Sh[:, h] - np.median(np.delete(Sh, h, axis=1), axis=1)
g0s.append(gg[:, 0])
gmaxs.append(gg.max(axis=1))
meds.append(np.median(Sh, axis=1))
sh0s.append(Sh[:, 0])
done += b
return dict(g0=np.concatenate(g0s), gmax=np.concatenate(gmaxs),
med=np.concatenate(meds), sh0=np.concatenate(sh0s))
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main() -> None:
print("=" * 100)
print("r0702 — XS01 rebalance PHASE timing-luck: 10 fasi del ciclo H=10 "
"(metodologia = audit ancore TP01)")
print(f"XS_CFG={XS_CFG} universo={len(load_prices().columns)} major HL "
f"HOLD-OUT >= {HOLDOUT.date()}")
print("=" * 100)
sanity()
# ---- (2) tabella delle 10 fasi ----------------------------------------
phases = {p: xs_phase(p) for p in range(H)}
T = pd.DataFrame({p: dmetrics(s) for p, s in phases.items()}).T
print("\n--- (2) PER-FASE (parametri IDENTICI, zero tuning; fase = i % 10 del ciclo) ---")
print(f"{'fase':>4} {'ShFULL':>7} {'CAGRf':>7} {'DDfull':>7} {'ShHOLD':>7} "
f"{'CAGRh':>7} {'DDhold':>7}")
for p, r in T.iterrows():
tag = " <- canonica" if p == 0 else ""
print(f"{p:>4} {r.sh_full:>7.3f} {r.cagr_full:>6.1%} {r.dd_full:>6.1%} "
f"{r.sh_hold:>7.3f} {r.cagr_hold:>6.1%} {r.dd_hold:>6.1%}{tag}")
print("\nDistribuzione fra le 10 fasi (min / mediana / max / std) [percentile della canonica]:")
for col, lbl in (("sh_full", "Sharpe FULL"), ("sh_hold", "Sharpe HOLD"),
("cagr_full", "CAGR FULL"), ("dd_full", "maxDD FULL"),
("dd_hold", "maxDD HOLD")):
v = T[col].values.astype(float)
print(f" {lbl:<14} {v.min():>7.3f} / {np.median(v):>7.3f} / {v.max():>7.3f} "
f"/ std {v.std():.3f} canonica al {pctl_of(v, v[0]):.0f}° pctl (val {v[0]:.3f})")
hold_v = T["sh_hold"].values.astype(float)
full_v = T["sh_full"].values.astype(float)
med_hold = float(np.median(hold_v))
p_med = int(min(range(H), key=lambda p: (abs(hold_v[p] - med_hold), p)))
p_worst = int(np.argmin(hold_v))
p_best = int(np.argmax(hold_v))
print(f"\n fase MEDIANA (per ShHOLD) = {p_med} | PEGGIORE = {p_worst} | MIGLIORE = {p_best}")
# correlazione fra fasi (contestualizza il bootstrap)
Jf = pd.concat(phases, axis=1, join="inner").dropna()
Mh = Jf[Jf.index >= HOLDOUT].values
cor = np.corrcoef(Mh.T)
iu = np.triu_indices(H, 1)
print(f" correlazione daily fra fasi (hold-out): mediana {np.median(cor[iu]):.3f}, "
f"min {cor[iu].min():.3f}")
# ---- (3) ensemble delle 10 fasi ----------------------------------------
ens = pd.Series(Jf.values.mean(axis=1), index=Jf.index)
me, m0 = dmetrics(ens), dmetrics(phases[0])
print("\n--- (3) ENSEMBLE (media dei ritorni delle 10 fasi = 1/10 capitale per fase) ---")
print(f"{'config':<18} {'ShFULL':>7} {'CAGRf':>7} {'DDfull':>7} {'ShHOLD':>7} {'DDhold':>7}")
for name, m in (("canonica (f0)", m0), ("ensemble 10 fasi", me)):
print(f"{name:<18} {m['sh_full']:>7.3f} {m['cagr_full']:>6.1%} {m['dd_full']:>6.1%} "
f"{m['sh_hold']:>7.3f} {m['dd_hold']:>6.1%}")
# ---- (4) impatto sul book 5-sleeve -------------------------------------
print("\n--- (4) BOOK 5-sleeve (TP 33 / XS 15 / VRP 12 / SKH 20 / GTAA 20, combine_outer, "
"attivazione era crypto) ---")
from src.portfolio.sleeves import (_gtaa_daily_returns, _skyhook_returns,
_tp01_returns, _vrp_combo_returns)
TPd = to_daily(_tp01_returns())
fixed = dict(TP=TPd, VRP=to_daily(_vrp_combo_returns()),
SKH=to_daily(_skyhook_returns()), GTAA=to_daily(_gtaa_daily_returns()))
Wt = dict(TP=0.33, XS=0.15, VRP=0.12, SKH=0.20, GTAA=0.20)
lo = TPd.index.min()
def book(xs: pd.Series | None) -> pd.Series:
cols = dict(fixed)
if xs is not None:
cols["XS"] = to_daily(xs)
wt = {k: v for k, v in Wt.items() if k in cols}
return combine_outer(cols, wt, lo=lo)
rows = [("XS canonica (f0)", phases[0]), (f"XS fase mediana ({p_med})", phases[p_med]),
(f"XS fase peggiore ({p_worst})", phases[p_worst]),
(f"XS fase migliore ({p_best})", phases[p_best]),
("XS ensemble 10f", ens), ("senza XS01 (rinorm.)", None)]
print(f"{'book con':<24} {'ShHOLD':>7} {'ShFULL':>7} {'DDfull':>7} {'DDhold':>7}")
book_stats = {}
for name, xs in rows:
b = book(xs)
bh = b[b.index >= HOLDOUT]
book_stats[name] = (_sh(bh), _sh(b))
print(f"{name:<24} {_sh(bh):>7.3f} {_sh(b):>7.3f} {_dd(b):>6.1%} {_dd(bh):>6.1%}")
bh0 = book_stats["XS canonica (f0)"][0]
print(f"\n fortuna di fase ereditata dal book HOLD: canonica {bh0:.3f} vs mediana "
f"{book_stats[f'XS fase mediana ({p_med})'][0]:.3f} "
f"(delta {bh0 - book_stats[f'XS fase mediana ({p_med})'][0]:+.3f}) | vs peggiore "
f"{book_stats[f'XS fase peggiore ({p_worst})'][0]:.3f} "
f"(delta {bh0 - book_stats[f'XS fase peggiore ({p_worst})'][0]:+.3f}) | vs senza-XS "
f"{book_stats['senza XS01 (rinorm.)'][0]:.3f}")
# ---- (5) gate di ammissione alla fase mediana: plateau ------------------
print("\n--- (5) GATE DI AMMISSIONE — plateau ricalcolato alla fase MEDIANA "
f"({p_med}) vs canonica (0) ---")
print("(ammissione XS01: FULL 1.50 / HOLD 1.71 / DD 11%, plateau lookback + disp_pct 15-35)")
print(f"{'variante':<28} {'f0 FULL':>8} {'f0 HOLD':>8} {'f0 DD':>7} "
f"{'fMED FULL':>9} {'fMED HOLD':>9} {'fMED DD':>8}")
grid = ([(f"disp_pct={dp} (lb 30,90)", dict(disp_pct=dp)) for dp in (15, 20, 25, 30, 35)]
+ [(f"lookbacks={lb} (p30)", dict(lookbacks=lb))
for lb in ((30,), (60,), (90,), (30, 60, 90))])
for name, kw in grid:
a = dmetrics(xs_phase(0, **kw))
b = dmetrics(xs_phase(p_med, **kw))
star = " *" if kw == dict(disp_pct=30) or kw.get("lookbacks") == (30, 90) else ""
print(f"{name:<28} {a['sh_full']:>8.3f} {a['sh_hold']:>8.3f} {a['dd_full']:>6.1%} "
f"{b['sh_full']:>9.3f} {b['sh_hold']:>9.3f} {b['dd_full']:>7.1%}{star}")
mmed = dmetrics(phases[p_med])
print(f"\n cella canonica alla fase mediana: FULL {mmed['sh_full']:.3f} "
f"(ammesso con 1.50) / HOLD {mmed['sh_hold']:.3f} (ammesso con 1.71) / "
f"DD {mmed['dd_full']:.1%} (ammesso con 11%)")
n_full_pos = int((full_v >= 1.0).sum())
n_hold_pos = int((hold_v >= 1.0).sum())
print(f" fasi con FULL>=1.0: {n_full_pos}/10 | fasi con HOLD>=1.0: {n_hold_pos}/10 | "
f"fasi con HOLD<=0: {int((hold_v <= 0).sum())}/10")
# ---- (6) bootstrap: la canonica e' speciale? ----------------------------
print("\n--- (6) BOOTSTRAP (block, congiunto sulle 10 fasi, hold-out) ---")
sh_hold_obs = _sh_mat(Mh.T)
g0_obs = float(sh_hold_obs[0] - np.median(sh_hold_obs[1:]))
print(f"hold-out: {Mh.shape[0]} giorni, 10 fasi; Sh canonica {sh_hold_obs[0]:.3f}, "
f"mediana altre {np.median(sh_hold_obs[1:]):.3f}, spike osservato g0 = {g0_obs:+.3f}")
for blk in (10, 20, 40):
bs = block_boot(Mh, B_BOOT, blk, np.random.default_rng(RNG_SEED + blk))
p_any = float(np.mean(bs["gmax"] >= g0_obs))
ci_g0 = np.percentile(bs["g0"], [2.5, 97.5])
ci_med = np.percentile(bs["med"], [2.5, 97.5])
ci_sh0 = np.percentile(bs["sh0"], [2.5, 97.5])
print(f" block={blk:>2}: P(spike di UNA QUALSIASI fase >= {g0_obs:+.2f}) = {p_any:.3f} | "
f"CI95 g0 [{ci_g0[0]:+.2f},{ci_g0[1]:+.2f}] | CI95 Sh mediana-fasi "
f"[{ci_med[0]:+.2f},{ci_med[1]:+.2f}] | CI95 Sh canonica [{ci_sh0[0]:+.2f},{ci_sh0[1]:+.2f}]")
# CI sulla FULL della canonica (storia corta -> quanto sono larghi?)
Mf = Jf.values
bsf = block_boot(Mf, B_BOOT, 20, np.random.default_rng(RNG_SEED))
ci_f0 = np.percentile(bsf["sh0"], [2.5, 97.5])
ci_fmed = np.percentile(bsf["med"], [2.5, 97.5])
print(f" FULL (block=20): CI95 Sh canonica [{ci_f0[0]:+.2f},{ci_f0[1]:+.2f}] | "
f"CI95 Sh mediana-fasi [{ci_fmed[0]:+.2f},{ci_fmed[1]:+.2f}] "
f"(~2.5 anni di storia: intervalli larghi)")
# ---- (7) caveat ---------------------------------------------------------
print("\n--- (7) CAVEAT DICHIARATI ---")
print(" (a) storia corta ~2.5y (914g, hold-out ~548g): i CI95 qui sopra quantificano "
"l'incertezza — nessuna stima puntuale di Sharpe e' affidabile a +/-1.")
print(" (b) dimensione ORA-DEL-GIORNO non auditata: le barre HL in data/raw sono solo 1d "
"(ancora daily fissa del feed) -> luck residua non testabile con i dati certificati.")
print(" (c) XS01 e' STAT-MODE (19 gambe, non eseguito live): l'impatto e' sulla STIMA del "
"book (reporting/ammissione), non sull'operativita' del book live Deribit.")
print("\nFatto. Nessuna selezione per-fase: parametri identici, giudizio su distribuzione "
"delle fasi + ensemble; l'hold-out e' solo riportato.")
if __name__ == "__main__":
main()
+6
View File
@@ -10,6 +10,12 @@ Architettura (dal brief):
Entrambi resampled dal feed 5m certificato con origin='epoch' -> i confini 690 sono un Entrambi resampled dal feed 5m certificato con origin='epoch' -> i confini 690 sono un
SOTTOINSIEME dei confini 230, quindi una barra HTF chiude esattamente su una chiusura LTF. SOTTOINSIEME dei confini 230, quindi una barra HTF chiude esattamente su una chiusura LTF.
NB GRID TIMING-LUCK (2026-07-02): i numeri headline (minHold +1.26, blend 0.31->1.17, maxDD<30%)
sono sull'offset 0 della griglia epoch, il MIGLIORE dei 23 offset a priori (93-98mo pctl; mediane:
minHold +0.39, blend 0.72; il gate DD<30% fallisce in 15/23 offset). Regge de-luckato: uplift del
blend positivo a TUTTE le 23 fasi (min +0.18) + corr ~0.08 -> diversificatore ADDS ridimensionato.
Vedi docs/diary/2026-07-02-anchor-audit-xs01-skh01.md e scripts/research/r0702_anchor_skh01.py.
Pipeline per barra (evaluate_bar): barre -> indicatori -> fasce regime -> pattern -> composer Pipeline per barra (evaluate_bar): barre -> indicatori -> fasce regime -> pattern -> composer
-> ingresso/uscita -> SkyhookDecision -> ingresso/uscita -> SkyhookDecision
1. INDICATORI (sul HTF, tipo-Chande, normalizzati 0-100): 1. INDICATORI (sul HTF, tipo-Chande, normalizzati 0-100):