4 Commits

Author SHA1 Message Date
Adriano Dal Pastro 745ba7d066 research(portfolio): simula PREVDAY come overlay tail-hedge su TP01+XS01+VRP01 (NON deploy)
Simulazione d'impatto (registry di produzione invariato): riscala i 3 sleeve attivi a (1-W) e
aggiunge PREVDAY a peso W; sweep W {0,5,10,15,20%}. A 10%:
 - FULL Sharpe 1.68->1.88 (+0.20), maxDD 14.3%->9.9% (-31%): la gamba short ammortizza i crash storici.
 - HOLD-OUT Sharpe 1.66->1.97 (+0.31), ret +16.7->+19.0% (DD gia' bassissimo 3.4%).
 - 10% ~ ottimo di DD: oltre, lo Sharpe sale ma il maxDD smette di scendere (solo piu' rischio short).
 - per-anno: migliora/pareggia quasi ovunque; costa solo nel toro 2021 (premio hedge), paga nel bear 2022.

Caveat: tutto IN-SAMPLE (i guadagni assumono che l'edge persista -> e' cio' che il forward-monitor
verifica); outer-join gonfia il peso effettivo 2019-20 -> l'hold-out e' il read pulito a 10%. PREVDAY
resta FORWARD-MONITOR. Lo script e' il riferimento per ri-valutare l'overlay a forward maturo.

Diario: 2026-06-21-prevday-overlay-portfolio.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 20:01:20 +00:00
Adriano Dal Pastro 043f141bf1 research(intraday): PREVDAY block-bootstrap -> coda-fortuna vs persistente (blocker #2/#3)
Chiarimento: il "top-5 giorni = 76-83%" del diario era sulle gambe REVERT scartate, non su PREVDAY
(breakout). Test su PREVDAY stesso + gamba short (= tutto il valore). Block bootstrap circolare 20g B=3000.

[A] Concentrazione: PREVDAY-full NON e' piu' coda-fortuna di TP01 (top5 22% vs 19%; 14.3% dei giorni
per il 50% del gain vs 8.0% -> piu' distribuito). MA la gamba short e' tail-dipendente (top5=130% del
netto: togliendo i 5 giorni migliori va in perdita; sono i giorni-crash).

[B] Bootstrap: full robustissimo (uplift mediana +0.28, 99% dei resample >0); hold-out regge con coda
piu' larga (uplift mediana +0.53, 93% >0, 5deg pctl appena negativo per hold-out corto + short
tail-dipendente).

Verdetto: #3 tail-luck DECLASSATO per PREVDAY-full, CONFERMATO per la gamba short (payoff grumoso, su
<10 giorni-crash/anno); #2 null-corr-zero RIDIMENSIONATO (uplift genuinamente positivo, era efficienza
relativa). Sintesi trilogia: PREVDAY = tail-hedge legittimo e bootstrap-robusto, eseguibile a taglia
reale, payoff concentrato sui crash -> candidato overlay tail-hedge, non sleeve-alpha. Forward-monitor.

Diario: 2026-06-21-prevday-bootstrap.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 19:56:11 +00:00
Adriano Dal Pastro b388c462e8 research(intraday): PREVDAY turnover-reduction (no free lunch) + long-only probe (e' un HEDGE)
La fee (~2.6%/anno) viene dai ~70 flip/anno, non dal churn sub-dollaro (da fill_haircut). Sweep
delle leve che riducono i flip a livello di segnale (buffer k, anchor multi-giorno, min-hold):
 - allargare buffer/anchor taglia fee e turnover ma l'uplift hold-out del blend cala monotono
   (k0.30->0.50->0.75 = upl 0.56->0.40->0.00); anchor multi-giorno tutto peggio (conferma anchor=1).
 - min_hold=24h e' l'unico ritocco quasi-gratis (upl 0.56->0.60) ma peggiora il DD -27%->-32%.
 - la config congelata e' gia' sulla frontiera efficiente turnover<->edge -> nessun cambio.

Bonus blocker #1: long-only vs long-short. long-only ha Sharpe standalone PIU' ALTO (1.55 vs 1.23)
ma corr a TP01 +0.64 e blend uplift solo +0.09. TUTTO il valore di portafoglio e' la gamba SHORT
(decorrelazione 0.64->0.15, uplift 0.09->0.56). PREVDAY non e' alpha: e' un HEDGE di regime-down
(costa nel toro, paga nell'orso 2022/2025-26), additivo alla flat-stance di TP01. Restano i blocker
null-corr-zero e tail-luck. Forward-monitor invariato; eventuale ruolo = overlay di tail-hedge.

Diario: 2026-06-21-prevday-turnover-and-hedge.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 19:45:54 +00:00
Adriano Dal Pastro 8514c096ea research(intraday): fill-haircut PREVDAY a basso capitale -> blocker d'esecuzione BENIGNO
Stima SUBITO (invece di aspettare il forward-monitor) quanto il fill reale a $600 erode il lead
PREVDAY, replicando i due libri di paper_prevday.py su tutto il path 1h (2019-03 -> 2026-06):
 - MODELED (continuo) vs REAL-$C (skip ribilanciamenti < $5 min-order), sweep C {600,2k,20k}.
 - HAIRCUT $600 = +0.01 Sharpe (FULL e HOLD): saltare il 98.4% dei micro-ribilanciamenti del
   vol-target non costa nulla (trade infinitesimi: fee risparmiata e tracking-error entrambi
   trascurabili; fee-drag 2.49% -> 2.39%). L'uplift hold-out del blend 80/20 regge +0.56 -> +0.55.

Conseguenza: dei 4 blocker no-deploy, il #4 (fill a basso capitale) e' SMONTATO. Restano i 3
strutturali (hedge-shaped, fallisce il null a corr-zero, tail-luck). PREVDAY resta forward-monitor.
Lezione: eseguire eval_weights_smallcap PRIMA di scartare un lead per 'fill irreale'.

Diario: 2026-06-21-prevday-fill-haircut.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 19:41:20 +00:00
8 changed files with 811 additions and 0 deletions
@@ -0,0 +1,68 @@
# PREVDAY block-bootstrap — coda-fortuna vs persistente (blocker #2/#3)
**Data:** 2026-06-21 (chiude la trilogia: fill-haircut → turnover/hedge → bootstrap)
**Script:** `scripts/research/intraday/prevday_bootstrap.py`
**Esito:** PREVDAY-full **non** è più coda-fortuna di TP01 e l'edge è **bootstrap-robusto** (full
99% / hold-out 93% dei resample con uplift>0). MA la gamba short (= tutto il valore) è
**tail-dipendente** (top-5 giorni = 130% del suo netto). PREVDAY = tail-hedge legittimo dal payoff
grumoso. Resta forward-monitor.
## Chiarimento di scope
Il "top-5 giorni = 76-83% del PnL" del diario intraday era sulle GAMBE REVERT del combo a 5 segnali
(vol_event/volume_spike/gap_fill), poi SCARTATE. Il sopravvissuto è PREVDAY (breakout-continuation).
Qui si testa PREVDAY STESSO — e la sua gamba SHORT, che (prevday_turnover) è l'intero valore di
portafoglio. Block bootstrap circolare (blocchi 20g, B=3000) per preservare autocorrelazione/regime.
## [A] Concentrazione del PnL nei top-K giorni
| serie | n | totRet | top5 | top10 | top20 | giorni→50% gain |
|-------|--:|-------:|-----:|------:|------:|----------------:|
| PREVDAY full | 2869 | +182% | 22% | 36% | 59% | 411 (14.3%) |
| **PREVDAY short-only** | 2869 | **+28%** | **130%** | 218% | 345% | 312 (10.9%) |
| PREVDAY long-only | 2869 | +154% | 18% | 30% | 49% | 287 (10.0%) |
| TP01 (riferimento) | 2657 | +116% | 19% | 33% | 55% | 213 (8.0%) |
- **PREVDAY-full NON è più coda-fortuna di TP01**: top5 22% vs 19%, e per il 50% del guadagno serve
*più* tempo (14.3% dei giorni vs 8.0% → più distribuito). Il tail-luck del diario era sulle gambe
revert scartate, non su PREVDAY.
- **Gamba short tail-dipendente:** top5 = **130% del netto** → togliendo i 5 giorni migliori la short
va in perdita (gli altri 2864 giorni nettano 8%). Sono i giorni-crash dove la short paga.
## [B] Circular block bootstrap (20g, B=3000)
| campione | PREVDAY Sharpe (mediana [5°,95°], %>0) | blend 80/20 uplift (mediana [5°,95°], %>0, %>+0.10) |
|----------|----------------------------------------|------------------------------------------------------|
| full (2018-08→2026-06) | +1.24 [+0.64,+1.80] 100% | +0.28 [+0.09,+0.47] 99% / 93% |
| hold-out (2025+) | +1.27 [0.01,+2.46] 95% | +0.53 [0.05,+1.21] 93% / 88% |
| short-only hold-out | +1.12 [0.32,+2.41] 90% | +0.53 [0.08,+1.31] 92% / 87% |
- **Full sample: edge robustissimo** — 99% dei resample dà uplift>0 (mediana +0.28). Non è "un blocco
fortunato".
- **Hold-out: regge con coda più larga** (5° pctl appena negativo: hold-out corto ~536g + short
tail-dipendente), ma 93% dei resample >0, 88% >+0.10.
## Verdetto blocker #2/#3
- **#3 tail-luck — DECLASSATO per PREVDAY-full, CONFERMATO per la gamba short.** La strategia intera
non è più concentrata di TP01 (che già deployamo); il motore di valore (la short) sì: vive su <10
giorni-crash/anno. Bootstrap-robusto (non un singolo blocco), ma il forward sarà GRUMOSO, non un
liscio +0.56/periodo.
- **#2 null-corr-zero — RIDIMENSIONATO.** L'uplift è genuinamente positivo (93-99% dei resample), non
rumore; il punto era di *efficienza relativa* (rende meno di un ipotetico asset perfettamente
scorrelato), non di esistenza dell'edge.
## Sintesi della trilogia (fill-haircut + turnover/hedge + bootstrap)
PREVDAY, dopo tre attacchi avversariali:
1. **Eseguibile alla taglia reale** ($600): haircut di fill +0.01 (blocker #4 smontato).
2. **Già a turnover efficiente**: ridurlo erode l'edge; nessuna ottimizzazione (config congelata).
3. **È un HEDGE, non alpha**: tutto il valore è la gamba short → tail-hedge di regime-down, additivo
alla flat-stance di TP01 (blocker #1 inchiodato).
4. **Edge bootstrap-robusto** ma **payoff grumoso** (il valore è in pochi giorni-crash) (blocker #3
declassato sul full, confermato sulla short; #2 ridimensionato).
**Candidato tail-hedge legittimo**, non sleeve-alpha. Resta in FORWARD-MONITOR: la domanda forward
non è più "è eseguibile / è overfit", ma **"la gamba short continua a pagare nei prossimi crash fuori
da 2022 e 2025-26?"**. Se sì → si valuta come overlay di tail-hedge (peso piccolo, atteso payoff
lumpy); se no → era beta-corto del regime down 2025-26.
@@ -0,0 +1,67 @@
# PREVDAY fill-haircut a basso capitale — il blocker d'esecuzione è BENIGNO (1/4 smontato)
**Data:** 2026-06-21 (follow-up di `2026-06-21-intraday-microstructure.md`)
**Script:** `scripts/research/intraday/fill_haircut.py`
**Esito:** l'haircut del fill reale a $600 è **+0.01 Sharpe** (trascurabile). Lo scettico
d'esecuzione (blocker #4) è **benigno**. Gli altri 3 blocker (hedge / null-corr-zero / tail-luck)
restano → PREVDAY resta in **forward-monitor, non deploy**.
## Domanda
Lo scettico d'esecuzione dell'onda intraday aveva segnalato: il vol-target di PREVDAY fa ~8500
ribilanciamenti/anno per gamba, 97-98% < $1 di nozionale a $600; a quel capitale (min_order $5) NON
puoi piazzarli, quindi il libro MODELED (ribilanciamento continuo, frictionless) è una finzione e lo
Sharpe modellato è gonfiato. Il forward-monitor traccia MODELED-$2000 vs REAL-$600 per misurarlo nei
mesi a venire — qui lo stimiamo SUBITO su tutto lo storico, replicando la STESSA logica dei due libri
di `paper_prevday.py` ma sull'intero path 1h (2019-03 → 2026-06, 63.732 barre).
Due libri identici tranne il fill:
- **MODELED**: ribilancia ad ogni barra (fee proporzionale su ogni |Δ|).
- **REAL-$C**: salta i ribilanciamenti con nozionale `|Δpos|·leg_cap < $5` (posizione stale →
tracking error, ma niente fee sui trade infinitesimi). Sweep C ∈ {600, 2000, 20000}.
## Risultati
| libro | FULL Sh | HOLD Sh | CAGR | DD | rebal/yr | skip% | fee-drag/yr |
|-------|---------|---------|------|----|---------:|------:|------------:|
| MODELED ($∞) | +1.23 | +1.27 | +24.3% | 27% | 17.484 | 0.0% | 2.49% |
| REAL-$20k | +1.23 | +1.27 | +24.4% | 27% | 3.747 | 78.6% | 2.47% |
| REAL-$2000 | +1.23 | +1.27 | +24.4% | 27% | 677 | 96.1% | 2.42% |
| REAL-$600 | +1.22 | +1.26 | +24.2% | 27% | 277 | 98.4% | 2.39% |
**HAIRCUT $600 (MODELED REAL): FULL Sharpe +0.01, HOLD-OUT +0.01.**
Domanda-soldi (l'uplift del blend regge col fill reale?):
| PV | w | FULL (uplift) | HOLD (uplift) |
|----|---|---------------|---------------|
| MODELED | 20% | 1.58 (+0.28) | 0.86 (+0.56) |
| MODELED | 30% | 1.65 (+0.36) | 1.08 (+0.78) |
| **REAL-$600** | 20% | 1.58 (+0.28) | 0.86 (**+0.55**) |
| **REAL-$600** | 30% | 1.65 (+0.35) | 1.08 (**+0.77**) |
(TP01 solo: FULL +1.30, HOLD +0.31.) L'uplift hold-out sopravvive **quasi intatto**.
## Lettura
Saltare il **98.4%** dei micro-ribilanciamenti a $600 non costa quasi nulla perché quei trade sono
*individualmente infinitesimi*: sia la fee risparmiata sia il tracking-error introdotto sono
trascurabili. Il PnL è dominato dai ~50 flip di direzione/anno + la deriva lenta del vol-target, che
il libro $600 cattura comunque sui movimenti grandi (la fee-drag passa solo da 2.49% a 2.39%). La
"finzione della fee sub-dollaro" è quindi **benigna**: non gonfia lo Sharpe modellato (MODELED e
REAL-$600 coincidono a ±0.01). NB: lo Sharpe **non si degrada** scendendo di capitale → l'edge
modellato di PREVDAY è eseguibile alla taglia reale; il blocker era altrove.
## Conseguenza sul verdetto
Dei 4 blocker che tenevano PREVDAY fuori dal deploy, il **#4 (fill a basso capitale) è SMONTATO**.
Restano in piedi i 3 strutturali (dall'onda intraday, non rivalutati qui):
1. **hedge-shaped** — l'uplift viene dai regimi TP01-down (uplift +0.79 TP01-down vs +0.20 TP01-up);
2. **fallisce il null a corr-zero** — uplift pre-2025 al 20-24° pctl del null di un asset random
scorrelato (aggiunge MENO del rumore);
3. **tail-luck** — top-5 giorni = 76-83% del PnL delle gambe revert, <10 eventi/anno.
PREVDAY resta il lead **più solido sull'esecuzione** di tutta la ricerca post-reset (il dubbio più
"fisico" è caduto), ma **forward-monitor, non deploy**, finché il track record forward non scioglie
hedge/coda/null. Lezione harness: `eval_weights_smallcap` (il gate min-order) va sempre eseguito
PRIMA di scartare un lead per "fill irreale" — qui avrebbe evitato di sopravvalutare il blocker #4.
@@ -0,0 +1,68 @@
# PREVDAY come overlay di tail-hedge sul portafoglio — simulazione d'impatto (NON deploy)
**Data:** 2026-06-21 (segue la trilogia fill-haircut / turnover-hedge / bootstrap)
**Script:** `scripts/portfolio/prevday_overlay.py`
**Esito:** a peso 10%, PREVDAY taglia il maxDD FULL del portafoglio **14.3% → 9.9% (31%)** e alza
l'hold-out Sharpe **1.66 → 1.97 (+0.31)**. 10% è vicino all'ottimo di DD. MA è tutto IN-SAMPLE: il
prize si materializza solo SE l'edge di PREVDAY persiste forward. PREVDAY resta FORWARD-MONITOR.
## Setup
Simulazione che NON tocca il registry di produzione: prende il portafoglio attivo (TP01 55% + XS01
25% + VRP01 20%), riscala i tre sleeve a (1W) mantenendone le proporzioni, e aggiunge PREVDAY a
peso W. Sweep W ∈ {0,5,10,15,20%}. PREVDAY = libro 1h breakout-continuation, parametri congelati,
50/50 BTC+ETH, fee 0.10% RT. Outer-join del portafoglio: PREVDAY dal 2018, VRP01 dal 2021, XS01 dal
2024 → nel 2019-20 PREVDAY pesa di fatto >W (solo TP01 accanto); nell'hold-out 2025+ (tutti e 4
attivi) pesa esattamente ~W → **l'HOLD-OUT è il confronto pulito a "10%"**.
## Sweep peso overlay
| peso PREVDAY | FULL Sharpe | FULL DD | HOLD Sharpe | HOLD DD | HOLD ret |
|--------------|------------:|--------:|------------:|--------:|---------:|
| BASELINE (55/25/20) | 1.68 | 14.3% | 1.66 | 3.4% | +16.7% |
| 5% | 1.80 | 11.1% | 1.83 | 3.3% | +17.8% |
| **10%** | **1.88** | **9.9%** | **1.97** | 3.3% | **+19.0%** |
| 15% | 1.93 | 10.3% | 2.06 | 3.3% | +20.2% |
| 20% | 1.95 | 10.6% | 2.09 | 3.4% | +21.4% |
(PREVDAY standalone: FULL Sh 1.23 / DD 26.7%; HOLD Sh 1.28 / DD 10.8%.)
## Lettura a 10%
- **FULL: Sharpe +0.20 (1.68→1.88), maxDD 14.3%→9.9% (4.4pp ≈ 31%).** Comportamento da tail-hedge:
la gamba short ammortizza i crash storici (2019-21, 2022).
- **HOLD-OUT: Sharpe +0.31 (1.66→1.97), ret +16.7%→+19.0%, DD 3.4%→3.3% (già bassissimo).** Nel
regime recente il beneficio è rendimento/Sharpe, non taglio DD.
- **10% ≈ ottimo di DD.** Oltre, lo Sharpe sale ancora (1.93→1.95) ma il maxDD FULL smette di
scendere (10.3→10.6%): stai solo aggiungendo rischio direzionale short. Argomento per ~10% in
chiave hedge (massimizza il taglio-coda per unità di rischio aggiunto).
## Per anno (baseline → overlay 10%)
| anno | ret | DD |
|------|-----|----|
| 2019 | +11.3 → +15.2 | 10.3 → 8.2 |
| 2020 | +51.1 → +53.1 | 8.4 → 6.3 |
| 2021 | +32.5 → +28.3 | 5.2 → 4.3 |
| 2022 | 3.0 → 1.6 | 3.7 → 3.0 |
| 2023 | +11.2 → +11.4 | 9.2 → 9.9 |
| 2024 | +24.4 → +25.7 | 3.9 → 3.4 |
| 2025 | +12.0 → +12.0 | 3.4 → 3.3 |
| 2026 | +4.2 → +6.2 | 2.6 → 2.2 |
Migliora o pareggia quasi ovunque; costa solo nel toro 2021 (premio d'assicurazione atteso per un
hedge) e leggermente sul DD 2023; paga nel bear 2022 e nel 2026.
## Verdetto
L'overlay 10% è **attraente in simulazione** — taglia il drawdown FULL di ~31% e alza l'hold-out
Sharpe +0.31, con 10% vicino all'ottimo di DD. Ma:
1. **È in-sample.** I guadagni assumono che l'edge di PREVDAY persista — il forward-monitor esiste
proprio per verificarlo. Questa simulazione quantifica il PRIZE, non lo prova.
2. **Outer-join:** il taglio-DD storico è gonfiato dal peso effettivo >10% nel 2019-20; il read
pulito a 10% è l'hold-out (prize = Sharpe +0.31).
3. Incidentale: il 3-way TP01+XS01+VRP01 baseline qui fa FULL 1.68 / HOLD 1.66.
**Azione: nessuna.** PREVDAY resta FORWARD-MONITOR (registry di produzione invariato). Quando il
track record forward avrà ~2-3 mesi, ri-valutare l'overlay 10% con la stessa metrica (taglio-DD +
hold-out Sharpe) su dati VERAMENTE fuori campione. Lo script è il riferimento per quel confronto.
@@ -0,0 +1,64 @@
# PREVDAY — la fee viene dai FLIP (no free lunch sul turnover) + è un HEDGE, non alpha
**Data:** 2026-06-21 (follow-up di `2026-06-21-prevday-fill-haircut.md`)
**Script:** `scripts/research/intraday/prevday_turnover.py`
**Esito:** (1) ridurre il turnover di PREVDAY erode l'edge — la config congelata è già efficiente.
(2) Il test long-only inchioda il blocker #1: **tutto il valore di portafoglio è la gamba SHORT**
PREVDAY è un **hedge di regime-down**, non alpha. Resta forward-monitor.
## Premessa (da fill_haircut)
Il libro REAL-$600 salta il 98.4% dei ribilanciamenti del vol-target e la fee-drag scende solo
2.49% → 2.39%/anno. Quindi la fee (~2.6%/anno) NON viene dal churn sub-dollaro ma dai **~70 flip di
direzione/anno**. Un deadband d'esecuzione è inutile; l'unica leva è ridurre i flip a LIVELLO DI
SEGNALE. Qui sweep delle leve (buffer, anchor, min-hold) + long-only vs long-short. Libro MODELED
(l'haircut di fill è +0.01, irrilevante). Metrica che conta = **uplift hold-out del blend 80/20**.
## (1) Turnover-reduction — no free lunch
| config | flip/yr | fee/yr | FULL Sh | HOLD Sh | DD | corrTP | blend HOLD upl |
|--------|--------:|-------:|--------:|--------:|---:|-------:|---------------:|
| **BASE** (anchor=1, k=0.30, LS) | 70 | 2.59% | +1.23 | +1.27 | 27% | +0.15 | **+0.56** |
| k=0.50 | 48 | 1.86% | +1.23 | +0.99 | 15% | +0.20 | +0.40 |
| k=0.75 | 32 | 1.31% | +1.06 | +0.13 | 16% | +0.27 | +0.00 |
| k=1.00 | 23 | 1.01% | +0.88 | +0.72 | 22% | +0.36 | +0.22 |
| anchor=2 | 39 | 1.55% | +0.89 | +0.54 | 22% | +0.25 | +0.20 |
| anchor=3 | 27 | 1.14% | +0.67 | 0.18 | 22% | +0.29 | 0.12 |
| anchor=5 | 15 | 0.75% | +1.15 | +0.70 | 19% | +0.41 | +0.25 |
| min_hold=24h | 70 | 2.59% | +1.22 | +1.37 | 32% | +0.15 | **+0.60** |
| min_hold=72h | 65 | 2.39% | +0.86 | +0.67 | 33% | +0.12 | +0.27 |
| combo-LT (k.75+anc2+24h) | 16 | 0.79% | +0.79 | +0.69 | 20% | +0.34 | +0.24 |
- **Allargare buffer/anchor taglia fee e turnover ma l'uplift cala monotonicamente** (k: 0.56→0.40→
0.00). Anchor multi-giorno tutto peggio → conferma il "anchor=1 only" del diario. I flip SONO
l'edge: meno flip = meno edge.
- **min_hold=24h** è l'unico ritocco "quasi gratis" (uplift +0.56→+0.60 a parità di fee) ma
**peggiora il DD 27%→−32%** → non vale cambiare una strategia congelata in forward-monitor.
- **Verdetto: la config base è già sulla frontiera efficiente turnover↔edge. Si lascia congelata.**
## (2) Long-only vs long-short — il blocker #1 inchiodato
| | FULL Sh | HOLD Sh | corrTP | blend HOLD upl | fee/yr |
|--|--------:|--------:|-------:|---------------:|-------:|
| **long-only** (no short) | **+1.55** | +0.52 | **+0.64** | **+0.09** | 1.30% |
| long-short (BASE) | +1.23 | +1.27 | +0.15 | +0.56 | 2.59% |
La versione **long-only ha Sharpe standalone più ALTO** (1.55 vs 1.23) ma è **correlata +0.64 a TP01
e non aggiunge quasi nulla al blend** (+0.09). **Tutto il valore di portafoglio viene dalla gamba
SHORT:** la short *abbassa* lo Sharpe standalone (shortare crypto nel toro 2019-24 perde) ma fornisce
**tutta** la decorrelazione (corrTP 0.64→0.15) e l'uplift hold-out (0.09→0.56).
**PREVDAY non è alpha: è strutturalmente un HEDGE di crash/regime-down.** Costa nel toro, paga
nell'orso (2022, 2025-26 down/chop). È additivo a TP01, che va *flat* nel risk-off ma non *short*.
Questo conferma e affina il blocker #1 dell'onda intraday ("l'uplift viene dai regimi TP01-down"):
non è solo conditional sui regimi down, è **interamente la gamba short** = una scommessa direzionale
che i ribassi continuino.
## Conseguenza sul verdetto
- Niente da ottimizzare: la config congelata è già efficiente; nessun cambio.
- **Riframing utile:** se PREVDAY un giorno avrà un ruolo, è come **overlay di tail-hedge** (non
sleeve-alpha), additivo alla difensività di TP01. Ma resta soggetto agli altri due blocker
(fallisce il null a corr-zero; tail-luck: top-5 giorni = 76-83% del PnL delle gambe revert).
- **Forward-monitor invariato.** Il test forward decisivo: la gamba short continua a pagare fuori da
2022 e 2025-26? Se sì → candidato tail-hedge; se no → era regime-luck.
+115
View File
@@ -0,0 +1,115 @@
"""SIMULAZIONE — PREVDAY come overlay di tail-hedge sul portafoglio attivo (NON deploy).
PREVDAY (src/strategies/prevday_breakout) resta in FORWARD-MONITOR. Qui misuriamo SOLO, in
simulazione, cosa farebbe al portafoglio live (TP01 55% + XS01 25% + VRP01 20%) aggiungerlo come
overlay a peso W, riscalando i tre sleeve esistenti a (1-W) e tenendo le loro proporzioni. La
trilogia (fill-haircut/turnover/bootstrap) ha stabilito che PREVDAY e' un HEDGE di regime-down
(tutto il valore = gamba short) eseguibile a taglia reale: l'overlay si giudica sul TAGLIO DEL
DRAWDOWN del portafoglio, non sul ritorno.
NB outer-join: PREVDAY parte dal 2018, XS01 dal 2024, VRP01 dal 2021. I pesi sono rinormalizzati
ogni giorno fra i soli sleeve con dato -> nel 2019-20 (solo TP01+PREVDAY) PREVDAY pesa di piu' del
target W; nell'hold-out 2025+ (tutti e 4 attivi) pesa esattamente ~W. Per questo l'HOLD-OUT e' il
confronto piu' pulito a "peso 10%".
uv run python scripts/portfolio/prevday_overlay.py
"""
from __future__ import annotations
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
import numpy as np
import pandas as pd
from src.backtest.harness import load
from src.strategies import prevday_breakout as pb
from src.portfolio.portfolio import StrategyPortfolio, Sleeve, metrics, HOLDOUT
from src.portfolio.sleeves import _tp01_returns, _xsec_returns, _vrp_combo_returns
ASSETS = ("BTC", "ETH")
FEE_SIDE = 0.0005
BASE_W = dict(TP01=0.55, XS01=0.25, VRP01=0.20) # proporzioni dei tre sleeve attivi
HEADLINE = 0.10
def _prevday_returns() -> pd.Series:
"""Rendimenti netti per-barra (1h) del libro PREVDAY 50/50 BTC+ETH (parametri congelati)."""
series = {}
for a in ASSETS:
df = load(a, "1h").reset_index(drop=True)
c = df["close"].values.astype(float)
r = np.zeros(len(c)); r[1:] = c[1:] / c[:-1] - 1.0
tgt = np.nan_to_num(pb.target(df), nan=0.0)
held = np.zeros(len(tgt)); held[1:] = tgt[:-1]
net = held * r - FEE_SIDE * np.abs(np.diff(tgt, prepend=tgt[0]))
series[a] = pd.Series(net, index=pd.to_datetime(df["datetime"], utc=True))
J = pd.concat(series, axis=1, join="inner").fillna(0.0)
return pd.Series(0.5 * J.iloc[:, 0].values + 0.5 * J.iloc[:, 1].values, index=J.index)
def build_portfolio(series_cache: dict, w_prev: float) -> StrategyPortfolio:
"""Portafoglio coi 3 sleeve riscalati a (1-w_prev) + PREVDAY a w_prev (0 = baseline)."""
sl = [
Sleeve("TP01_trend_1d", BASE_W["TP01"] * (1 - w_prev), lambda s=series_cache["TP01"]: s),
Sleeve("XS01_xsec_hl", BASE_W["XS01"] * (1 - w_prev), lambda s=series_cache["XS01"]: s),
Sleeve("VRP01_shortvol", BASE_W["VRP01"] * (1 - w_prev), lambda s=series_cache["VRP01"]: s),
]
if w_prev > 0:
sl.append(Sleeve("PREVDAY_hedge", w_prev, lambda s=series_cache["PREVDAY"]: s))
return StrategyPortfolio(sl)
def line(label, m):
return (f" {label:<22s} Sh {m['sharpe']:>5.2f} | ret {m['ret']*100:>+8.1f}% "
f"CAGR {m['cagr']*100:>+6.1f}% | DD {m['maxdd']*100:>5.1f}% | n {m['n']}")
def main():
print("=" * 92)
print(" PREVDAY OVERLAY (simulazione, NON deploy) — tail-hedge sul portafoglio TP01+XS01+VRP01")
print("=" * 92)
print(" Precalcolo sleeve...", flush=True)
cache = dict(TP01=_tp01_returns(), XS01=_xsec_returns(),
VRP01=_vrp_combo_returns(), PREVDAY=_prevday_returns())
print(f"\n PREVDAY standalone (per riferimento):")
from src.portfolio.portfolio import to_daily
pvd = to_daily(cache["PREVDAY"])
print(line("PREVDAY full", metrics(pvd)))
print(line("PREVDAY hold-out", metrics(pvd[pvd.index >= HOLDOUT])))
print(f"\n SWEEP PESO OVERLAY (FULL | HOLD-OUT) — headline {HEADLINE*100:.0f}%:")
print(f" {'peso PREVDAY':<14s} {'FULL Sharpe':>11s} {'FULL DD':>9s} | {'HOLD Sharpe':>11s} {'HOLD DD':>9s} {'HOLD ret':>9s}")
rows = {}
for w in (0.0, 0.05, 0.10, 0.15, 0.20):
bt = build_portfolio(cache, w).backtest()
rows[w] = bt
tag = "BASELINE" if w == 0 else f"{w*100:.0f}%"
star = " <-- headline" if abs(w - HEADLINE) < 1e-9 else ""
print(f" {tag:<14s} {bt['full']['sharpe']:>11.2f} {bt['full']['maxdd']*100:>8.1f}% | "
f"{bt['holdout']['sharpe']:>11.2f} {bt['holdout']['maxdd']*100:>8.1f}% "
f"{bt['holdout']['ret']*100:>+8.1f}%{star}")
base, ov = rows[0.0], rows[HEADLINE]
print(f"\n DETTAGLIO a {HEADLINE*100:.0f}% vs BASELINE:")
print(line("BASELINE FULL", base['full'])); print(line(f"OVERLAY{HEADLINE*100:.0f}% FULL", ov['full']))
print(line("BASELINE HOLD", base['holdout'])); print(line(f"OVERLAY{HEADLINE*100:.0f}% HOLD", ov['holdout']))
dSh = ov['holdout']['sharpe'] - base['holdout']['sharpe']
dDD = (ov['holdout']['maxdd'] - base['holdout']['maxdd']) * 100
print(f"\n >> HOLD-OUT: ΔSharpe {dSh:+.2f} | ΔmaxDD {dDD:+.1f}pp "
f"(tail-hedge = ci aspettiamo DD piu' basso)")
print(f"\n PER ANNO (baseline -> overlay {HEADLINE*100:.0f}%): ret% / DD%")
yb, yo = base['yearly'], ov['yearly']
for y in sorted(set(yb) | set(yo)):
b = yb.get(y, {}); o = yo.get(y, {})
print(f" {y}: ret {b.get('ret',0)*100:>+7.1f}% -> {o.get('ret',0)*100:>+7.1f}% "
f"DD {b.get('dd',0)*100:>5.1f}% -> {o.get('dd',0)*100:>5.1f}%")
print("=" * 92)
print(" Nota: PREVDAY resta FORWARD-MONITOR. Questa e' una simulazione di impatto, non un deploy.")
if __name__ == "__main__":
main()
+153
View File
@@ -0,0 +1,153 @@
"""fill_haircut — quanto il fill REALE a basso capitale erode il lead PREVDAY (e TP01)?
Lo scettico d'esecuzione (diario 2026-06-21) ha segnalato che il vol-target di PREVDAY fa ~8500
ribilanciamenti/anno, di cui 97-98% < $1 di nozionale a $600: a quel capitale NON puoi piazzare
quegli ordini (min_order $5), quindi il libro MODELED (ribilanciamento continuo, frictionless) è
una finzione. Il forward-monitor traccia MODELED-$2000 vs REAL-$600 per misurare il gap nei mesi
a venire — qui lo stimiamo SUBITO su tutto lo storico, replicando la STESSA logica di paper_prevday.
Due libri, identici tranne il fill:
MODELED : ribilancia ad ogni barra alla posizione-bersaglio (fee proporzionale su ogni |Δ|).
REAL-$C : capitale C, salta i ribilanciamenti con nozionale |Δpos|*leg_cap < min_order ($5)
(posizione resta "stale" -> tracking error, ma niente fee sui trade infinitesimi).
Sweep capitale {600, 2000, 20000} per mostrare a quanto l'haircut svanisce. Poi la domanda-soldi:
il blend 80%TP01+20%PREVDAY conserva l'uplift hold-out (+0.56 modellato) usando PREVDAY-REAL-$600?
uv run python scripts/research/intraday/fill_haircut.py
"""
import sys
from pathlib import Path
import numpy as np
import pandas as pd
ROOT = Path(__file__).resolve().parents[3]
sys.path.insert(0, str(ROOT))
from src.backtest.harness import load # noqa: E402
from src.strategies.prevday_breakout import target as pv_target # noqa: E402
from src.portfolio.portfolio import to_daily # noqa: E402
from src.portfolio.sleeves import _tp01_returns # noqa: E402
HOLD = pd.Timestamp("2025-01-01", tz="UTC")
FEE_SIDE = 0.0005 # 0.05%/side = 0.10% RT
MIN_ORDER = 5.0
WEIGHT = 0.5
ASSETS = ["BTC", "ETH"]
def _sh(x):
x = x.dropna()
return float(x.mean() / x.std() * np.sqrt(365.25)) if len(x) > 2 and x.std() > 0 else 0.0
def _dd(x):
eq = (1 + x.fillna(0)).cumprod()
return float(((eq - eq.cummax()) / eq.cummax()).min())
def simulate(targets: dict, rets: dict, idx_dt, capital):
"""Bar-by-bar 50/50 book. capital=None -> MODELED (continuous). Returns (daily_ret, stats)."""
n = len(idx_dt)
held = {a: 0.0 for a in ASSETS}
net = np.zeros(n)
exec_ct = {a: 0 for a in ASSETS}
skip_ct = {a: 0 for a in ASSETS}
fee_tot = 0.0
for i in range(n):
step = 0.0
for a in ASSETS:
tgt = float(targets[a][i]); r = float(rets[a][i]); h = held[a]
if capital is None: # MODELED: always rebalance
new_h = tgt; traded = abs(tgt - h)
exec_ct[a] += 1 if traded > 1e-9 else 0
else: # REAL-$C: skip sub-min_order
leg_cap = capital * WEIGHT
if abs(tgt - h) * leg_cap >= MIN_ORDER:
new_h = tgt; traded = abs(tgt - h); exec_ct[a] += 1
else:
new_h = h; traded = 0.0; skip_ct[a] += 1
fee = FEE_SIDE * traded
fee_tot += WEIGHT * fee
step += WEIGHT * (h * r - fee) # earn on position HELD into bar, pay fee on rebalance
held[a] = new_h
net[i] = step
s = pd.Series(net, index=idx_dt)
daily = s.groupby(s.index.floor("1D")).sum()
yrs = (idx_dt[-1] - idx_dt[0]).days / 365.25
ex = sum(exec_ct.values()); sk = sum(skip_ct.values())
stats = dict(execs_per_yr=ex / yrs, skip_frac=sk / (ex + sk) if (ex + sk) else 0.0,
fee_drag_per_yr=fee_tot / yrs)
return daily, stats
def build_targets():
targets, rets, ts_sets = {}, {}, {}
dts = {}
for a in ASSETS:
df = load(a, "1h").reset_index(drop=True)
c = df["close"].values.astype(float)
r = np.zeros(len(c)); r[1:] = c[1:] / c[:-1] - 1.0
targets[a] = pv_target(df); rets[a] = r
ts = df["timestamp"].values.astype("int64")
ts_sets[a] = ts
dts[a] = pd.to_datetime(df["datetime"], utc=True).values
common = sorted(set(ts_sets["BTC"]).intersection(ts_sets["ETH"]))
pos = {a: {int(t): i for i, t in enumerate(ts_sets[a])} for a in ASSETS}
T, R = {a: [] for a in ASSETS}, {a: [] for a in ASSETS}
dt_out = []
for t in common:
i_btc = pos["BTC"][int(t)]
dt_out.append(dts["BTC"][i_btc])
for a in ASSETS:
i = pos[a][int(t)]
T[a].append(targets[a][i]); R[a].append(rets[a][i])
idx = pd.to_datetime(dt_out, utc=True)
return {a: np.array(T[a]) for a in ASSETS}, {a: np.array(R[a]) for a in ASSETS}, idx
def row(label, daily):
J = daily.dropna(); JH = J[J.index >= HOLD]
yrs = (J.index[-1] - J.index[0]).days / 365.25
cagr = (1 + J).prod() ** (1 / yrs) - 1
return (f" {label:<18s} FULL Sh {_sh(J):+5.2f} HOLD Sh {_sh(JH):+5.2f} "
f"CAGR {cagr*100:+5.1f}% DD {_dd(J)*100:4.0f}%")
def main():
print("=" * 92)
print(" FILL-HAIRCUT — PREVDAY: libro MODELED (continuo) vs REAL-$C (skip < $5 min-order)")
print("=" * 92)
T, R, idx = build_targets()
print(f" path 1h: {len(idx)} barre {idx[0].date()} -> {idx[-1].date()}\n")
books = {}
for cap, lab in [(None, "MODELED ($∞)"), (20000, "REAL-$20k"), (2000, "REAL-$2000"), (600, "REAL-$600")]:
daily, st = simulate(T, R, idx, cap)
books[lab] = daily
print(row(lab, daily) +
f" | rebal/yr {st['execs_per_yr']:6.0f} skip {st['skip_frac']*100:4.1f}% "
f"fee-drag/yr {st['fee_drag_per_yr']*100:4.2f}%")
mod = books["MODELED ($∞)"]; real = books["REAL-$600"]
hc_full = _sh(mod.dropna()) - _sh(real.dropna())
JHm = mod[mod.index >= HOLD]; JHr = real[real.index >= HOLD]
hc_hold = _sh(JHm) - _sh(JHr)
print(f"\n >> HAIRCUT $600 (MODELED - REAL): FULL Sharpe {hc_full:+.2f} | HOLD-OUT Sharpe {hc_hold:+.2f}")
# money question: does the blend uplift survive at REAL-$600?
print("\n" + "-" * 92)
print(" BLEND 80%TP01 + 20%PREVDAY — sopravvive l'uplift hold-out col fill reale?")
tp = to_daily(_tp01_returns())
for lab, pv in [("MODELED", mod), ("REAL-$600", real)]:
J = pd.concat({"TP": tp, "PV": pv}, axis=1).dropna(); JH = J[J.index >= HOLD]
for w in (0.20, 0.30):
b = (1 - w) * J["TP"] + w * J["PV"]; bh = (1 - w) * JH["TP"] + w * JH["PV"]
print(f" PV={lab:<9s} w={w:.0%} FULL {_sh(b):+.2f} (upl {_sh(b)-_sh(J['TP']):+.2f}) "
f"HOLD {_sh(bh):+.2f} (upl {_sh(bh)-_sh(JH['TP']):+.2f})")
print(f" [TP01 solo: FULL {_sh(tp.dropna()):+.2f} HOLD {_sh(tp[tp.index>=HOLD]):+.2f}]")
print("=" * 92)
if __name__ == "__main__":
main()
@@ -0,0 +1,142 @@
"""prevday_bootstrap — l'edge di PREVDAY è coda-fortuna o persistente? (blocker #2/#3)
CHIARIMENTO: il "top-5 giorni = 76-83% del PnL" del diario intraday era sulle GAMBE REVERT del combo
a 5 segnali (vol_event/volume_spike/gap_fill), poi SCARTATE. Il sopravvissuto in forward-monitor è
PREVDAY (breakout-continuation). Qui testiamo la concentrazione e la robustezza di PREVDAY STESSO —
e in particolare della sua GAMBA SHORT, che (prevday_turnover) è l'intero valore di portafoglio.
Due test:
A) CONCENTRAZIONE — quota del PnL nei top-K giorni (riproduce la metrica del diario su PREVDAY,
full / short-only / long-only, vs TP01 come riferimento: PREVDAY è PIÙ concentrato di ciò che
già deployamo?). + giorni per arrivare al 50% del guadagno cumulato.
B) CIRCULAR BLOCK BOOTSTRAP (blocchi da 20g, preserva autocorrelazione/regime) — distribuzione di:
standalone Sharpe (full + hold-out) e dell'UPLIFT hold-out del blend 80%TP01+20%PREVDAY (la
metrica-soldi). %>0 e 5° percentile = quanto l'edge dipende da quali blocchi sono capitati.
uv run python scripts/research/intraday/prevday_bootstrap.py
"""
import sys
from pathlib import Path
import numpy as np
import pandas as pd
ROOT = Path(__file__).resolve().parents[3]
sys.path.insert(0, str(ROOT))
from src.backtest.harness import load # noqa: E402
from src.strategies import prevday_breakout as pb # noqa: E402
from src.portfolio.portfolio import to_daily # noqa: E402
from src.portfolio.sleeves import _tp01_returns # noqa: E402
HOLD = pd.Timestamp("2025-01-01", tz="UTC")
FEE_SIDE = 0.0005
WEIGHT = 0.5
ASSETS = ["BTC", "ETH"]
RNG = np.random.default_rng(12345)
B = 3000
BLOCK = 20
def _sh(x):
x = np.asarray(x, float); x = x[np.isfinite(x)]
return float(x.mean() / x.std() * np.sqrt(365.25)) if len(x) > 2 and x.std() > 0 else 0.0
def _leg_daily(dfs, leg):
"""Ritorni daily 50/50 di PREVDAY restringendo la direzione: 'full'|'short'|'long'."""
out = None
for a in ASSETS:
df = dfs[a]
c = df["close"].values.astype(float)
r = np.zeros(len(c)); r[1:] = c[1:] / c[:-1] - 1.0
d = pb._breakout_direction(df, pb.ANCHOR_DAYS, pb.BUFFER_K, True)
if leg == "short":
d = np.minimum(d, 0.0)
elif leg == "long":
d = np.maximum(d, 0.0)
tgt = np.nan_to_num(pb._vol_target(d, df, pb.TARGET_VOL, pb.VOL_WIN_DAYS, pb.LEV_CAP), nan=0.0)
held = np.zeros(len(tgt)); held[1:] = tgt[:-1]
net = held * r - FEE_SIDE * np.abs(np.diff(tgt, prepend=tgt[0]))
s = pd.Series(net, index=pd.to_datetime(df["datetime"], utc=True))
dd = s.groupby(s.index.floor("1D")).sum()
out = dd if out is None else out.add(dd, fill_value=0)
return WEIGHT * out
def concentration(daily, label):
s = daily.dropna()
total = s.sum()
pos = s[s > 0].sum()
topk = {k: s.nlargest(k).sum() / total if total != 0 else float("nan") for k in (5, 10, 20)}
# giorni (in ordine decrescente) per arrivare al 50% del guadagno lordo positivo
cum = s.sort_values(ascending=False).cumsum()
d50 = int((cum < 0.5 * pos).sum()) + 1 if pos > 0 else -1
n = len(s)
print(f" {label:<22s} n={n} totRet {total*100:+6.0f}% "
f"top5 {topk[5]*100:4.0f}% top10 {topk[10]*100:4.0f}% top20 {topk[20]*100:4.0f}% "
f"giorni->50% gain: {d50} ({d50/n*100:.1f}% dei giorni)")
def block_boot_joint(tp, pv, n_iter=B, block=BLOCK):
"""Bootstrap a blocchi circolari della serie CONGIUNTA (tp,pv) allineata. Ritorna i campioni di
(sh_pv, uplift_blend_80_20)."""
J = pd.concat({"TP": tp, "PV": pv}, axis=1, sort=True).dropna()
a = J["TP"].values; b = J["PV"].values
n = len(a)
nblocks = int(np.ceil(n / block))
sh_pv, upl = [], []
base_tp = _sh(a)
for _ in range(n_iter):
starts = RNG.integers(0, n, size=nblocks)
idx = np.concatenate([(np.arange(s, s + block) % n) for s in starts])[:n]
ta, tb = a[idx], b[idx]
sh_pv.append(_sh(tb))
blend = 0.8 * ta + 0.2 * tb
upl.append(_sh(blend) - _sh(ta))
return np.array(sh_pv), np.array(upl), base_tp
def report_boot(name, sh, upl):
def q(x, p):
return float(np.percentile(x, p))
print(f" {name}")
print(f" PREVDAY Sharpe : mediana {np.median(sh):+.2f} [5°,95°]=[{q(sh,5):+.2f},{q(sh,95):+.2f}] %>0 {np.mean(sh>0)*100:.0f}%")
print(f" blend 80/20 UPLIFT: mediana {np.median(upl):+.2f} [5°,95°]=[{q(upl,5):+.2f},{q(upl,95):+.2f}] "
f"%>0 {np.mean(upl>0)*100:.0f}% %>+0.10 {np.mean(upl>0.10)*100:.0f}%")
def main():
print("=" * 100)
print(" PREVDAY bootstrap — l'edge è coda-fortuna o persistente? (blocco da 20g, B=%d)" % B)
print("=" * 100)
dfs = {a: load(a, "1h").reset_index(drop=True) for a in ASSETS}
pv_full = _leg_daily(dfs, "full")
pv_short = _leg_daily(dfs, "short")
pv_long = _leg_daily(dfs, "long")
tp = to_daily(_tp01_returns())
print("\n[A] CONCENTRAZIONE del PnL nei top-K giorni (più alto = più coda-fortuna):")
concentration(pv_full, "PREVDAY full")
concentration(pv_short, "PREVDAY short-only")
concentration(pv_long, "PREVDAY long-only")
concentration(tp, "TP01 (riferimento)")
print(f"\n[B] CIRCULAR BLOCK BOOTSTRAP — FULL ({pv_full.dropna().index.min().date()}->{pv_full.dropna().index.max().date()}):")
sh, upl, base = block_boot_joint(tp, pv_full)
print(f" [TP01 base full Sharpe {base:+.2f}; uplift osservato +0.28 a w20]")
report_boot("full sample:", sh, upl)
print(f"\n[B] HOLD-OUT (2025+):")
tph = tp[tp.index >= HOLD]; pvh = pv_full[pv_full.index >= HOLD]
shH, uplH, baseH = block_boot_joint(tph, pvh)
print(f" [TP01 base hold Sharpe {baseH:+.2f}; uplift osservato +0.56 a w20]")
report_boot("hold-out:", shH, uplH)
print(f"\n[B] SHORT-ONLY hold-out (la gamba che è tutto il valore):")
shS, uplS, _ = block_boot_joint(tph, pv_short[pv_short.index >= HOLD])
report_boot("short-only hold-out:", shS, uplS)
print("=" * 100)
if __name__ == "__main__":
main()
@@ -0,0 +1,134 @@
"""prevday_turnover — la fee di PREVDAY viene dai FLIP, non dai micro-ribilanciamenti. Si può tagliare?
Da fill_haircut.py: il libro REAL-$600 salta il 98.4% dei ribilanciamenti del vol-target e la
fee-drag scende solo 2.49% -> 2.39%/anno. Quindi la fee (~2.4%/anno) e' dominata dai ~50 FLIP di
direzione/anno, non dal churn sub-dollaro. Un deadband d'esecuzione e' inutile; la leva e' ridurre i
flip a LIVELLO DI SEGNALE. Qui sweepiamo le leve che riducono i flip e misuriamo il trade-off
turnover <-> edge:
* BUFFER_K (break piu' deciso = meno flip) {0.30 base, 0.50, 0.75, 1.00}
* ANCHOR_DAYS (range multi-giorno = livelli piu' larghi){1 base, 2, 3, 5}
* MIN_HOLD (non flippare entro N ore dall'ultimo flip) {0 base, 24, 72}
e in piu' LONG-ONLY vs LONG-SHORT (isola la gamba short = l'hedge del blocker #1).
Per ogni config: flip/anno, fee-drag/anno, FULL/HOLD Sharpe, corr a TP01, e l'uplift hold-out del
blend 80%TP01+20%PV (la metrica che conta). Libro MODELED (l'haircut di fill e' +0.01, irrilevante).
uv run python scripts/research/intraday/prevday_turnover.py
"""
import sys
from pathlib import Path
import numpy as np
import pandas as pd
ROOT = Path(__file__).resolve().parents[3]
sys.path.insert(0, str(ROOT))
from src.backtest.harness import load # noqa: E402
from src.strategies import prevday_breakout as pb # noqa: E402
from src.portfolio.portfolio import to_daily # noqa: E402
from src.portfolio.sleeves import _tp01_returns # noqa: E402
HOLD = pd.Timestamp("2025-01-01", tz="UTC")
FEE_SIDE = 0.0005
WEIGHT = 0.5
ASSETS = ["BTC", "ETH"]
def _sh(x):
x = x.dropna()
return float(x.mean() / x.std() * np.sqrt(365.25)) if len(x) > 2 and x.std() > 0 else 0.0
def _dd(x):
eq = (1 + x.fillna(0)).cumprod()
return float(((eq - eq.cummax()) / eq.cummax()).min())
def _min_hold(direction, min_hold_bars):
"""Sopprime i flip entro min_hold_bars dall'ultimo cambio di segno (riduce il churn di segnale)."""
if min_hold_bars <= 0:
return direction
out = direction.copy()
last_flip = -10**9
cur = out[0]
for i in range(len(out)):
if np.sign(out[i]) != np.sign(cur):
if i - last_flip >= min_hold_bars:
cur = out[i]; last_flip = i
else:
out[i] = cur # mantieni la posizione (flip soppresso)
else:
cur = out[i]
return out
def build(dfs, anchor, k, min_hold_bars, allow_short):
"""Ritorni daily 50/50 + flip/anno + fee-drag/anno per una config di segnale (libro modeled)."""
legs, idx_ref = [], None
flips = 0; fee_tot = 0.0; yrs = None
daily_sum = None
for a in ASSETS:
df = dfs[a]
c = df["close"].values.astype(float)
r = np.zeros(len(c)); r[1:] = c[1:] / c[:-1] - 1.0
direction = pb._breakout_direction(df, anchor, k, allow_short)
direction = _min_hold(direction, min_hold_bars)
tgt = pb._vol_target(direction, df, pb.TARGET_VOL, pb.VOL_WIN_DAYS, pb.LEV_CAP)
tgt = np.nan_to_num(tgt, nan=0.0)
held = np.zeros(len(tgt)); held[1:] = tgt[:-1]
turn = np.abs(np.diff(tgt, prepend=tgt[0]))
net = held * r - FEE_SIDE * turn
dt = pd.to_datetime(df["datetime"], utc=True)
s = pd.Series(net, index=dt)
d = s.groupby(s.index.floor("1D")).sum()
daily_sum = d if daily_sum is None else daily_sum.add(d, fill_value=0)
flips += int((np.sign(direction[1:]) != np.sign(direction[:-1])).sum())
fee_tot += float((FEE_SIDE * turn).sum())
yrs = (dt.iloc[-1] - dt.iloc[0]).days / 365.25
daily = WEIGHT * daily_sum
return daily, flips / 2 / yrs, WEIGHT * fee_tot / yrs # flips mediati sulle 2 gambe
def main():
print("=" * 104)
print(" PREVDAY turnover-reduction — la fee viene dai FLIP. Si taglia senza perdere l'edge?")
print("=" * 104)
dfs = {a: load(a, "1h").reset_index(drop=True) for a in ASSETS}
tp = to_daily(_tp01_returns())
def line(label, daily, flips, fee):
J = pd.concat({"TP": tp, "PV": daily}, axis=1, sort=True).dropna(); JH = J[J.index >= HOLD]
b = 0.8 * J["TP"] + 0.2 * J["PV"]; bh = 0.8 * JH["TP"] + 0.2 * JH["PV"]
upl_h = _sh(bh) - _sh(JH["TP"])
print(f" {label:<26s} flip/yr {flips:5.0f} fee {fee*100:4.2f}% "
f"FULL {_sh(J['PV']):+5.2f} HOLD {_sh(JH['PV']):+5.2f} DD {_dd(J['PV'])*100:4.0f}% "
f"corrTP {J['PV'].corr(J['TP']):+.2f} blendHOLDupl {upl_h:+.2f}")
print(f" [TP01 solo: FULL {_sh(tp.dropna()):+.2f} HOLD {_sh(tp[tp.index>=HOLD]):+.2f}]\n")
print(" -- BASE (congelato: anchor=1, k=0.30, no min-hold, long-short) --")
d, f, fee = build(dfs, 1, 0.30, 0, True); line("BASE", d, f, fee)
print("\n -- BUFFER_K piu' ampio (break piu' deciso) --")
for k in (0.50, 0.75, 1.00):
d, f, fee = build(dfs, 1, k, 0, True); line(f"k={k:.2f}", d, f, fee)
print("\n -- ANCHOR_DAYS multi-giorno (range piu' largo) --")
for an in (2, 3, 5):
d, f, fee = build(dfs, an, 0.30, 0, True); line(f"anchor={an}", d, f, fee)
print("\n -- MIN_HOLD (no flip entro N ore) --")
for mh in (24, 72):
d, f, fee = build(dfs, 1, 0.30, mh, True); line(f"min_hold={mh}h", d, f, fee)
print("\n -- combo low-turnover (k=0.75 + anchor=2 + min_hold=24h) --")
d, f, fee = build(dfs, 2, 0.75, 24, True); line("combo-LT", d, f, fee)
print("\n -- LONG-ONLY vs LONG-SHORT (isola la gamba short = hedge del blocker #1) --")
d, f, fee = build(dfs, 1, 0.30, 0, False); line("long-only (no short)", d, f, fee)
d, f, fee = build(dfs, 1, 0.30, 0, True); line("long-short (BASE)", d, f, fee)
print("=" * 104)
if __name__ == "__main__":
main()