Compare commits
76 Commits
5ac4e16af8
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| b691f48f43 | |||
| 822aa1307e | |||
| cf7de40dc0 | |||
| eef200cf14 | |||
| 116abf5203 | |||
| 26f8d27a61 | |||
| 76120b59c2 | |||
| a74cc69583 | |||
| 73d74c5e53 | |||
| e6657fcb16 | |||
| 491411ac77 | |||
| ccf5e38101 | |||
| 567046953d | |||
| 31369b358c | |||
| 88f5f5a02a | |||
| ee82e0a056 | |||
| c52e0ab3f8 | |||
| cff5fa2bf5 | |||
| 02d875b07b | |||
| b4ec92734c | |||
| aad69f9790 | |||
| a158d0e2ae | |||
| e28176efef | |||
| 8d559c6f33 | |||
| 555977d987 | |||
| 8c3868cb31 | |||
| 9d586eeb58 | |||
| 69e3517f1b | |||
| cf72e395d3 | |||
| db738bce3b | |||
| 25a22fc7c1 | |||
| eeac97dde4 | |||
| c8a390d6b7 | |||
| 384b9cb0af | |||
| 160ad300be | |||
| 50e2adf837 | |||
| 7eb0f67956 | |||
| 8d1fe173f7 | |||
| de72e3ce1f | |||
| 8e46a62e67 | |||
| c7c07f4c35 | |||
| 2d8faf3896 | |||
| 64d98a070d | |||
| 237ca8da13 | |||
| 856a02fcc5 | |||
| 010d1f0733 | |||
| 1c15c3c1be | |||
| 3a3bdd5c7b | |||
| f983cc2447 | |||
| 389573e517 | |||
| 3b552a92da | |||
| 09c8bb7de8 | |||
| c4bc336a53 | |||
| 55c337144e | |||
| d916520f2c | |||
| d2d535cf6a | |||
| 67850b0dd8 | |||
| 437cf11199 | |||
| c9b89739c1 | |||
| 03267b8fc3 | |||
| 1c0b5f1869 | |||
| 61180637eb | |||
| 745ba7d066 | |||
| 043f141bf1 | |||
| b388c462e8 | |||
| 8514c096ea | |||
| b5db59bea9 | |||
| 5cce7acfe1 | |||
| d5dd6f4b72 | |||
| 4ae3b42442 | |||
| 24565974c0 | |||
| 62d3b23cc6 | |||
| 0adc69a357 | |||
| 1afb1014c9 | |||
| f5d30d88b9 | |||
| 9612560479 |
@@ -0,0 +1,7 @@
|
|||||||
|
# Credenziali IB Gateway PAPER per la ricerca dati (account paper, es. DUQ513966).
|
||||||
|
# COPIA questo file in .env.ibgw (gitignored) e riempi i valori REALI.
|
||||||
|
# cp .env.ibgw.example .env.ibgw && chmod 600 .env.ibgw && nano .env.ibgw
|
||||||
|
# NON committare mai .env.ibgw. Sono credenziali del CONTO PAPER (nessun denaro reale),
|
||||||
|
# l'API e' comunque READ_ONLY (solo dati storici, nessun ordine).
|
||||||
|
TWS_USERID=il_tuo_username_paper
|
||||||
|
TWS_PASSWORD=la_tua_password_paper
|
||||||
+15
@@ -6,6 +6,8 @@ build/
|
|||||||
.venv/
|
.venv/
|
||||||
.env
|
.env
|
||||||
!.env.example
|
!.env.example
|
||||||
|
.env.ibgw
|
||||||
|
!.env.ibgw.example
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
@@ -52,3 +54,16 @@ logs/
|
|||||||
# feed backup pre-rebuild (binari rigenerabili, NON in git) + stato paper trader (runtime)
|
# feed backup pre-rebuild (binari rigenerabili, NON in git) + stato paper trader (runtime)
|
||||||
data/_feed_backup/
|
data/_feed_backup/
|
||||||
data/paper_trend/
|
data/paper_trend/
|
||||||
|
data/paper_portfolio/
|
||||||
|
|
||||||
|
# output grezzo dello sweep di ricerca xsec (rigenerabile dagli script in runs/)
|
||||||
|
scripts/research/xsec/runs/out/
|
||||||
|
|
||||||
|
# blind-signal derived data (regenerable via make_blind.py)
|
||||||
|
data/blind/
|
||||||
|
scripts/research/blind/leaderboard.json
|
||||||
|
|
||||||
|
# forward-monitor runtime state (regenerable, forward-only)
|
||||||
|
data/paper_prevday/
|
||||||
|
data/paper_combo/
|
||||||
|
data/paper_statarb/
|
||||||
|
|||||||
@@ -13,7 +13,13 @@ Cosa è cambiato:
|
|||||||
**solo BTC/ETH** (tutti i TF). Gli alt sono esclusi (illiquidi/divergenti/non certificabili).
|
**solo BTC/ETH** (tutti i TF). Gli alt sono esclusi (illiquidi/divergenti/non certificabili).
|
||||||
- Tutto il codice vecchio (strategie, stack live, ~100 script di ricerca/gate, dati non
|
- Tutto il codice vecchio (strategie, stack live, ~100 script di ricerca/gate, dati non
|
||||||
certificati, 60+ diari) è **archiviato in `Old/`** (preservato in git, non cancellato).
|
certificati, 60+ diari) è **archiviato in `Old/`** (preservato in git, non cancellato).
|
||||||
- L'esecuzione è **DISABILITATA**, il conto mainnet è flat. **Non c'è trading live attivo.**
|
- ~~L'esecuzione è DISABILITATA, il conto mainnet è flat. Non c'è trading live attivo.~~
|
||||||
|
**AGGIORNATO 2026-06-20: l'esecuzione di TP01 è ARMATA e LIVE su Deribit mainnet** —
|
||||||
|
`config/live.json` `execution_enabled=true` + cron giornaliero `live_execute.py --execute`
|
||||||
|
(cablato in `scripts/cron_daily.sh`). Guardrail: cap **$300 notional/asset**, min order $5,
|
||||||
|
**disaster-SL on-book −30%**, alert Telegram su esecuzione/errori. **Capitale reale ≈ $600**
|
||||||
|
(NON i €2000 nominali del paper trader). Stato corrente: **flat** (target TSMOM risk-off →
|
||||||
|
BTC/ETH 0.0x, nessun ordine). Solo TP01 è eseguito; XS01/VRP01 restano paper/STAT-MODE.
|
||||||
- Si riparte dalla ricerca di strategie NUOVE, su dati certi, con la metodologia qui sotto.
|
- Si riparte dalla ricerca di strategie NUOVE, su dati certi, con la metodologia qui sotto.
|
||||||
|
|
||||||
### Ricerca post-reset (2026-06-19) — esito
|
### Ricerca post-reset (2026-06-19) — esito
|
||||||
@@ -34,6 +40,14 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis
|
|||||||
Deploy/paper a **1d**. Diari `2026-06-19-tp01-verification.md` / `-tp01-lookahead-fix-lf.md`.
|
Deploy/paper a **1d**. Diari `2026-06-19-tp01-verification.md` / `-tp01-lookahead-fix-lf.md`.
|
||||||
Paper trader: `scripts/live/paper_trend.py` (1d). Test: `tests/test_trend_portfolio.py`.
|
Paper trader: `scripts/live/paper_trend.py` (1d). Test: `tests/test_trend_portfolio.py`.
|
||||||
Ri-verifica: `scripts/analysis/{verify_tp01,stress_tp01,tp01_lowfreq}.py`.
|
Ri-verifica: `scripts/analysis/{verify_tp01,stress_tp01,tp01_lowfreq}.py`.
|
||||||
|
⚠️ **ANCHOR TIMING-LUCK (2026-07-02, confermato da scettico):** l'hold-out ~0.31 è calcolato
|
||||||
|
sull'ancora daily 00:00 UTC, che è la **migliore delle 24 possibili** (mediana ancore 0.04, banda
|
||||||
|
[−0.13,+0.30]; P~0.86 che una qualsiasi ancora mostri uno spike così per puro caso) → l'hold-out
|
||||||
|
2025-26 NON risolve l'edge di ritorno di TP01; ciò che regge a OGNI ancora è il **taglio del DD**
|
||||||
|
(7-10% vs ~60% B&H). FULL/plateau/deflated-Sharpe/gate INVARIATI (h=0 al 31° pctl su FULL).
|
||||||
|
Regola: i futuri numeri hold-out di strategie a ribilanciamento ancorato si citano con la banda
|
||||||
|
d'ancora. Diario `2026-07-02-timing-crt-wave.md`; script `scripts/research/r0702_tp01_offset.py`
|
||||||
|
+ `r0702_skeptic_offset.py`.
|
||||||
- **XS01 Cross-Sectional Momentum (Hyperliquid) — DIVERSIFICATORE che migliora il portafoglio** —
|
- **XS01 Cross-Sectional Momentum (Hyperliquid) — DIVERSIFICATORE che migliora il portafoglio** —
|
||||||
`src/portfolio/sleeves.py:_xsec_returns`. Market-neutral su **19 alt liquidi major** Hyperliquid (1d,
|
`src/portfolio/sleeves.py:_xsec_returns`. Market-neutral su **19 alt liquidi major** Hyperliquid (1d,
|
||||||
dal 2024): ogni 10g long i 5 più forti / short i 5 più deboli, vol-target 20%. **Scorrelato a TP01
|
dal 2024): ogni 10g long i 5 più forti / short i 5 più deboli, vol-target 20%. **Scorrelato a TP01
|
||||||
@@ -43,13 +57,55 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis
|
|||||||
regime compatto). Standalone FULL Sh **1.50** / HOLD 1.71 / DD 11%, plateau robusto (lookback, gate
|
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 (55%) + XS01 (25%) + VRP01 (20%)** (`src/portfolio/sleeves.active_sleeves`):
|
- **PORTAFOGLIO ATTIVO = TP01 (33%) + XS01 (15%) + VRP01 (12%) + SKH01 (20%) + GTAA01 (20%)**
|
||||||
TP01+XS01 combinato **FULL Sharpe 1.55, HOLD-OUT 1.55, DD 4.4%**. Aggiunto **VRP01** (options
|
(`src/portfolio/sleeves.active_sleeves`): TP01+XS01 combinato **FULL Sharpe 1.55, HOLD-OUT 1.55,
|
||||||
short-vol, sotto): TP01+VRP01 da solo fa FULL Sh 1.30→1.44 / HOLD 0.31→0.40 a peso 20% (3-way da
|
DD 4.4%**. Aggiunto **VRP01** (options short-vol, sotto): TP01+VRP01 da solo fa FULL Sh 1.30→1.44 /
|
||||||
validare locale con dati HL). Report `scripts/portfolio/run_portfolio.py`. Sleeve a date d'inizio
|
HOLD 0.31→0.40 a peso 20%. **Aggiunto SKH01-V2-DD @25% effettivo (2026-06-23, sotto)**: 4-sleeve
|
||||||
diverse → outer-join con pesi rinormalizzati (TP01 da solo 2019-20, VRP dal 2021, blend pieno dal 2024).
|
**FULL Sharpe 1.68→2.13, HOLD-OUT 1.63→2.30, DD full 14.3%→7.8%** (Skyhook quasi-ortogonale,
|
||||||
|
corr ~0.09). **Aggiunto GTAA01 @20% effettivo (2026-07-01, i 4 preesistenti scalati ×0.80):**
|
||||||
|
trend difensivo equity 6-ETF su IB (`src/portfolio/gtaa.py`, ~30 anni storia, validato 2026-06-22
|
||||||
|
su OOS equity 2015+ INDIPENDENTE dall'hold-out crypto, corr al book ~+0.10) → 5-sleeve
|
||||||
|
**FULL Sharpe 2.12→2.24, HOLD-OUT 2.21→2.46, DD full 7.8%→6.2%** (costo dichiarato: CAGR full
|
||||||
|
23.3→18.8%; 2022 unico anno con dSh−). Uplift positivo in-sample E su tutte le finestre disgiunte
|
||||||
|
(vs EW-STR refutato lo stesso giorno). Convenzioni: weekend/festivi equity = 0.0 (capitale IB
|
||||||
|
fermo, non riciclato); attivazione nel book all'era crypto 2019-03; **il book live Deribit
|
||||||
|
(`deribit_book_sleeves` TP01+SKH01 75/25) NON lo include** (GTAA in paper_combo dal 2026-06-23).
|
||||||
|
Test `tests/test_gtaa_sleeve.py`; diario `2026-07-01-strategy-wave-6threads.md` (addendum GTAA).
|
||||||
|
Report `scripts/portfolio/run_portfolio.py`. Sleeve a date d'inizio diverse → outer-join con pesi
|
||||||
|
rinormalizzati (TP01/SKH01/GTAA dal 2019*, VRP dal 2021, XS dal 2024; *GTAA troncato all'era book).
|
||||||
|
⚠️ **ANCHOR-LUCK del book (2026-07-02):** l'HOLD 2.46 è calcolato con TUTTI gli sleeve ancorati
|
||||||
|
alla loro ancora canonica, che per TP01/XS01/SKH01 è al top della rispettiva banda (eredità di
|
||||||
|
fortuna ~+0.10/+0.17/+0.5 HOLD). **Stima de-luckata onesta: HOLD ~1.9-2.1, FULL ~2.0-2.2, DD ~6%
|
||||||
|
invariato** — il book resta positivo e diversificato a ogni ancora testata, ma 2.46 è un massimo
|
||||||
|
di configurazioni d'ancora, non la stima centrale. Diario `2026-07-02-anchor-audit-xs01-skh01.md`.
|
||||||
|
- **SKH01-V2-DD "Skyhook" — DIVERSIFICATORE quasi-ortogonale (research)** — `src/strategies/skyhook.SKH01_V2_DD`,
|
||||||
|
sleeve `src/portfolio/sleeves._skyhook_returns`. Sistema dual-TF (segnale 690m / exec 230m) regime
|
||||||
|
(BuzVola/BuzVolume tipo-Chande) AND pattern (Donchian breakout), NON trend-follower, L/S. Vincitrice
|
||||||
|
di 2 onde multi-agente (la 2ª = DD-reduction): exit a **percentuale fissa ASIMMETRICA** (long sl4%/tp10%,
|
||||||
|
short sl2%/tp8% più stretto) → standalone **maxDD BTC 21% / ETH 27% (<30%)**, minFull +0.99, minHold
|
||||||
|
+1.26, causale (0/400), fee-surviving 0.40%RT. Marginal vs TP01 **ADDS** (corr 0.09, has_insample_edge,
|
||||||
|
robust_oos multicut 7/7, is_hedge=False); blend 0.75·TP01+0.25·SKH **hold-out 0.31→1.17**. Verificato
|
||||||
|
leak-free + 2 scettici. **CAVEAT:** equity daily-step (Sharpe lens), ETH DD margine sottile, book 230m
|
||||||
|
(costi ribilanciamento da verificare a deploy) → research win, forward-monitor. Diario `2026-06-23-skyhook.md`.
|
||||||
|
⚠️ **GRID TIMING-LUCK (2026-07-02, più forte di TP01):** i numeri headline sono sull'offset 0 della
|
||||||
|
griglia 230m/690m, al **93-98° pctl dei 23 offset a priori** — minHold +1.26, blend 1.17 e book
|
||||||
|
HOLD 2.44 sono il MASSIMO dei 23 (mediane: minHold +0.39, blend 0.72, book 1.96); spike, non
|
||||||
|
plateau (±30m crolla); P(spike)≈0.70. **Il gate DD<30% (criterio di selezione di V2-DD) fallisce
|
||||||
|
in 15/23 offset** (mediana ETH 29.2%). Regge de-luckato: uplift blend positivo a TUTTE le 23 fasi
|
||||||
|
(min +0.18, med +0.42) + corr 0.05-0.11 → ADDS sopravvive ridimensionato. **LIVE (SKH=25% del book
|
||||||
|
Deribit):** path reale cron orario + exit software → book 50/50 FULL 1.46→1.19 / HOLD 1.64→1.15 /
|
||||||
|
DD 18→25%; nei crash gap-through-stop reale (sl2% modellato → −11/−23% realizzato). Pesi/book
|
||||||
|
INVARIATI (ogni cambio passa weights_tilt_null); follow-up: cadenza 230m, peso live da rivedere.
|
||||||
|
Script `scripts/research/r0702_anchor_skh01.py`; diario `2026-07-02-anchor-audit-xs01-skh01.md`.
|
||||||
- **VRP01 Options Short-Vol — DIVERSIFICATORE da FinanceOld/OptionsAgent** — `src/portfolio/sleeves._vrp_combo_returns`.
|
- **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
|
||||||
@@ -59,6 +115,22 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis
|
|||||||
crash incluso). Scorrelato a TP01 (~+0.01-0.07). **CAVEAT:** premio MODELLATO su DVOL ATM (skew non
|
crash incluso). Scorrelato a TP01 (~+0.01-0.07). **CAVEAT:** premio MODELLATO su DVOL ATM (skew non
|
||||||
esplicito), book a 1d, f di stress reale non catturato → LEAD robusto, non deploy pieno. Ricerca
|
esplicito), book a 1d, f di stress reale non catturato → LEAD robusto, non deploy pieno. Ricerca
|
||||||
`scripts/research/options_vrp_v2.py` (vs baseline `options_vrp_lab.py`). Test `tests/test_vrp_sleeve.py`.
|
`scripts/research/options_vrp_v2.py` (vs baseline `options_vrp_lab.py`). Test `tests/test_vrp_sleeve.py`.
|
||||||
|
⚠️ **ANCHOR-AUDIT CHIUSO + ondata "migliora e proteggi" (2026-07-03, 7 filoni + 2 lenti + scettico):
|
||||||
|
VRP01 NON è migliorabile e la protezione DD si compra SOLO con la size.** (a) **Anchor-luck (ciclo
|
||||||
|
settimanale, 7 fasi): PRIMO sleeve SENZA firma di luck** — la fase canonica è la PEGGIORE delle 7 su
|
||||||
|
FULL (1.09 = 7° pctl) e su DD (11.8% = 93° pctl), mediana su HOLD (0.59); spike bootstrap NEGATIVO →
|
||||||
|
i numeri di ammissione FULL 1.10/HOLD 0.60/DD 12% sono CONSERVATIVI, non gonfiati. Da ora si citano
|
||||||
|
con banda: ShFULL [1.09,1.83], ShHOLD [−0.03,1.11], DD [5.7,11.8%]; edge OOS f-dipendente (f=0.8 →
|
||||||
|
HOLD~0). **Con questo l'audit anchor è completo su 4/4 sleeve ancorati.** (b) Griglia 288 strutture:
|
||||||
|
nessuna batte VRP01 (DSR 0.000; metà griglia = 3ª occorrenza "0-perdite/Sharpe implausibile" dopo
|
||||||
|
CC01/ALB-A → gate `implausible_sharpe` alzato di priorità). (c) 4 overlay DD (exit-spike/SL-MTM/
|
||||||
|
ala-coda/cooldown): 4/4 REFUTED dal null de-levering — la protezione crash vive già nel gate
|
||||||
|
d'ingresso IV-rank. (d) Gate nuovi: 4° fallimento su 4 (l'alpha è il binario IV-rank>0.30). (e)
|
||||||
|
Sizing: 12% deploy ≈ 0.27 Kelly onesto (anti-rovina); ⚠️ NON confondere col 12% di PESO del book
|
||||||
|
(~0.014 Kelly, fattore 19x). (f) Gate term-structure VIX/VXV su SPX (ΔSh +0.90, DSR 0.992) =
|
||||||
|
**confound di modello al 100%** (la var del gate coincide con l'errore BS-flat vs term-structure) →
|
||||||
|
nuova regola: riprezzare term-structure-consistent prima di credere a un gate vol su strutture
|
||||||
|
BS-flat. Book/pesi INVARIATI. Diario `2026-07-03-vrp-improve-dd.md`; script `scripts/research/r0703_vrpimp_*.py` (7 file).
|
||||||
Diario `2026-06-20-financeold-analysis-vrp-v2.md`.
|
Diario `2026-06-20-financeold-analysis-vrp-v2.md`.
|
||||||
- **Universo Hyperliquid: ESPANDERLO NON aiuta XS01** (provato): 52-asset / top-liquidità dinamico /
|
- **Universo Hyperliquid: ESPANDERLO NON aiuta XS01** (provato): 52-asset / top-liquidità dinamico /
|
||||||
trend-multi-asset → tutti peggiori (small-cap/memecoin diluiscono il momentum relativo; il trend
|
trend-multi-asset → tutti peggiori (small-cap/memecoin diluiscono il momentum relativo; il trend
|
||||||
@@ -78,6 +150,163 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis
|
|||||||
regime-luck), calendar/seasonality (buy&hold travestito), volume/vol e momentum-reversal (negativi).
|
regime-luck), calendar/seasonality (buy&hold travestito), volume/vol e momentum-reversal (negativi).
|
||||||
- **MORTO/confermato artefatto:** mean-reversion / fade (negativo anche a fee zero — la vecchia
|
- **MORTO/confermato artefatto:** mean-reversion / fade (negativo anche a fee zero — la vecchia
|
||||||
libreria +201%/+1238% era contaminazione); trend 5m/15m (fee).
|
libreria +201%/+1238% era contaminazione); trend 5m/15m (fee).
|
||||||
|
- **GAMMA SCALPING (long-vol) "scalping BTC/ETH con copertura in opzioni" — SCARTATO (2026-06-26)** —
|
||||||
|
`scripts/research/options_gamma_scalp.py`, test `tests/test_gamma_scalp.py`. È lo **specchio
|
||||||
|
esatto del VRP01** (long straddle ATM + delta-hedge: incassa **RV−IV**, dove VRP01 incassa IV−RV).
|
||||||
|
Perde **ogni anno, ogni variante, ogni frequenza** (Sharpe −3 a −6; nudo/cheap-gated/rich-skip;
|
||||||
|
rehedge 1d e 1h). Diagnostica strutturale: a 1d IV≈o>RV (BTC +4.9pp) → paghi il VRP; a 1h RV>IV
|
||||||
|
gross ma (a) gonfiata da microstruttura, (b) il rehedge orario paga **24× la fee di hedge** →
|
||||||
|
variante *peggiore* (−6). Marginale vs TP01 = **DILUTES**, non è nemmeno hedge (perde sia TP01-up
|
||||||
|
sia TP01-down). Muro eseguibilità: opzione BTC min $5.968 ≫ $600. Schiacciato tra due muri:
|
||||||
|
rehedge lento = premio, veloce = fee → **nessuna frequenza vince.** Regola gemella del VRP:
|
||||||
|
*niente long-vol scalp da modello in deploy*. Il VRP01 (lato short, gated) resta l'unico edge
|
||||||
|
opzioni — funziona perché sta sul lato *giusto* dello stesso premio. Diario `2026-06-26-gamma-scalp-options.md`.
|
||||||
|
- **CASH-AND-CARRY (basis trade) "CC01" — premio REALE, Sharpe ARTEFATTO, NON deployabile (2026-06-26)** —
|
||||||
|
`scripts/research/cash_carry_hl.py`, test `tests/test_cash_carry.py`. Diverso da FC01 (funding
|
||||||
|
cross-sectional, già scartato): qui delta-neutral long-spot/short-perp sullo *stesso* asset →
|
||||||
|
ritorno ≈ **+funding** (zero esposizione prezzo). Il premio di funding è **reale** (~+8-14%/anno
|
||||||
|
aggregato, positivo ogni anno in-sample, ortogonale a TP01 corr ~0.05). MA lo **Sharpe modellato
|
||||||
|
11-13 (DD 0.3%) è un ARTEFATTO**: il modello cattura solo il cashflow liscio del funding e i
|
||||||
|
rischi di coda sono **strutturalmente fuori dal dataset** — (1) storico funding dal 2023-05 →
|
||||||
|
**manca il 2022** (deleveraging, funding −, basis blow-out); (2) **procyclico** (carry +23% toro
|
||||||
|
2024 → +1.7% bear 2026, si comprime quando servirebbe); (3) liquidazione short/slippage non
|
||||||
|
modellati. Il mark-to-market della base (`premium` col → r=funding−Δpremium) sgonfia lo Sharpe
|
||||||
|
solo 13→11 → il basis-from-data NON è il rischio vero. Sharpe reale di un basis-trade ~1-3 con
|
||||||
|
code brusche. **NON eseguibile a $600** (spot+perp = 4-38 gambe, funding HL non Deribit) → STAT-MODE.
|
||||||
|
**LEAD da rivedere a scala (~20k+ e venue con funding eseguibile), non uno sleeve.** Sottoprodotto:
|
||||||
|
CC01 passa OGNI gate del marginal scorer → **punto cieco** (manca un gate "Sharpe implausibile →
|
||||||
|
rischio nascosto"; prossima indurita raccomandata). Diario `2026-06-26-cash-carry-hl.md`.
|
||||||
|
- **TP01 × DVOL vol-targeting — NON migliora (2026-06-26)** — `scripts/research/tp01_dvol_overlay.py`,
|
||||||
|
test `tests/test_tp01_dvol_overlay.py`. Angolo ESEGUIBILE (tocca il book live, non STAT-MODE):
|
||||||
|
usare il DVOL (vol implicita forward-looking) come denominatore del vol-target di TP01 invece
|
||||||
|
della vol realizzata. Su finestra comune 2021-2026: le varianti DVOL abbassano il DD (12.3%→9.2%)
|
||||||
|
ma anche Sharpe FULL (0.75→0.70) e CAGR (8%→6%). **Controllo decisivo:** il realized a target_vol
|
||||||
|
RIDOTTO (15%) eguaglia quel DD (9.4%) a Sharpe **più alto** (0.75) → il taglio di DD del DVOL è
|
||||||
|
solo **de-levering**, replicabile meglio con un semplice target_vol più basso. L'unico residuo
|
||||||
|
(hold-out +0.06) è single-window (storia DVOL <5 anni) → sotto la soglia multi-cut. Il gate
|
||||||
|
DVOL-spike de-risk è **ridondante col trend** (TP01 già flat nei crash, Δ 0.00). **Lezione: per meno
|
||||||
|
DD sul live la leva è `target_vol`, non un overlay DVOL** (20% resta canonico). Diario `2026-06-26-tp01-dvol-overlay.md`.
|
||||||
|
- **Filoni 2026-06-29 (1ª ondata) — tutti scartati/forward.** (A) **DVOL-DIREZIONALE** standalone BTC/ETH
|
||||||
|
(buy-the-fear / IV-RV come segnale di livello): l'unico edge è un **HEDGE** (is_hedge=True, paga solo
|
||||||
|
quando TP01 è debole), non alpha → earns_slot=False, forward-monitor come DD-dampener (diario
|
||||||
|
`2026-06-29-dvol-directional.md`). (B) **INTRADAY ERM** (efficiency-ratio regime momentum sub-daily):
|
||||||
|
**falso positivo da selezione-sull'hold-out** → SCARTATO (vedi gate SELECTION-ON-HOLDOUT;
|
||||||
|
`2026-06-29-intraday-regime.md`). (C) **XSEC-V2 NON-MOMENTUM** su HL (reversal/idio-reversal/low-vol/BAB):
|
||||||
|
solo LOWVOL 19-major regge standalone (FULL/HOLD 1.07) ma deflated-Sharpe 0.13 + storia 2.5a → **DEBOLE/
|
||||||
|
forward STAT-MODE** (`2026-06-29-xsec-v2-nonmom.md`). (D) **MACRO regime-gate** (equity/credito/oro/tassi →
|
||||||
|
de-risk crypto): **RIDONDANTE col trend** (corr→TP01 0.989; il gate lavora solo nel 2-3% dei giorni, TP01
|
||||||
|
già flat nei crash) → SCARTATO (`2026-06-29-macro-regime-gate.md`).
|
||||||
|
- **Sweep strategie a 5 thread (2026-06-29) — 0 nuovi sleeve, 1 LEAD che rompe 2 muri su 3.** Ricerca
|
||||||
|
parallela onesta su aree inesplorate (harness `altlib`+`xsec_v2_nonmom`, tutti i gate incl. il nuovo
|
||||||
|
`study_family_honest`): (1) low-risk cross-sectional, (2) momentum-structure vs XS01, (3) meta-allocazione
|
||||||
|
dinamica, (4) segnali ortogonali ETH/BTC, (5) 1-gamba a segnale. Esito: soffitto ~1.3 riconfermato; ogni
|
||||||
|
candidato ucciso dal gate giusto (deflated-Sharpe, is_hedge, selection-on-holdout, sostituzione-XS01,
|
||||||
|
multi-cut). Niente batte/diversifica XS01 (varianti = REDUNDANT); meta-allocazione < pesi fissi (i 4
|
||||||
|
sleeve già quasi-risk-parity); 1-gamba a segnale = TP01 travestito (trend) o hedge a DSR<0.95.
|
||||||
|
**LEAD forward-monitor:** **STATARB-RESID** (relative-MOMENTUM del residuo ETH−β·BTC, β OLS rolling, 2 gambe,
|
||||||
|
cella vincente **sgn=+1**: le dislocazioni ETH-vs-BTC CONTINUANO a 1d, la MR pura sgn=−1 perde) — primo
|
||||||
|
stream **insieme ortogonale (corr→book 0.027, β-mkt 0.013) ED eseguibile a $600** (haircut ~0, NON
|
||||||
|
STAT-MODE come XS01/opzioni): marginal ADDS, robust_oos, fee-survive 0.30%/gamba; resta sotto soglia solo
|
||||||
|
sull'**edge** (Sharpe 0.84, DSR 0.929 same-sign <0.95). **CABLATO in forward-monitor PAPER:**
|
||||||
|
`scripts/live/paper_statarb.py` (W=45/sgn=+1 congelati, doppio libro MODELED/REAL-$600), nel cron
|
||||||
|
giornaliero accanto a `paper_prevday`; test `tests/test_paper_statarb.py`. Se la finestra forward conferma
|
||||||
|
l'edge è *deployabile* (2 gambe BTC/ETH perp). Altri LEAD: IVOL (idio-vol XS, STAT-MODE), DVOLSPREAD
|
||||||
|
(storia DVOL corta). Diario `2026-06-29-strategy-search-5threads.md`. Script
|
||||||
|
`scripts/research/{xsec_v3_lowrisk,xsec_v3_momstruct,meta_allocation,orthogonal_signals,signal_inout_1leg}.py`.
|
||||||
|
- **Ondata 2026-07-01 (6 filoni multi-agente + scettico) — 0 edge nuovi dai filoni, 1 gate nuovo,
|
||||||
|
e 1 sleeve promosso DALL'ARCHIVIO (GTAA01, sotto nel bullet portafoglio).** Filoni su angoli non coperti dalle ondate precedenti: (1) **funding time-series**
|
||||||
|
BTC/ETH (posizionamento) = SCARTATO — FOLLOW è trend-beta ritardato, FADE shorta il toro, il gate è
|
||||||
|
TP01 travestito (DSR 0.215); **il filone funding è chiuso su 3 lati** (FC01 carry, price-clock, TS).
|
||||||
|
(2) **breadth/internals alt** (51 HL) = SCARTATO ma unico NON-ridondante col trend (corr→TP01 0.40);
|
||||||
|
muore su jackknife (uplift su 1 mese) + DSR 0.433 con ~8 mesi IS → **rivisitabile tra 1-2 anni di
|
||||||
|
storia HL nativa**. (3) **residual momentum XS** (β-hedged, 19 major) = REDUNDANT — cross-section la
|
||||||
|
residualizzazione è un no-op (lo z-score di XS01 rimuove già il mercato); l'edge resta solo nella
|
||||||
|
coppia ETH/BTC (STATARB-RESID). (4) **ri-ottimizzazione pesi + guardia-DD**: il candidato EW-STR
|
||||||
|
(TP30/XS25/VRP15/SKH30, HOLD 2.21→2.35) **refutato dallo scettico come selezione-sull'hold-out di 2°
|
||||||
|
ORDINE** — SKH01/XS01 furono ammessi/affinati *perché* forti su quell'hold-out; pre-2025 ΔSh −0.05,
|
||||||
|
finestre disgiunte −0.12/+0.06/+0.14, percentile 94-100° fra 500 tilt casuali ≈ firma best-of-15.
|
||||||
|
Guardia-DD 5%/0.5: inerte OOS (la diversificazione fa già il lavoro; solo circuit-breaker d'emergenza).
|
||||||
|
(5) **affinamento VRP01** = NON MIGLIORA (l'alpha è tutto nel gate binario IV-rank; gate TP01 =
|
||||||
|
trappola in-sample; **3° fallimento → filone "VRP dentro il modello" esaurito** fino a f di stress
|
||||||
|
reale). (6) **stagionalità cross-sectional HL** = morta allo step statistico (null permutato).
|
||||||
|
**GATE nuovo codificato: `weights_tilt_null`** in `src/portfolio/portfolio.py` (+ `combine_outer`
|
||||||
|
riusabile): ogni proposta di CAMBIO PESI si giudica vs il null dei tilt casuali cap-respecting —
|
||||||
|
gate_pass solo se delta_insample≥0 E percentile < firma best-of-k (necessario, non sufficiente);
|
||||||
|
test `tests/test_weights_tilt_null.py`. ⚠️ Lezione tecnica: `DatetimeIndex.view("int64")` su indici
|
||||||
|
tz-aware non-ns (pandas 2.x) → scala sbagliata → `merge_asof` broadcasta = **look-ahead che
|
||||||
|
`causality_ok` non vede**; usare epoca esplicita in ms (altlib verificato pulito). Diario di sintesi
|
||||||
|
`2026-07-01-strategy-wave-6threads.md` + 6 diari di filone; script `scripts/research/r0701_*.py`.
|
||||||
|
- **Ondata 2026-07-02 (TIMING + CRT, 8 filoni multi-agente + scettico) — 0 nuovi sleeve, 1 finding
|
||||||
|
strutturale (anchor timing-luck di TP01, vedi ⚠️ nel bullet TP01).** Goal: "strategie con timing
|
||||||
|
differenti". (1) **Event-clock bars** (volume/vol/range da 5m, TSMOM/Donchian/EWMA in tempo-informazione):
|
||||||
|
batte il wall-clock a pari segnale/frequenza solo in 4/45 coppie; cella best IS 1.45 → HOLD −0.46,
|
||||||
|
NEUTRAL (corr 0.74 = trend travestito) → SCARTATO: il clock non è dove vive l'edge. (2) **Calendario
|
||||||
|
scadenze Deribit** (expiry weekly/monthly/quarterly ven 08:00 UTC): 0/24 celle a Bonferroni; il drift
|
||||||
|
post-expiry monthly fallisce placebo-weekday e permutation e si INVERTE sul quarterly (dove l'OI massimo
|
||||||
|
dovrebbe amplificarlo); unico pattern robusto = gio→ven negativo, ma è day-of-week (SEA morta) a Sharpe
|
||||||
|
netto ~0 → SCARTATO. (3) **Anchor timing-luck TP01 + tranching**: finding confermato (dettagli nel
|
||||||
|
bullet TP01); tranching K=2/4 = sola riduzione della varianza della STIMA (ΔSharpe n.s., ΔDD ~0.5pt),
|
||||||
|
NO deploy a $600 (il min-order lo degenera in K=1; serve feed intraday fuori path certificato) —
|
||||||
|
rivalutare a ≥5-10k. **Audit d'ancora ESEGUITO su XS01 e SKH01 (stesso giorno): il finding si
|
||||||
|
replica su 3/3 sleeve ancorati** — vedi ⚠️ nei rispettivi bullet e la stima de-luckata del book
|
||||||
|
nel bullet portafoglio; diario `2026-07-02-anchor-audit-xs01-skh01.md`.
|
||||||
|
(4) **Clock lenti (2-7g) + bande isteresi**: fee drag di TP01 = ~0.4%/anno = tetto di ogni risparmio;
|
||||||
|
il lag costa più del risparmio (HOLD ensemble 0.34→0.11 da N=2 a 7); a $600 **il min-order $5 è GIÀ la
|
||||||
|
banda ottimale** (ordini −74% a costo ~0) → nessun cambio al book. (5) **Velocità trend
|
||||||
|
regime-condizionata** (pesi tra orizzonti 30/90/180g vs percentile vol RV/DVOL): pctl 0.71 vs null
|
||||||
|
pesi-statici-casuali = tilt-30d statico travestito (trappola EW-STR); pesi canonici 1/3 confermati →
|
||||||
|
SCARTATO. (6-8) **CRT "Candle Range Theory"** (sweep-and-reclaim 3 candele, mai coperto da MRV/MIC):
|
||||||
|
base 864 trial DSR 0.000 + anchor-flip + short "smart-money" negativo perfino in-sample; multi-TF
|
||||||
|
(4h→15m, 1h→5m, ~10k trade) expectancy negativa ovunque anche a fee zero, e **il ritest è informazione
|
||||||
|
negativa** (pattern con-ritest −40bps vs senza +52bps: aspettarlo seleziona i peggiori); contesto
|
||||||
|
(FVG/equal-highs/sessioni, 22 trial) non salva il fade, cella "Asia" = artefatto anchor-flip →
|
||||||
|
SCARTATO 3/3. Sottoprodotto: sugli stessi livelli prior-day **FOLLOW > FADE ogni anno 2019-26**
|
||||||
|
(conferma indipendente del lead prevday in forward-monitor). Lezione: il timing-luck d'ancora è
|
||||||
|
multiple-testing che il deflated-Sharpe NON conta (candidato gate futuro `anchor_luck_band`).
|
||||||
|
Diario `2026-07-02-timing-crt-wave.md`; script `scripts/research/r0702_*.py` (9 file).
|
||||||
|
- **Ondata 2026-07-02-bis ("video claims": Elliott 3 filoni + Albimarini 2 + capital scaling) — 0 nuovi
|
||||||
|
sleeve, 0 forward-monitor, 1 azione config pendente sul deposito.** Meccanizzazione onesta di claim da
|
||||||
|
video didattici: (1) **Elliott range-cycle** (onda1 compressa→onda3 ampia): rumore, 0/24 celle a
|
||||||
|
Bonferroni, nessuna cella weekly regge a tutte le 7 ancore → SCARTATO. (2) **Confluenza Fibonacci**:
|
||||||
|
vs null ingenui sembra buona (pctl 0.82-1.00), vs null **location-matched** (Fib±jitter: "0.618 vs
|
||||||
|
0.58?") crolla a 0.39-0.68 = l'apparente edge è la POSIZIONE dei livelli, non i numeri; confluenza
|
||||||
|
FAIL 4/4 → SCARTATO (il null location-matched è IL test per ogni claim su livelli "speciali").
|
||||||
|
(3) **Tecnica del canale Elliott**: Donchian travestito — non batte il Donchian a pari geometria
|
||||||
|
(corr 0.43-0.53), DSR 0.685, cella in-sample collassa in hold-out (1.40→−0.87), target 1.618 = caso
|
||||||
|
(5/6 celle), anchor-luck di nuovo (4h banda hold [0.35,1.54], 00:00 la migliore) → SCARTATO.
|
||||||
|
(4) **Albimarini double-diagonal** (short T + long T+1, deep OTM, via motore DVOL di VRP01): il
|
||||||
|
condor stessa-scadenza la batte a ogni f (la long T+1 = assicurazione di coda +12/33bps nel tail,
|
||||||
|
~1bps costo medio, non dominanza); senza gate IV-rank TUTTE le strutture perdono (3ª conferma:
|
||||||
|
l'alpha del VRP è il gate); su Deribit fee-negativa a QUALSIASI size (fee 8 gambe = 194-221% del
|
||||||
|
theta); celle deep-OTM 0-perdite/142 trade = 2° caso "Sharpe implausibile" dopo CC01 → gate
|
||||||
|
`implausible_sharpe` raccomandato con più forza. VRP01 resta superiore su tutta la banda skew →
|
||||||
|
nessun LEAD. (5) **Audit claims** (28 trade, 82% win, PF 5.16, "420%/anno"): consistente con ZERO
|
||||||
|
skill (P=20-45%; il 78.6% delle finestre 6-mesi 1996-2026 lo produce); replay con code reali =
|
||||||
|
rovina 1998/2002/2020 col sizing dichiarato; la diagonale lascia passare il 12-40% della perdita
|
||||||
|
naked. (6) **Capital scaling 600→2-5k** (`r0702_capital_scaling.py`): l'unico vincolo binding è
|
||||||
|
`max_notional_per_asset_usd=300` (a 5k il book live girerebbe al 49% del target) → **al deposito
|
||||||
|
alzare il cap a equity/2** ($1000/$1750/$2500 a 2k/3.5k/5k); min_order $5 da LASCIARE; tranching
|
||||||
|
K=2 non cablare (blocco feed intraday); opzioni ETH eseguibili da ~2.6k ma la regola no-short-vol-
|
||||||
|
da-modello non decade col capitale; XS01 ~20k confermata, CC01 fuori per struttura. Aspettativa
|
||||||
|
onesta col CAGR de-luckato (10-15%): 2k ≈ €0.6-0.8/g, 5k ≈ €1.4-2/g (€50/g resta ≈130k).
|
||||||
|
⚠️ Lezione pandas: `resample("7D", origin=...)` IGNORA origin (pandas 2.x, solo RuntimeWarning) →
|
||||||
|
bande d'ancora weekly finte; usare `"168h"`. Diario `2026-07-02-elliott-albimarini-capital.md`;
|
||||||
|
script `scripts/research/r0702_{ell_*,alb_*,capital_scaling}.py` (6 file).
|
||||||
|
- **Video-claim "CRT top-down multi-TF, 74% win rate" — SCARTATO (2026-07-07): il 74% è un KNOB, non
|
||||||
|
un edge.** `scripts/research/r0707_crt_topdown.py`. Metodo ICT/SMC top-down (H1 setup CRT → M15
|
||||||
|
struttura/FVG → M5 conferma → M1 entry) con uscita parziale-70/80%-a-1.5R + break-even + runner verso
|
||||||
|
prev-daily high/low. Il setup H1 CRT è **già triplo-refutato** (onda 2026-07-02: base DSR 0.000, MTF
|
||||||
|
expectancy neg. ovunque "il ritest è informazione negativa", contesto/FVG peggiora, fade<follow) →
|
||||||
|
qui testato l'UNICO angolo nuovo: la **gestione d'uscita** e il **claim 74%**. Su BTC/ETH certificati
|
||||||
|
(H1→5m/15m; M1 non nel feed): **WR reale 30–37%** a RR 1.5–2 (SOTTO il null gambler's-ruin 40% =
|
||||||
|
P(+1.5R prima di −1R)=1/(1+1.5) → il ritest tocca il target MENO di una moneta), **expectancy R
|
||||||
|
negativa a ogni schema/fee/finestra/asset** (−1.2…−3.3R netto). **Il 74% si fabbrica avvicinando il
|
||||||
|
target**: sweep rr1 0.5→2.0 mostra WR salire (51→32%) con expectancy R INVARIANTE (WR alto = target
|
||||||
|
vicino, non direzione). DSR 0.000 (48 trial); a **fee 0** expR −0.10/Sh −0.63 → **non è morte-per-fee,
|
||||||
|
l'edge lordo non c'è** (residuo = beta di trend dei time-exit); parziale+BE+runner = 3-4 ordini/trade,
|
||||||
|
alcuni sub-min-order a $600. **Regola candidata:** il win-rate di uno schema parziale+BE non è merito
|
||||||
|
(≈1/(1+rr1)); convertire ogni claim "WR X%" in **expectancy R netto fee** prima di crederci (helper
|
||||||
|
`winrate_is_a_knob()`). Diario `2026-07-07-crt-topdown-74winrate.md`. Book/pesi INVARIATI.
|
||||||
- **Soffitto strutturale BTC/ETH-direzionale ~1.3** superato SOLO espandendo a un meccanismo diverso:
|
- **Soffitto strutturale BTC/ETH-direzionale ~1.3** superato SOLO espandendo a un meccanismo diverso:
|
||||||
cross-sectional su universo Hyperliquid certificato (XS01) → portafoglio Sharpe ~1.55.
|
cross-sectional su universo Hyperliquid certificato (XS01) → portafoglio Sharpe ~1.55.
|
||||||
- **Sweep "strategie alternative" (2026-06-20) — 104 ipotesi / 153 agenti / NIENTE di nuovo regge.**
|
- **Sweep "strategie alternative" (2026-06-20) — 104 ipotesi / 153 agenti / NIENTE di nuovo regge.**
|
||||||
@@ -100,6 +329,41 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis
|
|||||||
(marginal==ADDS)`. **Regola: una nuova strategia direzionale si giudica su `earns_slot`, non sullo
|
(marginal==ADDS)`. **Regola: una nuova strategia direzionale si giudica su `earns_slot`, non sullo
|
||||||
Sharpe assoluto** (gli overlay-su-TSMOM ereditano lo Sharpe di trend e prendono PASS fasulli —
|
Sharpe assoluto** (gli overlay-su-TSMOM ereditano lo Sharpe di trend e prendono PASS fasulli —
|
||||||
es. CMB04 PASS assoluto → NEUTRAL marginale). Demo `marginal_demo.py`, test `tests/test_marginal_scorer.py`.
|
es. CMB04 PASS assoluto → NEUTRAL marginale). Demo `marginal_demo.py`, test `tests/test_marginal_scorer.py`.
|
||||||
|
⚠️ **INDURITO 2026-06-21 (onda ortho):** la versione fisso-HOLDOUT + jackknife-mese era
|
||||||
|
ingannabile — 17/18 book relative-value "ADDS" su una sola finestra 2025 (ETH-bleed dove TP01 è
|
||||||
|
debole). Tre gate nuovi in `marginal_vs_tp01`: **(1) persistenza multi-cut** (uplift positivo a più
|
||||||
|
date di taglio, non solo 2025); **(2) edge in-sample** (`has_insample_edge`: lo Sharpe standalone
|
||||||
|
PRE-holdout dev'essere ≥0.5 — un low-corr a Sharpe ~0.3 "aggiunge" solo matematica di
|
||||||
|
diversificazione, riportata via `null_pctl_*` vs un asset-rumore a corr-zero); **(3) hedge vs
|
||||||
|
alpha** (`is_hedge`: un low-corr che paga SOLO quando TP01 è debole — `corr(Sharpe-TP01, uplift
|
||||||
|
annuo)` molto negativa — è un hedge, non alpha). Verdetti nuovi: HEDGE, NOISE. Sull'onda ortho lo
|
||||||
|
scorer indurito collassa 17/18 → **1** (`dvol_spread`, unico con edge in-sample reale; comunque
|
||||||
|
forward-monitor per multiple-testing/storia DVOL corta). Lezione: un nuovo sleeve si giudica su
|
||||||
|
edge-in-sample + persistenza multi-cut + non-hedge, non sull'uplift di una finestra fortunata.
|
||||||
|
- **HARNESS REALISM (codificato 2026-06-21, onda intraday)** — due gate nuovi in `altlib.py`,
|
||||||
|
test `tests/test_harness_realism.py`:
|
||||||
|
- **`day_boundary_robust(target_fn, tf)`** — un effetto ora/sessione/giorno il cui uplift
|
||||||
|
marginale **si inverte** spostando il confine del giorno UTC di poche ore è un **artefatto di
|
||||||
|
etichettatura calendario** (ha ucciso `open_drive`: +0.23 a 00:00 → −0.33 a +8h → ARTIFACT-RISK).
|
||||||
|
Un segnale di prezzo è INVARIANT (spread 0); un effetto calendario vero è ROBUST (resta positivo;
|
||||||
|
es. `prevday_range_breakout`). **Regola: ogni segnale calendar/session/hour passa questo test
|
||||||
|
prima di crederci.**
|
||||||
|
- **`eval_weights_smallcap(df, target, capital=600, min_order=5)`** — a ~$600 un ribilanciamento
|
||||||
|
di nozionale < min_order **non si esegue**; la fee proporzionale che `eval_weights` applica a
|
||||||
|
migliaia di micro-trade sub-dollaro (tipici di un overlay vol-target) è **finzione**. Salta i
|
||||||
|
sub-min_order e riporta lo **Sharpe haircut** reale vs modellato. **Vale per OGNI sleeve a questo
|
||||||
|
capitale, TP01 incluso** — lo Sharpe netto onesto a $600 è quello small-cap, non quello modellato.
|
||||||
|
- **SELECTION-ON-HOLDOUT gate (codificato 2026-06-29, filone B intraday ERM)** — terzo gate in
|
||||||
|
`altlib.py`, test `tests/test_harness_realism.py`. Il lead ERM faceva `earns_slot=True` MA lo script
|
||||||
|
di scoperta sceglieva la cella per **`min_hold` massimo** su 60+ celle = **selezione-sull'hold-out**:
|
||||||
|
scegliendola in-sample-only ne esce un'altra (trend-beta corr→TP01 0.53, NEUTRAL) e il deflated-Sharpe
|
||||||
|
crolla (DSR 0.0-0.24 su 122 trial). `study_marginal` da solo non lo vede (giudica UNO stream, non *come*
|
||||||
|
è scelto). Tre funzioni: **`deflated_sharpe()`** (Bailey & Lopez de Prado, PASS ≥0.95), **`select_cell_insample()`**
|
||||||
|
(cella scelta col solo Sharpe pre-HOLDOUT), e il gate combinato **`study_family_honest(name, factory, grid, tfs)`**
|
||||||
|
→ `earns_slot_honest = earns_slot[cella in-sample] AND deflated-Sharpe≥0.95`. **Regola: una strategia
|
||||||
|
direzionale grid-searched si giudica con `study_family_honest`, non chiamando `study_marginal` sulla
|
||||||
|
cella a max hold-out.** Chiude il punto cieco gemello di CC01 ("Sharpe implausibile"). Diario
|
||||||
|
`2026-06-29-intraday-regime.md` (analisi `scripts/research/intraday_regime_analysis.py`).
|
||||||
- **Onestà sul target €50/giorno:** NON raggiungibile su 2000 in 1-2 anni (servono ~130k di
|
- **Onestà sul target €50/giorno:** NON raggiungibile su 2000 in 1-2 anni (servono ~130k di
|
||||||
capitale o un DD da rovina). La leva non è la scorciatoia; la via è target-vol + capitale +
|
capitale o un DD da rovina). La leva non è la scorciatoia; la via è target-vol + capitale +
|
||||||
tempo. La strategia che *guadagna* esiste, ma a ~+€1.5/giorno su 2000.
|
tempo. La strategia che *guadagna* esiste, ma a ~+€1.5/giorno su 2000.
|
||||||
@@ -128,7 +392,7 @@ src/strategies/indicators.py → indicatori condivisi (ema, atr, keltner, ...)
|
|||||||
src/strategies/trend_portfolio.py → TP01: strategia DIFENSIVA robusta (PORT LF1d, >=12h), causale
|
src/strategies/trend_portfolio.py → TP01: strategia DIFENSIVA robusta (PORT LF1d, >=12h), causale
|
||||||
src/portfolio/ → PORTAFOGLIO DI STRATEGIE estensibile (Sleeve + StrategyPortfolio)
|
src/portfolio/ → PORTAFOGLIO DI STRATEGIE estensibile (Sleeve + StrategyPortfolio)
|
||||||
portfolio.py → combina N sleeve per peso su griglia giornaliera; metriche FULL/hold-out/anno
|
portfolio.py → combina N sleeve per peso su griglia giornaliera; metriche FULL/hold-out/anno
|
||||||
sleeves.py → REGISTRY sleeve attivi: TP01 (55%) + XS01 (25%) + VRP01 (20%). Aggiungere = una riga
|
sleeves.py → REGISTRY sleeve attivi: TP01 33 / XS01 15 / VRP01 12 / SKH01 20 / GTAA01 20. Aggiungere = una riga
|
||||||
src/fractal/ → indicatori frattali (patterns.py, indicators.py, similarity.py)
|
src/fractal/ → indicatori frattali (patterns.py, indicators.py, similarity.py)
|
||||||
src/backtest/engine.py → engine di backtesting riusabile
|
src/backtest/engine.py → engine di backtesting riusabile
|
||||||
src/backtest/harness.py → harness ONESTO (load BTC/ETH, backtest_signals no-leakage, OOS)
|
src/backtest/harness.py → harness ONESTO (load BTC/ETH, backtest_signals no-leakage, OOS)
|
||||||
|
|||||||
+3
-1
@@ -1,7 +1,9 @@
|
|||||||
{
|
{
|
||||||
"_nota": "Config esecuzione LIVE di TP01. execution_enabled=true + --execute -> ordini REALI. ARMATO 2026-06-20.",
|
"_nota": "Config esecuzione LIVE del BOOK DERIBIT (TP01+SKH01 nettati in software). execution_enabled=true + --execute -> ordini REALI. ARMATO 2026-06-23: esecutore scripts/live/book_execute.py via cron ORARIO scripts/cron_book.sh (SKH01 e' a 230m). disaster-SL on-book -30% sulla posizione netta. Tutto flat all'arming -> nessun ordine finche' un segnale non arma.",
|
||||||
|
"_nota_cap": "Cap notional per-asset DINAMICO (frontiera 2026-07-03): con max_notional_per_asset_frac=0.5 il cap = equity/2, cosi' cresce col capitale e un deposito non resta strozzato. A ~$600 equity/2=~$300 -> INERTE (identico al vecchio cap fisso). Il cap dinamico si usa solo con equity reale fidata; su fallback/offline si ripiega su max_notional_per_asset_usd (protezione downside). Per tornare al cap fisso: rimuovere max_notional_per_asset_frac.",
|
||||||
"execution_enabled": true,
|
"execution_enabled": true,
|
||||||
"max_notional_per_asset_usd": 300,
|
"max_notional_per_asset_usd": 300,
|
||||||
|
"max_notional_per_asset_frac": 0.5,
|
||||||
"min_order_usd": 5,
|
"min_order_usd": 5,
|
||||||
"disaster_sl_pct": 0.30
|
"disaster_sl_pct": 0.30
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,3 +13,20 @@ services:
|
|||||||
# token mainnet (sola lettura) per lo "Shadow live": conto/posizioni reali sulla dashboard.
|
# token mainnet (sola lettura) per lo "Shadow live": conto/posizioni reali sulla dashboard.
|
||||||
# Montato a runtime (NON nell'immagine: .env.mainnet e' dockerignored). Solo letture, nessun ordine.
|
# Montato a runtime (NON nell'immagine: .env.mainnet e' dockerignored). Solo letture, nessun ordine.
|
||||||
- ./.env.mainnet:/app/.env.mainnet:ro
|
- ./.env.mainnet:/app/.env.mainnet:ro
|
||||||
|
|
||||||
|
# IB Gateway (PAPER) per la RICERCA DATI Interactive Brokers — replica il setup provato di BuzWay
|
||||||
|
# (scout). IBC fa login automatico headless; nessuna GUI desktop. API READ-ONLY (solo dati storici,
|
||||||
|
# MAI ordini). Bind SOLO su 127.0.0.1 -> non esposto in rete. Credenziali in .env.ibgw (gitignored).
|
||||||
|
# host 4002 -> container 4004 (socat paper), esattamente come nel connect("127.0.0.1", 4002).
|
||||||
|
ib-gateway:
|
||||||
|
image: ghcr.io/gnzsnz/ib-gateway:stable
|
||||||
|
container_name: pythagoras-ibgw
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file: .env.ibgw
|
||||||
|
environment:
|
||||||
|
TRADING_MODE: paper
|
||||||
|
READ_ONLY_API: "yes" # SOLO dati: nessun ordine possibile via API
|
||||||
|
TWOFA_TIMEOUT_ACTION: restart
|
||||||
|
TIME_ZONE: Europe/Rome
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:4002:4004" # gateway paper (socat) raggiungibile solo da localhost dell'host
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# VRP01 + gestione attiva intra-trade — A/B onesto (NEGATIVO)
|
||||||
|
|
||||||
|
**Data:** 2026-06-20
|
||||||
|
**Script:** `scripts/research/options_vrp_managed.py`
|
||||||
|
**Esito:** la gestione attiva del documento credit-spread **distrugge l'edge**. VRP01
|
||||||
|
**hold-to-expiry resta superiore.** → scartata.
|
||||||
|
|
||||||
|
## Cosa testava
|
||||||
|
|
||||||
|
Innesta sul put credit spread di VRP01 le regole intra-trade del doc `strategia-credit-spread-eth`:
|
||||||
|
profit-take 50% del credito, stop-loss 1.5× il credito, **VOL-STOP** (chiudi se DVOL sale ≥10 punti
|
||||||
|
dall'apertura — regola crypto-specifica nuova), **delta-exit** (chiudi se |delta| short put ≥0.30),
|
||||||
|
time-stop 7 DTE. A/B sugli **stessi ingressi gated** (VRP>0 + IV-rank>0.30) e dati certificati;
|
||||||
|
MTM giornaliero dello spread via BS sul path certificato + DVOL reale (causale).
|
||||||
|
BASE = hold-to-expiry (come VRP01) vs MANAGED = stesso trade gestito.
|
||||||
|
|
||||||
|
## Risultato (combo 50/50 BTC+ETH, sleeve-level)
|
||||||
|
|
||||||
|
| variante | Sharpe | DD | ret | HOLD Sh |
|
||||||
|
|----------|--------|------|------|---------|
|
||||||
|
| 14d hold-to-expiry (BASE) | **0.96** | 11.7% | +39% | +1.52 |
|
||||||
|
| 14d + solo vol-stop | 0.12 | 10.1% | +3% | +1.01 |
|
||||||
|
| 14d FULL managed | **−1.29** | 14.8% | −15% | −1.17 |
|
||||||
|
|
||||||
|
Per-asset: la gestione FULL ribalta entrambi (ETH 0.33→−1.15, BTC 1.88→−0.89). Il **delta-exit**
|
||||||
|
domina le uscite (18-25 trade su ~33-45) e taglia i vincenti prima della decadenza theta; persino
|
||||||
|
il **vol-stop da solo** quasi azzera il ritorno (combo Sh 0.12). Win-rate crolla 80-94% → ~40%.
|
||||||
|
|
||||||
|
## Lettura
|
||||||
|
|
||||||
|
Per un venditore di premio short-vol l'edge È la decadenza theta tenuta fino a scadenza: ogni
|
||||||
|
uscita anticipata (delta, vol-stop, PT) **monetizza meno theta e/o realizza la coda** invece di
|
||||||
|
lasciarla riassorbire. Le regole di "difesa" del doc azionario/ETH non trasferiscono al VRP crypto
|
||||||
|
modellato: l'unica gestione che non danneggia è **non gestire** (hold-to-expiry, come VRP01 già fa).
|
||||||
|
|
||||||
|
**Caveat invariato:** premio MODELLATO su DVOL ATM (no skew) + nessun fill di stress reale → tutto
|
||||||
|
ciò resta a livello di LEAD, non deploy. Ma la conclusione relativa (BASE > MANAGED) è robusta
|
||||||
|
perché è un A/B sugli **stessi** trade e dati.
|
||||||
|
|
||||||
|
## Azione
|
||||||
|
|
||||||
|
Nessuna modifica a VRP01 (`sleeves._vrp_combo_returns`, hold-to-expiry). Script conservato come
|
||||||
|
riferimento dell'esperimento scartato.
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
# Sweep strategie cross-sectional su Hyperliquid (xsec) — 43 script / 257 config
|
||||||
|
|
||||||
|
**Data:** 2026-06-20
|
||||||
|
**Harness:** `scripts/research/xsec/xslib.py` (nuovo) + 43 script in `scripts/research/xsec/runs/`
|
||||||
|
**Verifica:** `scripts/research/xsec/verify_survivors.py` (3 scettici, deterministico)
|
||||||
|
**Esito in una riga:** niente di deployabile; il cluster vincente appariscente è **una sola
|
||||||
|
scommessa di regime (short alt-beta)**, ma **2 lead genuini** (XM09 trend-gated x-sec momentum,
|
||||||
|
XR02 reversal vol-gated) sopravvivono a tutti gli scettici → **forward-monitor, non sleeve.**
|
||||||
|
|
||||||
|
## Contesto e motivazione
|
||||||
|
|
||||||
|
Dopo che il sweep BTC/ETH a 104 ipotesi (`2026-06-20-alt-strategies-100agent-sweep.md`) ha
|
||||||
|
esaurito lo spazio direzionale single-asset confermando il soffitto ~1.3, la frontiera indicata era
|
||||||
|
**cross-sectional / multi-asset** sul panel Hyperliquid certificato, dove quel soffitto non vincola
|
||||||
|
e dove c'è spazio DISTINTO da XS01 (x-sec momentum semplice sui 19 major).
|
||||||
|
|
||||||
|
Nuova harness condivisa `xslib.py`: il panel è N asset × ~810 giorni (universo `all` = **49 alt**
|
||||||
|
con ≥700g dopo il fix backfill; `majors` = 19 di XS01). Una strategia = uno **score per-asset
|
||||||
|
causale** (dati ≤ close[i]); l'harness lo classifica cross-section ad ogni ribilanciamento, va long
|
||||||
|
i top-k / short i bottom-k (market-neutral) o long-only, vol-targeta al 20%, addebita fee sul
|
||||||
|
turnover, e — strutturalmente leak-free — il peso deciso a `i` incassa il return di `i+1` (stessa
|
||||||
|
convenzione di `src.portfolio` xs_book / `sleeves._xsec_returns`).
|
||||||
|
|
||||||
|
**Scoring onesto** (`study_xs`): un candidato guadagna `earns_slot=True` SOLO se
|
||||||
|
`full Sharpe>0 AND hold-out 2025+ Sharpe>0 AND marginal_vs(active)=="ADDS" AND corr(XS01)<0.6`.
|
||||||
|
`ADDS` a sua volta richiede `holdUplift_w20 ≥ 0.05 AND robust_oos` (uplift hold-out >0.02 **e**
|
||||||
|
jackknife drop-one-month tutti positivi). È il marginal scorer del sweep precedente, portato sul
|
||||||
|
cross-sectional: si giudica **l'apporto al portafoglio live** (TP01+XS01+VRP01), non lo Sharpe
|
||||||
|
assoluto.
|
||||||
|
|
||||||
|
**Caveat cotto dentro l'harness:** il panel è **~2.5 anni** (2024-26). Ogni risultato è
|
||||||
|
SUGGESTIVO, non robusto come i 6 anni di BTC/ETH. E l'hold-out (2025-26) è **un singolo regime**
|
||||||
|
(alt-bear/chop relativo a BTC).
|
||||||
|
|
||||||
|
## Find phase — 43 script, 257 sotto-config
|
||||||
|
|
||||||
|
11 famiglie cross-sectional: MOM (varianti momentum), REV (reversal), VOL/RISK (low-vol, low-beta,
|
||||||
|
BAB, semivarianza, vol-of-vol), DIST (skew/coskew lottery), LIQ (Amihud/turnover/volume),
|
||||||
|
VAL (distanza da MA, RSI), STRUCT (double-sort, ensemble z-vote, risk-parity, low-corr, trend-R²,
|
||||||
|
lead-lag BTC), UNIV (sweep di universo). **Esito: 42/257 config `earns_slot=True`.**
|
||||||
|
|
||||||
|
Sembra molto. Ma **due tell** accomunano quasi tutti gli slot-earner:
|
||||||
|
1. corr a TP01 **fortemente negativa** (−0.2…−0.4) — è *per questo* che "aggiungono";
|
||||||
|
2. PnL **concentrato nel 2025** (ritorni +22%…+84% nel 2025).
|
||||||
|
|
||||||
|
Top per Sharpe/uplift (rappresentante per famiglia):
|
||||||
|
|
||||||
|
| id | meccanismo | univ | FULL Sh | HOLD Sh | upliftHold | jackknife | corr TP01 | corr XS01 |
|
||||||
|
|----|-----------|------|---------|---------|-----------|-----------|-----------|-----------|
|
||||||
|
| XR02-L3-p70-maj | reversal gated alta-vol | maj | 1.40 | **2.27** | 1.078 | 0.744 | 0.02 | 0.08 |
|
||||||
|
| XV02_majors_H10k5 | low **idio**-vol | maj | 1.32 | 1.95 | 1.196 | 0.792 | −0.20 | −0.06 |
|
||||||
|
| XL02-vz60r20-maj | vol-trend momentum | maj | **1.83** | 1.84 | 0.568 | 0.125 | 0.13 | 0.08 |
|
||||||
|
| XM09_all | trend-gated x-sec mom | all | 1.29 | 1.59 | 0.556 | 0.355 | −0.07 | 0.25 |
|
||||||
|
| XS01b-MAJ | double-sort mom×low-vol | maj | 1.36 | 1.23 | 0.427 | 0.16 | −0.29 | 0.38 |
|
||||||
|
| XU02/XV01 lowvol | low realized-vol | maj | 1.05 | 0.98 | 0.425 | 0.186 | −0.34 | 0.16 |
|
||||||
|
| XV03 lowbeta (BAB) | −beta | all | 0.36 | 0.71 | 0.22 | 0.051 | −0.38 | 0.19 |
|
||||||
|
| XS06b lowcorr | −corr(asset,market) | all | 0.74 | 1.00 | 0.286 | 0.092 | −0.19 | 0.18 |
|
||||||
|
|
||||||
|
## Verify phase — 3 scettici (`verify_survivors.py`)
|
||||||
|
|
||||||
|
Ipotesi sotto test: *"non sono N edge indipendenti, ma UNA scommessa di regime — short la
|
||||||
|
spazzatura high-beta nell'alt-bear 2024-26 — travestita da 30 maschere; il jackknife è robusto solo
|
||||||
|
DENTRO quel regime."* Ricostruito il book più forte per famiglia e:
|
||||||
|
|
||||||
|
**S1 — matrice di correlazione mutua (>0.6 = stessa scommessa).** Esito SFUMATO:
|
||||||
|
- Il cluster low-vol È una sola scommessa: **XV01 = XU02 = 1.00** (identici), XV01↔XV02 0.65,
|
||||||
|
XV01↔XV03 0.67, XV02↔XV03 0.44.
|
||||||
|
- MA **XM09, XL02, XS06b, XR02 sono distinti** dal cluster e tra loro (corr media off-diagonale
|
||||||
|
solo **+0.20**, solo 18% delle coppie |r|>0.6). L'ipotesi "tutto una scommessa" è **parzialmente
|
||||||
|
falsa**.
|
||||||
|
|
||||||
|
**S2 — carico su short-beta / short-market** (factor di riferimento sullo stesso panel:
|
||||||
|
SHORTBETA = book su −beta; SHORTMKT = −market alt equal-weight):
|
||||||
|
- **Cluster low-vol = short-alt-beta confermato:** XV03 1.00/0.70, XV01/XU02 **0.67/0.64**,
|
||||||
|
XV02 0.44/0.37. *Non* market-neutral: è un tilt short del mercato alt.
|
||||||
|
- **NON short-beta:** XM09 0.08/0.15, XR02 −0.21/−0.18, XL02 0.19/0.26, XS06b 0.36/0.39.
|
||||||
|
|
||||||
|
**S3 — Sharpe per anno solare (l'edge è ~solo 2025?):**
|
||||||
|
|
||||||
|
| survivor | 2024 | 2025 | 2026 |
|
||||||
|
|----------|------|------|------|
|
||||||
|
| XV02_lowidiovol | 0.07 | 1.87 | 2.12 |
|
||||||
|
| XV01/XU02 lowvol | 1.17 | 1.52 | **−0.09** |
|
||||||
|
| XV03_lowbeta | −0.25 | 0.98 | 0.12 |
|
||||||
|
| XS06b_lowcorr | 0.26 | 1.34 | 0.32 |
|
||||||
|
| **XM09_trendgmom** | **0.82** | **0.50** | **0.74** |
|
||||||
|
| XL02_voltrendmom | 0.30 | **−0.14** | **−0.43** |
|
||||||
|
| **XR02_revgated** | **0.84** | **0.40** | **2.68** |
|
||||||
|
|
||||||
|
## Conclusioni (oneste)
|
||||||
|
|
||||||
|
1. **Cluster low-vol / low-beta (XV01, XU02, XV02 in parte, XV03) = tilt short-alt-beta di regime.**
|
||||||
|
S2 lo inchioda (carico 0.44-0.70 su short-market): non è un fattore market-neutral, è "short la
|
||||||
|
spazzatura" mentre gli alt sanguinano vs BTC. XV01/XU02 **già in decadimento (2026 −0.09).** Non
|
||||||
|
può dimostrare di sopravvivere a un flip alt-bull. → **RIGETTATO come sleeve.** Conferma
|
||||||
|
l'osservazione 4874 (XS04b = regime-dependent short-beta tilt) generalizzata all'intera famiglia.
|
||||||
|
|
||||||
|
2. **XL02 (vol-trend momentum) = overfit al panel iniziale.** FULL Sharpe più alto (1.83) ma S3 lo
|
||||||
|
uccide: 2025 −0.14, 2026 −0.43. Il numero full è guidato dal 2024, ora è morto. → **RIGETTATO.**
|
||||||
|
|
||||||
|
3. **2 LEAD genuini** — distinti (S1), NON short-beta (S2), positivi in **tutti e 3 gli anni** (S3):
|
||||||
|
- **XM09 — cross-sectional momentum gated dal trend di mercato.** Long top-k/short bottom-k alt,
|
||||||
|
attivo solo quando la somma trailing del mercato equal-weight è >0. Sharpe 0.82/0.50/0.74,
|
||||||
|
short-beta-load 0.08, corr TP01 −0.07, uplift hold 0.556 / jackknife 0.355. È il candidato più
|
||||||
|
regime-robusto. **Caveat:** stessa FAMIGLIA di XS01 (x-sec momentum) su universo più largo (49)
|
||||||
|
con gate diverso (trend di mercato vs dispersione) → più un **possibile affinamento di XS01**
|
||||||
|
che una sleeve nuova; corr XS01 0.25, ma marginal scorer dice che ADDS oltre XS01.
|
||||||
|
- **XR02 — short-term reversal gated da alta-vol.** Reversal a 3g attivo solo quando la vol
|
||||||
|
realizzata di mercato è nel regime alto (>p70 espandente). Sharpe 0.84/0.40/**2.68**,
|
||||||
|
short-beta-load −0.21, corr a tutto il resto ~0/negativa, hold-out Sharpe 2.27. Microstruttura
|
||||||
|
reale (overreaction in panico). **Caveat:** H=3 → **turnover alto**; il reversal vive proprio
|
||||||
|
sull'illiquidità che lo rende costoso da eseguire (l'harness addebita fee sul turnover e regge,
|
||||||
|
ma il fill reale su alt minori è ottimistico).
|
||||||
|
|
||||||
|
## Perché NON deployabili adesso (caveat trasversali)
|
||||||
|
|
||||||
|
- **Panel ~2.5 anni a regime unico.** Anche i 2 lead hanno hold-out = 2025-26 = stesso macro-regime.
|
||||||
|
Suggestivi, non robusti come i 6 anni BTC/ETH.
|
||||||
|
- **STAT-MODE di esecuzione.** Un book cross-sectional a 10-19 gambe (long-k+short-k) su alt non è
|
||||||
|
eseguibile col capitale attuale (conto reale ~$600; servono ~$20k per gambe sensate, come già
|
||||||
|
notato per XS01). Sono segnali da monitorare, non ordini.
|
||||||
|
- **Lezione confermata (di nuovo):** su un panel corto a regime unico il jackknife drop-one-month
|
||||||
|
certifica la robustezza DENTRO il regime, non ATTRAVERSO i regimi. Il discriminante decisivo è
|
||||||
|
stato **S2 (carico su short-beta) + S3 (consistenza per-anno)**, non lo Sharpe né l'uplift
|
||||||
|
hold-out (che il cluster regime-bet aveva altissimi: upliftHold fino a 1.20).
|
||||||
|
|
||||||
|
## Azioni
|
||||||
|
|
||||||
|
- **Nessuna modifica al portafoglio live** (TP01 55% + XS01 25% + VRP01 20% invariato).
|
||||||
|
- **Forward-monitor** i 2 lead (XM09, XR02) quando il panel HL accumula un secondo regime.
|
||||||
|
- **XM09 come affinamento candidato di XS01** (gate trend di mercato + universo 49) da valutare a
|
||||||
|
parità di sleeve, NON come sleeve aggiuntiva, in una prossima iterazione.
|
||||||
|
- Harness `xslib.py` + 43 script + `verify_survivors.py` committati come riferimento riusabile.
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
# 2026-06-21 — Blind signal fleet: 52 agenti "esperti di segnali" su curve anonime BTC/ETH
|
||||||
|
|
||||||
|
## Obiettivo (richiesta utente)
|
||||||
|
|
||||||
|
Far partire ~50 subagenti **esperti di segnali** a cui passare lo storico di **ETH e BTC
|
||||||
|
in forma ANONIMA** ("senza dire di cosa sono, con curve sovrapposte"): devono trovare come
|
||||||
|
**anticipare l'andamento**, liberi di scrivere script o reti neurali ad hoc. L'**orchestratore**
|
||||||
|
valuta la validità su **PnL e maxDD**.
|
||||||
|
|
||||||
|
L'idea forte del setup cieco: se gli agenti non sanno che sono BTC/ETH, non possono
|
||||||
|
pattern-matchare a memoria il crash COVID 2020 / l'orso 2022 / l'halving 2024 — devono trovare
|
||||||
|
un timing **trasferibile**, non riconoscere l'era. È anche un test di onestà del metodo: l'edge
|
||||||
|
deve reggere su un hold-out che gli agenti non hanno mai visto.
|
||||||
|
|
||||||
|
## Setup — harness cieco e leak-free (prima degli agenti)
|
||||||
|
|
||||||
|
> 50 agenti su un harness che perde = 50 fantasie (lezione fondante del progetto). Quindi prima
|
||||||
|
> l'infrastruttura, poi la flotta.
|
||||||
|
|
||||||
|
- `scripts/research/blind/make_blind.py` — esporta BTC/ETH **1d** (via il path certificato
|
||||||
|
`altlib.get`) come **"Series A" / "Series B"**: rebase a **100** (curve sovrapposte, il livello
|
||||||
|
non urla più "$60k bitcoin"), **calendario sintetico** dal 2001 (niente era-crypto da
|
||||||
|
riconoscere), volume normalizzato alla mediana. Split **70% train (visibile agli agenti) / 30%
|
||||||
|
test (solo orchestratore)**. Mapping A=BTC, B=ETH tenuto FUORI dal meta visibile.
|
||||||
|
- `scripts/research/blind/blindlib.py` — l'unico modulo che un agente importa. Evaluator
|
||||||
|
leak-free: la posizione decisa a `close[i]` è **shiftata** e tenuta nella barra `i+1` (impossibile
|
||||||
|
leakare moltiplicando un peso per il rendimento della stessa barra), fee su turnover (Deribit
|
||||||
|
0.10% RT). Toolkit di indicatori causali ri-esportati da altlib.
|
||||||
|
- **Guardia di causalità automatica** (`causality_ok`): ri-chiama `signal()` su un **prefisso
|
||||||
|
troncato** e pretende che la coda combaci con `signal()` sull'array intero. Qualunque segnale che
|
||||||
|
sbircia il futuro (shift(-k), finestre centrate, fit globale, statistiche full-sample) **diverge →
|
||||||
|
squalificato**. È ciò che rende onesta anche la "rete neurale ad hoc": un modello fittato sul df
|
||||||
|
intero (che a test-time contiene il futuro) fallisce la guardia; passa solo l'expanding/walk-forward.
|
||||||
|
- `score_all.py` — il **giudice unico dell'orchestratore**: per ogni modulo gira la guardia, valuta
|
||||||
|
sul **test held-out** A e B, ordina per PnL/maxDD vs benchmark buy&hold.
|
||||||
|
- `verify_top.py` — secondo strato avversariale: corr al trend canonico TSMOM, fee-stress 0.20% RT,
|
||||||
|
jackknife drop-block.
|
||||||
|
|
||||||
|
Verifica dell'harness: momentum onesto → causale ok, OOS +44% a 19% DD; segnale **deliberatamente
|
||||||
|
leaky** (guarda domani) → Sharpe 18 assurdo ma **correttamente squalificato**. Benchmark buy&hold
|
||||||
|
OOS sul tail = **−7% PnL, 68% DD, Sharpe 0.22** (il tail 2024-26 contiene un drawdown brutale →
|
||||||
|
anticipare il movimento ha spazio reale per vincere).
|
||||||
|
|
||||||
|
## Flotta — 52 agenti, 52 ipotesi distinte
|
||||||
|
|
||||||
|
Workflow `blind-signal-fleet` (52 agenti in parallelo, ~2h, 2.5M token, 971 tool-call). A ognuno
|
||||||
|
**un'ipotesi diversa** (per non riscoprire tutti il momentum): 11 famiglie — trend/TSMOM,
|
||||||
|
breakout (Donchian/Keltner/squeeze/pivot/volbreak), mean-rev/oscillatori (RSI/Bollinger/zrev/stoch/
|
||||||
|
DPO/WillR), vol-regime (vol-target/regime-switch/ATR-ride/dd-derisk/**vol-of-vol**), struttura
|
||||||
|
(HHLL/channel-pos), statistici (Hurst/autocorr/efficiency/skew/entropy), ciclo (FFT/Kalman),
|
||||||
|
volume (OBV/PVT/vol-div), **8 ML** (Ridge, logistic, MLP-reg, MLP-clf, GBM, kNN-analog, RLS,
|
||||||
|
RandomForest) e 5 meta/ensemble.
|
||||||
|
|
||||||
|
**Esito flotta: 52/52 riportati, 52/52 passano la guardia di causalità** (zero look-ahead — la
|
||||||
|
disciplina dell'harness ha tenuto su tutta la flotta, ML inclusi).
|
||||||
|
|
||||||
|
## Risultati OOS (orchestratore — PnL & maxDD sul test held-out)
|
||||||
|
|
||||||
|
Benchmark buy&hold OOS: **PnL −7%, maxDD 68%**. Top per Sharpe-min (peggiore tra A e B):
|
||||||
|
|
||||||
|
| # | strategia | PnL_A | PnL_B | DD worst | Sh_min | famiglia |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| 1 | macd | +23% | +19% | **11%** | 0.84 | trend |
|
||||||
|
| 2 | accel | +40% | +22% | 12% | 0.79 | trend (2ª diff) |
|
||||||
|
| 3 | vol_of_vol | +30% | +32% | 21% | 0.69 | vol-regime |
|
||||||
|
| 4 | regime_switch | +25% | +46% | 20% | 0.63 | vol-regime |
|
||||||
|
| 5 | rf (ML) | +12% | +8% | **7%** | 0.62 | ML walk-fwd |
|
||||||
|
| 6 | obv | +22% | +20% | 16% | 0.60 | volume |
|
||||||
|
|
||||||
|
Tutti i top sono varianti **trend/vol-regime**. Mean-reversion e ML (logistic/gbm/mlp) in fondo →
|
||||||
|
ri-conferma cieca di "mean-rev morto" e "ML walk-forward debole" del progetto. Lo **Sharpe OOS ~0.84
|
||||||
|
decade dal train ~1.4** (firma classica di overfit/regime). Ma vs buy&hold (−7%/68% DD) i top trend
|
||||||
|
**ribaltano il segno e tagliano il DD ~3-6×**: è il valore reale, identico alla lezione TP01.
|
||||||
|
|
||||||
|
## Verifica avversariale — 3 scettici indipendenti (REFUTE, non confirm)
|
||||||
|
|
||||||
|
1. **Regime-luck** → **REFUTED ×3.** I top-5 bar su ~800 OOS forniscono il **67-102% di tutto il
|
||||||
|
PnL**; togliendo 10 bar la serie va **negativa**; `accel` crolla nel terzo finale (COMB Sharpe
|
||||||
|
**−1.21**); A e B non concordano su *quando* funziona. Edge concentrato, non distribuito.
|
||||||
|
2. **Trend-redundancy** → **REFUTED ×4.** Regressione `cand ~ α + β·TSMOM` (Newey-West HAC):
|
||||||
|
**t(α) = +0.92..+1.51, nessuno supera 1.96**. corr-al-trend 0.34-0.74, β 0.45-0.73; media residua
|
||||||
|
+0.05-0.08/anno = rumore. Sono TSMOM meglio tarati, **non alpha ortogonale**; contro il TP01 reale
|
||||||
|
(~1.3) il margine svanisce.
|
||||||
|
3. **Overfit/robustezza** → MACD **non-refuted** (plateau vero a un asse, 0% celle <0.5) ma Sharpe OOS
|
||||||
|
onesto **0.84, non 1.40** (numero da docstring = in-sample). `accel` **REFUTED** (il termine di
|
||||||
|
accelerazione, la sua tesi, **danneggia** l'OOS; LAG knife-edge: −20% → −63% Sharpe; corner
|
||||||
|
congiunti negativi). `vol_of_vol` **REFUTED** (gate threshold-fit: PCTL 0.80→0.60 distrugge il 73%
|
||||||
|
dello Sharpe OOS). Fee = drag secondario ~10%, non il killer; il killer è la sensibilità ai parametri.
|
||||||
|
|
||||||
|
## Verdetto
|
||||||
|
|
||||||
|
**52 agenti ciechi, orchestratore che valuta PnL e maxDD su hold-out, e NIENTE di nuovo
|
||||||
|
sopravvive alla verifica avversariale.** Ogni "vincitore" è trend-beta di due curve strutturalmente
|
||||||
|
rialziste; soffitto Sharpe OOS **~0.84** su questo singolo hold-out; nessun alpha statisticamente
|
||||||
|
distinguibile dal TSMOM. È una **ri-conferma INDIPENDENTE e CIECA del soffitto direzionale ~1.3** del
|
||||||
|
progetto e del pattern "TSMOM travestito" — raggiunta da agenti che non sapevano nemmeno fossero
|
||||||
|
BTC/ETH. Il più solido è **macd** (plateau vero, OOS Sharpe 0.84, DD 11%): classe-TP01,
|
||||||
|
**forward-monitor al più, non deploy**. Conferma le regole: (a) giudicare lo Sharpe **marginale vs
|
||||||
|
TP01**, non assoluto; (b) un hold-out corto premia chi è stato fortunato in pochi bar.
|
||||||
|
|
||||||
|
### Valore metodologico (cosa resta)
|
||||||
|
|
||||||
|
L'harness cieco riusabile: `data/blind/` + `blindlib`/`blind_eval`/`score_all`/`verify_top`. La
|
||||||
|
**guardia di causalità online** ha tenuto 52 strategie (ML incluso) leak-free senza intervento
|
||||||
|
manuale → strumento da riusare per ogni futura flotta. La pipeline "anonimizza → fan-out cieco →
|
||||||
|
giudice unico OOS → 3 scettici (regime-luck / trend-redundancy / overfit)" ha ucciso ogni falso
|
||||||
|
positivo che lo Sharpe assoluto avrebbe promosso.
|
||||||
|
|
||||||
|
File: `scripts/research/blind/{make_blind,blindlib,blind_eval,score_all,verify_top}.py`,
|
||||||
|
`agents/agent_00..51_*.py` (52 moduli), `leaderboard.json`, `verify_top.json`,
|
||||||
|
`SKEPTIC_VERDICTS.json`. Dati rigenerabili: `data/blind/` (gitignored).
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
# 2026-06-21 — Asse intraday/microstruttura: il lead più vicino al reale, ma NON deployabile
|
||||||
|
|
||||||
|
## Perché (utente: "cerchiamo qualcosaltro")
|
||||||
|
|
||||||
|
Direzionale e relative-value su BTC/ETH esauriti (flotte blind + ortho). L'unico asse mai
|
||||||
|
sfruttato dopo il reset = il **tempo intraday** (feed certificati 5m/15m/1h; tutto era a 1d).
|
||||||
|
Meccanismi diversi da trend e relative-value: bias ora/sessione (perp con funding a 00/08/16 UTC),
|
||||||
|
reversione post-evento (vol/volume/gap), breakout del range del giorno prima.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
`scripts/research/intraday/intra_score.py`: wrappa `altlib.study_marginal` a un TF a scelta
|
||||||
|
(compone i rendimenti intraday a daily, li valuta col **marginal scorer indurito** = multi-cut +
|
||||||
|
edge-in-sample + hedge-vs-alpha) e riporta **turnover + fee-sweep a 0.20% RT**. Il muro: a 0.10% RT
|
||||||
|
il churn intraday è morte (un flip orario fa 2152 trade/anno → −8.6 Sharpe netto). Vincolo agli
|
||||||
|
agenti: **basso turnover**, l'intraday come informazione (timing/sizing/gating), non HFT.
|
||||||
|
|
||||||
|
## Flotta — 16 agenti
|
||||||
|
|
||||||
|
16 ipotesi low-turnover. Esito grezzo: 16 riportati, **10 "earns_slot"** (di nuovo gonfiato).
|
||||||
|
|
||||||
|
## Diagnosi orchestratore — separare ortogonale vero da trend-beta
|
||||||
|
|
||||||
|
Per corr-a-TP01 (`meta_intra.py`): 2 sono **trend-beta** (close_location 0.81, trend_quality 0.75 —
|
||||||
|
Sharpe in-sample alto ma preso in prestito dal trend), 3 **mixed**, **5 genuinamente ortogonali**
|
||||||
|
(|corr|<0.4): open_drive (0.13), prevday_range_breakout (0.15), vol_event_revert_15m (−0.1),
|
||||||
|
volume_spike_revert (0.14), gap_fill (0.04) — 2 famiglie (breakout-continuation + capitulation-revert),
|
||||||
|
mutuamente de-correlate. **Combo dei 5: Sharpe standalone 1.80, corr-TP01 0.17, uplift +0.33/+0.27/
|
||||||
|
+0.34/+0.34/+0.53 a OGNI cut** (non solo 2025).
|
||||||
|
|
||||||
|
## Gauntlet deterministico (`verify_intra.py`) — passa TUTTO ciò che uccise le onde precedenti
|
||||||
|
|
||||||
|
- **In-sample pre-2025 Sharpe 1.75; uplift pre-2025-ONLY +0.281** (l'ortho faceva +0.027 = null).
|
||||||
|
- **Walk-forward selection** (scegli su solo passato, testa avanti): **+0.303 / +0.368** (l'ortho dava −0.07).
|
||||||
|
- **Drop-one robusto** (+0.24..+0.31 pre-2025), **fee-robusto a 0.30% RT**, **leak-free**
|
||||||
|
(online-consistency: max_tail_diff = 0.0 su tutti e 5). Sembrava IL lead.
|
||||||
|
|
||||||
|
## Verifica avversariale (3 scettici indipendenti) — il verdetto vero
|
||||||
|
|
||||||
|
1. **Execution/microstruttura:** **open_drive = ARTEFATTO di etichettatura UTC.** Spostando il
|
||||||
|
confine del giorno di 4h l'uplift va NEGATIVO (−0.10); togliendo l'ancora UTC (trailing-8h) Sharpe
|
||||||
|
0.01; funziona solo a 00:00 UTC, solo alle ore 3 e 7. **Scartare.** `prevday_range_breakout` invece
|
||||||
|
**REGGE** (plateau su k, robusto allo shift del confine, fill eseguibili a close) = unico candidato
|
||||||
|
onesto, ma la decorrelazione viene tutta dalla gamba SHORT che si appoggia al regime down 2025-26;
|
||||||
|
anchor=1 only. **Caveat $600:** il vol-target fa ~8500 ribilanciamenti/anno, 97-98% < $1 di nozionale
|
||||||
|
→ la fee proporzionale modellata su trade infinitesimi è **finzione** a $300/gamba (vale anche per TP01).
|
||||||
|
2. **Hedge + tail:** **REFUTED.** L'uplift pre-2025 +0.281 sta al **20-24° percentile del null di un
|
||||||
|
asset a corr-zero** (mediana null +0.371) — essendo a corr +0.175 (non 0) e bassa vol, **aggiunge
|
||||||
|
MENO del rumore scorrelato**. È **hedge** (corr Sharpe-TP01/uplift −0.57..−0.80; TP01-down uplift
|
||||||
|
+0.79 vs TP01-up +0.20) e **tail-luck** (le gambe revert: top-5 giorni = 76-83% del PnL, <10
|
||||||
|
eventi/anno, front-loaded 2019-21; combo: metà uplift in ~10 giorni).
|
||||||
|
3. **Overfit/robustezza:** **ROBUST-PLATEAU** (243-cell joint grid pre-2025 uplift min +0.134/med
|
||||||
|
+0.211, 99% celle >+0.15; ogni anno positivo). MA segnala lui stesso il **null-pctl 0.20**: "il
|
||||||
|
beneficio è la matematica di diversificazione di uno stream ortogonale a Sharpe 1.75, NON timing-alpha
|
||||||
|
specifico-TP01" + storia corta sulle gambe revert + fill modellati vs reali.
|
||||||
|
|
||||||
|
## Verdetto
|
||||||
|
|
||||||
|
**Niente in live.** L'asse intraday ha prodotto il lead **più vicino al reale** di tutta la ricerca,
|
||||||
|
ma sotto 3 scettici: **open_drive è artefatto** (UTC-labeling); la combo **fallisce il null a
|
||||||
|
corr-zero** (aggiunge meno del rumore), è **hedge-shaped** e **tail-luck**; e lo Sharpe modellato è
|
||||||
|
gonfiato dal micro-ribilanciamento sub-dollaro a $600. Lo Sharpe standalone 1.80 NON è affidabile
|
||||||
|
(artefatto + coda + finzione di fill). **Resta solo TP01.**
|
||||||
|
|
||||||
|
**Lead reale (forward-monitor, non deploy):** `prevday_range_breakout` — l'unico segnale sopravvissuto
|
||||||
|
allo scettico d'esecuzione (breakout del range del giorno prima, eseguibile, leak-free), con caveat
|
||||||
|
short-leg/regime-2025. Trattamento = come `dvol_spread` / XS01 / STA05.
|
||||||
|
|
||||||
|
### Lezioni harness — CODIFICATE (il vero ritorno)
|
||||||
|
|
||||||
|
1. ✅ **`altlib.day_boundary_robust(target_fn, tf)`** — shifta il confine del giorno UTC e ri-misura
|
||||||
|
l'uplift marginale: INVARIANT (segnale di prezzo, spread 0) / ROBUST (effetto calendario vero,
|
||||||
|
resta positivo) / **ARTIFACT-RISK** (l'uplift si inverte = etichettatura). Verificato: riproduce
|
||||||
|
da solo il verdetto degli scettici — open_drive → ARTIFACT-RISK (+0.23→−0.33), prevday_breakout
|
||||||
|
→ ROBUST. Test `tests/test_harness_realism.py`.
|
||||||
|
2. ✅ **`altlib.eval_weights_smallcap(df, target, capital=600, min_order=5)`** — salta i
|
||||||
|
ribilanciamenti sub-min_order (la finzione del micro-trading a $600), riporta lo Sharpe haircut
|
||||||
|
reale vs modellato. Vale per ogni sleeve a questo capitale, TP01 incluso. Test idem.
|
||||||
|
3. ✅ **`altlib.causality_ok(target_fn, tf)`** — guardia look-ahead/online-consistency (ricalcola
|
||||||
|
il target su un prefisso e pretende che la coda combaci con il full): eval_weights shifta la
|
||||||
|
posizione ma NON vede una feature non-causale (finestra centrata / shift(-k) / stat full-sample).
|
||||||
|
Integrata in `intra_score` (un leak è squalificato prima dello scoring). + il calendar-artifact
|
||||||
|
gate (`day_boundary_robust`) ora gira dentro `intra_score`: **open_drive/weekly_seasonality/
|
||||||
|
overnight → CAL-ARTIFACT, fuori dagli slot da soli**; prevday_breakout resta (ROBUST). Il lab
|
||||||
|
intraday ora auto-becca leak e artefatti-calendario che ieri richiedevano gli scettici. Test idem.
|
||||||
|
|
||||||
|
File: `scripts/research/intraday/{intra_score,meta_intra,verify_intra}.py`,
|
||||||
|
`agents/agent_00..15_*.py`, `intra_leaderboard.json`.
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
# 2026-06-21 — Caccia all'ORTOGONALE a TP01: relative-value BTC/ETH (eseguibile a $600)
|
||||||
|
|
||||||
|
## Perché (richiesta utente: "cerca ortogonale a TP01")
|
||||||
|
|
||||||
|
La flotta cieca (stesso giorno) ha confermato: niente di NUOVO in direzionale BTC/ETH — tutto è
|
||||||
|
trend-beta di TP01 (soffitto ~1.3). L'unica via a un nuovo slot LIVE è un meccanismo **ortogonale**
|
||||||
|
(bassa correlazione, alpha residua). Il più promettente **eseguibile al capitale reale ~$600** è un
|
||||||
|
**book RELATIVE-VALUE a 2 gambe BTC/ETH** (long una / short l'altra), grosso modo market-neutral →
|
||||||
|
correlazione naturale bassa col trend, e a 2 gambe è eseguibile (a differenza del book a 19 gambe di
|
||||||
|
XS01 che serve ~$20k).
|
||||||
|
|
||||||
|
## Setup — ortho-lab + giudice MARGINALE (non Sharpe assoluto)
|
||||||
|
|
||||||
|
`scripts/research/ortho/ortholib.py`: BTC/ETH 1d allineati su date comuni; `eval_book(book_fn)` con
|
||||||
|
`book(btc,eth)->(w_btc,w_eth)`, **shift di entrambe le gambe** (no leak), fee su entrambe, serie netta
|
||||||
|
**giornaliera**; guardia di causalità online; check **eseguibilità a $600** (max gamba ≤ 0.5 = cap
|
||||||
|
$300/asset). Il giudice è `altlib.marginal_vs_tp01`: **corr a TP01, uplift OOS del blend, alpha
|
||||||
|
residua, robust_oos** (clean-year + jackknife drop-month). Verdetto = ADDS, **non** Sharpe assoluto.
|
||||||
|
`ortho_score.py` (giudice), `meta_ortho.py` (corr mutua + persistenza multi-cut), `sleeve_rv.py`.
|
||||||
|
|
||||||
|
Sanity: ratio-momentum → ADDS (corr 0.05); ratio-mean-reversion → DILUTES. L'harness discrimina.
|
||||||
|
|
||||||
|
## Flotta — 18 agenti relative-value (~40 min)
|
||||||
|
|
||||||
|
18 ipotesi distinte: ratio-momentum multi-orizzonte, XS a 2 asset, beta-neutral residuo, Donchian
|
||||||
|
sul ratio, EMA-cross, accel, carry lento, Kalman-spread, gate-correlazione, gate-vol, inverse-vol,
|
||||||
|
rebalance-harvest, lead-lag, **DVOL-spread**, **VRP relativo**, dispersione, ensemble.
|
||||||
|
|
||||||
|
**Esito grezzo: 18 riportati, 17 "ADDS / earns_slot".** → **bandiera rossa**: non esistono 17 alpha.
|
||||||
|
Gli agenti stessi l'hanno annotato ("hold-out corto ~537g", "uplift dipende dal regime ETH-bleed
|
||||||
|
2025", "forward-monitor non full-weight").
|
||||||
|
|
||||||
|
## Diagnosi dell'orchestratore — il "17 slot" è gonfiato
|
||||||
|
|
||||||
|
1. **Una scommessa o tante?** corr mutua media **0.43** → collassano a **8 rappresentanti**
|
||||||
|
de-correlati. Non 17, non 1.
|
||||||
|
2. **Persistente o solo finestra 2025?** `marginal_vs_tp01` fissa l'hold-out al 2025-01-01 = proprio
|
||||||
|
la finestra dove ETH ha perso vs BTC e TP01 è debole. Ri-misurando l'uplift a **più cut**
|
||||||
|
(2022/23/24/25): il basket selection-free era +0.06/+0.06/+0.11/+0.38 (positivo ovunque ma
|
||||||
|
crescente verso il 2025). Smaschera anche i **falsi** che il robust_oos fisso-2025 non vede:
|
||||||
|
`kalman_spread` (−0.14/−0.16/−0.10 poi +0.37) e `xs2_zscore` sono **2025-only**.
|
||||||
|
3. **Selezione walk-forward (senza hindsight):** scegliere i top-4 per uplift sul **solo passato** e
|
||||||
|
testare in avanti → uplift **−0.07** (sel <2023) / +0.05 (<2024) / +0.43 (<2025). **Scegliere la
|
||||||
|
variante vincente in anticipo è inaffidabile**; il mio "curated 4" è in parte hindsight.
|
||||||
|
|
||||||
|
## Verifica avversariale (scettico indipendente) — REFUTED
|
||||||
|
|
||||||
|
Sul **basket selection-free** (equal-weight di tutti i book market-neutral, NESSUN cherry-picking):
|
||||||
|
- standalone Sharpe **0.61**, maxDD 15%, **corr a TP01 0.05** (genuinamente ortogonale).
|
||||||
|
- **uplift full +0.078 = pre-2025 +0.027 / solo-2025+ +0.401.** Il pre-2025 **+0.027 sta al 49°
|
||||||
|
percentile di 500 asset-rumore a corr-zero** (+0.029 per costruzione) → è **matematica di
|
||||||
|
diversificazione, non segnale**.
|
||||||
|
- **corr(Sharpe annuo TP01, uplift annuo basket) = −0.87**; condizionato: TP01 su → +0.014, TP01 giù
|
||||||
|
→ +0.369. **È un hedge dei drawdown di TP01, non un premio autonomo.** Paga nel 2022 (orso) e
|
||||||
|
2025-26 (ETH-bleed) — i due anni peggiori di TP01 — rumore altrove (2023 −0.06, 2024 −0.12).
|
||||||
|
- Block-bootstrap P(uplift>0): full 90%, **pre-2025 66% (testa o croce)**, 2025+ 99%.
|
||||||
|
- Fee: a **0.30% RT il pre-2025 va NEGATIVO** (−0.021); sopravvive solo il numero del regime 2025.
|
||||||
|
- Eseguibilità OK ($264/gamba, turnover 12/yr) — non è quello il problema.
|
||||||
|
|
||||||
|
## Verdetto
|
||||||
|
|
||||||
|
**Niente di questa flotta merita uno slot LIVE.** Il meccanismo relative-value BTC/ETH è REALE e
|
||||||
|
genuinamente ortogonale (corr ~0.05), ma è un **hedge della debolezza di TP01 travestito da alpha**:
|
||||||
|
il suo contributo pre-2025 è indistinguibile da un asset-rumore a corr-zero (49° percentile del null)
|
||||||
|
e muore a fee realistiche; l'unico payoff vero è una singola finestra di 537 giorni (2025-26).
|
||||||
|
Deployarlo = deployare un backtest mono-regime. **Resta live solo TP01** (l'unica cosa che supera
|
||||||
|
tutto questo scrutinio). Coerente con XS01 (stessa famiglia cross-sectional): diversificatore
|
||||||
|
da monitorare, non alpha da eseguire — e la versione a 2 asset è ancora più sottile della 19-gambe.
|
||||||
|
|
||||||
|
### Valore metodologico (cosa resta, ed è importante)
|
||||||
|
|
||||||
|
- **Il marginal scorer fisso-2025 è ingannabile** (17/18 "ADDS"). Ciò che ha ucciso i falsi positivi:
|
||||||
|
**persistenza multi-cut** + **selezione walk-forward** + **bootstrap vs null a corr-zero**. Lezione
|
||||||
|
da cablare nello scorer: testare PIÙ cut e confrontare l'uplift col **null di un asset-rumore
|
||||||
|
ortogonale** (un'asset scorrelato con drift positivo "aggiunge" +0.03 per pura matematica — non è
|
||||||
|
un edge). Un basso-corr che paga solo quando il core è debole è un **hedge**, va prezzato come tale.
|
||||||
|
- Lab riusabile: `ortholib`/`ortho_score`/`meta_ortho` (giudice marginale + persistenza). I 18 book +
|
||||||
|
`sleeve_rv.py` (curated, **selection-biased — non deployare**) restano come riferimento.
|
||||||
|
|
||||||
|
File: `scripts/research/ortho/{ortholib,ortho_score,meta_ortho,sleeve_rv}.py`,
|
||||||
|
`agents/agent_00..17_*.py`, `ortho_leaderboard.json`, skeptic `skeptic_{basket,regime,null}.py`.
|
||||||
|
|
||||||
|
## AGGIORNAMENTO — lezione codificata in `altlib.marginal_vs_tp01` (stesso giorno)
|
||||||
|
|
||||||
|
I tre gate sono ora **codice**, non solo prosa (test `tests/test_marginal_scorer.py`, +5 test):
|
||||||
|
1. **persistenza multi-cut** (`multicut_uplift`/`multicut_persistent`): uplift a ogni inizio anno,
|
||||||
|
non solo all'HOLDOUT fisso → uccide i 2025-only (es. `kalman_spread`, negativo a ogni cut pre-2025).
|
||||||
|
2. **edge in-sample** (`has_insample_edge`): lo Sharpe standalone PRE-holdout dev'essere ≥0.5. È il
|
||||||
|
discriminante onesto (la basket faceva 0.29). I `null_pctl_*` (vs asset-rumore a corr-zero) restano
|
||||||
|
come CONTESTO — mostrano che un low-corr "aggiunge" ~+0.03 per matematica, vero per sleeve buoni e
|
||||||
|
cattivi, quindi non possono essere IL gate; l'edge in-sample sì.
|
||||||
|
3. **hedge vs alpha** (`is_hedge`): `corr(Sharpe-TP01, uplift annuo)` molto negativa + paga solo
|
||||||
|
quando TP01 è giù → HEDGE, non alpha.
|
||||||
|
|
||||||
|
Verdetti nuovi **HEDGE** e **NOISE**; `earns_slot` ora pretende ADDS + robust_oos + has_insample_edge
|
||||||
|
+ not is_hedge. **Sull'onda ortho lo scorer indurito ribalta 17/18 "ADDS" → 1** (`dvol_spread`, unico
|
||||||
|
con edge in-sample reale 0.57; gli altri 16 → NOISE/HEDGE). Controllo: un sleeve sintetico Sharpe~1.3
|
||||||
|
scorrelato resta **ADDS** (non rigetta i diversificatori veri — XS01-like). La verifica avversariale
|
||||||
|
di 3 giorni è ora una chiamata di funzione.
|
||||||
@@ -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 (1−W) 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.
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# 2026-06-22 — Sweep 65-agenti: crypto -> mercati IB (mercati × timing × anni)
|
||||||
|
|
||||||
|
## Obiettivo (goal utente)
|
||||||
|
Usare >=50 agenti per prendere l'anticipazione crypto->equity e trovare la MIGLIORE soluzione,
|
||||||
|
provando diversi mercati e timing, su piu' anni.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
- **Dati**: universo IB esteso a **26 ETF certificati** (azioni US/settori/intl/bond/credito/oro/
|
||||||
|
commodity/REIT), cache su disco (`fetch_ib_equities.py` + BROAD2). Crypto BTC/ETH 1h (Deribit).
|
||||||
|
- **Harness onesto** (`crypto_lead_harness.py`): per ogni sessione equity, lead = crypto nella
|
||||||
|
finestra equity-CHIUSO [P 21:00 -> D 13:00 UTC] (overnight; il weekend e' il caso lungo). Predice
|
||||||
|
gap/intraday/full. Metriche: corr, **t incrementale vs sessione equity precedente**, Sharpe
|
||||||
|
eseguibile (sign(lead)*predict, net costi) FULL/IS/OOS, **hit per-anno**.
|
||||||
|
- **Workflow** (`wf_crypto_lead.js`): grid 416 config (2 lead × 26 mercati × 2 giorni × 2 predict ×
|
||||||
|
2 finestre). **52 agenti sweep** -> **12 agenti verifica avversariale** (stress 10bps + OOS 2024+ +
|
||||||
|
multi-anno) -> **1 sintesi**. Totale **65 agenti**, 1.7M token.
|
||||||
|
|
||||||
|
## Risultato
|
||||||
|
|
||||||
|
### Fenomeno fortissimo: crypto overnight -> GAP di apertura equity
|
||||||
|
Cluster coerente in cima, TUTTI predict=gap/overnight, su ogni target risk-on:
|
||||||
|
| lead->target | t-incr | Sh OOS@4bps | @10bps | OOS-recente | anni+ |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| ETH->IWM gap | 17.1 | 2.49 | 1.96 | 2.41 | 7/8 |
|
||||||
|
| ETH->QQQ gap | 17.9 | 2.36 | 1.83 | 2.31 | 7/8 |
|
||||||
|
| ETH->XLK gap | 17.4 | 2.40 | 1.93 | 2.30 | 7/8 |
|
||||||
|
| **BTC->QQQ gap** | 15.0 | 2.31 | 1.78 | 2.16 | **9/9** |
|
||||||
|
| BTC->SPY gap | 14.4 | 2.14 | 1.69(lf) | 2.03 | 9/9 |
|
||||||
|
|
||||||
|
Statisticamente schiacciante (t 14-18, sopra Bonferroni su 416 test), regge stress costi e OOS
|
||||||
|
recente, **positivo 8-9 anni su 8-9**.
|
||||||
|
|
||||||
|
### Ma DUE killer (i verificatori avversariali concordi)
|
||||||
|
1. **NON tradabile via ETF**: il gap e' gia' prezzato all'open dell'ETF -> serve un FUTURE indice
|
||||||
|
tenuto overnight (MNQ/MES/M2K). A $0.5-2k il margin overnight di anche un micro consuma il
|
||||||
|
capitale e rischia la liquidazione su un gap avverso -> **fuori portata per costruzione**.
|
||||||
|
2. **E' RISK-BETA, non alpha**: la finestra-lead crypto e' quasi CONTEMPORANEA al gap (stesso shock
|
||||||
|
macro overnight, equity chiuso). t enorme = co-movimento risk-on/off, non ETH/BTC che *anticipa*.
|
||||||
|
Firma: la forza e' negli anni alta-vol (2022 hit 0.71-0.75), piatta negli anni calmi (2019/21/23).
|
||||||
|
corr ~0.37 -> beta implicito ~37%, alpha residuo piccolo.
|
||||||
|
|
||||||
|
### L'unico tradabile via ETF e' troppo debole
|
||||||
|
ETH->XLE intraday 6h (compri XLE al day-open, chiudi +6h): Sh OOS 0.48@4bps **-> 0.15@10bps** (annuo
|
||||||
|
4.1%->1.0%), t-incr 2.38 **sotto Bonferroni** (~3.5 su 416 test). Edge netto onesto ~ZERO.
|
||||||
|
|
||||||
|
## Verdetto (sintesi multi-agente)
|
||||||
|
**Nessun edge proprietario deployabile a basso capitale.** Il fenomeno crypto->equity-overnight e'
|
||||||
|
statisticamente reale e robustissimo su 9 anni, ma e' (a) risk-beta condiviso, non anticipazione
|
||||||
|
sfruttabile, e (b) catturabile solo con futures overnight, fuori dal nostro capitale. L'unica
|
||||||
|
versione ETF-eseguibile e' dentro il rumore da multiple-testing. Coerente col soffitto del progetto:
|
||||||
|
"niente di nuovo regge" alla verifica onesta.
|
||||||
|
|
||||||
|
**Migliore soluzione (come FENOMENO da forward-monitor, non deploy):** BTC->QQQ gap overnight — la
|
||||||
|
storia piu' lunga (9/9 anni), lead noto prima dell'open. Da monitorare; deployabile solo con capitale
|
||||||
|
~>$20-30k su micro-futures indice e con i costi notturni modellati.
|
||||||
|
|
||||||
|
## Lezione
|
||||||
|
Anche con 65 agenti e una ricerca esaustiva su mercati/timing/anni, la disciplina onesta
|
||||||
|
(tradabilita' al capitale reale + multiple-testing + beta-vs-alpha) riduce un "Sharpe 2.5 su 9 anni"
|
||||||
|
a un non-edge per noi. Il valore della ricerca: aver QUANTIFICATO e CLASSIFICATO il fenomeno
|
||||||
|
(risk-beta overnight) invece di scambiarlo per alpha.
|
||||||
|
Artefatti: `crypto_lead_harness.py`, `wf_crypto_lead.js`.
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# 2026-06-22 — Crypto × mercati IB: correlazioni e ANTICIPAZIONI (lead-lag)
|
||||||
|
|
||||||
|
## Obiettivo
|
||||||
|
Cercare correlazioni e soprattutto ANTICIPAZIONI tra crypto e mercati IB: un mercato fa capire
|
||||||
|
l'andamento dell'altro? Dati: cache su disco (BTC/ETH Deribit 1h->1d UTC; ETF eq_* con OPEN). Nessun
|
||||||
|
IB online. Disciplina: attenzione ai tranelli di timing daily (crypto chiude 00:00 UTC, US equity
|
||||||
|
21:00 -> lag-0 contaminato), test del segno + OOS + multiple-testing.
|
||||||
|
Script: `crypto_macro_leadlag.py`, `crypto_weekend_signal.py`.
|
||||||
|
|
||||||
|
## (1) Correlazione contemporanea
|
||||||
|
Crypto = asset RISK-ON: corr BTC/ETH ~ **+0.32/0.37** con SPY/QQQ/IWM, **+0.25/0.28** con HYG
|
||||||
|
(credito), **+0.13** GLD, **~-0.02** TLT (bond). Atteso.
|
||||||
|
|
||||||
|
## (2) Lead-lag giornaliero: NIENTE
|
||||||
|
corr(BTC_{t-k}, ETF_t) ha picco a **k=0** (~0.32) e crolla a rumore (±0.05) per |k|>=1. Al daily
|
||||||
|
**nessuno anticipa l'altro** (ne' crypto->equity ne' viceversa). Honest negative.
|
||||||
|
|
||||||
|
## (3) EFFETTO WEEKEND: anticipazione PULITA, significativa, OOS-robusta
|
||||||
|
La crypto si muove Sab+Dom (azionario chiuso) -> quel movimento e' info PRIOR al lunedi'.
|
||||||
|
- **Anticipa il GAP del lunedi'**: corr +0.22/+0.24 (SPY/QQQ/IWM/HYG), hit 59-62%, e **si RAFFORZA
|
||||||
|
OOS (2022+): +0.30/+0.36**. Coerente su 4 ETF (non cherry-pick).
|
||||||
|
- Intraday del lunedi' (open->close) piu' debole ma presente (corr 0.10-0.15, OOS 0.18-0.22).
|
||||||
|
|
||||||
|
### Validazione avversariale
|
||||||
|
- **(A) INCREMENTALE vs venerdi'**: regressione `Mon ~ weekend_crypto + friday_eq`. Coeff weekend
|
||||||
|
crypto significativo ovunque (QQQ gap **t=+4.7**, intr t=+2.9; SPY +4.4/+2.0; IWM +4.7/+2.7);
|
||||||
|
friday_eq NON significativo. -> e' info CRYPTO-SPECIFICA del weekend, non momentum equity.
|
||||||
|
- **(B) TRADABILE** (osservo weekend crypto Dom 24:00, entro Monday OPEN, esco CLOSE, net 4bps):
|
||||||
|
| ETF | hit | Sharpe FULL / IS / OOS22+ | long-flat OOS | ann |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| QQQ | 60% | 1.46 / 1.61 / 1.33 | **1.91** | ~+9-11% |
|
||||||
|
| SPY | 60% | 0.96 / 0.91 / 1.01 | 1.70 | ~+5% |
|
||||||
|
| IWM | 56% | 0.89 / 0.73 / 1.04 | 1.07 | ~+6% |
|
||||||
|
|
||||||
|
## Verdetto
|
||||||
|
**Trovata UNA anticipazione reale**: il weekend crypto anticipa il lunedi' azionario (massimo su QQQ,
|
||||||
|
risk-on/tech). Significativa (t>4 sul gap), incrementale al venerdi', tradabile net costi, **regge e
|
||||||
|
si rafforza OOS**, coerente su piu' ETF. Meccanismo economico sensato: crypto = proxy 24/7 del
|
||||||
|
risk-sentiment; nel weekend l'equity e' chiuso e lunedi' "recupera" la direzione crypto.
|
||||||
|
|
||||||
|
### Caveat onesti
|
||||||
|
- **Capacita' bassa**: ~52 lunedi'/anno, intraday -> ~+9%/yr sul capitale impiegato il lunedi', non
|
||||||
|
una macchina da compounding. E' un segnale TATTICO, non un cornerstone.
|
||||||
|
- Il GAP (t=4.7) e' piu' forte dell'intraday (t=2.9) ma per catturarlo serve entrare PRIMA del Monday
|
||||||
|
open -> via **futures indice IB (MNQ/MES, aperti la domenica sera)**: enhancement eseguibile da
|
||||||
|
validare (cattura gap+sessione).
|
||||||
|
- Multiple-testing 3 ETF x 2 target: ma TUTTI significativi e coerenti -> effetto ampio, non fortuna.
|
||||||
|
- Niente IB online qui (cache); per il deploy servirebbe il feed crypto live la domenica sera.
|
||||||
|
|
||||||
|
## Prossimo (se si procede)
|
||||||
|
Validare la variante FUTURES (MNQ domenica sera -> cattura il gap del lunedi') e il sizing a basso
|
||||||
|
capitale; eventualmente paper-trade. E' la prima ANTICIPAZIONE cross-mercato trovata: crypto come
|
||||||
|
lead di sentiment sul lunedi' equity.
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# 2026-06-22 — Combo DEPLOYABLE: TP01 (Deribit) + GTAA (IB)
|
||||||
|
|
||||||
|
## Perche'
|
||||||
|
Il combo crypto-pieno (TP01+XS01+VRP01)+GTAA diversificava (Sharpe 1.81), ma XS01/VRP01 sono
|
||||||
|
STAT-MODE (non eseguibili a $600). Validazione del combo ONESTO/eseguibile: solo le gambe deployable
|
||||||
|
a basso capitale — TP01 (gia' armato live su Deribit) + GTAA vt12 (eseguibile su IB, frazioni,
|
||||||
|
switch mensile). `eq_tp01_gtaa_combo.py`. TP01 compoundato sul calendario giorni-di-borsa.
|
||||||
|
|
||||||
|
## Risultati (finestra comune 2019-03 .. 2026-06, ~7y)
|
||||||
|
| | Sharpe | CAGR | volAnn | maxDD |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| TP01 (crypto, Deribit) | 1.25 | 16.4% | 12.9% | 14% |
|
||||||
|
| GTAA vt12 (equity, IB) | 1.12 | 6.0% | 5.3% | 8% |
|
||||||
|
| **blend 50/50** | **1.48** | 11.3% | 7.5% | **8%** |
|
||||||
|
| blend 40/60 (best cap-mix) | 1.52 | 10.2% | 6.6% | 8% |
|
||||||
|
| risk-parity (29c/71e) | 1.52 | 9.1% | 5.9% | 8% |
|
||||||
|
|
||||||
|
**corr TP01<->GTAA = +0.21**. Il blend (1.48-1.52) batte entrambe le gambe (best solo 1.25),
|
||||||
|
maxDD 14%->8%. **DIVERSIFICA anche da deployable.**
|
||||||
|
|
||||||
|
## Caveat onesti
|
||||||
|
- Per-anno 50/50: 2019 2.11, 2020 2.51, 2021 1.66, **2022 -2.64**, 2023 1.40, 2024 1.73, 2025 0.98,
|
||||||
|
2026 0.94. Anni boom iniziali gonfiano lo Sharpe assoluto; il **2022 e' negativo** (trend whipsaw
|
||||||
|
su entrambe le gambe nel bear). Recenti ~0.95. -> il numero robusto e' il GUADAGNO da
|
||||||
|
diversificazione (+0.27 Sharpe del blend vs solo), non il livello assoluto.
|
||||||
|
- **Costo deployability**: crypto-pieno+GTAA = 1.81 vs deployable = 1.48. I ~0.33 di Sharpe persi sono
|
||||||
|
cio' che XS01/VRP01 darebbero se eseguibili (servirebbe ~20k).
|
||||||
|
- **Cross-venue** Deribit+IB: due conti, capitale split. Entrambe switch mensile/basso turnover,
|
||||||
|
frazionabili a $0.5-2k.
|
||||||
|
|
||||||
|
## Verdetto
|
||||||
|
Combo deployable VALIDO: due trend difensivi scorrelati (corr 0.21) su mercati diversi -> Sharpe
|
||||||
|
~1.5 / maxDD ~8%, meglio di ciascuna gamba. E' il candidato concreto per un paper-trade cross-venue.
|
||||||
|
NON risolve EUR50/g (resta capitale), ma e' la migliore configurazione rischio-aggiustata
|
||||||
|
EFFETTIVAMENTE eseguibile trovata finora. Lezione cross-mercato confermata: il salto di qualita' non
|
||||||
|
e' un nuovo alpha ma un SECONDO mercato scorrelato.
|
||||||
|
|
||||||
|
## Prossimo (se si procede)
|
||||||
|
Paper-trade della gamba GTAA su IB (forward-only, come paper_trend per TP01), per validare
|
||||||
|
l'esecuzione cross-venue a rischio zero prima di qualunque capitale reale.
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# 2026-06-22 — EQ-GTAA01 (trend multi-asset) + COMBO cross-mercato equity×crypto
|
||||||
|
|
||||||
|
## (1) EQ-GTAA01 — trend difensivo multi-asset (GTAA)
|
||||||
|
|
||||||
|
EQ-TREND01 (trend su SPY) taglia il DD. Diversificare le SORGENTI di trend (azioni US/tech/small +
|
||||||
|
bond + oro + high-yield) migliora il rischio-aggiustato. `eq_gtaa_trend.py`: ogni asset gestito col
|
||||||
|
proprio trend long-flat (TSMOM multi-orizzonte), equal-weight tra gli asset disponibili (outer-join,
|
||||||
|
cash dove off/assente). Universo SPY/QQQ/IWM/TLT/GLD/HYG. Causale, netto fee, OOS 2015+.
|
||||||
|
|
||||||
|
| strategia | CAGR | Sharpe (pre15/OOS) | maxDD | corr SPY |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| SPY buy&hold | 9.7% | 0.58 (0.45/0.82) | 55% | — |
|
||||||
|
| EW statico (no trend) | 9.4% | 0.59 | 62% | 0.89 |
|
||||||
|
| SPY-trend mono | 5.5% | 0.56 (/0.78) | 30% | 0.72 |
|
||||||
|
| **GTAA lf vt12%** | 3.8% | **0.64** (0.53/**0.89**) | **15%** | **0.64** |
|
||||||
|
| GTAA vt12 (6-asset, 2016+) | 5.6% | **1.08** | **8%** | 0.60 |
|
||||||
|
|
||||||
|
DD nei bear (GTAA vs SPY): dot-com 32%/49% · GFC **14%/55%** · COVID **10%/34%** · 2022 11%/24%.
|
||||||
|
Marginale vs SPY: corr 0.64; 50/50 uplift +0.041 FULL / **+0.086 OOS** (meglio del mono-SPY). Plateau
|
||||||
|
stabile (Sh 0.55-0.61, DD 25-35%). **Migliore sleeve equity**: Sharpe più alto, maxDD bassissimo
|
||||||
|
(8-15%), corr SPY più bassa (0.64) = diversificatore migliore. Tradeoff: CAGR molto più basso
|
||||||
|
(fortemente difensiva). Caveat: la finestra 6-asset (Sh 1.08) è tutta OOS ma un solo regime (toro).
|
||||||
|
|
||||||
|
## (2) COMBO cross-mercato — equity-trend × crypto
|
||||||
|
|
||||||
|
La via che alza il Sharpe COMPLESSIVO senza nuovo alpha: combinare due book scorrelati.
|
||||||
|
`eq_crypto_combo.py`: crypto = portafoglio attivo TP01+XS01+VRP01 (`StrategyPortfolio.combined_daily`,
|
||||||
|
rinormalizzato); equity = GTAA lf vt12%. Crypto compoundato sul calendario giorni-di-borsa (cattura
|
||||||
|
i weekend). Finestra comune = era crypto (2019-03 .. 2026-06, 1827 giorni di borsa).
|
||||||
|
|
||||||
|
| | Sharpe | CAGR | volAnn | maxDD |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| crypto TP01+XS01+VRP01 | 1.60 | 18.7% | 11.1% | 14% |
|
||||||
|
| equity GTAA vt12 | 1.12 | 6.0% | 5.3% | 8% |
|
||||||
|
| **blend 50/50** | **1.81** | 12.4% | 6.6% | **7%** |
|
||||||
|
| risk-parity (32c/68e) | 1.78 | 10.1% | 5.5% | 8% |
|
||||||
|
|
||||||
|
**Correlazione crypto↔equity = +0.167** (bassissima). Il blend 50/50 fa **Sharpe 1.81 > di ciascuno**
|
||||||
|
(crypto 1.60, equity 1.12), **maxDD dimezzato 14%→7%**. VERDETTO: DIVERSIFICA (blend > miglior solo
|
||||||
|
di +0.21 Sharpe). È il guadagno STRUTTURALE: due fonti di rischio scorrelate alzano il Sharpe
|
||||||
|
complessivo senza cercare un nuovo edge.
|
||||||
|
|
||||||
|
### Caveat onesti
|
||||||
|
- **Finestra crypto corta (~7y) e favorevole**: il crypto Sharpe 1.60 e' alto (regime toro + XS01/VRP01
|
||||||
|
STAT-MODE a storia corta). Gli SHARPE ASSOLUTI sono ottimistici. Ma il PUNTO della diversificazione
|
||||||
|
(corr 0.17, blend > solo, DD dimezzato) è robusto al livello assoluto.
|
||||||
|
- **Cross-venue**: crypto su Deribit, equity su IB → due conti, due percorsi d'esecuzione. A $0.5-2k
|
||||||
|
totali, ogni sleeve è minuscola. La parte equity (GTAA) e la TP01 sono entrambe eseguibili a basso
|
||||||
|
capitale; XS01/VRP01 restano STAT-MODE (il blend "reale" deployable è ~TP01 + GTAA).
|
||||||
|
|
||||||
|
## Lettura strategica
|
||||||
|
Il fronte equity da' due cose: (a) una sleeve difensiva robusta (GTAA, maxDD ~10%), (b) — piu'
|
||||||
|
importante — un DIVERSIFICATORE quasi-scorrelato al crypto che alza il Sharpe del portafoglio
|
||||||
|
complessivo da ~1.6 a ~1.8. Non risolve €50/g (resta capitale), ma e' il primo miglioramento
|
||||||
|
STRUTTURALE del rischio-aggiustato complessivo trovato in tutta la ricerca post-reset, ed e' del tipo
|
||||||
|
giusto (diversificazione vera, non alpha fittizio). Prossimo: validare il combo deployable TP01+GTAA
|
||||||
|
(solo le due gambe eseguibili), e valutare l'operativita' cross-venue.
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# 2026-06-22 — Fronte EQUITY aperto + EQ-MOM01 (momentum settoriale): NON batte SPY
|
||||||
|
|
||||||
|
## Apertura fronte (branch research/equities-ib)
|
||||||
|
|
||||||
|
Le 4 ondate crypto hanno esaurito gli angoli su BTC/ETH (soffitto ~1.3). L'unico modo di superarlo è
|
||||||
|
un **mercato diverso**. Aperto il fronte azioni/ETF via IB (paper, `gnzsnz/ib-gateway`, read-only).
|
||||||
|
|
||||||
|
**Dati certificati + cache su disco** (`fetch_ib_equities.py` → `data/raw/eq_*.parquet`, ADJUSTED_LAST
|
||||||
|
div+split, gitignored = cache locale; loader `eqlib.py` con lru_cache → ricerca legge da disco, MAI
|
||||||
|
da IB). Universo: 9 SPDR settoriali classici dal **1998 (27.5y)** + XLRE(2015)/XLC(2018) + SPY(1996,
|
||||||
|
30y)/QQQ/IWM/GLD/HYG/TLT. Tutti integri (monotoni, no dup, no spike>50%, 0 gap lunghi).
|
||||||
|
NB bug timestamp risolto: `pd.Timestamp` a risoluzione µs → salvati in secondi, corretti a ms.
|
||||||
|
|
||||||
|
## EQ-MOM01 — momentum cross-sectional settoriale
|
||||||
|
|
||||||
|
Costruzione causale (`eq_sector_momentum.py`): ogni 21g, momentum = blend lookback [63,126,252]g con
|
||||||
|
skip-21 (12-1 classico), z-score cross-sectional. long-only top-k (full-invested, confronto
|
||||||
|
like-for-like con SPY) e long-short (dollar-neutral, test alpha puro). Netto fee, hold-out OOS 2015+.
|
||||||
|
|
||||||
|
### Risultati (9 settori, 1998-2026)
|
||||||
|
| strategia | CAGR | Sharpe (pre15/OOS15+) | maxDD | corr SPY |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| **SPY buy&hold** | 8.2% | **0.51** (0.31/0.82) | 55% | — |
|
||||||
|
| EW 9 settori | 8.9% | 0.56 (0.44/0.76) | 53% | 0.96 |
|
||||||
|
| MOM long top-3 | 7.7% | 0.50 (0.32/0.76) | 47% | 0.85 |
|
||||||
|
| MOM long vol-target 15% | 7.3% | 0.52 | 39% | 0.75 |
|
||||||
|
| **MOM long-short top-3** | −0.9% | **−0.08** (−0.19/0.08) | 32% | −0.20 |
|
||||||
|
|
||||||
|
### Verdetto: NESSUN edge vs SPY
|
||||||
|
- **Long-short Sharpe −0.08** → l'alpha cross-sectional di momentum settoriale è **morto** su 27 anni
|
||||||
|
(decadimento post-2000 noto in letteratura). Niente alpha market-neutral.
|
||||||
|
- **Long-only ≈ SPY**: corr 0.85, **uplift marginale ~0.00** (blend 75/25 +0.012 FULL / +0.001 OOS;
|
||||||
|
50/50 +0.015 / −0.010). È un SPY a beta più basso, non un edge. Plateau stabile ma sempre ~0.50
|
||||||
|
(vs SPY 0.51); sugli 11 settori (2018+) fa peggio (0.69 vs 0.82). Fee-robusto (ma niente da salvare).
|
||||||
|
- L'unico beneficio (maxDD 55%→39%) è del **vol-target**, non del momentum (lo daresti a SPY stesso).
|
||||||
|
|
||||||
|
## Lezione (coerente col progetto)
|
||||||
|
Il momentum **relative-value** è morto anche in equity, come nel crypto (ortho wave). Il baseline
|
||||||
|
equity da battere è SPY buy&hold (Sharpe ~0.51 full / 0.82 OOS), ostico come il toro crypto.
|
||||||
|
|
||||||
|
## Prossimo angolo plausibile (NON ancora testato)
|
||||||
|
L'analogo equity di TP01 (l'unica cosa che ha retto nel crypto = trend DIFENSIVO): **time-series
|
||||||
|
trend su SPY long-flat/long-bonds** — non per battere il CAGR ma per **tagliare il 55% di drawdown**
|
||||||
|
restando vicino al ritorno. È il punto dove vive il valore robusto in equity (e dove il cross-section
|
||||||
|
NON guarda). Da provare con lo stesso gauntlet: marginale vs SPY, OOS lungo, plateau.
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# 2026-06-22 — EQ-TREND01: trend DIFENSIVO su SPY = edge difensivo REALE (analogo di TP01)
|
||||||
|
|
||||||
|
## Contesto
|
||||||
|
Il momentum cross-sectional settoriale è morto (EQ-MOM01: long-short Sharpe −0.08, long-only ≈ SPY).
|
||||||
|
Ma nel crypto l'unica cosa che ha retto NON era relative-value: era **TP01**, un trend DIFENSIVO che
|
||||||
|
taglia il drawdown. L'equity ha lo stesso buco: SPY buy&hold Sharpe ~0.54 ma maxDD **55%**.
|
||||||
|
|
||||||
|
## Costruzione (causale, stile TP01)
|
||||||
|
`eq_spy_trend.py`. TSMOM multi-orizzonte [21,63,126,252]g, target = frazione di orizzonti in
|
||||||
|
trend-up (allocazione graduale 0..1), opz. vol-target. Posizione decisa a ≤i-1, tenuta da i. Netto
|
||||||
|
fee. Varianti: long-flat (cash in risk-off), long-bonds (TLT, solo 2016+), SMA-200 (Faber). Dati da
|
||||||
|
cache eqlib (ADJUSTED, nessun IB). Periodo 1997-2026, OOS 2015+.
|
||||||
|
|
||||||
|
## Risultati
|
||||||
|
| strategia | CAGR | Sharpe (pre15/OOS) | maxDD | in-mkt |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| SPY buy&hold | 9.0% | 0.54 (0.38/0.82) | 55% | 99% |
|
||||||
|
| **SMA-200 (Faber)** | 7.0% | **0.65** (0.52/0.88) | **29%** | 76% |
|
||||||
|
| TSMOM lf cap1.0 | 5.7% | 0.57 (0.44/0.78) | 30% | 92% |
|
||||||
|
| TSMOM lf vt15% | 5.7% | 0.62 (0.51/0.78) | **25%** | 92% |
|
||||||
|
|
||||||
|
**Drawdown nei bear (TSMOM vs SPY):** dot-com 26%/49% · GFC **19%/55%** · COVID 17%/34% · 2022 16%/24%.
|
||||||
|
|
||||||
|
**Plateau** (long-flat): ogni config Sharpe 0.56-0.65 (> SPY 0.54), maxDD 25-31% (~metà di SPY).
|
||||||
|
SMA-200 il più semplice E il migliore (Sh 0.65, OOS 0.88, DD 29%). **Fee-robusto** (Sh 0.48 a
|
||||||
|
0.10%/lato), basso turnover.
|
||||||
|
|
||||||
|
**Marginale vs SPY:** corr 0.73. blend 50/50 uplift +0.035 FULL / +0.031 OOS (modesto positivo);
|
||||||
|
100% trend uplift −0.012 / −0.041 (nel toro recente la difesa costa).
|
||||||
|
|
||||||
|
## Verdetto: edge DIFENSIVO reale (non alpha) — analogo di TP01
|
||||||
|
- ✅ Sharpe 0.54→0.62/0.65, **maxDD dimezzato** (55%→~27%, nei bear lenti più che dimezzato),
|
||||||
|
plateau robusto, fee-robusto, **eseguibile a $0.5-2k** (switch mensile SPY/cash).
|
||||||
|
- ⚠️ NON genera ritorno (CAGR −2/3pp): è risk-management, come TP01.
|
||||||
|
- ⚠️ I tagli grossi (dot-com/GFC) sono IN-SAMPLE; l'OOS 2015-26 è quasi tutto toro → lì ha seguito
|
||||||
|
SPY a beta minore (ma COVID, OOS, dimezzato). La difesa "serve" nei bear, rari nell'OOS.
|
||||||
|
- ⚠️ long-bonds (TLT) non convince (TLT distrutto 2022).
|
||||||
|
|
||||||
|
## Lettura strategica
|
||||||
|
Primo positivo del fronte equity, e dello stesso TIPO che ha retto nel crypto: trend difensivo, non
|
||||||
|
relative-value. Conferma la lezione cross-mercato: **il valore robusto è nel ridurre il rischio
|
||||||
|
(trend long-flat), non nel battere il buy&hold**. Da solo non risolve €50/g (problema di capitale).
|
||||||
|
|
||||||
|
## Prossimo angolo plausibile
|
||||||
|
**Trend multi-asset / GTAA** sull'universo ETF in cache (SPY/QQQ/IWM + TLT/GLD/HYG): un portafoglio
|
||||||
|
di trend long-flat su classi d'attivo diverse di solito batte il trend mono-SPY sul rischio-aggiustato
|
||||||
|
(diversificazione dei trend). + domanda cross-mercato: la sleeve equity-trend DIVERSIFICA il
|
||||||
|
portafoglio crypto (TP01+XS01+VRP01)? (esecuzione split Deribit+IB).
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
# 2026-06-22 — Funding-CARRY cross-sectional su Hyperliquid (FC01): LEAD fragile, NON regge
|
||||||
|
|
||||||
|
## Contesto
|
||||||
|
|
||||||
|
Onda "nuova ricerca mirata" (l'utente ha chiesto di cercare un angolo non coperto dalle due grandi
|
||||||
|
ondate — sweep 104-ipotesi e ortho relative-value, entrambe esaurite sul *prezzo* BTC/ETH). L'unico
|
||||||
|
meccanismo con una **fonte di ritorno diversa** non ancora testato su dati certi è il **carry da
|
||||||
|
funding**: incassare il cashflow dei perp stando delta-neutral.
|
||||||
|
|
||||||
|
### Scan di fattibilità dati (prima di tutto, lezione v2.0.0)
|
||||||
|
- **Funding price-clock** (drift attorno agli stamp 00/08/16) sul feed Deribit certificato →
|
||||||
|
già testato nell'onda intraday (`agent_03_funding_clock_15m`) = **FAIL** ("il funding è un cashflow
|
||||||
|
perp-vs-spot; il prezzo index non ha drift tradabile attorno allo stamp al netto del trend").
|
||||||
|
- **Funding carry su Deribit** (dove eseguiamo) → ccxt `fetch_funding_rate_history` = **0 righe**
|
||||||
|
(bloccato), Cerbero MCP espone solo `get_historical` (candele), endpoint funding = 404.
|
||||||
|
- **Funding carry su Hyperliquid** → API pubblica `/info {"type":"fundingHistory"}` = **disponibile**,
|
||||||
|
oraria, tokenless, serie native dal 2023-05. HL è già l'universo certificato di XS01.
|
||||||
|
|
||||||
|
### Dato scaricato e certificato
|
||||||
|
`scripts/research/fetch_hl_funding.py` (backoff anti-429) → **19 major** (gli stessi di XS01),
|
||||||
|
`data/raw/hlfund_<sym>_1h.parquet`. Certificazione: cadenza ~1h, **0 gap**, copertura 98-100%,
|
||||||
|
funding annualizzato per asset da **APT +1.0%** a **NEAR +21.6%** (mediana ~+11.7%). Pochi `cap_hit`
|
||||||
|
(ore con |funding|>0.06%/h) su INJ/TIA/SEI, plausibili in alt ad alta vol. Dato pulito.
|
||||||
|
|
||||||
|
## Ipotesi e costruzione (FC01)
|
||||||
|
|
||||||
|
Book dollar-neutral che **SHORTA i k perp ad alto funding** e **COMPRA i k a basso** → incassa il
|
||||||
|
premio (chi è long paga il funding). Ritorno perp per un long = `price_ret − funding`. Causale come
|
||||||
|
XS01: ogni H=10 giorni, segnale = media causale del funding giornaliero realizzato sugli ultimi L
|
||||||
|
giorni (shift 1), rank cross-section, vol-target 20%, fee 0.05%/lato sul turnover.
|
||||||
|
`scripts/research/funding_carry_hl.py`. Domanda chiave: **edge reale e ORTOGONALE a XS01**, o XS01
|
||||||
|
travestito? (gli alt ad alto funding sono spesso i pompati = quelli che XS01 *compra*; qui li
|
||||||
|
*shortiamo* → potenziale anti-correlazione, oppure il carry domina).
|
||||||
|
|
||||||
|
## Risultati
|
||||||
|
|
||||||
|
### Premio reale ma direzione-dipendente
|
||||||
|
`carry` (short alto-funding) batte sistematicamente `anti` (long alto-funding, sempre molto negativo)
|
||||||
|
→ **il premio di funding esiste**: shortare i perp ad alto funding paga, in aggregato.
|
||||||
|
|
||||||
|
### Ma il book NON regge il gauntlet (19 asset, 2024-2026, 904g)
|
||||||
|
- **Standalone base (L=7 k=5): FULL Sharpe −0.12, in-sample 0.44, HOLD −0.50, DD 28.6%**, −2.8%/anno.
|
||||||
|
Decadimento netto: 2024 **+0.44** → 2025 −0.06 → 2026 **−1.42**.
|
||||||
|
- Correlazioni: TP01 −0.02, **XS01 −0.19** (ortogonale, come da ipotesi — NON è XS01 travestito),
|
||||||
|
VRP01 +0.05.
|
||||||
|
- **`marginal_vs_tp01` = DILUTES**: `has_insample_edge=False` (in-sample 0.44 < 0.5),
|
||||||
|
`multicut_persistent=False`, blend w25 uplift FULL −0.21 / HOLD −0.39.
|
||||||
|
- **Non aggiunge a XS01**: uplift w25 FULL −0.04 / HOLD −0.19.
|
||||||
|
|
||||||
|
### Il colpo di grazia: FRAGILITÀ all'universo
|
||||||
|
Un preview su 17 asset (mancavano NEAR e AAVE) dava FULL **+0.62**, ADDS, +0.22 uplift — un PASS
|
||||||
|
tentatore. Sui 19 completi: **DILUTES**. Jackknife lascia-fuori-uno (base L=7 k=5):
|
||||||
|
|
||||||
|
```
|
||||||
|
19 asset: FULL -0.12 HOLD -0.50
|
||||||
|
-SUI FULL -0.39 ... -BTC FULL +0.17
|
||||||
|
-SEI FULL -0.31 -AAVE FULL +0.26
|
||||||
|
-BNB FULL -0.29 -NEAR FULL +0.30
|
||||||
|
=> FULL oscilla in [-0.39, +0.30] togliendo UN solo asset (range 0.70), attraversa lo zero.
|
||||||
|
```
|
||||||
|
|
||||||
|
Togliere **NEAR o AAVE** (i due assenti nel preview) **recupera il segno** → il preview era fortunato
|
||||||
|
*proprio* perché quei due non c'erano ancora. **Un edge robusto non cambia segno per un singolo nome.**
|
||||||
|
Le poche celle "buone" del plateau (es. L=7 k=3: HOLD 0.91) hanno **in-sample debole + hold-out forte**
|
||||||
|
= la firma del hold-out-luck che la metodologia indurita uccide.
|
||||||
|
|
||||||
|
## Perché fallisce (meccanismo)
|
||||||
|
|
||||||
|
Tensione fondamentale **carry vs momentum**: il funding-carry shorta i forti (alto funding = domanda
|
||||||
|
long aggressiva), ma in un mercato alt toro i forti **continuano a correre** (NEAR/AAVE: alto funding
|
||||||
|
*e* prezzo su → shortarli perde più del premio incassato). Il premio di funding è reale in aggregato,
|
||||||
|
ma il book cross-sectional equal-weight top-k è dominato da pochi nomi a funding estremo che *anche*
|
||||||
|
trendano, e su 2.5 anni / 19 nomi questo basta a ribaltare il segno.
|
||||||
|
|
||||||
|
## Verdetto
|
||||||
|
|
||||||
|
**FC01 NON è uno sleeve.** Né deploy (è STAT-MODE: 10 gambe market-neutral, non eseguibile a $600),
|
||||||
|
né lead affidabile: fragile all'universo (sign-flip su un nome), DILUTES vs TP01, non aggiunge a XS01,
|
||||||
|
in-sample edge < 0.5, niente persistenza multi-cut, decadimento 2026. Conferma — di nuovo — il
|
||||||
|
soffitto del progetto: promettente su un sottoinsieme fortunato, collassa sotto il gauntlet onesto.
|
||||||
|
**Win metodologico:** lo scorer indurito + il jackknife d'universo hanno intercettato un falso
|
||||||
|
positivo che il preview a 17 asset avrebbe promosso.
|
||||||
|
|
||||||
|
## Lascito / lavoro futuro (NON inseguire ora)
|
||||||
|
- I 19 parquet funding (`hlfund_*`) restano certificati per ricerca futura. Il fetcher NON va in cron
|
||||||
|
(FC01 fallito → niente da monitorare in forward).
|
||||||
|
- Idee se mai si tornasse sul carry (NON ora): (a) **gate sul LIVELLO** di funding (short solo quando
|
||||||
|
estremo, regime-filter alla VRP01 IV-rank) invece dello short-top-k incondizionato; (b) cap sul peso
|
||||||
|
per-nome / neutralizzazione momentum per togliere il dominio NEAR/AAVE. Entrambe rischiano
|
||||||
|
overfitting su storia corta — soglia di prova alta.
|
||||||
|
|
||||||
|
## Nota IB (thread parallelo, stessa sessione)
|
||||||
|
Esplorato come fonte per il **basis CME crypto** (cugino eseguibile del carry). Gateway paper
|
||||||
|
`gnzsnz/ib-gateway` su `127.0.0.1:4002` (read-only, `docker-compose.yml`), sonda `ib_probe.py`.
|
||||||
|
Esito dati: **backtest del basis NON fattibile** (ContFuture back-adjusted; contratti scaduti = 1
|
||||||
|
barra). IB resta valido per esecuzione/forward, non per scoprire l'edge. Dettagli nel corpo sessione.
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# 2026-06-23 — Combo DEPLOYABLE in PAPER: TP01 (Deribit) + GTAA (IB), cross-venue
|
||||||
|
|
||||||
|
## Decisione
|
||||||
|
Dopo aver esaurito (onestamente) la ricerca di nuovi edge e anticipazioni cross-mercato, l'unica cosa
|
||||||
|
VERA e deployabile e' la DIVERSIFICAZIONE: TP01 (crypto, Deribit) + GTAA (equity, IB), corr ~0.21 ->
|
||||||
|
blend Sharpe ~1.5, maxDD dimezzato (diari 2026-06-22-deployable-combo). Si va in PAPER cross-venue.
|
||||||
|
|
||||||
|
## Costruito
|
||||||
|
- **`src/portfolio/gtaa.py`** — GTAA come sleeve di prima classe: trend difensivo long-flat TSMOM
|
||||||
|
[21/63/126/252g], vol-target 12%, EW su SPY/QQQ/IWM/TLT/GLD/HYG. Espone `gtaa_returns()` (Sharpe
|
||||||
|
full 0.64, 7542 barre 1996+) e `gtaa_weights()` (pesi ETF CORRENTI azionabili). Legge cache eq_*.
|
||||||
|
- **`scripts/live/paper_combo.py`** — paper-tracker FORWARD-ONLY del blend 50/50 TP01+GTAA (crypto
|
||||||
|
compoundato sul grid giorni-di-borsa). Stato in data/paper_combo/. Mostra le posizioni azionabili
|
||||||
|
su entrambi i venue. SOLO le gambe eseguibili (XS01/VRP01 STAT-MODE esclusi).
|
||||||
|
- **`fetch_ib_equities.py --only SPY,QQQ,...`** — refresh mirato dei 6 ETF GTAA (per il cron).
|
||||||
|
- **`scripts/cron_daily.sh`** — aggiunto: up gateway IB (idempotente) -> refresh ETF GTAA ->
|
||||||
|
avanza paper_combo. Dipendenza cross-venue gestita (gateway paper sempre-up, restart unless-stopped).
|
||||||
|
|
||||||
|
## Stato iniziale (2026-06-23)
|
||||||
|
Paper combo init a 2000, forward da 2026-06-22. Posizioni azionabili:
|
||||||
|
- TP01 (Deribit): BTC/ETH 0.0x (flat, TSMOM risk-off — coerente col live).
|
||||||
|
- GTAA (IB): SPY 13% / QQQ 8% / IWM 9% / TLT 17% / GLD 2% / HYG 17% / cash 34% (difensivo).
|
||||||
|
Catena end-to-end testata: gateway -> refresh ETF -> avanza paper. OK.
|
||||||
|
|
||||||
|
## Onesta'
|
||||||
|
- E' PAPER (rischio zero). Valida l'OPERATIVITA' cross-venue prima di capitale reale.
|
||||||
|
- Sharpe atteso ~1.5 e' ottimistico (finestra crypto corta/favorevole); il dato robusto e' la
|
||||||
|
diversificazione (corr 0.21, DD dimezzato), non il livello assoluto.
|
||||||
|
- A capitale reale e' un portafoglio su DUE conti (Deribit ~$600 + IB); GTAA frazionabile a basso
|
||||||
|
capitale, TP01 gia' armato. Prossimo passo eventuale: dashboard del combo + (molto dopo) capitale.
|
||||||
|
|
||||||
|
## Prossimo
|
||||||
|
Lasciar girare il paper forward (cron giornaliero) e ricontrollare l'equity tra qualche settimana.
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# 2026-06-23 — Cross-market crypto-lead OLTRE l'SP500: bond, commodity, indici esteri -> niente
|
||||||
|
|
||||||
|
## Obiettivo
|
||||||
|
Estendere il test "crypto anticipa il mercato?" oltre SP500/azionario USA: commodity, bond, indici
|
||||||
|
ESTERI (Europa/Asia, fasi orarie diverse = il caso a priori piu' favorevole a un lead vero).
|
||||||
|
|
||||||
|
## Dati (IB, orari, cache fut_*_1h)
|
||||||
|
ES/NQ/RTY (gia'); + ZN (T-note 10y), ESTX50 (Euro Stoxx50), DAX, NKD (Nikkei). Storia ~2-2.4y (2024+).
|
||||||
|
Commodity GC/CL/HG: VUOTE (market-data subscription COMEX/NYMEX mancante sul paper) -> non testate.
|
||||||
|
|
||||||
|
## Test (`fut_leadlag_generic.py`): crypto[T-8h->T] -> future[T->T+6h], non-sovrapposto, controllo=moto proprio future
|
||||||
|
- ES/NQ/RTY: nessun edge (gia' noto).
|
||||||
|
- ZN (bond): NEGATIVO (0/3 anni).
|
||||||
|
- NKD (Nikkei): debole (t_crypto 0.2, Sharpe 0.66 ~ overnight drift, non crypto).
|
||||||
|
- **ESTX50 / DAX: forte all'apparenza** — BTC->T0h: t_crypto ~7.8, Sharpe 2.5, ann ~22%, 3/3 anni.
|
||||||
|
|
||||||
|
## Ma e' un ARTEFATTO DI CONFINE UTC (deep-dive `eu_overnight_deepdive.py`)
|
||||||
|
- **Picco a coltello a T=00:00 UTC**: t/Sharpe salgono T20->T0 (2.5->7.8 / 0.24->2.45) e CROLLANO a
|
||||||
|
T=1h (t 1.3, Sharpe -0.09). Un lead vero non e' a coltello su una sola ora.
|
||||||
|
- **GAP test**: inserendo 1h tra fine-segnale (00:00) e inizio-cattura, l'effetto MUORE
|
||||||
|
(Sharpe 2.45 -> -0.52, t 7.8 -> 1.6).
|
||||||
|
- **Singola ora**: T=0h/H=1h (cattura 00:00->01:00) Sharpe +2.93 (t 8.7); T=1h/H=1h (01:00->02:00)
|
||||||
|
Sharpe -1.02. L'INTERO "edge" e' la barra di confine 00:00->01:00.
|
||||||
|
- vs SEMPRE-LONG: always-long overnight e' negativo (-0.8/-1.2), quindi non e' overnight-drift; ma
|
||||||
|
l'uplift del crypto e' tutto nella barra di confine.
|
||||||
|
-> Firma esatta di `day_boundary_robust` (CLAUDE.md): effetto che vive/muore spostando il confine del
|
||||||
|
giorno UTC di poche ore = etichettatura/contaminazione, NON anticipazione economica.
|
||||||
|
|
||||||
|
## Verdetto
|
||||||
|
NIENTE di tradabile oltre l'SP500 nemmeno. Su TUTTI i mercati il legame crypto->X e' o co-movimento
|
||||||
|
contemporaneo (risk-beta) o artefatto di confine. L'anticipazione crypto->altri-mercati sfruttabile
|
||||||
|
NON esiste su dati onesti (finestre non-sovrapposte + boundary-robust + gap). Conferma definitiva del
|
||||||
|
soffitto del progetto, ora anche cross-mercato.
|
||||||
|
|
||||||
|
## Cosa resta di valore (immutato)
|
||||||
|
La diversificazione TP01(crypto)+GTAA(equity), corr 0.21 -> Sharpe portafoglio ~1.5, DD dimezzato.
|
||||||
|
Quello e' strutturale e deployabile; l'anticipazione cross-mercato no.
|
||||||
|
Script: fetch_ib_futures.py (multi-exchange), fut_leadlag_generic.py, eu_overnight_deepdive.py.
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# 2026-06-23 — "Monitor Deribit / trade IB": il gap crypto->equity e' LOOK-AHEAD
|
||||||
|
|
||||||
|
## Idea testata (utente)
|
||||||
|
Guardare crypto live su Deribit (24/7) e tradare l'indice su IB sul segnale del gap overnight.
|
||||||
|
|
||||||
|
## Trappola trovata
|
||||||
|
Il segnale crypto [P 21:00 -> D 13:00 UTC] e il "gap" equity [P close -> D open 13:30] coprono QUASI
|
||||||
|
LE STESSE ORE. Condizionare il gap sul crypto-overnight = correlare due ritorni dello STESSO intervallo
|
||||||
|
notturno -> look-ahead. All'entrata (D 13:00, pre-open) il gap e' GIA' avvenuto: non catturabile.
|
||||||
|
|
||||||
|
## Prova (net 2bps, sqrt(252))
|
||||||
|
| target | OVERLAP gap (look-ahead) | TRADABILE intraday (post-entrata) |
|
||||||
|
|---|---|---|
|
||||||
|
| SPY | Sharpe 3.60 (OOS 5.23) | -0.03 (OOS 0.12) |
|
||||||
|
| QQQ | Sharpe 4.01 (OOS 5.47) | 0.25 (OOS 0.43) |
|
||||||
|
| IWM | Sharpe 3.98 (OOS 5.72) | 0.15 (OOS 0.44) |
|
||||||
|
|
||||||
|
Lo "Sharpe 5" e' artefatto. L'edge REALE tradabile via ETF (intraday, entri all'open) ~0, muore a costi.
|
||||||
|
NB: anche i Sharpe "gap" del workflow 65-agenti erano (a) look-ahead di overlap e (b) sotto-annualizzati
|
||||||
|
(sqrt(52) invece di sqrt(252)); il verdetto "non deployabile" resta, rafforzato.
|
||||||
|
|
||||||
|
## Cosa resta possibile (non testato, serve dato)
|
||||||
|
L'unica versione onesta dell'idea: entrare a META' notte via FUTURES IB e vedere se crypto [P21:00->T]
|
||||||
|
predice il future indice [T->open] su finestre NON sovrapposte (crypto come sensore di rischio piu'
|
||||||
|
veloce). Richiede dati INTRADAY dei futures (ES/NQ/RTY), non in cache -> data step se si vuole indagare.
|
||||||
|
|
||||||
|
## Lezione
|
||||||
|
Un risultato "troppo bello" (Sharpe 5) e' un test di disciplina: era overlap di finestre. Catturato.
|
||||||
|
Script: crypto_overnight_equity.py (versione artefatto), crypto_overnight_honest.py (decomposizione).
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# 2026-06-23 — "Monitor Deribit / trade IB" su futures: test ONESTO non-sovrapposto -> edge ~0
|
||||||
|
|
||||||
|
## Idea
|
||||||
|
Monitorare crypto live (Deribit 24/7) ed entrare sul FUTURE indice IB (ES/NQ/RTY, tradato di notte)
|
||||||
|
a meta' notte, catturando il moto SUCCESSIVO -> finestre NON sovrapposte (no look-ahead, vs il "gap"
|
||||||
|
che era contemporaneo al segnale).
|
||||||
|
|
||||||
|
## Dati
|
||||||
|
`fetch_ib_futures.py` -> data/raw/fut_{es,nq,rty}_1h.parquet (ContFuture orario, UTC). ES 3y (2023-06+),
|
||||||
|
NQ 2.75y, RTY 2.3y. (NB: ContFuture NON accetta endDateTime -> chiamata singola "4 Y" = ~3y max orari.)
|
||||||
|
|
||||||
|
## Test (`fut_overnight_leadlag.py`)
|
||||||
|
entrata a T (ora UTC notturna): segnale = crypto[P21:00->T]; controllo = future[P21:00->T] (moto
|
||||||
|
PROPRIO del future); cattura = future[T->open 13:00]. Incrementale: crypto predice la cattura OLTRE il
|
||||||
|
moto proprio del future? Trade: sign(crypto[P21:00->T]) * future[T->open], net 2bps. T in {0,3,6,9}h.
|
||||||
|
|
||||||
|
## Risultati
|
||||||
|
| future | miglior Sharpe (trade crypto) | t_crypto incrementale | esito |
|
||||||
|
|---|---|---|---|
|
||||||
|
| ES (S&P500) | ~0 / negativo (-0.03..-0.93) | 0..1.5 | NESSUN edge |
|
||||||
|
| NQ (Nasdaq) | 0.41 (T=3h) | 0.5 (debole) | momentum del future, non crypto |
|
||||||
|
| RTY (Russell) | 0.40-0.77 | 2.0-2.7 (BTC->RTY) | soffio debole, non robusto |
|
||||||
|
|
||||||
|
- **SP500: NIENTE.** Il crypto della prima notte non predice l'ES della seconda. Il "Sharpe 5" del gap
|
||||||
|
era interamente look-ahead (finestre sovrapposte): catturato e ucciso.
|
||||||
|
- **RTY (small-cap)** e' l'unico con t_crypto incrementale ~2-2.7 e crypto che AGGIUNGE oltre il moto
|
||||||
|
proprio del future (futOwn Sharpe negativo). MA: Sharpe 0.4-0.5 modesto, 24 config (multiple-testing),
|
||||||
|
storia 2.3y, per-anno INCOERENTE (BTC->RTY T=3h: 2024 +0.99 / 2025 +0.52 / 2026 -0.31).
|
||||||
|
|
||||||
|
## Verdetto
|
||||||
|
L'idea "monitor Deribit / trade IB" NON da' un edge tradabile, men che meno su SP500. Il forte
|
||||||
|
fenomeno crypto<->equity e' CO-MOVIMENTO contemporaneo (risk-beta overnight), non anticipazione: quando
|
||||||
|
si impone una finestra causale non-sovrapposta, l'edge svanisce (efficienza di mercato). L'unico
|
||||||
|
residuo (crypto->small-cap overnight) e' debole, borderline su multiple-testing e instabile per anno
|
||||||
|
-> forward-monitor al piu', NON deploy. Coerente col soffitto del progetto.
|
||||||
|
|
||||||
|
Script: fetch_ib_futures.py, fut_overnight_leadlag.py. (look-ahead documentato: 2026-06-23-crypto-overnight-lookahead.md)
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
# 2026-06-23 — SKH01 "Skyhook": porting onesto del sistema ES dual-timeframe su BTC/ETH
|
||||||
|
|
||||||
|
Branch: `strategy_skyhook`. Engine: `src/strategies/skyhook.py`. Harness: `scripts/research/skyhook/skyhooklib.py`.
|
||||||
|
Test: `tests/test_skyhook.py` (5 pass). Ricerca: `scripts/research/skyhook/{sweep,grid,check_v1}.py` + `runs/`.
|
||||||
|
|
||||||
|
## Il brief
|
||||||
|
|
||||||
|
Sistema "Skyhook" (origine ES / E-mini S&P, genetico, a doppio timeframe), da portare su crypto:
|
||||||
|
- **data2 = 690 min (segnale)**, **data1 = 230 min (esecuzione)**. NB **690 = 3 × 230**.
|
||||||
|
- NON trend-follower: entra **solo** quando coincidono (a) un **regime** di volatilità/volume e
|
||||||
|
(b) un **pattern** di breakout.
|
||||||
|
- Pipeline per barra: indicatori (BuzVola su ATR, BuzVolume su volume, tipo-Chande 0-100) →
|
||||||
|
fasce regime → pattern (Donchian/breakout su data2) → composer (regime AND pattern) →
|
||||||
|
ingresso (max 1/giorno, stop-and-reverse) → uscite (time-based asimmetrico uscitalong=24 /
|
||||||
|
uscitashort=18 + stop/profit).
|
||||||
|
- Ancore demo: trend lineare → **BuzVola=50** (vol steady → neutro), **BuzVolume=100** (volume in rampa).
|
||||||
|
|
||||||
|
## Ricostruzione (fedele + onesta)
|
||||||
|
|
||||||
|
- **Resample dal feed 5m certificato** con `origin='epoch'`: 230 min = 46×5m, 690 min = 138×5m,
|
||||||
|
e i confini 690 sono un **sottoinsieme** dei confini 230 → una barra HTF chiude esattamente su
|
||||||
|
una chiusura LTF. Merge HTF→LTF causale: `merge_asof` backward sulla **chiusura HTF** (≤ chiusura
|
||||||
|
LTF), così una barra HTF è usata solo quando è davvero chiusa. (~2287 barre/anno LTF, ~762 HTF.)
|
||||||
|
- **BuzVola / BuzVolume = `chande01`** (Chande Momentum Oscillator normalizzato 0-100): serie
|
||||||
|
steady → 50, rampa-su → 100, rampa-giù → 0. Le ancore demo sono soddisfatte a livello di
|
||||||
|
indicatore (è la lettura fedele: "vol steady → neutro"). NB: l'EMA-ATR su un *linspace* sintetico
|
||||||
|
dà 100 per drift di warm-up/floating-point, non per comportamento reale — su BTC reale BuzVola
|
||||||
|
oscilla intorno a 50 (EMA-ATR vs SMA-ATR corr 0.90).
|
||||||
|
- **Pattern** = Donchian breakout leak-free (shift(1)) su HTF, `ptn_n` barre (default 13 da 13/13/1).
|
||||||
|
- **Regime** = bande-soglia tunabili su BuzVola/BuzVolume (i magici interi 4/3/2 - 4/2/2 non sono
|
||||||
|
nel brief; ricostruiti come `[vola_lo,vola_hi]` × `[vol_lo,vol_hi]`).
|
||||||
|
- **Composer** = regime AND pattern. **Ingressi** ≤1/giorno (prima barra qualificante).
|
||||||
|
- **Uscite**: time-based asimmetrico (`uscitalong`/`uscitashort` barre LTF) + hard stop/profit. Lo
|
||||||
|
"stop 2000 / profit 5000" in $ del sistema ES → **multipli di ATR LTF** (scale-free): default
|
||||||
|
`sl_atr=2.0`, `tp_atr=5.0` (~ rapporto 40:100 pt ES), con modalità `pct` alternativa.
|
||||||
|
- Engine espresso come **entries `{dir,tp,sl,max_bars}`** per `backtest_signals` (motore onesto del
|
||||||
|
progetto: TP/SL intrabar, max_bars, non-overlap). Causalità verificata con prefix-recompute
|
||||||
|
(0 mismatch).
|
||||||
|
|
||||||
|
## Baseline → V1 (lever scout + grid, inline, veloce)
|
||||||
|
|
||||||
|
- **Baseline** (default 13/13, sl2/tp5, vola[35,95], vol_lo50): causale, fee-surviving, FULL Sharpe
|
||||||
|
BTC +0.91 / ETH +0.64, ma **HOLD-OUT debole** (BTC −0.09 / ETH +0.17) → FAIL del gate onesto.
|
||||||
|
- **Lever scout** (`sweep.py`): gli **short servono** (long_only → HOLD −0.52); il **regime gate
|
||||||
|
conta** (togliere la banda vola → HOLD −0.80); il **floor di volume** a 50 *frenava* l'hold-out
|
||||||
|
(vol_lo=40 o 0 → PASS); **breakout più lento** (ptn_n=55) e **stop più larghi** (sl2.5/tp6)
|
||||||
|
alzano l'hold-out.
|
||||||
|
- **Grid combinato** (`grid.py`): vincitrice **SKH01-V1**
|
||||||
|
`SkyhookParams(ptn_n=55, sl_atr=2.5, tp_atr=6.0, vola_lo=35, vola_hi=95, vol_lo=0.0)`:
|
||||||
|
- **min-asset FULL +0.69, HOLD-OUT +0.64** (BTC 0.64 / ETH 0.64), **PASS**, fee-surviving a 0.30%RT.
|
||||||
|
- BTC FULL +0.69/+275% DD49% ; ETH FULL +1.01/+871% DD31% ; entrambi HOLD-OUT positivi.
|
||||||
|
- **Marginal vs TP01 = ADDS** e regge i gate induriti: **corr 0.06** (ortogonale, NON trend-beta),
|
||||||
|
`has_insample_edge=True` (Sharpe in-sample standalone 1.15), `is_hedge=False`, multi-cut
|
||||||
|
persistente. Blend **0.75·TP01 + 0.25·SKH01: HOLD Sharpe 0.31 → 0.74 (+0.44), DD 11.9%**;
|
||||||
|
blend 50/50 HOLD 0.88, DD 17.8%.
|
||||||
|
- Unico sub-gate fallito: `clean_year_uplift` +0.014 (sotto 0.02) → `earns_slot=False` per un pelo,
|
||||||
|
nonostante tutto il resto sia forte. **Debolezza principale: DD standalone alto (40-49%).**
|
||||||
|
|
||||||
|
→ SKH01 è un **diversificatore quasi-ortogonale** reale (non un TP01 travestito): da solo è
|
||||||
|
volatile, ma come sleeve al 25% migliora moltissimo l'hold-out del portafoglio a DD bassissimo.
|
||||||
|
|
||||||
|
## Onda 1 (`skyhook-improve`, 30 agenti) — winner intermedio
|
||||||
|
|
||||||
|
Famiglie: param (RR, ptn_n, regime bands, exit bars, chande, local), regime-redef (percentile,
|
||||||
|
realized-vol, vol-expansion, LTF), pattern (confirmation, ROC, Keltner, NR, dual), exit + overlay,
|
||||||
|
ognuna verificata da 2 scettici. Risultato: **winner intermedio**
|
||||||
|
`SkyhookParams(ptn_n=45, sl_atr=2.5, tp_atr=7.0, uscitalong=24, uscitashort=16, vola_lo=35, vola_hi=95, vol_lo=0)`
|
||||||
|
— **minFull +0.83, minHold +0.81** (vs V1 +0.69/+0.64), causale, fee-surviving 0.30%RT, marginal
|
||||||
|
**ADDS** (corr 0.05, has_insample_edge, robust_oos, multicut, clean_year_uplift +0.37), blend w25
|
||||||
|
uplift_hold +0.58. **MA standalone maxDD ancora 34% (BTC) / 31% (ETH) → l'unico goal mancato era il DD<30%.**
|
||||||
|
|
||||||
|
## Onda 2 (`skyhook-improve-v2`, 14 famiglie DD-reduction) — SKH01-V2-DD vince
|
||||||
|
|
||||||
|
Obiettivo: tagliare il **DD standalone <30%** tenendo hold-out + `earns_slot`, e alzare l'uplift di
|
||||||
|
portafoglio. 14 famiglie (ensemble param/struct, vol-target, DD kill-switch, RR/stop grid, regime
|
||||||
|
tight, percentile, vol-expansion, breakout confirmation, dual-TF, asimmetria L/S, cadenza, chande,
|
||||||
|
Keltner), ognuna verificata da 2 scettici avversariali (window-luck/multicut/jackknife +
|
||||||
|
causalità/fee/plateau/overfit). Esito: **il winner intermedio cade.** Nuovo campione **SKH01-V2-DD**
|
||||||
|
(famiglia ASYM_LS, `src/strategies/skyhook.py:SKH01_V2_DD`, run `runs/SKH2_ASYM_LS.py`):
|
||||||
|
|
||||||
|
- **Config:** stesso SEGNALE del winner (`ptn_n=45, vola_lo=35, vola_hi=95, vol_lo=0, exit-bars 24/16`)
|
||||||
|
ma EXIT commutati da ATR a **percentuale fissa ASIMMETRICA** — long `sl=4% / tp=10%`, short
|
||||||
|
`sl=2% (più stretto) / tp=8%`. Motivazione meccanica: in crypto lo short si fa steamrollare da uno
|
||||||
|
spike vola e lo stop-ATR si allarga lasciando correre la perdita → il %-SL stretto sullo short
|
||||||
|
**cappa la perdita per-trade** che FORMA il maxDD. (Implementato come override per-direzione nel
|
||||||
|
motore, backward-compatible: campi `*_short=None` → comportamento simmetrico invariato.)
|
||||||
|
- **Numeri veri (verificati indipendentemente via `sk.study(SKH01_V2_DD)`):** standalone maxDD
|
||||||
|
**BTC 21.4% / ETH 27.4%** (<30% ✓, vs 34.4/30.5 del winner) — **goal RAGGIUNTO**; minFull **+0.99**,
|
||||||
|
minHold **+1.26**; causalità **0/400** entrambi gli asset; fee@0.30%RT BTC +1.05 / ETH +0.80
|
||||||
|
(positiva anche a 0.40%). Marginal vs TP01 **ADDS** (corr 0.09, has_insample_edge, is_hedge=False,
|
||||||
|
robust_oos, multicut, clean_year_uplift +0.57). **Blend 0.75·TP01 + 0.25·SKH: uplift_hold +0.87**
|
||||||
|
(vs +0.58 del winner); **blend 50/50: full 1.84 / hold 1.59 / DD 10.7%**. earns_slot=True,
|
||||||
|
beats_winner=True. **Plateau reale** (i vicini Spct_mb14/16 sl2% tengono DD 27-28%), non knife-edge.
|
||||||
|
Entrambi gli scettici: holds_up=True, confidence high, killer_finding=null.
|
||||||
|
|
||||||
|
**Top-3 dell'onda 2 (criteri onesti):**
|
||||||
|
|
||||||
|
| # | Famiglia | maxDD (BTC/ETH) | minHold | w25 uplift_hold | Verifica |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| **1** | **ASYM_LS → SKH01-V2-DD** | 27.4% (21.4/27.4) | +1.26 | **+0.87** | 2/2 high, killer=null ✅ |
|
||||||
|
| 2 | ENS_STRUCT (3-regime ensemble) | **22.9%** (21.2/22.8) | +1.00 | +0.67 | 2/2 high — ma 3 motori da eseguire |
|
||||||
|
| 3 | TPSL_DD (%-SL/TP hard) | 28.0% (28/25.5) | +1.11 | +0.75 | 1/1 (rate-limit) — caveat hedge-like |
|
||||||
|
|
||||||
|
**Lezioni anti-DD:**
|
||||||
|
- **Ha funzionato (STRUTTURA dell'exit, non i parametri):** cambiare il MECCANISMO di uscita — %-SL
|
||||||
|
hard, asimmetria L/S, o ensemble di exit/regime diversi (decorrelazione). Il DD del winner nasce
|
||||||
|
dalla coda intra-trade negli spike ATR; il %-SL la cappa.
|
||||||
|
- **NON ha funzionato (la leva non raggiunge il DD vincolante):** DD kill-switch entry-only (sopprime
|
||||||
|
solo le NUOVE entry, non chiude il trade aperto che forma il maxDD → floor 33-36%); vol-target
|
||||||
|
causale (DD<30 e uplift≥0.55 mutuamente esclusivi; cap>1 PEGGIORA il DD levereggiando nel pre-crash);
|
||||||
|
cadenza/FREQ (accorciare gli hold short fa esplodere ETH a 50-66%); dual-TF (LTF è resample dello
|
||||||
|
stesso prezzo → quasi-tautologico, DD invariato).
|
||||||
|
- **Bocciato dagli scettici come overfit:** PATTERN_CONF (sub-30 solo a vola_lo=45, knife-edge: sl_atr
|
||||||
|
±0.5 → ETH 40-47%; la conferma "close_loc" da sola NON taglia il DD). Esempio canonico del perché
|
||||||
|
serviva la doppia verifica.
|
||||||
|
- **Non promuovibili:** PCTL_DD (numeri spettacolari ma **0 verifiche**, le 2 sono morte per rate-limit
|
||||||
|
→ forward-monitor, non fidato); ENS_PARAM / TPSL_DD (battono i gate ma uplift recency/hedge-loaded,
|
||||||
|
concentrato nei regimi TP01-down → forward-monitor).
|
||||||
|
|
||||||
|
**Promozione (questa sessione):** `SKH01_V2_DD` canonico nel motore + override exit-short
|
||||||
|
asimmetrici (backward-compatible, V1/winner invariati) + 3 test nuovi (8/8 pass).
|
||||||
|
|
||||||
|
**Sleeve cablato @0.25 effettivo** (`src/portfolio/sleeves.skyhook_sleeve` → `active_sleeves`): i tre
|
||||||
|
sleeve preesistenti scalati nel restante 0.75 mantenendo il rapporto 55:25:20 → **TP01 41.25% / XS01
|
||||||
|
18.75% / VRP01 15% / SKH01 25%**. Report del portafoglio (4 sleeve, `run_portfolio.py`):
|
||||||
|
|
||||||
|
| | FULL Sharpe | FULL DD | HOLD-OUT Sharpe | HOLD-OUT DD |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 3 sleeve (TP01+XS01+VRP01) | 1.68 | 14.3% | 1.63 | 3.4% |
|
||||||
|
| **+ SKH01 @25%** | **2.13** | **7.8%** | **2.30** | 3.5% |
|
||||||
|
| Δ | **+0.45** | **−6.5pt** | **+0.67** | ~0 |
|
||||||
|
|
||||||
|
→ aggiungere Skyhook **alza lo Sharpe full +0.45 e DIMEZZA il DD full (14.3→7.8%)**, e alza l'hold-out
|
||||||
|
+0.67 a DD invariato. Portafoglio combinato: FULL Sh 2.13 / ret +365% / DD 7.8%, HOLD Sh 2.30 / DD 3.5%,
|
||||||
|
positivo ogni anno (2019-26, DD annuo ≤7.8%) vs buy&hold 50/50 FULL Sh 0.93 / DD 76%.
|
||||||
|
|
||||||
|
**Caveat onesti / NON deploy:** è un portafoglio di **ricerca** (peso fisso, no costi di ribilanciamento
|
||||||
|
reale a $600; lo Sharpe daily-step di Skyhook è la convenzione del lens). ETH DD standalone 27.4% ha
|
||||||
|
margine sottile vs 30%. Prima di un eventuale deploy: ri-verificare la causalità sul **codice di
|
||||||
|
esecuzione reale** (qui è l'harness di ricerca) e i costi del book a 230m (ribilanciamento più frequente
|
||||||
|
del resto). XS01/VRP01 restano STAT-MODE/lead. Per ora: research win + sleeve cablato, forward-monitor.
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# 2026-06-23 — Tail-hedge / protezione DD del combo (incl. OPZIONI): vince la guardia-drawdown
|
||||||
|
|
||||||
|
## Obiettivo (goal utente)
|
||||||
|
Trovare uno sleeve/overlay da AGGIUNGERE al combo (TP01+GTAA) per proteggere il drawdown e gli anni
|
||||||
|
tipo 2022. Valutare anche le opzioni.
|
||||||
|
|
||||||
|
## Diagnosi del rischio (decisiva)
|
||||||
|
Il MaxDD del combo (1x) e' **8.4%** e il 2022 fu **-4.4%**: NON un crash, un **grind lento** (peggior
|
||||||
|
giorno -2.8%, peggior mese 2022 -1.6%). Il doppio trend (TP01+GTAA long-flat) gia' taglia i crash
|
||||||
|
veloci. Il tail residuo = (a) whipsaw da mercato choppy (2022), (b) gap/crash overnight LATENTE (TP01
|
||||||
|
non reagisce intraday, non nel campione storico), (c) la LEVA.
|
||||||
|
|
||||||
|
## Candidati testati (`tail_hedge_lab.py`)
|
||||||
|
| protezione | MaxDD | 2022 | Sharpe | CAGR |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| combo baseline | 8.4% | -4.4% | 1.48 | 11.3% |
|
||||||
|
| **+ guardia-DD -4%** | **5.8%** | **-1.8%** | 1.38 | 9.2% |
|
||||||
|
| + vol-target 5% | 8.4% | -5.9% | 1.46 | 8.0% |
|
||||||
|
| + opzioni (put/put-spread, budget 3%/y) | 8.4% | -4.4% | 1.48 | 11.3% |
|
||||||
|
|
||||||
|
### OPZIONI (valutate): NON adatte al 2022
|
||||||
|
- Put-spread/long-put LONG su indice 50/50 BTC/ETH (mirror di VRP01), premio BS su DVOL reale, payoff
|
||||||
|
sul path. Strike corretti (compra -0.30delta, vende -0.10delta).
|
||||||
|
- Sempre-on costa **~50%/anno** di premio -> con budget 3%/anno size ~0.06-0.10x = effetto ~nullo.
|
||||||
|
- Nel 2022 (grind, niente crash settimanali) **sanguinano** (scadono inutili) -> Δ2022 ~0.
|
||||||
|
- Pagano SOLO nei crash secchi: stress -30% overnight -> put paga **+25% netto**, put-spread +3.8%.
|
||||||
|
- Verdetto: assicurazione BLACK-SWAN cara e fuori-bersaglio per il grind. Utile solo come piccola
|
||||||
|
copertura del gap overnight latente, NON come fix del 2022.
|
||||||
|
|
||||||
|
### GUARDIA DRAWDOWN: centra il rischio
|
||||||
|
De-risk (esposizione 1.0->0.4) quando il DD da picco supera -4%, ri-rischia a -1.6%. Targetizza il
|
||||||
|
grind: MaxDD 8.4%->5.8%, 2022 -4.4%->-1.8%, ogni anno DD intra <=5.4%. Costo: Sharpe 1.48->1.38,
|
||||||
|
CAGR -2.1pp (de-risca sui cali, perde rimbalzo — prezzo onesto della protezione).
|
||||||
|
|
||||||
|
### A LEVA (dove il tail morde)
|
||||||
|
guard applicato pre-leva: 2x 2022 -15.6%->-10.9%, MaxDD 28%->24%; 3x resta MARGIN-CALL (DD 39%>=33%).
|
||||||
|
-> la protezione rende il 2x sopportabile; il 3x va evitato comunque.
|
||||||
|
|
||||||
|
## Raccomandazione
|
||||||
|
AGGIUNGERE una **guardia-drawdown a livello di portafoglio** al combo (overlay, niente premio):
|
||||||
|
e' la protezione che colpisce il rischio reale (grind/2022) a costo Sharpe minimo. Le opzioni NO come
|
||||||
|
fix del 2022; eventualmente una micro-allocazione deep-OTM come assicurazione black-swan separata.
|
||||||
|
Vol-target NON aiuta (il 2022 non e' uno spike di vol).
|
||||||
|
|
||||||
|
## Onesta'
|
||||||
|
- Il guard e' REATTIVO (de-risca dopo l'inizio del DD, restituisce un po' di rimbalzo) -> costa CAGR.
|
||||||
|
- Trade: -2.1pp CAGR per dimezzare il MaxDD e azzerare quasi il 2022. Sensato se la priorita' e' il DD.
|
||||||
|
- Parametri (-4% trigger) semplici; il meccanismo (non la soglia esatta) e' la sostanza.
|
||||||
|
Script: tail_hedge_lab.py.
|
||||||
|
|
||||||
|
## Aggiornamento — protezioni CLASSICHE (stop-loss), goal "prova anche SL" (`stops_lab.py`)
|
||||||
|
Confronto equo (trigger/re-entry sul NAV di mercato, non sull'equity congelata):
|
||||||
|
| protezione | Sharpe | MaxDD | 2022 | CAGR | in-mkt |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| baseline | 1.48 | 8.4% | -4.4% | 11.3% | 100% |
|
||||||
|
| **soft-guard -4% (0.4x)** | **1.38** | **5.8%** | **-1.8%** | 9.2% | 100% |
|
||||||
|
| trail-stop -4% (uscita tot.) | 1.07 | 7.5% | +0.0% | 6.6% | 42% |
|
||||||
|
| trail-stop -6% (re:newhigh) | 1.34 | 6.6% | -2.1% | 9.0% | 72% |
|
||||||
|
| trail-stop -8% | 1.41 | 8.3% | -4.2% | 10.0% | 87% |
|
||||||
|
| stop mensile -5% | 1.48 | 8.4% | -4.4% | 11.3% | 100% (mai scatta) |
|
||||||
|
| vol-stop (>90pctl) | 1.48 | 8.4% | -4.4% | 10.4% | 100% |
|
||||||
|
|
||||||
|
VERDETTO: lo SL classico funziona solo a -6% (e resta inferiore al soft-guard); a -4% fa WHIPSAW
|
||||||
|
(Sh 1.48->1.07, fuori mercato 58%) perche' l'uscita TOTALE viene choppata nel grind. Il soft-guard
|
||||||
|
alla stessa soglia non whippa (de-risk parziale 0.4x). Stop mensile/vol inutili (bersaglio sbagliato).
|
||||||
|
Conferma: per un DD da grind, de-risk PARZIALE > stop-loss duro. Soft-guard -4% confermato come scelta.
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# 2026-06-26 — Cash-and-carry (basis trade) delta-neutral su HL (CC01): premio REALE, Sharpe ARTEFATTO, NON deployabile
|
||||||
|
|
||||||
|
**Goal utente:** dopo lo scarto del gamma scalping, "provare" gli altri due angoli proposti —
|
||||||
|
funding-carry cross-sectional e basis/cash-and-carry. Il **funding cross-sectional (FC01) è già
|
||||||
|
SCARTATO** (diario 2026-06-22: DILUTES, sign-flip su un nome). L'angolo non testato è il
|
||||||
|
**cash-and-carry delta-neutral (CC01)**. Branch `research/gamma-scalp-options`,
|
||||||
|
script `scripts/research/cash_carry_hl.py`.
|
||||||
|
|
||||||
|
## Meccanismo (diverso da FC01)
|
||||||
|
|
||||||
|
FC01 pickava cross-section (short alto-funding / long basso-funding) → scommessa relativa, fragile.
|
||||||
|
CC01 **non picka**: harvesta il premio di funding AGGREGATO restando delta-neutral sullo *stesso*
|
||||||
|
asset. Long spot + short perp (stessa size): le gambe lineari nel prezzo si cancellano → ritorno
|
||||||
|
≈ **+funding** (lo short incassa il funding quando f>0), zero esposizione direzionale. Dati: funding
|
||||||
|
orario reale HL dal 2023-05 (`hlfund_*_1h.parquet`), gli stessi 19 major di XS01.
|
||||||
|
|
||||||
|
## Risultato grezzo — e perché NON va creduto
|
||||||
|
|
||||||
|
| config | Sharpe | CAGR | maxDD | per-anno |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| BTC/ETH CC-static | **+12.9** | +13% | 0.8% | 2023:+10 2024:+23 2025:+8 2026:+1% |
|
||||||
|
| BTC/ETH CC-gated | +13.9 | +14% | 0.3% | tutti positivi |
|
||||||
|
| 19-major CC-gated | +11.6 | +16% | 0.4% | tutti positivi |
|
||||||
|
|
||||||
|
**Uno Sharpe di 13 con DD 0.3% è un ALLARME, non un edge.** È la firma di un modello che cattura
|
||||||
|
solo un cashflow liscio (il funding) e azzera la volatilità reale. Il marginal scorer dà **ADDS /
|
||||||
|
robust_oos / has_insample_edge=True / multicut_persistent** su OGNI config — ma questo **espone un
|
||||||
|
punto cieco dello scorer**: si fida della vol riportata dal candidato e non ha un gate "Sharpe
|
||||||
|
implausibile → rischio nascosto". Un Sharpe di 13 dovrebbe squalificare-e-indagare, non passare.
|
||||||
|
|
||||||
|
## Il rischio è NASCOSTO, non assente
|
||||||
|
|
||||||
|
- **Mark-to-market della base** (`premium` col → `r = funding − Δpremium`): sgonfia lo Sharpe solo
|
||||||
|
13→11. La colonna premium di HL è già lisciata/clampata → a risoluzione daily il basis-vol è
|
||||||
|
minimo. **Quindi il basis-from-data NON è il rischio vero.**
|
||||||
|
- **I rischi che giustificherebbero uno Sharpe realistico (~1-3) sono ASSENTI dal dataset:**
|
||||||
|
1. **2022 mancante.** Lo storico funding parte 2023-05 → niente deleveraging LUNA/3AC/FTX, il
|
||||||
|
regime di funding profondamente negativo + basis blow-out che farebbe il vero drawdown.
|
||||||
|
2. **Procyclicità.** Funding aggregato BTC/ETH: +23% nel toro 2024 → **+1.7% nel bear 2026**
|
||||||
|
(e 19-major CC-static 2026 = **−1%**). Solo 10% di giorni a funding negativo *in-sample* — ma
|
||||||
|
in un vero bear quella frazione esplode. Il carry **non è all-weather**: è un risk-on premium.
|
||||||
|
3. **Liquidazione dello short** in uno squeeze e **slippage** su spot+perp: non modellati.
|
||||||
|
- **Sharpe reale di un crypto basis-trade ≈ 1-3 con code brusche** (Mar-2020, Mag-2021, FTX), non 13.
|
||||||
|
|
||||||
|
## Eseguibilità a $600 — muro indipendente
|
||||||
|
|
||||||
|
Serve spot **+** perp per gamba: BTC/ETH = 4 posizioni, 19-major = 38. Su Deribit (dove operiamo) lo
|
||||||
|
storico funding è **bloccato** (404) e non tradiamo spot HL. **STAT-MODE**, come XS01/VRP01 ma peggio
|
||||||
|
(doppia gamba per asset). Non deployabile a questo capitale.
|
||||||
|
|
||||||
|
## Verdetto
|
||||||
|
|
||||||
|
**CC01 = premio di funding REALE (~+8-14%/anno aggregato, positivo ogni anno in-sample) e
|
||||||
|
genuinamente ortogonale a TP01 (corr ~0.05, NON direzionale).** È la "fonte di ritorno diversa" più
|
||||||
|
concreta trovata finora. MA:
|
||||||
|
- lo **Sharpe headline è fiction** (rischi di coda strutturalmente fuori dal dataset);
|
||||||
|
- è **procyclico** (si comprime/inverte nel bear — proprio quando servirebbe);
|
||||||
|
- **non eseguibile** a $600 / sul nostro venue.
|
||||||
|
|
||||||
|
→ **LEAD da rivedere a scala** (capitale ~20k+ e un venue con funding eseguibile), **non uno sleeve**.
|
||||||
|
Nessun impatto sul book live (branch separato).
|
||||||
|
|
||||||
|
## Sottoprodotto metodologico (raccomandazione)
|
||||||
|
|
||||||
|
CC01 ha passato ogni gate del marginal scorer pur essendo un'illusione di rischio. **Prossima
|
||||||
|
indurita dello scorer:** un gate di *plausibilità dello Sharpe* — uno standalone Sharpe ≫ 4 deve
|
||||||
|
declassare a "SUSPECT/INVESTIGATE" invece di alimentare ADDS, perché segnala vol sottostimata
|
||||||
|
(mark-to-cashflow, dati che escludono il regime di stress, leva nascosta). Documentato qui, non
|
||||||
|
implementato in questa passata per non toccare lo scorer condiviso su `main`.
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# 2026-06-26 — "Short su fondamentali/notizie NEG ma prezzo SU": screener forward, edge non provato
|
||||||
|
|
||||||
|
**Goal utente (chiarito):** se i dati finanziari/notizie di un'azienda sono negativi ma la
|
||||||
|
quotazione è positiva (sale), andare SHORT — scommessa che il prezzo scenda a riallinearsi ai
|
||||||
|
fondamentali. Branch `research/equity-meanrev-ib`, script `scripts/research/eq_fundnews_short.py`.
|
||||||
|
|
||||||
|
## Gate dati (lezione v2.0.0) — NON backtestabile
|
||||||
|
|
||||||
|
I fondamentali e le notizie scaricabili dalla rete (Yahoo) sono **snapshot CORRENTI**, non
|
||||||
|
point-in-time storici. Applicarli a prezzi passati = **look-ahead** (restatement + survivorship):
|
||||||
|
esattamente la classe di errore che ha prodotto la libreria fasulla v2.0.0. Un backtest onesto
|
||||||
|
richiede un DB point-in-time (Compustat PIT, news storiche con timestamp) — **assente**. Quindi,
|
||||||
|
come per la vol term-structure: **niente backtest**, l'unica via onesta è uno **screener forward**.
|
||||||
|
|
||||||
|
## Cosa è scaricabile (feasibility, fatto)
|
||||||
|
|
||||||
|
- **Fondamentali strutturati** via Yahoo quoteSummary (flusso cookie+**crumb**, tokenless):
|
||||||
|
`recommendationMean` (1=buy..5=sell), `revenueGrowth`, `earningsHistory` surprise%,
|
||||||
|
`recommendationTrend` (skew buy/sell). ✅
|
||||||
|
- **Notizie**: Yahoo news search (headline + publisher), sentiment lessicale crudo. ✅ (proxy debole)
|
||||||
|
- **Momentum**: chart API (return 1m/3m). ✅
|
||||||
|
|
||||||
|
## Screener costruito + eseguito dal vivo
|
||||||
|
|
||||||
|
`fund_neg` ∈ [0,1] (media di: recMean→sell, surprise negative, ricavi in calo, analisti a sell) +
|
||||||
|
`news_neg` lessicale + momentum. **SHORT candidate = (fond/news neg) AND prezzo in salita** (la
|
||||||
|
divergenza). Idempotente, logga forward su `data/raw/fundnews_short_screen.parquet`.
|
||||||
|
|
||||||
|
**Run di oggi (2026-06-26): NESSUN candidato.** Quasi tutto l'universo ha momentum 1m **negativo**
|
||||||
|
(mercato in flessione ampia, coerente col bear) → la gamba "prezzo che sale" non scatta. I peggiori
|
||||||
|
per fondamentali (TSLA 0.48, BA 0.39, NKE 0.38) stanno comunque *scendendo*. Output sensato.
|
||||||
|
|
||||||
|
## L'intuizione analitica chiave (il valore vero di questa analisi)
|
||||||
|
|
||||||
|
La premessa ha una **tensione interna** che la rende la versione *rischiosa* dell'anomalia:
|
||||||
|
- **Shortare un prezzo che SALE combatte il momentum** — una delle anomalie più robuste e persistenti
|
||||||
|
(i winner continuano a vincere su 3-12 mesi). Si scommette contro di essa.
|
||||||
|
- **Il PEAD** (post-earnings drift) dice che dopo cattive notizie il prezzo continua a **scendere**;
|
||||||
|
ma qui il prezzo *sale malgrado* le cattive notizie → spesso significa che il mercato sta
|
||||||
|
**prezzando informazione** che i fondamentali *trailing* non contengono (turnaround, M&A, squeeze).
|
||||||
|
- Quindi: la versione *pulita* dell'edge "short su fondamentali deboli" funziona meglio quando il
|
||||||
|
prezzo **conferma** (fondamentali deboli + prezzo che SCENDE = momentum e fondamentali allineati),
|
||||||
|
**non** quando diverge. La richiesta "shorta il titolo che sale su brutte notizie" è il caso
|
||||||
|
contrarian, più difficile e storicamente più punito (squeeze, perdita illimitata).
|
||||||
|
|
||||||
|
## Eseguibilità (muro)
|
||||||
|
|
||||||
|
Short = **borrow** (locate + fee; hard-to-borrow caro/assente), **perdita illimitata**, **squeeze**,
|
||||||
|
**PDT $25k** per i day-trade, IB instabile qui, $600, universo single-stock (non i nostri ETF). NON
|
||||||
|
deployabile.
|
||||||
|
|
||||||
|
## Verdetto
|
||||||
|
|
||||||
|
**Non un edge schierabile, né dimostrabile ora.** Deliverable onesto = lo **screener forward**
|
||||||
|
(funzionante, da dati di rete) + il log che accumula i candidati per una validazione futura, con
|
||||||
|
la chiara avvertenza che l'edge è **non provato** e che la *direzione contrarian* della scommessa
|
||||||
|
(short sulla forza) è la parte fragile. Strumento utile (fond+news+momentum da rete in un colpo),
|
||||||
|
tesi da maneggiare con scetticismo. Nessun impatto sul book live (branch separato).
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# 2026-06-26 — Scalping azioni IB "quando sottoquotate" + check dati dalla rete
|
||||||
|
|
||||||
|
**Goal utente:** su IB, comprare azioni quando "sottoquotate" (oversold/sotto fair-value), con
|
||||||
|
verifica incrociata dei dati dalla rete. Branch `research/equity-meanrev-ib`,
|
||||||
|
script `scripts/research/eq_meanrev_ib.py`. Due pezzi.
|
||||||
|
|
||||||
|
## 1) CHECK DATI DALLA RETE — il pezzo richiesto (e perfettamente in-metodo)
|
||||||
|
|
||||||
|
Confronto i dati certificati IB (`data/raw/eq_*_1d.parquet`, ADJUSTED_LAST) con una **sorgente di
|
||||||
|
rete indipendente** (Yahoo Finance chart API, tokenless). È la lezione fondante del progetto: non
|
||||||
|
fidarti di un feed solo (il disastro v2.0.0 era un feed contaminato).
|
||||||
|
|
||||||
|
**Risultato (180g, BTC… no: 6 ETF):** dopo l'allineamento corretto **tutti CONCORDE**, deviazione
|
||||||
|
sui rendimenti **≤1.2bps**, ultimo close identico (0.0bps). Il feed IB è validato.
|
||||||
|
|
||||||
|
**La lezione (errore che ho fatto e corretto):** un primo confronto naïf (nostro close *adjusted*
|
||||||
|
vs `close` *grezzo* di Yahoo) ha **falso-allarmato 4/6 ticker** a 30-52bps (SPY 30, TLT 40, HYG 52).
|
||||||
|
Le divergenze erano **tutte su singoli giorni = stacco dividendo**: confrontavo dividend-adjusted vs
|
||||||
|
non-adjusted. Usando l'`adjclose` di Yahoo (apples-to-apples) → ~0bps. **Regola: ogni divergenza va
|
||||||
|
SPIEGATA prima di gridare "feed sporco" — e prima di tradarci sopra.** Lo strumento resta come
|
||||||
|
validatore di feed riutilizzabile / template del pre-trade price-check live.
|
||||||
|
|
||||||
|
## 2) "SCALPING quando sottoquotata" — non testabile/eseguibile; testato lo swing MR
|
||||||
|
|
||||||
|
Lo **scalping intraday non è valutabile**: (a) non abbiamo dati intraday (solo `eq_*_1d`), (b) non
|
||||||
|
è eseguibile — **PDT rule**: il day-trading di azioni US sotto **$25.000** è limitato a 3
|
||||||
|
day-trade/5gg → lo scalping è *regolatoriamente bloccato* al capitale del progetto (l'analogo equity
|
||||||
|
del muro STAT-MODE a $600 sul crypto), (c) IB Gateway è instabile qui (timeout ordini diagnosticato).
|
||||||
|
|
||||||
|
Versione onesta e testabile = **swing mean-reversion** "compra quando sottoquotata": Connors RSI2<10
|
||||||
|
+ filtro trend MA200, exit a MA5. Causale (segnale ≤ close[i], entry a close[i]), netto fee, hold-out 2015+:
|
||||||
|
|
||||||
|
| ETF | FULL Sh | DD | CAGR | expo | HOLD Sh | **B&H HOLD Sh** |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| SPY | +0.75 | 15% | +5% | 13% | +0.70 | **+0.81** |
|
||||||
|
| QQQ | +0.50 | 16% | +4% | 14% | +0.56 | **+0.92** |
|
||||||
|
| IWM | +0.32 | 22% | +2% | 13% | +0.15 | +0.53 |
|
||||||
|
| DIA | +0.38 | 13% | +2% | 14% | +0.55 | +0.74 |
|
||||||
|
| EEM | +0.41 | 20% | +3% | 13% | +0.36 | +0.46 |
|
||||||
|
|
||||||
|
Fee sweep SPY: Sh 0.82 (0bps) → 0.75 (3) → 0.70 (5) → 0.58 (10bps RT).
|
||||||
|
|
||||||
|
**Lettura onesta:** la mean-reversion "buy the dip" sugli indici equity ha un edge **reale ma
|
||||||
|
modesto** (Sharpe ~0.5-0.75, investito solo ~13% dei giorni) **che NON batte il buy&hold
|
||||||
|
risk-adjusted** (B&H hold-out 0.81 su SPY vs 0.70 della MR), con CAGR piccolo (2-5%) e sensibile ai
|
||||||
|
costi. È capital-efficient in teoria (cash 87% del tempo) ma il valore aggiunto vs "tieni l'indice"
|
||||||
|
è negativo o nullo, e la frequenza *scalping* che potrebbe aggiungere valore è bloccata dalla PDT.
|
||||||
|
|
||||||
|
## Verdetto
|
||||||
|
|
||||||
|
- **Check dati dalla rete: FATTO e utile** — feed IB validato (≤1.2bps vs Yahoo); strumento
|
||||||
|
riutilizzabile. Unico vero keeper della sessione equity.
|
||||||
|
- **Scalping "sottoquotate": NON deployabile** — non testabile (no intraday), non eseguibile (PDT
|
||||||
|
$25k, IB instabile). Lo swing MR daily testabile **non batte il buy&hold** → niente edge schierabile.
|
||||||
|
|
||||||
|
Conferma il pattern del progetto: l'idea promettente sopravvive solo finché non incontra
|
||||||
|
eseguibilità + benchmark onesto. Nessun impatto sul book live crypto (branch separato).
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# 2026-06-26 — "Scalping BTC/ETH con copertura in opzioni" = gamma scalping → SCARTATO
|
||||||
|
|
||||||
|
**Domanda (goal utente):** analizzare lo scalping su BTC/ETH con copertura in opzioni come
|
||||||
|
possibile nuovo sleeve. **Esito: negativo, strutturale, su tutte le frequenze. Non è uno sleeve,
|
||||||
|
non è nemmeno un hedge.** Branch `research/gamma-scalp-options`, script
|
||||||
|
`scripts/research/options_gamma_scalp.py`.
|
||||||
|
|
||||||
|
## Cos'è davvero "scalping con copertura in opzioni"
|
||||||
|
|
||||||
|
L'interpretazione rigorosa è il **gamma scalping**: compri un'opzione (la *copertura* = long
|
||||||
|
gamma), delta-hedgi il sottostante a cadenza fissa (lo *scalp*), e il P&L netto è
|
||||||
|
≈ dollar-gamma × (vol realizzata² − vol implicita²). È lo **specchio esatto del VRP01** (short-vol):
|
||||||
|
VRP01 incassa IV−RV (positivo in media), il gamma scalping incassa **RV−IV** (negativo in media).
|
||||||
|
|
||||||
|
Modello (mirror della struttura VRP per comparabilità): long straddle ATM, tenor 7g, IV = DVOL
|
||||||
|
Deribit (la stessa fonte del VRP), delta-hedge sui prezzi certificati, fee opzioni cap 12.5% del
|
||||||
|
premio + fee perp 0.05%/lato sull'hedge turnover, return-on-notional vol-targeted 20%.
|
||||||
|
|
||||||
|
## La diagnostica strutturale (il cuore)
|
||||||
|
|
||||||
|
| asset | IV (DVOL) | RV 1d | IV−RV 1d | RV 1h | IV−RV 1h |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| BTC | 60.9% | 56.0% | **+4.9pp** | 64.5% | −3.6pp |
|
||||||
|
| ETH | 74.9% | 76.0% | −1.0pp | 81.0% | −6.1pp |
|
||||||
|
|
||||||
|
- A **rehedge giornaliero** l'IV ≈ o > la RV → il long gamma **paga il VRP** (sei sul lato sbagliato
|
||||||
|
del premio). Perde gross, prima di ogni fee.
|
||||||
|
- A **rehedge orario** la RV *supera* l'IV (BTC +3.6pp, ETH +6.1pp gross) — sembrerebbe un edge…
|
||||||
|
**ma è una trappola doppia:** (a) la RV_1h è gonfiata da microstruttura (bid-ask bounce), quindi
|
||||||
|
il "gross edge" è in parte fittizio; (b) il rehedge orario paga **24× la fee di hedge**, che
|
||||||
|
spazza via qualunque edge intraday reale.
|
||||||
|
|
||||||
|
## I numeri (ogni variante, ogni anno, ogni frequenza: PERDE)
|
||||||
|
|
||||||
|
| variante | rehedge | Sharpe | CAGR | per-anno |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| NAKED | 1d | **−3.99** | −56% | 2021..2026 tutti −25..−68% |
|
||||||
|
| CHEAP-GATED (IVr<0.30) | 1d | −3.05 | −47% | tutti negativi |
|
||||||
|
| RICH-SKIP (IVr>0.90) | 1d | −3.92 | −55% | tutti negativi |
|
||||||
|
| NAKED | 1h | **−6.06** | −71% | tutti −38..−81% (peggio: le fee) |
|
||||||
|
| CHEAP-GATED (IVr<0.30) | 1h | −4.96 | −64% | tutti negativi |
|
||||||
|
|
||||||
|
Il gate "compra vol solo quando è a sconto" (lo specchio del gate vincente del VRP, *vendi vol solo
|
||||||
|
quando è cara*) **migliora ma non salva**: −3.05 invece di −3.99. Il segno resta negativo perché
|
||||||
|
l'IV-rank basso non garantisce RV > IV su 7 giorni.
|
||||||
|
|
||||||
|
## Scoring marginale vs TP01
|
||||||
|
|
||||||
|
Tutte le varianti: **DILUTES**, `is_hedge=False`, `has_insample_edge=False` (IS Sharpe −3..−4.5),
|
||||||
|
blend-25 uplift hold −1.2/−1.5. Non aiuta nemmeno come hedge: perde sia quando TP01 è su
|
||||||
|
(uplift −1.2..−1.6) sia quando TP01 è giù (uplift −2.5..−3.2). Corr a TP01 ≈ 0 ma il drift negativo
|
||||||
|
domina la matematica di diversificazione.
|
||||||
|
|
||||||
|
## Eseguibilità a ~$600 (muro indipendente)
|
||||||
|
|
||||||
|
- **BTC**: contratto 1 BTC, min 0.1 BTC → notional minimo **$5.968** ≫ $600 → **NON eseguibile**.
|
||||||
|
- **ETH**: min 0.1 ETH → **$157** → marginalmente OK, ma un solo straddle = ~25% del conto in premio.
|
||||||
|
|
||||||
|
Anche se l'edge esistesse, BTC è fuori e ETH è troppo grosso per diversificare.
|
||||||
|
|
||||||
|
## Conclusione
|
||||||
|
|
||||||
|
Lo "scalping con copertura in opzioni" è **schiacciato tra due muri**: rehedge lento → paghi il
|
||||||
|
premio (RV < IV); rehedge veloce → paghi le fee (24×/day). **Nessuna frequenza vince.** Conferma
|
||||||
|
due lezioni di prim'ordine del progetto, ora quantificate sul long-vol:
|
||||||
|
1. **Le fee uccidono lo scalping** (il rehedge orario, vera frequenza di scalping, è la variante
|
||||||
|
*peggiore*: −6 Sharpe).
|
||||||
|
2. **Vol-da-modello è fragile in entrambe le direzioni.** Regola gemella del VRP: *niente long-vol
|
||||||
|
scalp da modello in deploy*.
|
||||||
|
|
||||||
|
**Il VRP01 (lato short, gated, rischio-definito) resta l'unico edge opzioni del progetto** —
|
||||||
|
e funziona proprio perché sta sul lato *giusto* dello stesso premio che il gamma scalp paga.
|
||||||
|
|
||||||
|
SCARTATO. Script preservato come riferimento. Nessun impatto sul book live (branch separato).
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# 2026-06-26 — TP01 × DVOL vol-targeting: NON migliora (il taglio di DD è solo de-levering)
|
||||||
|
|
||||||
|
**Goal utente:** "continua" ad analizzare strategie. Dopo i tre angoli di diversificazione tutti
|
||||||
|
STAT-MODE (gamma scalp scartato, funding cross-sectional già morto, cash-and-carry lead non
|
||||||
|
deployabile), ho puntato sull'angolo a **più alto valore perché ESEGUIBILE**: migliorare il
|
||||||
|
risk-sizing del book **live** TP01 (BTC/ETH perp Deribit, già armato). Branch
|
||||||
|
`research/gamma-scalp-options`, script `scripts/research/tp01_dvol_overlay.py`.
|
||||||
|
|
||||||
|
## Ipotesi
|
||||||
|
|
||||||
|
TP01 vol-targeta sulla vol **realizzata** 30g (backward-looking). Il **DVOL** (vol implicita 30g
|
||||||
|
Deribit, forward-looking, che spesso anticipa i salti di vol) come denominatore → de-risking più
|
||||||
|
tempestivo prima dei crash → DD più basso / hold-out migliore senza affondare il FULL.
|
||||||
|
|
||||||
|
Onestà: DVOL parte 2021-03 → confronto TUTTE le varianti sulla **finestra comune 2021-04 → 2026**
|
||||||
|
(1891g, perdo il toro 2019-2020 pre-DVOL). Causale, fee 0.10% RT, config canonica TP01.
|
||||||
|
|
||||||
|
## Risultati (finestra comune, hold-out 2025+)
|
||||||
|
|
||||||
|
| variante (denominatore vol) | FULL Sh | FULL DD | CAGR | HOLD Sh |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| **realized** (baseline) | **+0.75** | 12.3% | +8% | +0.30 |
|
||||||
|
| dvol (implicita) | +0.70 | 9.3% | +6% | +0.33 |
|
||||||
|
| blend 50/50 | +0.72 | 10.4% | +7% | +0.32 |
|
||||||
|
| max(realiz, dvol) | +0.70 | 9.2% | +6% | +0.36 |
|
||||||
|
| derisk (gate DVOL>p90 ×0.5) | +0.75 | 12.3% | +8% | +0.30 |
|
||||||
|
|
||||||
|
Le varianti DVOL **abbassano il DD** (12.3% → ~9.2%) ma **anche** Sharpe FULL (−0.04/−0.05) e CAGR
|
||||||
|
(8% → 6%). Lo Sharpe è scale-invariant → sospetto: il taglio di DD è solo *posizioni più piccole*
|
||||||
|
(l'IV > RV per il VRP → denominatore più grande → leva minore), non vero timing.
|
||||||
|
|
||||||
|
## Il controllo decisivo
|
||||||
|
|
||||||
|
Confronto le varianti DVOL col **realized a target_vol RIDOTTO** (stesso de-levering, senza DVOL):
|
||||||
|
|
||||||
|
| | FULL Sh | DD | CAGR | HOLD Sh |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| **max-DVOL** | +0.70 | 9.2% | +6% | +0.36 |
|
||||||
|
| **realized @ vol-tgt 15%** | **+0.75** | 9.4% | +6% | +0.30 |
|
||||||
|
| **realized @ vol-tgt 13%** | +0.75 | **8.1%** | +5% | +0.30 |
|
||||||
|
|
||||||
|
**Conclusivo:** il realized a 15% eguaglia il DD del max-DVOL (9.4% ≈ 9.2%) a Sharpe **più alto**
|
||||||
|
(0.75 > 0.70) e stesso CAGR; a 13% fa DD ancora più basso (8.1%), sempre a 0.75. **Il taglio di DD
|
||||||
|
del DVOL è interamente "leva minore", replicabile MEGLIO con un semplice target_vol più basso.**
|
||||||
|
|
||||||
|
## Verdetto
|
||||||
|
|
||||||
|
**DVOL vol-targeting NON migliora TP01.** Risk-adjusted è marginalmente *peggiore* (FULL 0.70 vs
|
||||||
|
0.75 a parità di DD). L'unico residuo a favore — hold-out +0.06 (max 0.36 vs 0.30) — è **single-window**
|
||||||
|
su 5 anni scarsi di storia DVOL: sotto la soglia di **persistenza multi-cut** del progetto (la lezione
|
||||||
|
che ha ucciso 13/14 falsi positivi nello sweep alt). Il gate **DVOL-spike de-risk è RIDONDANTE col
|
||||||
|
trend**: quando il DVOL esplode (crash) TP01 è già flat (momentum negativo → posizione 0) → il gate
|
||||||
|
non tocca nulla (Δ 0.00 su tutto). Niente da deployare.
|
||||||
|
|
||||||
|
## Lezioni
|
||||||
|
|
||||||
|
1. **Per meno DD sul book live, la leva giusta è `target_vol`, non un overlay DVOL.** Abbassare TP01
|
||||||
|
da vol-tgt 20% → 15% taglia il DD 12.3% → ~9.4% allo **stesso Sharpe** (0.75). Dial pulito e noto,
|
||||||
|
se mai si volesse un profilo più difensivo (oggi 20% è la scelta canonica — invariata).
|
||||||
|
2. **La vol implicita non batte la realizzata come denominatore di sizing per un trend long-flat**:
|
||||||
|
il segnale di trend gestisce già il de-risking direzionale; il denominatore fissa solo la
|
||||||
|
magnitudine, e la realizzata lo fa marginalmente meglio per unità di rischio.
|
||||||
|
3. Conferma indiretta del soffitto: anche un raffinamento "ovvio" e *eseguibile* su TP01 non produce
|
||||||
|
alpha; al massimo sposta il punto sulla stessa frontiera rischio/rendimento.
|
||||||
|
|
||||||
|
SCARTATO come miglioria. Nessun impatto sul book live (branch separato, config canonica invariata).
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# 2026-06-26 — Calendar-vol / term-structure DVOL: il DATO STORICO NON ESISTE (data-first gate)
|
||||||
|
|
||||||
|
**Goal utente:** "continua" — angolo scelto: scaricare e certificare la term-structure DVOL per
|
||||||
|
testare un calendar-vol (front IV vs back IV). **Esito: bloccato a monte dal gate "il dato prima
|
||||||
|
della strategia" (lezione v2.0.0).** Branch `research/gamma-scalp-options`.
|
||||||
|
Script: `scripts/research/probe_vol_termstructure.py` (scan) + `log_vol_termstructure.py` (forward).
|
||||||
|
|
||||||
|
## Scan di fattibilità (prima di costruire qualunque backtest)
|
||||||
|
|
||||||
|
Cosa l'API pubblica Deribit espone davvero per la vol per-scadenza:
|
||||||
|
|
||||||
|
| dato | disponibile? |
|
||||||
|
|---|---|
|
||||||
|
| Snapshot CORRENTE della term-structure ATM (mark_iv per scadenza) | **SÌ** (book_summary_by_currency, tokenless) |
|
||||||
|
| DVOL storico | solo **30g** fisso — nessun indice 7g/60g/90g pubblico |
|
||||||
|
| Trade-history IV per strumento | solo per strumenti **VIVI** (gli scaduti spariscono) |
|
||||||
|
| **Serie storica continua front-vs-back IV** | **NO** — il front-month rotola/espira → irricostruibile |
|
||||||
|
|
||||||
|
**Snapshot oggi (2026-06-26), per riferimento:**
|
||||||
|
- BTC spot $59.9k: 7g 45.6% · 30g 43.2% · 60g 42.7% · 90g 42.9% · 180g 44.5% — **contango** lieve.
|
||||||
|
- ETH spot $1.58k: 7g 57.6% · 30g 56.4% · 90g 57.5% · 180g 59.2% — contango (+1.6pp slope).
|
||||||
|
- (I valori sono interpolati a tenor fissi: il front a 1-giorno grezzo, 38%/54%, è rumore escluso.)
|
||||||
|
|
||||||
|
## Verdetto
|
||||||
|
|
||||||
|
**Un calendar-vol NON è backtestabile ora su dati certificati.** La storia per-scadenza non è
|
||||||
|
pubblica su Deribit (la nostra fonte di verità), e — coerentemente con la metodologia — **nessun
|
||||||
|
edge va creduto senza backtest OOS su dati certificati**. Costruire un calendar-vol su uno snapshot
|
||||||
|
singolo o su una storia ricostruita-a-pezzi sarebbe esattamente il tipo di scorciatoia che ha
|
||||||
|
prodotto la libreria fasulla v2.0.0. Quindi: **stop, niente backtest.**
|
||||||
|
|
||||||
|
## L'unica via legittima: costruire il dato in avanti
|
||||||
|
|
||||||
|
`scripts/research/log_vol_termstructure.py` — logger forward idempotente: ogni run prende lo snapshot
|
||||||
|
ATM, lo interpola ai tenor fissi {7,30,60,90,180}g e appende una riga/asset a
|
||||||
|
`data/raw/vol_term_<asset>.parquet`. Seminate le prime righe BTC/ETH oggi. Test offline
|
||||||
|
`tests/test_vol_termstructure.py` (interpolazione pura).
|
||||||
|
|
||||||
|
**Non auto-cablato in cron** (è ricerca forward, e non tocco il cron live senza ok). Per accumulare:
|
||||||
|
aggiungere una riga giornaliera al crontab. Dopo **~6-12 mesi** → certificare (monotonia, spike,
|
||||||
|
cross-venue) e SOLO ALLORA testare front-vs-back. Prima, nulla.
|
||||||
|
|
||||||
|
## Lezione
|
||||||
|
|
||||||
|
Il data-first gate ha funzionato: ha fermato un backtest su un dato che non esiste, invece di
|
||||||
|
produrre un numero non falsificabile. La term-structure è in **contango** oggi (regime calmo) —
|
||||||
|
osservazione da forward-monitor, non un edge. Nessun impatto sul book live (branch separato).
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
# 2026-06-29 — DVOL come segnale DIREZIONALE/REGIME su BTC/ETH: l'unico edge è un HEDGE, non alpha
|
||||||
|
|
||||||
|
**Filone A — "DVOL-direzionale".** Tesi: usare la vol IMPLICITA Deribit (DVOL, `data/raw/dvol_*.parquet`,
|
||||||
|
2021-03→oggi) come **segnale direzionale o di regime** sul perp BTC/ETH (long-flat o L/S sul LIVELLO di
|
||||||
|
mercato), non come denominatore del vol-target. Script `scripts/research/dvol_directional.py`, test
|
||||||
|
`tests/test_dvol_directional.py`. Branch `research/equity-meanrev-ib` (separato, nessun impatto live).
|
||||||
|
|
||||||
|
## Cosa è diverso dal lavoro DVOL precedente
|
||||||
|
|
||||||
|
- `tp01_dvol_overlay.py` (2026-06-26): DVOL come **denominatore** del vol-target → solo de-levering, SCARTATO.
|
||||||
|
- VOL03/04/10/11 (sweep alt 100-agent): DVOL che **gata/scala un TSMOM** → eredita lo Sharpe di trend di
|
||||||
|
TP01 (il marginal scorer li boccia NEUTRAL/REDUNDANT).
|
||||||
|
- `agent_14_dvol_spread` (onda ortho): IV **relativa BTC-vs-ETH**, market-NEUTRAL 2-leg (l'unico LEAD vivo).
|
||||||
|
|
||||||
|
Qui invece: DVOL/IV-RV come segnale **direzionale STANDALONE sul livello di mercato**, per vedere se porta
|
||||||
|
alpha direzionale **ortogonale** a TP01 (non un overlay sul trend). Tutto causale (decisione ≤ `close[i]`,
|
||||||
|
tenuta in `i+1` dallo shift di `eval_weights`), fee 0.10% RT, vol-target 20%, leva cap 2x, harness onesto
|
||||||
|
condiviso `altlib`. Allineamento DVOL: `al.dvol` (merge_asof backward) → DVOL close del giorno *t* (≈
|
||||||
|
contemporaneo a `close[i]`), noto a decisione; verificato leak-free (`causality_ok` max_tail_diff 0.0).
|
||||||
|
|
||||||
|
DVOL parte 2021-03 → pre-DVOL il segnale è flat. Valuto sia **FULL** (col flat pre-2021, deflaziona lo
|
||||||
|
Sharpe) sia **ERA-only** (da 2021-10, equo per la tesi).
|
||||||
|
|
||||||
|
## 1) Probe direzionale (corr segnale[i] vs ritorno[i+1], pooled BTC+ETH, era DVOL)
|
||||||
|
|
||||||
|
| segnale | corr | long bps | flat bps | **edge bps** | frac_long |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| VRP-Z+ (long VRP ricco, IV-RV alto) | +0.010 | +7.8 | −0.9 | **+8.6** | 0.52 |
|
||||||
|
| VRP-Z− (flip) | −0.017 | −0.9 | +7.8 | −8.6 | 0.48 |
|
||||||
|
| **DVOL-LV fear (rank>0.5 → long)** | **+0.024** | +20.4 | +0.9 | **+19.5** | 0.14 |
|
||||||
|
| DVOL-LV calm (rank<0.5 → long) | −0.013 | +0.8 | +21.2 | −20.4 | 0.86 |
|
||||||
|
| DVOL-MOM (DVOL in calo → long) | −0.041 | −10.0 | +22.9 | **−32.9** | 0.59 |
|
||||||
|
| VRP>0 (quasi buy&hold) | +0.003 | +5.3 | −0.7 | +5.9 | 0.72 |
|
||||||
|
|
||||||
|
Lettura: l'unico contenuto direzionale reale è **"buy-the-fear"** (long quando il DVOL è alto nel suo
|
||||||
|
percentile espandente: edge +19.5 bps il giorno dopo). Il VRP-ricco→long è coerente ma debolissimo (+8.6).
|
||||||
|
Il "DVOL in calo → long" è **negativo** (−33 bps): comprare quando la paura scende è perdente (regola
|
||||||
|
falsificata). Nota: il percentile espandente è dominato dai picchi 2021-22 → "fear" scatta solo nel 14%
|
||||||
|
delle barre (gli spike veri), non metà del tempo.
|
||||||
|
|
||||||
|
## 2) Robustezza assoluta (study_weights 1d, fee sweep 0.00-0.20% RT) — ERA-only il giudizio equo
|
||||||
|
|
||||||
|
| segnale | abs | minFull Sh | minHold Sh | feeOK | **ERA Sh** | ERA DD | ERA CAGR |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| **DVOL-fear q0.4 LF** | WEAK | +0.39 | +0.14 | sì | **+0.59** | **8%** | +5% |
|
||||||
|
| VRP-Z60 LF | FAIL | +0.42 | −0.39 | sì | +0.46 | 23% | +7% |
|
||||||
|
| DVOL-fear q0.5 LF | FAIL | +0.31 | −0.17 | sì | +0.40 | 10% | +3% |
|
||||||
|
| VRP-Z90 LF | FAIL | +0.32 | −0.26 | sì | +0.32 | 29% | +4% |
|
||||||
|
| VRP-Z90 L/S | FAIL | +0.25 | +0.32 | sì | +0.31 | 28% | +3% |
|
||||||
|
| DVOL-calm q0.5 LF | FAIL | +0.00 | −1.05 | no | +0.09 | 37% | −0% |
|
||||||
|
| DVOL-MOM k10/k20 LF | FAIL | −0.6/−0.7 | <−1 | no | −0.6/−0.7 | ~40% | ~−10% |
|
||||||
|
| DVOL-MOM k10 L/S | FAIL | −1.16 | −1.29 | no | −1.23 | 72% | −23% |
|
||||||
|
|
||||||
|
Nessun segnale supera lo Sharpe assoluto in **valore** (tutti < 0.6, soffitto direzionale BTC/ETH ~1.3
|
||||||
|
confermato). Unico con profilo decente: **DVOL-fear q0.4 long-flat** (ERA Sh +0.59, DD **8%**) — il DD
|
||||||
|
bassissimo perché è long solo nei picchi di paura (poche barre, mean-reversion). Le famiglie momentum e
|
||||||
|
"buy-the-calm" sono nettamente negative e non sopravvivono nemmeno alle fee.
|
||||||
|
|
||||||
|
## 3) Marginal vs TP01 (il gate vero) — DVOL-fear q0.4 long-flat
|
||||||
|
|
||||||
|
| | corr→TP01 | in-sample Sh | uplift_full w25 | uplift_hold w25 | multi-cut persistente | is_hedge | verdetto |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| **ERA-only** (2021-10+) | 0.37 | 0.54 (has_edge) | +0.06 | +0.17 | sì | **No** | **ADDS** (earns_slot=True) |
|
||||||
|
| **FULL** (gate ufficiale) | 0.28 | 0.64 (has_edge) | +0.03 | +0.17 | sì | **Sì** | **HEDGE** (earns_slot=**False**) |
|
||||||
|
|
||||||
|
Il candidato ha tutto ciò che serve a un nuovo sleeve **tranne una cosa**: il gate canonico full-history
|
||||||
|
lo classifica **HEDGE**, non alpha. `hedge_yearly_corr(TP01-Sh, uplift) = −0.76`, `uplift TP01-up −0.003`
|
||||||
|
/ `uplift TP01-down +0.194` → **paga SOLO quando TP01 è debole**. Ha perfettamente senso: il DVOL esplode
|
||||||
|
durante i crash, proprio quando il trend di TP01 è flat/in-drawdown; comprare la paura cattura il rimbalzo
|
||||||
|
e smorza il DD del portafoglio, ma **non è un premio permanente**. Il blend 0.75·TP01 + 0.25·cand alza
|
||||||
|
l'hold-out 0.30→**0.47** (w50 → 0.62) e taglia il DD a ~11%/8% — un effetto **drawdown-dampener** reale,
|
||||||
|
ma classificato (giustamente) come hedge.
|
||||||
|
|
||||||
|
Perché ERA dice ADDS e FULL dice HEDGE: il rivelatore di hedge richiede ≥3 anni; sulla finestra ERA
|
||||||
|
(2021-10+) la `corr(TP01-Sh annuale, uplift)` è meno netta e non scatta. **Il gate full-history è quello
|
||||||
|
giusto** (più dati per il test di hedge) → mi attengo a quello: **HEDGE, non alpha.**
|
||||||
|
|
||||||
|
Gli altri tre candidati (VRP-Z60/90, DVOL-fear q0.5): **NEUTRAL o HEDGE**, mai ADDS. I VRP-Z hanno beta a
|
||||||
|
TP01 alto (0.88-0.90) → sono trend-correlati travestiti (resid Sharpe ~0), uplift hold-out negativo.
|
||||||
|
|
||||||
|
## 4) Causalità, alignment, eseguibilità, sign-falsification
|
||||||
|
|
||||||
|
- **Causale**: `causality_ok` ok=True, max_tail_diff **0.0** su tutti i candidati (rank espandente +
|
||||||
|
z-score ricalcolati su prefisso = identici → nessun future-peeking). Test dedicato `test_expanding_rank_is_causal`.
|
||||||
|
- **Alignment DVOL**: laggando il segnale di +1 giorno (extra-conservativo, usa solo DVOL di ieri) l'ERA
|
||||||
|
Sharpe del leader scende +0.59 → **+0.45**. L'edge sopravvive ma è **sensibile all'allineamento**
|
||||||
|
(parte del vantaggio è nel reagire allo spike DVOL del giorno stesso) → cautela in esecuzione.
|
||||||
|
- **Eseguibile a $600**: `eval_weights_smallcap` (min_order $5, cap $300/asset): haircut Sharpe **≈ 0.00**
|
||||||
|
su BTC ed ETH (segnale long-flat a basso turnover, ~210-270 trade su tutta la storia) → lo Sharpe netto
|
||||||
|
onesto a $600 = quello modellato. È l'unico aspetto pienamente promosso.
|
||||||
|
- **Sign-falsification**: tesi buy-the-fear ERA Sh +0.40 vs flip buy-the-calm +0.09; VRP-Z+ +0.32 vs
|
||||||
|
VRP-Z− (flip) −0.14. Il segno della tesi è quello giusto (l'edge è reale, non rumore simmetrico).
|
||||||
|
|
||||||
|
## Verdetto onesto: **LEAD (hedge / drawdown-dampener), NON sleeve.** SCARTATO come alpha direzionale.
|
||||||
|
|
||||||
|
- Nessun segnale DVOL-direzionale ha Sharpe assoluto sopra ~0.6 → **conferma il soffitto BTC/ETH
|
||||||
|
direzionale ~1.3** (la via per superarlo resta il cross-sectional XS01, non un nuovo segnale a 2 asset).
|
||||||
|
- L'unico con edge in-sample reale + eseguibile + leak-free è **DVOL-fear "buy-the-fear" long-flat**, ma il
|
||||||
|
gate canonico lo bolla **HEDGE** (`is_hedge=True`, paga solo quando TP01 è in difficoltà). Per la regola
|
||||||
|
del progetto un hedge **non si arma come slot di alpha** → **earns_slot=False**.
|
||||||
|
- È comunque un **LEAD utile in forward-monitor** come **smorzatore di drawdown** del portafoglio: il blend
|
||||||
|
porta l'hold-out 0.30→0.47/0.62 e abbassa il DD. Se in futuro si vorrà esplicitamente un tail/DD-hedge
|
||||||
|
*eseguibile* (a differenza del tail-hedge in opzioni, non eseguibile a $600), questo è il candidato più
|
||||||
|
pulito visto finora — ma da prezzare **come hedge**, non come ritorno atteso permanente.
|
||||||
|
|
||||||
|
## Caveat
|
||||||
|
|
||||||
|
1. **Storia DVOL <5 anni** (2021-03→oggi, ~4.7 anni; era valutata 2021-10+) → multiple-testing e
|
||||||
|
finestra corta. Il multi-cut 2026 è un assurdo +1.944 (manciata di giorni, picco DVOL recente):
|
||||||
|
l'uplift hold-out è trascinato dal regime 2025-26 ad alta-paura, non da un edge stazionario.
|
||||||
|
2. **È un hedge, non alpha**: il valore è condizionato ai drawdown di TP01 (2022, ed eventuali futuri).
|
||||||
|
In un toro lungo a bassa vol il segnale resta **flat l'86% del tempo** e non contribuisce nulla.
|
||||||
|
3. **Sensibile all'allineamento** DVOL (haircut −0.14 Sharpe col lag +1g): l'esecuzione reale dovrebbe
|
||||||
|
usare il DVOL già stampato a decisione, non assumere il close contemporaneo.
|
||||||
|
4. Long-flat → cattura il rimbalzo post-paura ma **non protegge** durante la discesa (non è uno short):
|
||||||
|
è "buy-the-dip gated by fear", con tutti i rischi del comprare coltelli che cadono se il regime peggiora.
|
||||||
|
|
||||||
|
Nessun impatto sul book live (branch separato, nessuno sleeve registrato, config canonica invariata).
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
# 2026-06-29 — Filone B: INTRADAY REGIME BTC/ETH — ERM (efficiency-ratio) è un LEAD reale, forward-monitor
|
||||||
|
|
||||||
|
**Tesi.** Cercare un meccanismo **sub-daily** sui dati certificati 1h→12h BTC/ETH che sia ortogonale
|
||||||
|
**sia** a TP01 (TSMOM trend daily, long-flat) **sia** a SKH01 (regime BuzVola/BuzVolume + Donchian a
|
||||||
|
230m). SKH01 prova che il sub-daily PUÒ funzionare ed essere quasi-ortogonale: qui un MECCANISMO DIVERSO,
|
||||||
|
basato sulla **qualità del moto intraday** (efficiency-ratio / vol-expansion / thrust) come **regime** che
|
||||||
|
condiziona una posizione direzionale tenuta ~1 giorno. Script `scripts/research/intraday_regime.py`, test
|
||||||
|
`tests/test_intraday_regime.py`. Branch `research/equity-meanrev-ib` (separato, nessun impatto live).
|
||||||
|
|
||||||
|
Killer ricorrente del progetto sotto le 12h = **muro-fee 0.10% RT + overfitting**. Ricetta SKH01:
|
||||||
|
decisione sub-daily ma **hold ~1 giorno** → pochi trade → la fee non uccide. Ogni meccanismo qui è
|
||||||
|
costruito a basso turnover e giudicato col **fee-sweep alla sua frequenza reale**.
|
||||||
|
|
||||||
|
## Meccanismi provati (tutti come posizione CONTINUA decisa ≤ `close[i]`, causali)
|
||||||
|
|
||||||
|
| | meccanismo |
|
||||||
|
|---|---|
|
||||||
|
| **ERM** | **Efficiency-Ratio regime momentum** (Kaufman): ER = \|moto netto su L barre\| / \|percorso\|. Prendi la direzione del moto netto **solo quando ER ≥ soglia** (regime intraday "pulito"/trendy), altrimenti flat |
|
||||||
|
| VEM | Vol-Expansion Momentum: direzione = segno del moto, attiva solo quando vol-corta > vol-lunga |
|
||||||
|
| VBR | Volatility/thrust breakout (Larry-Williams ROLLING, no calendario): segui i moti > k·ATR |
|
||||||
|
| TOD | Time-of-day seasonality — **CONTROLLO calendario**, incluso APPOSTA per `day_boundary_robust` |
|
||||||
|
|
||||||
|
## Selezione + fee-sweep a frequenza reale (vincitori per famiglia, min-asset)
|
||||||
|
|
||||||
|
```
|
||||||
|
ERM: 8h L=2.0 thr=0.35 L/S minFull +0.88 minHold +0.99 f@.20 +0.55 feeOK=True turn/y 125
|
||||||
|
VEM: 12h Lmom=3 Ls=2 Ll=10 minFull +0.72 minHold -0.29 f@.20 +0.51 feeOK=True (hold<0 → out)
|
||||||
|
VBR: 12h k=1.0 atr=14 L/S minFull +0.33 minHold +1.20 f@.20 +0.25 feeOK=True
|
||||||
|
TOD: 1h minFull -3.99 minHold -4.49 feeOK=False (controllo → FAIL atteso)
|
||||||
|
```
|
||||||
|
|
||||||
|
Solo **ERM** e **VBR** sopravvivono al gate fee+OOS. VEM ha hold-out negativo. TOD esplode (vedi sotto).
|
||||||
|
|
||||||
|
## Il candidato: ERM 8h L=2.0 thr=0.35 (L/S) — `earns_slot=True`
|
||||||
|
|
||||||
|
```
|
||||||
|
abs=PASS marginal=ADDS EARNS_SLOT=True
|
||||||
|
standalone: cand FULL 0.919 / HOLD 1.105 (TP01 ref FULL 1.297 / HOLD 0.303)
|
||||||
|
corr->TP01 full 0.146 hold 0.057 beta 0.197 resid Sharpe 0.738 alpha/yr 0.121
|
||||||
|
robust_oos=True (clean-year +0.103, drop-best-month +0.283)
|
||||||
|
multi-cut persistence: 2020 .166 / 2021 .167 / 2022 .298 / 2023 .337 / 2024 .418 / 2025 .497 / 2026 2.861 persistent=True
|
||||||
|
has_insample_edge=True (standalone PRE-holdout Sharpe 0.874, > null-pctl 0.317)
|
||||||
|
is_hedge=False (yearly corr(TP01-Sh, uplift) -0.762; paga sia TP01-up 0.139 sia TP01-down 0.384)
|
||||||
|
blend w25: FULL 1.481 (+0.184) HOLD 0.80 (+0.497) DD 11.0%
|
||||||
|
corr con SKH01: full 0.278 hold 0.291
|
||||||
|
day_boundary=INVARIANT (spread 0.0) haircut $600 ≈ 0.00 su BTC/ETH
|
||||||
|
```
|
||||||
|
|
||||||
|
È il **primo sub-daily da SKH01** a passare TUTTI i gate del marginal scorer indurito: edge in-sample
|
||||||
|
reale (non diversification-math), persistenza multi-cut, robust_oos, NON hedge, scorrelato a TP01 (0.15).
|
||||||
|
|
||||||
|
### ERM aggiunge OLTRE SKH01? (gate #5 — il book esistente ne ha già uno sub-daily)
|
||||||
|
|
||||||
|
| blend | FULL Sh | FULL DD | HOLD Sh | HOLD DD |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| TP01 | +1.30 | 14.3% | +0.30 | 7.5% |
|
||||||
|
| TP01+SKH 75/25 | +1.78 | 9.4% | +1.17 | 6.2% |
|
||||||
|
| **TP01+SKH+ERM 60/25/15** | **+1.88** | 8.9% | **+1.46** | 5.3% |
|
||||||
|
| TP01+SKH+ERM 55/20/25 | +1.84 | 8.8% | +1.48 | 5.1% |
|
||||||
|
|
||||||
|
Sì: corr con SKH01 solo 0.28 → ERM **aggiunge oltre SKH** (FULL +0.10, HOLD +0.29 a peso 15%, DD ancora
|
||||||
|
giù). Non è SKH01 travestito.
|
||||||
|
|
||||||
|
## Il controllo TOD (calendario) — fa esattamente ciò che doveva
|
||||||
|
|
||||||
|
`TOD` (direzione per ora-del-giorno, media espandente causale) è incluso come **trappola**: è il tipo di
|
||||||
|
effetto che uccise `open_drive` (artefatto di etichettatura UTC). Esito: **FAIL** (FULL −3.99, 31.811
|
||||||
|
trade, fee-killed), marginal=DILUTES. `day_boundary_robust=INVARIANT` → l'effetto è **robustamente
|
||||||
|
negativo** a ogni offset (non un artefatto di confine giorno: è proprio che la time-of-day-direzionale non
|
||||||
|
ha edge e sanguina fee). Il controllo conferma che l'harness non si fa ingannare e che il segno
|
||||||
|
dei segnali di prezzo (ERM/VBR) è reale, non rumore di calendario.
|
||||||
|
|
||||||
|
## Causalità / eseguibilità
|
||||||
|
|
||||||
|
- **Leak-free**: `causality_ok=True`, max_tail_diff **0.0** su tutti i candidati (ER, rank espandente,
|
||||||
|
medie su prefisso = identiche → nessun future-peeking). Test dedicato in `test_intraday_regime.py`.
|
||||||
|
- **day_boundary_robust=INVARIANT** (spread 0.0) per ERM/VBR/TOD: segnali di prezzo, non di calendario.
|
||||||
|
- **Eseguibile a $600**: `eval_weights_smallcap` haircut **≈ 0.00** su BTC ed ETH. **MA** ERM a 8h fa
|
||||||
|
~3.158 trade BTC / ~2.823 ETH su tutta la storia (turnover 125/anno): haircut nullo nel modello, ma
|
||||||
|
l'esecuzione reale sub-daily sul book è operativamente più pesante di un segnale 1d (slippage/spread
|
||||||
|
intraday non interamente catturati dalla fee proporzionale).
|
||||||
|
|
||||||
|
## Caveat (perché LEAD, non sleeve)
|
||||||
|
|
||||||
|
1. **Plateau hold-out a UNA SOLA RIGA.** Il FULL è robusto su tutta la griglia L∈[2.0,3.0] (+0.6..+1.0),
|
||||||
|
ma l'**hold-out è positivo SOLO a L=2.0** (a L=2.5/3.0 crolla a −0.5..−0.8). Il plateau sul full è
|
||||||
|
ampio, quello che conta — l'hold-out — è single-row. Da rinforzare prima di credere alla stazionarietà.
|
||||||
|
2. **Standalone FULL 0.92 < soffitto ~1.3.** Coerente col soffitto direzionale BTC/ETH: il valore di ERM
|
||||||
|
è **marginale/diversificante** (corr 0.15 a TP01, 0.28 a SKH01), non assoluto. Non rompe il soffitto.
|
||||||
|
3. **Multiple-testing non deflazionato.** 102 celle testate (60 ERM + 16 VEM + 24 VBR + 2 TOD) senza
|
||||||
|
deflated-Sharpe (a differenza del filone C). Il multi-cut 2026 = +2.861 è una manciata di giorni che
|
||||||
|
gonfia. Storia sub-daily certificata utile ~quanto SKH01 → finestra non lunghissima.
|
||||||
|
4. **Esecuzione 8h** = complessità operativa reale (vedi sopra), oltre il modello a haircut nullo.
|
||||||
|
|
||||||
|
## Analisi di robustezza / de-bias (`intraday_regime_analysis.py`) — il lead NON regge
|
||||||
|
|
||||||
|
I caveat #1 (plateau hold-out single-row) e #3 (multiple-testing) erano i sospetti giusti. Tre test di
|
||||||
|
de-bias li trasformano da sospetto in **bocciatura** dello slot:
|
||||||
|
|
||||||
|
| test | esito |
|
||||||
|
|---|---|
|
||||||
|
| **A) Deflated-Sharpe** (Bailey & Lopez de Prado) su 122 trial cercati | **FAIL.** DSR 0.000 (tutti) / **0.163 (escludendo i trap TOD)** / 0.241 (solo-ERM) — tutti ≪ 0.95. Lo Sharpe winner (0.92) è sotto lo Sharpe-max-atteso-null (1.16–2.51): il search ha trovato celle a 1.6 full / 1.7 in-sample, il winner 0.92 **non è eccezionale**. |
|
||||||
|
| **B) Selezione IN-SAMPLE-only** (scelgo la cella ERM col solo Sharpe < 2025) | **earns_slot=False.** La cella migliore pre-hold-out è un'**ALTRA** (8h L=2.0 thr=0.4 **long-flat**), con corr→TP01 **0.53** (è trend-beta travestito) → marginal=**NEUTRAL**. Il winner max-hold **non si seleziona senza guardare l'hold-out** → il suo `earns_slot=True` era **selezione-sull'hold-out**. |
|
||||||
|
| **C) Ensemble del plateau** (media 20 celle L×thr, niente cherry-pick) | **earns_slot=False.** marginal=ADDS, in-sample Sh 1.01, corr→TP01 0.18 — ma **`robust_oos=False`** (clean-year + jackknife): l'uplift hold-out è trascinato dal **2026 (+2.09 multicut)**, manciata di giorni. |
|
||||||
|
|
||||||
|
**Dove vive l'(eventuale) edge** (per-anno, blend 3-way 60/25/15 vs 2-way 75/25): uplift FULL solo **+0.10**,
|
||||||
|
**negativo nel 2021 (−0.23) e 2022 (−0.15)**, positivo altrove; l'uplift HOLD **+0.30 è concentrato nel
|
||||||
|
2026 (+0.46)**. corr(ERM,SKH) 0.28 full (fino a 0.42 in alcuni anni) → **parziale sovrapposizione con SKH**,
|
||||||
|
non ortogonalità piena.
|
||||||
|
|
||||||
|
**Lettura.** Il segnale efficiency-ratio non è rumore puro (l'ensemble ha in-sample Sh ~1.0, positivo nella
|
||||||
|
maggior parte degli anni), ma come **slot** fallisce ogni de-bias: il `earns_slot=True` della scoperta era
|
||||||
|
prodotto da **(1) selezione della cella sull'hold-out** + **(2) coda 2026** + **(3) multiple-testing non
|
||||||
|
corretto**. È lo stesso falso-positivo che l'alt-sweep 100-agent imparò a uccidere — qui ucciso dai gate.
|
||||||
|
|
||||||
|
## Caveat originari (ora risolti dall'analisi sopra)
|
||||||
|
|
||||||
|
1. ~~Plateau hold-out single-row~~ → **confermato fatale**: l'edge hold-out a L=2.0 è cell-selection.
|
||||||
|
2. ~~Multiple-testing non deflazionato~~ → **deflazionato: DSR FAIL** anche senza i trap.
|
||||||
|
3. Esecuzione 8h: irrilevante ormai (lo slot non c'è).
|
||||||
|
|
||||||
|
## Verdetto onesto: **NON è uno slot. Falso positivo da selezione-hold-out + coda 2026.** SCARTATO come sleeve.
|
||||||
|
|
||||||
|
Lo "earns_slot=True" della scoperta **non sopravvive** alla de-selezione: deflated-Sharpe FAIL (anche
|
||||||
|
escludendo i controlli), selezione in-sample-only → NEUTRAL su un'altra cella (trend-beta corr 0.53),
|
||||||
|
ensemble del plateau → robust_oos FAIL. **Conferma ennesima del soffitto direzionale BTC/ETH ~1.3**: un
|
||||||
|
segnale a 2 asset non lo supera; la via resta il cross-sectional (XS01). Resta al più una **curiosità in
|
||||||
|
forward-monitor** (l'efficiency-ratio ha un debole edge in-sample reale), ma da non armare come alpha.
|
||||||
|
Gli altri tre meccanismi: VBR=NOISE, VEM hold-out<0, TOD=controllo FAIL come atteso.
|
||||||
|
|
||||||
|
**Lezione harness (CODIFICATA).** Lo script di scoperta selezionava il vincitore per `min_hold` massimo
|
||||||
|
sulla griglia = **selezione-sull'hold-out**, il punto cieco che ha generato il falso PASS. Il marginal
|
||||||
|
scorer da solo non basta se la *cella* è scelta guardando l'hold-out: serve **(a)** scegliere la cella
|
||||||
|
in-sample-only (o walk-forward) **prima** di valutare il marginal, e **(b)** deflazionare per il numero di
|
||||||
|
celle/famiglie testate. Stesso buco segnalato per CC01 ("Sharpe implausibile") e per l'alt-sweep
|
||||||
|
(hold-out-fitting): qui in forma "selection-on-holdout". Ora è **codice** in `altlib` (LESSON 4): tre
|
||||||
|
funzioni nuove — `deflated_sharpe()` (Bailey & Lopez de Prado), `select_cell_insample()` (scelta cella
|
||||||
|
col solo Sharpe pre-HOLDOUT), e il gate combinato **`study_family_honest()`** (`earns_slot_honest =
|
||||||
|
earns_slot[cella in-sample] AND deflated-Sharpe≥0.95`). Verificato: su ERM il gate ritorna
|
||||||
|
`earns_slot_honest=False` (cella in-sample = trend-beta NEUTRAL, DSR 0.74). Analisi completa in
|
||||||
|
`scripts/research/intraday_regime_analysis.py`; test in `tests/test_harness_realism.py`.
|
||||||
|
**Regola nuova: una strategia direzionale grid-searched si giudica con `study_family_honest`, non
|
||||||
|
chiamando `study_marginal` sulla cella a max hold-out.**
|
||||||
|
|
||||||
|
Nessun impatto sul book live (branch separato, config canonica invariata).
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# 2026-06-29 — Filone D: MACRO regime-gate sul book crypto — RIDONDANTE col trend, SCARTATO
|
||||||
|
|
||||||
|
**Tesi.** Usare segnali macro/cross-market — equity (SPY/QQQ/IWM), credito (HYG/LQD), oro (GLD/SLV),
|
||||||
|
tassi (TLT/IEF) — come **gate risk-on/risk-off** sul book BTC/ETH, per migliorare il *timing* del
|
||||||
|
drawdown di TP01: regime risk-off (credito che cede, equity sotto trend, fuga sui bond) → riduci/azzera
|
||||||
|
l'esposizione crypto; risk-on → lascia agire TP01. **È eseguibile** (gata solo BTC/ETH perp, non aggiunge
|
||||||
|
gambe). Script `scripts/research/macro_regime_gate.py`, test `tests/test_macro_regime_gate.py`. Branch
|
||||||
|
`research/equity-meanrev-ib` (separato, nessun impatto live). Dati: `data/raw/eq_*_1d.parquet` (ETF daily,
|
||||||
|
allineati causale `merge_asof backward` sul calendario SPY).
|
||||||
|
|
||||||
|
**NON è un lead-lag direzionale** (già morto: diari 2026-06-22/-23). L'angolo nuovo = un overlay
|
||||||
|
binario/continuo di **de-risk** sul book esistente.
|
||||||
|
|
||||||
|
## Il rischio noto in partenza (CLAUDE.md)
|
||||||
|
|
||||||
|
La lezione DVOL-spike (2026-06-26): un gate di de-risk rischia di essere **ridondante col trend** — TP01
|
||||||
|
è già long-flat e va a 0 nei crash (Δ 0.00). Questo script DOVEVA dimostrare che il gate aggiunge **oltre**
|
||||||
|
quel che TP01 fa da solo. **Non ci riesce.**
|
||||||
|
|
||||||
|
## Gate provati (tutti causali: SMA/ratio/z-score rolling, merge_asof backward)
|
||||||
|
|
||||||
|
`SPY>MA{50,100,150,200,250}`, `HYG>MA`, ratio di credito `HYG/LQD`, `HYG/IEF`, combo a maggioranza
|
||||||
|
(SPY+HYG+HYG/LQD, binario e continuo), flight-to-quality (`TLTup&SPYdn`, `GLDup&SPYdn`), regime di vol
|
||||||
|
equity `SPYvol_z1` (proxy "VIX-spike" senza VIX). g_off ∈ {0.0, 0.5}.
|
||||||
|
|
||||||
|
## Esiti
|
||||||
|
|
||||||
|
I gate di trend equity alzano lo Sharpe FULL della combo (SPY>MA150/200/250 → FULL ~1.22-1.25 vs TP01-solo
|
||||||
|
1.30) ma **non l'hold-out** (HOLD ~0.17-0.19) → è **de-levering**, non timing: tagliano DD spegnendo il
|
||||||
|
book in mercati equity laterali, non anticipando i crash crypto. Il miglior profilo DD è `SPYvol_z1`.
|
||||||
|
|
||||||
|
### Deep-dive `SPYvol_z1` — il controllo decisivo (ridondanza col trend)
|
||||||
|
|
||||||
|
| | exposure TP01 risk-off | exposure risk-on | giorni risk-off | **giorni in cui il gate LAVORA** | corr(1-gate, 1-expo) |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| BTC | 0.023 | 0.093 | 18% | **3%** | 0.278 |
|
||||||
|
| ETH | 0.013 | 0.068 | 16% | **2%** | 0.296 |
|
||||||
|
|
||||||
|
"Il gate lavora" = giorno risk-off **E** posizione TP01 non già flat. Accade nel **2-3% dei giorni**: nel
|
||||||
|
resto, quando il gate vorrebbe de-riskare, TP01 è **già flat da solo**. Il gate spegne un motore già spento.
|
||||||
|
|
||||||
|
### Marginal scorer vs TP01 (gate come candidato-sleeve)
|
||||||
|
|
||||||
|
```
|
||||||
|
abs=FAIL marginal=REDUNDANT EARNS_SLOT=False
|
||||||
|
corr->TP01 full 0.989 hold 0.984 beta 0.978 resid Sharpe 0.739
|
||||||
|
multi-cut persistent=True has_insample_edge=True (standalone 1.634) is_hedge=False
|
||||||
|
blend w25: full 1.323 (+0.026) hold 0.257 (-0.046) blend w50: full 1.348 (+0.051) hold 0.21 (-0.093)
|
||||||
|
```
|
||||||
|
|
||||||
|
corr→TP01 **0.989**, beta 0.98: lo stream gated **È** TP01. L'uplift hold-out è **negativo** (de-levering
|
||||||
|
che toglie ritorno fuori dai crash). overlay-delta (gated−solo) come stream a sé: `verdict=NEUTRAL`.
|
||||||
|
|
||||||
|
## Causalità / eseguibilità (gli unici aspetti promossi)
|
||||||
|
|
||||||
|
- **Leak-free**: `causality_ok=True` (max_tail_diff 0.0); STRICT align (1 barra equity extra di lag) →
|
||||||
|
FULL invariato; `day_boundary_robust=INVARIANT` (spread 0.0). Allineamento macro→crypto backward-only.
|
||||||
|
- **Eseguibile a $600**: haircut `eval_weights_smallcap` ≈ **0.00** su BTC ed ETH. Fee-survivente
|
||||||
|
(0.00→0.30% RT: 1.42→1.33). Tecnicamente impeccabile — semplicemente **non aggiunge alpha**.
|
||||||
|
|
||||||
|
## Verdetto: **SCARTATO (ridondante col trend).** earns_slot=False.
|
||||||
|
|
||||||
|
Conferma esatta della lezione in CLAUDE.md: un overlay di de-risk su un book che è **già** long-flat e
|
||||||
|
già a 0 nei crash non ha quasi nulla da fare (lavora il 2-3% dei giorni, corr→TP01 0.99). Il taglio di DD
|
||||||
|
che mostra è **de-levering**, replicabile meglio con un `target_vol` più basso (stessa lezione del
|
||||||
|
TP01×DVOL overlay del 2026-06-26). Il macro-regime **non anticipa** i drawdown crypto meglio del prezzo
|
||||||
|
crypto stesso. Nessuno sleeve registrato; nessun impatto sul book live.
|
||||||
|
|
||||||
|
**Sottoprodotto utile:** il gate `redundancy_diag` (exposure risk-off vs risk-on + "quota giorni in cui il
|
||||||
|
gate lavora") è il test pulito da riusare ogni volta che si propone un overlay di de-risk — se "il gate
|
||||||
|
lavora" è basso e corr→TP01 ~1, è ridondante, a prescindere dallo Sharpe FULL che mostra.
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
# 2026-06-29 — Ricerca strategie a 5 thread paralleli: 0 nuovi sleeve, 1 LEAD che rompe 2 muri su 3
|
||||||
|
|
||||||
|
**Mandato.** "Cerca altre strategie" (+ "senza correlazioni, a segnale" + "a 1 gamba in/out a segnale").
|
||||||
|
Cinque ricerche parallele su aree genuinamente inesplorate, tutte sull'harness onesto condiviso
|
||||||
|
(`altlib` + `xsec_v2_nonmom`) con TUTTI i gate: causalità (prefix-check), netto fee 0.10% RT + sweep,
|
||||||
|
OOS hold-out 2025+, **deflated-Sharpe** (multiple-testing), **gate anti-selection-on-holdout**
|
||||||
|
(`study_family_honest`: cella scelta IN-SAMPLE-only), marginal vs TP01/XS01, corr vs book, haircut $600.
|
||||||
|
Branch `main`, **nessun impatto live** (solo file nuovi, nessuno sleeve registrato).
|
||||||
|
|
||||||
|
Premessa onesta (base-rate): il soffitto direzionale BTC/ETH ~1.3 è già confermato e lo sweep di 104
|
||||||
|
ipotesi (2026-06-20) non produsse nulla di nuovo che reggesse. L'attesa era "quasi tutto SCARTATO".
|
||||||
|
|
||||||
|
## Esiti per thread
|
||||||
|
|
||||||
|
| # | Thread | File | Verdetto |
|
||||||
|
|---|--------|------|----------|
|
||||||
|
| 1 | **XSEC low-risk cousins** (MAX/lottery, idio-vol, Amihud) | `xsec_v3_lowrisk.py` | 1 LEAD (IVOL), 0 sleeve |
|
||||||
|
| 2 | **XSEC momentum-structure** (risk-adj/accel/frog-in-pan/vol-managed vs XS01) | `xsec_v3_momstruct.py` | tutto REDUNDANT/SCARTATO |
|
||||||
|
| 3 | **Meta-allocazione** (allocazione dinamica tra i 4 sleeve) | `meta_allocation.py` | pesi fissi vincono |
|
||||||
|
| 4 | **Segnali ortogonali ETH/BTC** (relative-value dollar-neutral) | `orthogonal_signals.py` | 2 LEAD (STATARB, DVOLSPREAD) |
|
||||||
|
| 5 | **1-gamba a segnale** (MACD/RSI/Supertrend/Donchian/BBands/EMA) | `signal_inout_1leg.py` | 0/12 earns_slot |
|
||||||
|
|
||||||
|
**Netto: 0 nuovi sleeve.** Il soffitto regge. Test 143/143.
|
||||||
|
|
||||||
|
### Thread 1 — Low-risk cross-sectional (51 alt HL)
|
||||||
|
Tre fattori mai provati (il filone C aveva fatto total-vol e BAB, non questi). **IVOL** (idiosyncratic-vol
|
||||||
|
basso, 19-major B30 H5 k8): FULL 1.06 / HOLD **1.22** / corr ~0 a XS01 e TP01 / positivo ogni anno /
|
||||||
|
uplift portafoglio HOLD +0.42/+0.50 — l'unico LEAD di valore. **MAX** e **AMIHUD-liquido** sono lo stesso
|
||||||
|
tema "evita speculativo/illiquido/volatile" in altre vesti (corr fra loro 0.33-0.59). **AMIHUD
|
||||||
|
long-illiquido SCARTATO** (premio illiquidità invertito in crypto: vincono i major liquidi). Bocciatura
|
||||||
|
del claim forte: **deflated-Sharpe 0.30-0.37 ≪ 0.95** (96 trial), storia ~2.5 anni, book 10-16 gambe →
|
||||||
|
**STAT-MODE**. Nota di rigore: il naive best-HOLD atterrava su celle in-sample-negative (holdout-fitting) →
|
||||||
|
l'agente ha auto-aggiunto il gate has_insample_edge. → **forward-monitor IVOL**.
|
||||||
|
|
||||||
|
### Thread 2 — Momentum-structure vs XS01
|
||||||
|
4 varianti (risk-adjusted, acceleration, frog-in-pan, vol-managed). **Nessuna batte né diversifica XS01**
|
||||||
|
(standalone 1.42). Sostituire XS01 con una variante **distrugge l'hold-out** del portafoglio (−0.58…−1.38)
|
||||||
|
— prova diretta che l'edge di XS01 sta nella sua struttura SPECIFICA (blend z-score [30,90] + gate
|
||||||
|
dispersione), non in varianti generiche. Deflated-Sharpe max 0.49. Tutte REDUNDANT/SCARTATO.
|
||||||
|
|
||||||
|
### Thread 3 — Meta-allocazione dinamica
|
||||||
|
Vol-parity / momentum-of-sleeves / dispersion-regime / drawdown-control vs pesi fissi, con costo di
|
||||||
|
ribilancio realistico. **Nessuno batte i pesi fissi OOS.** Vol-parity = trappola da manuale (+0.09…+0.44
|
||||||
|
sui tagli in-sample 2022-24, **−0.11 sull'hold-out 2025** → intercettato dal multi-cut). Drawdown-control
|
||||||
|
RIDONDANTE (TP01 va già flat nei crash, il gate non si attiva mai). I 4 sleeve sono già quasi-scorrelati
|
||||||
|
(corr ~0.12 max) → i pesi fissi sono già vicini al risk-parity ottimo statico. **Mantenere i pesi fissi.**
|
||||||
|
|
||||||
|
### Thread 4 — Segnali ortogonali ETH/BTC (il risultato più notevole)
|
||||||
|
Relative-value dollar-neutral sul ratio log(ETH/BTC): 6 segnali, evaluator a 2 gambe (fee × 2), cella
|
||||||
|
scelta in-sample-only.
|
||||||
|
|
||||||
|
| segnale | FULL/HOLD | corr→book | β-mkt | marginal | DSR (grid/same-sign) | exec $600 | verdetto |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| **STATARB-RESID** (residual relative-mom ETH−β·BTC, W45 **sgn+1**) | 0.84/0.56 | **0.027** | 0.013 | **ADDS** | 0.056 / 0.929 | haircut ~0 | **LEAD** |
|
||||||
|
| DVOLSPREAD (IV relativa, W60) | 0.74/0.77 | 0.017 | 0.012 | ADDS | 0.082 / 0.907 | haircut ~0 | LEAD |
|
||||||
|
| RATIO-MOM/REV/ACCEL | 0.25-0.68 / ≤0 | ~0 | ~0 | NEUTRAL | <0.23 | ok | NEUTRAL (diversification-math) |
|
||||||
|
| VOLSPREAD | −0.24/−1.47 | ~0 | 0.013 | DILUTES | — | — | SCARTATO |
|
||||||
|
|
||||||
|
**STATARB-RESID rompe 2 dei 3 muri storici:** è **ortogonale per costruzione** (corr→book 0.027, beta di
|
||||||
|
mercato 0.013 — meglio di SKH ~0.09) **ED eseguibile a $600** (book a 2 gambe BTC+ETH perp Deribit,
|
||||||
|
haircut ≈ 0, fee-survive fino a 0.30%/gamba → **NON STAT-MODE**, a differenza di XS01 e delle opzioni). Il
|
||||||
|
muro che **resta** è l'**edge**: Sharpe 0.84 / DSR 0.929 same-sign (ottimistico) ma comunque <0.95.
|
||||||
|
Verificato indipendentemente dal coordinatore: residuo causale (β OLS rolling backward, decisione a
|
||||||
|
close[i], return in i+1), fee a 2 gambe corretta. **⚠ Correzione di etichetta:** la cella vincente è
|
||||||
|
**sgn=+1**, NON sgn=−1 → NON è mean-reversion ma **momentum relativo sul residuo** (long lo spread quando
|
||||||
|
ETH è ricco vs il suo fit su BTC: le dislocazioni ETH-vs-BTC *continuano* a 1d; la MR pura sgn=−1 perde
|
||||||
|
−1.4 in-sample). L'ortogonalità (β~0) viene dalla struttura dollar-neutral, non dal verso. → **LEAD
|
||||||
|
forward-monitor** (monitorabile a costo reale ~0), NON deploy. **CABLATO:** `scripts/live/paper_statarb.py`
|
||||||
|
(config CONGELATA W=45/sgn=+1, doppio libro MODELED/REAL-$600, riusa il segnale esatto di
|
||||||
|
`orthogonal_signals.py`), aggiunto al cron giornaliero accanto a `paper_prevday`; test `tests/test_paper_statarb.py`.
|
||||||
|
**DVOLSPREAD** ri-valida l'ex-lead `dvol_spread`: ADDS ma storia DVOL corta (2021+) → resta forward-monitor
|
||||||
|
come già noto. Secondario crypto-vs-macro = "TSMOM travestito" (corr→book 0.17-0.20), non ortogonale.
|
||||||
|
|
||||||
|
### Thread 5 — 1-gamba a segnale (eseguibile)
|
||||||
|
12 famiglie (MACD/RSI/Supertrend/Donchian/BBands/EMA, ±ADX) su 1d/12h/8h. **0/12 earns_slot_honest.**
|
||||||
|
**Eseguibilità validata** (haircut $600 = 0 ovunque; RSI-MR ~2.5 trade/anno) — ma è l'unica cosa che
|
||||||
|
regge. I trend-follower sono **TP01 travestito** (corr 0.44-0.79, full ~1.2-1.3 = il soffitto) e le celle
|
||||||
|
sub-daily scelte in-sample **collassano OOS** (dimostrazione da manuale del gate selection-on-holdout). La
|
||||||
|
mean-reversion è morta (BBands-MR has_insample_edge=False). I "low-corr interessanti" (RSI-MR, MACD-LS,
|
||||||
|
Donchian-LS) sono **HEDGE non alpha** (`is_hedge=True`, pagano solo quando TP01 è debole), e con
|
||||||
|
deflated-Sharpe <0.95 (RSI-MR 0.861; Donchian-LS passa DSR ma is_hedge). **A $600 un 1-gamba a segnale è
|
||||||
|
eseguibile ma non aggiunge nulla a TP01.**
|
||||||
|
|
||||||
|
## Sintesi / cosa ho imparato
|
||||||
|
|
||||||
|
1. **0 nuovi sleeve, soffitto ~1.3 riconfermato** dal lato direzionale e dal lato struttura-momentum. La
|
||||||
|
ricerca è onesta: ogni candidato è stato ucciso dal gate giusto (deflated-Sharpe, is_hedge,
|
||||||
|
selection-on-holdout, sostituzione-XS01, multi-cut), non da un giudizio a occhio.
|
||||||
|
2. **Il LEAD di valore è STATARB-RESID** (ETH/BTC residual mean-reversion). È il primo stream visto che è
|
||||||
|
**insieme ortogonale (β~0) ED eseguibile a 2 gambe** — cadono i due muri che bloccano XS01 (STAT-MODE)
|
||||||
|
e le opzioni. Manca solo l'edge sopra-soglia. È il candidato n.1 per il **forward-monitor**, e l'unico
|
||||||
|
che — se la finestra forward confermasse l'edge — sarebbe *deployabile* a $600 (non statistico).
|
||||||
|
3. **Forward-monitor (STAT-MODE / sub-soglia):** IVOL (idio-vol XS, 19-major), DVOLSPREAD (storia corta),
|
||||||
|
STATARB-RESID (eseguibile — il più promettente). Nessuno armato come sleeve.
|
||||||
|
4. **I gate nuovi funzionano:** `study_family_honest` (selection-on-holdout) ha intercettato i collassi
|
||||||
|
OOS dei trend 1-gamba e dei low-risk XS; il deflated-Sharpe ha tenuto sotto la soglia ogni Sharpe ~1
|
||||||
|
su storia corta. La lezione del filone B (de-bias prima di credere) è ora applicata di default.
|
||||||
|
|
||||||
|
## Caveat
|
||||||
|
- Universi/finestre: HL ~2.5 anni, DVOL dal 2021 → multiple-testing reale; tutti i LEAD sono sotto la
|
||||||
|
soglia deflazionata. Niente di questo va creduto come alpha finché una finestra forward non lo conferma.
|
||||||
|
- Nessuno sleeve registrato, config canonica invariata (TP01+XS01+VRP01+SKH01 a pesi fissi). Book live
|
||||||
|
intatto. Tutto il lavoro è statistico/forward, su `main` come ricerca.
|
||||||
|
|
||||||
|
Script: `scripts/research/{xsec_v3_lowrisk,xsec_v3_momstruct,meta_allocation,orthogonal_signals,signal_inout_1leg}.py`.
|
||||||
|
Test: i rispettivi `tests/test_*.py` (143/143 verdi).
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# 2026-06-29 — Filone C: cross-sectional NON-momentum su Hyperliquid — DEBOLE / forward-monitor
|
||||||
|
|
||||||
|
**Tesi.** XS01 (sleeve attivo) è momentum cross-sectional sui 19 major HL. Lezione del progetto
|
||||||
|
(2026-06-19/20): **espandere il numero di asset NON aiuta il momentum** (gli small-cap diluiscono/
|
||||||
|
invertono il segnale). Quindi qui NON si ri-prova l'espansione-universo: si cerca un **meccanismo
|
||||||
|
DIVERSO dal momentum** che, market-neutral e scorrelato, possa diversificare il portafoglio. Script
|
||||||
|
`scripts/research/xsec_v2_nonmom.py`, test `tests/test_xsec_v2.py`. Branch `research/equity-meanrev-ib`.
|
||||||
|
|
||||||
|
## Meccanismi provati (tutti L/S dollar-neutral, vol-target ~20%, ribilancio periodico, CAUSALI)
|
||||||
|
|
||||||
|
| | meccanismo |
|
||||||
|
|---|---|
|
||||||
|
| REV | short-term reversal grezzo (long loser / short winner di breve) |
|
||||||
|
| IREV | reversal **idiosincratico** (sul residuo dopo aver tolto il mercato, beta-adjusted) |
|
||||||
|
| LOWVOL | factor **low-vol** (long bassa vol realizzata / short alta vol) |
|
||||||
|
| IMOM | momentum **idiosincratico** (sul residuo, ≠ raw mom) — *riferimento* |
|
||||||
|
| BAB | betting-against-beta (long basso beta / short alto beta) |
|
||||||
|
| MOM | momentum grezzo — *riferimento* |
|
||||||
|
|
||||||
|
Universi: **19 major** e **51 all** (i parquet certificati). Gate canonico di selezione: FULL>0.5,
|
||||||
|
HOLD>0, |corr→XS01|<0.6 (escluse le famiglie momentum MOM/IMOM, che sono riferimento, non candidati).
|
||||||
|
|
||||||
|
## Esiti — un solo candidato non-momentum supera il gate standalone
|
||||||
|
|
||||||
|
```
|
||||||
|
[OK] LOWVOL 19-major B30 H10 k5 FULL +1.07 HOLD +1.07 DD 19% corrXS +0.16 corrTP -0.28
|
||||||
|
[--] BAB 19-major B60 H10 k8 FULL +0.47 HOLD +1.15 DD 28% corrXS +0.20 corrTP -0.27
|
||||||
|
[--] REV/IREV (19 e 51) FULL ~0 o NEGATIVO (reversal cross-sectional non paga)
|
||||||
|
[--] LOWVOL 51-all FULL +0.42 (il long-tail diluisce, come per il momentum)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **REV / IREV negativi** (FULL −0.10..−0.70): il reversal cross-sectional grezzo o idiosincratico **non
|
||||||
|
è un edge** su questo universo/finestra — coerente con la morte storica del mean-reversion del progetto.
|
||||||
|
- **BAB** ha un buon hold-out ma FULL solo +0.47 (sotto soglia) e DD 28%.
|
||||||
|
- **LOWVOL 19-major** (betting-against-vol) è l'unico idoneo: FULL **1.07** / HOLD **1.07** / DD 19%,
|
||||||
|
causale (max_tail_diff 0), corr→XS01 +0.16 (scorrelato), corr→TP01 −0.28.
|
||||||
|
- Espandere a **51 asset peggiora** anche il low-vol (FULL 1.07→0.42): riconferma che i margini
|
||||||
|
cross-sectional sono nella **struttura del segnale**, non nel numero di asset.
|
||||||
|
|
||||||
|
### Uplift portafoglio (4 → 5 sleeve, LOWVOL come 5° sleeve)
|
||||||
|
|
||||||
|
```
|
||||||
|
BASE (TP01+XS01+VRP01+SKH01) FULL 2.12 DD 8% | HOLD 2.25 DD 3%
|
||||||
|
+LOWVOL @10% FULL 2.17 (+0.05) | HOLD 2.57 (+0.32)
|
||||||
|
+LOWVOL @15% FULL 2.19 (+0.06) | HOLD 2.62 (+0.37)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Il muro (perché NON è uno sleeve)
|
||||||
|
|
||||||
|
1. **Deflated Sharpe 0.130 ≪ 0.95** (N=216 trial globali; soglia Sharpe-max-null annualizz. 1.79). Con il
|
||||||
|
numero di celle/famiglie/universi testati, lo Sharpe 1.07 **non è significativo** dopo la correzione
|
||||||
|
per multiple-testing. Per-anno: 2024 +0.23 / 2025 +0.50 / **2026 −0.09** (il 2026 è già negativo).
|
||||||
|
2. **Storia ~2.5 anni** (HL nativo 2024-2026): finestra troppo corta per un fattore cross-sectional.
|
||||||
|
3. **Non eseguibile a $600**: book L/S a molte gambe (10+ per lato) → **STAT-MODE**, mai deploy. Stesso
|
||||||
|
vincolo strutturale di XS01 (serve ~20k di capitale).
|
||||||
|
|
||||||
|
## Verdetto: **DEBOLE / forward-monitor (STAT-MODE).** Nessuno sleeve registrato.
|
||||||
|
|
||||||
|
Il **low-vol cross-sectional** (betting-against-vol sui 19 major) è l'unico meccanismo non-momentum con un
|
||||||
|
profilo decente e scorrelato sia a XS01 sia a TP01, e migliorerebbe l'hold-out del portafoglio (+0.32/+0.37).
|
||||||
|
**MA** non supera il deflated-Sharpe (multiple-testing), ha storia cortissima ed è non-eseguibile a questo
|
||||||
|
capitale. → lavoro **puramente statistico**, da tenere in forward-monitor come *secondo* segnale
|
||||||
|
cross-sectional a fianco di XS01, da rivalutare solo a scala (capitale ~20k+) e con più storia HL.
|
||||||
|
|
||||||
|
Reversal cross-sectional (REV/IREV) **confermato non-edge** su HL. Nessun impatto sul book live.
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# 2026-07-01 — Breadth / market-internals del mercato ALT come segnale su BTC/ETH — SCARTATO
|
||||||
|
|
||||||
|
**Tesi.** Gli "internals" del mercato crypto — la partecipazione degli alt — come segnale
|
||||||
|
direzionale o gate di de-risk su BTC/ETH perp (2 gambe, eseguibile a ~$600): % di alt sopra la
|
||||||
|
propria SMA(N), advance/decline, % di alt che battono BTC (risk-appetite relativo), breadth-thrust.
|
||||||
|
Script `scripts/research/r0701_breadth_internals.py` (non committato per policy del filone; harness
|
||||||
|
`altlib`). Dati: i 51 parquet certificati `data/raw/hl_*_1d.parquet` → panel di **49 alt**
|
||||||
|
(esclusi hl_btc/hl_eth dalla breadth; hl_btc solo come riferimento per la famiglia RS), barre a
|
||||||
|
volume 0 mascherate (lezione backfill 2026-06-20; il fetch le aveva già ripulite: vol0=0 su tutti
|
||||||
|
i file, gate comunque attivo).
|
||||||
|
|
||||||
|
**Limite strutturale dichiarato in partenza:** l'universo HL parte dal 2024-01 → dopo il warmup
|
||||||
|
(max 120g) la finestra comune è **2024-05-05 → 2026-07-01 (~2.2 anni)** e l'in-sample pre-HOLDOUT
|
||||||
|
è **~8 mesi del toro 2024**. Qualunque esito poteva essere al massimo un lead.
|
||||||
|
|
||||||
|
## Famiglie e griglia (108 celle, tutte contate nel deflated Sharpe)
|
||||||
|
|
||||||
|
4 famiglie × N∈{20,50,100} × soglia∈{0.3,0.5,0.7} × forma∈{LS, long-flat, gate-su-TP01}:
|
||||||
|
|
||||||
|
- **FAM-MA**: % di alt validi con close > SMA(N) (breadth classica).
|
||||||
|
- **FAM-AD**: frazione di advancers (ret 1g>0), SMA(N).
|
||||||
|
- **FAM-RS**: % di alt che battono BTC sul ritorno a N giorni (≈ market-neutral, l'angolo più ortogonale).
|
||||||
|
- **FAM-TH**: breadth-thrust = Δ a N giorni della breadth MA20 (thrust/collapse), score 0.5+Δ.
|
||||||
|
|
||||||
|
Tutte causali (rolling/shift soltanto), MIN_VALID=20 asset per data (osservato: 42-49). LS/LF
|
||||||
|
vol-target 20% cap 2x (sizing TP01-style); GATE = posizione TP01 × gate binario. Fee 0.10% RT +
|
||||||
|
sweep 0-0.30%. Procedura onesta = mirror di `study_family_honest` sulla finestra comune (il
|
||||||
|
padding pre-2024 avrebbe contaminato il ranking full-history: le celle GATE con gate=1 pre-storia
|
||||||
|
erediterebbero lo Sharpe 2019-2024 di TP01): **cella scelta col SOLO Sharpe in-sample**, DSR su
|
||||||
|
tutti i trial, `marginal_vs_tp01` di altlib sulla cella scelta.
|
||||||
|
|
||||||
|
## ⚠️ Bug trovato e corretto: allineamento merge_asof su indice non-ns
|
||||||
|
|
||||||
|
La prima versione di `_align` usava `b.index.view("int64")//10**6` per i timestamp della breadth:
|
||||||
|
su un DatetimeIndex tz-aware **a risoluzione non-ns** (pandas 2.x) la scala esce sbagliata →
|
||||||
|
`merge_asof backward` matchava OGNI barra BTC/ETH all'**ultimo** valore della breadth = **il futuro
|
||||||
|
broadcast su tutta la storia**. Sintomo: decine di celle con lo stesso identico Sharpe (il segnale
|
||||||
|
era una costante). **`causality_ok` NON lo vede** — la serie breadth è un input esterno *fisso*,
|
||||||
|
quindi il target sul prefisso troncato coincide col target full per costruzione. Fix: epoca
|
||||||
|
esplicita `(idx - Timestamp(1970, tz=UTC)) // Timedelta(1ms)` (come `resample_tf`). **Lezione
|
||||||
|
harness:** ogni allineamento di un panel esterno via merge_asof va fatto con epoch-ms esplicita, e
|
||||||
|
il sintomo "molte celle con Sharpe identico" è un red-flag di allineamento rotto (gemella
|
||||||
|
dell'osservazione merge_asof del 2026-06-20).
|
||||||
|
|
||||||
|
## Esito (procedura onesta, finestra comune 2024-05+)
|
||||||
|
|
||||||
|
104/108 celle valutabili (4 degeneri flat), 47 con FULL>0. **Cella scelta in-sample** (IS 2.11,
|
||||||
|
la selezione non tocca mai l'hold-out): **TH N=100 thr=0.7 long-flat** — long solo dopo un
|
||||||
|
breadth-thrust (+0.2 di breadth MA20 in 100g), attiva il 32% dei giorni.
|
||||||
|
|
||||||
|
| metrica | valore |
|
||||||
|
|---|---|
|
||||||
|
| standalone finestra comune | IS **+2.11** → FULL **+0.99**, HOLD **+0.41** |
|
||||||
|
| assoluto (per-asset, trimmed) | **PASS** — minFull +0.68, minHold +0.38, fee-survive 0.30%RT, DD 7-11% |
|
||||||
|
| marginale vs TP01 | **ADDS** — corr full **0.401**/hold 0.358, resid Sharpe 0.87, multicut {2025:+0.076, 2026:+0.366} persistente, is_hedge=False |
|
||||||
|
| **robust_oos** | **False** — jackknife drop-best-month **−0.068** (l'uplift hold-out sta in piedi su UN mese) |
|
||||||
|
| **deflated Sharpe** | **0.433** su 104 trial (null-max atteso 1.10) — lontanissimo dal PASS ≥0.95 |
|
||||||
|
| earns_slot / earns_slot_honest | **False / False** |
|
||||||
|
| causality_ok | True (post-fix) |
|
||||||
|
| smallcap $600 | haircut ≈ **0.00** (BTC −0.01, ETH +0.00; 137-152 trade) — eseguibile |
|
||||||
|
|
||||||
|
**Ridondanza col trend (il rischio n.1, lezione macro-gate):** qui NON è la modalità di morte.
|
||||||
|
corr(pos, TP01pos) 0.15-0.30 e il segnale "lavora" (off mentre TP01 non è flat) nel **24-32% dei
|
||||||
|
giorni** (il macro-gate lavorava nel 2-3%). Le forme direzionali LS/LF sono genuinamente diverse
|
||||||
|
da TP01 (corr→TP01 0.1-0.4). La forma **gate-su-TP01** invece sì: best gate = NEUTRAL a corr
|
||||||
|
0.803, e i gate a soglia bassa = REDUNDANT corr 1.0 (gate mai off → TP01 identico). **La breadth
|
||||||
|
come gate di de-risk non aggiunge nulla; come segnale direzionale è diversa dal trend ma l'edge
|
||||||
|
non è dimostrabile.**
|
||||||
|
|
||||||
|
**Il gemello in hindsight (trasparenza, NON selezionabile).** TH N=100 thr=0.7 **LS** fa FULL 1.13
|
||||||
|
/ HOLD 1.06, corr→TP01 **−0.26**, marginal ADDS con robust_oos=True e jackknife +0.46. MA il suo
|
||||||
|
in-sample (1.30) è sotto quello della cella lf (2.11): sceglierlo = **selezione-sull'hold-out**
|
||||||
|
(esattamente il falso positivo ERM del 2026-06-29). E comunque **DSR 0.518 < 0.95**: anche la
|
||||||
|
lettura più caritatevole muore sulla deflazione multiple-testing.
|
||||||
|
|
||||||
|
## Verdetto: SCARTATO (niente sleeve, niente forward-monitor cablato)
|
||||||
|
|
||||||
|
1. La cella scelta onestamente è ADDS ma **robust_oos=False** (uplift a un mese) e **DSR 0.433**
|
||||||
|
su 104 trial: su ~8 mesi di in-sample (toro 2024) un IS di 2.1 è indistinguibile dal massimo
|
||||||
|
atteso per caso (1.10) e non regge.
|
||||||
|
2. Il candidato migliore col senno di poi (TH-LS) sarebbe selezione-sull'hold-out e fallisce
|
||||||
|
comunque il DSR (0.518) — il gate `study_family_honest` fa esattamente il suo lavoro.
|
||||||
|
3. Il limite è la **storia**: 2.2 anni totali, 8 mesi di in-sample. Non c'è modo onesto di
|
||||||
|
dimostrare l'edge oggi.
|
||||||
|
|
||||||
|
**Cosa resta.** (a) La famiglia **breadth-thrust (TH)** è l'unico angolo con celle OOS-positive e
|
||||||
|
bassa/negativa corr a TP01 — **rivisitabile quando il panel HL avrà ≥1-2 anni di storia nativa in
|
||||||
|
più** (in-sample multi-regime, DSR ricalcolabile); nessun cablaggio forward oggi, il costo
|
||||||
|
dell'attesa è zero. (b) Il breadth-**gate** su TP01 va in bacino con macro-gate e DVOL-gate: terza
|
||||||
|
conferma che TP01 non ha bisogno di de-risk esterno (long-flat, già flat nei crash). (c) Il fix
|
||||||
|
`_align` (epoch esplicita) è la lezione riusabile per ogni futuro panel esterno.
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# 2026-07-01 — Book live: verifica gate SKH01 + emersione errori silenziosi
|
||||||
|
|
||||||
|
## Contesto
|
||||||
|
Il BOOK DERIBIT armato (TP01 0.75 + SKH01 0.25 nettati) è **flat da quando è stato armato
|
||||||
|
(2026-06-23)**: 193 run orari, tutte HOLD, zero ordini reali. Domanda: il flat è legittimo o il
|
||||||
|
gate di SKH01 è muto per un bug?
|
||||||
|
|
||||||
|
## Verifica del gate SKH01 — SANO
|
||||||
|
- **Logica viva:** `skyhook_entries` produce 335 entry (BTC, 222 long/113 short) e 341 (ETH,
|
||||||
|
226/115) su tutta la storia. Non è degenere.
|
||||||
|
- **Shorta davvero i crash:** ha aperto SHORT sulla gamba di crollo di fine maggio/inizio giugno —
|
||||||
|
BTC 05-28→06-04 ($73k→$64k), ETH 06-02→06-06 ($1865→$1555).
|
||||||
|
- **Flat da 06-23 = legittimo:** ultimo entry 06-06, `max_bars=16 × 230m ≈ 2,5g` → chiuso ~06-08/09,
|
||||||
|
**prima** dell'arming (06-23). Dal grind post-crash nessun setup breakout+regime valido → flat
|
||||||
|
corretto per design ("NON trend-follower"). Contesto prezzo al 01-07: BTC −53% dal picco, ETH
|
||||||
|
−67,5%, momentum negativo su 1/3/6/12 mesi → anche TP01 (long-flat TSMOM) è a 0x.
|
||||||
|
- **Entrambi i feed valutano senza crash:** certificato e live `fresh_5m` → `skh_error=None`.
|
||||||
|
|
||||||
|
## Bug scoperto e corretto — pattern "eccezione ingoiata → stato safe silenzioso"
|
||||||
|
`src/live/` aveva più `try/except` che, in errore, producono uno stato safe (flat/fallback) **senza
|
||||||
|
farlo emergere**. Rischio: quando l'errore è transitorio ma reale, l'esecutore opera su dati sbagliati
|
||||||
|
credendoli legittimi.
|
||||||
|
|
||||||
|
### (1) `skh_error` — CORRETTO
|
||||||
|
`book_report` scriveva `skh_error` in un dict locale **mai incluso nel return** → `r.get("skh_error")`
|
||||||
|
sempre `None`; e `book_execute` non lo leggeva comunque. Un feed SKH rotto avrebbe forzato SKH→flat in
|
||||||
|
silenzio (entry mancato, nessun alert). Fix: `skh_error` esposto nel return + `book_execute` emette
|
||||||
|
log `⚠️ SKH FEED ERRORE` + alert Telegram. (commit 31369b3)
|
||||||
|
|
||||||
|
### (2) Gate fail-safe posizione (Opzione A) — CORRETTO
|
||||||
|
`src/live/shadow.py::_positions`: se la read della posizione reale Deribit lancia, assume flat (0.0)
|
||||||
|
con una `note`, MAI propagata (`pos_src`) a `book_report`/`book_execute`. Severità mitigata a valle
|
||||||
|
da `rebalance_signed` che **rilegge la posizione fresca** (no doppia-posizione), ma resta il caso
|
||||||
|
"target≈0 + posizione aperta + read fallita → close saltato quel run" (transitorio, backstoppato dal
|
||||||
|
disaster-SL −30%). **Scelta A = gate fail-safe:** `_positions` ritorna un `pos_error` esplicito →
|
||||||
|
propagato `shadow_report → book_report → book_execute`; se presente (ONLINE ma posizione IGNOTA)
|
||||||
|
l'esecutore **NON opera a cieco** (return + alert), come già fa per `online`.
|
||||||
|
|
||||||
|
### (3) Diagnostica equity (Opzione B) — CORRETTO
|
||||||
|
`src/live/shadow.py::_equity`: se `account_summary('USDC')` e tutte le per-asset falliscono,
|
||||||
|
`real_eq=None` e `shadow_report` ripiega su `paper_cap` (~$2000 dallo state paper) mentre il conto
|
||||||
|
reale è ~$598 → il book dimensionerebbe su $2000 invece di $598 (**sovradimensiona ~33%**). Il danno
|
||||||
|
è limitato dall'**hard-cap $300/asset** in `book_net_target`. **Scelta B = SOLO DIAGNOSTICA** (niente
|
||||||
|
gate/halt, l'hard-cap già protegge il downside): `shadow_report` calcola `eq_fallback` (ONLINE +
|
||||||
|
equity reale non leggibile + no override), propagato `→ book_report → book_execute`, che stampa
|
||||||
|
`⚠️ EQUITY FALLBACK` + alert Telegram **ma prosegue** (a differenza del gate posizione, che blocca).
|
||||||
|
|
||||||
|
## Test
|
||||||
|
Suite completa **156/156** (era 148; +4 gate A + 4 diagnostica B). Dry-run reale di `book_execute`
|
||||||
|
invariato (nessuna riga d'errore, nessun ordine, conto $598.06 al target). Path live: `pos_error=None`,
|
||||||
|
`skh_error=None`, `eq_fallback=None`.
|
||||||
|
|
||||||
|
## Sintesi finale — i 3 pattern "errore silenzioso" del path live sono ora tutti tracciati
|
||||||
|
| # | Punto | Comportamento | Emersione |
|
||||||
|
|---|-------|---------------|-----------|
|
||||||
|
| 1 | `book_report` feed SKH | SKH → flat | log `⚠️ SKH FEED ERRORE` + alert |
|
||||||
|
| 2 | `_positions` read fallita | posizione → flat | **HALT** (non esegue) + alert (gate A) |
|
||||||
|
| 3 | `_equity` non leggibile | sizing → paper_cap | log `⚠️ EQUITY FALLBACK` + alert (diagnostica B) |
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
# 2026-07-01 — Funding rate come segnale TIME-SERIES direzionale su BTC/ETH (R0701): SCARTATO
|
||||||
|
|
||||||
|
## Contesto
|
||||||
|
|
||||||
|
Filone: il **funding orario Hyperliquid come proxy di POSIZIONAMENTO/sentiment** con valenza
|
||||||
|
direzionale a orizzonte giornaliero su BTC/ETH — NON carry. Angolo diverso dal prior art già
|
||||||
|
chiuso: **FC01** (carry cross-sectional delta-neutral 19 alt → LEAD fragile SCARTATO,
|
||||||
|
`2026-06-22-funding-carry-hl.md`) e **funding price-clock** intraday (drift attorno agli stamp →
|
||||||
|
FAIL, onda intraday). Qui l'ipotesi è: funding estremo = affollamento long → fade; funding in
|
||||||
|
espansione = domanda persistente → follow; funding affollato = de-risk del trend (gate).
|
||||||
|
Attrattiva a priori: 2 gambe BTC/ETH perp = **eseguibile al capitale reale ~$600** (non STAT-MODE
|
||||||
|
come XS01/opzioni) — se avesse retto sarebbe stato deployabile.
|
||||||
|
|
||||||
|
Script: `scripts/research/r0701_funding_ts.py`.
|
||||||
|
|
||||||
|
## Dati e causalità (data-first)
|
||||||
|
|
||||||
|
- `data/raw/hlfund_{btc,eth}_1h.parquet`: **2023-05-12 → 2026-06-22, 1138/1138 giorni validi,
|
||||||
|
0 gap** dopo il 2023-06 (primi ~27 giorni a cadenza 8h — la somma giornaliera degli stamp li
|
||||||
|
gestisce indifferentemente). Funding medio **+14.4%/anno BTC, +14.6% ETH**; std del funding
|
||||||
|
giornaliero ~5.5 bps.
|
||||||
|
- **Join causale** (il punto delicato): le barre 1d di `altlib.get` sono **open-labeled**
|
||||||
|
(datetime = 00:00 UTC del giorno D; il close della barra D è noto alle 00:00 di D+1). Il
|
||||||
|
feature-day D aggrega i SOLI stamp in **[D 00:00, D+24h)** — l'ultimo alle 23:00 — quindi tutto
|
||||||
|
è noto PRIMA della decisione al close della barra D; `eval_weights` shifta la posizione al
|
||||||
|
giorno D+1. Prefix-consistency check esplicito: **max diff 0.0** (nessun leak).
|
||||||
|
- **Valutazione su finestra TRONCATA** alla copertura funding, non sul frame prezzi 2018+: fuori
|
||||||
|
copertura il target sarebbe zero per costruzione e i giorni-zero (a) **gonfiano il T del
|
||||||
|
deflated-Sharpe** (anti-conservativo) e (b) diluiscono `cand_insample_sharpe` (il gate
|
||||||
|
`has_insample_edge` scatterebbe a vuoto). La logica di `study_family_honest` è replicata
|
||||||
|
ESATTAMENTE sui frame troncati coi primitivi altlib (selezione IN-SAMPLE-ONLY → gates
|
||||||
|
study_marginal → DSR sull'intera griglia). Cross-check `study_marginal` non-troncato in coda:
|
||||||
|
stessi verdetti (abs WEAK / marginal NEUTRAL / earns_slot False).
|
||||||
|
- ⚠️ **STORIA CORTA**: ~3.1 anni totali → in-sample pre-HOLDOUT ~1.6a (meno warmup z), hold-out
|
||||||
|
2025+ ~1.5a. Dichiarato: nessun numero qui ha il peso statistico di TP01 (7 anni).
|
||||||
|
|
||||||
|
## Metodo
|
||||||
|
|
||||||
|
Famiglia a griglia modesta, 1d, 50/50 BTC+ETH, vol-target 20% cap 2x (convenzione TP01), fee
|
||||||
|
0.10% RT + sweep 0.00-0.30%:
|
||||||
|
|
||||||
|
- **FADE**: z-score(funding daily, lb) ≥ thr → short; ≤ −thr → long (fade dell'affollamento).
|
||||||
|
- **FOLLOW**: z ≥ thr → long; ≤ −thr → short (sentiment momentum).
|
||||||
|
- **GATE**: trend long-flat TP01-like (sgn mom 30d+90d) ma FLAT quando z ≥ thr (de-risk crowding).
|
||||||
|
- **DIVERGE**: momentum prezzo 20d seguito se funding NON affollato (z ≤ −thr), fatto se z ≥ thr.
|
||||||
|
|
||||||
|
Griglia: 4 forme × lb {7,14,30,60}g × thr {0.5,1.0,1.5} = **48 celle**. Selezione cella
|
||||||
|
**in-sample-only** (mai sul hold-out), deflated-Sharpe su tutte le 48, gates marginali
|
||||||
|
(`marginal_vs_tp01`), boundary-shift della finestra di aggregazione funding (−0/3/6/9/12h),
|
||||||
|
`eval_weights_smallcap` a $600.
|
||||||
|
|
||||||
|
## Risultati
|
||||||
|
|
||||||
|
Best in-sample per forma: **fade 0.34, follow 0.81, gate 1.76, diverge −0.34** — il segnale
|
||||||
|
puro-funding è debole ovunque; solo la forma che CONTIENE il trend ha Sharpe.
|
||||||
|
|
||||||
|
**Cella scelta (in-sample): GATE lb=60 thr=1.5** — IS 1.76, FULL 1.16, HOLD 0.29.
|
||||||
|
|
||||||
|
| Gate | Esito |
|
||||||
|
|---|---|
|
||||||
|
| Deflated-Sharpe (48 celle) | **0.215** (null-max atteso 1.59) → **FAIL** (soglia 0.95) |
|
||||||
|
| Assoluto (frame troncati) | **WEAK** (minFull +0.97, ma minHold **+0.04** BTC); fee-survive fino a 0.30% RT |
|
||||||
|
| Marginale vs TP01 | **NEUTRAL**: corr **0.879** (hold 0.92), beta 0.77; uplift w25 hold **+0.001**; robust_oos=False (jackknife −0.074); multicut 2026 **−0.423** |
|
||||||
|
| earns_slot / earns_slot_honest | **False / False** |
|
||||||
|
| Realism | prefix-diff 0.0 (causale); boundary-shift sign-stable (uplift spread 0.047); smallcap $600 haircut ~0.00 (eseguibile, irrilevante senza edge) |
|
||||||
|
|
||||||
|
**Controllo decisivo (lezione TP01-DVOL-overlay)** — lo stesso trend long-flat SENZA gate funding:
|
||||||
|
|
||||||
|
```
|
||||||
|
trend NUDO : IS 1.65 FULL 1.14 HOLD 0.37
|
||||||
|
trend+GATE : IS 1.76 FULL 1.16 HOLD 0.29 corr(gated,nudo)=0.928
|
||||||
|
delta del gate funding: FULL +0.013, HOLD −0.080
|
||||||
|
```
|
||||||
|
|
||||||
|
→ **tutto lo Sharpe della "vincitrice" è il trend**; il contributo del funding è ~zero in-sample
|
||||||
|
e **negativo sul hold-out**. Il gate è TP01 travestito.
|
||||||
|
|
||||||
|
**Miglior cella puro-funding** (la vera tesi del filone): FOLLOW lb=60 thr=0.5 — IS 0.81 ma
|
||||||
|
**FULL −0.28, HOLD −1.69**, marginale **DILUTES** (uplift w25 hold −0.778). L'in-sample 0.81 era
|
||||||
|
"long-il-toro-2023/24" travestito: z-alto di funding = rally già in corso; nel 2025-26 la stessa
|
||||||
|
regola è sistematicamente dalla parte sbagliata. FADE (shortare il crowding) perde sul toro e non
|
||||||
|
recupera abbastanza nel chop; DIVERGE negativa ovunque. NB: esistono celle gate con HOLD migliore
|
||||||
|
(es. gate 7/1.5 HOLD 0.96) — sceglierle sarebbe **selection-on-holdout**, il gate del 2026-06-29
|
||||||
|
esiste esattamente per questo.
|
||||||
|
|
||||||
|
## Perché fallisce (meccanismo)
|
||||||
|
|
||||||
|
1. Su asset MAJOR il funding è **quasi sempre positivo e segue il prezzo con lag**: lo z-score
|
||||||
|
alto coincide col momentum già espresso → FOLLOW è trend-beta ritardato (muore quando il regime
|
||||||
|
gira), FADE è shortare il toro. Non c'è l'anti-persistenza ("crowding → reversal") che la tesi
|
||||||
|
richiede — a 1d, su BTC/ETH, le liquidazioni-da-crowding sono già nel prezzo prima che lo
|
||||||
|
z-score giornaliero diventi estremo.
|
||||||
|
2. L'unico uso "funzionante" (gate di de-risk sul trend) è **ridondante**: TP01-like è già flat
|
||||||
|
quando serve. Stesso verdetto di macro-regime-gate (corr 0.989) e DVOL-overlay: la famiglia
|
||||||
|
"overlay prudenziale su TSMOM" è esaurita — eredita lo Sharpe del trend e non aggiunge nulla.
|
||||||
|
|
||||||
|
## Verdetto
|
||||||
|
|
||||||
|
**SCARTATO.** `earns_slot=False`, `earns_slot_honest=False` (DSR 0.215). Il funding time-series
|
||||||
|
non ha informazione direzionale propria su BTC/ETH a 1d; l'unica cella con Sharpe è trend
|
||||||
|
travestito con contributo funding OOS negativo. Niente forward-monitor: non c'è una componente
|
||||||
|
funding da monitorare. Con FC01 (carry cross-sectional) e il price-clock intraday, il filone
|
||||||
|
"funding come segnale" su dati certi è ora **chiuso su tre lati**.
|
||||||
|
|
||||||
|
## Lascito
|
||||||
|
|
||||||
|
- `scripts/research/r0701_funding_ts.py` riusabile: (a) loader funding→daily **causale** per barre
|
||||||
|
open-labeled (finestra [D, D+24h), gestione cadenza mista 8h/1h, flag validità copertura);
|
||||||
|
(b) pattern "**truncation-honest**" per dataset a copertura parziale — replica di
|
||||||
|
`study_family_honest` su finestra troncata per non gonfiare il T del DSR né diluire
|
||||||
|
`has_insample_edge` con anni di zeri (riusabile per ogni serie corta tipo DVOL/HL).
|
||||||
|
- I parquet `hlfund_*` restano certificati. Se mai si tornasse sul funding: gli estremi VERI
|
||||||
|
(squeeze) vivono sugli alt, ma è di nuovo cross-sectional → STAT-MODE a $600, soglia di prova
|
||||||
|
alta (e FC01 ha già mostrato la fragilità d'universo).
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
# 2026-07-01 — Pesi statici ri-ottimizzati + guardia-DD sul portafoglio a 4 sleeve
|
||||||
|
|
||||||
|
## Mandato
|
||||||
|
Migliorare il portafoglio ATTIVO (TP01 41.25 / XS01 18.75 / VRP01 15 / SKH01 25) SENZA nuove
|
||||||
|
strategie: (A) ri-ottimizzazione onesta dei pesi statici, (B) guardia-drawdown a livello
|
||||||
|
portafoglio (replica del soft-guard vincente del tail-hedge lab 2026-06-23), (C) combinazione.
|
||||||
|
NON meta-allocazione dinamica (già scartata, thread 3 del 2026-06-29).
|
||||||
|
Script: `scripts/research/r0701_portfolio_opt.py`. Solo ricerca: nessun file di src/ o live toccato,
|
||||||
|
i pesi sono PROPOSTI, non applicati.
|
||||||
|
|
||||||
|
## Baseline riprodotta
|
||||||
|
`run_portfolio.py` oggi: **FULL Sh 2.12 / HOLD 2.21 / DD full 7.8% / CAGR 23.3%** (vs 2.13/2.30
|
||||||
|
del diario Skyhook: i dati sono avanzati di una settimana, coerente). Matrice sleeve outer-join
|
||||||
|
2019-03-14 → 2026-07-01 (TP01/SKH01 dal 2019, VRP01 dal 2021-05, XS01 dal 2024-01), combine
|
||||||
|
identico a `portfolio.combined_daily` (pesi rinormalizzati per-riga sugli sleeve attivi).
|
||||||
|
|
||||||
|
## (A) Pesi statici — metodo
|
||||||
|
- Ottimizzazione SOLO in-sample (pre-cut), valutazione OOS (post-cut), multi-cut
|
||||||
|
{2024-01, 2024-07, 2025-01}. Sleeve con <120 giorni in-sample al cut (XS01 al 2024-01)
|
||||||
|
PINNED al peso corrente (l'ottimizzatore non ha dati per giudicarli).
|
||||||
|
- Criteri: MAXSH (max-Sharpe del combo simulato), RP (inverse-vol), ERC, MINVAR-R (min-var con
|
||||||
|
vincolo ritorno ≥ pesi correnti). Null: EW (25×4) e CURRENT. Vincoli 5%≤w≤60%, Σ=1.
|
||||||
|
- **Cap STRUTTURALI dichiarati** (prudenza, non statistica): **VRP01 ≤ 15%** (sleeve MODELLATO su
|
||||||
|
DVOL ATM, stress-f non catturato — "niente short-vol da modello in deploy"), **XS01 ≤ 25%**
|
||||||
|
(STAT-MODE, storia ~2.5 anni). Varianti sotto cap: MAXSH-STR (ottimizzato) e EW-STR
|
||||||
|
(EW proiettato sui cap, parameter-free → TP 30 / XS 25 / VRP 15 / SKH 30).
|
||||||
|
|
||||||
|
### Risultati (pesi ottimizzati pre-2025, valutati FULL e HOLD-OUT 2025-26)
|
||||||
|
| criterio | pesi TP/XS/VRP/SKH | FULL Sh | FULL DD | FULL CAGR | HOLD Sh | HOLD DD | HOLD CAGR | multi-cut ΔSh OOS | anni vinti |
|
||||||
|
|---|---|---|---|---|---|---|---|---|---|
|
||||||
|
| CURRENT | 41.2/18.8/15/25 | 2.12 | 7.8% | +23.3% | 2.21 | 3.5% | +16.0% | — | — |
|
||||||
|
| MAXSH | 27.6/17.5/**39.5**/15.4 | 2.17 | 6.3% | +21.8% | 2.27 | 2.8% | +12.7% | +0.24/+0.08/+0.06 | **1/8** |
|
||||||
|
| RP-invvol | 26.7/15.5/**41.6**/16.3 | 2.17 | 6.4% | +21.8% | 2.25 | 2.8% | +12.4% | +0.28/+0.07/+0.04 | — |
|
||||||
|
| ERC | 26.6/16.2/**41.6**/15.7 | 2.17 | 6.3% | +21.7% | 2.25 | 2.8% | +12.4% | +0.29/+0.07/+0.05 | — |
|
||||||
|
| MINVAR-R | 26.7/23.3/**29.6**/20.4 | 2.18 | 6.4% | +23.3% | 2.37 | 2.6% | +16.1% | +0.20/+0.10/+0.16 | 4/8 |
|
||||||
|
| MAXSH-STR | 38.6/25/15/21.4 | 2.14 | 7.5% | +23.4% | 2.28 | 3.3% | +16.8% | **−0.00**/+0.03/+0.07 | 3/8 |
|
||||||
|
| EW | 25/25/25/25 | 2.14 | 7.0% | +24.5% | 2.39 | 2.8% | +18.1% | +0.16/+0.17/+0.18 | 6/8 |
|
||||||
|
| **EW-STR** | **30/25/15/30** | 2.10 | 8.5% | +25.2% | **2.35** | 3.2% | **+19.7%** | **+0.06/+0.13/+0.14** | **7/8** |
|
||||||
|
|
||||||
|
### Lettura onesta
|
||||||
|
1. **Gli OTTIMIZZATORI non aggiungono nulla — anzi overfittano.** Tutti (MAXSH/RP/ERC/MINVAR-R)
|
||||||
|
convergono sulla stessa mossa: scaricare TP01 e caricare **VRP01 al 30-42%** perché ha la vol
|
||||||
|
più bassa (7.7%/anno). Ma quella è la vol del MODELLO (premio BS su DVOL, coda di stress non
|
||||||
|
catturata): è esattamente il punto cieco tipo-CC01 ("Sharpe/vol implausibile → rischio
|
||||||
|
nascosto"). E per-anno MAXSH vince **1/8 anni** vs CURRENT — l'uplift multi-cut è smoothing di
|
||||||
|
vol modellata, non alpha di allocazione. **Dentro i cap** l'ottimizzatore (MAXSH-STR) converge
|
||||||
|
quasi sui pesi correnti (38.6/25/15/21.4) e NON è persistente (−0.00 al cut 2024-01) →
|
||||||
|
**i pesi correnti sono già ~ottimi per ciò che il backtest può giudicare lecitamente**.
|
||||||
|
2. **L'unico segnale robusto è la DE-CONCENTRAZIONE parameter-free**: EW-STR (EW sotto i cap,
|
||||||
|
nessun parametro fittato) vince **7/8 anni** (unica perdita 2021 −3.1pp), tutti i cut OOS
|
||||||
|
(ΔSh +0.06/+0.13/+0.14), HOLD Sh 2.21→2.35, HOLD CAGR +16.0→+19.7%. Classico "1/N batte
|
||||||
|
l'ottimizzazione" (DeMiguel). La sostanza: CURRENT dà il 41% al peggior Sharpe FULL del book
|
||||||
|
(TP01 1.30) e EW-STR sposta ~11pp da TP01 verso SKH01 (+5pp) e XS01 (+6.25pp).
|
||||||
|
3. **MA non è un free lunch risk-adjusted**: FULL Sh 2.12→**2.10** e FULL DD 7.8→**8.5%**
|
||||||
|
(più SKH01, che ha DD standalone 18%, pesa di più nei primi anni a 2 sleeve). L'uplift è un
|
||||||
|
TILT di ritorno verso gli sleeve research-grade con la storia più corta/caveat più pesanti
|
||||||
|
(SKH01 = research win, book 230m da verificare a deploy; XS01 = STAT-MODE non eseguibile a 2k).
|
||||||
|
E TP01 resta l'unico sleeve *deployed* live: il suo 41% nel report riflette una scelta di
|
||||||
|
prudenza/eseguibilità, non solo statistica.
|
||||||
|
|
||||||
|
### VERDETTO (A)
|
||||||
|
- **Ottimizzazione formale: NON MIGLIORA** (viola i cap strutturali o converge ai pesi correnti).
|
||||||
|
**Per il book reale i pesi correnti reggono.**
|
||||||
|
- **Candidato PROPOSTO (non applicato): EW-STR = TP01 30 / XS01 25 / VRP01 15 / SKH01 30** —
|
||||||
|
uplift OOS persistente e per-anno (7/8), dentro i cap, al costo di FULL DD +0.7pp e FULL Sh
|
||||||
|
−0.02. È una decisione di risk-appetite (più ritorno atteso spostando peso su sleeve
|
||||||
|
research-grade), non un miglioramento dominante. Se si vuole adottare, farlo consapevoli che
|
||||||
|
aumenta la dipendenza da SKH01/XS01, e NON tocca il book Deribit eseguibile (TP75/SKH25).
|
||||||
|
|
||||||
|
### Caveat metodologici (A)
|
||||||
|
- Le 3 finestre multi-cut sono ANNIDATE (tutte contengono il 2025-26): non 3 conferme
|
||||||
|
indipendenti. Il test disgiunto vero è il per-anno (7/8 per EW-STR è il dato forte).
|
||||||
|
- EW-STR è stato costruito DOPO aver visto EW vincere (proiezione del null sui cap dichiarati):
|
||||||
|
costruzione principled e parameter-free, ma va detto — non era nella griglia iniziale.
|
||||||
|
- I pesi CURRENT stessi furono fissati (2026-06-23) conoscendo l'hold-out → il confronto
|
||||||
|
"in-sample-opt vs CURRENT su hold-out" è semmai sbilanciato A FAVORE di CURRENT; EW-STR vince
|
||||||
|
comunque.
|
||||||
|
|
||||||
|
## (B) Guardia-drawdown a livello portafoglio — metodo
|
||||||
|
Soft-guard causale sul combo 4-sleeve (pesi CURRENT): esposizione del giorno D decisa dal DD
|
||||||
|
dell'equity fino a **D−1**; trigger/re-entry sul **NAV ombra** non-guardato (lezione stops_lab:
|
||||||
|
sull'equity congelata a expo=0 il DD non rientra mai); isteresi re-risk a 0.4·X come nel
|
||||||
|
tail-hedge lab. Griglia X∈{3,4,5,6}%, derisk∈{0,0.5}. Selezione cella IN-SAMPLE (max Calmar
|
||||||
|
pre-2025), verifica hold-out + multi-cut, **null de-levering** (expo costante c che pareggia il
|
||||||
|
MaxDD in-sample della guardia: Sharpe di c·r è invariante → la guardia vale solo se batte quello).
|
||||||
|
|
||||||
|
### Risultati
|
||||||
|
- Cella scelta in-sample: **X=5% / derisk=0.5** (IS Calmar 3.64 vs 3.24 baseline; IS DD 7.8→6.6%,
|
||||||
|
IS CAGR 25.3→23.9%, IS Sh 2.13→2.12). In-sample batte il null de-levering c=0.84 a pari DD
|
||||||
|
(CAGR 23.9% vs 21.0%) — il timing esiste in-sample.
|
||||||
|
- **MA OOS è INERTE**: a ogni cut la cella scelta non scatta MAI (ΔSh +0.00, ΔDD 0.0pp, ΔCAGR
|
||||||
|
0.0pp su tutti e 3 i cut) perché il DD OOS del 4-sleeve resta 3.5-4.8% < trigger 5%.
|
||||||
|
**La diversificazione a 4 sleeve fa già il lavoro della guardia** (il 2022 del book è +11.6%
|
||||||
|
con DD 3.6%: non c'è il grind da proteggere che aveva il combo TP01+GTAA del tail-hedge lab,
|
||||||
|
DD 8.4% / 2022 −4.4%).
|
||||||
|
- Trigger più stretti che scattano davvero COSTANO: X=3%/d=0.5 → HOLD Sh 2.10 (< 2.21), CAGR
|
||||||
|
13.7% (< 16.0%); X=3-4%/d=0 whipsaw pieno (HOLD Sh 1.83-2.09; a X=4/d=0 il DD guardato IS
|
||||||
|
sale addirittura a 8.8% > 7.8% baseline — accumulo di perdite su cicli off/on, lo stesso
|
||||||
|
fenomeno visto in stops_lab con l'uscita totale).
|
||||||
|
|
||||||
|
### VERDETTO (B)
|
||||||
|
**NON MIGLIORA.** La guardia-DD che vinceva sul combo TP01+GTAA (DD 8.4%, 2022 −4.4%) qui non ha
|
||||||
|
bersaglio: il 4-sleeve ha già DD ~3.5-4.8% OOS. Coerente col thread meta-allocazione 2026-06-29
|
||||||
|
(drawdown-control ridondante). Nota operativa: la cella X=5%/d=0.5 è *innocua* (non fa nulla
|
||||||
|
finché il DD non supera il 5%) e in-sample mostrava timing genuino sopra il null de-levering —
|
||||||
|
può valere come **circuit-breaker di emergenza sul live** (risk management, non alpha), ma non è
|
||||||
|
un miglioramento statistico e non la si adotta come tale.
|
||||||
|
|
||||||
|
## (C) Combinazione
|
||||||
|
(B) è inerte → niente da combinare. Per completezza: EW-STR + guard X=5%/d=0.5 → FULL Sh 2.08,
|
||||||
|
DD 6.7%, CAGR 23.7% | HOLD invariato 2.35/3.2%/19.7% (la guardia lima il DD 2021-2023 del tilt
|
||||||
|
a costo di −0.02 Sh FULL). Nessuna adozione proposta.
|
||||||
|
|
||||||
|
## Sintesi
|
||||||
|
| config | FULL Sh | FULL DD | FULL CAGR | HOLD Sh | HOLD DD | HOLD CAGR |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| **CURRENT (attivo)** | 2.12 | 7.8% | +23.3% | 2.21 | 3.5% | +16.0% |
|
||||||
|
| EW-STR (proposta A, non applicata) | 2.10 | 8.5% | +25.2% | 2.35 | 3.2% | +19.7% |
|
||||||
|
| CURRENT + guard 5%/0.5 (B) | 2.11 | 6.6% | +22.3% | 2.21 | 3.5% | +16.0% |
|
||||||
|
| EW-STR + guard (C) | 2.08 | 6.7% | +23.7% | 2.35 | 3.2% | +19.7% |
|
||||||
|
|
||||||
|
- **(A) NON MIGLIORA in senso forte** — i pesi correnti reggono; l'unico candidato onesto è il
|
||||||
|
tilt de-concentrante EW-STR (30/25/15/30), un trade-off di risk-appetite da decidere, non un
|
||||||
|
upgrade dominante. Gli ottimizzatori in-sample o violano i cap strutturali (VRP01 40%) o
|
||||||
|
riconvergono sui pesi correnti.
|
||||||
|
- **(B) NON MIGLIORA** — guardia inerte OOS sul 4-sleeve; utile al più come circuit-breaker.
|
||||||
|
- Nessuna modifica a `src/portfolio/sleeves.py` o al live. Test 160/160 verdi.
|
||||||
|
|
||||||
|
Script: `scripts/research/r0701_portfolio_opt.py` (~4s, vettoriale).
|
||||||
|
|
||||||
|
## Verifica avversariale (agente scettico)
|
||||||
|
|
||||||
|
Script: `scripts/research/r0701_portfolio_skeptic.py`. **VERDETTO: NON APPLICARE — CURRENT resta.**
|
||||||
|
I numeri dell'agente riproducono ESATTAMENTE (via `StrategyPortfolio.combined_daily`, path di
|
||||||
|
produzione: CURRENT 2.12/2.21, EW-STR 2.10/2.35, multi-cut +0.06/+0.13/+0.14, |diff|=0). Il claim
|
||||||
|
muore altrove: l'uplift è **selezione-sull'hold-out di 2° ordine**, non alpha di allocazione.
|
||||||
|
|
||||||
|
1. **L'uplift OOS vive TUTTO nel 2025-26 — la finestra che ha selezionato i beneficiari.**
|
||||||
|
Finestre OOS **DISGIUNTE** (i 3 cut del diario sono annidati): ΔSh EW-STR vs CURRENT =
|
||||||
|
**−0.12** (2024H1) / +0.06 (2024H2) / **+0.14** (2025+). L'unica finestra OOS non toccata
|
||||||
|
dalla selezione hold-out è negativa. Sharpe standalone IS→HOLD per sleeve: TP01 1.49→**0.30**,
|
||||||
|
SKH01 1.42→**1.64**, XS01 1.22→**1.51**: EW-STR compra esattamente i due sleeve ammessi/affinati
|
||||||
|
(2026-06-23 / 2026-06-19) PERCHÉ forti sull'hold-out e vende quello che lì collassa. Meccanico.
|
||||||
|
2. **Pre-2025 (finestra non contaminata) EW-STR PERDE risk-adjusted**: ΔSh aggregato **−0.05**,
|
||||||
|
Sharpe per-anno vinti **2/6** (2021 −0.29, 2020 −0.10, 2023/2024 negativi).
|
||||||
|
3. **"7/8 anni vinti" è un artefatto di metrica**: è RITORNO composto (un tilt a più vol/ritorno
|
||||||
|
li vince quasi per costruzione), non risk-adjusted. Su **Sharpe per-anno: 3/8**. E due dei 7
|
||||||
|
"vinti" (2023, 2024) hanno margine **+0.03pp** = tie. Bilancio materiale sul ritorno:
|
||||||
|
5 vinti / 1 perso (2021 −3.1pp) / 2 pari.
|
||||||
|
4. **Plateau: liscio ma il gradiente è solo "meno TP01"**; a TP fisso, alzare SKH 25→30 è
|
||||||
|
piatto/negativo (min multi-cut a TP35: +0.07→+0.02). Scomposizione: il tilt **solo-SKH**
|
||||||
|
(+5pp, TP 36.25) da solo NON è persistente (multi-cut **−0.00**/+0.05/+0.04); il tilt
|
||||||
|
**solo-XS** (TP35/XS25/VRP15/SKH25) è l'unico positivo su tutte le finestre disgiunte
|
||||||
|
(+0.05/+0.02/+0.11) — ma pre-2025 fa ΔSh −0.00 (edge ≈ zero fuori dal 2025-26) e carica lo
|
||||||
|
sleeve MENO eseguibile (STAT-MODE).
|
||||||
|
5. **Realismo**: con l'outer-join rinormalizzato EW-STR tiene SKH01 al **50% effettivo nel
|
||||||
|
2019-20 e 40% nel 2021-23** (book research 230m a metà portafoglio: da lì il FULL DD 8.5%).
|
||||||
|
Punto onesto A FAVORE: un haircut d'esecuzione a drag su SKH01(-20/30%) e SKH+XS non ribalta
|
||||||
|
il ranking hold-out (ΔSh resta +0.08..+0.13), ma i livelli assoluti crollano (HOLD Sh CURRENT
|
||||||
|
2.21→1.64 al −30% su entrambi) e il beneficio resta di CARTA: il book Deribit eseguibile
|
||||||
|
(TP75/SKH25) non è toccato dalla decisione.
|
||||||
|
6. **Forking paths quantificato**: ≥15 config valutate sull'hold-out (7 vettori pesi + 8 celle
|
||||||
|
guardia), EW-STR costruito dopo aver visto EW vincere. Su **500 tilt CASUALI cap-respecting**:
|
||||||
|
51% batte CURRENT sull'hold-out (mediana ΔSh +0.00), solo 11% sul FULL, 30% su tutti e 3 i cut.
|
||||||
|
EW-STR è al **94° percentile** dell'uplift hold-out fra i tilt casuali ≈ il **massimo atteso di
|
||||||
|
~15 tentativi** (k/(k+1)=93.8%): l'uplift osservato è indistinguibile dal "best of 15 tilt
|
||||||
|
casuali scelto sull'hold-out".
|
||||||
|
|
||||||
|
**Conclusione**: il tilt non è disonesto nei conti ma non è evidenza — è la stessa lezione del gate
|
||||||
|
SELECTION-ON-HOLDOUT applicata al livello dei PESI (2° ordine: gli sleeve stessi sono stati
|
||||||
|
selezionati su quell'hold-out). Unica direzione sopravvissuta: XS01 18.75→25 (solo-XS, positivo su
|
||||||
|
tutte le finestre disgiunte), MA edge ≈0 fuori dal 2025-26, sleeve STAT-MODE non eseguibile a $600,
|
||||||
|
e l'hold-out-strength di XS01 è a sua volta concentrata dal gate di dispersione nel regime 2025-26
|
||||||
|
→ da rivalutare SOLO con più storia forward, non ora. **CURRENT (41.25/18.75/15/25) resta.**
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
# 2026-07-01 — Ondata multi-agente a 6 filoni: 0 nuovi sleeve, pesi CURRENT confermati, 1 gate nuovo
|
||||||
|
|
||||||
|
**Goal utente:** "cerca con diversi agenti altre strategie di trading. Prova di tutto e arriva a
|
||||||
|
migliorare il portfolio presente. Lo scopo è sempre 50 euro days."
|
||||||
|
|
||||||
|
**Esito in una riga:** 6 filoni nuovi (scelti dopo scan dei ~65 diari per non ripetere strade morte)
|
||||||
|
+ 1 verifica avversariale → nessun candidato supera i gate onesti; il portafoglio a 4 sleeve
|
||||||
|
(41.25/18.75/15/25) **sopravvive a un attacco serio**, che è l'esito corretto per un book onesto.
|
||||||
|
Miglioramento concreto dell'ondata = metodologico: **gate `weights_tilt_null`** codificato (sotto).
|
||||||
|
|
||||||
|
## I 6 filoni (tutti su harness onesto: study_family_honest, marginal scorer, DSR≥0.95, smallcap $600)
|
||||||
|
|
||||||
|
| # | filone | verdetto | perché muore |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | **Funding time-series** BTC/ETH (posizionamento, non carry) — `r0701_funding_ts.py` | SCARTATO | FOLLOW = trend-beta ritardato (HOLD −1.69), FADE = shortare il toro; la cella gate è TP01 travestito (controllo senza funding = stessi numeri, corr 0.93, ΔHOLD −0.08); DSR 0.215. **Filone funding chiuso su 3 lati** (FC01 carry, price-clock, TS-signal). |
|
||||||
|
| 2 | **Breadth/internals alt** (51 HL) → BTC/ETH — `r0701_breadth_internals.py` | SCARTATO (rivisitabile) | Unico non-ridondante col trend (corr→TP01 0.40, lavora dove TP01 è attivo), assoluto PASS, marginal ADDS — ma jackknife −0.068 (uplift su UN mese) e DSR 0.433/104 celle. Con ~8 mesi di IS il 2024-toro non basta. **Rivisitare tra 1-2 anni di storia HL nativa.** |
|
||||||
|
| 3 | **Residual momentum XS** (β-hedged vs BTC, 19 major) — `r0701_xs_residmom.py` | REDUNDANT | Cross-section la residualizzazione è quasi un no-op (lo z-score di XS01 già rimuove il mercato): corr→XS01 0.54, HOLD −0.24, corr(IS,HOLD) tra le celle **−0.37** (anti-predittivo = rumore). L'edge residuo-momentum vive nella coppia ETH/BTC (STATARB-RESID, resta in `paper_statarb`), non generalizza. |
|
||||||
|
| 4 | **Pesi + guardia-DD** — `r0701_portfolio_opt.py` | vedi sotto | Unico candidato dell'ondata (EW-STR) → refutato dallo scettico. Guardia-DD X5%/d0.5: in-sample batte perfino il null de-levering, ma **OOS non scatta mai** (DD book 3.5-4.8% < trigger): la diversificazione fa già il lavoro. Utilizzabile solo come circuit-breaker di emergenza (risk mgmt, non alpha). |
|
||||||
|
| 5 | **Affinamento VRP01** (sizing IV−RV, DVOL-mom, gate TP01) — `r0701_vrp_refine.py` | NON MIGLIORA | L'alpha è già tutto nel gate binario IV-rank; il gate TP01 è la trappola IS perfetta (schiva il 2022, ma taglia le settimane migliori dell'hold-out: multi-cut 0/5). **3° fallimento del filone "affinare VRP dentro il modello" → esaurito** finché cerbero-bite non cattura un crash reale (f di stress). |
|
||||||
|
| 6 | **Stagionalità cross-sectional** HL — `r0701_xs_seasonal.py` | SCARTATO (allo step statistico) | Nessuna persistenza split-half sopra il null permutato max-statistic (p 0.16-0.23); l'unica struttura è il canale-beta dell'effetto weekday di mercato (famiglia trackF, già morta). Turnover ~2×gross/die = fee-death comunque. |
|
||||||
|
|
||||||
|
## EW-STR: il candidato refutato (caso di scuola di selezione-sull'hold-out di 2° ORDINE)
|
||||||
|
|
||||||
|
Il filone 4 proponeva **EW-STR** (TP01 30 / XS01 25 / VRP01 15 / SKH01 30 = equal-weight proiettato
|
||||||
|
sui cap strutturali): HOLD Sh 2.21→2.35, multi-cut +0.06/+0.13/+0.14, "7/8 anni vinti" — dichiarando
|
||||||
|
onestamente i caveat. La verifica avversariale (`r0701_portfolio_skeptic.py`, addendum nel diario
|
||||||
|
pesi) lo uccide:
|
||||||
|
|
||||||
|
1. **Selezione di 2° ordine**: SKH01 fu ammesso (2026-06-23) *perché* alzava l'hold-out 2025-26;
|
||||||
|
XS01 fu affinato conoscendo l'hold-out. EW-STR vende lo sleeve che collassa OOS (TP01 IS 1.49→
|
||||||
|
HOLD 0.30) e compra i due selezionati su quell'OOS. Pre-2025: **ΔSh −0.05** (perde risk-adjusted).
|
||||||
|
2. **Su finestre OOS disgiunte**: −0.12 (2024H1) / +0.06 (2024H2) / +0.14 (2025+) — tutto l'uplift
|
||||||
|
vive nella finestra che ha selezionato gli sleeve. "7/8 anni" era sul ritorno composto; su
|
||||||
|
Sharpe è 3/8.
|
||||||
|
3. **Firma best-of-k**: su 500 tilt casuali cap-respecting, EW-STR siede al 94-100° percentile ≈
|
||||||
|
k/(k+1) con k≈15 config viste sull'hold-out → uplift indistinguibile dal *best-of-15 scelto
|
||||||
|
sull'hold-out*.
|
||||||
|
4. **Realismo**: in pesi effettivi (outer-join rinormalizzato) EW-STR = SKH01 al 40-50% del book
|
||||||
|
2019-23 (sleeve research a book 230m). Il book Deribit eseguibile (TP+SKH) non cambierebbe comunque.
|
||||||
|
|
||||||
|
Residuo onesto: il sub-tilt XS01 18.75→25 è positivo su tutte le finestre disgiunte ma edge≈0
|
||||||
|
pre-2025 e STAT-MODE → rivalutare solo con storia forward.
|
||||||
|
|
||||||
|
## GATE NUOVO codificato: `weights_tilt_null` (src/portfolio/portfolio.py)
|
||||||
|
|
||||||
|
La lezione è ora codice, come per marginal-scorer e study_family_honest:
|
||||||
|
- **`combine_outer(daily_cols, weights, lo, hi)`** — la combinazione outer-join/rinormalizzata di
|
||||||
|
`combined_daily` estratta come funzione su pesi arbitrari (riusabile per studi di tilt).
|
||||||
|
- **`weights_tilt_null(daily_cols, w_current, w_proposed, caps, floor, n, seed, k_seen)`** —
|
||||||
|
null di tilt casuali uniformi sul simplesso dentro floor/caps: riporta `delta_hold/full/insample`,
|
||||||
|
`frac_random_beat_hold`, `pctl_hold`, `bestofk_pctl=k/(k+1)` e `gate_pass`.
|
||||||
|
**Regola: un cambio-pesi si applica solo se (1) delta_insample ≥ 0 e (2) pctl_hold < firma
|
||||||
|
best-of-k** (necessario, non sufficiente: restano finestre disgiunte + realismo).
|
||||||
|
Sul caso reale EW-STR: delta_hold +0.139, delta_insample **−0.054**, pctl **100°** vs best-of-15
|
||||||
|
93.75° → `gate_pass=False`. Test: `tests/test_weights_tilt_null.py` (5 test, sintetici,
|
||||||
|
deterministici; incluso lo sleeve "forte-solo-nell'hold-out" che DEVE fallire il gate).
|
||||||
|
|
||||||
|
## Lezione tecnica riusabile (dal filone 2)
|
||||||
|
|
||||||
|
`DatetimeIndex.view("int64")` su indici **tz-aware a risoluzione non-ns** (pandas 2.x) produce
|
||||||
|
timestamp in scala sbagliata → un `merge_asof` può broadcastare l'ultimo valore su tutta la storia
|
||||||
|
= **look-ahead che `causality_ok` non vede** (input fisso). Sintomo: decine di celle con Sharpe
|
||||||
|
identico. Fix: epoca esplicita in ms. Verificato: `altlib.py` non è affetto (grep pulito); dettagli
|
||||||
|
nel diario breadth.
|
||||||
|
|
||||||
|
## Onestà sul goal €50/giorno
|
||||||
|
|
||||||
|
Invariata (vedi CLAUDE.md): a capitale reale ~$600 il book onesto fa ~+€1.5/giorno atteso; la via
|
||||||
|
resta capitale + tempo + target-vol, non un tilt di pesi né leva. Questa ondata conferma che il
|
||||||
|
book attuale è vicino al massimo estraibile dai dati certificati disponibili oggi; le opzioni
|
||||||
|
reali di crescita restano (a) storia forward che promuova i lead (STATARB-RESID, breadth tra 1-2
|
||||||
|
anni), (b) capitale, (c) nuove fonti di dato (f di stress reale per VRP).
|
||||||
|
|
||||||
|
**File dell'ondata:** `scripts/research/r0701_{funding_ts,breadth_internals,xs_residmom,portfolio_opt,portfolio_skeptic,vrp_refine,xs_seasonal}.py`; diari `2026-07-01-{funding-ts-signal,alt-breadth-internals,xs-residual-momentum,portfolio-weights-ddguard,vrp-refinement,xs-seasonality}.md`; gate in `src/portfolio/portfolio.py` + `tests/test_weights_tilt_null.py`.
|
||||||
|
|
||||||
|
## ADDENDUM (stessa sera) — GTAA01 promosso a 5° SLEEVE: il miglioramento c'era, ma era in casa
|
||||||
|
|
||||||
|
Il secondo passaggio sull'archivio (non sui dati!) ha trovato il candidato che l'ondata di *ricerca*
|
||||||
|
non poteva trovare: **EQ-GTAA01** (`src/portfolio/gtaa.py` — trend difensivo equity 6-ETF, vt12%,
|
||||||
|
~30 anni di storia, netto fee IB) era stato validato il 2026-06-22 come *"primo miglioramento
|
||||||
|
STRUTTURALE del rischio-aggiustato trovato in tutta la ricerca post-reset"*, messo in paper
|
||||||
|
cross-venue (`paper_combo`, nel cron dal 2026-06-23) e **mai valutato come 5° sleeve del
|
||||||
|
portafoglio attivo** (il giorno dopo arrivò SKH01 e il filone si fermò).
|
||||||
|
|
||||||
|
### Valutazione onesta (`r0701_gtaa_5th_sleeve.py`) — perché GTAA passa dove EW-STR è morto
|
||||||
|
|
||||||
|
| gate | EW-STR (refutato) | +GTAA01 (passa) |
|
||||||
|
|---|---|---|
|
||||||
|
| uplift IN-SAMPLE (pre-2025) | **−0.05** | **+0.09..+0.13** (2.134→2.23-2.26) |
|
||||||
|
| finestre OOS disgiunte | −0.12 / +0.06 / +0.14 | **+0.05 / +0.19 / +0.25** (tutte >0) |
|
||||||
|
| multi-cut (4 tagli) | solo annidate | **+0.21 / +0.21 / +0.22 / +0.25** |
|
||||||
|
| provenienza dell'edge | sleeve selezionati sull'hold-out crypto | validato su OOS equity 2015+, **indipendente** dall'hold-out crypto |
|
||||||
|
| corr al book | — | **+0.10** full, ~0.00 hold-out; is_hedge +0.30 (non-hedge) |
|
||||||
|
| plateau | gradiente generico anti-TP01 | **monotono su w 10-30%, ogni cella migliora IS/FULL/HOLD/DD** |
|
||||||
|
|
||||||
|
Convenzione di realismo cross-venue (nuova, cablata nello sleeve): nei giorni senza barra equity
|
||||||
|
(weekend/festivi US) il rendimento è **0.0** — capitale IB fermo, NON rinormalizzato sul crypto
|
||||||
|
(due conti reali non si travasano nel weekend). Numeri quasi identici alla versione outer-join
|
||||||
|
(differenze al 3° decimale) ma il modello è quello giusto. Attivazione nel book all'era crypto
|
||||||
|
(2019-03-14): senza troncatura l'outer-join farebbe partire il book nel 1996 con 23 anni di
|
||||||
|
solo-GTAA rinormalizzato al 100%.
|
||||||
|
|
||||||
|
### Decisione pesi
|
||||||
|
Cella IS-best = w=30% (bordo griglia), **ingresso @20%** per scelta strutturale dichiarata:
|
||||||
|
(a) cross-venue IB con paper operativo di soli 8 giorni — non diventa lo sleeve più grande del
|
||||||
|
book al day-one; (b) precedente: ogni sleeve nuovo è entrato ≤25% (SKH01 25%, VRP01 15%);
|
||||||
|
(c) costo CAGR (a 30%: 16.8% vs 18.8% a 20%) — e l'obiettivo €50/g vive di CAGR, non solo di
|
||||||
|
Sharpe. Tutte le celle 10-30% passano comunque ogni gate. Pesi nuovi: **TP01 33 / XS01 15 /
|
||||||
|
VRP01 12 / SKH01 20 / GTAA01 20** (i 4 preesistenti scalati ×0.80, rapporti conservati).
|
||||||
|
|
||||||
|
### Portafoglio 5-sleeve (run_portfolio, capitale 2k)
|
||||||
|
| | FULL Sh | FULL DD | FULL CAGR | HOLD Sh | HOLD DD | HOLD CAGR |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| 4-sleeve (baseline) | 2.12 | 7.8% | +23.3% | 2.21 | 3.5% | +16.0% |
|
||||||
|
| **5-sleeve (+GTAA01 20%)** | **2.24** | **6.2%** | +18.8% | **2.46** | **2.2%** | +14.6% |
|
||||||
|
|
||||||
|
Per-anno: tutti gli 8 anni positivi, 2022 +7.0% (era +11.6%: GTAA soffre il whipsaw 2022, unico
|
||||||
|
anno con dSh negativo −0.49 — dichiarato). Il trade-off è esplicito: −4.5pp di CAGR full per
|
||||||
|
+0.12/+0.25 di Sharpe e DD 7.8→6.2 / hold-out 3.5→2.2. A pesi research (XS/VRP/SKH modellati o
|
||||||
|
STAT-MODE) il CAGR era comunque di carta; lo Sharpe/DD del book è la metrica di verità.
|
||||||
|
|
||||||
|
### Cosa NON cambia
|
||||||
|
Il **book live Deribit** (`deribit_book_sleeves` TP01+SKH01 75/25, `book_execute`) è INTATTO —
|
||||||
|
GTAA è su IB, già tracciato forward da `paper_combo`. `paper_portfolio` da oggi traccia la
|
||||||
|
composizione a 5 (regime-break dichiarato qui). Test: `tests/test_gtaa_sleeve.py` (registry,
|
||||||
|
weekend-zero, attivazione, book live invariato) — suite 168/168.
|
||||||
|
|
||||||
|
### Lezione dell'addendum
|
||||||
|
Prima di una nuova ondata di *ricerca*, ri-leggere l'archivio dei **validati-ma-non-integrati**:
|
||||||
|
l'unico upgrade onesto del book in 3 ondate (104+5+6 filoni) era un edge già validato che
|
||||||
|
aspettava solo la valutazione da sleeve. La diversificazione strutturale (secondo mercato,
|
||||||
|
corr ~0.1) resta l'unica cosa che alza lo Sharpe del book senza nuovo alpha.
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
# 2026-07-01 — Affinamento VRP01 (sizing IV−RV / filtro DVOL-momentum / gate TP01): NON MIGLIORA
|
||||||
|
|
||||||
|
**Script:** `scripts/research/r0701_vrp_refine.py` (solo ricerca: `src/` e `scripts/live/` non toccati).
|
||||||
|
**Esito: NON MIGLIORA — VRP01 baseline resta la config canonica.** La cella IS-best (gate TP01-skip)
|
||||||
|
è un falso positivo in-sample da manuale: IS Sharpe 2.07 ma multi-cut **0/5**, hold-out **−0.14**
|
||||||
|
(vs 0.59 del baseline), DSR 0.71. Nessuna modifica a `sleeves._vrp_combo_returns`.
|
||||||
|
|
||||||
|
## Cosa testava (celle NUOVE, verificate non ripetute nei diari)
|
||||||
|
|
||||||
|
Stessa pipeline di `options_vrp_v2.py` (pricing BS su DVOL reale, payoff sul path certificato,
|
||||||
|
fee 12.5% del premio, f=1.0, tenor 7d, spread −0.28/−0.10) — cambiati SOLO gate/sizing:
|
||||||
|
|
||||||
|
1. **Sizing sul gap IV−RV** (carry atteso): size lineare `clip(vrp/scale,0,1)` (scale 8/12 pt) o
|
||||||
|
percentile espandente causale del VRP; in aggiunta o in sostituzione del gate binario IV-rank.
|
||||||
|
NB: il gate composito "IV-rank>0.30 AND IV−RV>0" è **già il baseline** (`gate_vrp=True`).
|
||||||
|
2. **Filtro DVOL-momentum**: non vendere se `dvol[i]−dvol[i−k] > thr` (k∈{5,10}, thr∈{0, 5pt}).
|
||||||
|
(Diverso da `dvol_directional` 2026-06-29, dove il DVOL-mom era segnale direzionale sul perp.)
|
||||||
|
3. **Gate di regime da TP01**: skip o half-size quando TP01 è flat su entrambi gli asset (risk-off).
|
||||||
|
4. Croce completa: **105 celle**, tutte contate nel deflated-Sharpe.
|
||||||
|
|
||||||
|
NON ripetuto (già scartato): gestione attiva intra-trade (PT/SL/vol-stop/delta-exit,
|
||||||
|
`2026-06-20-vrp-active-management.md` → hold-to-expiry vince).
|
||||||
|
|
||||||
|
Metodo: selezione cella **in-sample (pre-2025)** con floor di attività IS ≥20%, hold-out 2025-26,
|
||||||
|
**multi-cut 5 tagli** (2023-01→2025-01, richiesti ≥4/5 uplift>0), `altlib.deflated_sharpe` su tutti
|
||||||
|
i 105 trial, effetto sul **portafoglio 4-sleeve** (TP01 41.25 / XS01 18.75 / VRP 15 / SKH01 25).
|
||||||
|
|
||||||
|
## Risultati (book 50/50 BTC+ETH, rendimenti settimanali)
|
||||||
|
|
||||||
|
Baseline riprodotto esattamente: **FULL Sh 1.09 / IS 1.26 / HOLD 0.59 / DD 12% / worst −7.4% / attivo 41%.**
|
||||||
|
|
||||||
|
| cella | IS Sh | FULL | HOLD | DD | worst | attivo |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| **BASELINE** bin+ivr30 | 1.26 | **1.09** | **0.59** | 12% | −7.4% | 41% |
|
||||||
|
| sizing lin 8pt (+ivr30) | 1.20 | 1.05 | 0.61 | 11% | −7.4% | 41% |
|
||||||
|
| sizing lin 12pt (+ivr30) | 1.12 | 0.98 | 0.54 | 11% | −7.4% | 41% |
|
||||||
|
| sizing rank (+ivr30) | 1.07 | 0.94 | 0.54 | 9% | −5.7% | 41% |
|
||||||
|
| sizing lin 8pt **al posto di** ivr | 0.89 | 0.58 | **−0.38** | 21% | −7.4% | 86% |
|
||||||
|
| sizing rank al posto di ivr | 1.03 | 0.69 | **−0.36** | 11% | −5.7% | 86% |
|
||||||
|
| mom k=5 thr=5pt | 1.24 | 1.02 | 0.37 | 9% | −6.5% | 34% |
|
||||||
|
| mom k=10 thr=5pt | 1.26 | 1.08 | 0.55 | 9% | −6.5% | 31% |
|
||||||
|
| mom k=5 thr=0 | 1.78 | 1.24 | −0.10 | 5% | −3.9% | 20% |
|
||||||
|
| mom k=10 thr=0 | 1.36 | 0.78 | **−0.47** | 7% | −3.9% | 18% |
|
||||||
|
| tp01-skip | **2.05** | 1.43 | **−0.14** | 7% | −4.7% | 27% |
|
||||||
|
| tp01-half | 1.72 | 1.34 | 0.27 | 7% | −4.7% | 41% |
|
||||||
|
| IS-best: lin8+tp01-skip | **2.07** | 1.43 | **−0.14** | 6% | −4.2% | 27% |
|
||||||
|
|
||||||
|
**Multi-cut della IS-best vs baseline: uplift negativo in 5/5 tagli** (−0.35 → −0.73, monotono
|
||||||
|
peggiorando verso il presente). **DSR** IS-best 0.709 (<0.95); baseline 0.472 dentro questa griglia
|
||||||
|
(contesto onesto: nemmeno il baseline è statisticamente distinguibile su 105 trial correlati — ma è
|
||||||
|
pre-committed, non selezionato qui). Celle che battono l'hold-out del baseline: **1/105** (lin8pt
|
||||||
|
tp-off, +0.02 — rumore; non selezionabile: sarebbe selection-on-holdout).
|
||||||
|
|
||||||
|
### Frequenza d'intervento dei gate (settimane-trade del baseline = 163)
|
||||||
|
|
||||||
|
- **TP01-skip: blocca il 32.5%** delle settimane tradabili (TP01 flat il 38% dei giorni della
|
||||||
|
finestra DVOL). NON è ridondante col baseline — lavora tanto — ma lavora **male** OOS.
|
||||||
|
- mom thr=5pt: blocca 24-32%; mom thr=0: blocca 60-64% (quasi metà del book).
|
||||||
|
|
||||||
|
### Portafoglio 4-sleeve (VRP @15%)
|
||||||
|
|
||||||
|
| VRP nello slot | FULL Sh | FULL DD | HOLD Sh | HOLD DD |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| **baseline** | **2.12** | 7.8% | **2.21** | 3.5% |
|
||||||
|
| IS-best (lin8+tp01-skip) | 2.11 | 7.8% | 2.12 | 3.5% |
|
||||||
|
|
||||||
|
Anche a livello portafoglio la variante **peggiora** l'hold-out (2.21→2.12) e non migliora nulla.
|
||||||
|
|
||||||
|
## Lettura (perché non migliora)
|
||||||
|
|
||||||
|
1. **L'alpha del VRP è già tutto nel gate IV-rank binario** ("vendi solo vol ricca"). Il sizing
|
||||||
|
proporzionale al gap IV−RV è ridondante *dato* il gate (Δ ≈ ±0.05, dentro il rumore); usato al
|
||||||
|
posto del gate, riporta dentro le settimane di vol povera e distrugge l'hold-out (−0.36/−0.38).
|
||||||
|
Il carry atteso (IV−RV) NON è un segnale di *intensità* utile oltre il suo segno+rank.
|
||||||
|
2. **Il gate TP01 è la trappola in-sample perfetta** (la lezione macro-gate al contrario): schiva il
|
||||||
|
2022 (−6%→0%) gonfiando l'IS a 2.05, ma nel 2025-26 TP01 è flat proprio nei regimi di vol ricca
|
||||||
|
in cui la vendita gated di put **guadagna** — il gate taglia le settimane migliori dell'hold-out
|
||||||
|
(0.59→−0.14). Short-vol gated e trend sono complementari, non condizionabili l'uno sull'altro.
|
||||||
|
3. **Il DVOL-momentum non aggiunge protezione**: il crash-skip IV-rank>0.90 + il long wing dello
|
||||||
|
spread coprono già il run-up di vol; thr=0 è troppo aggressivo (blocca ~60%), thr=5pt è neutro
|
||||||
|
(mai meglio del baseline su hold-out).
|
||||||
|
|
||||||
|
## Onestà / caveat (invariati)
|
||||||
|
|
||||||
|
Premio MODELLATO su DVOL ATM (no skew), book 1d, f di stress reale non catturato → tutto questo
|
||||||
|
resta dentro il modello; il verdetto massimo possibile era "sleeve modellato migliorato", e non è
|
||||||
|
stato raggiunto. Il DSR sulla griglia è un limite *superiore* (celle correlate + ~20 config storiche
|
||||||
|
di options_vrp_lab/_v2 non contate).
|
||||||
|
|
||||||
|
## Azione
|
||||||
|
|
||||||
|
**Nessuna.** VRP01 resta com'è (`bin + gate_vrp + ivr30 + crash-skip 0.90`, hold-to-expiry).
|
||||||
|
Terzo tentativo di affinamento fallito (dopo active-management e — per il portafoglio — DVOL
|
||||||
|
overlay): il filone "migliorare VRP01 dentro il modello" è da considerarsi **esaurito**; il prossimo
|
||||||
|
passo utile resta quello noto: f di stress reale quando cerbero-bite cattura un crash + skew reale
|
||||||
|
sul long wing.
|
||||||
|
|
||||||
|
Script: `scripts/research/r0701_vrp_refine.py` (`--skip-portfolio` per la sola griglia).
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# 2026-07-01 — Residual (idiosyncratic) momentum cross-sectional sui 19 major HL — SCARTATO/REDUNDANT
|
||||||
|
|
||||||
|
**Tesi.** STATARB-RESID (thread 4, 2026-06-29) ha mostrato che il momentum del residuo ETH−β·BTC
|
||||||
|
(β OLS rolling, **sgn=+1**: le dislocazioni continuano a 1d) passa quasi tutti i gate su 2 gambe,
|
||||||
|
fallendo solo il deflated-Sharpe (0.929<0.95). Ipotesi qui: portare lo **stesso meccanismo
|
||||||
|
cross-sectional** sui 19 major di XS01 — per ogni alt, residuo vs β·BTC (β OLS rolling B), momentum
|
||||||
|
del residuo (blend z-score [30,90] come XS01 o singolo L), long top-k / short bottom-k, vol-target
|
||||||
|
20% — sperando che la **breadth** (18 stream invece di 1) alzasse il DSR dove il 2-gambe falliva.
|
||||||
|
Script `scripts/research/r0701_xs_residmom.py` (riusa l'harness collaudato `xsec_v2_nonmom`:
|
||||||
|
load_matrix, xs_engine — uguaglianza verificata maxdiff 0 —, deflated_sharpe, StrategyPortfolio).
|
||||||
|
|
||||||
|
Distinzione dal già-testato: IREV (idio-**reversal**, sgn=−1) era FALLITO nel filone C; IMOM di
|
||||||
|
xsec_v2 usava il mercato equal-weight come fattore, B=60 fisso, senza blend ed era solo riferimento.
|
||||||
|
Qui fattore = **BTC** (come STATARB-RESID, BTC escluso dal cross-section), B in griglia, blend
|
||||||
|
z-score, probe con gate di dispersione p30 (parità strutturale con XS01), **selezione in-sample-only**.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
- Universo: 19 major HL certificati (`data/raw/hl_*_1d.parquet`), 913 giorni [2024-01-01 → 2026-07-01],
|
||||||
|
barre vol=0 escluse. Fee 0.10% RT/gamba + sweep fino a 0.30%.
|
||||||
|
- Griglia (57 trial): β-window {60,90,120} × lookback {blend[30,90], 30, 90} × k {3,5} × H {5,10,20}
|
||||||
|
+ 3 probe gated (blend, k5, H10, gate dispersione p30). Cella scelta col solo Sharpe **pre-2025**;
|
||||||
|
hold-out 2025-01-01 bloccato. Causalità: prefix-check ok (max_tail_diff 0).
|
||||||
|
- Baseline (stessa finestra, sleeve attivi): XS01 FULL Sh **1.38** / HOLD **1.51** / DD 11%;
|
||||||
|
book 4-sleeve FULL 2.12 / HOLD 2.21.
|
||||||
|
|
||||||
|
## Esito — collasso OOS della cella in-sample e nessuna cella competitiva
|
||||||
|
|
||||||
|
Cella scelta in-sample: **B60 L30 k3 H20** (IS Sh 2.00):
|
||||||
|
|
||||||
|
```
|
||||||
|
FULL +0.67 | HOLD −0.24 | DD 35% | per-anno 2024 +54% / 2025 −15% / 2026 +5%
|
||||||
|
corr→XS01 +0.54 | corr→TP01 −0.09 | DSR 0.510 (57 trial) | fee-sweep ok (F+0.59 a 0.30%)
|
||||||
|
small-cap $600/min$5: haircut ≈ 0 (ma book multi-gamba → STAT-MODE comunque, come XS01)
|
||||||
|
```
|
||||||
|
|
||||||
|
Portafoglio (il bar del mandato):
|
||||||
|
|
||||||
|
```
|
||||||
|
BASE 4-sleeve FULL 2.12 DD 7.8% | HOLD 2.21 DD 3.5%
|
||||||
|
SOSTITUZIONE XS01 → cand FULL 2.01 (−0.10) | HOLD 1.18 (−1.03) ← distrugge l'hold-out
|
||||||
|
AGGIUNTA 5° sleeve @10/15% HOLD −0.31 / −0.53 ← diluisce
|
||||||
|
MARGINAL vs BOOK: corr +0.23, uplift@10% full −0.04 / hold −0.31, multi-cut 2025 −0.31 / 2026 −0.14
|
||||||
|
```
|
||||||
|
|
||||||
|
**Il verdetto non dipende dalla selezione** — aggregato delle 57 celle:
|
||||||
|
- FULL max **+0.94**, HOLD max **+0.82** (mediane +0.52/+0.33): *nessuna* cella si avvicina a XS01
|
||||||
|
(1.38/1.51), nemmeno cherry-pickando sull'hold-out.
|
||||||
|
- corr→XS01 in [+0.47, +0.71], mediana **+0.62**: è momentum XS01 travestito (la residualizzazione
|
||||||
|
vs β·BTC sposta poco: lo z-scoring cross-sectional di XS01 già rimuove implicitamente il mercato).
|
||||||
|
- Solo 4/57 celle passerebbero un gate "diversificatore" (|corrXS|<0.6, FULL>0.5, HOLD>0), tutte con
|
||||||
|
FULL ≤0.68 e IS incoerente.
|
||||||
|
- **corr(Sharpe in-sample, Sharpe hold-out) attraverso le celle = −0.37**: l'in-sample è
|
||||||
|
anti-predittivo dell'OOS su questa griglia — la firma del rumore, non di un edge con plateau.
|
||||||
|
|
||||||
|
## Perché la tesi è falsa (3 righe)
|
||||||
|
|
||||||
|
1. La breadth non ha alzato il DSR (0.51 ≪ 0.95) perché non c'è edge da deflazionare: il residuo
|
||||||
|
vs β·BTC sul cross-section degli alt NON eredita l'edge del 2-gambe ETH−β·BTC — quel lead vive
|
||||||
|
nella coppia specifica ETH/BTC (i due asset profondi/cointegrati), non nel long-tail degli alt.
|
||||||
|
2. Cross-sectional, la residualizzazione è quasi un no-op: demeaning z-score (XS01) ≈ togliere il
|
||||||
|
mercato → il candidato è una variante rumorosa di XS01 (corrXS mediana 0.62) con meno segnale.
|
||||||
|
3. In-sample = solo 2024 (~1 anno utile dopo il warmup): la selezione onesta atterra su una cella
|
||||||
|
2024-lucky che perde nel 2025-26 — l'ennesima conferma del gate selection-on-holdout.
|
||||||
|
|
||||||
|
## Verdetto: **SCARTATO/REDUNDANT.** Nessuno sleeve, nessun forward-monitor.
|
||||||
|
|
||||||
|
Coerente con il filone momentum-structure (2026-06-29): l'edge di XS01 sta nella sua struttura
|
||||||
|
specifica (blend + gate dispersione), e le sue varianti — incluso l'idio-momentum vs BTC — sono
|
||||||
|
REDUNDANT o peggio. Chiude anche il cerchio su STATARB-RESID: il meccanismo residuo-momentum resta
|
||||||
|
valido SOLO come coppia ETH/BTC (forward-monitor `paper_statarb`), non generalizza al cross-section.
|
||||||
|
Nessun impatto sul book live; config canonica invariata.
|
||||||
|
|
||||||
|
Script: `scripts/research/r0701_xs_residmom.py` (non committato, prefisso r0701_xs).
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# 2026-07-01 — Stagionalità CROSS-SECTIONAL sull'universo Hyperliquid → SCARTATO
|
||||||
|
|
||||||
|
## Ipotesi
|
||||||
|
|
||||||
|
Effetti calendario **relativi tra i 19 alt major HL** (weekday tilt, turn-of-month,
|
||||||
|
pattern weekend→lunedì nel cross-section), long/short market-neutral. L'angolo è diverso
|
||||||
|
dal prior art già morto (trackF seasonality BTC/ETH = buy&hold travestito;
|
||||||
|
crypto_weekend_signal/overnight = look-ahead; open_drive = artefatto calendario ucciso da
|
||||||
|
`day_boundary_robust`): essendo il segnale **demeaned cross-section**, il buy&hold
|
||||||
|
travestito è strutturalmente escluso… *in prima battuta* (vedi diagnostica beta sotto).
|
||||||
|
|
||||||
|
## Metodo (test statistico PRIMA della strategia)
|
||||||
|
|
||||||
|
Script `scripts/research/r0701_xs_seasonal.py`. Dati `data/raw/hl_*_1d.parquet`, 19 major
|
||||||
|
XS01 (lista `src/portfolio/sleeves.XS_UNIVERSE`), **912 giorni** (2024-01-02 → 2026-07-01,
|
||||||
|
~2.5 anni), 0 barre vol=0. Ritorno relativo x[t,a] = r[t,a] − media cross-section del
|
||||||
|
giorno. Per ogni bucket calendario: **tilt bucket-specifico** per asset = media di x nel
|
||||||
|
bucket − tilt incondizionato dell'asset (isola il calendario dal drift relativo generico).
|
||||||
|
**Persistenza split-half**: Spearman rank-corr del vettore tilt (19 asset) H1 vs H2, per
|
||||||
|
bucket. **Null**: 2000 permutazioni delle etichette entro ciascuna metà → distribuzione del
|
||||||
|
**max-statistic** (controlla il multiple-testing sui bucket e il confound "alpha persistente
|
||||||
|
dell'asset"). Soglia severa p<0.05 sul max. Regola del mandato: senza persistenza →
|
||||||
|
**nessun backtest**.
|
||||||
|
|
||||||
|
## Numeri
|
||||||
|
|
||||||
|
| Test | Statistica | Null 95° pctl | p | Esito |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| [A] Weekday (7 bucket) | max rank-corr **+0.511** (Wed); Mon +0.38, Sun +0.37, Fri +0.04 | +0.604 | **0.159** | FAIL |
|
||||||
|
| [B] Weekend vs feriali | +0.268 | +0.428 | **0.178** | FAIL |
|
||||||
|
| [C] Turn-of-month (±2gg) | +0.221 | +0.451 | **0.226** | FAIL |
|
||||||
|
| [D] IC weekend→lunedì | FULL −0.053 (t=−1.94); H1 −0.006 (t=−0.17) vs H2 −0.100 (t=−2.4) | — | non persiste | FAIL |
|
||||||
|
|
||||||
|
Con ~65 osservazioni per weekday per metà, lo std di una rank-corr su 19 asset è ~0.24 → il
|
||||||
|
max-su-7 sotto puro rumore arriva a ~0.60: il +0.51 reale è *dentro* il rumore.
|
||||||
|
|
||||||
|
## Diagnostica: perché tutte le rank-corr weekday sono positive?
|
||||||
|
|
||||||
|
Non è tilt idiosincratico, è il **canale-beta**: il mercato EW ha un pattern weekday di
|
||||||
|
*livello* che si ripete nelle due metà (Sun −42/−30 bps, Sat +26/+25, Mon +35/+32) e la
|
||||||
|
dispersione di beta (0.53→1.26) lo proietta nel cross-section — rank-corr(tilt, beta):
|
||||||
|
**Sun −0.642**, Sat +0.325, feriali ~0. Cioè la poca "persistenza" visibile è l'effetto
|
||||||
|
weekday di MERCATO (dominio trackF, già scartato come buy&hold travestito) che rientra
|
||||||
|
dalla finestra via beta. Un eventuale L/S "weekday tilt" sarebbe stato in gran parte un
|
||||||
|
book short-beta-la-domenica — non un edge calendario cross-sectional.
|
||||||
|
|
||||||
|
## Note sui gate non raggiunti
|
||||||
|
|
||||||
|
- **day_boundary_robust** (obbligatorio per ogni effetto calendario): NON eseguibile sui
|
||||||
|
19 alt — i dati HL locali sono **solo 1d**, il confine giorno non è ri-tagliabile. Anche
|
||||||
|
con persistenza, il verdetto massimo sarebbe stato LEAD-forward *condizionato* a barre
|
||||||
|
orarie HL, mai sleeve. Con lo SCARTATO allo step 1 il punto è moot, ma resta il vincolo
|
||||||
|
per chiunque riapra il filone.
|
||||||
|
- **Fee**: mai arrivati al backtest; nota strutturale: un weekday-tilt L/S ribilancia ogni
|
||||||
|
giorno (weekday di domani ≠ oggi) → turnover ~2×gross/die, morte-per-fee quasi certa a
|
||||||
|
0.10% RT anche con edge lordo.
|
||||||
|
- **DSR**: non calcolato (nessuna cella backtestata — il branch strategia esiste nello
|
||||||
|
script ma non si attiva senza persistenza).
|
||||||
|
|
||||||
|
## Verdetto
|
||||||
|
|
||||||
|
**SCARTATO** allo step statistico, senza backtest. Tre motivi: (1) nessuna persistenza
|
||||||
|
split-half sopra il null permutato (p 0.16–0.23 su tutti i test, IC weekend→lunedì non
|
||||||
|
persiste tra le metà); (2) la sola struttura visibile è il canale-beta di un pattern
|
||||||
|
weekday di mercato — la stessa famiglia già scartata in trackF, non un effetto
|
||||||
|
cross-sectional; (3) ~2.5 anni × effetto calendario × turnover giornaliero = rumore +
|
||||||
|
morte-per-fee anche nello scenario migliore.
|
||||||
|
|
||||||
|
File: `scripts/research/r0701_xs_seasonal.py` (deterministico, seed 20260701, ~2 min).
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
# 2026-07-02 — Audit anchor timing-luck su XS01 e SKH01 (chiusura del pendente dell'ondata TIMING)
|
||||||
|
|
||||||
|
**Contesto.** L'ondata TIMING+CRT dello stesso giorno (`2026-07-02-timing-crt-wave.md`) ha stabilito
|
||||||
|
che l'hold-out di TP01 (0.31) è la **migliore delle 24 ancore daily possibili** (mediana 0.04,
|
||||||
|
P~0.86 di uno spike così per caso). Lo scettico ha flaggato l'audit come **pendente** sugli altri
|
||||||
|
due sleeve a ribilanciamento/griglia ancorata: **XS01** (fase del ciclo di ribilanciamento H=10
|
||||||
|
giorni) e **SKH01** (offset della griglia dual-TF 230m/690m, `origin='epoch'`). Questo diario
|
||||||
|
chiude entrambi. Due agenti indipendenti, metodologia identica allo scettico TP01
|
||||||
|
(`r0702_skeptic_offset.py`): ancore dichiarate **a priori**, parametri **identici** in ogni
|
||||||
|
fase/offset (zero tuning per-ancora), **sanity replica bit-exact** della sleeve canonica prima di
|
||||||
|
qualsiasi numero, block-bootstrap congiunto per P(spike).
|
||||||
|
|
||||||
|
**Script:** `scripts/research/r0702_anchor_xs01.py`, `scripts/research/r0702_anchor_skh01.py`.
|
||||||
|
Nessun file di produzione toccato dagli agenti (solo docstring/narrativa aggiornate a valle, qui).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verdetto in una riga
|
||||||
|
|
||||||
|
**Il finding TP01 si replica su TUTTI e tre gli sleeve ancorati — su SKH01 più forte che su TP01 —
|
||||||
|
e i numeri headline del book (HOLD 2.46) sono in parte best-of-anchors.** Ciò che sopravvive
|
||||||
|
de-luckato: XS01 resta un diversificatore ammissibile (ensemble di fase FULL 1.25 / HOLD 1.31 /
|
||||||
|
DD 11%); SKH01 resta ADDS (uplift del blend positivo a **tutte** le 23 fasi, corr ~0.08) ma i suoi
|
||||||
|
numeri headline e — cruciale — **il gate DD<30% che l'ha selezionata** non reggono alla fase
|
||||||
|
mediana. Stima onesta de-luckata del book 5-sleeve: **HOLD ~1.9–2.1, FULL ~2.0–2.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.91–0.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.10–2.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 93–98° 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.03–0.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.9–2.1, FULL ~2.0–2.2, DD ~6%
|
||||||
|
(invariato)** — il book resta largamente positivo e diversificato a ogni combinazione testata,
|
||||||
|
ma il 2.46 è un massimo di ~24×10×23 configurazioni di ancora, non la stima centrale.
|
||||||
|
|
||||||
|
## Regole codificate in narrativa (CLAUDE.md aggiornato)
|
||||||
|
|
||||||
|
1. **I numeri headline di OGNI sleeve ancorato si citano con la banda d'ancora** (o come ensemble
|
||||||
|
di fase): vale già per TP01, ora anche per XS01 e SKH01 e per l'HOLD del book.
|
||||||
|
2. **Un gate di selezione (es. DD<30%) va verificato sulla banda di fase, non sull'ancora
|
||||||
|
canonica** — SKH01-V2-DD fu selezionata per una proprietà che tiene in 8/23 fasi.
|
||||||
|
3. Il candidato gate `anchor_luck_band()` in altlib sale di priorità: tre sleeve su tre ancorati
|
||||||
|
mostrano la stessa firma, e il deflated-Sharpe non conta questo multiple-testing.
|
||||||
|
|
||||||
|
## Follow-up (non eseguiti, da valutare)
|
||||||
|
|
||||||
|
- **Rivalutare il peso 25% di SKH01 nel book live Deribit**: l'aspettativa onesta della gamba SKH
|
||||||
|
è ≈ fase-mediana + path orario (blend HOLD ~0.72, non 1.17) con gap-through-stop reale nei
|
||||||
|
crash. L'uplift resta positivo a ogni fase → non c'è motivo di rimuoverla, ma il sizing fu
|
||||||
|
deciso su numeri best-of-23.
|
||||||
|
- Cadenza cron a 230m (o trigger sui confini di barra) per chiudere il gap di path orario.
|
||||||
|
- `anchor_luck_band()` in `altlib.py` come gate standard per candidati ancorati.
|
||||||
|
- Audit ora-del-giorno di XS01 quando esisterà un feed HL intraday certificato.
|
||||||
|
|
||||||
|
**Stato:** XS01/SKH01/pesi/book live INVARIATI (nessun cambio operativo da questo audit — è un
|
||||||
|
audit di reporting e di gate, e ogni cambio pesi dovrà passare `weights_tilt_null` +
|
||||||
|
selection-on-holdout). Test suite 168 verdi. Script audit committati.
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
# 2026-07-02 — Ondata "video claims": Elliott (3 filoni), Albimarini (2), capital scaling 2-5k
|
||||||
|
|
||||||
|
**Contesto.** Su richiesta: meccanizzare e testare le claim falsificabili di 4 video didattici
|
||||||
|
italiani (3 sulle onde di Elliott, 1 ricostruzione della "strategia Albimarini" — double diagonal
|
||||||
|
a credito su SPX), più l'audit dei muri di scala visto l'annuncio che il conto live verrà portato
|
||||||
|
a **2.000-5.000 $**. Sei agenti indipendenti, harness altlib, fee 0.10% RT, hold-out 2025-26 mai
|
||||||
|
usato per selezionare, banda d'ancora obbligatoria (regola dello stesso giorno). Nessun PASS →
|
||||||
|
nessuno scettico necessario. Nessun file di produzione toccato.
|
||||||
|
|
||||||
|
**Scripts:** `scripts/research/r0702_ell_{rangecycle,fibconfluence,channel}.py`,
|
||||||
|
`r0702_alb_{structure,claims}.py`, `r0702_capital_scaling.py`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verdetto in una riga
|
||||||
|
|
||||||
|
**Elliott 3/3 SCARTATO** (range-cycle = rumore 0/24 Bonferroni; canale = Donchian travestito che
|
||||||
|
non batte il Donchian; Fibonacci = effetto posizione, non numeri magici). **Albimarini 2/2
|
||||||
|
SCARTATO** (la diagonale non domina nemmeno nel modello ed è fee-negativa su Deribit; il track
|
||||||
|
record dichiarato è consistente con zero skill al 20-45%, e va in rovina in ogni finestra storica
|
||||||
|
con una coda). **Capital scaling: l'unico vincolo vero a 2-5k è il cap $300/asset** — da alzare a
|
||||||
|
equity/2 al momento del deposito; tutto il resto si scioglie da solo o resta fuori comunque.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ELL-A — "range-cycle" (onda 1 compressa → onda 3 ampia): RUMORE
|
||||||
|
|
||||||
|
Claim di Frost&Prechter/Larry Williams meccanizzato senza conteggio soggettivo: 2 definizioni di
|
||||||
|
segmento (flip TSMOM, zigzag k·ATR causale), impulso iniziale (N∈{3,5,10} barre, range/ATR
|
||||||
|
pre-segmento) vs ampiezza del seguito (claim A) e troncatura dell'ultimo terzo (claim B).
|
||||||
|
BTC/ETH 1d+1w, Spearman con null permutato (2000), Bonferroni m=24.
|
||||||
|
|
||||||
|
- **0/24 celle significative** (miglior p raw: A 0.061, B 0.058; Bonferroni 1.00 ovunque, FULL e
|
||||||
|
pre-2025). 7-9/12 celle col segno "giusto" = moneta su celle correlate.
|
||||||
|
- **Nessuna cella weekly regge a tutte e 7 le ancore** (p 0.02-0.05 di singole ancore = il
|
||||||
|
timing-luck d'ancora che da oggi non crediamo più; alcune celle flippano segno tra ancore).
|
||||||
|
- **Artefatto previsto e smascherato**: normalizzando col range POST-impulso la correlazione
|
||||||
|
diventa negativa per pura meccanica del denominatore — chi "conferma" il claim così misura
|
||||||
|
l'artefatto.
|
||||||
|
- Coerente con le famiglie di compressione già fallite nello sweep 104 (BRK08 NR7, BRK10
|
||||||
|
BB-squeeze, CMB05, VOL12). Campione strutturalmente piccolo (mediana 78 segmenti daily, ~30
|
||||||
|
weekly). Strategizzazione correttamente non eseguita (statistica prima della strategia).
|
||||||
|
|
||||||
|
⚠️ **LEZIONE PANDAS (gemella di `view("int64")` del 2026-07-01):** `resample("7D",
|
||||||
|
origin=...)` in pandas 2.x **ignora silenziosamente `origin`** ("D" non è Tick-like, solo un
|
||||||
|
RuntimeWarning) → tutte le ancore weekly uscivano identiche e la banda d'ancora sarebbe stata una
|
||||||
|
banda finta di larghezza zero. Serve **`"168h"`**. Da ricordare per ogni futura banda d'ancora
|
||||||
|
settimanale.
|
||||||
|
|
||||||
|
## ELL-B — Confluenza di Fibonacci: EFFETTO POSIZIONE, non numeri magici
|
||||||
|
|
||||||
|
Zigzag causale non-repainting (livelli usabili solo dalla conferma del pivot, causality_ok
|
||||||
|
max_tail_diff=0.0), tocco fresh intrabar, reazione ATR-normalizzata a 5/20 barre, ~24k/14k tocchi
|
||||||
|
IS a 1h. **Tre null in scala di durezza**: (A) rapporti uniformi casuali, (B) ri-sorteggiati per
|
||||||
|
swing, **(C) location-matched** (Fib ± jitter 2-8%: "0.618 è speciale vs 0.58/0.66?").
|
||||||
|
|
||||||
|
- Vs null A/B i Fib sembrano buoni (pctl 0.82-1.00) — **vs null C crollano a 0.39-0.68 = puro
|
||||||
|
caso**. L'apparente vantaggio è interamente l'effetto di *posizione* (i rapporti Fib cadono in
|
||||||
|
zone mediamente decenti del range), non dei numeri di Fibonacci.
|
||||||
|
- **Confluenza** (2 griglie entro 0.25·ATR): FAIL 4/4 celle — non reagisce più delle confluenze
|
||||||
|
di rapporti casuali. Break-rate al tocco ~46-50% (moneta); su 1h i Fib veri vengono bucati più
|
||||||
|
spesso del placebo.
|
||||||
|
- In assoluto comunque nulla da tradare: +2.6/+5.6 bps a 5 barre (fee = 10 bps), negativo a 20
|
||||||
|
barre e su 1d. Strategizzazione correttamente saltata da gate a priori.
|
||||||
|
- **Pattern di test riusabile**: il null location-matched è IL test giusto per ogni claim su
|
||||||
|
"livelli speciali" (Fib, pivot, round numbers) — separa la qualità della posizione dal numero.
|
||||||
|
|
||||||
|
## ELL-C — Tecnica del canale (Ftaonline): DONCHIAN TRAVESTITO
|
||||||
|
|
||||||
|
Implementazione onesta completa (zigzag causale, vincolo onda-2>origine, canale 0→min-2 con
|
||||||
|
parallela dal max-1, entry al close della barra fuori, target 1.618×, stop=invalidazione,
|
||||||
|
speculare short, 27 celle TF×k×varianti).
|
||||||
|
|
||||||
|
- **family_honest boccia**: cella scelta in-sample (1h k4 long, IS 1.40) → hold-out **−0.87**;
|
||||||
|
DSR 0.685 < 0.95; earns_slot=False. Marginale vs TP01: uplift positivo solo 2020-21 =
|
||||||
|
beta-trend del toro (corr 0.35).
|
||||||
|
- **Il Donchian banale a pari geometria/frequenza eguaglia o batte il canale a ogni TF**
|
||||||
|
(corr 0.43-0.53): vincoli d'onda + 1.618 non aggiungono nulla. Il segnale-2 ("onda 5") è
|
||||||
|
quasi inerte (+0.00-0.07).
|
||||||
|
- **Target 1.618 = geometria, non magia**: toccato con la frequenza di distanze identiche
|
||||||
|
piazzate a caso (5/6 celle p>0.18). Claim discriminante "in canale = correttivo":
|
||||||
|
direzionalmente sensata, mai significativa (p 0.08-0.45); l'uscita dal canale è momentum da
|
||||||
|
breakout, già in casa.
|
||||||
|
- **Anchor-luck di nuovo**: la miglior cella 4h fa hold 1.54 all'ancora 00:00, la migliore delle
|
||||||
|
4 (banda [0.35, 1.54]) — quarta conferma della firma nello stesso giorno.
|
||||||
|
|
||||||
|
## ALB-A — Double diagonal "Albimarini" su BTC/ETH: NON DOMINA, niente LEAD
|
||||||
|
|
||||||
|
Portata su Deribit (che ha le scadenze giornaliere per la long T+1) col motore DVOL di VRP01
|
||||||
|
(riprodotto esatto: 1.09/0.59/11.8%), strike scalati in vol (2σ/5g ≈ il "9% su SPY"), gate
|
||||||
|
IV-rank canonico non riottimizzato, 2021-26 con LUNA/FTX/2022, banda skew f∈{0.6,0.8,1.0,1.3}.
|
||||||
|
|
||||||
|
- **Il claim strutturale del corso è falsificato**: gated, l'iron condor stessa-scadenza batte la
|
||||||
|
diagonale a ogni f (1.05 vs 0.95 a f=1.0), Δ negativo ogni anno. La long T+1 è **assicurazione
|
||||||
|
di coda quantificata** (+12/+33 bps nei 5% peggiori vs ~1 bps di costo medio), non dominanza.
|
||||||
|
- **Senza gate tutte le strutture perdono** a f≤1.0 → terza conferma indipendente: nel VRP
|
||||||
|
l'alpha è il filtro di regime, non la vendita di premio. VRP01 resta superiore su tutta la
|
||||||
|
banda f (CAGR 8.3% vs 1.4%).
|
||||||
|
- **Il meccanismo dell'illusione riprodotto in vitro**: celle z≥2.5 = 0 perdite su 142 trade,
|
||||||
|
Sharpe 5.9-∞ (il punto cieco CC01 in forma opzioni); la best in-sample (7.88) crolla a −0.51
|
||||||
|
in hold-out. Seconda occorrenza del pattern "0-perdite = Sharpe implausibile" in una settimana
|
||||||
|
→ **si rafforza il caso per il gate `implausible_sharpe` in altlib** (raccomandato dal filone
|
||||||
|
CC01, ora con 2 casi d'uso).
|
||||||
|
|
||||||
|
## ALB-B — Audit dei numeri dichiarati (28 trade, 82%, PF 5.16, "420% annuo"): ZERO SKILL
|
||||||
|
|
||||||
|
SPY 1d 1996-2026 (parquet già in repo dal filone GTAA) + BTC/ETH certificati; MC 200k; replay
|
||||||
|
claim-anchored calibrato per riprodurre esattamente il video.
|
||||||
|
|
||||||
|
- **L'82% è incoerente col "deep OTM" in entrambe le letture**: strike a 9% → win strutturale
|
||||||
|
99.5% (P(|move 4td|>9%) = 0.53%, fat-tail 88× la normale); 82% letterale → strike a 2.63% ≈
|
||||||
|
1.2σ, non deep. In entrambi i casi win-rate = delta venduto.
|
||||||
|
- **P(track record del video senza edge) = 20-45%**; il **78.6%** di tutte le finestre 6-mesi
|
||||||
|
1996-2026 lo avrebbe prodotto (lettura deep). Il PF ~5.1 esce per costruzione quando la coda
|
||||||
|
non è campionata.
|
||||||
|
- **Replay attraverso le code reali**: rovina nel 1998/2002/2020 col sizing 1→4 dichiarato;
|
||||||
|
perfino regalando +20% di premio, 1 contratto fisso fa CAGR +4.1% con maxDD −95%. La stessa
|
||||||
|
semestrale del video annualizza a +73-77%, non 420%. Su BTC/ETH a EV=0: +12%/DD−68% e
|
||||||
|
+0%/DD−88%.
|
||||||
|
- **La diagonale lascia passare il 12-40%** della perdita naked in un −10% overnight (dipende dal
|
||||||
|
vol-spike). Su Deribit la double diagonal è **fee-negativa a qualsiasi size** (fee 8 gambe =
|
||||||
|
194-221% del theta raccolto a questi strike): il capitale sposta solo il muro di margine.
|
||||||
|
- Che cosa distingue VRP01 (sopravvissuto allo stesso tipo di audit): defined-risk per
|
||||||
|
costruzione, gate IV-rank che ribalta l'hold-out, sizing fisso — l'opposto esatto su tutti e
|
||||||
|
tre gli assi.
|
||||||
|
|
||||||
|
## Capital scaling 600 → 2000/3500/5000 (r0702_capital_scaling.py)
|
||||||
|
|
||||||
|
| capitale | TP01 haircut Sh | book investibile % (cap $300) | K=2 exec-turn % | statarb haircut | spread ETH conc. | XS01 exec-turn % |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| 600 | +0.010 | 100.0% | 90.5 | −0.004 | 0 | 56.9 |
|
||||||
|
| 2000 | −0.002 | 85.9% | 95.7 | 0.000 | 3 | 87.4 |
|
||||||
|
| 3500 | −0.002 | 63.7% | 97.5 | 0.000 | 5 | 89.3 |
|
||||||
|
| 5000 | −0.002 | **48.9%** | 98.2 | 0.000 | 7 | 90.8 |
|
||||||
|
|
||||||
|
- **Il vincolo dominante è UNO: `max_notional_per_asset_usd`=300.** A 5k il book girerebbe a
|
||||||
|
metà del target (cap binding nel 39-46% delle barre). **Proposta config (da applicare AL
|
||||||
|
deposito): cap = equity/2** → $1000 a 2k, $1750 a 3.5k, $2500 a 5k. `min_order_usd`=5 va
|
||||||
|
LASCIATO (il fee drag risale solo al tetto noto ~0.4%/anno; l'isteresi artificiale costerebbe
|
||||||
|
più in lag — lezione ondata timing).
|
||||||
|
- Min-order/haircut TP01 ≈ 0 a ogni capitale (conferma indipendente del "−74% ordini" a 600 →
|
||||||
|
39-43% saltati a 5k, tutti micro). Statarb: haircut 0 da 2k (il muro era l'edge, non la scala).
|
||||||
|
- Tranching K=2: matematicamente regge a 5k (98.2% turnover-tranche), ma il blocco vero resta il
|
||||||
|
feed intraday fuori path certificato + beneficio solo di varianza → **non cablare**.
|
||||||
|
- Opzioni: il muro d'ingresso ETH cade a ~2.6k (3-7 spread concorrenti a 2-5k, fee solo 5-6% del
|
||||||
|
credito grazie al cap 12.5%; rettifica vs ALB-A: margine spread ETH oggi $66-76). BTC granulare
|
||||||
|
da ~$4.7k. **La regola "niente short-vol da modello" non decade col capitale.**
|
||||||
|
- XS01: soglia ~20k confermata (a 5k il 9%+ del turnover non esegue). CC01: il capitale non è
|
||||||
|
mai stato il muro (fuori per ragioni strutturali).
|
||||||
|
- **Aspettativa onesta**: col CAGR de-luckato del book live (10-15%, audit SKH01): **2k ≈
|
||||||
|
€0.55-0.82/giorno; 5k ≈ €1.37-2.05/giorno**. Il salto di capitale cambia cosa è eseguibile,
|
||||||
|
non l'ordine di grandezza del ritorno (€50/g ≈ 130k, invariato).
|
||||||
|
|
||||||
|
## Lezioni
|
||||||
|
|
||||||
|
1. **Il null giusto smonta il livello "speciale"**: location-matched (ELL-B) per i livelli,
|
||||||
|
Donchian-equivalente (ELL-C) per i pattern, EV=0-calibrato (ALB-B) per i track record. In
|
||||||
|
tutti e tre i casi il claim sopravviveva ai null ingenui e moriva su quello giusto.
|
||||||
|
2. **`resample("7D", origin=...)` ignora origin in pandas 2.x** → usare `"168h"` per le bande
|
||||||
|
d'ancora settimanali.
|
||||||
|
3. Secondo caso "0-perdite = Sharpe implausibile" (celle deep-OTM di ALB-A, dopo CC01) → il gate
|
||||||
|
`implausible_sharpe` in altlib sale in coda ai lavori insieme ad `anchor_luck_band`.
|
||||||
|
4. L'anchor-luck è comparso spontaneamente anche in questa ondata (celle weekly ELL-A, 4h ELL-C)
|
||||||
|
— la regola della banda d'ancora sta già pagando.
|
||||||
|
|
||||||
|
**Stato:** nessun nuovo sleeve, nessun forward-monitor aggiunto, book live INVARIATO. Un'azione
|
||||||
|
operativa pende sul FUTURO deposito: alzare `max_notional_per_asset_usd` a equity/2 (decisione
|
||||||
|
utente al momento del funding). Test suite verde. 6 script committati.
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
# 2026-07-02 — Ondata timing + CRT (8 filoni multi-agente + scettico): 0 nuovi sleeve, 1 finding strutturale (anchor timing-luck di TP01)
|
||||||
|
|
||||||
|
**Obiettivo (goal utente):** "cerca su Deribit altre strategie profittevoli magari con timing
|
||||||
|
differenti oltre a quelle già valutate". Prima ondata interamente dedicata al TIMING: dopo che le
|
||||||
|
ondate precedenti hanno coperto segnali (104 famiglie alt-sweep), universi (HL cross-sectional),
|
||||||
|
opzioni (VRP/gamma) e pesi (EW-STR/weights_tilt_null), qui si attacca la dimensione rimasta: *quando*
|
||||||
|
si campiona, si decide e si esegue. Su richiesta utente, seconda tranche dedicata alla **CRT (Candle
|
||||||
|
Range Theory)**, il pattern sweep-and-reclaim a 3 candele di scuola ICT/Wyckoff.
|
||||||
|
|
||||||
|
Inventario preventivo per non rifare lavoro: SEA01-09 coprivano già hour-of-day, day-of-week,
|
||||||
|
weekend, turn-of-month, sessioni; l'onda intraday 2026-06-21 il sub-daily (con il lead prevday in
|
||||||
|
forward-monitor); `tp01_lowfreq` i TF 4h/12h/1d. Angoli NON coperti individuati e testati: clock a
|
||||||
|
eventi, calendario scadenze Deribit, ancora di ribilanciamento, clock più lenti del daily, velocità
|
||||||
|
del segnale regime-condizionata — più i 3 tagli CRT (base, multi-TF, contesto).
|
||||||
|
|
||||||
|
**Esito: 7 filoni FAIL + 1 finding strutturale confermato dallo scettico (timing-luck dell'ancora
|
||||||
|
di TP01). Nessun nuovo sleeve. Nessuna modifica al book live. Il soffitto ~1.3 direzionale
|
||||||
|
BTC/ETH resta intatto anche cambiando il clock.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Filone 1 — Event-clock bars (tempo-informazione): FAIL
|
||||||
|
|
||||||
|
`scripts/research/r0702_eventclock.py`. Volume/vol/range bars da 5m certificato (soglie causali
|
||||||
|
EWMA-90g shift(1), barre medie 4h/12h/24h), 5 strategie (TSMOM multi-orizzonte, Donchian 10/30d,
|
||||||
|
EWMA-cross), 45 celle event + 15 controlli wall-clock = 60 trial.
|
||||||
|
|
||||||
|
- Null decisivo (stesso segnale, stessa frequenza, wall-clock): event batte wall su ENTRAMBE le
|
||||||
|
finestre solo in **4/45 coppie (9%)** — rumore. Pattern incoerenti (EWMA event vince IS 9/9 ma
|
||||||
|
perde HOLD 9/9; Donchian-30d l'esatto opposto = selection-on-holdout in agguato).
|
||||||
|
- Cella scelta in-sample (volume-bars 24h, DONCH-10d): IS 1.45 → **HOLD −0.46** (2025 −7.4%,
|
||||||
|
2026 −18.1%). Marginal NEUTRAL (corr 0.74 a TP01 = trend travestito), earns_slot_honest=False.
|
||||||
|
- Executability comunque assente: in alta attività le barre "24h" chiudono in 3.6-4.6h (p5) → il
|
||||||
|
cron orario le eseguirebbe in ritardo; i clock 4h richiedono monitoraggio sub-orario.
|
||||||
|
|
||||||
|
**Lezione: campionare a tempo-informazione non normalizza nulla di monetizzabile sul feed
|
||||||
|
certificato; il clock non è la dimensione dove vive l'edge.**
|
||||||
|
|
||||||
|
## Filone 2 — Calendario scadenze Deribit: FAIL (0/24 celle)
|
||||||
|
|
||||||
|
`scripts/research/r0702_expiry_calendar.py`. Griglia dichiarata a priori: 4 finestre × 3 tipi expiry
|
||||||
|
(weekly ven 08:00 UTC / monthly ultimo ven / quarterly) × 2 asset, Bonferroni |t|≥3.08; 3 null
|
||||||
|
(placebo weekday, anchor-shift ±2/4h, permutation 500 calendari).
|
||||||
|
|
||||||
|
- Nessuna cella passa Bonferroni (max |t| 2.69). Il post-expiry drift monthly (+0.61/+0.77%) ha il
|
||||||
|
segno della teoria ma: non batte il placebo giovedì, permutation pctl 84-95 (non estremo), si
|
||||||
|
INVERTE sul quarterly (dove l'OI massimo dovrebbe amplificarlo) ed è guidato dal 2019 → rumore.
|
||||||
|
- Unica cella robusta ai null: weekly [-24,0) negativa (gio→ven), ma è indistinguibile dal
|
||||||
|
day-of-week (famiglia SEA già morta) e netta fee fa Sharpe ~0.08 (52 eventi/anno × 0.10% RT).
|
||||||
|
- Best-in-sample tradabile: IS +0.90 → **HOLD −0.52**; DSR 0.230 su 24 trial.
|
||||||
|
- Nota tecnica: colpito ESATTAMENTE il pitfall pandas 2.x documentato (epoca in secondi da
|
||||||
|
`date_range` su tz-aware → matching vuoto); fix con epoca ms esplicita. La lezione 2026-07-01
|
||||||
|
ha pagato.
|
||||||
|
|
||||||
|
## Filone 3 — Anchor timing-luck di TP01 + tranching: FINDING (verificato dallo scettico)
|
||||||
|
|
||||||
|
`scripts/research/r0702_tp01_offset.py` + scettico indipendente `r0702_skeptic_offset.py`
|
||||||
|
(ricostruzione bit-exact con codice diverso, zero riuso; h=0 riproduce `tp01_baseline_daily`
|
||||||
|
esattamente; vol-target ricalcolata per ancora; nessun look-ahead — verificato con troncamenti).
|
||||||
|
|
||||||
|
**Claim 1 CONFERMATO: l'hold-out 2025-26 "Sharpe 0.31" di TP01 è in larga parte fortuna
|
||||||
|
dell'ancora 00:00 UTC.** La stessa strategia CANONICAL alle altre 23 ancore orarie: HOLD mediano
|
||||||
|
**0.04**, banda **[−0.13, +0.30]** — e il massimo è proprio h=0 (98° pctl; in-sample era al 10°:
|
||||||
|
sfortunato lì, fortunato qui). Spike, non plateau (h=23/h=1 fanno 0.13/0.04). Bootstrap dello
|
||||||
|
scettico: P(una qualsiasi ancora mostri uno spike ≥ così) = **0.86** — è il massimo atteso di 24
|
||||||
|
stime correlate, non un'anomalia; best-anchor per finestra annuale ruota a caso (16→23→21→12→5→0,
|
||||||
|
Spearman ≈ 0, nessuna seasonality d'ancora). Formulazione corretta (NON "il vero hold-out è 0.07",
|
||||||
|
che ha CI95 [−1.2,+1.4] su 547 giorni): **l'hold-out 2025-26 non risolve l'edge di RITORNO di TP01**
|
||||||
|
(ritorno totale mediano ancore ≈ 0%); ciò che regge A OGNI ancora è il claim DIFENSIVO
|
||||||
|
(DD 6.7-10.1% vs ~60% B&H). Che è da sempre il vero valore dichiarato di TP01.
|
||||||
|
|
||||||
|
**Claim 2 (tranching multi-ancora) RIDIMENSIONATO:** K=4 ≡ esattamente EW di 4 book ancorati
|
||||||
|
(diff 1.4e-17), turnover identico (~8.3/y), haircut $600 ≈ 0. MA il "miglioramento" IS 1.49→1.56 è
|
||||||
|
per ~90% *tornare alla media delle ancore* (h=0 era sfortunato in-sample): vs ancora tipica
|
||||||
|
+0.005 mediano, P(K4≤h0)=0.18 n.s.; il taglio DD strutturale è ~0.5pt (non 2.8: la mediana delle
|
||||||
|
singole è già 12.6%). Ciò che regge: la compressione della VARIANZA DELLA STIMA (HOLD
|
||||||
|
[−0.12,+0.30]→[−0.01,+0.13] a K=4) → **lens di reporting, non alpha**.
|
||||||
|
|
||||||
|
**Impatto a valle (quantificato dallo scettico):**
|
||||||
|
- Blend SKH: HOLD 1.16 (h=0) → 0.97 mediana ancore, ma l'UPLIFT a mediana è +0.93 > +0.86
|
||||||
|
dichiarato → **il verdetto ADDS di SKH01 era conservativo, regge**.
|
||||||
|
- Book 5-sleeve: HOLD 2.44 → 2.34 mediana (min 2.22), FULL 2.24→2.22 → eredita **~+0.10** di
|
||||||
|
fortuna d'ancora (non +0.25: il peso di TP01 diluisce). Nessuna decisione (GTAA/SKH/pesi) cambia.
|
||||||
|
- NON toccati: tutti i FULL (h=0 al 31° pctl = normale), plateau, deflated-Sharpe, fee-sweep,
|
||||||
|
causalità, validazione GTAA (equity OOS), gate weights_tilt_null (relativo).
|
||||||
|
|
||||||
|
**Raccomandazione operativa (sottoscritta dallo scettico): cambio di narrativa e di standard di
|
||||||
|
reporting, NESSUN cambio del book live oggi.** (1) I numeri hold-out di TP01 si citano d'ora in poi
|
||||||
|
come banda d'ancora (mediana ~0.04, [−0.13,+0.30]) con valore = taglio DD; (2) valutazione
|
||||||
|
anchor-agnostic (media 24 ancore) come lens di reporting per i futuri hold-out; (3) NO deploy
|
||||||
|
K=2/K=4 a $600: guadagno ≈ 0, e a $225/asset di quota TP01 i delta per-ancora (~$1-2) sono sotto il
|
||||||
|
min-order $5 → degenererebbe comunque in K=1; inoltre le ancore intraday richiederebbero un feed
|
||||||
|
fresco fuori dal path certificato (staleness ×4 finestre di guasto). **Rivalutare K=2 (0,12) a
|
||||||
|
capitale ≥ ~5-10k.** (4) ⚠️ Audit analogo RACCOMANDATO su XS01 (rebalance 10g ancorato = spazio di
|
||||||
|
luck 10g×24h, potenzialmente peggiore) e SKH01 (fase griglia 230m/690m) — TP01 è oggi l'unico
|
||||||
|
sleeve de-luckato.
|
||||||
|
|
||||||
|
## Filone 4 — Clock lenti + banded rebalancing: FAIL (negativo utile)
|
||||||
|
|
||||||
|
`scripts/research/r0702_slow_clock.py`. Fatto a monte che chiude il filone: il fee drag di TP01 a
|
||||||
|
1d è **~0.40%/anno ≈ 0.03 Sharpe** (8 turnover/y × 0.10% RT) — il tetto di QUALSIASI risparmio.
|
||||||
|
- Clock N∈{2,3,5,7}g: degrado monotono dell'ensemble di fase (HOLD 0.34→0.11 da N=2 a N=7); lo
|
||||||
|
spread TRA fasi esplode a N≥5 (HOLD −0.19…+0.37 a N=7) = timing luck pura, coerente col filone 3.
|
||||||
|
- Bande di isteresi {2.5,5,10,20}%: tagliano il 77-94% degli ORDINI ma quasi zero TURNOVER (gli
|
||||||
|
ordini di TP01 sono micro-aggiustamenti del vol-target). Cella best-IS (band 20%): hold-out
|
||||||
|
0.13 vs 0.30 baseline, e il "guadagno" IS è effetto-segnale (posizioni stantie fortunate), non
|
||||||
|
effetto-costo → fitting. Non-monotonia sulla griglia = firma di rumore.
|
||||||
|
- **Finding utile per il live: a $600 il vincolo min-order $5 È GIÀ la banda ottimale** (ordini
|
||||||
|
427→111/y a costo Sharpe ~0, banda implicita 1.67%): cattura ~100% del risparmio catturabile.
|
||||||
|
Nessun cambio al book, a nessun capitale testato (600/2k/10k).
|
||||||
|
|
||||||
|
## Filone 5 — Velocità del trend regime-condizionata: FAIL
|
||||||
|
|
||||||
|
`scripts/research/r0702_regime_speed.py`. Pesi tra gli orizzonti TSMOM 30/90/180g condizionati al
|
||||||
|
percentile espandente di vol (realized e DVOL), 16 celle, sanity = riproduzione esatta del canonico.
|
||||||
|
- Cella best-IS (alta-vol→lento, linear, rv): FULL +0.06 ma **HOLD BTC −0.31**, 50/50 +0.005 →
|
||||||
|
dominanza fallita; multi-cut a segno instabile (+0.18/−0.08/+0.00).
|
||||||
|
- Null decisivo (300 pesi statici Dirichlet): la cella sta al pctl 0.71-0.72, sotto il p90 —
|
||||||
|
un peso statico casuale la batte spesso. Meccanismo smascherato: corr(peso-30d, Sharpe) = +0.93 →
|
||||||
|
le celle "regime" vincono in-sample perché tengono il tilt-30d la maggior parte dei giorni =
|
||||||
|
**tilt statico travestito da regime**, la stessa trappola di EW-STR. E il tilt statico verso il
|
||||||
|
30d NON regge su BTC hold-out (−0.15). RV e DVOL indistinguibili come misura di regime (coerente
|
||||||
|
con l'esito overlay DVOL 2026-06-26). Pesi canonici 1/3-1/3-1/3 confermati.
|
||||||
|
|
||||||
|
## Filoni 6-8 — CRT "Candle Range Theory" (base / multi-TF / contesto): FAIL 3/3
|
||||||
|
|
||||||
|
Il pattern sweep-and-reclaim a 3 candele (C1 range forte; C2 rompe un estremo ma chiude dentro =
|
||||||
|
"manipolazione"; C3 ingresso contro il breakout, stop dietro lo sweep, target all'estremo opposto),
|
||||||
|
meccanizzato onestamente in tre tagli. Overlap dichiarato: MRV01-11 e MIC07 non coprivano questa
|
||||||
|
meccanica; ora è coperta.
|
||||||
|
|
||||||
|
**6. Base single-TF** (`r0702_crt_base.py`, 864 trial su 1h/4h/12h/1d, motore trade-level
|
||||||
|
conservativo SL-first cross-checkato con l'harness): **DSR 0.000** (il null best-of-grid atteso è
|
||||||
|
Sharpe 2.27, ottenuto 0.74); cella best-IS (4h long color-rule) IS 0.90 → HOLD 0.07 con 2026 a
|
||||||
|
WR 0%; anchor-shift +1/+2h flippa l'hold-out a −1.0 (artifact-risk); lo **short su sweep dell'alto
|
||||||
|
(la narrativa smart-money canonica) perde perfino in-sample (−0.59)**. Autopsia: l'expectancy IS
|
||||||
|
veniva dal time-exit in trend (beta del toro ETH 2021-23), non dal target strutturale. Dettaglio
|
||||||
|
informativo: CRT batte nettamente sia il fade incondizionato (IS −0.49) sia il breakout-confermato
|
||||||
|
(IS −0.31) → il close-back-inside FILTRA davvero tossicità, ma "meno tossico del fade morto" non è
|
||||||
|
un edge.
|
||||||
|
|
||||||
|
**7. Multi-timeframe** (`r0702_crt_mtf.py`, 4h→15m e 1h→5m, ~10k trade): expectancy netta negativa
|
||||||
|
OVUNQUE (FULL e HOLD, entrambi gli asset); il MTF alza il R:R medio da ~3 a ~10-20 ma il WR collassa
|
||||||
|
da ~36% a 9-16% → "migliora" solo perdendo meno. **Refutazione strutturale: il ritest è informazione
|
||||||
|
negativa** — pattern CON ritest −40 bps, SENZA +52 bps (non tradabile: condiziona sul futuro):
|
||||||
|
aspettare il ritest per entrare seleziona sistematicamente i pattern peggiori; i buoni scappano
|
||||||
|
subito e il metodo non li prende mai. A fee ZERO l'edge lordo è ~0 (non è morte per fee: non c'è).
|
||||||
|
Stop 0.1-0.35% ineseguibili col cron orario (26-75% dei segnali già invalidati all'esecuzione).
|
||||||
|
|
||||||
|
**8. Contesto** (`r0702_crt_context.py`, 22 trial: livelli prevday/Donchian/prevweek × equal-H/L ×
|
||||||
|
FVG × sessioni): baseline incondizionata morta (8/8 celle HOLD<0); FVG semmai peggiora; sessione
|
||||||
|
EU consistentemente negativa; unica cella IS-positiva (sweep in Asia su livello prevday) uccisa 4
|
||||||
|
volte (DSR 0.001, anchor-flip a −2/−4h con la firma di open_drive — la sessione "Asia" inizia dove
|
||||||
|
il livello prevday viene ricreato —, solo-ETH, morta a 0.20% fee). **Sottoprodotto prezioso: sugli
|
||||||
|
STESSI livelli prior-day, FOLLOW batte FADE ogni singolo anno 2019-2026** (follow IS/HOLD
|
||||||
|
+1.22/+1.25 vs fade −0.66/−1.46, corr −0.19/−0.27) → conferma indipendente e rafforzativa del lead
|
||||||
|
prevday-breakout in forward-monitor (paper_prevday).
|
||||||
|
|
||||||
|
**Verdetto CRT: concetto Wyckoff riverniciato che, spogliato della discrezionalità, non contiene
|
||||||
|
edge su BTC/ETH Deribit certificati. La direzione giusta sui livelli affollati resta il FOLLOW
|
||||||
|
(breakout), non il fade — coerente con SKH01 (breakout) vivo e mean-reversion morta.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lezioni codificabili
|
||||||
|
|
||||||
|
1. **Il timing-luck dell'ancora è una dimensione di multiple-testing NON coperta dai gate
|
||||||
|
esistenti** (deflated-Sharpe conta i trial, non le ancore implicite). Nuova regola di reporting:
|
||||||
|
ogni metrica hold-out di una strategia a ribilanciamento ancorato si cita con la banda d'ancora
|
||||||
|
(o la media anchor-agnostic). Candidato gate futuro: `anchor_luck_band()` in altlib. Audit
|
||||||
|
pendente: XS01 (10g), SKH01 (fase 230m/690m).
|
||||||
|
2. **La famiglia day_boundary_robust / anchor-shift continua a uccidere** (CRT base, CRT-Asia,
|
||||||
|
6/24 celle expiry): ogni effetto legato a etichettatura di barre VA spostato d'ancora prima di
|
||||||
|
crederci. Confermata la regola 2026-06-21.
|
||||||
|
3. **Il fee drag di TP01 (~0.4%/anno) non è un problema da risolvere** — chiude a priori il filone
|
||||||
|
"esecuzione più furba" a questo turnover; il min-order small-cap è già la banda ottimale.
|
||||||
|
4. Il pitfall pandas tz-aware/epoca (2026-07-01) è stato evitato/gestito 2 volte su 9 agenti grazie
|
||||||
|
alla documentazione in CLAUDE.md — il costo di documentare i bug paga.
|
||||||
|
|
||||||
|
## Stato finale
|
||||||
|
|
||||||
|
- 0 nuovi sleeve; portafoglio e book live INVARIATI (5-sleeve 33/15/12/20/20; book Deribit
|
||||||
|
TP01+SKH01 75/25, flat da armamento — il segnale resta risk-off).
|
||||||
|
- Narrativa TP01 aggiornata (CLAUDE.md + docstring): hold-out = banda d'ancora, valore = difesa DD.
|
||||||
|
- Script: `scripts/research/r0702_{eventclock,expiry_calendar,tp01_offset,slow_clock,regime_speed,
|
||||||
|
crt_base,crt_mtf,crt_context,skeptic_offset}.py`. Test suite: invariata, verde.
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
# 2026-07-03 — Frontiera rischio/rendimento del book: quanto caldo correre a 2k-5k
|
||||||
|
|
||||||
|
**Contesto.** Dopo la diagnosi strategica ("€50/g è un problema di capitale e di frontiera, non di
|
||||||
|
alpha; hai costruito una macchina difensiva a DD 6% ma hai capitale piccolo e obiettivo di
|
||||||
|
crescita"), goal: **misurare** (non cercare segnali) la frontiera CAGR/DD/P-rovina in funzione del
|
||||||
|
target-vol, pesata per confidenza per-sleeve, a €2k e €5k — per decidere quanto caldo girare il book.
|
||||||
|
Workflow: 3 filoni (curva / onestà-della-coda / eseguibile+Kelly), refuter fat-tail su ciascuno,
|
||||||
|
scettico sintetizzatore. 7 agenti, 0 errori. Sanity bit-exact di entrambi i book prima di ogni numero.
|
||||||
|
|
||||||
|
**Scripts:** `scripts/research/r0703_frontier_{curve,tail,exec}.py`. Nessun file di produzione
|
||||||
|
toccato, nessun commit dagli agenti, `config/live.json` NON modificato (cap solo proposto).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verdetto in una riga
|
||||||
|
|
||||||
|
**La tesi "corri più caldo" era in gran parte un MIRAGGIO DI REGIME.** Il "~6% attuale" non è un
|
||||||
|
target scelto: è la vol *realizzata* in regime risk-off (TP01 spesso flat 0.0x → il conto è flat
|
||||||
|
*by design*). A lambda=1 il book LIVE a 2 sleeve gira **già a ~11% vol / 9.4% DD**. Quindi "saltare
|
||||||
|
da 6% a 10%" NON è aggiungere leva — è solo assumere che il book non sia risk-off. **Il numero
|
||||||
|
onesto è: gira alla vol NATIVA (~10%, banda 8-11%), non levare.** Guadagno reale vs il 6% risk-off:
|
||||||
|
**+€0.2/g @2k, +€0.5/g @5k** — reale ma modesto, comprato con un DD plausibile di ~€500 su €2k. €50/g
|
||||||
|
resta ~130k, invariato.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## I numeri (frontiera fidata, non full-Sharpe)
|
||||||
|
|
||||||
|
Sanity (deriva nulla): 5-sleeve FULL Sh **2.238** / HOLD 2.452 / DD 6.17% / vol nativa 7.84%;
|
||||||
|
2-sleeve live FULL Sh **1.776** / HOLD 1.162 / DD 9.42% / vol nativa 10.96%.
|
||||||
|
|
||||||
|
**Scale-invarianza confermata da tutti e 3 i refuter:** il capitale NON cambia la matematica del
|
||||||
|
target-vol (le tabelle sono in %), cambia solo l'ESEGUIBILITÀ → **stesso % a 2k e 5k.**
|
||||||
|
|
||||||
|
### Book LIVE 2-sleeve (TP01+SKH01) — l'unico eseguibile <20k
|
||||||
|
- **Target-vol raccomandato: ~10%, banda [8%, 11%], lambda ≈ 0.9.** Sopra la vol nativa non si va.
|
||||||
|
- €/giorno al punto fidato (haircut): CAGR full-sample 12.9% → €0.71@2k / €1.77@5k; ma **HOLD-onesto**
|
||||||
|
(de-luck) CAGR ~4.5% → **€0.25@2k / €0.62@5k**. Il lato-reward headline è ~3× l'onesto (HOLD≪FULL).
|
||||||
|
- **Stima centrale onesta: €0.3-0.7/g @2k, €0.8-1.8/g @5k.**
|
||||||
|
|
||||||
|
### Book PIENO 5-sleeve — ASPIRAZIONALE, non gira <20k
|
||||||
|
Frontiera fidata regge ~12-15% sotto tail estremo, ~20% sotto tail mite — ma XS01 serve ~20k e VRP01
|
||||||
|
è modellato → rilevante solo per il tracking di `paper_combo`, non per una decisione di deploy a 2-5k.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## I quattro insegnamenti strutturali
|
||||||
|
|
||||||
|
### 1. La rovina-50% è la metrica SBAGLIATA (trappola esposta dal curve-refuter)
|
||||||
|
P(rovina-50%|5y) resta <1% fino a ~20-22% di target-vol su ogni book — i book sono troppo lisci/
|
||||||
|
diversificati per bruciare metà capitale entro la distribuzione campionaria. **La risposta letterale
|
||||||
|
"max CAGR a P(rovina)<1%" direbbe "gira al 22%" — ed è un suicidio**: lì P(DD>30%|5y) ≈ 20-25%. Il
|
||||||
|
vincolo che MORDE è **P(DD>30%), super-lineare** (raddoppia ~ogni 2.5 punti di vol). E la rovina-50%
|
||||||
|
non è nemmeno robusta ad alta vol (block-15 vs block-20 danno 7.1% vs 0.9% a tv30%) → altro motivo
|
||||||
|
per stare bassi.
|
||||||
|
|
||||||
|
### 2. Il muro fat-tail ONESTO è ~12%, più basso della carta (~15%)
|
||||||
|
Tre ragioni che il modello sottostima: **(a) gap-through-stop SKH già osservato reale** (sl2%
|
||||||
|
modellato → −11/−23% realizzato — la coda vera NON è nei rendimenti modellati; l'iniezione dei
|
||||||
|
finder usava lo *clean-stop* → ~2× troppo mite); **(b) il 2-sleeve è 4× più crash-sensibile del
|
||||||
|
5-sleeve** (+14.5pt vs +3.5pt di P(DD>30) all'iniezione, diversificazione sottile); **(c)
|
||||||
|
vol-targeting a lambda costante lagga nello spike** → sei più levato *entrando* nel crash.
|
||||||
|
**LIVE 2-sleeve: non superare ~12%; mai 15-20%** (triplica il DD-in-euro senza margine di coda).
|
||||||
|
|
||||||
|
### 3. Aggressività differenziata-per-confidenza (TP01 più caldo di VRP) — REFUTATA
|
||||||
|
La mia proposta era intuitiva ma **sbagliata, e i numeri lo dicono netto**: differenziare la vol per
|
||||||
|
confidenza PEGGIORA la frontiera (5s Sharpe 2.24→2.11, 2s 1.78→1.69) perché **taglia la
|
||||||
|
diversificazione** di VRP/XS (corr uniforme-vs-differenziato 0.98-0.99). **Regola corretta: la bassa
|
||||||
|
fiducia sull'EDGE si esprime DE-MEANando lo sleeve in aspettativa (= la frontiera haircut), NON
|
||||||
|
riducendone l'esposizione/vol.** La confidenza vive nell'haircut della media, non nella leva. Tutti
|
||||||
|
gli sleeve alla stessa leva.
|
||||||
|
|
||||||
|
### 4. La diversificazione LIMA la coda sinistra in-sample (sorpresa onesta)
|
||||||
|
Per il 5-sleeve ben diversificato il block-bootstrap dà P(DD>30) SOTTO il Gaussiano iso-vol (30%:
|
||||||
|
10.6% reale vs 21.1% gauss) — la coda in-sample è benigna. Il segnale fat-tail vive in due posti:
|
||||||
|
l'**iniezione di crash sintetico** (il campione non contiene il crash peggiore di ogni sleeve) e
|
||||||
|
il **book live sottile** (2 gambe, drift haircut) dove il bootstrap SUPERA il Gaussiano. Cioè: la
|
||||||
|
frontiera è affidabile finché il futuro somiglia al passato campionato, e fragile esattamente dove
|
||||||
|
le storie sono corte (VRP mai stressato reale, XS 2.5y, nessun 2018/pre-2018).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Azione concreta (proposta, NON applicata)
|
||||||
|
|
||||||
|
1. **Nessun cambio di leva sul codice.** Il book gira già ≈ al punto fidato: LASCIARE `target_vol=20%`
|
||||||
|
di TP01 (canonico, la vol nativa del book emerge a ~8-11%). Non c'è una leva da girare su.
|
||||||
|
2. **⚠️ L'UNICO cambio che conta: `config/live.json` `max_notional_per_asset_usd` da $300 a
|
||||||
|
equity/2** ($1000@2k / $1750@3.5k / $2500@5k). Senza, il cap $300 tappa la vol raggiungibile a
|
||||||
|
13%@2k / 8%@3.5k / **6%@5k** → **throttling paradossale: più capitale = book più freddo**, forzato
|
||||||
|
sotto il punto fidato. Da applicare AL deposito. (config NON toccato ora.)
|
||||||
|
3. `min_order_usd=5`: LASCIARE (banda d'isteresi gratuita). Tranching K=2: NON cablare. Vol
|
||||||
|
differenziata: NON implementare (Q3).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Brutalità sui limiti
|
||||||
|
|
||||||
|
- **Storie corte = il difetto strutturale.** TP01/SKH dal 2019 (no 2018), VRP dal 2021, XS **2.5
|
||||||
|
anni** (no 2022). Il block-bootstrap non può campionare un crash mai visto; l'iniezione sintetica
|
||||||
|
è l'unico proxy, e quella dei finder era ~2× troppo mite.
|
||||||
|
- **VRP01 è MODELLATO** (f-di-stress mai catturato su un crash reale) → l'haircut −50% è stima, non
|
||||||
|
misura; resta STAT-MODE anche a 5k (regola: niente short-vol da modello in deploy).
|
||||||
|
- **HOLD ≪ FULL sul book live** (1.78 vs 1.16) → il lato-reward headline è in gran parte artefatto
|
||||||
|
full-sample; citare i €/g su base HOLD-onesta.
|
||||||
|
- **€50/g NON è risolto e non lo sarà da questo.** Il warm-up compra ~€0.2/g@2k. La via resta
|
||||||
|
**capitale + tempo**; la regola "niente leva alta come base" è rispettata (la raccomandazione è
|
||||||
|
"gira alla vol nativa", non "corri caldo").
|
||||||
|
|
||||||
|
**Numero unico da portare a casa:** book LIVE a **~10% target-vol (lambda ≈ 0.9, banda 8-11%)**,
|
||||||
|
uguale a 2k e 5k, con **cap → equity/2** come unica azione config; muro invalicabile a **~12%**;
|
||||||
|
guadagno reale vs il 6% risk-off ~**+€0.2/g@2k / +€0.5/g@5k** — reale ma modesto.
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# 2026-07-03 — Cap notional per-asset DINAMICO (= equity/2): operazionalizza la decisione frontier
|
||||||
|
|
||||||
|
**Contesto.** La frontiera del 2026-07-03 (`docs/diary/2026-07-03-book-frontier.md`) aveva identificato
|
||||||
|
**una sola** azione config con impatto reale: portare `max_notional_per_asset_usd` da $300 fisso a
|
||||||
|
**equity/2**, perché il cap fisso strozza il book man mano che si deposita (paradosso: più capitale =
|
||||||
|
book più freddo — a $5k il cap $300 fa girare il libro al ~49% del target). Il diario la lasciava
|
||||||
|
"da applicare AL deposito". Qui la **operazionalizzo in codice** rendendo il cap dinamico, così è
|
||||||
|
sempre corretta e non richiede un edit manuale al deposito.
|
||||||
|
|
||||||
|
## Cosa è cambiato
|
||||||
|
|
||||||
|
- **`src/live/book.py` — `_cap()`**: se in config c'è `max_notional_per_asset_frac` (opt-in), il cap
|
||||||
|
per-asset diventa **`equity × frac`** (0.5 → equity/2). Senza la chiave → comportamento vecchio
|
||||||
|
(cap fisso `max_notional_per_asset_usd`). Il call site in `book_report()` passa
|
||||||
|
`equity`, `real_equity`, `eq_fallback` a `_cap()`.
|
||||||
|
- **`config/live.json`**: aggiunto `max_notional_per_asset_frac: 0.5` (+ nota). `max_notional_per_asset_usd`
|
||||||
|
resta come **cap fisso di fallback**.
|
||||||
|
- **`tests/test_book_live.py`**: +4 test (scaling con equity fidata, fallback su eq_fallback/real=None/
|
||||||
|
equity 0, no-frac, integrazione online). Suite: **172 passed**.
|
||||||
|
|
||||||
|
## Il guardrail di sicurezza (il punto delicato)
|
||||||
|
|
||||||
|
Il cap dinamico si usa **SOLO con equity reale fidata** (letta dal conto). Se l'equity reale non è
|
||||||
|
leggibile — `eq_fallback` attivo, `real_equity=None`, offline, o equity ≤ 0 — si **torna al cap fisso
|
||||||
|
$300**. Motivo: in fallback `equity` è il paper-cap ($2000 ipotetico); calcolare `equity/2 = $1000` su
|
||||||
|
un conto di importo reale **ignoto** sizerebbe troppo. Preserva esattamente la protezione downside che
|
||||||
|
il test `test_book_execute_eq_fallback_warns_but_proceeds` verificava (l'hard-cap $/asset protegge
|
||||||
|
quando E è ignota). Regola: **equity/2 solo quando ci fidiamo di E; altrimenti $300 fisso.**
|
||||||
|
|
||||||
|
## Verifica
|
||||||
|
|
||||||
|
- **Live dry-run reale** (nessun `--execute`, nessun ordine): `cap/asset **$299**` = $598.06/2 →
|
||||||
|
**INERTE** oggi (≡ vecchio $300). Book flat, HOLD.
|
||||||
|
- Offline / `equity_override`: cap resta **$300 fisso** (nessuna equity reale da fidarsi) → il test
|
||||||
|
di parità pre-esistente regge invariato.
|
||||||
|
- A $5.000 di equity il cap diventerebbe automaticamente **$2.500** (equity/2), rimuovendo il
|
||||||
|
throttling che la frontiera aveva quantificato.
|
||||||
|
|
||||||
|
## Impatto e limiti
|
||||||
|
|
||||||
|
- **Già attivo**: il cron orario (`cron_book.sh`) esegue dal working tree → il prossimo run legge già
|
||||||
|
`frac=0.5`. Nessun deploy separato. **Inerte a $600**, si "accende" da sé quando l'equity cresce.
|
||||||
|
- **NON avvicina €50/g**: la frontiera lo aveva già detto — è una miglioria marginale (+€0.2/g @2k,
|
||||||
|
+€0.5/g @5k), il cui unico scopo è non lasciare capitale futuro strozzato. €50/g resta un problema
|
||||||
|
di **capitale + tempo** (~130k @ CAGR onesto), non di questo cap.
|
||||||
|
- Nessun cambio di **leva** (TP01 `target_vol=20%` invariato, la vol nativa ~10% emerge da sola —
|
||||||
|
coerente col verdetto frontier "gira alla vol nativa, non levare").
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
# 2026-07-03 — STATO COMPLETO DEL PROGETTO (snapshot)
|
||||||
|
|
||||||
|
Foto d'insieme dopo le ondate di ricerca del 1-3 luglio 2026 (timing/CRT, anchor-audit,
|
||||||
|
video-claims Elliott/Albimarini, capital-scaling, migliora-e-proteggi VRP01). Serve come punto di
|
||||||
|
riferimento unico: *dove siamo, cosa gira, cosa è provato, cosa resta aperto.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. In una frase
|
||||||
|
|
||||||
|
Progetto post-reset v2.0.0: su dati Deribit mainnet certificati (solo BTC/ETH) abbiamo costruito un
|
||||||
|
**portafoglio di 5 strategie diversificate** (research Sharpe ~2.2, DD ~6%), di cui **2 sono
|
||||||
|
eseguite live su Deribit** in un book nettato in software. Il conto reale è **~$598, flat** perché
|
||||||
|
il segnale trend è risk-off. Il valore delle strategie è **difensivo** (taglio del drawdown), non
|
||||||
|
generazione di ritorno: il target €50/giorno resta lontano (serve ~130k di capitale). L'onestà
|
||||||
|
metodologica è il vero asset accumulato: ~10 gate anti-illusione codificati, decine di edge
|
||||||
|
falsificati.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Il book LIVE (soldi reali)
|
||||||
|
|
||||||
|
- **Conto:** Deribit mainnet, USDC linear, **equity reale $598.06** (NON i €2000 nominali del paper).
|
||||||
|
- **Cosa gira:** `deribit_book_sleeves` = **TP01 (0.75) + SKH01 (0.25)** nettati in software su un
|
||||||
|
solo conto; UN ordine con segno per asset (BTC, ETH). `config/live.json`
|
||||||
|
**`execution_enabled=true`** → esecuzione ARMATA e reale.
|
||||||
|
- **Cadenza:** cron ORARIO `scripts/cron_book.sh` → `scripts/live/book_execute.py` (SKH01 decide su
|
||||||
|
griglia 230m; exit software, latenza fino a fine barra).
|
||||||
|
- **Guardrail:** cap **$300 notional/asset**, min order $5, **disaster-SL on-book −30%** sulla
|
||||||
|
posizione netta, alert Telegram su esecuzione/errori, gate fail-safe su posizione/equity non
|
||||||
|
leggibili (non opera a cieco).
|
||||||
|
- **Stato corrente (ultimo cron 2026-07-03 07:00Z):** BTC e ETH **flat, HOLD a target** — il target
|
||||||
|
TSMOM è risk-off (trend 0.0x) e SKH01 è flat (nessun breakout) → **zero ordini reali mai eseguiti**
|
||||||
|
da quando è armato (2026-06-23): il conto è flat *by design*, non per un errore.
|
||||||
|
- **NON nel book live:** XS01, VRP01, GTAA01 (research/paper/STAT-MODE).
|
||||||
|
|
||||||
|
## 3. Il portafoglio RESEARCH (5 sleeve) — `src/portfolio/sleeves.active_sleeves`
|
||||||
|
|
||||||
|
| Sleeve | Peso | Meccanismo | Dati | Stato |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| **TP01** | 33% | TSMOM difensivo trend BTC/ETH 1d, long-flat, vol-target 20% | 2019→ | **Deploy pieno** (nel book live) |
|
||||||
|
| **SKH01** | 20% | Dual-TF regime+breakout BTC/ETH, L/S, exit %-asimmetrici | 2019→ | Research → nel book live |
|
||||||
|
| **GTAA01** | 20% | Trend difensivo equity 6-ETF (IB), ~30 anni storia | 1996→ | Paper (paper_combo) |
|
||||||
|
| **VRP01** | 12% | Options short-vol (put credit spread + gate IV-rank) | 2021→ | LEAD modellato (STAT-MODE) |
|
||||||
|
| **XS01** | 15% | Cross-sectional momentum 19 alt Hyperliquid | 2024→ | STAT-MODE (serve ~20k) |
|
||||||
|
|
||||||
|
- **Combinato (research, ancore canoniche):** FULL Sharpe **2.24**, HOLD-OUT **2.46**, DD full **6.2%**.
|
||||||
|
- **⚠️ Stima DE-LUCKATA onesta** (dopo gli anchor-audit, vedi §5): **HOLD ~1.9-2.1, FULL ~2.0-2.2,
|
||||||
|
DD ~6%**. Il 2.46 è un massimo di configurazioni d'ancora, non la stima centrale.
|
||||||
|
- Sleeve a date d'inizio diverse → outer-join con pesi rinormalizzati (`combine_outer`).
|
||||||
|
- Report: `scripts/portfolio/run_portfolio.py`. Il book live (TP01+SKH01 75/25) è un sottoinsieme.
|
||||||
|
|
||||||
|
## 4. Paper trader / forward-monitor attivi (nessun soldo reale)
|
||||||
|
|
||||||
|
- **paper_trend** — TP01 1d, capitale nominale 2000, forward-only.
|
||||||
|
- **paper_combo** — book 5-sleeve completo (incl. GTAA/VRP/XS) dal 2026-06-22.
|
||||||
|
- **paper_statarb** — STATARB-RESID (ETH−β·BTC residual momentum, W=45/sgn=+1 congelati), doppio
|
||||||
|
libro MODELED/REAL-$600. LEAD eseguibile a $600 sotto soglia solo sull'edge (DSR 0.929).
|
||||||
|
- **paper_prevday** — FOLLOW prior-day levels (3 trade registrati); confermato indipendentemente da
|
||||||
|
più ondate (FOLLOW > FADE ogni anno 2019-26).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Finding strutturale del blocco 1-3 luglio: ANCHOR TIMING-LUCK (audit COMPLETO 4/4)
|
||||||
|
|
||||||
|
Le strategie a ribilanciamento/griglia ancorata hanno un'ancora "canonica" che è spesso il
|
||||||
|
**migliore** di molte configurazioni equivalenti (giorno/ora/offset) — un multiple-testing che il
|
||||||
|
deflated-Sharpe NON conta. Audit eseguito su tutti e 4 gli sleeve ancorati:
|
||||||
|
|
||||||
|
| Sleeve | Spazio d'ancora | Firma | Conseguenza |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **TP01** | 24 ore (barra daily) | canonica = migliore (P=0.86) | hold-out 0.31 non risolve l'edge di ritorno; regge solo il taglio DD |
|
||||||
|
| **XS01** | 10 fasi (ciclo 10g) | canonica al 15° pctl di DD | numeri headline conservativi sul DD, non sull'hold-out |
|
||||||
|
| **SKH01** | 23 offset (griglia 230/690m) | canonica = 93-98° pctl (peggiore firma) | gate DD<30% fallisce in 15/23 offset; ~+0.5 HOLD di fortuna sul book |
|
||||||
|
| **VRP01** | 7 giorni (ciclo settimanale) | **canonica = PEGGIORE** (7° pctl) | **numeri CONSERVATIVI**, non gonfiati — unico sleeve senza luck |
|
||||||
|
|
||||||
|
**Regola codificata:** i numeri hold-out di strategie ancorate si citano **con banda d'ancora**.
|
||||||
|
Candidato gate futuro: `anchor_luck_band()` in altlib. Diari: `2026-07-02-timing-crt-wave.md`,
|
||||||
|
`2026-07-02-anchor-audit-xs01-skh01.md`, `2026-07-03-vrp-improve-dd.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Cosa è PROVATO e MORTO (per non ri-testarlo)
|
||||||
|
|
||||||
|
- **Soffitto BTC/ETH-direzionale ~1.3** confermato da ogni ondata; superato solo dal cross-sectional
|
||||||
|
(XS01) e dalla diversificazione multi-sleeve.
|
||||||
|
- **Sweep 104 famiglie** (2026-06-20): 1 sola sopravvissuta, comunque non deployabile.
|
||||||
|
- **Opzioni long-vol** (gamma scalping): specchio del VRP, perde ogni anno/variante/frequenza.
|
||||||
|
- **Cash-and-carry (CC01):** premio reale ma Sharpe artefatto (manca il 2022, procyclico) → STAT-MODE.
|
||||||
|
- **Timing/clock alternativi** (event-clock, expiry calendar, clock lenti, regime-speed): 0 edge.
|
||||||
|
- **CRT "Candle Range Theory"** (3/3 FAIL): DSR 0.000, expectancy negativa anche a fee zero, il
|
||||||
|
ritest è informazione negativa; sui livelli prior-day FOLLOW > FADE.
|
||||||
|
- **Elliott (3/3 FAIL):** range-cycle = rumore; Fibonacci = effetto posizione non numeri magici;
|
||||||
|
canale = Donchian travestito.
|
||||||
|
- **Albimarini double-diagonal (2/2 FAIL):** non domina il condor, fee-negativa su Deribit, track
|
||||||
|
record consistente con zero skill (P=20-45%), rovina nei replay con code reali.
|
||||||
|
- **VRP01 miglioramenti (0/7 filoni):** struttura, overlay-DD, gate nuovi (4° fallimento), sizing,
|
||||||
|
gate SPX (confound di modello) — **VRP01 non migliorabile; la protezione DD si compra con la size.**
|
||||||
|
|
||||||
|
## 7. Regole/gate anti-illusione codificati (l'asset metodologico)
|
||||||
|
|
||||||
|
- **Certificazione dati** obbligatoria (solo Deribit mainnet, BTC/ETH; alt esclusi).
|
||||||
|
- **`marginal_vs_tp01` / `study_marginal`** (earns_slot) — Sharpe MARGINALE, non assoluto.
|
||||||
|
- **`study_family_honest` / `select_cell_insample` / `deflated_sharpe≥0.95`** — anti selection-on-holdout.
|
||||||
|
- **`day_boundary_robust`** — effetti calendario che si invertono spostando il confine = artefatto.
|
||||||
|
- **`eval_weights_smallcap`** — Sharpe haircut reale a capitale piccolo (sub-min-order = finzione).
|
||||||
|
- **`weights_tilt_null`** — ogni cambio pesi vs null dei tilt casuali cap-respecting.
|
||||||
|
- **Banda d'ancora** (nuova, luglio) — per ogni costrutto ancorato.
|
||||||
|
- **Candidati in coda:** `anchor_luck_band` (multiple-testing d'ancora); `implausible_sharpe`
|
||||||
|
(0-perdite / Sharpe implausibile — 3 occorrenze: CC01, ALB-A, griglia struttura VRP).
|
||||||
|
- **Lezioni pandas:** mai `DatetimeIndex.view("int64")` su tz-aware; `resample("7D", origin=)`
|
||||||
|
ignora origin (usare `"168h"`); un gate che si correla con un errore di modello è indistinguibile
|
||||||
|
da alpha finché non correggi il modello.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Onestà sui numeri e sul target
|
||||||
|
|
||||||
|
- **€50/giorno da €1.000-2.000 NON è raggiungibile** in 1-2 anni: serve ~130k di capitale o un DD da
|
||||||
|
rovina. La leva non è la scorciatoia (5x raddoppia il DD). La via è target-vol + capitale + tempo.
|
||||||
|
- Aspettativa onesta col CAGR de-luckato del book (10-15%): **~€0.6-0.8/giorno a 2k, ~€1.4-2/giorno
|
||||||
|
a 5k**. Il salto di capitale cambia *cosa è eseguibile*, non l'ordine di grandezza del ritorno.
|
||||||
|
- Il valore reale di TP01/del book è il **taglio del drawdown ~6× vs buy&hold**, non l'alpha.
|
||||||
|
|
||||||
|
## 9. Capitale in arrivo 2-5k — cosa cambia (audit r0702_capital_scaling)
|
||||||
|
|
||||||
|
- **Unico vincolo binding: il cap `max_notional_per_asset_usd=300`.** A 5k il book live girerebbe al
|
||||||
|
~49% del target. **Azione pendente sul deposito: alzare il cap a equity/2** ($1000/$1750/$2500 a
|
||||||
|
2k/3.5k/5k). `min_order_usd=5` da lasciare.
|
||||||
|
- Tranching K=2: matematicamente regge a 5k ma serve feed intraday fuori path certificato → non cablare.
|
||||||
|
- Opzioni ETH eseguibili da ~2.6k (spread), ma **la regola "niente short-vol da modello in deploy"
|
||||||
|
non decade col capitale**. XS01 resta fuori fino a ~20k. CC01 fuori per struttura.
|
||||||
|
- ⚠️ Disambiguazione unità: il "12%" come peso book (~0.014 Kelly) ≠ "12%" come margine/equity
|
||||||
|
(~0.27 Kelly) — **fattore 19x**.
|
||||||
|
|
||||||
|
## 10. Aperto / prossimi passi
|
||||||
|
|
||||||
|
1. **Al deposito 2-5k:** alzare il cap $/asset (proposta pronta, `config/live.json` non ancora toccato).
|
||||||
|
2. **Forward-monitor da maturare:** paper_statarb (edge sotto soglia DSR), paper_prevday (FOLLOW),
|
||||||
|
paper_combo (book completo). Se la finestra forward conferma STATARB-RESID → deployabile a 2 gambe.
|
||||||
|
3. **Gate da codificare:** `anchor_luck_band`, `implausible_sharpe`.
|
||||||
|
4. **Rivisite temporizzate:** term-structure DVOL (logger dal 2026-06-26, rivalutare a 6-12 mesi);
|
||||||
|
breadth/internals HL (1-2 anni di storia nativa); tranching a ≥5-10k; VRP stress-f su un crash reale.
|
||||||
|
5. **Ricerca strategie nuove:** il soffitto direzionale ~1.3 è saturo; i margini restano in meccanismi
|
||||||
|
ORTOGONALI (cross-sectional, relative-value, options gated) e nella diversificazione, non in nuovi
|
||||||
|
segnali direzionali BTC/ETH.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Stato tecnico
|
||||||
|
|
||||||
|
- **Versione:** 2.0.0 (semver in `VERSION`). **Test:** 168 verdi (28 file). **Diari:** 76.
|
||||||
|
- **Ultimi commit:** `26f8d27` (migliora+proteggi VRP01), `76120b5` (video-claims + capital-scaling),
|
||||||
|
`a74cc69` (anchor-audit XS01/SKH01), `e6657fc` (GTAA01 5° sleeve).
|
||||||
|
- **Working tree pulito, tutto pushato.** Book live sano e invariato da tutta la ricerca (research
|
||||||
|
isolata: nessun agente tocca src/config/live).
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
# 2026-07-03 — Ondata "migliora e proteggi VRP01 dai DD" (7 filoni + verifica a 2 lenti + scettico incrociato)
|
||||||
|
|
||||||
|
**Contesto.** Dopo la chiusura del dossier Albimarini (3 backtest indipendenti convergenti: premio
|
||||||
|
piccolo e reale, code non campionate, numeri del corso spiegabili al 100% dalla struttura del
|
||||||
|
payoff), goal esplicito: **migliorare la strategia short-vol** — la famiglia Albimarini → il nostro
|
||||||
|
**VRP01**, l'unica forma sopravvissuta agli audit — **e proteggerla dai drawdown**, con
|
||||||
|
orchestrazione a decine di agenti. Workflow deterministico: 7 filoni di ricerca in parallelo, 2
|
||||||
|
refuter avversariali (lente selection-bias + lente de-levering) su ogni miglioramento proposto,
|
||||||
|
scettico incrociato finale sui sopravvissuti. **26 agenti, 0 errori.**
|
||||||
|
|
||||||
|
**Scripts:** `scripts/research/r0703_vrpimp_{structgrid,ddoverlay,sizing,newgates,anchor,spxgate,stresslab}.py`.
|
||||||
|
Nessun file di produzione toccato, nessun commit dagli agenti. Motore riusato: `r0702_alb_structure.py`
|
||||||
|
(DVOL, riproduce VRP01 bit-exact: baseline bridge max|diff|=0.00e+00 su 266 settimane).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verdetto in una riga
|
||||||
|
|
||||||
|
**VRP01 non è migliorabile oltre lo status quo, e la protezione DD si compra SOLO con la size.**
|
||||||
|
Zero nuovi sleeve, zero overlay che reggono, zero gate nuovi, zero cambio di sizing. I due unici
|
||||||
|
"sopravvissuti" alla verifica **non propongono alcun cambiamento** (una conferma dello status quo +
|
||||||
|
una lente di misura). Il candidato più luccicante dell'ondata — un gate term-structure VIX su SPX
|
||||||
|
con ΔSharpe +0.90 e DSR 0.992 — è stato smontato dallo scettico come **confound di modello al
|
||||||
|
100%**. Deliverable reale = **audit anchor-luck di VRP01 CHIUSO** (ultimo sleeve ancorato): la fase
|
||||||
|
canonica è all'estremo **conservativo** della banda, quindi i numeri di ammissione reggono e da ora
|
||||||
|
si citano con banda.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## I 7 filoni
|
||||||
|
|
||||||
|
### 1. Griglia di struttura (288 celle) — NESSUNA batte VRP01
|
||||||
|
6 strutture (vertical put/call, condor, 3 diagonali) × distanza {1,1.5,2,2.5}σ × ali {0.5,1,2}σ ×
|
||||||
|
tenor {3,5,7,10}g, gate IV-rank canonico sempre attivo. La cella scelta col **solo in-sample**
|
||||||
|
(DIAG-PUT z=1.0 3g, ShIS 2.28) crolla in hold-out a **+0.19** vs 0.86 del bridge stesso-motore;
|
||||||
|
DSR 0.000/0.004 su 288 trial; multi-cut negativo a 3-4 cut su 4; banda d'ancora che non tiene
|
||||||
|
nemmeno il segno (ShH [−0.21,+0.64]). **Metà griglia (150/288) è il pattern "0-perdite = Sharpe
|
||||||
|
implausibile"** (celle z≥2 lunghe, 0 perdite su ~97 trade, ShH fasulli 4-6.5) — **terza occorrenza
|
||||||
|
dopo CC01 e ALB-A**. Quarta conferma indipendente: nel VRP l'alpha sta nel gate di regime + nella
|
||||||
|
zona ~δ-0.28 dove la coda è campionata, non nella geometria.
|
||||||
|
|
||||||
|
### 2. Overlay di protezione DD (14 celle) — 4/4 REFUTED (2 lenti ciascuno)
|
||||||
|
(a) exit intra-settimana su spike DVOL, (b) stop-loss MTM, (c) ala di coda comprata (broken-wing),
|
||||||
|
(d) cooldown post-perdita. **Nessuno batte il null del de-levering** a pari maxDD su tutta la banda
|
||||||
|
f; nessuno passa multi-cut (max 1/4) né DSR (0.70-0.88, tutti <0.95). Esempio decisivo: SL MTM 3x fa
|
||||||
|
DD 7.9%/worst −5.7%, ma VRP01 × 0.66 (semplice riduzione di size) fa lo **stesso DD a Sharpe
|
||||||
|
invariato e worst-week migliore (−4.9%)**. Meccanismo confermato: i trigger di spike scattano solo
|
||||||
|
2-3 volte in 5 anni **perché il gate IV-rank/crash-skip evita già gli ingressi vicino alle
|
||||||
|
esplosioni di vol** — la protezione crash vive nel gate d'ingresso, non serve un overlay a valle.
|
||||||
|
Coerente con TP01×DVOL (2026-06-26): "per meno DD la leva è la size, non un overlay".
|
||||||
|
|
||||||
|
### 3. Sizing anti-rovina — CONFERMA (12% ≈ quarter-Kelly), 1 candidato REFUTED
|
||||||
|
Il sizing non crea alpha (Sharpe del flusso invariante ~0.95 a ogni frazione). Frontiera onesta
|
||||||
|
CAGR-DD-P(rovina) mappata su 92 trade gated ETH 2021-26 (pooled banda-f + coda sintetica full-loss):
|
||||||
|
Kelly onesto q*=44% → il **12% del book = 0.27 Kelly, già in zona quarter-Kelly anti-rovina**
|
||||||
|
(P(rovina-50%|5y) 0.0-0.1%, P(DD>30%) 9-18%, CAGR med 13.5-17% a 2k/5k). Salire al 25% → P(DD>30%)
|
||||||
|
> 92%; Albimarini 1→4 → P(rovina) 53-55%. **Nessun cambio raccomandato.** Il candidato anti-streak
|
||||||
|
(N=2, size 12%→3% dopo 2 vittorie, l'opposto di Albimarini) era il più luccicante mai visto sullo
|
||||||
|
sleeve (FULL CAGR +19.4%/DD 6.3%/Sh 2.08, random-null 0.996, DSR 0.999) ma **auto-refutato come
|
||||||
|
artefatto di selezione**: plateau assente (solo N=2 vince, N=3/5 collassano), P(loss|streak) non
|
||||||
|
monotona, delever-null perde in 2/7 ancore — tutto l'effetto sono ~6 perdite su 15 trade a
|
||||||
|
streak==2 (binomtest p=0.033, CI contiene la base). Istruttivo: mostra come un DSR 0.999 possa
|
||||||
|
convivere con l'assenza di meccanismo.
|
||||||
|
|
||||||
|
### 4. Gate nuovi — 4° FALLIMENTO su 4 (barra alta)
|
||||||
|
(a) term-structure DVOL **non testabile oggi** (storia vol_term = 7 giorni, logger forward dal
|
||||||
|
2026-06-26 → rivalutare a 6-12 mesi); (b) vol-of-vol, (c) RV-acceleration, (d) combinazioni-AND. Il
|
||||||
|
candidato IS-best (vol-of-vol pctl>0.80) passa DSR 0.960 e il de-levering all'ancora di default ma
|
||||||
|
la **banda d'ancora lo smaschera** (mediana uplift FULL −0.07 su 7 ancore, uplift concentrato in UNA
|
||||||
|
finestra 2021-22 di 5 settimane, hold-out uplift 0.00). Bonus strutturale: i veti RV-acceleration
|
||||||
|
bloccano settimane **profittevoli** (+0.93/+1.25% vs +0.6% tenute) — dopo il gate IV-rank, la vol
|
||||||
|
che accelera è dove il premio venduto è più ricco. Conferma il verdetto 2026-07-01: l'alpha è tutto
|
||||||
|
nel binario IV-rank>0.30.
|
||||||
|
|
||||||
|
### 5. Audit anchor-luck VRP01 — CHIUSO: primo sleeve SENZA firma di luck
|
||||||
|
VRP01 è l'ultimo sleeve ancorato dopo TP01/XS01/SKH01. Ciclo settimanale → 7 ancore (giorno di
|
||||||
|
apertura). Replica bit-exact della sleeve (max|diff|=0.0). Le 7 fasi con parametri identici:
|
||||||
|
|
||||||
|
| metrica | canonica (fase-0) | mediana 7 fasi | banda | pctl canonica |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Sh FULL | 1.09 | 1.32 | [1.09, 1.83] | **7° (peggiore)** |
|
||||||
|
| Sh HOLD | 0.59 | 0.59 | [−0.03, 1.11] | 50° (mediana) |
|
||||||
|
| maxDD | 11.8% | 7.2% | [5.7, 11.8%] | **93° (peggiore)** |
|
||||||
|
|
||||||
|
**Firma OPPOSTA a TP01/XS01/SKH01**: lo spike bootstrap è NEGATIVO (g0 HOLD −0.06, FULL −0.21,
|
||||||
|
P(g0≤0) 0.54/0.74) → niente da de-luckare. La fase canonica ha beccato le settimane crash 2022
|
||||||
|
**peggio** di ogni altra (unico anno-fase negativo, −5.8%). **I numeri di ammissione FULL 1.10 /
|
||||||
|
HOLD 0.60 / DD 12% sono quindi CONSERVATIVI, non gonfiati** — il timore di "protezione fittizia" è
|
||||||
|
refutato: il DD 12% è il caso peggiore della banda, non quello fortunato. Impatto della fase sul
|
||||||
|
book 5-sleeve ≤ 0.04 Sh (banda ShFULL 2.23-2.27, ShHOLD 2.35-2.48, DD 6.2-6.3%). **Con questo
|
||||||
|
l'audit anchor-luck è completo su tutti gli sleeve ancorati (4/4).**
|
||||||
|
|
||||||
|
### 6. Gate di regime sulla diagonale SPX (l'esperimento mancante) — REFUTED, con confound istruttivo
|
||||||
|
L'esperimento che mancava ai 3 backtest Albimarini: il gate di regime sulla diagonale SPX (VIX +
|
||||||
|
VXV da FRED, motore BS sintetico con skew log-lineare). **Domanda secca: il gate IV-rank ribalta gli
|
||||||
|
anni di coda su SPX come su crypto? NO.** Il gate VIX-rank canonico *abbassa* lo Sharpe (1.36→1.19),
|
||||||
|
non ribalta nessun anno di coda, peggiora il 2018, muore sul de-levering (ΔSh −0.17). Meccanismo:
|
||||||
|
su SPX il VRP è già positivo l'**84% dei giorni** (vs ~58% delle settimane crypto) → il filtro di
|
||||||
|
rank non ha lavoro discriminante; su equity IV-rank alto = crisi, non vol ricca.
|
||||||
|
Il candidato **term-structure VIX/VXV<1 (contango)** sembrava reale — Sh 1.18→2.08, DD 3.5→1.3%,
|
||||||
|
ΔSh **+0.90** a pari DD, multicut 3/3, flippa il 2020, DSR 0.992, plateau su c∈[0.93,1.05], jackknife
|
||||||
|
robusto — **e passa la lente selection-bias pulito**. Ma la lente de-levering lo affonda:
|
||||||
|
**l'intero uplift è un confound di modello, quantificato al 100%.** La variabile del gate (VIX/VXV>1)
|
||||||
|
coincide 1:1 con la variabile dell'errore di pricing (tenor 6g prezzato a VIX-30g). Il refuter ha
|
||||||
|
riprezzato con σ(6g)=VIX·(VIX/VXV)^α term-structure-consistent (α=1.42 = estrapolazione log-lineare
|
||||||
|
zero-parametri della pendenza osservata 30g→93g) → l'edge sparisce. **Lezione: un gate che si
|
||||||
|
correla con un errore di modello è indistinguibile da alpha finché non correggi il modello** — vale
|
||||||
|
per ogni futuro test su strutture prezzate BS-flat.
|
||||||
|
|
||||||
|
### 7. Stress lab di coda — fisica dello strumento, nessun cambio
|
||||||
|
La protezione di coda del VRP01 viene per ~metà dall'**ala far-OTM** (nel replay COVID taglia ~90%
|
||||||
|
della perdita naked) e ~metà dal **gate canonico** (sulle 10 peggiori finestre 14g taglia la perdita
|
||||||
|
cumulata del 44-71% saltando i re-entry a IV-rank>0.90). L'ala T+1 "Albimarini" è assicurazione solo
|
||||||
|
contro i gap **medi** (−15/−20%, dove atterra ATM con un giorno di time value: +150bps) e **NON**
|
||||||
|
contro i gap profondi (k(−30%)=0.98 → il de-levering la batte), con valore interamente dipendente
|
||||||
|
dal vol-spike (banda f/mult da −324 a +263bps). Worst-case onesto a sizing 12%: piccolo in
|
||||||
|
convenzione book (−€12/sett a 2k, −€31 a 5k alla cella −30%) ma **è il margine intero** se il 12%
|
||||||
|
fosse deployato fisicamente come margine (−€164..−227).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scettico incrociato — verdetto e disambiguazione critica
|
||||||
|
|
||||||
|
I 2 sopravvissuti (sizing-conferma, anchor-ensemble) usano leve diverse (posizione sulla frontiera
|
||||||
|
vs varianza della stima), ma lo scettico ha trovato **tre sovrapposizioni che i refuter singoli non
|
||||||
|
potevano vedere**:
|
||||||
|
|
||||||
|
1. **Stessa dipendenza dal modello f** — entrambi flippano segno allo stesso punto (f=0.6-0.8 →
|
||||||
|
negativo). Due sopravvissuti = **un solo grado di libertà di rischio-modello** (il caveat "premio
|
||||||
|
MODELLATO su DVOL ATM" resta il collo di bottiglia dell'intera famiglia).
|
||||||
|
2. **⚠️ CONFLITTO DI UNITÀ sul "12%"** (il punto più pericoloso per la narrativa): il **12% di PESO
|
||||||
|
del book** (convenzione pnl/Ks di `sleeves.py`) = ~**0.6% margine/equity ≈ 0.014 Kelly**, mentre
|
||||||
|
il "FISSO 12%" del filone sizing = **12% margine/equity ≈ 0.27 Kelly** — **fattore 19x**. Entrambe
|
||||||
|
corrette internamente, ma non vanno confuse: worst-week −12.2% del conto (convenzione margine) vs
|
||||||
|
−0.64% (convenzione peso book).
|
||||||
|
3. **L'ensemble-7-fasi è ridondante con il tranching TP01** (matematica di diversificazione: corr fra
|
||||||
|
fasi ρ=0.355, boost teorico ×1.50 vs osservato ×1.53, residuo alpha ≈ 0) → **non promuovere a LEAD
|
||||||
|
autonomo**; la coda strutturale è invariata (worst = 7 tranche a full-loss simultaneo), il DD 4.7%
|
||||||
|
è campionario, non un cap.
|
||||||
|
|
||||||
|
**Effetto congiunto sul book 5-sleeve: ZERO** (entrambi propongono nessun cambio; controfattuale
|
||||||
|
|ΔSh| ≤ 0.04). Il book live Deribit (TP01+SKH01) non contiene VRP01 → impatto operativo nullo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cosa entra nella narrativa (una sola voce, status = misura research)
|
||||||
|
|
||||||
|
- **Audit anchor-luck VRP01 CHIUSO** → i numeri di ammissione reggono e si citano con banda
|
||||||
|
(ShFULL [1.09,1.83], ShHOLD [−0.03,1.11], DD [5.7,11.8%]); l'edge OOS resta f-dipendente.
|
||||||
|
Con questo l'audit anchor è completo su 4/4 sleeve ancorati.
|
||||||
|
- **Sizing (conferma, con disambiguazione unità obbligatoria):** 12% deploy ≈ 0.27 Kelly onesto,
|
||||||
|
anti-rovina; NON confondere col 12% di peso book (0.014 Kelly, 19x).
|
||||||
|
- **VRP01 canonico resta lo sleeve giusto, INVARIATO** — non migliorabile per struttura, gate,
|
||||||
|
overlay o sizing; la protezione DD si compra con la size.
|
||||||
|
|
||||||
|
## Regole/candidati rafforzati
|
||||||
|
|
||||||
|
- **`implausible_sharpe` gate in altlib**: 3ª occorrenza del pattern "0-perdite = Sharpe
|
||||||
|
implausibile" (griglia struttura, dopo CC01 e ALB-A) → priorità alzata.
|
||||||
|
- **Nuova regola metodologica**: un gate che si correla con un errore di modello (term-structure vs
|
||||||
|
BS-flat) è indistinguibile da alpha finché non si corregge il modello — riprezzare
|
||||||
|
term-structure-consistent prima di credere a qualsiasi gate vol su strutture BS-flat.
|
||||||
|
- **Conferma (4ª): l'alpha del VRP è il gate IV-rank binario**, non struttura/sizing/overlay/gate
|
||||||
|
aggiuntivi.
|
||||||
|
|
||||||
|
**Stato:** book live INVARIATO, nessun nuovo sleeve, nessun forward-monitor. Test suite verde. 7
|
||||||
|
script committati.
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
# 2026-07-07 — Video-claim "CRT top-down multi-TF, 74% win rate": SCARTATO (il 74% è un knob, non un edge)
|
||||||
|
|
||||||
|
**Richiesta utente:** analizzare e testare una strategia da video, metodo ICT/SMC top-down
|
||||||
|
multi-timeframe con claim **74% win rate**:
|
||||||
|
1. **H1** — setup CRT: candela di displacement forte, poi candela che prende liquidità (spike oltre
|
||||||
|
il range di C1) ma richiude dentro → falso breakout → fade/reversal.
|
||||||
|
2. **M15** — struttura: swing (U a 3 candele) + imbalance che diventa inverse-imbalance (FVG mitigato).
|
||||||
|
3. **M5** — conferma: displacement direzionale + zona di protezione (order block / imbalance).
|
||||||
|
4. **M1** — entry: attende la correzione, entra con SL dietro la zona di protezione.
|
||||||
|
Uscita: **TP1 a fine zona CRT (RR ~1.5–2), chiude 70–80% lì → resto a break-even → runner 20–30%**
|
||||||
|
verso la "liquidità successiva" (previous daily high/low). Due esempi mostrati (gold, nasdaq), entrambi
|
||||||
|
vincenti. Metà finale del video = vendita academy → lead magnet.
|
||||||
|
|
||||||
|
La valutazione critica dell'utente ("occhio Milito") aveva già isolato le red flag giuste (74% non
|
||||||
|
dimostrato, cherry-picking di 2 esempi, concetti discrezionali non falsificabili, RR basso + WR alto =
|
||||||
|
fragile, funnel commerciale). Questo diario **meccanizza e falsifica** la parte testabile.
|
||||||
|
|
||||||
|
## Inventario preventivo (non rifare lavoro)
|
||||||
|
|
||||||
|
Il setup H1 CRT è **già stato meccanizzato e scartato 3 volte** nell'onda 2026-07-02:
|
||||||
|
- `r0702_crt_base` (864 trial, 1h/4h/12h/1d): **DSR 0.000**; short "smart-money" negativo perfino IS.
|
||||||
|
- `r0702_crt_mtf` (4h→15m, 1h→5m, ~10k trade): **expectancy netta negativa OVUNQUE**; refutazione
|
||||||
|
strutturale = **"il ritest è informazione negativa"** (pattern CON ritest −40bps, SENZA +52bps).
|
||||||
|
- `r0702_crt_context` (FVG/sessioni/livelli): FVG semmai peggiora; **FADE < FOLLOW ogni anno 2019-26**.
|
||||||
|
|
||||||
|
**Angolo NUOVO non coperto** = la **gestione d'uscita (parziale+BE+runner)** e il **claim 74% WR**.
|
||||||
|
È lì che ho concentrato il test: `scripts/research/r0707_crt_topdown.py` (non tocca src/config/live;
|
||||||
|
nessun file scritto). Riusa l'harness certificato (dati/atr/sharpe/DSR di `altlib`).
|
||||||
|
|
||||||
|
## Disegno
|
||||||
|
|
||||||
|
- Detection CRT C1-C2 su **H1** (causale, nota alla chiusura di C2). Direzione = fade dello sweep.
|
||||||
|
- Entry sul TF basso (**5m/15m**, proxy di M5/M1 — ⚠️ **M1 non è nel feed certificato**): ritest della
|
||||||
|
zona violata + close-back attraverso il livello ("displacement di conferma"), **SL = estremo dello
|
||||||
|
swing basso = zona di protezione**.
|
||||||
|
- Tre schemi d'uscita sugli **stessi ingressi**: `fix1.5` (full, TP 1.5R), `fix2.0` (full, TP 2.0R),
|
||||||
|
`managed` (75% a 1.5R → resto 25% a BE, runner target = prev-daily high/low o estremo opposto C1).
|
||||||
|
- Gate progetto: hold-out 2025-01-01, griglia (k, s, d, tf, scheme) con **selezione cella solo
|
||||||
|
in-sample** + **DSR su tutti i trial**, **fee sweep** 0/0.10/0.20% RT, executability $600.
|
||||||
|
- Metrica invariante = **expectancy in R netto fee** (non il win-rate).
|
||||||
|
|
||||||
|
## Esito — il 74% è un artefatto della distanza del target, non un edge
|
||||||
|
|
||||||
|
**1) Win-rate reale (fade, tutti gli schemi, pooled BTC+ETH):**
|
||||||
|
|
||||||
|
| schema | WR mediana | expectancy R (netto 10bps) |
|
||||||
|
|---|---|---|
|
||||||
|
| fix1.5 | **35.8%** | −2.38 |
|
||||||
|
| fix2.0 | 31.3% | −2.34 |
|
||||||
|
| managed | **35.8%** | −2.17 |
|
||||||
|
|
||||||
|
Il WR a RR 1.5 è **~35%**, cioè **SOTTO** il null gambler's-ruin di un random walk senza edge
|
||||||
|
(P(+1.5R prima di −1R) = 1/(1+1.5) = **40%**). Il setup tocca 1.5R **meno spesso di una moneta** →
|
||||||
|
conferma indipendente che *il ritest è informazione negativa*. Lo schema `managed` **non gonfia** il
|
||||||
|
WR sopra `fix1.5` (per costruzione: "vinci" solo se raggiungi il parziale a 1.5R, che è la stessa
|
||||||
|
condizione di `fix1.5`). **In nessuna cella si avvicina al 74%.**
|
||||||
|
|
||||||
|
**2) Il win-rate È un knob — sweep del target parziale `rr1` (managed, 5m, cella tipica):**
|
||||||
|
|
||||||
|
| rr1 | WR reale | null 1/(1+rr1) | expR (fee 10bps) | expR (fee 0) |
|
||||||
|
|------|----------|----------------|------------------|--------------|
|
||||||
|
| 0.50 | 51.4% | 66.7% | **−2.54** | +0.11 |
|
||||||
|
| 0.75 | 46.9% | 57.1% | −2.51 | +0.14 |
|
||||||
|
| 1.00 | 43.2% | 50.0% | −2.47 | +0.17 |
|
||||||
|
| 1.50 | 36.3% | 40.0% | −2.45 | +0.19 |
|
||||||
|
| 2.00 | 31.8% | 33.3% | −2.42 | +0.23 |
|
||||||
|
|
||||||
|
Avvicinando il target il WR sale (esattamente come previsto dalla formula gambler's-ruin): **un "74%"
|
||||||
|
si fabbrica scegliendo un parziale vicino** (~0.35R), NON perché il metodo indovina la direzione. Ma
|
||||||
|
l'**expectancy R è costante e negativa a ogni rr1**: il WR alto non porta soldi. NB anche a WR massimo
|
||||||
|
il setup resta *sotto* il null teorico ad ogni rr1 → è pure peggio di un random walk nel raggiungere
|
||||||
|
target vicini.
|
||||||
|
|
||||||
|
**3) Gate statistici (cella scelta in-sample = 15m, k1.5, s0.1, d0.15, fix2.0):**
|
||||||
|
- **DSR = 0.000** su 48 trial (null max-Sharpe atteso 4.35). FAIL.
|
||||||
|
- **Fee sweep:** a **0 bps** expR = −0.10 e Sharpe book −0.63 → **non è morte-per-fee: l'edge lordo
|
||||||
|
non esiste** (il residuo lordo marginalmente positivo su 5m è la beta di trend dei time-exit, la
|
||||||
|
stessa autopsia del CRT base). A 10bps expR −1.26; a 20bps −2.43. Stop stretti (0.13–0.24%) →
|
||||||
|
la fee 0.10% pesa 0.75–2.6 R per trade → qualunque micro-edge lordo è polverizzato.
|
||||||
|
- **Hold-out 2025-26:** WR 32%, expR −1.56 (peggio dell'IS). Nessuna reggenza fuori campione.
|
||||||
|
|
||||||
|
**4) Executability $600:** SL sul TF basso = 0.13% (5m) / 0.24% (15m). Lo schema parziale+BE+runner
|
||||||
|
richiede **3–4 ordini per trade**, alcuni **sotto il min-order $5** a nozionale $1200 → non eseguibile
|
||||||
|
pulito al capitale reale.
|
||||||
|
|
||||||
|
## Verdetto
|
||||||
|
|
||||||
|
**SCARTATO.** Il claim "74% win rate" è **coerente con ZERO edge**: è la scelta di un parziale vicino
|
||||||
|
(gambler's ruin), non capacità direzionale. Smontato lo schema d'uscita, resta il setup H1 CRT che
|
||||||
|
questo progetto ha già triplo-refutato (DSR 0.000, ritest = informazione negativa, fade < follow).
|
||||||
|
Sul feed certificato BTC/ETH il metodo ha **win-rate reale ~30–37% e expectancy R negativa a ogni
|
||||||
|
schema, fee, finestra e asset**; lordo di fee non c'è edge; a $600 non è nemmeno eseguibile pulito.
|
||||||
|
|
||||||
|
La parte *utile* del video è il framework di lettura (CRT + imbalance + parziali), non il numero — come
|
||||||
|
aveva già concluso la valutazione dell'utente. Da trattare come lettura del mercato, non come sistema.
|
||||||
|
|
||||||
|
## Lezione codificabile (candidata)
|
||||||
|
|
||||||
|
**Il win-rate di uno schema parziale+BE non è un parametro di merito: è ~P(tocca il primo target prima
|
||||||
|
dello stop) = 1/(1+rr1) per un processo senza drift.** Ogni claim "WR X%" da schema SMC va convertito
|
||||||
|
in **expectancy R netto fee** prima di crederci; il WR alto si compra con un target vicino a costo di
|
||||||
|
un'expectancy identica (o peggiore). Prossimo giro di gate: un helper `winrate_is_a_knob()` che, dato
|
||||||
|
un book a schema parziale, riporta expectancy-R vs WR al variare del target (rende esplicito l'artefatto).
|
||||||
|
|
||||||
|
## File
|
||||||
|
|
||||||
|
- `scripts/research/r0707_crt_topdown.py` — il test (griglia + 74%-check + knob-sweep + DSR + fee + exec).
|
||||||
|
- Prior: `r0702_crt_{base,mtf,context}.py`, diario `2026-07-02-timing-crt-wave.md`.
|
||||||
|
- Nessuna modifica a src/, config/, scripts/live/, tests/. Book live e pesi INVARIATI.
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
# 2026-07-09 — Incident infra: "BOOK LIVE conto offline" = cert Traefik di default (non un problema del conto)
|
||||||
|
|
||||||
|
**Tipo:** incident operativo (infra VPS), non ricerca. Nessun edge, nessun cambio a strategie/pesi/config
|
||||||
|
di trading. Documentato qui perché ha **bloccato la lettura del conto del book live** e ha prodotto
|
||||||
|
l'alert Telegram `⚠️ BOOK LIVE — conto offline`.
|
||||||
|
|
||||||
|
## Sintomo
|
||||||
|
|
||||||
|
Alert dal `cron_book` orario (`scripts/live/book_execute.py --execute`):
|
||||||
|
|
||||||
|
```
|
||||||
|
⚠️ BOOK LIVE — conto offline
|
||||||
|
nota: salto l'esecuzione, non opero a cieco
|
||||||
|
```
|
||||||
|
|
||||||
|
Nel log (`logs/cron_book.log`) le run recenti mostravano `conto non leggibile (offline) -> stop, non
|
||||||
|
eseguo a cieco`. 10 run offline su 382 totali → **regressione recente**, non uno stato storico.
|
||||||
|
|
||||||
|
## Diagnosi (catena completa)
|
||||||
|
|
||||||
|
Il book legge il conto Deribit mainnet in sola lettura via **Cerbero MCP** (`DeribitRead`,
|
||||||
|
`src/live/deribit.py` → `https://cerbero-mcp.tielogic.xyz`). `book_report` marca `online=False` quando
|
||||||
|
`DeribitRead.mark_price(BTC)` lancia. Riproduzione diretta:
|
||||||
|
|
||||||
|
```
|
||||||
|
mark_price BTC: FAIL -> SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] self-signed certificate
|
||||||
|
```
|
||||||
|
|
||||||
|
Il proxy presentava il **cert self-signed di default di Traefik** (`CN = TRAEFIK DEFAULT CERT`,
|
||||||
|
`notBefore Jul 9 19:55 2026`) al posto del vero Let's Encrypt. `curl -k` → HTTP 401 (backend
|
||||||
|
`cerbero-mcp` healthy, routing OK): **solo la presentazione del cert TLS era rotta**. `requests`
|
||||||
|
verifica il cert → fallisce → `online=False` → il book salta l'esecuzione (comportamento di sicurezza
|
||||||
|
**corretto**: non opera a cieco).
|
||||||
|
|
||||||
|
**Causa a monte** (log di avvio Traefik, 11:37:16Z, container `traefik-traefik-1`):
|
||||||
|
|
||||||
|
```
|
||||||
|
ERR The ACME resolve is skipped from the resolvers list
|
||||||
|
error="unable to get ACME account: permissions 660 for /acme.json are too open, please use 600"
|
||||||
|
resolver=mytlschallenge
|
||||||
|
→ (a cascata) Router uses a nonexistent certificate resolver certificateResolver=mytlschallenge
|
||||||
|
routerName=cerbero-mcp@docker [+ ~14 altri router: git, vault, portfolio, budget, cerbero-bite, ...]
|
||||||
|
```
|
||||||
|
|
||||||
|
Sequenza:
|
||||||
|
1. Immagine `traefik:latest` + **Watchtower** (`com.centurylinklabs.watchtower.enable=true`) →
|
||||||
|
auto-upgrade a **Traefik 3.7.7** con restart alle 11:37 di oggi.
|
||||||
|
2. Traefik 3.x **rifiuta** `acme.json` con permessi più larghi di `600`; il file era `660`
|
||||||
|
(`-rw-rw---- root:adriano`) → **resolver `mytlschallenge` scartato**.
|
||||||
|
3. Tutti i ~15 router che lo referenziano → "nonexistent certificate resolver" → Traefik serve il
|
||||||
|
**cert di default self-signed** a ogni host HTTPS del VPS (non solo cerbero-mcp).
|
||||||
|
4. I cert veri erano **già dentro `acme.json`** (14 domini, incl. `cerbero-mcp.tielogic.xyz`) → nessuna
|
||||||
|
ri-emissione necessaria, **zero rischio rate-limit** Let's Encrypt.
|
||||||
|
|
||||||
|
## Impatto
|
||||||
|
|
||||||
|
- **VPS-wide:** ogni servizio HTTPS dietro Traefik serviva un cert invalido (warning browser, fallimento
|
||||||
|
di ogni client che verifica il TLS). Non solo il trading.
|
||||||
|
- **Trading:** **nessun trade perso.** Target del book **flat** (TP01/SKH01 risk-off) + conto reale
|
||||||
|
~$598 (non finanziato al livello nominale): anche online il book avrebbe fatto HOLD. Il doppio gate
|
||||||
|
di sicurezza ha lavorato come previsto.
|
||||||
|
|
||||||
|
## Fix
|
||||||
|
|
||||||
|
**Immediato** (eseguito dall'utente):
|
||||||
|
```bash
|
||||||
|
sudo chmod 600 /opt/docker/traefik/acme.json
|
||||||
|
docker restart traefik-traefik-1
|
||||||
|
```
|
||||||
|
Il resolver ricarica i cert già presenti in `acme.json` e li ripresenta subito.
|
||||||
|
|
||||||
|
**Durevole** (pin dell'immagine, `/opt/docker/traefik/docker-compose.yml`):
|
||||||
|
```yaml
|
||||||
|
- image: "traefik" # = traefik:latest → Watchtower salta ai major (causa dell'incident)
|
||||||
|
+ image: "traefik:3.7" # Watchtower resta su 3.7.x: patch sì, salto a 3.8+ breaking no
|
||||||
|
```
|
||||||
|
Applicato con `docker compose up -d` (container ricreato su `traefik:3.7`, 20:05:05Z).
|
||||||
|
|
||||||
|
## Verifica (end-to-end, post-fix)
|
||||||
|
|
||||||
|
| Check | Risultato |
|
||||||
|
|---|---|
|
||||||
|
| `acme.json` perms | `600` |
|
||||||
|
| Cert `cerbero-mcp.tielogic.xyz` | issuer **Let's Encrypt** (fino 2026-09-27) |
|
||||||
|
| Traefik log | nessun `ACME resolve is skipped` / `too open` |
|
||||||
|
| Container image | `traefik:3.7` (running) |
|
||||||
|
| `DeribitRead.mark_price` | BTC 63.193 / ETH 1.746 |
|
||||||
|
| `book_report.online` | **True** · `eq_basis=mainnet USDC` · `real_equity=$598.06` · `pos_error=None` |
|
||||||
|
|
||||||
|
## Lezioni
|
||||||
|
|
||||||
|
- **`traefik:latest` + Watchtower = auto-upgrade non deterministico.** Un bump di major (qui il giro
|
||||||
|
di vite sui permessi di `acme.json`) diventa un'interruzione TLS di tutto il VPS senza intervento
|
||||||
|
umano. Pinnare la minor (`3.7`) rende i restart prevedibili pur ricevendo le patch.
|
||||||
|
- **Traefik 3.x pretende `acme.json` a `600`** (non solo warning: **scarta il resolver**). Se il file
|
||||||
|
è group/other-readable, tutti i router cadono sul cert di default.
|
||||||
|
- **Il gate `online` del book ha fatto il suo lavoro:** un problema *infra di lettura* non ha prodotto
|
||||||
|
ordini a cieco. L'alert "conto offline" va letto prima come possibile problema di **connettività/cert
|
||||||
|
al Cerbero MCP**, non del conto Deribit.
|
||||||
|
- **Runbook rapido** per "BOOK LIVE conto offline":
|
||||||
|
1. `python -c "from src.live.deribit import DeribitRead; DeribitRead().mark_price('BTC-PERPETUAL')"`
|
||||||
|
→ se `SSLError`/`CERTIFICATE_VERIFY_FAILED` è il cert del proxy, non il conto.
|
||||||
|
2. `openssl s_client -connect cerbero-mcp.tielogic.xyz:443 -servername cerbero-mcp.tielogic.xyz | openssl x509 -noout -issuer`
|
||||||
|
→ se `TRAEFIK DEFAULT CERT`, Traefik non serve il cert reale.
|
||||||
|
3. `docker logs traefik-traefik-1 | grep -iE "ACME resolve is skipped|too open|nonexistent"`
|
||||||
|
→ conferma resolver scartato; controlla `stat -c %a /opt/docker/traefik/acme.json` (deve essere `600`).
|
||||||
|
|
||||||
|
Nessun file di trading toccato; `config/live.json`, sleeve e pesi **invariati**.
|
||||||
Executable
+14
@@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# BOOK DERIBIT-ONLY (TP01+SKH01 nettati) — esecuzione LIVE a cadenza ORARIA. v2.0.0+.
|
||||||
|
# SKH01 decide su griglia 230m -> serve girare piu' spesso del daily; orario IDEMPOTENTE:
|
||||||
|
# riconcilia al target NETTO corrente (se non cambia nulla -> HOLD). Il feed 5m fresco per il
|
||||||
|
# segnale SKH e' preso IN MEMORIA dentro book_execute (livefeed.fresh_5m): NON tocca i dati
|
||||||
|
# certificati su disco. Esecuzione reale gated da config/live.json (execution_enabled) + --execute.
|
||||||
|
export PATH="/home/adriano/.local/bin:$PATH"
|
||||||
|
cd /opt/docker/PythagorasGoal || exit 1
|
||||||
|
mkdir -p logs
|
||||||
|
{
|
||||||
|
echo "===== $(date -u '+%Y-%m-%dT%H:%M:%SZ') cron_book ====="
|
||||||
|
uv run python scripts/live/book_execute.py --execute
|
||||||
|
echo "===== done $(date -u '+%H:%M:%SZ') ====="
|
||||||
|
} >> logs/cron_book.log 2>&1
|
||||||
+10
-1
@@ -9,6 +9,15 @@ mkdir -p logs
|
|||||||
uv run python scripts/analysis/fetch_hyperliquid.py # 52 alt Hyperliquid (certify)
|
uv run python scripts/analysis/fetch_hyperliquid.py # 52 alt Hyperliquid (certify)
|
||||||
uv run python scripts/research/fetch_dvol.py # DVOL (per ricerca opzioni)
|
uv run python scripts/research/fetch_dvol.py # DVOL (per ricerca opzioni)
|
||||||
uv run python scripts/live/paper_portfolio.py # avanza paper TP01+XS01
|
uv run python scripts/live/paper_portfolio.py # avanza paper TP01+XS01
|
||||||
uv run python scripts/live/live_execute.py --execute # TP01 LIVE su Deribit (gated da config/live.json)
|
uv run python scripts/live/paper_prevday.py # forward-monitor lead prevday-breakout (PAPER, non deploy)
|
||||||
|
uv run python scripts/live/paper_statarb.py # forward-monitor lead STATARB-RESID ETH/BTC ortogonale (PAPER, non deploy)
|
||||||
|
# NB: l'esecuzione Deribit e' passata al BOOK (TP01+SKH01 nettati) via scripts/cron_book.sh a
|
||||||
|
# cadenza ORARIA (SKH01 e' a 230m: il daily mancherebbe gli ingressi). live_execute.py
|
||||||
|
# (TP01-only) NON va piu' eseguito qui, sennò i due farebbero a pugni sullo stesso strumento.
|
||||||
|
# --- COMBO cross-venue (PAPER): refresh ETF IB (GTAA) + avanza paper TP01+GTAA ---
|
||||||
|
docker compose up -d ib-gateway >/dev/null 2>&1 # gateway IB paper (idempotente)
|
||||||
|
for i in $(seq 1 25); do (echo > /dev/tcp/127.0.0.1/4002) >/dev/null 2>&1 && break; sleep 6; done
|
||||||
|
uv run --with ib_async python scripts/research/fetch_ib_equities.py --only SPY,QQQ,IWM,TLT,GLD,HYG # ETF GTAA freschi
|
||||||
|
uv run python scripts/live/paper_combo.py # avanza paper combo (forward-only)
|
||||||
echo "===== done $(date -u '+%H:%M:%SZ') ====="
|
echo "===== done $(date -u '+%H:%M:%SZ') ====="
|
||||||
} >> logs/cron_daily.log 2>&1
|
} >> logs/cron_daily.log 2>&1
|
||||||
|
|||||||
Executable
+14
@@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# LOGGER FORWARD della vol term-structure Deribit — cadenza GIORNALIERA. v2.0.0+.
|
||||||
|
# Costruisce il dataset per un futuro calendar-vol (oggi NON backtestabile: storia per-scadenza non
|
||||||
|
# pubblica — vedi docs/diary/2026-06-26-vol-termstructure-feasibility.md). Append idempotente per
|
||||||
|
# giorno su data/raw/vol_term_<asset>.parquet. SOLO ricerca forward: NON tocca il book live ne' i
|
||||||
|
# dati certificati BTC/ETH; legge l'API pubblica Deribit (tokenless) e scrive un parquet dedicato.
|
||||||
|
export PATH="/home/adriano/.local/bin:$PATH"
|
||||||
|
cd /opt/docker/PythagorasGoal || exit 1
|
||||||
|
mkdir -p logs
|
||||||
|
{
|
||||||
|
echo "===== $(date -u '+%Y-%m-%dT%H:%M:%SZ') cron_vol_term ====="
|
||||||
|
uv run python scripts/research/log_vol_termstructure.py
|
||||||
|
echo "===== done $(date -u '+%H:%M:%SZ') ====="
|
||||||
|
} >> logs/cron_vol_term.log 2>&1
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
"""BOOK DERIBIT-ONLY LIVE EXECUTE — TP01 + SKH01 NETTATI in software su un solo conto Deribit mainnet.
|
||||||
|
|
||||||
|
Porta il conto reale al target NETTO per asset (vedi src/live/book.py): per ogni asset combina la
|
||||||
|
frazione long-flat di TP01 (peso 0.75) e il segno L/S di SKH01 (peso 0.25), e manda UN ordine con
|
||||||
|
segno (long/short/flip) per raggiungerlo. Poi assicura un disaster-SL on-book sulla posizione NETTA.
|
||||||
|
|
||||||
|
DOPPIO GATE DI SICUREZZA (entrambi necessari per inviare ordini reali):
|
||||||
|
1. config/live.json -> "execution_enabled": true (master switch, default false)
|
||||||
|
2. flag CLI --execute
|
||||||
|
Senza entrambi e' un DRY-RUN (stampa il piano, NON invia). Reconciliation dopo ogni ordine; log in
|
||||||
|
data/live/book_executions.jsonl.
|
||||||
|
|
||||||
|
⚠️ CADENZA: SKH01 decide su griglia 230m -> questo script va lanciato ogni ~230 minuti con la feed
|
||||||
|
fresca all'ultima barra chiusa (NON il cron giornaliero, che mancherebbe gli ingressi). Gli exit di
|
||||||
|
SKH sono SOFTWARE (latenza fino a fine barra 230m); solo il disaster-SL (-30%) e' on-book.
|
||||||
|
|
||||||
|
uv run python scripts/live/book_execute.py # DRY-RUN (piano, nessun ordine)
|
||||||
|
uv run python scripts/live/book_execute.py --execute # esegue SOLO se execution_enabled=true
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
|
||||||
|
from src.live.book import book_report
|
||||||
|
from src.live.execution import DeribitTrader
|
||||||
|
from src.live.notifier import notify
|
||||||
|
|
||||||
|
CONFIG = PROJECT_ROOT / "config" / "live.json"
|
||||||
|
LOG_DIR = PROJECT_ROOT / "data" / "live"
|
||||||
|
LOG = LOG_DIR / "book_executions.jsonl"
|
||||||
|
|
||||||
|
|
||||||
|
def load_config() -> dict:
|
||||||
|
cfg = json.loads(CONFIG.read_text()) if CONFIG.exists() else {}
|
||||||
|
cfg.setdefault("execution_enabled", False)
|
||||||
|
cfg.setdefault("max_notional_per_asset_usd", 300.0)
|
||||||
|
cfg.setdefault("min_order_usd", 5.0)
|
||||||
|
cfg.setdefault("disaster_sl_pct", 0.30)
|
||||||
|
return cfg
|
||||||
|
|
||||||
|
|
||||||
|
def log_event(rec: dict):
|
||||||
|
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(LOG, "a") as f:
|
||||||
|
f.write(json.dumps(rec) + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
def _run():
|
||||||
|
cfg = load_config()
|
||||||
|
want_execute = "--execute" in sys.argv[1:]
|
||||||
|
enabled = bool(cfg["execution_enabled"])
|
||||||
|
do_execute = want_execute and enabled
|
||||||
|
min_order = float(cfg["min_order_usd"])
|
||||||
|
sl_pct = float(cfg["disaster_sl_pct"])
|
||||||
|
|
||||||
|
r = book_report(live_feed=True) # target NETTO + conto/posizioni reali (feed SKH fresco)
|
||||||
|
equity = r["equity"]
|
||||||
|
|
||||||
|
print("=" * 88)
|
||||||
|
print(" BOOK DERIBIT LIVE EXECUTE — TP01(0.75)+SKH01(0.25) NETTATI — Deribit mainnet (USDC linear)")
|
||||||
|
print("=" * 88)
|
||||||
|
mode = ("ESECUZIONE REALE" if do_execute else
|
||||||
|
("ARMATO ma manca --execute" if enabled else "DRY-RUN (execution_enabled=false)"))
|
||||||
|
print(f" modo : {mode}")
|
||||||
|
print(f" gate : execution_enabled={enabled} | --execute={want_execute}")
|
||||||
|
print(f" conto reale : ${r['real_equity']:,.2f}" if r["real_equity"] else f" conto: {r['eq_basis']}")
|
||||||
|
print(f" sizing base : ${equity:,.2f} | cap/asset ${r['cap_per_asset']:.0f} | min ${min_order:.0f} | disaster-SL -{sl_pct*100:.0f}%")
|
||||||
|
print(f" ultima barra : {r['last_data']}\n")
|
||||||
|
|
||||||
|
if r.get("skh_error"): # SKH feed fallito -> book.py ha forzato flat IN SILENZIO
|
||||||
|
print(f" ⚠️ SKH FEED ERRORE (SKH forzato flat!): {r['skh_error']}")
|
||||||
|
notify("⚠️ BOOK — SKH feed fallito (sleeve forzato flat)", {"error": r["skh_error"]})
|
||||||
|
|
||||||
|
if not r["online"]:
|
||||||
|
print(" conto non leggibile (offline) -> stop, non eseguo a cieco.")
|
||||||
|
if do_execute:
|
||||||
|
notify("⚠️ BOOK LIVE — conto offline", {"nota": "salto l'esecuzione, non opero a cieco"})
|
||||||
|
return
|
||||||
|
|
||||||
|
if r.get("pos_error"): # ONLINE ma posizione IGNOTA (read fallita -> assunta flat)
|
||||||
|
print(f" 🛑 POSIZIONE NON LEGGIBILE -> NON eseguo a cieco: {r['pos_error']}")
|
||||||
|
if do_execute:
|
||||||
|
notify("🛑 BOOK LIVE — posizione non leggibile", {"error": r["pos_error"],
|
||||||
|
"nota": "salto l'esecuzione, non opero a cieco"})
|
||||||
|
return
|
||||||
|
|
||||||
|
if r.get("eq_fallback"): # equity reale non leggibile -> sizing su paper_cap
|
||||||
|
print(f" ⚠️ EQUITY FALLBACK (sizing su paper_cap, NON blocco): {r['eq_fallback']}")
|
||||||
|
if do_execute: # solo diagnostica: l'hard-cap $/asset limita il downside
|
||||||
|
notify("⚠️ BOOK LIVE — equity fallback (sizing su paper_cap)", {"nota": r["eq_fallback"]})
|
||||||
|
|
||||||
|
trader = DeribitTrader() if do_execute else None
|
||||||
|
actions = []
|
||||||
|
for a in r["assets"]:
|
||||||
|
asset, inst = a["asset"], a["instrument"]
|
||||||
|
net, cur, mark = a["net_target"], a["position_usd"], a["mark"]
|
||||||
|
sk = a["skh_state"]
|
||||||
|
sk_txt = "flat" if sk == "flat" else f"{sk['dir']}@{sk.get('entry')}"
|
||||||
|
order = a["order"]
|
||||||
|
if order is None:
|
||||||
|
act = "HOLD (a target)"
|
||||||
|
elif order.get("is_close"):
|
||||||
|
act = f"CLOSE ${cur:,.0f}"
|
||||||
|
elif order.get("needs_flip"):
|
||||||
|
act = f"FLIP -> ${net:,.0f}"
|
||||||
|
else:
|
||||||
|
act = f"{order['side'].upper()} ${order['delta']:+,.0f}"
|
||||||
|
print(f" {asset:<3} TP {a['tp_frac']:+.3f} · SKH {a['skh_sign']:+d}({sk_txt}) -> net ${net:+,.0f} "
|
||||||
|
f"| pos ${cur:+,.0f} -> {act}")
|
||||||
|
|
||||||
|
if do_execute and order is not None:
|
||||||
|
fills = trader.rebalance_signed(inst, net, mark, min_usd=min_order)
|
||||||
|
newpos = trader.position_usd(inst)
|
||||||
|
for f in fills:
|
||||||
|
print(f" -> {f.side.upper()} {f.filled:.4f} @ ${f.price or 0:,.1f} fee {f.fee_usdc:.5f} "
|
||||||
|
f"({'OK' if f.verified else 'NON VERIFICATO: ' + f.notes})")
|
||||||
|
log_event(dict(ts_utc=str(pd.Timestamp(r['last_data'])), asset=asset, action=act,
|
||||||
|
side=f.side, filled=f.filled, price=f.price, fee=f.fee_usdc,
|
||||||
|
verified=f.verified, notes=f.notes, net_target=net, pos_after=newpos,
|
||||||
|
tp_frac=a["tp_frac"], skh_sign=a["skh_sign"]))
|
||||||
|
det = dict(asset=asset, side=f.side, amount=round(f.filled, 4), price=round(f.price or 0, 1),
|
||||||
|
fee=round(f.fee_usdc, 5), net=round(net, 0), pos_after=round(newpos, 0))
|
||||||
|
notify(f"✅ BOOK {act}" if f.verified else "⚠️ BOOK ORDINE NON VERIFICATO",
|
||||||
|
det if f.verified else {**det, "notes": f.notes})
|
||||||
|
print(f" reconcile: pos ${newpos:,.0f}")
|
||||||
|
if do_execute:
|
||||||
|
ds = trader.ensure_disaster_sl(inst, sl_pct) # bracket su posizione NETTA (adatta long/short)
|
||||||
|
print(f" disaster-SL: {ds.get('state')}" + (f" @ ${ds['stop']:,.1f}" if ds.get("stop") else ""))
|
||||||
|
if ds.get("state") == "placed":
|
||||||
|
notify("🛡️ BOOK disaster-SL piazzato", {"asset": asset, "stop": round(ds.get("stop") or 0, 1),
|
||||||
|
"amount": round(ds.get("amount") or 0, 4)})
|
||||||
|
elif ds.get("state") == "place-failed":
|
||||||
|
notify("⚠️ BOOK disaster-SL FALLITO", {"asset": asset, "notes": ds.get("notes")})
|
||||||
|
actions.append(act)
|
||||||
|
|
||||||
|
print()
|
||||||
|
if not do_execute:
|
||||||
|
print(" => DRY-RUN: nessun ordine inviato." +
|
||||||
|
("" if enabled else " Per armare: config/live.json execution_enabled=true + --execute."))
|
||||||
|
elif all(x.startswith("HOLD") for x in actions):
|
||||||
|
print(" => Nessuna azione: conto gia' al target netto del book.")
|
||||||
|
else:
|
||||||
|
print(" => Esecuzione completata (vedi data/live/book_executions.jsonl).")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
try:
|
||||||
|
_run()
|
||||||
|
except Exception as e:
|
||||||
|
notify("🛑 BOOK LIVE — ERRORE", {"error": f"{type(e).__name__}: {e}"})
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
"""PAPER COMBO — forward-only del combo cross-venue TP01 (Deribit) + GTAA (IB), NUDO vs PROTETTO.
|
||||||
|
|
||||||
|
Le due gambe eseguibili a basso capitale (XS01/VRP01 STAT-MODE esclusi), scorrelate (corr ~0.21) ->
|
||||||
|
blend Sharpe ~1.5, DD dimezzato. Traccia FORWARD-ONLY DUE versioni in parallelo:
|
||||||
|
* NUDO = blend 50/50 TP01+GTAA
|
||||||
|
* PROTETTO = stesso blend + GUARDIA-DRAWDOWN -4% (de-risk a 0.4x quando il DD da picco supera -4%,
|
||||||
|
ri-rischia a -1.6%). Backtest: MaxDD 8.4%->5.8%, 2022 -4.4%->-1.8%, Sharpe 1.48->1.38
|
||||||
|
(diario 2026-06-23-tail-hedge-lab). Le opzioni NON aiutano il grind del 2022 -> escluse.
|
||||||
|
Crypto compoundato sul grid giorni-di-borsa. NESSUNA esecuzione reale. Mostra posizioni azionabili.
|
||||||
|
|
||||||
|
uv run python scripts/live/paper_combo.py [--status|--reset]
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import sys, json
|
||||||
|
from pathlib import Path
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
import numpy as np, pandas as pd
|
||||||
|
from src.portfolio.sleeves import _tp01_returns, _tp01_positions
|
||||||
|
from src.portfolio.gtaa import gtaa_returns, gtaa_weights
|
||||||
|
|
||||||
|
STATE_DIR = PROJECT_ROOT / "data" / "paper_combo"
|
||||||
|
STATE = STATE_DIR / "state.json"
|
||||||
|
EQ = STATE_DIR / "equity.csv"
|
||||||
|
INITIAL = 2000.0
|
||||||
|
W_CRYPTO = 0.5
|
||||||
|
DD_TRIGGER = 0.04 # guardia-drawdown della versione PROTETTA
|
||||||
|
|
||||||
|
|
||||||
|
def combo_daily(wc: float = W_CRYPTO) -> pd.Series:
|
||||||
|
tp = _tp01_returns()
|
||||||
|
if tp.index.tz is None:
|
||||||
|
tp.index = tp.index.tz_localize("UTC")
|
||||||
|
eq = gtaa_returns().dropna()
|
||||||
|
grid = eq.index[eq.index >= tp.index[0]]
|
||||||
|
cum = (1.0 + tp).cumprod()
|
||||||
|
tpg = (cum.reindex(cum.index.union(grid)).ffill().reindex(grid)).pct_change()
|
||||||
|
J = pd.concat({"c": tpg, "e": eq.reindex(grid)}, axis=1).dropna()
|
||||||
|
return (wc * J["c"] + (1 - wc) * J["e"]).dropna()
|
||||||
|
|
||||||
|
|
||||||
|
def apply_dd_guard(r: pd.Series, trigger: float = DD_TRIGGER) -> pd.Series:
|
||||||
|
"""De-risk a 0.4x quando il DD da picco > trigger; ri-rischia a 1.0x quando < 0.4*trigger."""
|
||||||
|
rv = r.values; n = len(rv); eq = np.cumprod(1 + rv); pk = np.maximum.accumulate(eq)
|
||||||
|
expo = np.ones(n); on = True
|
||||||
|
for i in range(1, n):
|
||||||
|
ddi = (pk[i - 1] - eq[i - 1]) / pk[i - 1] if pk[i - 1] > 0 else 0.0
|
||||||
|
if ddi > trigger: on = False
|
||||||
|
if ddi < trigger * 0.4: on = True
|
||||||
|
expo[i] = 1.0 if on else 0.4
|
||||||
|
return pd.Series(expo * rv, index=r.index)
|
||||||
|
|
||||||
|
|
||||||
|
def both_daily():
|
||||||
|
naked = combo_daily()
|
||||||
|
return naked, apply_dd_guard(naked)
|
||||||
|
|
||||||
|
|
||||||
|
def load():
|
||||||
|
return json.loads(STATE.read_text()) if STATE.exists() else None
|
||||||
|
|
||||||
|
|
||||||
|
def save(st):
|
||||||
|
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
STATE.write_text(json.dumps(st, indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
def advance():
|
||||||
|
naked, guard = both_daily()
|
||||||
|
st = load()
|
||||||
|
if st is None or "equity_g" not in st: # init (o migrazione a doppia versione)
|
||||||
|
last = str(naked.index[-1])
|
||||||
|
st = dict(start=last, last=last, initial=INITIAL, n_days=0, w_crypto=W_CRYPTO, dd_trigger=DD_TRIGGER,
|
||||||
|
equity=INITIAL, peak=INITIAL, max_dd=0.0,
|
||||||
|
equity_g=INITIAL, peak_g=INITIAL, max_dd_g=0.0)
|
||||||
|
save(st); EQ.write_text("date,nudo,protetto\n" + f"{last},{INITIAL},{INITIAL}\n")
|
||||||
|
return st
|
||||||
|
last = pd.Timestamp(st["last"])
|
||||||
|
nn = naked[naked.index > last]; gg = guard[guard.index > last]
|
||||||
|
if len(nn):
|
||||||
|
e = st["equity"]; pk = st["peak"]; dd = st["max_dd"]
|
||||||
|
eg = st["equity_g"]; pkg = st["peak_g"]; ddg = st["max_dd_g"]; lines = []
|
||||||
|
for d in nn.index:
|
||||||
|
e *= (1 + float(nn[d])); pk = max(pk, e); dd = max(dd, (pk - e) / pk if pk > 0 else 0)
|
||||||
|
eg *= (1 + float(gg[d])); pkg = max(pkg, eg); ddg = max(ddg, (pkg - eg) / pkg if pkg > 0 else 0)
|
||||||
|
lines.append(f"{d},{e:.4f},{eg:.4f}")
|
||||||
|
st.update(equity=e, peak=pk, max_dd=dd, equity_g=eg, peak_g=pkg, max_dd_g=ddg,
|
||||||
|
last=str(nn.index[-1]), n_days=st["n_days"] + len(nn))
|
||||||
|
save(st)
|
||||||
|
with open(EQ, "a") as f:
|
||||||
|
f.write("\n".join(lines) + "\n")
|
||||||
|
return st
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
a = sys.argv[1:]
|
||||||
|
if "--reset" in a:
|
||||||
|
for f in (STATE, EQ):
|
||||||
|
f.unlink(missing_ok=True)
|
||||||
|
print("paper combo azzerato.")
|
||||||
|
st = load() if "--status" in a else advance()
|
||||||
|
if st is None or "equity_g" not in st:
|
||||||
|
st = advance()
|
||||||
|
days = (pd.Timestamp(st["last"]) - pd.Timestamp(st["start"])).days
|
||||||
|
rn = st["equity"] / st["initial"] - 1; rg = st["equity_g"] / st["initial"] - 1
|
||||||
|
gw = gtaa_weights(); asof = gw.pop("_asof", "?"); cash = gw.pop("_cash", None)
|
||||||
|
print("PAPER COMBO — TP01 (Deribit) + GTAA (IB), forward-only, blend 50/50")
|
||||||
|
print(f" start {st['start'][:10]} -> last {st['last'][:10]} ({days}g, {st['n_days']} barre)")
|
||||||
|
print(f" NUDO : eq {st['equity']:.2f} ret {rn*100:+.2f}% maxDD {st['max_dd']*100:.1f}%")
|
||||||
|
print(f" PROTETTO : eq {st['equity_g']:.2f} ret {rg*100:+.2f}% maxDD {st['max_dd_g']*100:.1f}% (guardia-DD -{st.get('dd_trigger',DD_TRIGGER)*100:.0f}%)")
|
||||||
|
print(f" --- POSIZIONI AZIONABILI ---")
|
||||||
|
print(f" TP01 (Deribit): {_tp01_positions()}")
|
||||||
|
print(f" GTAA (IB, asof {asof}): " + ", ".join(f"{k} {v:.0%}" for k, v in gw.items() if v) + f" | cash {cash:.0%}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
"""FORWARD-MONITOR — PREVDAY RANGE BREAKOUT (lead ortogonale a TP01), forward-only, PAPER.
|
||||||
|
|
||||||
|
NON è esecuzione reale. È il monitoraggio forward-only del LEAD validato dall'onda intraday
|
||||||
|
(src/strategies/prevday_breakout.py, parametri CONGELATI) per vedere se l'edge in-sample regge
|
||||||
|
FUORI CAMPIONE VERO nei prossimi mesi. Stesso trattamento di XS01 STAT-MODE / STA05.
|
||||||
|
|
||||||
|
DESIGN (onesto):
|
||||||
|
- Legge i parquet certificati BTC/ETH 1h (data/raw). Segnale a 1h, libro 50/50.
|
||||||
|
- Alla prima esecuzione parte dall'ultima barra 1h CHIUSA (forward-only: lo storico NON entra
|
||||||
|
nel PnL di paper, si traccia solo da ora in avanti).
|
||||||
|
- Ogni run processa le NUOVE barre 1h chiuse: applica il rendimento della posizione tenuta,
|
||||||
|
addebita le fee sul turnover, registra i flip di segno, poi ricalcola la posizione-bersaglio.
|
||||||
|
- Traccia DUE libri in parallelo per onestà sull'esecuzione (lo scettico ha segnalato che a $600
|
||||||
|
il micro-ribilanciamento del vol-target ha un haircut di fill):
|
||||||
|
* MODELED : capitale nominale $2000, ribilanciamento continuo (fee proporzionale su ogni |Δ|).
|
||||||
|
* REAL-$600: capitale reale $600, salta i ribilanciamenti di nozionale < min_order ($5) —
|
||||||
|
cosa che il conto vero catturerebbe davvero. Il gap MODELED-REAL = l'haircut di fill reale.
|
||||||
|
- Per barre fresche, aggiornare prima i dati:
|
||||||
|
uv run python scripts/analysis/rebuild_history.py --asset BTC ETH
|
||||||
|
|
||||||
|
Stato: data/paper_prevday/{state.json, trades.jsonl, returns.jsonl} (append-only).
|
||||||
|
|
||||||
|
uv run python scripts/live/paper_prevday.py # avanza col dato disponibile
|
||||||
|
uv run python scripts/live/paper_prevday.py --status # solo stato, non avanza
|
||||||
|
uv run python scripts/live/paper_prevday.py --reset # azzera (riparte da ora)
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
|
||||||
|
from src.backtest.harness import load # noqa: E402
|
||||||
|
from src.strategies.prevday_breakout import target as prevday_target # noqa: E402
|
||||||
|
from src.strategies import prevday_breakout as pb # noqa: E402
|
||||||
|
|
||||||
|
STATE_DIR = PROJECT_ROOT / "data" / "paper_prevday"
|
||||||
|
STATE_FILE = STATE_DIR / "state.json"
|
||||||
|
TRADES_FILE = STATE_DIR / "trades.jsonl"
|
||||||
|
RETURNS_FILE = STATE_DIR / "returns.jsonl"
|
||||||
|
ASSETS = ["BTC", "ETH"]
|
||||||
|
WEIGHT = 0.5
|
||||||
|
FEE_SIDE = 0.0005 # 0.05%/side = 0.10% round-trip (Deribit taker)
|
||||||
|
MODELED_CAPITAL = 2000.0 # nominale, ribilanciamento continuo
|
||||||
|
REAL_CAPITAL = 600.0 # capitale mainnet reale
|
||||||
|
MIN_ORDER = 5.0 # min order Deribit -> sotto, il conto vero NON ribilancia
|
||||||
|
|
||||||
|
|
||||||
|
def build_bars() -> dict[str, pd.DataFrame]:
|
||||||
|
return {a: load(a, "1h").reset_index(drop=True) for a in ASSETS}
|
||||||
|
|
||||||
|
|
||||||
|
def _state_io(write: dict | None = None):
|
||||||
|
if write is not None:
|
||||||
|
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
STATE_FILE.write_text(json.dumps(write, indent=2))
|
||||||
|
return write
|
||||||
|
return json.loads(STATE_FILE.read_text()) if STATE_FILE.exists() else None
|
||||||
|
|
||||||
|
|
||||||
|
def _append(path: Path, rec: dict):
|
||||||
|
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(path, "a") as f:
|
||||||
|
f.write(json.dumps(rec) + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
def init_state(dfs) -> dict:
|
||||||
|
last_ts = min(int(dfs[a]["timestamp"].iloc[-1]) for a in ASSETS)
|
||||||
|
pos = {a: pb.current_target(dfs[a][dfs[a]["timestamp"] <= last_ts]) for a in ASSETS}
|
||||||
|
return dict(
|
||||||
|
start_ts=last_ts, last_ts=last_ts, n_bars=0,
|
||||||
|
pos_modeled=pos, pos_real=dict(pos),
|
||||||
|
cap_modeled=MODELED_CAPITAL, cap_real=REAL_CAPITAL,
|
||||||
|
peak_modeled=MODELED_CAPITAL, peak_real=REAL_CAPITAL,
|
||||||
|
dd_modeled=0.0, dd_real=0.0, n_trades=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def advance(st: dict, dfs: dict) -> dict:
|
||||||
|
data = {}
|
||||||
|
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
|
||||||
|
data[a] = dict(ts=df["timestamp"].values.astype("int64"),
|
||||||
|
dt=pd.to_datetime(df["datetime"]).values, r=r,
|
||||||
|
tgt=prevday_target(df))
|
||||||
|
common = sorted(set(data["BTC"]["ts"]).intersection(data["ETH"]["ts"]))
|
||||||
|
new_ts = [t for t in common if t > st["last_ts"]]
|
||||||
|
if not new_ts:
|
||||||
|
return st
|
||||||
|
idx = {a: {int(t): i for i, t in enumerate(data[a]["ts"])} for a in ASSETS}
|
||||||
|
pm, pr = dict(st["pos_modeled"]), dict(st["pos_real"])
|
||||||
|
cm, cr = st["cap_modeled"], st["cap_real"]
|
||||||
|
pkm, pkr = st["peak_modeled"], st["peak_real"]
|
||||||
|
ddm, ddr = st["dd_modeled"], st["dd_real"]
|
||||||
|
ntr = st.get("n_trades", 0)
|
||||||
|
|
||||||
|
for t in new_ts:
|
||||||
|
net_m = net_r = 0.0
|
||||||
|
nm, nr = {}, {}
|
||||||
|
for a in ASSETS:
|
||||||
|
i = idx[a][int(t)]
|
||||||
|
r = float(data[a]["r"][i]); tgt = float(data[a]["tgt"][i])
|
||||||
|
# MODELED: continuous rebalance
|
||||||
|
hm = pm[a]
|
||||||
|
net_m += WEIGHT * (hm * r - FEE_SIDE * abs(tgt - hm))
|
||||||
|
nm[a] = tgt
|
||||||
|
if np.sign(tgt) != np.sign(hm):
|
||||||
|
_append(TRADES_FILE, dict(ts=int(t), dt=str(pd.Timestamp(data[a]["dt"][i])),
|
||||||
|
asset=a, action="ENTRY" if tgt != 0 else "EXIT",
|
||||||
|
from_pos=round(hm, 4), to_pos=round(tgt, 4)))
|
||||||
|
ntr += 1
|
||||||
|
# REAL-$600: skip sub-min_order rebalances
|
||||||
|
hr = pr[a]
|
||||||
|
leg_cap = cr * WEIGHT
|
||||||
|
executed = abs(tgt - hr) * leg_cap >= MIN_ORDER
|
||||||
|
new_hr = tgt if executed else hr
|
||||||
|
net_r += WEIGHT * (hr * r - FEE_SIDE * abs(new_hr - hr))
|
||||||
|
nr[a] = new_hr
|
||||||
|
cm *= (1.0 + max(net_m, -0.99)); cr *= (1.0 + max(net_r, -0.99))
|
||||||
|
pkm = max(pkm, cm); pkr = max(pkr, cr)
|
||||||
|
ddm = max(ddm, (pkm - cm) / pkm if pkm > 0 else 0.0)
|
||||||
|
ddr = max(ddr, (pkr - cr) / pkr if pkr > 0 else 0.0)
|
||||||
|
pm, pr = nm, nr
|
||||||
|
_append(RETURNS_FILE, dict(ts=int(t), dt=str(pd.Timestamp(data["BTC"]["dt"][idx["BTC"][int(t)]])),
|
||||||
|
net_modeled=round(net_m, 6), net_real=round(net_r, 6),
|
||||||
|
pos_btc=round(pr["BTC"], 4), pos_eth=round(pr["ETH"], 4),
|
||||||
|
cap_modeled=round(cm, 2), cap_real=round(cr, 2)))
|
||||||
|
|
||||||
|
st.update(last_ts=int(new_ts[-1]), n_bars=st.get("n_bars", 0) + len(new_ts),
|
||||||
|
pos_modeled=pm, pos_real=pr, cap_modeled=cm, cap_real=cr,
|
||||||
|
peak_modeled=pkm, peak_real=pkr, dd_modeled=ddm, dd_real=ddr, n_trades=ntr)
|
||||||
|
return st
|
||||||
|
|
||||||
|
|
||||||
|
def print_status(st: dict, dfs: dict):
|
||||||
|
days = (max(int(dfs[a]["timestamp"].iloc[-1]) for a in ASSETS) - st["start_ts"]) / 86400_000
|
||||||
|
rm = st["cap_modeled"] / MODELED_CAPITAL - 1
|
||||||
|
rr = st["cap_real"] / REAL_CAPITAL - 1
|
||||||
|
print(f"\n PREVDAY-BREAKOUT forward-monitor (PAPER, lead ortogonale a TP01 — non deploy)")
|
||||||
|
print(f" forward da {pd.Timestamp(st['start_ts'], unit='ms', tz='UTC').date()} "
|
||||||
|
f"({st['n_bars']} barre 1h ~{days:.0f}g) trade(flip): {st['n_trades']}")
|
||||||
|
print(f" posizione corrente: BTC {st['pos_real']['BTC']:+.3f} ETH {st['pos_real']['ETH']:+.3f}")
|
||||||
|
print(f" MODELED ($2000 nominale): {rm*100:+6.2f}% eq ${st['cap_modeled']:.2f} maxDD {st['dd_modeled']*100:.1f}%")
|
||||||
|
print(f" REAL-$600 (min-order $5) : {rr*100:+6.2f}% eq ${st['cap_real']:.2f} maxDD {st['dd_real']*100:.1f}%")
|
||||||
|
print(f" -> fill-haircut MODELED-REAL: {(rm-rr)*100:+.2f} pp (lo scettico l'aveva segnalato)")
|
||||||
|
print(f" log: {RETURNS_FILE}\n")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("--status", action="store_true")
|
||||||
|
ap.add_argument("--reset", action="store_true")
|
||||||
|
args = ap.parse_args()
|
||||||
|
dfs = build_bars()
|
||||||
|
if args.reset:
|
||||||
|
for p in (STATE_FILE, TRADES_FILE, RETURNS_FILE):
|
||||||
|
if p.exists():
|
||||||
|
p.unlink()
|
||||||
|
st = init_state(dfs); _state_io(st)
|
||||||
|
print("forward-monitor inizializzato (forward-only da ora).")
|
||||||
|
print_status(st, dfs); return
|
||||||
|
st = _state_io()
|
||||||
|
if st is None:
|
||||||
|
st = init_state(dfs); _state_io(st)
|
||||||
|
print("forward-monitor inizializzato (forward-only da ora).")
|
||||||
|
print_status(st, dfs); return
|
||||||
|
if not args.status:
|
||||||
|
st = advance(st, dfs); _state_io(st)
|
||||||
|
print_status(st, dfs)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
"""FORWARD-MONITOR — STATARB-RESID (relative-value ETH/BTC, dollar-neutral 2 gambe), PAPER.
|
||||||
|
|
||||||
|
NON è esecuzione reale. È il monitoraggio forward-only del LEAD dello sweep 2026-06-29 (thread
|
||||||
|
"segnali ortogonali"): il PRIMO stream visto che è INSIEME ortogonale al book (corr→book 0.027,
|
||||||
|
beta-mkt 0.013) ED eseguibile a $600 (book a 2 gambe BTC+ETH perp, NON STAT-MODE come XS01). Resta
|
||||||
|
sotto la soglia solo sull'EDGE (Sharpe ~0.84, deflated-Sharpe <0.95 su storia ~2.5 anni). Questo
|
||||||
|
monitor accumula la finestra OUT-OF-SAMPLE VERA per decidere se l'edge regge — stesso trattamento di
|
||||||
|
PREVDAY/STA05/XS01 STAT-MODE. Nessun edge creduto prima.
|
||||||
|
|
||||||
|
ⓘ NATURA DEL SEGNALE (onesto): la cella vincente è W=45 **sgn=+1**, NON sgn=-1. Cioè il residuo di
|
||||||
|
ETH dopo β·BTC (β OLS rolling causale) viene seguito in MOMENTUM (long lo spread quando ETH è ricco
|
||||||
|
vs il suo fit su BTC), non in mean-reversion: le dislocazioni relative ETH-vs-BTC CONTINUANO a 1d
|
||||||
|
(la MR pura, sgn=-1, perde -1.4 in-sample). È relative-MOMENTUM, dollar-neutral → ortogonale al
|
||||||
|
trend assoluto di TP01.
|
||||||
|
|
||||||
|
CONFIG CONGELATA (dallo sweep, selezione IN-SAMPLE-only, mai sul max hold-out):
|
||||||
|
W=45, sgn=+1, tf=1d, vol-target 20%, leva cap 2x. Riusa il segnale ESATTO di
|
||||||
|
scripts/research/orthogonal_signals.py (nessuna reimplementazione → niente drift).
|
||||||
|
|
||||||
|
DESIGN (onesto, come paper_prevday):
|
||||||
|
- Legge BTC/ETH 1d certificati (build_joint), calcola il segnale CAUSALE su tutta la storia.
|
||||||
|
- Alla prima esecuzione fissa l'INCEPTION all'ultima barra 1d chiusa (forward-only: lo storico
|
||||||
|
NON entra nel PnL di paper).
|
||||||
|
- Ogni run processa le NUOVE barre 1d chiuse: applica il rendimento dello spread tenuto,
|
||||||
|
addebita le fee su 2 GAMBE (ogni Δpos muove ETH e BTC → 2×|Δpos|), registra i flip di segno.
|
||||||
|
- DUE libri in parallelo (onestà sull'esecuzione a piccolo capitale):
|
||||||
|
* MODELED : capitale nominale $2000, ribilanciamento continuo (fee su ogni |Δpos|).
|
||||||
|
* REAL-$600: capitale reale $600, salta i ribilanci di nozionale-per-gamba < min_order ($5)
|
||||||
|
→ il gap MODELED-REAL = l'haircut di fill reale (qui ~0: turnover bassissimo a 1d).
|
||||||
|
- Per barre fresche aggiornare prima i dati: uv run python scripts/analysis/rebuild_history.py --asset BTC ETH
|
||||||
|
|
||||||
|
Stato: data/paper_statarb/{state.json, trades.jsonl, returns.jsonl} (append-only).
|
||||||
|
|
||||||
|
uv run python scripts/live/paper_statarb.py # avanza col dato disponibile
|
||||||
|
uv run python scripts/live/paper_statarb.py --status # solo stato, non avanza
|
||||||
|
uv run python scripts/live/paper_statarb.py --reset # azzera (riparte da ora)
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT / "scripts" / "research"))
|
||||||
|
|
||||||
|
# Segnale ESATTO dello sweep (nessuna reimplementazione → identità garantita col backtest).
|
||||||
|
from orthogonal_signals import build_joint, f_statarb_resid, spread_ret # noqa: E402
|
||||||
|
|
||||||
|
STATE_DIR = PROJECT_ROOT / "data" / "paper_statarb"
|
||||||
|
STATE_FILE = STATE_DIR / "state.json"
|
||||||
|
TRADES_FILE = STATE_DIR / "trades.jsonl"
|
||||||
|
RETURNS_FILE = STATE_DIR / "returns.jsonl"
|
||||||
|
|
||||||
|
# --- CONFIG CONGELATA (frozen) -----------------------------------------------------------
|
||||||
|
W_FROZEN = 45
|
||||||
|
SGN_FROZEN = +1
|
||||||
|
FEE_SIDE = 0.0005 # 0.05%/side = 0.10% round-trip (Deribit taker)
|
||||||
|
MODELED_CAPITAL = 2000.0 # nominale, ribilanciamento continuo
|
||||||
|
REAL_CAPITAL = 600.0 # capitale mainnet reale
|
||||||
|
MIN_ORDER = 5.0 # min order Deribit -> sotto, il conto vero NON ribilancia
|
||||||
|
|
||||||
|
|
||||||
|
def _signal(j: pd.DataFrame):
|
||||||
|
"""(ts, dt, pos, sr): posizione vol-targeted sullo spread (decisa <= close[i]) + ritorno spread."""
|
||||||
|
pos = np.nan_to_num(f_statarb_resid(W=W_FROZEN, sgn=SGN_FROZEN)(j))
|
||||||
|
return (j["timestamp"].values.astype("int64"),
|
||||||
|
pd.to_datetime(j["datetime"]).values, pos, spread_ret(j))
|
||||||
|
|
||||||
|
|
||||||
|
def _state_io(write: dict | None = None):
|
||||||
|
if write is not None:
|
||||||
|
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
STATE_FILE.write_text(json.dumps(write, indent=2))
|
||||||
|
return write
|
||||||
|
return json.loads(STATE_FILE.read_text()) if STATE_FILE.exists() else None
|
||||||
|
|
||||||
|
|
||||||
|
def _append(path: Path, rec: dict):
|
||||||
|
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(path, "a") as f:
|
||||||
|
f.write(json.dumps(rec) + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
def init_state(j: pd.DataFrame) -> dict:
|
||||||
|
ts, _, pos, _ = _signal(j)
|
||||||
|
last = int(ts[-1])
|
||||||
|
return dict(start_ts=last, last_ts=last, n_bars=0,
|
||||||
|
pos_modeled=float(pos[-1]), pos_real=float(pos[-1]),
|
||||||
|
cap_modeled=MODELED_CAPITAL, cap_real=REAL_CAPITAL,
|
||||||
|
peak_modeled=MODELED_CAPITAL, peak_real=REAL_CAPITAL,
|
||||||
|
dd_modeled=0.0, dd_real=0.0, n_trades=0)
|
||||||
|
|
||||||
|
|
||||||
|
def advance(st: dict, j: pd.DataFrame) -> dict:
|
||||||
|
ts, dt, pos, sr = _signal(j)
|
||||||
|
new = [i for i in range(len(ts)) if ts[i] > st["last_ts"]]
|
||||||
|
if not new:
|
||||||
|
return st
|
||||||
|
pm, pr = st["pos_modeled"], st["pos_real"] # posizioni TENUTE (decise alla barra precedente)
|
||||||
|
cm, cr = st["cap_modeled"], st["cap_real"]
|
||||||
|
pkm, pkr, ddm, ddr = st["peak_modeled"], st["peak_real"], st["dd_modeled"], st["dd_real"]
|
||||||
|
ntr = st.get("n_trades", 0)
|
||||||
|
for i in new:
|
||||||
|
s = float(sr[i])
|
||||||
|
tgt = float(pos[i]) # bersaglio deciso a close[i] (eseguito da i+1)
|
||||||
|
# MODELED: rendimento dello spread tenuto durante la barra i, poi ribilancia al target
|
||||||
|
net_m = pm * s - FEE_SIDE * 2.0 * abs(tgt - pm)
|
||||||
|
# REAL-$600: salta i ribilanci sub-min_order (nozionale per-gamba |Δpos|*cap < $5)
|
||||||
|
executed = abs(tgt - pr) * REAL_CAPITAL >= MIN_ORDER
|
||||||
|
new_pr = tgt if executed else pr
|
||||||
|
net_r = pr * s - FEE_SIDE * 2.0 * abs(new_pr - pr)
|
||||||
|
if np.sign(new_pr) != np.sign(pr):
|
||||||
|
_append(TRADES_FILE, dict(ts=int(ts[i]), dt=str(pd.Timestamp(dt[i])),
|
||||||
|
action="FLIP", from_pos=round(pr, 4), to_pos=round(new_pr, 4)))
|
||||||
|
ntr += 1
|
||||||
|
cm *= (1.0 + max(net_m, -0.99)); cr *= (1.0 + max(net_r, -0.99))
|
||||||
|
pkm = max(pkm, cm); pkr = max(pkr, cr)
|
||||||
|
ddm = max(ddm, (pkm - cm) / pkm if pkm > 0 else 0.0)
|
||||||
|
ddr = max(ddr, (pkr - cr) / pkr if pkr > 0 else 0.0)
|
||||||
|
pm, pr = tgt, new_pr
|
||||||
|
_append(RETURNS_FILE, dict(ts=int(ts[i]), dt=str(pd.Timestamp(dt[i])),
|
||||||
|
net_modeled=round(net_m, 6), net_real=round(net_r, 6),
|
||||||
|
pos=round(new_pr, 4), cap_modeled=round(cm, 2), cap_real=round(cr, 2)))
|
||||||
|
st.update(last_ts=int(ts[new[-1]]), n_bars=st.get("n_bars", 0) + len(new),
|
||||||
|
pos_modeled=pm, pos_real=pr, cap_modeled=cm, cap_real=cr,
|
||||||
|
peak_modeled=pkm, peak_real=pkr, dd_modeled=ddm, dd_real=ddr, n_trades=ntr)
|
||||||
|
return st
|
||||||
|
|
||||||
|
|
||||||
|
def print_status(st: dict, j: pd.DataFrame):
|
||||||
|
last_data = int(j["timestamp"].iloc[-1])
|
||||||
|
days = (last_data - st["start_ts"]) / 86400_000
|
||||||
|
rm = st["cap_modeled"] / MODELED_CAPITAL - 1
|
||||||
|
rr = st["cap_real"] / REAL_CAPITAL - 1
|
||||||
|
print("\n STATARB-RESID forward-monitor (PAPER — relative-value ETH/BTC, ortogonale, NON deploy)")
|
||||||
|
print(f" config CONGELATA: W={W_FROZEN} sgn={SGN_FROZEN:+d} (residual relative-momentum, dollar-neutral)")
|
||||||
|
print(f" forward da {pd.Timestamp(st['start_ts'], unit='ms', tz='UTC').date()} "
|
||||||
|
f"({st['n_bars']} barre 1d ~{days:.0f}g) flip di segno: {st['n_trades']}")
|
||||||
|
print(f" posizione spread corrente (long ETH / short BTC per unità): {st['pos_real']:+.3f}")
|
||||||
|
print(f" MODELED ($2000 nominale): {rm*100:+6.2f}% eq ${st['cap_modeled']:.2f} maxDD {st['dd_modeled']*100:.1f}%")
|
||||||
|
print(f" REAL-$600 (min-order $5) : {rr*100:+6.2f}% eq ${st['cap_real']:.2f} maxDD {st['dd_real']*100:.1f}%")
|
||||||
|
print(f" -> fill-haircut MODELED-REAL: {(rm-rr)*100:+.2f} pp (atteso ~0: turnover basso a 1d)")
|
||||||
|
print(f" log: {RETURNS_FILE}\n")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("--status", action="store_true")
|
||||||
|
ap.add_argument("--reset", action="store_true")
|
||||||
|
args = ap.parse_args()
|
||||||
|
j = build_joint("1d")
|
||||||
|
if args.reset:
|
||||||
|
for p in (STATE_FILE, TRADES_FILE, RETURNS_FILE):
|
||||||
|
if p.exists():
|
||||||
|
p.unlink()
|
||||||
|
st = init_state(j); _state_io(st)
|
||||||
|
print("forward-monitor STATARB-RESID inizializzato (forward-only da ora).")
|
||||||
|
print_status(st, j); return
|
||||||
|
st = _state_io()
|
||||||
|
if st is None:
|
||||||
|
st = init_state(j); _state_io(st)
|
||||||
|
print("forward-monitor STATARB-RESID inizializzato (forward-only da ora).")
|
||||||
|
print_status(st, j); return
|
||||||
|
if not args.status:
|
||||||
|
st = advance(st, j); _state_io(st)
|
||||||
|
print_status(st, j)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
"""PROIEZIONE ACCUMULO del book DERIBIT-ONLY (TP01 + SKH01) — compounding puro (reinvesti le
|
||||||
|
vincite), allineamento MENSILE, NESSUN versamento esterno (non e' un PAC).
|
||||||
|
|
||||||
|
Base: rendimenti mensili del book Deribit-only (rebal mensile, netto costo turnover). Proietta in
|
||||||
|
avanti l'equity da un capitale iniziale:
|
||||||
|
- DETERMINISTICO @CAGR storico e @CAGR conservativo (= storico × cons_frac, default metà);
|
||||||
|
- MONTE CARLO block-bootstrap dei rendimenti mensili storici (mediana + banda p10/p90);
|
||||||
|
- €/giorno run-rate (cresce col capitale, perche' si rigiocano le vincite).
|
||||||
|
|
||||||
|
⚠️ ONESTA': lo storico e' un BULL crypto ~2019-26 -> il futuro sara' quasi certamente piu' magro.
|
||||||
|
Pianificare sulla colonna conservativa; il MC non contiene un vero bear pluriennale (anche il p10
|
||||||
|
e' ottimista). Nessuna leva. SKH01 e' research/forward-monitor (solo TP01 e' armato live). Non e'
|
||||||
|
una garanzia: e' una proiezione condizionata "se il futuro somigliasse al passato".
|
||||||
|
|
||||||
|
uv run python scripts/portfolio/forecast_deribit_book.py
|
||||||
|
uv run python scripts/portfolio/forecast_deribit_book.py --capital 5000 --years 1,3,5,10 --cons-frac 0.5
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from src.portfolio.portfolio import StrategyPortfolio, rebalance_sim
|
||||||
|
from src.portfolio.sleeves import deribit_book_sleeves
|
||||||
|
|
||||||
|
|
||||||
|
def book_monthly_returns(rebal_days: int, cost_rate: float) -> pd.Series:
|
||||||
|
"""Rendimenti MENSILI del book Deribit-only (rebal periodico, netto costo)."""
|
||||||
|
sl = deribit_book_sleeves()
|
||||||
|
w = StrategyPortfolio(sl).weights()
|
||||||
|
cols = {s.name: s.daily() for s in sl}
|
||||||
|
r = rebalance_sim(cols, w, period_days=rebal_days, cost_rate=cost_rate)["daily"]
|
||||||
|
return ((1.0 + r).resample("ME").prod() - 1.0).dropna()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(description="Proiezione accumulo book Deribit-only (compounding, allineamento mensile)")
|
||||||
|
ap.add_argument("--capital", type=float, default=5000.0)
|
||||||
|
ap.add_argument("--years", type=str, default="1,2,3,5,8,10")
|
||||||
|
ap.add_argument("--cons-frac", type=float, default=0.5, help="CAGR conservativo = storico × questo (default 0.5)")
|
||||||
|
ap.add_argument("--sims", type=int, default=4000)
|
||||||
|
ap.add_argument("--block-months", type=int, default=3, help="blocco del bootstrap (mesi)")
|
||||||
|
ap.add_argument("--rebal-days", type=int, default=30)
|
||||||
|
ap.add_argument("--cost-rate", type=float, default=0.0005, help="fee per-lato sul turnover (Deribit taker)")
|
||||||
|
ap.add_argument("--seed", type=int, default=7)
|
||||||
|
a = ap.parse_args()
|
||||||
|
|
||||||
|
cap = a.capital
|
||||||
|
HY = [int(x) for x in a.years.split(",") if x.strip()]
|
||||||
|
m = book_monthly_returns(a.rebal_days, a.cost_rate)
|
||||||
|
mv = m.values
|
||||||
|
nm = len(mv)
|
||||||
|
g_month = float(np.prod(1.0 + mv) ** (1.0 / nm) - 1.0)
|
||||||
|
cagr = (1.0 + g_month) ** 12 - 1.0
|
||||||
|
vol_ann = float(mv.std() * np.sqrt(12))
|
||||||
|
cons_cagr = cagr * a.cons_frac
|
||||||
|
|
||||||
|
print("=" * 90)
|
||||||
|
print(f" PROIEZIONE ACCUMULO — book Deribit-only (TP01+SKH01) | compounding, allineamento mensile, no versamenti")
|
||||||
|
print(f" storico: {nm} mesi · CAGR {cagr*100:.1f}% · vol annua {vol_ann*100:.0f}% (bull crypto, no leva) | capitale €{cap:,.0f}")
|
||||||
|
print("=" * 90)
|
||||||
|
|
||||||
|
# Monte Carlo: block-bootstrap dei rendimenti mensili
|
||||||
|
rng = np.random.default_rng(a.seed)
|
||||||
|
blk = a.block_months
|
||||||
|
maxM = max(HY) * 12
|
||||||
|
nb = maxM // blk + 1
|
||||||
|
starts = rng.integers(0, nm - blk, size=(a.sims, nb))
|
||||||
|
paths = np.concatenate([mv[starts[:, k][:, None] + np.arange(blk)[None, :]] for k in range(nb)], axis=1)[:, :maxM]
|
||||||
|
eqMC = cap * np.cumprod(1.0 + paths, axis=1)
|
||||||
|
|
||||||
|
cons_m = (1.0 + cons_cagr) ** (1.0 / 12) - 1.0
|
||||||
|
print(f"\n ACCUMULO (reinvesti le vincite):")
|
||||||
|
print(f" {'oriz.':>6} | {'det @storico':>13} | {'det @conserv.':>14} | {'MC mediana':>11} | {'MC p10':>9} | {'MC p90':>10}")
|
||||||
|
print(f" {'':>6} | {'('+format(cagr*100,'.0f')+'%)':>13} | {'('+format(cons_cagr*100,'.0f')+'%)':>14} |")
|
||||||
|
print(" " + "-" * 80)
|
||||||
|
for y in HY:
|
||||||
|
mo = y * 12
|
||||||
|
det = cap * (1.0 + g_month) ** mo
|
||||||
|
detc = cap * (1.0 + cons_m) ** mo
|
||||||
|
c = eqMC[:, mo - 1]
|
||||||
|
print(f" {y:>4}a | €{det:>11,.0f} | €{detc:>12,.0f} | €{np.median(c):>9,.0f} | €{np.percentile(c,10):>7,.0f} | €{np.percentile(c,90):>8,.0f}")
|
||||||
|
|
||||||
|
# €/giorno run-rate @conservativo (cresce col capitale)
|
||||||
|
rd = (1.0 + cons_cagr) ** (1.0 / 365.0) - 1.0
|
||||||
|
print(f"\n €/GIORNO run-rate @conservativo ({cons_cagr*100:.1f}% CAGR) — cresce col capitale (rigiochi le vincite):")
|
||||||
|
print(f" {'oriz.':>6} | {'equity':>9} | {'€/giorno':>10} | {'€/mese':>8}")
|
||||||
|
print(" " + "-" * 42)
|
||||||
|
for y in [0] + HY:
|
||||||
|
E = cap * (1.0 + cons_cagr) ** y
|
||||||
|
print(f" {('oggi' if y==0 else str(y)+'a'):>6} | €{E:>7,.0f} | €{E*rd:>7,.2f} | €{E*rd*30:>6,.0f}")
|
||||||
|
E_end = cap * (1.0 + cons_cagr) ** max(HY)
|
||||||
|
print(f" media €/giorno su {max(HY)} anni: €{(E_end-cap)/(max(HY)*365):.2f}/g (profitto €{E_end-cap:,.0f})")
|
||||||
|
need = 50 * 365 / cons_cagr if cons_cagr > 0 else float('inf')
|
||||||
|
print(f" capitale per ~€50/giorno @{cons_cagr*100:.1f}%: ≈ €{need:,.0f}")
|
||||||
|
|
||||||
|
print(f"\n ⚠️ Proiezione condizionata (storico = bull crypto); pianifica sul conservativo. Nessuna leva.")
|
||||||
|
print(f" SKH01 = research/forward-monitor; solo TP01 e' armato live. Non e' una garanzia.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -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()
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
"""REPORT del BOOK DERIBIT-ONLY realmente eseguibile = TP01 + SKH01 (75/25).
|
||||||
|
|
||||||
|
Le due gambe direzionali BTC/ETH sullo STESSO venue (Deribit), entrambe dal 2019. Esclude XS01
|
||||||
|
(Hyperliquid, stat-mode) e VRP01 (opzioni modellate). Mostra:
|
||||||
|
1. metriche oneste continuo (rebalance-continuo) vs RIBILANCIAMENTO PERIODICO realistico
|
||||||
|
(settimanale/mensile) con costo turnover Deribit-taker;
|
||||||
|
2. per-anno, accumulo da €2k (e nota €600 reale + min-order $5);
|
||||||
|
3. posizioni correnti per gamba.
|
||||||
|
|
||||||
|
uv run python scripts/portfolio/run_deribit_book.py
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from src.portfolio.portfolio import StrategyPortfolio, metrics, yearly, rebalance_sim, HOLDOUT
|
||||||
|
|
||||||
|
CAP = 2000.0
|
||||||
|
REAL = 600.0 # capitale reale (vedi CLAUDE.md), min-order Deribit $5
|
||||||
|
COST_RATE = 0.0005 # Deribit taker per-lato (~0.10% RT sul turnover netto)
|
||||||
|
|
||||||
|
|
||||||
|
def line(tag, daily, extra=""):
|
||||||
|
m = metrics(daily); h = metrics(daily[daily.index >= HOLDOUT])
|
||||||
|
eqf = CAP * float(np.prod(1.0 + daily.values))
|
||||||
|
print(f" {tag:<26} FULL Sh {m['sharpe']:.2f} ret {m['ret']*100:+.0f}% DD {m['maxdd']*100:.1f}% "
|
||||||
|
f"| HOLD Sh {h['sharpe']:.2f} DD {h['maxdd']*100:.1f}% | €2k→€{eqf:,.0f} {extra}")
|
||||||
|
return m, h
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
from src.portfolio.sleeves import deribit_book_sleeves
|
||||||
|
sleeves = deribit_book_sleeves()
|
||||||
|
pf = StrategyPortfolio(sleeves, capital=CAP)
|
||||||
|
w = pf.weights()
|
||||||
|
cols = {s.name: s.daily() for s in sleeves}
|
||||||
|
|
||||||
|
print("=" * 100)
|
||||||
|
print(f" BOOK DERIBIT-ONLY (eseguibile) — {' + '.join(f'{k} {v*100:.0f}%' for k, v in w.items())} "
|
||||||
|
f"| capitale €{CAP:,.0f} (reale ≈ ${REAL:,.0f}) | hold-out {HOLDOUT.date()}+")
|
||||||
|
print("=" * 100)
|
||||||
|
|
||||||
|
# standalone per-gamba
|
||||||
|
print("\n PER-GAMBA (standalone):")
|
||||||
|
for s in sleeves:
|
||||||
|
d = s.daily(); m = metrics(d); h = metrics(d[d.index >= HOLDOUT])
|
||||||
|
print(f" {s.name:<16} [{w[s.name]*100:>3.0f}%] FULL Sh {m['sharpe']:.2f} DD {m['maxdd']*100:.0f}% "
|
||||||
|
f"| HOLD Sh {h['sharpe']:.2f} DD {h['maxdd']*100:.0f}%")
|
||||||
|
|
||||||
|
print("\n COMBINATO — rebalance-CONTINUO (idealizzato, no costi) vs PERIODICO (reale, costo turnover):")
|
||||||
|
cont = pf.combined_daily()
|
||||||
|
line("continuo (no costo)", cont)
|
||||||
|
sims = {}
|
||||||
|
for tag, period in (("settimanale (7g)", 7), ("bisettimanale (14g)", 14), ("mensile (30g)", 30)):
|
||||||
|
sim = rebalance_sim(cols, w, period_days=period, cost_rate=COST_RATE)
|
||||||
|
sims[tag] = sim
|
||||||
|
line(f"rebal {tag}", sim["daily"], extra=f"| turnover {sim['turnover_per_year']:.1f}×/anno, {sim['n_rebalances']} ribilanci")
|
||||||
|
|
||||||
|
# raccomandato = mensile
|
||||||
|
rec = sims["mensile (30g)"]["daily"]
|
||||||
|
print("\n PER ANNO (rebal mensile, netto costo):")
|
||||||
|
for y, d in yearly(rec).items():
|
||||||
|
print(f" {y}: ret {d['ret']*100:>+7.1f}% DD {d['dd']*100:>5.1f}%")
|
||||||
|
|
||||||
|
print("\n ACCUMULO (rebal mensile):")
|
||||||
|
for cap, lbl in ((CAP, "€2k nominale"), (REAL, "$600 reale")):
|
||||||
|
eq = cap * np.cumprod(1.0 + rec.values)
|
||||||
|
yrs = len(rec) / 365.25
|
||||||
|
print(f" {lbl:<14}: {cap:,.0f} → {eq[-1]:,.0f} (×{eq[-1]/cap:.1f}, ~{(eq[-1]-cap)/(yrs*365.25):+,.2f}/g)")
|
||||||
|
|
||||||
|
print("\n POSIZIONI CORRENTI (ultima barra chiusa):")
|
||||||
|
for name, pos in pf.current_positions().items():
|
||||||
|
print(f" {name}: {pos if pos is not None else 'segnale dual-TF (no pos-fn) — vedi engine'}")
|
||||||
|
|
||||||
|
print("\n NOTE ONESTE:")
|
||||||
|
print(" · TP01 = unico armato live su Deribit (flat=risk-off). SKH01 = 2a gamba candidata (perp BTC/ETH).")
|
||||||
|
print(" · SKH01 equity daily-step (Sharpe lens). A $600 il min-order è $5: un ribilancio mensile")
|
||||||
|
print(" muove abbastanza nozionale da eseguirsi; il giornaliero NO (Δ sub-$5 = finzione) → usa ≥ settimanale.")
|
||||||
|
print(" · Prima del deploy 2a gamba: validare causalità sul CODICE D'ESECUZIONE reale e costi del book a 230m.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
+329
-11
@@ -29,12 +29,14 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
import json
|
import json
|
||||||
|
import math
|
||||||
import sys
|
import sys
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
from scipy.stats import norm
|
||||||
|
|
||||||
# --- make `from src...` work no matter where the agent's script lives -------
|
# --- make `from src...` work no matter where the agent's script lives -------
|
||||||
_ROOT = Path(__file__).resolve().parents[3]
|
_ROOT = Path(__file__).resolve().parents[3]
|
||||||
@@ -334,14 +336,56 @@ def candidate_daily(target_fn, tf: str = "1d", fee_side: float = FEE_SIDE) -> pd
|
|||||||
return _to_daily(0.5 * J[CERTIFIED[0]] + 0.5 * J[CERTIFIED[1]])
|
return _to_daily(0.5 * J[CERTIFIED[0]] + 0.5 * J[CERTIFIED[1]])
|
||||||
|
|
||||||
|
|
||||||
|
def _uplift_series(B: pd.Series, C: pd.Series, w: float = 0.25) -> float:
|
||||||
|
"""Sharpe of the (1-w)*TP01 + w*candidate blend minus Sharpe of TP01 alone."""
|
||||||
|
return _sh((1 - w) * B + w * C) - _sh(B)
|
||||||
|
|
||||||
|
|
||||||
|
def _null_uplift_pctl(B: pd.Series, C: pd.Series, w: float = 0.25,
|
||||||
|
n: int = 300, seed: int = 20260621):
|
||||||
|
"""Where does the candidate's blend-uplift sit vs the NULL of a zero-correlation
|
||||||
|
noise asset with the SAME mean & vol? Lesson of 2026-06-21: a low-corr asset with a
|
||||||
|
little positive drift 'adds' ~+0.03 Sharpe by pure diversification MATH — that is not
|
||||||
|
a signal. We draw `n` iid-normal assets (same mean/std as C, independent of B => corr 0
|
||||||
|
by construction), measure each one's uplift, and return (real_uplift, percentile of
|
||||||
|
real vs the null). pctl >= ~0.8 => the uplift is meaningfully above diversification
|
||||||
|
math; pctl ~0.5 => it IS diversification math. Seeded -> deterministic."""
|
||||||
|
Bx, Cx = B.align(C, join="inner")
|
||||||
|
bs, cs = Bx.values.astype(float), Cx.values.astype(float)
|
||||||
|
if len(cs) < 30:
|
||||||
|
return None, None
|
||||||
|
base = _sh(Bx)
|
||||||
|
real = _sh((1 - w) * Bx + w * Cx) - base
|
||||||
|
mu, sd = float(np.nanmean(cs)), float(np.nanstd(cs))
|
||||||
|
if sd == 0:
|
||||||
|
return round(real, 3), None
|
||||||
|
rng = np.random.default_rng(seed)
|
||||||
|
draws = rng.normal(mu, sd, size=(n, len(cs)))
|
||||||
|
blends = (1 - w) * bs[None, :] + w * draws
|
||||||
|
m, s = blends.mean(axis=1), blends.std(axis=1)
|
||||||
|
null = np.where(s > 0, m / s * np.sqrt(365.25), 0.0) - base
|
||||||
|
return round(float(real), 3), round(float(np.mean(null <= real)), 3)
|
||||||
|
|
||||||
|
|
||||||
def marginal_vs_tp01(cand_daily: pd.Series, weights=(0.25, 0.5)) -> dict:
|
def marginal_vs_tp01(cand_daily: pd.Series, weights=(0.25, 0.5)) -> dict:
|
||||||
"""Does this candidate IMPROVE the TP01 portfolio? Returns correlation, blend uplift
|
"""Does this candidate IMPROVE the TP01 portfolio? Returns correlation, blend uplift
|
||||||
(full & hold-out, per weight), TP01-beta + residual alpha, and a verdict:
|
(full & hold-out, per weight), TP01-beta + residual alpha, and a verdict:
|
||||||
ADDS -> meaningfully lifts the OOS blend and is not just leverage-of-trend
|
ADDS -> lifts the blend, PERSISTENTLY (multi-cut), beats the zero-corr noise
|
||||||
|
null, in BOTH TP01-up and TP01-down regimes
|
||||||
|
HEDGE -> low corr but only pays when TP01 is WEAK (a drawdown dampener, not a
|
||||||
|
standing premium): real, but price it as a hedge, not as alpha
|
||||||
|
NOISE -> uplift indistinguishable from a random zero-corr asset (diversification
|
||||||
|
math, not a signal)
|
||||||
REDUNDANT -> ~identical to TP01 (corr high, ~zero uplift): a re-skin, no slot
|
REDUNDANT -> ~identical to TP01 (corr high, ~zero uplift): a re-skin, no slot
|
||||||
DILUTES -> drags the blend down
|
DILUTES -> drags the blend down
|
||||||
NEUTRAL -> changes little either way (a weak, optional satellite at best)
|
NEUTRAL -> changes little either way (a weak, optional satellite at best)
|
||||||
Score a NEW sleeve on THIS, not on absolute Sharpe."""
|
Score a NEW sleeve on THIS, not on absolute Sharpe.
|
||||||
|
|
||||||
|
Hardened 2026-06-21 (ortho wave): the fixed-HOLDOUT uplift + drop-month jackknife was
|
||||||
|
fooled (17/18 relative-value books 'ADDS' on a single 2025 ETH-bleed window). Three
|
||||||
|
gates added: (1) MULTI-CUT persistence (positive uplift at several hold-out starts, not
|
||||||
|
only 2025); (2) NOISE-NULL (uplift must beat a zero-corr random asset); (3) HEDGE vs
|
||||||
|
alpha (a low-corr sleeve that only helps when TP01 is down is a hedge)."""
|
||||||
B = tp01_baseline_daily()
|
B = tp01_baseline_daily()
|
||||||
J = pd.concat({"B": B, "C": cand_daily}, axis=1, join="inner").dropna()
|
J = pd.concat({"B": B, "C": cand_daily}, axis=1, join="inner").dropna()
|
||||||
if len(J) < 30:
|
if len(J) < 30:
|
||||||
@@ -378,12 +422,10 @@ def marginal_vs_tp01(cand_daily: pd.Series, weights=(0.25, 0.5)) -> dict:
|
|||||||
# the blend uplift to be positive in the earliest CLEAN hold-out year AND to survive a
|
# the blend uplift to be positive in the earliest CLEAN hold-out year AND to survive a
|
||||||
# drop-one-month jackknife. This is lesson #2 of the 2026-06-20 sweep, in code.
|
# drop-one-month jackknife. This is lesson #2 of the 2026-06-20 sweep, in code.
|
||||||
out["clean_year_uplift"] = out["jackknife_min_uplift"] = None
|
out["clean_year_uplift"] = out["jackknife_min_uplift"] = None
|
||||||
out["robust_oos"] = False
|
robust_h = False
|
||||||
if has_h:
|
if has_h:
|
||||||
ww = 0.25
|
|
||||||
|
|
||||||
def _u(sub):
|
def _u(sub):
|
||||||
return _sh((1 - ww) * sub["B"] + ww * sub["C"]) - _sh(sub["B"])
|
return _uplift_series(sub["B"], sub["C"])
|
||||||
yrs = sorted(set(JH.index.year))
|
yrs = sorted(set(JH.index.year))
|
||||||
clean = JH[JH.index.year == yrs[0]]
|
clean = JH[JH.index.year == yrs[0]]
|
||||||
cu = _u(clean) if len(clean) > 20 else None
|
cu = _u(clean) if len(clean) > 20 else None
|
||||||
@@ -392,17 +434,79 @@ def marginal_vs_tp01(cand_daily: pd.Series, weights=(0.25, 0.5)) -> dict:
|
|||||||
if len(months) > 1 else _u(JH))
|
if len(months) > 1 else _u(JH))
|
||||||
out["clean_year_uplift"] = round(cu, 3) if cu is not None else None
|
out["clean_year_uplift"] = round(cu, 3) if cu is not None else None
|
||||||
out["jackknife_min_uplift"] = round(jk, 3) if jk is not None else None
|
out["jackknife_min_uplift"] = round(jk, 3) if jk is not None else None
|
||||||
out["robust_oos"] = bool(cu is not None and cu > 0.02 and jk is not None and jk > 0.0)
|
robust_h = bool(cu is not None and cu > 0.02 and jk is not None and jk > 0.0)
|
||||||
# verdict (weight 0.25 = a satellite slot; hold-out is what the defensive stack cares about)
|
|
||||||
|
# --- GATE 1: MULTI-CUT PERSISTENCE -------------------------------------------------
|
||||||
|
# Uplift at the start of each year (not only the fixed HOLDOUT). A real edge adds at
|
||||||
|
# SEVERAL cuts incl. an early one; a regime artifact only adds at the latest window.
|
||||||
|
mc = {}
|
||||||
|
for y in sorted(set(J.index.year))[1:]:
|
||||||
|
sub = J[J.index >= pd.Timestamp(f"{y}-01-01", tz="UTC")]
|
||||||
|
if len(sub) >= 120:
|
||||||
|
mc[y] = round(_uplift_series(sub["B"], sub["C"]), 3)
|
||||||
|
out["multicut_uplift"] = mc
|
||||||
|
pos = [u for u in mc.values() if u > 0]
|
||||||
|
earliest = mc[min(mc)] if mc else None
|
||||||
|
multicut_persistent = bool(len(mc) >= 2 and len(pos) / len(mc) >= 0.6
|
||||||
|
and earliest is not None and earliest > 0.0)
|
||||||
|
out["multicut_persistent"] = multicut_persistent
|
||||||
|
|
||||||
|
# --- GATE 2: NOISE-NULL (uplift must beat a random zero-corr asset) -----------------
|
||||||
|
JI = J[J.index < HOLDOUT] # in-sample part (not the lucky recent window)
|
||||||
|
real_is, pctl_is = _null_uplift_pctl(JI["B"], JI["C"]) if len(JI) >= 60 else (None, None)
|
||||||
|
real_f, pctl_f = _null_uplift_pctl(J["B"], J["C"])
|
||||||
|
cand_is_sharpe = round(_sh(JI["C"]), 3) if len(JI) >= 60 else None
|
||||||
|
out["null_pctl_insample"] = pctl_is
|
||||||
|
out["null_pctl_full"] = pctl_f
|
||||||
|
out["cand_insample_sharpe"] = cand_is_sharpe
|
||||||
|
# A candidate must STAND ON ITS OWN before the hold-out: a real in-sample standalone
|
||||||
|
# Sharpe. The ortho basket's in-sample Sharpe was 0.29 -> its only "value" was the
|
||||||
|
# diversification math of a near-zero-Sharpe stream, dressed up by the lucky 2025 window.
|
||||||
|
# (null_pctl_* are reported as the diversification-math context: a low-corr asset adds
|
||||||
|
# ~+0.03 Sharpe by math, so pctl~0.5 just means "no TP01-specific timing" — true of GOOD
|
||||||
|
# and BAD uncorrelated sleeves alike, so it can't be the gate. The in-sample edge is.)
|
||||||
|
has_insample_edge = (cand_is_sharpe is None) or (cand_is_sharpe >= 0.5)
|
||||||
|
out["has_insample_edge"] = bool(has_insample_edge)
|
||||||
|
out["beats_noise_null"] = bool(has_insample_edge) # back-compat alias for the gate
|
||||||
|
|
||||||
|
# --- GATE 3: HEDGE vs ALPHA (does it only pay when TP01 is weak?) -------------------
|
||||||
|
yr_sh, yr_up = [], []
|
||||||
|
for y in sorted(set(J.index.year)):
|
||||||
|
sub = J[J.index.year == y]
|
||||||
|
if len(sub) >= 40:
|
||||||
|
yr_sh.append(_sh(sub["B"])); yr_up.append(_uplift_series(sub["B"], sub["C"]))
|
||||||
|
hedge_corr = (round(float(np.corrcoef(yr_sh, yr_up)[0, 1]), 3)
|
||||||
|
if len(yr_sh) >= 3 and np.std(yr_sh) > 0 and np.std(yr_up) > 0 else None)
|
||||||
|
trail = J["B"].rolling(60, min_periods=20).sum().shift(1)
|
||||||
|
up_seg, dn_seg = J[trail > 0], J[trail <= 0]
|
||||||
|
u_up = _uplift_series(up_seg["B"], up_seg["C"]) if len(up_seg) > 30 else None
|
||||||
|
u_dn = _uplift_series(dn_seg["B"], dn_seg["C"]) if len(dn_seg) > 30 else None
|
||||||
|
out["hedge_yearly_corr"] = hedge_corr
|
||||||
|
out["uplift_tp01_up"] = round(u_up, 3) if u_up is not None else None
|
||||||
|
out["uplift_tp01_down"] = round(u_dn, 3) if u_dn is not None else None
|
||||||
|
is_hedge = bool(hedge_corr is not None and hedge_corr < -0.5
|
||||||
|
and u_up is not None and u_up <= 0.0
|
||||||
|
and u_dn is not None and u_dn > 0.05)
|
||||||
|
out["is_hedge"] = is_hedge
|
||||||
|
|
||||||
|
# robust_oos now REQUIRES multi-cut persistence (kills the single-window winners)
|
||||||
|
out["robust_oos"] = bool(robust_h and multicut_persistent)
|
||||||
|
|
||||||
|
# --- VERDICT ----------------------------------------------------------------------
|
||||||
up_h = blends["w25"]["uplift_hold"]
|
up_h = blends["w25"]["uplift_hold"]
|
||||||
up_f = blends["w25"]["uplift_full"]
|
up_f = blends["w25"]["uplift_full"]
|
||||||
ch = out["corr_hold"] if out["corr_hold"] is not None else out["corr_full"]
|
ch = out["corr_hold"] if out["corr_hold"] is not None else out["corr_full"]
|
||||||
if out["corr_full"] > 0.9 and (up_h is None or abs(up_h) < 0.05):
|
if out["corr_full"] > 0.9 and (up_h is None or abs(up_h) < 0.05):
|
||||||
v = "REDUNDANT"
|
v = "REDUNDANT"
|
||||||
elif up_h is not None and up_h >= 0.05 and up_f > -0.15 and ch < 0.85:
|
|
||||||
v = "ADDS"
|
|
||||||
elif up_f <= -0.10 and (up_h is None or up_h <= 0.0):
|
elif up_f <= -0.10 and (up_h is None or up_h <= 0.0):
|
||||||
v = "DILUTES"
|
v = "DILUTES"
|
||||||
|
elif is_hedge:
|
||||||
|
v = "HEDGE"
|
||||||
|
elif not has_insample_edge:
|
||||||
|
v = "NOISE"
|
||||||
|
elif (up_h is not None and up_h >= 0.05 and up_f > -0.15 and ch < 0.85
|
||||||
|
and multicut_persistent):
|
||||||
|
v = "ADDS"
|
||||||
else:
|
else:
|
||||||
v = "NEUTRAL"
|
v = "NEUTRAL"
|
||||||
out["marginal_verdict"] = v
|
out["marginal_verdict"] = v
|
||||||
@@ -416,8 +520,12 @@ def study_marginal(name: str, target_fn, tf: str = "1d", fee_side: float = FEE_S
|
|||||||
absolute = study_weights(name, target_fn, tfs=(tf,))
|
absolute = study_weights(name, target_fn, tfs=(tf,))
|
||||||
marg = marginal_vs_tp01(candidate_daily(target_fn, tf=tf, fee_side=fee_side))
|
marg = marginal_vs_tp01(candidate_daily(target_fn, tf=tf, fee_side=fee_side))
|
||||||
abs_grade = absolute["verdict"]["grade"]
|
abs_grade = absolute["verdict"]["grade"]
|
||||||
|
# ADDS already embeds multi-cut + beats-null + not-hedge; we also require robust_oos
|
||||||
|
# (multi-cut robustness) explicitly. A HEDGE/NOISE/NEUTRAL never earns a live slot.
|
||||||
earns_slot = (abs_grade != "FAIL" and marg.get("marginal_verdict") == "ADDS"
|
earns_slot = (abs_grade != "FAIL" and marg.get("marginal_verdict") == "ADDS"
|
||||||
and marg.get("robust_oos", False))
|
and marg.get("robust_oos", False)
|
||||||
|
and marg.get("beats_noise_null", False)
|
||||||
|
and not marg.get("is_hedge", False))
|
||||||
return dict(name=name, tf=tf, absolute=absolute, marginal=marg,
|
return dict(name=name, tf=tf, absolute=absolute, marginal=marg,
|
||||||
abs_grade=abs_grade, marginal_verdict=marg.get("marginal_verdict"),
|
abs_grade=abs_grade, marginal_verdict=marg.get("marginal_verdict"),
|
||||||
earns_slot=earns_slot)
|
earns_slot=earns_slot)
|
||||||
@@ -432,6 +540,13 @@ def fmt_marginal(rep: dict) -> str:
|
|||||||
f"beta {m.get('beta_to_tp01')} resid Sharpe {m.get('resid_sharpe_full')} alpha/yr {m.get('alpha_ann')}")
|
f"beta {m.get('beta_to_tp01')} resid Sharpe {m.get('resid_sharpe_full')} alpha/yr {m.get('alpha_ann')}")
|
||||||
lines.append(f" OOS robustness: clean-year uplift {m.get('clean_year_uplift')} "
|
lines.append(f" OOS robustness: clean-year uplift {m.get('clean_year_uplift')} "
|
||||||
f"drop-best-month {m.get('jackknife_min_uplift')} robust_oos={m.get('robust_oos')}")
|
f"drop-best-month {m.get('jackknife_min_uplift')} robust_oos={m.get('robust_oos')}")
|
||||||
|
lines.append(f" multi-cut persistence: {m.get('multicut_uplift')} persistent={m.get('multicut_persistent')}")
|
||||||
|
lines.append(f" in-sample edge: standalone Sharpe {m.get('cand_insample_sharpe')} "
|
||||||
|
f"has_insample_edge={m.get('has_insample_edge')} "
|
||||||
|
f"(diversification-math null pctl in-sample {m.get('null_pctl_insample')} full {m.get('null_pctl_full')})")
|
||||||
|
lines.append(f" hedge check: yearly corr(TP01-Sh, uplift) {m.get('hedge_yearly_corr')} "
|
||||||
|
f"uplift TP01-up {m.get('uplift_tp01_up')} / TP01-down {m.get('uplift_tp01_down')} "
|
||||||
|
f"is_hedge={m.get('is_hedge')}")
|
||||||
lines.append(f" standalone: TP01 full {m.get('tp01_full_sharpe')}/hold {m.get('tp01_hold_sharpe')} | "
|
lines.append(f" standalone: TP01 full {m.get('tp01_full_sharpe')}/hold {m.get('tp01_hold_sharpe')} | "
|
||||||
f"cand full {m.get('cand_full_sharpe')}/hold {m.get('cand_hold_sharpe')}")
|
f"cand full {m.get('cand_full_sharpe')}/hold {m.get('cand_hold_sharpe')}")
|
||||||
for w, d in bl.items():
|
for w, d in bl.items():
|
||||||
@@ -442,6 +557,209 @@ def fmt_marginal(rep: dict) -> str:
|
|||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# HARNESS REALISM — two gates codified from the 2026-06-21 intraday wave.
|
||||||
|
#
|
||||||
|
# LESSON 1 (day-boundary): open_drive ("first 8h UTC predicts rest-of-day") scored a
|
||||||
|
# +0.23 uplift but INVERTED to -0.10 when the UTC day start was shifted 4h — a calendar-
|
||||||
|
# LABELING artifact, not an intraday effect. A real hour/session/day edge degrades
|
||||||
|
# gracefully under a boundary shift; an artifact flips sign.
|
||||||
|
#
|
||||||
|
# LESSON 2 (small-cap fills): eval_weights charges fee on EVERY |Δposition|, incl. the
|
||||||
|
# thousands of sub-dollar rebalances a vol-target overlay produces. At ~$600 real capital a
|
||||||
|
# $0.03 trade can't execute — the modeled proportional fee is a continuous-rebalancing
|
||||||
|
# fiction. eval_weights_smallcap skips changes below min_order and reports the Sharpe haircut.
|
||||||
|
# ===========================================================================
|
||||||
|
def _shift_calendar(df: pd.DataFrame, offset_hours: int) -> pd.DataFrame:
|
||||||
|
"""Relabel the clock the SIGNAL sees by +offset_hours (datetime & timestamp), leaving
|
||||||
|
prices/returns untouched -> the signal's .dt.hour / day-grouping shifts, the backtest
|
||||||
|
does not. (get() is cached; copy so we never mutate the shared frame.)"""
|
||||||
|
d = df.copy()
|
||||||
|
dt = pd.to_datetime(d["datetime"], utc=True) + pd.Timedelta(hours=offset_hours)
|
||||||
|
d["datetime"] = dt
|
||||||
|
if "timestamp" in d:
|
||||||
|
d["timestamp"] = d["timestamp"].astype("int64") + int(offset_hours * 3600 * 1000)
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
def day_boundary_robust(target_fn, tf: str = "1h",
|
||||||
|
offsets=(0, 3, 6, 9, 12, 15, 18, 21), w: float = 0.25) -> dict:
|
||||||
|
"""Is a candidate's marginal uplift ROBUST to shifting the UTC day boundary? For each
|
||||||
|
offset we relabel the calendar the signal sees, recompute its 50/50 BTC+ETH daily series
|
||||||
|
and the blend uplift vs TP01. A datetime-independent signal is INVARIANT (spread ~0); a
|
||||||
|
calendar signal that stays positive is ROBUST; one whose uplift flips sign is ARTIFACT-RISK
|
||||||
|
(open_drive). Run this on ANY hour/session/day-of-week signal before believing it."""
|
||||||
|
B = tp01_baseline_daily()
|
||||||
|
per = {}
|
||||||
|
for off in offsets:
|
||||||
|
series = {}
|
||||||
|
for a in CERTIFIED:
|
||||||
|
df0 = get(a, tf) # ORIGINAL bars/dates
|
||||||
|
tgt = _call_target(target_fn, _shift_calendar(df0, off), a) # signal sees shifted clock
|
||||||
|
ev = eval_weights(df0, tgt) # backtest on the real calendar
|
||||||
|
series[a] = pd.Series(ev["net"], index=ev["idx"])
|
||||||
|
J = pd.concat(series, axis=1, join="inner").fillna(0.0)
|
||||||
|
cand = _to_daily(0.5 * J[CERTIFIED[0]] + 0.5 * J[CERTIFIED[1]])
|
||||||
|
JJ = pd.concat({"B": B, "C": cand}, axis=1, join="inner").dropna()
|
||||||
|
per[int(off)] = round(_sh((1 - w) * JJ["B"] + w * JJ["C"]) - _sh(JJ["B"]), 3) if len(JJ) > 30 else None
|
||||||
|
ups = [v for v in per.values() if v is not None]
|
||||||
|
if not ups:
|
||||||
|
return dict(per_offset=per, verdict="N/A", reason="no evaluable offsets")
|
||||||
|
spread = round(max(ups) - min(ups), 3)
|
||||||
|
calendar_sensitive = spread > 0.02
|
||||||
|
robust = min(ups) > 0
|
||||||
|
verdict = ("INVARIANT" if not calendar_sensitive else ("ROBUST" if robust else "ARTIFACT-RISK"))
|
||||||
|
return dict(per_offset=per, base=per[offsets[0]], min=min(ups), max=max(ups),
|
||||||
|
spread=spread, calendar_sensitive=calendar_sensitive,
|
||||||
|
robust_to_boundary=robust, verdict=verdict)
|
||||||
|
|
||||||
|
|
||||||
|
def eval_weights_smallcap(df: pd.DataFrame, target, capital: float = 600.0,
|
||||||
|
min_order: float = 5.0, fee_side: float = FEE_SIDE) -> dict:
|
||||||
|
"""Honest net at SMALL capital. A desired position change whose notional |Δw|*capital is
|
||||||
|
below min_order is NOT executed (held -> tracking error, no trade) — removing the
|
||||||
|
continuous-rebalancing fiction. Returns realistic vs modeled metrics, the Sharpe haircut,
|
||||||
|
and the number of trades that actually execute. (Applies to ANY sleeve at this capital,
|
||||||
|
TP01 included.)"""
|
||||||
|
c = df["close"].values.astype(float)
|
||||||
|
tgt = np.clip(np.nan_to_num(np.asarray(target, float)), -10, 10)
|
||||||
|
held = np.empty(len(tgt)); cur = 0.0; n_tr = 0
|
||||||
|
for i in range(len(tgt)):
|
||||||
|
if abs(tgt[i] - cur) * capital >= min_order:
|
||||||
|
cur = tgt[i]; n_tr += 1
|
||||||
|
held[i] = cur
|
||||||
|
r = simple_returns(c)
|
||||||
|
pos = np.zeros(len(held)); pos[1:] = held[:-1]
|
||||||
|
turn = np.abs(np.diff(pos, prepend=0.0))
|
||||||
|
net = pos * r - fee_side * turn; net[0] = 0.0
|
||||||
|
idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True))
|
||||||
|
real = _metrics_from_net(net, idx)
|
||||||
|
modeled = eval_weights(df, tgt, fee_side=fee_side)["full"]
|
||||||
|
bpy_d = bars_per_day(df) * 365.25
|
||||||
|
return dict(realistic=real, modeled=modeled,
|
||||||
|
sharpe_haircut=round(modeled["sharpe"] - real["sharpe"], 3),
|
||||||
|
n_executed_trades=int(n_tr),
|
||||||
|
executed_turnover_per_year=round(float(turn.sum() / (len(turn) / bpy_d)), 1))
|
||||||
|
|
||||||
|
|
||||||
|
def causality_ok(target_fn, tf: str = "1h", assets=CERTIFIED,
|
||||||
|
tail: int = 80, tol: float = 1e-3) -> dict:
|
||||||
|
"""Online-consistency / LOOK-AHEAD guard for a continuous target_fn(df) [or (df, asset)].
|
||||||
|
eval_weights SHIFTS the position so you cannot leak by multiplying a weight by the SAME
|
||||||
|
bar's return — but it does NOT verify the FEATURE construction is causal: a centered
|
||||||
|
window, a .shift(-k), or a full-sample statistic would pass eval_weights yet peek at the
|
||||||
|
future. Here we recompute the target on a TRUNCATED prefix and require its tail to MATCH
|
||||||
|
target(full)[:cut] (the bars a deployable signal would have emitted in real time). Any
|
||||||
|
future-peeking diverges. Run this in every altlib-based lab (blind/ortho already do)."""
|
||||||
|
worst = 0.0; bad = False; checked = 0
|
||||||
|
for a in assets:
|
||||||
|
df = get(a, tf)
|
||||||
|
full = np.nan_to_num(np.asarray(_call_target(target_fn, df, a), float))
|
||||||
|
n = len(df)
|
||||||
|
for cut in (int(n * 0.80), int(n * 0.92)):
|
||||||
|
if cut <= tail + 5 or cut >= n:
|
||||||
|
continue
|
||||||
|
sub = df.iloc[:cut].reset_index(drop=True)
|
||||||
|
s = np.nan_to_num(np.asarray(_call_target(target_fn, sub, a), float))
|
||||||
|
if len(s) != cut:
|
||||||
|
bad = True
|
||||||
|
continue
|
||||||
|
d = np.abs(s[cut - tail:cut] - full[cut - tail:cut])
|
||||||
|
worst = max(worst, float(np.max(d)) if len(d) else 0.0)
|
||||||
|
checked += 1
|
||||||
|
return dict(ok=bool((not bad) and worst <= tol),
|
||||||
|
max_tail_diff=round(worst, 8), checked=checked,
|
||||||
|
reason=("length-mismatch on prefix" if bad else None))
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# SELECTION-ON-HOLDOUT GATE — codified 2026-06-29 from filone B (intraday ERM).
|
||||||
|
#
|
||||||
|
# LESSON 3: intraday_regime.py picked its "winner" cell by MAX hold-out Sharpe over a ~60-cell
|
||||||
|
# grid, then ran study_marginal on THAT cell -> earns_slot=True. But the slot was an artifact of
|
||||||
|
# SELECTING THE CELL ON THE HOLD-OUT: picking the cell IN-SAMPLE-ONLY (no peeking) lands on a
|
||||||
|
# DIFFERENT, TP01-correlated cell that scores NEUTRAL, and the standalone Sharpe deflates to
|
||||||
|
# DSR~0.0-0.24 over the trials searched. study_marginal alone can't catch this — it judges ONE
|
||||||
|
# stream and never sees how the cell was chosen. The fix is two-fold and lives here:
|
||||||
|
# (1) choose the cell IN-SAMPLE-ONLY (or walk-forward) BEFORE scoring the marginal, and
|
||||||
|
# (2) DEFLATE the standalone Sharpe for the number of cells/families searched.
|
||||||
|
# Twin of the CC01 ("implausible Sharpe -> hidden risk") and alt-sweep ("hold-out-fitting") blind
|
||||||
|
# spots, in its "selection-on-holdout" form.
|
||||||
|
# ===========================================================================
|
||||||
|
def deflated_sharpe(sr_ann, all_sr_ann, daily_ret, dpy: float = 365.25):
|
||||||
|
"""Deflated Sharpe Ratio (Bailey & Lopez de Prado): P(true Sharpe > the MAX Sharpe expected
|
||||||
|
under the null of N independent trials). Penalizes multiple-testing — a standalone Sharpe ~1
|
||||||
|
over a 100+ cell grid is routinely NOT significant once deflated. sr_ann = annualized Sharpe
|
||||||
|
of the CHOSEN config; all_sr_ann = the Sharpe of EVERY cell searched; daily_ret = the chosen
|
||||||
|
config's daily returns (for skew/kurt/T). Returns (DSR, expected_null_max_sharpe_ann);
|
||||||
|
PASS if DSR >= 0.95."""
|
||||||
|
r = np.asarray(pd.Series(daily_ret).dropna().values, float)
|
||||||
|
T = len(r)
|
||||||
|
if T < 30 or np.std(r) == 0:
|
||||||
|
return float("nan"), float("nan")
|
||||||
|
sr = sr_ann / math.sqrt(dpy)
|
||||||
|
trials = np.asarray([s / math.sqrt(dpy) for s in all_sr_ann if np.isfinite(s)], float)
|
||||||
|
N = max(len(trials), 2)
|
||||||
|
var_tr = float(np.var(trials, ddof=1)) if N > 1 else 0.0
|
||||||
|
emc = 0.5772156649
|
||||||
|
z1 = norm.ppf(1 - 1.0 / N)
|
||||||
|
z2 = norm.ppf(1 - 1.0 / (N * math.e))
|
||||||
|
sr0 = math.sqrt(var_tr) * ((1 - emc) * z1 + emc * z2)
|
||||||
|
sk = float(pd.Series(r).skew())
|
||||||
|
ku = float(pd.Series(r).kurt()) + 3.0 # pandas kurt = excess
|
||||||
|
den = math.sqrt(max(1e-9, 1 - sk * sr + (ku - 1) / 4.0 * sr ** 2))
|
||||||
|
dsr = float(norm.cdf((sr - sr0) * math.sqrt(T - 1) / den))
|
||||||
|
return dsr, sr0 * math.sqrt(dpy)
|
||||||
|
|
||||||
|
|
||||||
|
def select_cell_insample(factory, grid, tfs, fee_side: float = FEE_SIDE) -> dict:
|
||||||
|
"""Pick a config WITHOUT looking at the hold-out: rank grid cells by IN-SAMPLE (pre-HOLDOUT)
|
||||||
|
standalone Sharpe of the 50/50 candidate. `factory(tf=..., **params)` -> target_fn; each grid
|
||||||
|
item is a dict of factory kwargs (besides tf). Returns the in-sample-best cell, all rows
|
||||||
|
(sorted), and EVERY cell's FULL Sharpe (for deflated_sharpe). This is the honest replacement
|
||||||
|
for picking the max-hold-out cell."""
|
||||||
|
rows = []
|
||||||
|
for tf in tfs:
|
||||||
|
for params in grid:
|
||||||
|
try:
|
||||||
|
daily = candidate_daily(factory(tf=tf, **params), tf=tf, fee_side=fee_side)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
ins = daily[daily.index < HOLDOUT]
|
||||||
|
is_sh = _sh(ins) if len(ins) > 60 else float("nan")
|
||||||
|
rows.append(dict(tf=tf, params=params, insample_sharpe=round(is_sh, 3),
|
||||||
|
full_sharpe=round(_sh(daily), 3)))
|
||||||
|
valid = [r for r in rows if np.isfinite(r["insample_sharpe"])]
|
||||||
|
chosen = max(valid, key=lambda r: r["insample_sharpe"]) if valid else None
|
||||||
|
return dict(chosen=chosen,
|
||||||
|
rows=sorted(valid, key=lambda r: r["insample_sharpe"], reverse=True),
|
||||||
|
all_full_sharpe=[r["full_sharpe"] for r in rows])
|
||||||
|
|
||||||
|
|
||||||
|
def study_family_honest(name: str, factory, grid, tfs, fee_side: float = FEE_SIDE,
|
||||||
|
dsr_min: float = 0.95) -> dict:
|
||||||
|
"""HARDENED family gate. A grid-searched directional candidate earns a slot ONLY if, picking
|
||||||
|
the cell IN-SAMPLE-ONLY (no hold-out peeking), it STILL earns_slot via study_marginal AND its
|
||||||
|
standalone Sharpe survives deflation for the WHOLE grid searched. Use this INSTEAD of
|
||||||
|
cherry-picking the max-hold cell and calling study_marginal on it."""
|
||||||
|
sel = select_cell_insample(factory, grid, tfs, fee_side=fee_side)
|
||||||
|
ch = sel["chosen"]
|
||||||
|
if ch is None:
|
||||||
|
return dict(name=name, chosen=None, earns_slot_honest=False,
|
||||||
|
reason="no valid in-sample cell")
|
||||||
|
fn = factory(tf=ch["tf"], **ch["params"])
|
||||||
|
sm = study_marginal(f"{name} ISpick {ch['params']}", fn, tf=ch["tf"], fee_side=fee_side)
|
||||||
|
daily = candidate_daily(fn, tf=ch["tf"], fee_side=fee_side)
|
||||||
|
dsr, sr0 = deflated_sharpe(_sh(daily), sel["all_full_sharpe"], daily)
|
||||||
|
dsr_pass = bool(np.isfinite(dsr) and dsr >= dsr_min)
|
||||||
|
return dict(name=name, n_cells=len(sel["all_full_sharpe"]), chosen=ch, rows=sel["rows"],
|
||||||
|
marginal=sm, earns_slot_marginal=bool(sm["earns_slot"]),
|
||||||
|
deflated_sharpe=round(dsr, 3) if np.isfinite(dsr) else None,
|
||||||
|
expected_null_max=round(sr0, 3) if np.isfinite(sr0) else None,
|
||||||
|
dsr_pass=dsr_pass,
|
||||||
|
earns_slot_honest=bool(sm["earns_slot"] and dsr_pass))
|
||||||
|
|
||||||
|
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
# DRIVERS — run a hypothesis across both assets, several TFs, with a fee sweep.
|
# DRIVERS — run a hypothesis across both assets, several TFs, with a fee sweep.
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"oos_benchmark_buyhold": {"pnl": -0.07, "maxdd": 0.68, "sharpe": 0.22},
|
||||||
|
"top_survivors_oos": {
|
||||||
|
"agent_04_macd": {"pnl_A": 0.23, "pnl_B": 0.19, "maxdd": 0.11, "sharpe_min": 0.84, "corr_to_trend": 0.52},
|
||||||
|
"agent_06_accel": {"pnl_A": 0.40, "pnl_B": 0.22, "maxdd": 0.12, "sharpe_min": 0.79, "corr_to_trend": 0.50},
|
||||||
|
"agent_23_vol_of_vol":{"pnl_A": 0.30, "pnl_B": 0.32, "maxdd": 0.21, "sharpe_min": 0.69, "corr_to_trend": 0.46},
|
||||||
|
"agent_44_obv": {"pnl_A": 0.22, "pnl_B": 0.20, "maxdd": 0.16, "sharpe_min": 0.60, "corr_to_trend": 0.31}
|
||||||
|
},
|
||||||
|
"skeptic_regime_luck": "REFUTED x3 - top-5 of ~800 OOS bars supply 67-102% of PnL; drop-10 turns negative; accel COMB final-third Sharpe -1.21; A & B disagree on WHEN it works.",
|
||||||
|
"skeptic_trend_redundancy": "REFUTED x4 - Newey-West HAC alpha t-stats +0.92..+1.51 (none > 1.96); corr-to-trend 0.34-0.74, beta 0.45-0.73; residual mean +0.05-0.08/yr = noise. Better-tuned TSMOM, not orthogonal alpha.",
|
||||||
|
"skeptic_overfit": "MACD not-refuted (genuine one-axis plateau, OOS Sh 0.84 not train 1.40); ACCEL REFUTED (acceleration term HURTS OOS, LAG knife-edge -63% on -20%); VOV REFUTED (PCTL 0.80->0.60 destroys 73% of OOS Sharpe).",
|
||||||
|
"verdict": "52 blind agents, orchestrator scored all on OOS PnL & maxDD. NOTHING new survives. All winners are trend-beta of two up-trending curves; OOS Sharpe ceiling ~0.84 (decayed from train ~1.4); no statistically distinguishable alpha vs TSMOM. Independent BLIND re-confirmation of the project's ~1.3 directional ceiling. macd = least-bad, TP01-class, forward-monitor not deploy."
|
||||||
|
}
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
"""Adversarial parameter-perturbation harness for the 3 blind survivors.
|
||||||
|
Re-implements each signal parameterized; perturbs each key param +/-25% (and larger
|
||||||
|
jumps), re-evaluates OOS (test slice, A & B) and train. Reports min/median/max OOS
|
||||||
|
Sharpe across the grid and the train->test Sharpe decay. Also a fee bump to 0.20% RT.
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/blind")
|
||||||
|
import blindlib as bl
|
||||||
|
|
||||||
|
FEE_BASE = 0.0005 # 0.10% RT
|
||||||
|
FEE_BUMP = 0.001 # 0.20% RT
|
||||||
|
|
||||||
|
|
||||||
|
def _masks(series):
|
||||||
|
df = bl.load(series, "full")
|
||||||
|
cut = bl.split_cut(series)
|
||||||
|
test = np.zeros(len(df), bool); test[cut:] = True
|
||||||
|
train = np.zeros(len(df), bool); train[:cut] = True
|
||||||
|
return df, train, test
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------- agent_04 MACD ----------------
|
||||||
|
def macd_signal(df, FAST=26, SLOW=52, SIGNAL=9, SLOPE_W=0.20, SHORT_W=0.5,
|
||||||
|
TARGET_VOL=0.20, VOL_WIN=30, LEV_CAP=1.0):
|
||||||
|
c = df["close"].values.astype(float)
|
||||||
|
macd = bl.ema(c, FAST) - bl.ema(c, SLOW)
|
||||||
|
signal_line = bl.ema(macd, SIGNAL)
|
||||||
|
hist = macd - signal_line
|
||||||
|
base = np.where(np.sign(hist) == np.sign(macd), np.sign(macd), 0.0)
|
||||||
|
slope = np.sign(np.diff(hist, prepend=hist[0]))
|
||||||
|
raw = (1.0 - SLOPE_W) * base + SLOPE_W * slope
|
||||||
|
raw = np.clip(raw, -1.0, 1.0)
|
||||||
|
raw = np.where(raw < 0, raw * SHORT_W, raw)
|
||||||
|
raw = np.nan_to_num(raw, nan=0.0)
|
||||||
|
pos = bl.vol_target(raw, df, target_vol=TARGET_VOL, vol_win_days=VOL_WIN,
|
||||||
|
leverage_cap=LEV_CAP)
|
||||||
|
return np.clip(pos, -1.0, 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------- agent_06 accel ----------------
|
||||||
|
def _lagged_diff(x, lag):
|
||||||
|
out = np.zeros(len(x))
|
||||||
|
if lag < len(x):
|
||||||
|
out[lag:] = x[lag:] - x[:-lag]
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def accel_signal(df, FAST=28, LAG=30, Z_WIN=200, KV=1.5, KA=1.5, W_VEL=0.4,
|
||||||
|
W_ACC=0.6, SHORT_W=0.0, TARGET_VOL=0.27, VOL_WIN=25, LEV_CAP=1.5):
|
||||||
|
c = df["close"].values.astype(float)
|
||||||
|
lr = np.zeros(len(c)); lr[1:] = np.log(c[1:] / c[:-1])
|
||||||
|
vel = bl.ema(lr, FAST)
|
||||||
|
acc = _lagged_diff(vel, LAG)
|
||||||
|
zv = np.nan_to_num(bl.zscore(vel, Z_WIN), nan=0.0)
|
||||||
|
za = np.nan_to_num(bl.zscore(acc, Z_WIN), nan=0.0)
|
||||||
|
raw = W_VEL * np.tanh(KV * zv) + W_ACC * np.tanh(KA * za)
|
||||||
|
raw = np.clip(raw, -1.0, 1.0)
|
||||||
|
raw = np.where(raw >= 0.0, raw, raw * SHORT_W)
|
||||||
|
pos = bl.vol_target(raw, df, target_vol=TARGET_VOL, vol_win_days=VOL_WIN,
|
||||||
|
leverage_cap=LEV_CAP)
|
||||||
|
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------- agent_23 vol_of_vol ----------------
|
||||||
|
def _expanding_pctl_rank(x, min_hist):
|
||||||
|
n = len(x); rank = np.full(n, np.nan); seen = []
|
||||||
|
for i in range(n):
|
||||||
|
v = x[i]
|
||||||
|
if np.isfinite(v):
|
||||||
|
seen.append(v)
|
||||||
|
if len(seen) >= min_hist:
|
||||||
|
rank[i] = float(np.mean(np.asarray(seen) <= v))
|
||||||
|
return rank
|
||||||
|
|
||||||
|
|
||||||
|
def _tsmom_sign(c, h):
|
||||||
|
out = np.zeros(len(c))
|
||||||
|
if h < len(c):
|
||||||
|
out[h:] = np.sign(c[h:] / c[:-h] - 1.0)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _vol_of_vol(rv, win):
|
||||||
|
rv_s = pd.Series(rv)
|
||||||
|
logrv = np.log(rv_s.where(rv_s > 0))
|
||||||
|
dlog = logrv.diff()
|
||||||
|
return dlog.rolling(win, min_periods=max(5, win // 2)).std().values
|
||||||
|
|
||||||
|
|
||||||
|
def vov_signal(df, RV_WIN=30, VOV_WIN=40, PCTL=0.80, HORIZONS=(25, 60, 120),
|
||||||
|
TARGET_VOL=0.22, VOL_WIN=45, LEV_CAP=1.5, MIN_HIST=60):
|
||||||
|
c = df["close"].values.astype(float)
|
||||||
|
bpy = bl.bars_per_day(df) * 365.25
|
||||||
|
rv = bl.realized_vol(bl.simple_returns(c), RV_WIN, bpy)
|
||||||
|
vov = _vol_of_vol(rv, VOV_WIN)
|
||||||
|
rank = _expanding_pctl_rank(vov, MIN_HIST)
|
||||||
|
stable = np.isfinite(rank) & (rank <= PCTL)
|
||||||
|
sig = np.zeros(len(c))
|
||||||
|
for h in HORIZONS:
|
||||||
|
sig += _tsmom_sign(c, h)
|
||||||
|
sig /= len(HORIZONS)
|
||||||
|
raw = np.where(stable, sig, 0.0)
|
||||||
|
pos = bl.vol_target(raw, df, target_vol=TARGET_VOL, vol_win_days=VOL_WIN,
|
||||||
|
leverage_cap=LEV_CAP)
|
||||||
|
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
def score(sig_fn, kwargs, fee=FEE_BASE):
|
||||||
|
"""Return dict of train & test sharpe/pnl, averaged over A&B (min/mean)."""
|
||||||
|
out = {}
|
||||||
|
for s in ("A", "B"):
|
||||||
|
df, train, test = _masks(s)
|
||||||
|
tgt = sig_fn(df, **kwargs)
|
||||||
|
rtr = bl.eval_target(df, tgt, fee_side=fee, metric_mask=train)
|
||||||
|
rte = bl.eval_target(df, tgt, fee_side=fee, metric_mask=test)
|
||||||
|
out[s] = dict(tr_sh=rtr["sharpe"], tr_pnl=rtr["pnl"],
|
||||||
|
te_sh=rte["sharpe"], te_pnl=rte["pnl"], te_dd=rte["maxdd"])
|
||||||
|
# combined: min across A,B (the agents tuned on sharpe_min)
|
||||||
|
te_sh_min = min(out["A"]["te_sh"], out["B"]["te_sh"])
|
||||||
|
tr_sh_min = min(out["A"]["tr_sh"], out["B"]["tr_sh"])
|
||||||
|
te_sh_mean = 0.5 * (out["A"]["te_sh"] + out["B"]["te_sh"])
|
||||||
|
te_pnl_mean = 0.5 * (out["A"]["te_pnl"] + out["B"]["te_pnl"])
|
||||||
|
return dict(out=out, te_sh_min=te_sh_min, tr_sh_min=tr_sh_min,
|
||||||
|
te_sh_mean=te_sh_mean, te_pnl_mean=te_pnl_mean)
|
||||||
|
|
||||||
|
|
||||||
|
def perturb_grid(sig_fn, base, grid):
|
||||||
|
"""grid: {param: [values]}. Sweep one param at a time around base."""
|
||||||
|
base_sc = score(sig_fn, base)
|
||||||
|
rows = []
|
||||||
|
for p, vals in grid.items():
|
||||||
|
for v in vals:
|
||||||
|
kw = dict(base); kw[p] = v
|
||||||
|
sc = score(sig_fn, kw)
|
||||||
|
rows.append(dict(param=p, val=v, te_sh_min=sc["te_sh_min"],
|
||||||
|
te_sh_mean=round(sc["te_sh_mean"], 3),
|
||||||
|
te_pnl_mean=round(sc["te_pnl_mean"], 3),
|
||||||
|
tr_sh_min=sc["tr_sh_min"]))
|
||||||
|
return base_sc, rows
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import json
|
||||||
|
pd.set_option("display.width", 160)
|
||||||
|
pd.set_option("display.max_rows", 300)
|
||||||
|
|
||||||
|
print("="*70)
|
||||||
|
print("AGENT 04 — MACD")
|
||||||
|
print("="*70)
|
||||||
|
base04 = dict(FAST=26, SLOW=52, SIGNAL=9, SLOPE_W=0.20, SHORT_W=0.5,
|
||||||
|
TARGET_VOL=0.20, VOL_WIN=30, LEV_CAP=1.0)
|
||||||
|
b, rows = perturb_grid(macd_signal, base04, dict(
|
||||||
|
FAST=[20, 22, 26, 30, 32, 39], # +/-25% + bigger
|
||||||
|
SLOW=[39, 45, 52, 60, 65, 78],
|
||||||
|
SIGNAL=[5, 7, 9, 11, 13, 18],
|
||||||
|
SLOPE_W=[0.10, 0.15, 0.20, 0.25, 0.30, 0.40],
|
||||||
|
SHORT_W=[0.0, 0.25, 0.375, 0.5, 0.625, 0.75, 1.0],
|
||||||
|
VOL_WIN=[15, 22, 30, 38, 45, 60],
|
||||||
|
TARGET_VOL=[0.15, 0.20, 0.25, 0.30],
|
||||||
|
))
|
||||||
|
print("BASE:", json.dumps({k: b[k] for k in ("tr_sh_min","te_sh_min","te_sh_mean","te_pnl_mean")}))
|
||||||
|
print(" per-series base:", b["out"])
|
||||||
|
print(pd.DataFrame(rows).to_string(index=False))
|
||||||
|
|
||||||
|
print("\n" + "="*70)
|
||||||
|
print("AGENT 06 — ACCEL")
|
||||||
|
print("="*70)
|
||||||
|
base06 = dict(FAST=28, LAG=30, Z_WIN=200, KV=1.5, KA=1.5, W_VEL=0.4,
|
||||||
|
W_ACC=0.6, SHORT_W=0.0, TARGET_VOL=0.27, VOL_WIN=25, LEV_CAP=1.5)
|
||||||
|
b, rows = perturb_grid(accel_signal, base06, dict(
|
||||||
|
FAST=[21, 24, 28, 32, 35, 42],
|
||||||
|
LAG=[20, 26, 30, 36, 40, 50],
|
||||||
|
Z_WIN=[140, 160, 200, 240, 260, 320],
|
||||||
|
KV=[1.0, 1.2, 1.5, 1.8, 2.0, 3.0],
|
||||||
|
KA=[1.0, 1.2, 1.5, 1.8, 2.0, 3.0],
|
||||||
|
W_ACC=[0.3, 0.45, 0.6, 0.75, 0.9, 1.0],
|
||||||
|
TARGET_VOL=[0.18, 0.22, 0.27, 0.32],
|
||||||
|
VOL_WIN=[18, 22, 25, 30, 35],
|
||||||
|
))
|
||||||
|
print("BASE:", json.dumps({k: b[k] for k in ("tr_sh_min","te_sh_min","te_sh_mean","te_pnl_mean")}))
|
||||||
|
print(" per-series base:", b["out"])
|
||||||
|
print(pd.DataFrame(rows).to_string(index=False))
|
||||||
|
|
||||||
|
print("\n" + "="*70)
|
||||||
|
print("AGENT 23 — VOL_OF_VOL")
|
||||||
|
print("="*70)
|
||||||
|
base23 = dict(RV_WIN=30, VOV_WIN=40, PCTL=0.80, HORIZONS=(25, 60, 120),
|
||||||
|
TARGET_VOL=0.22, VOL_WIN=45, LEV_CAP=1.5, MIN_HIST=60)
|
||||||
|
b, rows = perturb_grid(vov_signal, base23, dict(
|
||||||
|
RV_WIN=[22, 26, 30, 34, 38, 45],
|
||||||
|
VOV_WIN=[30, 35, 40, 45, 50, 60],
|
||||||
|
PCTL=[0.60, 0.70, 0.76, 0.80, 0.84, 0.90, 1.00],
|
||||||
|
TARGET_VOL=[0.18, 0.22, 0.26, 0.30],
|
||||||
|
VOL_WIN=[34, 40, 45, 55, 60],
|
||||||
|
MIN_HIST=[40, 60, 90],
|
||||||
|
))
|
||||||
|
print("BASE:", json.dumps({k: b[k] for k in ("tr_sh_min","te_sh_min","te_sh_mean","te_pnl_mean")}))
|
||||||
|
print(" per-series base:", b["out"])
|
||||||
|
# horizons sweep separately (tuple param)
|
||||||
|
hz_rows = []
|
||||||
|
for hz in [(20,50,100),(25,60,120),(30,70,140),(20,40,80),(40,90,180),(15,30,60)]:
|
||||||
|
kw = dict(base23); kw["HORIZONS"] = hz
|
||||||
|
sc = score(vov_signal, kw)
|
||||||
|
hz_rows.append(dict(param="HORIZONS", val=str(hz), te_sh_min=sc["te_sh_min"],
|
||||||
|
te_sh_mean=round(sc["te_sh_mean"],3),
|
||||||
|
te_pnl_mean=round(sc["te_pnl_mean"],3), tr_sh_min=sc["tr_sh_min"]))
|
||||||
|
print(pd.DataFrame(rows + hz_rows).to_string(index=False))
|
||||||
|
|
||||||
|
# ---- FEE BUMP to 0.20% RT, base params ----
|
||||||
|
print("\n" + "="*70)
|
||||||
|
print("FEE BUMP 0.10% -> 0.20% RT (base params)")
|
||||||
|
print("="*70)
|
||||||
|
for name, fn, base in [("MACD", macd_signal, base04),
|
||||||
|
("ACCEL", accel_signal, base06),
|
||||||
|
("VOV", vov_signal, base23)]:
|
||||||
|
lo = score(fn, base, fee=FEE_BASE)
|
||||||
|
hi = score(fn, base, fee=FEE_BUMP)
|
||||||
|
print(f"{name:6s} te_sh_min {lo['te_sh_min']:+.3f} -> {hi['te_sh_min']:+.3f} | "
|
||||||
|
f"te_sh_mean {lo['te_sh_mean']:+.3f} -> {hi['te_sh_mean']:+.3f} | "
|
||||||
|
f"te_pnl_mean {lo['te_pnl_mean']:+.3f} -> {hi['te_pnl_mean']:+.3f}")
|
||||||
|
print(f" per-series @0.20%: A te_sh {score(fn,base,fee=FEE_BUMP)['out']['A']['te_sh']} "
|
||||||
|
f"B te_sh {score(fn,base,fee=FEE_BUMP)['out']['B']['te_sh']}")
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
"""TEMPLATE for a blind-signal agent. COPY this, rename, implement `signal`.
|
||||||
|
|
||||||
|
You are given two anonymized, overlaid price curves ("A" and "B"), rebased to 100.
|
||||||
|
You do NOT know what they are. Find a way to ANTICIPATE the next move.
|
||||||
|
|
||||||
|
Rules (enforced automatically — break them and you are disqualified):
|
||||||
|
* `signal(df)` returns float array len(df). position[i] in [-1,+1] = how much of
|
||||||
|
equity to hold during the NEXT bar (sign=long/short, 0=flat). The evaluator
|
||||||
|
shifts it -> you trade bar i+1 with a decision made at close[i].
|
||||||
|
* CAUSAL/ONLINE only: position[i] uses ONLY rows 0..i. No .shift(-k), no centered
|
||||||
|
windows, no fitting a model on the whole df then predicting the whole df.
|
||||||
|
If you train a model, use an EXPANDING/WALK-FORWARD scheme (refit using only
|
||||||
|
past rows) or fit once on an EARLY fixed warmup and freeze.
|
||||||
|
* Tune ONLY on split='train'. The held-out tail is scored by the orchestrator.
|
||||||
|
|
||||||
|
Score it:
|
||||||
|
uv run python scripts/research/blind/blind_eval.py --module <this file> --split train
|
||||||
|
Make sure the output has "causality": {"ok": true, ...}.
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
import blindlib as bl
|
||||||
|
|
||||||
|
|
||||||
|
def signal(df):
|
||||||
|
c = df["close"].values.astype(float)
|
||||||
|
# --- EXAMPLE: vol-targeted dual-timescale momentum (replace with your idea) ---
|
||||||
|
fast = c / bl.sma(c, 20) - 1.0
|
||||||
|
slow = c / bl.sma(c, 100) - 1.0
|
||||||
|
raw = np.sign(fast) * 0.5 + np.sign(slow) * 0.5 # -1..1 direction
|
||||||
|
pos = bl.vol_target(raw, df, target_vol=0.20, vol_win_days=30, leverage_cap=1.0)
|
||||||
|
return np.clip(pos, -1.0, 1.0)
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
"""agent_00_sma_trend — ANGLE: trend / single long SMA (long/flat).
|
||||||
|
|
||||||
|
Idea (assigned angle): go LONG only while price is meaningfully above a single long
|
||||||
|
simple moving average, otherwise FLAT. The long SMA defines the macro trend; staying
|
||||||
|
flat below it is what cuts the asset's ~77% buy&hold drawdown to ~1/3.
|
||||||
|
|
||||||
|
Tuned on split='train' only (both Series A and B, equal weight):
|
||||||
|
* window W = 150 (canonical long SMA; sits on a wide robust plateau W=135..165)
|
||||||
|
* band B = 0.02 (require close > 1.02*SMA -> avoids whipsaw chop near the line)
|
||||||
|
* vol-target the long exposure to 35% ann vol (vol_win=30d, cap 1.0). This is what
|
||||||
|
actually controls drawdown: long size shrinks when realized vol spikes (every
|
||||||
|
crypto-like crash is a vol spike), so we're never full-size into the worst bars.
|
||||||
|
|
||||||
|
Everything is causal: SMA(close[..i]), realized vol(returns[..i]). No future rows.
|
||||||
|
The evaluator shifts position by one bar (decision at close[i] -> held bar i+1).
|
||||||
|
|
||||||
|
Train (combined A&B): pnl_mean ~ 5.4, maxdd_worst ~ 0.30, sharpe_min ~ 1.36.
|
||||||
|
Honest note: this is a DEFENSIVE trend filter, not alpha — its value is converting a
|
||||||
|
high-PnL/high-DD uptrend into comparable risk-adjusted PnL at a MUCH smaller drawdown.
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
import blindlib as bl
|
||||||
|
|
||||||
|
W = 150 # single long SMA window
|
||||||
|
BAND = 0.02 # long only when close > (1+BAND)*SMA(W)
|
||||||
|
TARGET_VOL = 0.35
|
||||||
|
VOL_WIN_DAYS = 30
|
||||||
|
LEV_CAP = 1.0
|
||||||
|
|
||||||
|
|
||||||
|
def signal(df):
|
||||||
|
c = df["close"].values.astype(float)
|
||||||
|
sma = bl.sma(c, W) # causal SMA up to i
|
||||||
|
|
||||||
|
# long/flat gate vs the single long SMA, with a band to dodge whipsaw near the line
|
||||||
|
long_gate = np.where(c > sma * (1.0 + BAND), 1.0, 0.0)
|
||||||
|
long_gate[:W] = 0.0 # no signal before the SMA is defined
|
||||||
|
long_gate[~np.isfinite(sma)] = 0.0
|
||||||
|
|
||||||
|
# size the long with causal vol-targeting (shrinks into vol spikes -> cuts DD)
|
||||||
|
pos = bl.vol_target(long_gate, df, target_vol=TARGET_VOL,
|
||||||
|
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
|
||||||
|
|
||||||
|
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
"""Agent 01 — Dual EMA crossover (family=trend, slug=ema_cross).
|
||||||
|
|
||||||
|
The angle: long/short on the sign of (fast EMA - slow EMA). The two spans are the
|
||||||
|
core tuned knobs. One refinement that survived a plateau check on split='train':
|
||||||
|
the two anonymized curves are strongly up-trending, so a SYMMETRIC short is pure
|
||||||
|
drag (it shorts the dips of a bull market). We keep the long/short crossover but
|
||||||
|
size the SHORT side down by `SHORT_W` — still a genuine long/short EMA cross, just
|
||||||
|
risk-asymmetric. Direction is then vol-targeted (causal trailing window) so the two
|
||||||
|
curves are sized comparably and the drawdown stays bounded.
|
||||||
|
|
||||||
|
Tuning (train only): a broad plateau f in [18..30], s in [40..50], SHORT_W in
|
||||||
|
[0.1..0.3] all give sharpe_min ~1.3 / DD ~0.23. f=25, s=40, SHORT_W=0.25 sits in
|
||||||
|
the plateau interior (not on a grid edge) -> robust, not a lucky cell.
|
||||||
|
|
||||||
|
CAUSAL: ema(c, span) is an online recursion (value at i uses rows 0..i only);
|
||||||
|
vol_target uses a trailing vol window. No look-ahead, no centered windows, no
|
||||||
|
global fit. Verified by causality_ok (max_diff 0.0).
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
import blindlib as bl
|
||||||
|
|
||||||
|
# --- tuned ONLY on split='train' (plateau interior) ---
|
||||||
|
FAST_SPAN = 25
|
||||||
|
SLOW_SPAN = 40
|
||||||
|
SHORT_W = 0.25 # short side sized down (asymmetric L/S); 0 -> long-flat
|
||||||
|
TARGET_VOL = 0.20
|
||||||
|
VOL_WIN = 30
|
||||||
|
LEV_CAP = 1.0
|
||||||
|
|
||||||
|
|
||||||
|
def signal(df):
|
||||||
|
c = df["close"].values.astype(float)
|
||||||
|
fast = bl.ema(c, FAST_SPAN)
|
||||||
|
slow = bl.ema(c, SLOW_SPAN)
|
||||||
|
# +1 when fast above slow, -SHORT_W when below: genuine EMA-cross direction,
|
||||||
|
# short side de-weighted because the curves are persistently up-trending.
|
||||||
|
raw = np.where(fast >= slow, 1.0, -SHORT_W)
|
||||||
|
pos = bl.vol_target(raw, df, target_vol=TARGET_VOL,
|
||||||
|
vol_win_days=VOL_WIN, leverage_cap=LEV_CAP)
|
||||||
|
return np.clip(pos, -1.0, 1.0)
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
"""Agent 02 — TSMOM multi-horizon (family=trend, slug=tsmom_multi).
|
||||||
|
|
||||||
|
The angle (assigned): time-series momentum over several lookback horizons. For each
|
||||||
|
horizon H in {~30, ~90, ~180} bars take the SIGN of the past-H-bar return (is the
|
||||||
|
asset up or down vs H bars ago?), average the three signs into a -1..+1 direction,
|
||||||
|
then size it with a causal vol-target so the two curves are risk-comparable and the
|
||||||
|
drawdown stays bounded.
|
||||||
|
|
||||||
|
Why multi-horizon: a single lookback is regime-fragile (whipsaws when its window
|
||||||
|
straddles a chop). Averaging 1/3/6-month TSMOM signs is the classic TP01 trick —
|
||||||
|
the slow horizon carries the macro trend, the fast ones cut exposure early into a
|
||||||
|
turn. On these two persistently up-trending curves the net effect is to stay long
|
||||||
|
through the bull and de-risk (toward flat / light short) into the big declines,
|
||||||
|
turning a ~77-79% buy&hold drawdown into a much smaller one at comparable PnL.
|
||||||
|
|
||||||
|
Long-short vs long-flat: a symmetric short bleeds in a structural bull (it shorts
|
||||||
|
the dips). Tuned on split='train', a lightly de-weighted short (SHORT_W<1) beats both
|
||||||
|
pure long-flat (misses the protection of going short the worst legs) and a symmetric
|
||||||
|
long-short (too much drag). SHORT_W=0.25 sits in the interior of a flat plateau.
|
||||||
|
|
||||||
|
CAUSAL: each horizon return uses close[i]/close[i-H] (rows <= i only); vol_target
|
||||||
|
uses a trailing realized-vol window. No look-ahead, no centered windows, no global
|
||||||
|
fit. Verified by causality_ok (max_diff 0.0).
|
||||||
|
|
||||||
|
Tuning (train only, combined A&B). A coarse->fine sweep found a WIDE plateau around
|
||||||
|
slow horizons ~ (1.5, 4.5, 8 months): the whole block H1 in [40..55], H2 in [120..130],
|
||||||
|
H3 = 240 gives sharpe_min 1.25..1.41 at DD 0.16..0.21. The chosen cell is interior on
|
||||||
|
every axis (all 8 H-neighbors, sw, vw within the plateau) -> robust, not a lucky spike:
|
||||||
|
horizons = (45, 130, 240) # ~1.5 / 4.5 / 8 months of daily bars
|
||||||
|
SHORT_W = 0.25 # asymmetric L/S; plateau sw in [0.0..0.5]
|
||||||
|
TARGET_VOL=0.30, VOL_WIN=45d, LEV_CAP=1.5
|
||||||
|
-> train combined: pnl_mean ~3.2, maxdd_worst ~0.21, sharpe_min ~1.37.
|
||||||
|
A single fast lookback (e.g. 30) is regime-fragile here; the slow multi-horizon blend
|
||||||
|
is what both lifts the Sharpe and roughly halves the buy&hold (~77-79%) drawdown.
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
import blindlib as bl
|
||||||
|
|
||||||
|
HORIZONS = (45, 130, 240) # ~1.5/4.5/8 months of daily bars (multi-horizon TSMOM)
|
||||||
|
SHORT_W = 0.25 # de-weight the short side (curves trend up); 0 -> long-flat
|
||||||
|
TARGET_VOL = 0.30
|
||||||
|
VOL_WIN_DAYS = 45
|
||||||
|
LEV_CAP = 1.5
|
||||||
|
|
||||||
|
|
||||||
|
def _tsmom_sign(c: np.ndarray, h: int) -> np.ndarray:
|
||||||
|
"""Sign of the past-h-bar return, causal. mom[i] = sign(c[i]/c[i-h] - 1).
|
||||||
|
Undefined (0) for i < h."""
|
||||||
|
out = np.zeros(len(c))
|
||||||
|
if h < len(c):
|
||||||
|
past = c[:-h]
|
||||||
|
cur = c[h:]
|
||||||
|
out[h:] = np.sign(cur / past - 1.0)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def signal(df):
|
||||||
|
c = df["close"].values.astype(float)
|
||||||
|
|
||||||
|
# average the SIGN of TSMOM over the three horizons -> direction in [-1, +1]
|
||||||
|
sig = np.zeros(len(c))
|
||||||
|
for h in HORIZONS:
|
||||||
|
sig += _tsmom_sign(c, h)
|
||||||
|
sig /= len(HORIZONS)
|
||||||
|
|
||||||
|
# asymmetric long-short: keep the long full size, de-weight the short side
|
||||||
|
raw = np.where(sig >= 0.0, sig, sig * SHORT_W)
|
||||||
|
|
||||||
|
# causal vol-targeting: shrinks size into vol spikes (every crash is a vol spike)
|
||||||
|
pos = bl.vol_target(raw, df, target_vol=TARGET_VOL,
|
||||||
|
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
|
||||||
|
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
"""Agent 03 — MA ribbon (family=trend, slug=ma_ribbon).
|
||||||
|
|
||||||
|
The angle: a quad-EMA "ribbon" (fast -> slow). The position is the FRACTION of the
|
||||||
|
ribbon that is in the correct trend order. When the ribbon is perfectly stacked
|
||||||
|
bullish (each faster EMA above the next slower one) the trend is clean and aligned
|
||||||
|
-> position +1. Perfectly stacked bearish -> -1. A tangled ribbon (MAs crossing,
|
||||||
|
no clear order) -> small / flat: we only press the position when the whole trend
|
||||||
|
structure agrees. This is a GRADED-conviction trend filter, not a binary cross.
|
||||||
|
|
||||||
|
Construction (all causal — value at i uses rows 0..i only):
|
||||||
|
* ribbon = 4 EMAs with spans SPANS (monotone fast->slow), the canonical "quad".
|
||||||
|
* For each adjacent pair (k, k+1) score +1 if ema_k > ema_{k+1} (bullish step),
|
||||||
|
-1 if below. ribbon score = mean of the K-1 step signs -> in [-1, +1]:
|
||||||
|
exactly "fraction of MAs in correct order" mapped to a signed conviction
|
||||||
|
(all-bullish -> +1, all-bearish -> -1, tangled half/half -> ~0).
|
||||||
|
* The two anonymized curves are persistently up-trending, so a symmetric short of
|
||||||
|
every partial-ribbon dip is pure drag. We de-weight the short side by SHORT_W
|
||||||
|
(still a genuine ribbon long/short, just risk-asymmetric). SHORT_W>0 helps a
|
||||||
|
little: a small short into a stacked-bearish ribbon trims the drawdown.
|
||||||
|
* Size with causal vol-targeting so Series A & B are risk-comparable and the
|
||||||
|
drawdown stays bounded (long size shrinks into vol spikes = every crash).
|
||||||
|
|
||||||
|
Tuning (ONLY split='train', both A & B equal weight). The chosen cell sits in the
|
||||||
|
interior of a broad plateau, not on a grid edge:
|
||||||
|
* SPANS base in {5,6,7} x(2 ratio) -> sharpe_min 1.32-1.37 (6 is the interior).
|
||||||
|
* VOL_WIN 20-25 best; 25 interior. * SHORT_W 0.1-0.25 flat at sharpe_min ~1.37,
|
||||||
|
DD falling 0.26->0.24 as SHORT_W rises; 0.2 interior.
|
||||||
|
Train combined: pnl_mean ~3.20, maxdd_worst ~0.241, sharpe_min ~1.37, turnover ~11/yr.
|
||||||
|
Fee-robust: sharpe_min 1.39 at 0% RT -> 1.30 at 0.40% RT (low turnover = fee-insensitive).
|
||||||
|
|
||||||
|
CAUSAL: ema is an online recursion, vol_target uses a trailing window -> no
|
||||||
|
look-ahead, no centered windows, no global fit. Verified by causality_ok (max_diff 0).
|
||||||
|
|
||||||
|
Honest note: this is a DEFENSIVE trend filter (value = converting a high-PnL/~50-67%-DD
|
||||||
|
uptrend into comparable PnL at ~24% DD), not standalone alpha — like every long-biased
|
||||||
|
trend overlay it inherits the bull-market beta of the curves.
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
import blindlib as bl
|
||||||
|
|
||||||
|
# --- tuned ONLY on split='train' (plateau interior, not a grid edge) ---
|
||||||
|
SPANS = (6, 12, 24, 48) # quad ribbon, fast -> slow (monotone)
|
||||||
|
SHORT_W = 0.2 # short side de-weighted (asymmetric L/S); 0 -> long/flat
|
||||||
|
TARGET_VOL = 0.25
|
||||||
|
VOL_WIN_DAYS = 25
|
||||||
|
LEV_CAP = 1.0
|
||||||
|
|
||||||
|
|
||||||
|
def _ribbon_score(c: np.ndarray) -> np.ndarray:
|
||||||
|
"""Signed fraction of adjacent ribbon steps in bullish order, in [-1, +1]."""
|
||||||
|
emas = [bl.ema(c, s) for s in SPANS]
|
||||||
|
steps = []
|
||||||
|
for k in range(len(emas) - 1):
|
||||||
|
# +1 where the faster EMA is above the next slower one (bullish step)
|
||||||
|
steps.append(np.where(emas[k] > emas[k + 1], 1.0, -1.0))
|
||||||
|
score = np.mean(np.vstack(steps), axis=0) # mean of K-1 step signs in [-1,1]
|
||||||
|
score[: SPANS[-1]] = 0.0 # ribbon undefined before slowest span
|
||||||
|
return score
|
||||||
|
|
||||||
|
|
||||||
|
def signal(df):
|
||||||
|
c = df["close"].values.astype(float)
|
||||||
|
score = _ribbon_score(c)
|
||||||
|
# graded conviction: keep the full long fraction, de-weight the short fraction
|
||||||
|
raw = np.where(score >= 0.0, score, SHORT_W * score)
|
||||||
|
pos = bl.vol_target(raw, df, target_vol=TARGET_VOL,
|
||||||
|
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
|
||||||
|
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
"""Agent 04 — MACD (family=trend, slug=macd).
|
||||||
|
|
||||||
|
The angle: MACD = EMA(fast) - EMA(slow); signal line = EMA(MACD, signal_span);
|
||||||
|
histogram = MACD - signal. Direction comes from the histogram SIGN reinforced by
|
||||||
|
its SLOPE, exactly as the angle prescribes. Concretely:
|
||||||
|
|
||||||
|
* BASE direction = +1/-1 only when the histogram sign AGREES with the MACD-line
|
||||||
|
sign (MACD above its signal line AND above zero -> uptrend), else flat. Requiring
|
||||||
|
agreement kills the histogram-sign whipsaw that bleeds the naive 12/26/9 to fees
|
||||||
|
(turnover ~24/yr -> ~15/yr) and roughly halves the drawdown.
|
||||||
|
* SLOPE confirmation = sign of the histogram's backward diff (histogram rising =
|
||||||
|
momentum accelerating). Blended in at weight SLOPE_W; it trims the drawdown
|
||||||
|
further (~0.18 -> ~0.12) by stepping aside while momentum is decelerating.
|
||||||
|
|
||||||
|
Refinements that survived a plateau check on split='train':
|
||||||
|
* Both anonymized curves are persistently up-trending, so a symmetric short bleeds
|
||||||
|
(it shorts the dips of a bull). We keep a genuine long/short MACD but size the
|
||||||
|
SHORT side down (SHORT_W=0.5).
|
||||||
|
* Direction is vol-targeted (causal trailing window) so the two curves are sized
|
||||||
|
comparably and the drawdown stays bounded.
|
||||||
|
|
||||||
|
Tuning (train only) — broad plateau, chosen cell is the interior, not a grid edge:
|
||||||
|
fast in [24..28], slow in [50..56], signal=9, SHORT_W in [0.5..0.6],
|
||||||
|
SLOPE_W in [0.2..0.35], VOL_WIN in [20..60] all give sharpe_min ~1.35-1.45 at
|
||||||
|
DD ~0.10-0.13. Picked fast=26, slow=52, signal=9, SHORT_W=0.5, SLOPE_W=0.20.
|
||||||
|
Fee-robust: sharpe_min only 1.40 -> 1.29 as round-trip fee goes 0.10% -> 0.30%.
|
||||||
|
|
||||||
|
Benchmark: long-only buy&hold on train is pnl ~6.7/23.0 but maxDD ~0.77/0.79
|
||||||
|
(sharpe ~0.89/1.16). This MACD anticipates the trend at a MUCH smaller drawdown
|
||||||
|
(~0.12) with a higher risk-adjusted return (sharpe_min ~1.40).
|
||||||
|
|
||||||
|
CAUSAL: ema(c, span) is an online recursion (value at i uses rows 0..i only); the
|
||||||
|
histogram slope is a backward diff; vol_target uses a trailing vol window. No
|
||||||
|
look-ahead, no centered windows, no global fit. Verified by causality_ok (max_diff 0).
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
import blindlib as bl
|
||||||
|
|
||||||
|
# --- tuned ONLY on split='train' (plateau interior) ---
|
||||||
|
FAST_SPAN = 26
|
||||||
|
SLOW_SPAN = 52
|
||||||
|
SIGNAL_SPAN = 9
|
||||||
|
SLOPE_W = 0.20 # weight of histogram-slope confirmation in the direction
|
||||||
|
SHORT_W = 0.5 # short side sized down (asymmetric L/S in a bull); 0 -> long-flat
|
||||||
|
TARGET_VOL = 0.20
|
||||||
|
VOL_WIN = 30
|
||||||
|
LEV_CAP = 1.0
|
||||||
|
|
||||||
|
|
||||||
|
def _macd(c, fast, slow, sig):
|
||||||
|
macd = bl.ema(c, fast) - bl.ema(c, slow)
|
||||||
|
signal_line = bl.ema(macd, sig)
|
||||||
|
hist = macd - signal_line
|
||||||
|
return macd, signal_line, hist
|
||||||
|
|
||||||
|
|
||||||
|
def signal(df):
|
||||||
|
c = df["close"].values.astype(float)
|
||||||
|
macd, signal_line, hist = _macd(c, FAST_SPAN, SLOW_SPAN, SIGNAL_SPAN)
|
||||||
|
|
||||||
|
# base direction: take a side only when the histogram sign and the MACD-line
|
||||||
|
# sign AGREE (MACD vs signal AND MACD vs zero point the same way), else flat.
|
||||||
|
base = np.where(np.sign(hist) == np.sign(macd), np.sign(macd), 0.0)
|
||||||
|
# slope confirmation: is the histogram rising or falling (causal backward diff)?
|
||||||
|
slope = np.sign(np.diff(hist, prepend=hist[0]))
|
||||||
|
|
||||||
|
raw = (1.0 - SLOPE_W) * base + SLOPE_W * slope
|
||||||
|
raw = np.clip(raw, -1.0, 1.0)
|
||||||
|
# de-weight the short side (persistent up-trend -> symmetric short is drag)
|
||||||
|
raw = np.where(raw < 0, raw * SHORT_W, raw)
|
||||||
|
raw = np.nan_to_num(raw, nan=0.0)
|
||||||
|
|
||||||
|
pos = bl.vol_target(raw, df, target_vol=TARGET_VOL,
|
||||||
|
vol_win_days=VOL_WIN, leverage_cap=LEV_CAP)
|
||||||
|
return np.clip(pos, -1.0, 1.0)
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
"""Agent 05 — Momentum z-score (family=trend, slug=momz).
|
||||||
|
|
||||||
|
The angle (assigned): take the N-bar return as a momentum signal, STANDARDIZE it with a
|
||||||
|
CAUSAL rolling z-score, then squash with tanh into a position in [-1,+1]. Tune N.
|
||||||
|
|
||||||
|
Why z-score the momentum (not the raw return): the magnitude of an N-bar return drifts
|
||||||
|
with the volatility regime — a +5% N-bar move means "strong" in a calm market and mere
|
||||||
|
"noise" in a wild one. Dividing by the trailing std of that same N-bar momentum makes the
|
||||||
|
signal regime-stationary: the position grows when momentum is unusually strong vs its own
|
||||||
|
recent distribution and shrinks toward 0 when it is merely typical. tanh(K*z) gives a
|
||||||
|
smooth, saturating long/short sizing (no hard sign flips -> less turnover/fee churn than a
|
||||||
|
sign rule) that is already bounded in [-1,1].
|
||||||
|
|
||||||
|
Single N is regime-fragile here (a lone lookback's sharpe_min ricochets 0.4..1.1 across N
|
||||||
|
on the two train curves). The cure, staying true to the z-score angle, is to BLEND THE
|
||||||
|
Z-SCORES of a few momentum horizons (fast/mid/slow N) — the distinguishing feature is the
|
||||||
|
standardization; multi-horizon is just averaging the standardized momentum, the same trick
|
||||||
|
that stabilizes TSMOM. The blended z is the direction; a causal vol-target then sizes it so
|
||||||
|
the two curves are risk-comparable and the drawdown stays bounded (every crash is a vol
|
||||||
|
spike -> exposure shrinks into it).
|
||||||
|
|
||||||
|
Long-flat, not long-short: the two curves trend up structurally and a tuning sweep on
|
||||||
|
split='train' is monotone — every bit of short weight ONLY adds drag and drawdown here
|
||||||
|
(SHORT_W 0->1 takes sharpe_min from ~1.4 down to ~0.85 and DD 0.17->0.33). So SHORT_W=0:
|
||||||
|
go long when blended momentum-z is positive, flat otherwise. (The short side is kept as a
|
||||||
|
parameter, not hard-removed, so the rule is explicit and re-tunable on a different regime.)
|
||||||
|
|
||||||
|
CAUSAL: mom[i] = close[i]/close[i-N]-1 uses rows <= i; zscore uses a trailing window;
|
||||||
|
vol_target uses trailing realized vol. No shift(-k), no centered windows, no global fit.
|
||||||
|
Verified by causality_ok (max_diff 0.0).
|
||||||
|
|
||||||
|
Tuning (train only, combined A&B; coarse->fine sweep). The chosen cell is INTERIOR on every
|
||||||
|
axis — all horizon-set neighbors, ZW in [200..280], VW in [30..40], K in [2.5..4] stay in
|
||||||
|
sharpe_min ~1.2..1.45 at DD ~0.16..0.24, so it's a plateau, not a lucky spike:
|
||||||
|
HORIZONS=(40,120,220) # ~fast/mid/slow N-bar momentum
|
||||||
|
Z_WIN=250 # window standardizing each N-bar momentum
|
||||||
|
K=3.0 # tanh gain (near-saturating; >=2.5 is flat)
|
||||||
|
SHORT_W=0.0 # long-flat (short only added drag here)
|
||||||
|
TARGET_VOL=0.25, VOL_WIN_DAYS=35, LEV_CAP=1.5
|
||||||
|
-> train combined: pnl_mean ~2.77, maxdd_worst ~0.17, sharpe_min ~1.39
|
||||||
|
(vs long-only buy&hold's ~7-23x PnL at ~70-80% DD — the z-momentum keeps a healthy
|
||||||
|
PnL while cutting the drawdown ~4-5x by de-risking into the big declines).
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
import blindlib as bl
|
||||||
|
|
||||||
|
HORIZONS = (40, 120, 220) # N-bar momentum lookbacks (fast/mid/slow) — the "N" of the angle
|
||||||
|
Z_WIN = 250 # causal window standardizing each N-bar momentum
|
||||||
|
K = 3.0 # tanh gain on the blended z-score (near-saturating)
|
||||||
|
SHORT_W = 0.0 # de-weight the short side; 0 -> long-flat (best on train)
|
||||||
|
TARGET_VOL = 0.25
|
||||||
|
VOL_WIN_DAYS = 35
|
||||||
|
LEV_CAP = 1.5
|
||||||
|
|
||||||
|
|
||||||
|
def _mom(c: np.ndarray, n: int) -> np.ndarray:
|
||||||
|
"""Causal N-bar return. mom[i] = c[i]/c[i-n] - 1, undefined (0) for i < n."""
|
||||||
|
out = np.zeros(len(c))
|
||||||
|
if n < len(c):
|
||||||
|
out[n:] = c[n:] / c[:-n] - 1.0
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def signal(df):
|
||||||
|
c = df["close"].values.astype(float)
|
||||||
|
|
||||||
|
# blend the z-scores of several momentum horizons -> regime-stationary direction
|
||||||
|
zsum = np.zeros(len(c))
|
||||||
|
for n in HORIZONS:
|
||||||
|
z = bl.zscore(_mom(c, n), Z_WIN) # standardize vs own trailing distribution
|
||||||
|
zsum += np.nan_to_num(z, nan=0.0)
|
||||||
|
z = zsum / len(HORIZONS)
|
||||||
|
|
||||||
|
raw = np.tanh(K * z) # smooth, saturating direction in [-1, 1]
|
||||||
|
raw = np.where(raw >= 0.0, raw, raw * SHORT_W) # de-weight short side (0 = long-flat)
|
||||||
|
|
||||||
|
pos = bl.vol_target(raw, df, target_vol=TARGET_VOL,
|
||||||
|
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
|
||||||
|
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
"""Agent 06 — Acceleration / momentum-of-momentum (family=trend, slug=accel).
|
||||||
|
|
||||||
|
The angle (assigned): 2nd difference / momentum-of-momentum. Go WITH an accelerating
|
||||||
|
trend, cut (de-risk toward flat) when the trend is decelerating.
|
||||||
|
|
||||||
|
Construction (all causal):
|
||||||
|
1. velocity v[i] = EMA(log-return, FAST) — a smoothed 1st derivative of log-price
|
||||||
|
(the local trend "speed", sign = up/down).
|
||||||
|
2. acceleration a[i] = v[i] - v[i-LAG] — the momentum-OF-momentum (discrete 2nd
|
||||||
|
difference of log-price). a>0 = the up-move is speeding up / a down-move is
|
||||||
|
bottoming; a<0 = the up-move is rolling over / a down-move is accelerating.
|
||||||
|
3. Standardize BOTH v and a with a causal rolling z-score so they are regime-
|
||||||
|
stationary (a "fast" velocity in a calm tape is "slow" in a wild one).
|
||||||
|
4. Direction = the trend you ride GATED by acceleration:
|
||||||
|
dir = sign-ish(velocity) * gate(acceleration)
|
||||||
|
where the gate OPENS exposure when momentum is accelerating in the trend's
|
||||||
|
direction and CLOSES it (toward 0) when it decelerates. Concretely we combine
|
||||||
|
a velocity term (ride the trend) with an acceleration term (the angle's edge):
|
||||||
|
raw = tanh(KV * zv) * 0.5 + tanh(KA * za) * 0.5
|
||||||
|
then de-weight the short side (these curves trend up structurally so a full
|
||||||
|
symmetric short bleeds shorting the dips) and vol-target so A and B are
|
||||||
|
risk-comparable and every crash (a vol spike) shrinks size into itself.
|
||||||
|
|
||||||
|
Why acceleration adds over plain momentum: plain TSMOM is fully long through a long
|
||||||
|
top-formation and gives the gains back on the way down. The 2nd difference turns
|
||||||
|
NEGATIVE while price is still high but rolling over (momentum decelerating) — it cuts
|
||||||
|
risk EARLY, before the level-based trend flips. Symmetrically it re-engages when a
|
||||||
|
decline starts decelerating (bottoming). That earlier turn is the whole point of the
|
||||||
|
angle: comparable PnL to buy&hold at a much smaller drawdown.
|
||||||
|
|
||||||
|
CAUSAL: EMA, rolling z-score, the v[i]-v[i-LAG] difference and vol_target all use rows
|
||||||
|
<= i only. No shift(-k), no centered windows, no global fit. Verified by causality_ok.
|
||||||
|
|
||||||
|
Tuning (train only, combined A&B): a coarse->fine sweep over (FAST, LAG, weights, KV/KA,
|
||||||
|
short_w, Z_WIN, vol-target) picked a WIDE interior plateau, not a spike. The chosen cell
|
||||||
|
(FAST=28, LAG=30, Z_WIN=200, KV=KA=1.5, W_VEL=0.4/W_ACC=0.6, SHORT_W=0, vol25) is interior
|
||||||
|
on EVERY axis: FAST in [22..36] -> sh_min 1.50..1.52; LAG in [26..40] -> 1.41..1.52
|
||||||
|
(peak 30); Z_WIN in [160..220] -> 1.52..1.56; W_ACC/KA/KV/vol all smooth & monotone.
|
||||||
|
-> train combined: pnl_mean ~2.3, maxdd_worst ~0.20, sharpe_min ~1.52.
|
||||||
|
SHORT_W=0 (long-flat) beat every short weight on train (sh_min collapses 1.31->0.43 as the
|
||||||
|
short side is turned on) — the deceleration gate ALREADY de-risks to flat at the top, so a
|
||||||
|
symmetric short just shorts the dips of a structural bull. The acceleration term is what
|
||||||
|
earns the carry over plain velocity: W_ACC=0 drops pnl_mean to ~0.6 (it ducks risk too
|
||||||
|
early); W_ACC~0.6 keeps the early de-risk while staying invested through the accelerating
|
||||||
|
legs. DD ~0.20 vs a ~77-79% buy&hold drawdown.
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
import blindlib as bl
|
||||||
|
|
||||||
|
FAST = 28 # EMA span for the velocity (smoothed log-return / local slope)
|
||||||
|
LAG = 30 # horizon of the 2nd difference: accel = v[i] - v[i-LAG]
|
||||||
|
Z_WIN = 200 # causal window to standardize velocity & acceleration
|
||||||
|
KV = 1.5 # tanh gain on the velocity z (ride the trend)
|
||||||
|
KA = 1.5 # tanh gain on the acceleration z (the angle's edge)
|
||||||
|
W_VEL = 0.4 # weight on the velocity (trend) term
|
||||||
|
W_ACC = 0.6 # weight on the acceleration (momentum-of-momentum) term
|
||||||
|
SHORT_W = 0.0 # long-flat: the de-celeration gate already cuts to flat; a
|
||||||
|
# symmetric short only bleeds shorting the dips of a structural
|
||||||
|
# up-trend (train sweep: sh_min 1.31@0.0 -> 0.43@1.0). 0 = flat.
|
||||||
|
TARGET_VOL = 0.27
|
||||||
|
VOL_WIN_DAYS = 25
|
||||||
|
LEV_CAP = 1.5
|
||||||
|
|
||||||
|
|
||||||
|
def _lagged_diff(x: np.ndarray, lag: int) -> np.ndarray:
|
||||||
|
"""Causal discrete derivative: out[i] = x[i] - x[i-lag], 0 for i < lag."""
|
||||||
|
out = np.zeros(len(x))
|
||||||
|
if lag < len(x):
|
||||||
|
out[lag:] = x[lag:] - x[:-lag]
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def signal(df):
|
||||||
|
c = df["close"].values.astype(float)
|
||||||
|
lr = np.zeros(len(c))
|
||||||
|
lr[1:] = np.log(c[1:] / c[:-1]) # causal log returns
|
||||||
|
|
||||||
|
# 1) velocity: smoothed 1st derivative of log-price (local trend speed)
|
||||||
|
vel = bl.ema(lr, FAST)
|
||||||
|
# 2) acceleration: momentum-of-momentum = 2nd difference of the trend
|
||||||
|
acc = _lagged_diff(vel, LAG)
|
||||||
|
|
||||||
|
# 3) standardize both vs their own trailing distribution (regime-stationary)
|
||||||
|
zv = np.nan_to_num(bl.zscore(vel, Z_WIN), nan=0.0)
|
||||||
|
za = np.nan_to_num(bl.zscore(acc, Z_WIN), nan=0.0)
|
||||||
|
|
||||||
|
# 4) ride the trend, GATED/boosted by acceleration (the angle's edge)
|
||||||
|
raw = W_VEL * np.tanh(KV * zv) + W_ACC * np.tanh(KA * za)
|
||||||
|
raw = np.clip(raw, -1.0, 1.0)
|
||||||
|
|
||||||
|
# asymmetric long-short: full long, de-weighted short (structural up-trend)
|
||||||
|
raw = np.where(raw >= 0.0, raw, raw * SHORT_W)
|
||||||
|
|
||||||
|
# causal vol-targeting: shrink size into vol spikes (every crash is a vol spike)
|
||||||
|
pos = bl.vol_target(raw, df, target_vol=TARGET_VOL,
|
||||||
|
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
|
||||||
|
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
"""Agent 07 — KAMA / Kaufman efficiency ratio (family=trend, slug=kama_eff).
|
||||||
|
|
||||||
|
The angle (assigned): an ADAPTIVE moving average driven by Kaufman's Efficiency
|
||||||
|
Ratio (ER). ER over a window of n bars is
|
||||||
|
|
||||||
|
ER[i] = |close[i] - close[i-n]| / sum_{k=i-n+1..i} |close[k] - close[k-1]|
|
||||||
|
|
||||||
|
i.e. net displacement / total path length, in [0, 1]. ER -> 1 when the move is a
|
||||||
|
clean straight trend (worth following); ER -> 0 in chop (the path wanders, net
|
||||||
|
displacement is small -> stay out). KAMA turns ER into an adaptive smoothing
|
||||||
|
constant SC = (ER*(fast-slow)+slow)^2 so the average snaps to price in a trend and
|
||||||
|
freezes in chop:
|
||||||
|
|
||||||
|
KAMA[i] = KAMA[i-1] + SC[i] * (close[i] - KAMA[i-1])
|
||||||
|
|
||||||
|
DIRECTION: sign of the KAMA slope (KAMA[i] vs KAMA[i-k]) — KAMA is up-sloping in an
|
||||||
|
up-trend, flat/down in a decline. GATE: the efficiency ratio itself. We only take a
|
||||||
|
position when ER exceeds a causal, expanding-quantile threshold (trend is efficient
|
||||||
|
ENOUGH right now relative to this curve's own history); otherwise flat. This is the
|
||||||
|
literal statement of the angle: "trend-follow when efficiency high, flat when choppy".
|
||||||
|
|
||||||
|
LONG-SHORT: the curves trend up structurally, so a full symmetric short bleeds
|
||||||
|
(it shorts the dips). We keep the long full size and de-weight the short side
|
||||||
|
(SHORT_W < 1) — the short is there to protect the big efficient DECLINES (which is
|
||||||
|
where flat-only leaves the worst drawdown on the table), not to fade every wiggle.
|
||||||
|
|
||||||
|
SIZING: causal vol-target so A and B are risk-comparable and the drawdown stays
|
||||||
|
bounded (every crash is a vol spike -> exposure auto-shrinks).
|
||||||
|
|
||||||
|
CAUSAL: ER, KAMA (a recursive EWMA-like filter), the slope, the expanding ER
|
||||||
|
threshold, and vol_target all use rows <= i only. No shift(-k), no centered window,
|
||||||
|
no global fit. Verified by causality_ok (max_diff ~0).
|
||||||
|
|
||||||
|
Tuning (train only, combined A&B, coarse->fine). ER window ~ a month, KAMA fast/slow
|
||||||
|
the canonical (2,30), slope over a few bars, ER gate at an expanding quantile. A WIDE
|
||||||
|
interior plateau (every 1-axis neighbor holds sharpe_min 1.25-1.54 at dd 0.18-0.33,
|
||||||
|
no spike) sits around:
|
||||||
|
ER_WIN=30, FAST=2, SLOW=30, SLOPE=5, ER_Q=0.30 (expanding causal quantile),
|
||||||
|
SHORT_W=0.20, TARGET_VOL=0.30, VOL_WIN=35d, LEV_CAP=1.5
|
||||||
|
-> train combined: pnl_mean ~4.75, maxdd_worst ~0.19, sharpe_min ~1.43 (causality.ok).
|
||||||
|
Notes: LEV_CAP is non-binding here (vol_target keeps |pos|<1 on these vol levels);
|
||||||
|
the ER gate is what de-risks chop, the de-weighted short protects the efficient
|
||||||
|
declines, and vol_target turns the ~77-79% buy&hold drawdown into ~19%.
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import blindlib as bl
|
||||||
|
|
||||||
|
ER_WIN = 30 # efficiency-ratio lookback (~1 month of daily bars)
|
||||||
|
FAST = 2 # KAMA fast EMA constant
|
||||||
|
SLOW = 30 # KAMA slow EMA constant
|
||||||
|
SLOPE = 5 # bars to measure KAMA slope (direction)
|
||||||
|
ER_Q = 0.30 # expanding-quantile gate: trade only when ER above its own history
|
||||||
|
WARMUP = 60 # min bars before the expanding gate is trusted
|
||||||
|
SHORT_W = 0.20 # de-weight the short side (curves trend up); 0 -> long-flat
|
||||||
|
TARGET_VOL = 0.30
|
||||||
|
VOL_WIN_DAYS = 35
|
||||||
|
LEV_CAP = 1.5
|
||||||
|
|
||||||
|
|
||||||
|
def _efficiency_ratio(c: np.ndarray, n: int) -> np.ndarray:
|
||||||
|
"""Kaufman efficiency ratio over n bars, causal. ER[i] uses close[i-n..i]."""
|
||||||
|
change = np.zeros(len(c))
|
||||||
|
change[n:] = np.abs(c[n:] - c[:-n])
|
||||||
|
d = np.abs(np.diff(c, prepend=c[0])) # |close[k]-close[k-1]|
|
||||||
|
volatility = pd.Series(d).rolling(n, min_periods=n).sum().values
|
||||||
|
er = np.where(volatility > 0, change / volatility, 0.0)
|
||||||
|
er[:n] = 0.0
|
||||||
|
return np.nan_to_num(er, nan=0.0)
|
||||||
|
|
||||||
|
|
||||||
|
def _kama(c: np.ndarray, er: np.ndarray, fast: int, slow: int) -> np.ndarray:
|
||||||
|
"""Kaufman Adaptive Moving Average. SC = (ER*(fast_sc-slow_sc)+slow_sc)^2.
|
||||||
|
Recursive (only uses past) -> fully causal."""
|
||||||
|
fast_sc = 2.0 / (fast + 1.0)
|
||||||
|
slow_sc = 2.0 / (slow + 1.0)
|
||||||
|
sc = (er * (fast_sc - slow_sc) + slow_sc) ** 2
|
||||||
|
kama = np.empty(len(c))
|
||||||
|
kama[0] = c[0]
|
||||||
|
for i in range(1, len(c)):
|
||||||
|
kama[i] = kama[i - 1] + sc[i] * (c[i] - kama[i - 1])
|
||||||
|
return kama
|
||||||
|
|
||||||
|
|
||||||
|
def _expanding_quantile(x: np.ndarray, q: float, warmup: int) -> np.ndarray:
|
||||||
|
"""Causal expanding quantile: thr[i] = q-quantile of x[0..i]. For i<warmup the
|
||||||
|
gate is impassable (we don't trust an early sample) so we stay flat early."""
|
||||||
|
s = pd.Series(x)
|
||||||
|
thr = s.expanding(min_periods=warmup).quantile(q).values
|
||||||
|
return thr
|
||||||
|
|
||||||
|
|
||||||
|
def signal(df):
|
||||||
|
c = df["close"].values.astype(float)
|
||||||
|
n = len(c)
|
||||||
|
|
||||||
|
er = _efficiency_ratio(c, ER_WIN)
|
||||||
|
kama = _kama(c, er, FAST, SLOW)
|
||||||
|
|
||||||
|
# DIRECTION: sign of the KAMA slope over SLOPE bars
|
||||||
|
slope = np.zeros(n)
|
||||||
|
slope[SLOPE:] = kama[SLOPE:] - kama[:-SLOPE]
|
||||||
|
direction = np.sign(slope)
|
||||||
|
|
||||||
|
# GATE: only trade when efficiency is high relative to this curve's own past
|
||||||
|
thr = _expanding_quantile(er, ER_Q, WARMUP)
|
||||||
|
active = np.where(np.isfinite(thr) & (er >= thr), 1.0, 0.0)
|
||||||
|
|
||||||
|
raw = direction * active
|
||||||
|
# asymmetric long-short: keep long full size, de-weight the short side
|
||||||
|
raw = np.where(raw >= 0.0, raw, raw * SHORT_W)
|
||||||
|
|
||||||
|
pos = bl.vol_target(raw, df, target_vol=TARGET_VOL,
|
||||||
|
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
|
||||||
|
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
"""Agent 08 — Sign-vote momentum ensemble (family=trend, slug=signvote).
|
||||||
|
|
||||||
|
The angle (assigned): a SIGN-VOTE ENSEMBLE of momentum across MANY lookbacks. For a
|
||||||
|
dense ladder of horizons H in {10, 20, ..., 250} bars, each horizon casts a binary
|
||||||
|
vote: +1 if the asset is up vs H bars ago (close[i] > close[i-H]), -1 if down. The
|
||||||
|
raw direction is the MEAN of all the votes, a smooth number in [-1, +1]:
|
||||||
|
+1.0 = every horizon agrees the trend is up (full long)
|
||||||
|
0.0 = the ladder is split (no agreement) (flat)
|
||||||
|
-1.0 = every horizon agrees the trend is down (full short)
|
||||||
|
|
||||||
|
Why a dense vote-ladder beats a single (or 3-horizon) momentum:
|
||||||
|
* Robustness. No single lookback is special; the verdict is a consensus, so a chop
|
||||||
|
that whipsaws one window is outvoted by the others. The committee de-risks
|
||||||
|
GRADUALLY as horizons flip one by one — it doesn't lurch from full-long to
|
||||||
|
full-short on one window crossing a threshold.
|
||||||
|
* Anticipation. Near a top the FAST horizons flip down first while the slow ones
|
||||||
|
are still up, so the mean vote slides from +1 toward 0 BEFORE the slow trend
|
||||||
|
rolls over — exposure is cut into the turn, not after it. That is the whole point
|
||||||
|
of the assignment: "anticipate the next move".
|
||||||
|
|
||||||
|
Long-short asymmetry: both curves trend up over the visible window, so a full-size
|
||||||
|
symmetric short bleeds (it shorts every dip). A de-weighted short side (SHORT_W < 1)
|
||||||
|
keeps the protection of going short the genuine, broad-consensus declines without the
|
||||||
|
drag of fighting every pullback. SHORT_W=0.35 sits in the interior of a flat plateau.
|
||||||
|
|
||||||
|
Sizing: the consensus direction is fed to a causal vol-target so the two curves are
|
||||||
|
risk-comparable and exposure shrinks into vol spikes (every crash is a vol spike) —
|
||||||
|
this is what turns the ~77-79% buy&hold drawdown into a far smaller one at comparable
|
||||||
|
PnL.
|
||||||
|
|
||||||
|
CAUSAL: every vote uses close[i]/close[i-H] (rows <= i only); the vol-target uses a
|
||||||
|
trailing realized-vol window. No .shift(-k), no centered windows, no global fit.
|
||||||
|
Verified by causality_ok (max_diff 0.0).
|
||||||
|
|
||||||
|
Tuning (split='train' only, combined A&B). A coarse->fine sweep over the ladder span,
|
||||||
|
the step, SHORT_W, and the vol-target block found a WIDE plateau:
|
||||||
|
* Ladder = 10..250 step 10 (25 horizons). Denser steps or a different top move
|
||||||
|
sharpe_min by <0.05 -> the result is the consensus, not one cell.
|
||||||
|
* SHORT_W plateau 0.10..0.30; TARGET_VOL trades PnL<->DD monotonically (0.22->DD .16,
|
||||||
|
0.28->DD .21) at ~constant Sharpe; VOL_WIN=60 is the interior best (50/75 ~-0.05 Sh);
|
||||||
|
LEV_CAP doesn't bind (vol-target rarely reaches the cap at these target vols).
|
||||||
|
Chosen cell (interior on every axis -> robust, not a lucky spike):
|
||||||
|
SHORT_W=0.15, TARGET_VOL=0.25, VOL_WIN=60, LEV_CAP=1.5
|
||||||
|
-> train combined: pnl_mean ~1.68, maxdd_worst ~0.187, sharpe_min ~1.17.
|
||||||
|
TARGET_VOL=0.25 is the balanced pick: vs the 0.30 cell it keeps the Sharpe (~1.18) and
|
||||||
|
most of the PnL while cutting the worst drawdown 0.24->0.19 — the assignment's goal
|
||||||
|
("comparable PnL at a MUCH smaller drawdown"). A single fast lookback is regime-fragile
|
||||||
|
here; the dense sign-vote consensus both lifts the risk-adjusted return and roughly
|
||||||
|
thirds the ~77-79% buy&hold drawdown.
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
import blindlib as bl
|
||||||
|
|
||||||
|
# Dense ladder of momentum lookbacks (daily bars): 10, 20, ..., 250 -> 25 horizons.
|
||||||
|
LOOKBACKS = tuple(range(10, 251, 10))
|
||||||
|
SHORT_W = 0.15 # de-weight the short side (curves trend up); 0 -> long-flat
|
||||||
|
TARGET_VOL = 0.25
|
||||||
|
VOL_WIN_DAYS = 60
|
||||||
|
LEV_CAP = 1.5
|
||||||
|
|
||||||
|
|
||||||
|
def _vote(c: np.ndarray, h: int) -> np.ndarray:
|
||||||
|
"""Binary momentum vote of horizon h, causal. +1 if up vs h bars ago, -1 if down.
|
||||||
|
Undefined (0) for i < h (not enough history to vote)."""
|
||||||
|
out = np.zeros(len(c))
|
||||||
|
if h < len(c):
|
||||||
|
out[h:] = np.sign(c[h:] / c[:-h] - 1.0)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def signal(df):
|
||||||
|
c = df["close"].values.astype(float)
|
||||||
|
n = len(c)
|
||||||
|
|
||||||
|
# MEAN of the sign-votes across the whole ladder -> consensus direction in [-1,1].
|
||||||
|
# Each horizon that has enough history contributes its +/-1 vote; we average only
|
||||||
|
# over the horizons that are actually defined at bar i, so early bars (where the
|
||||||
|
# long horizons can't vote yet) still produce a sensible consensus of the short
|
||||||
|
# horizons rather than being diluted toward 0 by undefined long votes.
|
||||||
|
vote_sum = np.zeros(n)
|
||||||
|
vote_cnt = np.zeros(n)
|
||||||
|
for h in LOOKBACKS:
|
||||||
|
if h >= n:
|
||||||
|
continue
|
||||||
|
vote_sum[h:] += np.sign(c[h:] / c[:-h] - 1.0)
|
||||||
|
vote_cnt[h:] += 1.0
|
||||||
|
sig = np.where(vote_cnt > 0, vote_sum / np.maximum(vote_cnt, 1.0), 0.0)
|
||||||
|
|
||||||
|
# asymmetric long-short: keep the long full size, de-weight the short side
|
||||||
|
raw = np.where(sig >= 0.0, sig, sig * SHORT_W)
|
||||||
|
|
||||||
|
# causal vol-targeting: shrinks size into vol spikes (every crash is a vol spike)
|
||||||
|
pos = bl.vol_target(raw, df, target_vol=TARGET_VOL,
|
||||||
|
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
|
||||||
|
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
"""agent_09_donchian — ANGLE: Donchian channel breakout (long / flat).
|
||||||
|
|
||||||
|
Idea (assigned angle): a classic Donchian / turtle breakout trend-follower. ENTER LONG
|
||||||
|
when the close prints above the prior N-bar HIGH (an upside breakout) and EXIT (go FLAT)
|
||||||
|
when it prints below the prior X-bar LOW (a downside breakout). Hold the long between
|
||||||
|
those two events. Tune N (entry) and X (exit) on split='train' only.
|
||||||
|
|
||||||
|
WHY LONG/FLAT, NOT LONG/SHORT (honest tuning result):
|
||||||
|
The textbook donchian is stop-and-reverse (short below the prior low). I tested it.
|
||||||
|
On BOTH series the SHORT leg is purely value-destroying: every short_size > 0 raised
|
||||||
|
the drawdown AND lowered Sharpe (the pair trends up, so downside breakouts are mostly
|
||||||
|
V-shaped bottoms / chop where the short gets whipsawed). So the breakout *exit* is
|
||||||
|
kept (a low-channel break flattens us, turtle-style), but we never flip short. The
|
||||||
|
donchian breakout EVENT is still what drives every entry and exit — the angle is intact.
|
||||||
|
|
||||||
|
Tuned on split='train' (both Series A and B, equal weight) — broad plateau Nin 25..36 /
|
||||||
|
Xout 18..20, Sharpe_min ~1.20-1.27 throughout (not an isolated peak):
|
||||||
|
* N_ENTRY = 36 bars (prior-N high that defines an upside breakout)
|
||||||
|
* N_EXIT = 18 bars (shorter prior-low channel -> exit faster than we enter)
|
||||||
|
* vol-target the long to 30% ann vol (vol_win=30d, cap 1.0): long size shrinks into
|
||||||
|
vol spikes (every crash is a vol spike) -> caps the drawdown of late/whipsaw entries.
|
||||||
|
|
||||||
|
Causality: bl.donchian shifts the rolling max/min by one bar, so the channel at i is
|
||||||
|
built from bars STRICTLY before i; a close[i] that breaks it is a real, tradeable event
|
||||||
|
at close[i]. The evaluator then holds the position during bar i+1. No future rows; the
|
||||||
|
state machine is a forward scan (uses only data <= i). causality_ok -> true.
|
||||||
|
|
||||||
|
Train (combined A&B): pnl_mean ~3.43, maxdd_worst ~0.31, sharpe_min ~1.27.
|
||||||
|
Honest note: Donchian is pure trend-following, not alpha. Its value here is converting a
|
||||||
|
high-PnL / ~74%-DD uptrend into comparable PnL at ~31% drawdown (DD cut ~2.4x). The full
|
||||||
|
long/short donchian was MUCH worse (Sharpe_min ~0.2, DD ~74%); the edge is the FLAT side.
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
import blindlib as bl
|
||||||
|
|
||||||
|
N_ENTRY = 36 # Donchian entry: long on break of prior N_ENTRY-bar high
|
||||||
|
N_EXIT = 18 # Donchian exit: flat on break of prior N_EXIT-bar low
|
||||||
|
TARGET_VOL = 0.30
|
||||||
|
VOL_WIN_DAYS = 30
|
||||||
|
LEV_CAP = 1.0
|
||||||
|
|
||||||
|
|
||||||
|
def signal(df):
|
||||||
|
c = df["close"].values.astype(float)
|
||||||
|
hi_entry, _ = bl.donchian(df, N_ENTRY) # prior N_ENTRY-bar high (shifted, causal)
|
||||||
|
_, lo_exit = bl.donchian(df, N_EXIT) # prior N_EXIT-bar low (shifted, causal)
|
||||||
|
|
||||||
|
up = c > hi_entry # upside breakout -> enter/stay long
|
||||||
|
dn = c < lo_exit # downside breakout -> exit to flat
|
||||||
|
|
||||||
|
# turtle long/flat state machine (forward scan, uses only data <= i)
|
||||||
|
n = len(c)
|
||||||
|
state = np.zeros(n)
|
||||||
|
s = 0.0
|
||||||
|
for i in range(n):
|
||||||
|
if up[i]:
|
||||||
|
s = 1.0
|
||||||
|
elif dn[i]:
|
||||||
|
s = 0.0
|
||||||
|
state[i] = s
|
||||||
|
|
||||||
|
# size the long with causal vol-targeting (shrinks into vol spikes -> caps DD)
|
||||||
|
pos = bl.vol_target(state, df, target_vol=TARGET_VOL,
|
||||||
|
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
|
||||||
|
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
"""agent_10_keltner — ANGLE: Keltner channel breakout (long / flat).
|
||||||
|
|
||||||
|
Idea (assigned angle): a Keltner channel is an EMA mid-line wrapped by an ATR band,
|
||||||
|
upper[i] = EMA_N(close)[i-1] + K * ATR_M[i-1]
|
||||||
|
lower[i] = EMA_N(close)[i-1] - K_EXIT * ATR_M[i-1]
|
||||||
|
Ride breakouts: go LONG when close[i] pierces the prior-bar UPPER band (an upside
|
||||||
|
breakout out of the channel); EXIT to FLAT when close[i] pierces the prior-bar LOWER
|
||||||
|
band. Hold the long between those two events (a turtle-style state machine) so we stay
|
||||||
|
in persistent trends and keep turnover (fees) low. Tune N, M, K, K_EXIT on train only.
|
||||||
|
|
||||||
|
WHY LONG/FLAT, NOT LONG/SHORT (honest tuning result on split='train'):
|
||||||
|
The textbook Keltner breakout is stop-and-reverse (short below the lower band). I
|
||||||
|
tuned both. Long/SHORT tops out at sharpe_min ~1.04 (maxdd ~0.39); switching the short
|
||||||
|
leg to FLAT lifts sharpe_min to ~1.56 and cuts maxdd to ~0.28. On BOTH series the short
|
||||||
|
leg is value-destroying: the pair trends up, so downside breakouts are mostly V-shaped
|
||||||
|
bottoms / chop where a short gets whipsawed. So the breakout *exit* is kept (a lower-
|
||||||
|
band break flattens us) but we never flip short. The Keltner breakout EVENT still drives
|
||||||
|
every entry and exit — the angle is intact.
|
||||||
|
|
||||||
|
Tuned on split='train' (Series A & B, equal weight). Broad plateau: 59/340 nearby cells
|
||||||
|
keep sharpe_min > 1.40, so the chosen point is a plateau CENTER, not an isolated peak:
|
||||||
|
* N_EMA = 20 (Keltner mid-line EMA span)
|
||||||
|
* N_ATR = 30 (ATR window for the band half-width)
|
||||||
|
* K = 1.0 (entry band multiplier: close above EMA + 1.0*ATR -> upside breakout)
|
||||||
|
* K_EXIT = 0.5 (exit band multiplier: close below EMA - 0.5*ATR -> flatten; tighter
|
||||||
|
than entry so we exit a failing trend faster than we re-enter)
|
||||||
|
* vol-target the long to 30% ann vol (vol_win=30d, cap 1.0): the long size shrinks into
|
||||||
|
vol spikes (every crash is a vol spike) -> caps the drawdown of late/whipsaw entries.
|
||||||
|
Sharpe is ~flat (1.55-1.56) across target_vol 0.20-0.40; target_vol only trades PnL
|
||||||
|
for DD (0.20 -> pnl 2.7/DD 0.19 ... 0.40 -> pnl 9.2/DD 0.34). 0.30 is the balance.
|
||||||
|
|
||||||
|
Causality: the channel that close[i] is tested against is EMA/ATR evaluated at i-1 (one-
|
||||||
|
bar lag via .shift(1)), so it is built from bars STRICTLY before i; a close[i] that
|
||||||
|
pierces it is a real, tradeable event at close[i]. The state machine is a forward scan
|
||||||
|
(uses only data <= i). The evaluator then holds the position during bar i+1. No future
|
||||||
|
rows -> causality_ok = true.
|
||||||
|
|
||||||
|
Train (combined A&B): pnl_mean ~5.55, maxdd_worst ~0.28, sharpe_min ~1.56.
|
||||||
|
Honest note: Keltner breakout is pure trend-following, not alpha. Its value here is
|
||||||
|
converting a high-PnL / ~77-79%-DD uptrend into comparable PnL at ~28% drawdown (DD cut
|
||||||
|
~2.7x). The full long/short Keltner was MUCH worse (sharpe_min ~1.04, DD ~0.39) — the
|
||||||
|
edge that matters is the FLAT side, exactly as for the sibling donchian breakout.
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import blindlib as bl
|
||||||
|
|
||||||
|
N_EMA = 20 # Keltner mid-line EMA span
|
||||||
|
N_ATR = 30 # ATR window for the band half-width
|
||||||
|
K = 1.0 # entry band multiplier: break of EMA + K*ATR -> long
|
||||||
|
K_EXIT = 0.5 # exit band multiplier: break of EMA - K_EXIT*ATR -> flat
|
||||||
|
TARGET_VOL = 0.30
|
||||||
|
VOL_WIN_DAYS = 30
|
||||||
|
LEV_CAP = 1.0
|
||||||
|
|
||||||
|
|
||||||
|
def _keltner_band(df, n_ema, n_atr, k):
|
||||||
|
"""Lagged Keltner upper/lower at multiplier k: EMA[i-1] +/- k*ATR[i-1]."""
|
||||||
|
c = df["close"].values.astype(float)
|
||||||
|
mid = pd.Series(bl.ema(c, n_ema)).shift(1).values # EMA built <= i-1
|
||||||
|
band = pd.Series(bl.atr(df, n_atr)).shift(1).values # ATR built <= i-1
|
||||||
|
return mid + k * band, mid - k * band
|
||||||
|
|
||||||
|
|
||||||
|
def signal(df):
|
||||||
|
c = df["close"].values.astype(float)
|
||||||
|
upper, _ = _keltner_band(df, N_EMA, N_ATR, K) # entry channel (wider)
|
||||||
|
_, lower = _keltner_band(df, N_EMA, N_ATR, K_EXIT) # exit channel (tighter)
|
||||||
|
|
||||||
|
up = c > upper # upside breakout -> enter / stay long (tradeable at close[i])
|
||||||
|
dn = c < lower # downside breakout of tighter band -> exit to flat
|
||||||
|
|
||||||
|
# turtle long/flat state machine (forward scan, uses only data <= i).
|
||||||
|
n = len(c)
|
||||||
|
state = np.zeros(n)
|
||||||
|
s = 0.0
|
||||||
|
for i in range(n):
|
||||||
|
if np.isfinite(upper[i]) and up[i]:
|
||||||
|
s = 1.0
|
||||||
|
elif np.isfinite(lower[i]) and dn[i]:
|
||||||
|
s = 0.0
|
||||||
|
state[i] = s
|
||||||
|
|
||||||
|
# size the long with causal vol-targeting (shrinks into vol spikes -> caps DD).
|
||||||
|
pos = bl.vol_target(state, df, target_vol=TARGET_VOL,
|
||||||
|
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
|
||||||
|
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
"""agent_11_squeeze — ANGLE [family=breakout, slug=squeeze].
|
||||||
|
|
||||||
|
Range-compression (NR / Bollinger-squeeze) THEN expansion: after a low-volatility
|
||||||
|
"coil", price tends to break out and run. We (1) detect the squeeze causally, (2) wait
|
||||||
|
for the breakout out of the coil, (3) enter in the breakout direction, vol-targeted.
|
||||||
|
|
||||||
|
Mechanics (all causal — value at i uses only rows 0..i):
|
||||||
|
* SQUEEZE detector: Bollinger bandwidth = (BB_upper - BB_lower) / mid, using a
|
||||||
|
rolling window ending at i. A bar is "coiled" when its bandwidth sits in the low
|
||||||
|
tail of its own EXPANDING history (causal percentile, no future). This is the
|
||||||
|
classic Bollinger-squeeze / NR proxy: bands pinch when realized vol compresses.
|
||||||
|
* BREAKOUT trigger: a Donchian channel built STRICTLY from bars < i (bl.donchian
|
||||||
|
shifts by 1). When close[i] pierces the prior N-bar high -> upside expansion;
|
||||||
|
pierces the prior N-bar low -> downside expansion. The break is only ARMED if we
|
||||||
|
were recently in a squeeze (coil within the last LOOKBACK bars) — that is the
|
||||||
|
whole thesis: expansion out of compression, not a random breakout.
|
||||||
|
* STATE machine: once a squeeze-armed breakout fires, carry that side (stop-and-
|
||||||
|
reverse on the opposite squeeze-armed breakout) so we ride the post-coil
|
||||||
|
expansion and keep turnover low. Decay to flat if the move stalls back inside
|
||||||
|
the channel for a while (the coil's energy is spent).
|
||||||
|
* SIZING: the +/-1 direction is vol-targeted (TP01-style) so exposure shrinks into
|
||||||
|
vol spikes -> caps drawdown on whipsaws / failed breakouts.
|
||||||
|
|
||||||
|
Tuned ONLY on split='train' (Series A and B, equal weight). Causality verified by the
|
||||||
|
harness (signal on a prefix matches signal on the full array over its tail).
|
||||||
|
|
||||||
|
Honest notes:
|
||||||
|
* Squeeze-breakout is trend-following with a regime filter. On these trending curves
|
||||||
|
it captures up-legs with ~3x less drawdown than buy&hold (DD ~29% vs ~70-80%) at
|
||||||
|
only ~25-33% time-in-market; the cost is failed-breakout whipsaws after a fake-out
|
||||||
|
coil. Value is risk-adjusted, not raw PnL.
|
||||||
|
* Shorts were dropped (SHORT_SCALE=0): on both train curves the downside-breakout leg
|
||||||
|
was a net loser (coils on an uptrend mostly fake out down -> V-bottoms), so the
|
||||||
|
long/flat version is strictly better on Sharpe AND drawdown.
|
||||||
|
* ABLATION CAVEAT: a pure Donchian breakout with the SAME hold/exit logic but NO coil
|
||||||
|
gate scores marginally HIGHER on train (Sh ~1.05 / PnL ~1.34) than the coil-gated
|
||||||
|
version. The squeeze gate trims turnover and DD but is NOT the source of the edge
|
||||||
|
here — the edge is the breakout + vol-target. Kept the coil gate because the
|
||||||
|
assigned angle is *squeeze*; it is a mild, honest improvement on risk, not magic.
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import blindlib as bl
|
||||||
|
|
||||||
|
# --- tuned on split='train' (broad plateau, see header / grid in commit) ------
|
||||||
|
BB_WIN = 20 # Bollinger window for bandwidth
|
||||||
|
BB_K = 2.0 # Bollinger multiplier
|
||||||
|
SQ_PCTL = 0.45 # bandwidth below this expanding-percentile = coil (sub-median
|
||||||
|
# compression; tighter pctl over-filters and loses good breaks)
|
||||||
|
DON_WIN = 25 # Donchian breakout lookback
|
||||||
|
ARM_LOOKBACK = 15 # breakout must occur within this many bars of a coil
|
||||||
|
HOLD_BARS = 40 # ride the post-coil expansion for ~this many bars, then decay
|
||||||
|
STALL_BARS = 12 # if price falls back inside the channel this long, exit early
|
||||||
|
SHORT_SCALE = 0.0 # downside-breakout sizing (0 = long/flat; coils on these
|
||||||
|
# uptrends mostly fake out to the downside, so shorts bleed)
|
||||||
|
TARGET_VOL = 0.20
|
||||||
|
VOL_WIN_DAYS = 30
|
||||||
|
LEV_CAP = 1.0
|
||||||
|
|
||||||
|
|
||||||
|
def _expanding_pctl_rank(x: np.ndarray, min_n: int = 60) -> np.ndarray:
|
||||||
|
"""Causal expanding percentile rank of x[i] within x[0..i]. rank in [0,1].
|
||||||
|
rank = fraction of past (<=i) values that are <= x[i]. Uses only rows 0..i."""
|
||||||
|
n = len(x)
|
||||||
|
out = np.full(n, np.nan)
|
||||||
|
# incremental sorted insertion would be O(n log n); n~2000 so an O(n^2) pass is
|
||||||
|
# fine (<30s). Keep it simple and obviously causal.
|
||||||
|
for i in range(n):
|
||||||
|
xi = x[i]
|
||||||
|
if not np.isfinite(xi):
|
||||||
|
continue
|
||||||
|
window = x[: i + 1]
|
||||||
|
valid = window[np.isfinite(window)]
|
||||||
|
if len(valid) < min_n:
|
||||||
|
continue
|
||||||
|
out[i] = float(np.mean(valid <= xi))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def signal(df):
|
||||||
|
c = df["close"].values.astype(float)
|
||||||
|
n = len(c)
|
||||||
|
|
||||||
|
# 1) Bollinger bandwidth (causal) -> squeeze when bandwidth is in its low tail.
|
||||||
|
upper, mid, lower = bl.bbands(c, BB_WIN, BB_K)
|
||||||
|
with np.errstate(invalid="ignore", divide="ignore"):
|
||||||
|
bw = (upper - lower) / np.where(np.abs(mid) > 0, mid, np.nan)
|
||||||
|
bw_rank = _expanding_pctl_rank(bw, min_n=max(60, BB_WIN * 2))
|
||||||
|
coil = np.nan_to_num(bw_rank, nan=1.0) <= SQ_PCTL # True where compressed
|
||||||
|
|
||||||
|
# "recently coiled" = a coil within the last ARM_LOOKBACK bars (causal).
|
||||||
|
coil_recent = (
|
||||||
|
pd.Series(coil.astype(float)).rolling(ARM_LOOKBACK, min_periods=1).max().values > 0
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2) Donchian breakout (prior-bar channel; bl.donchian already shifts by 1).
|
||||||
|
don_hi, don_lo = bl.donchian(df, DON_WIN)
|
||||||
|
up_break = np.isfinite(don_hi) & (c > don_hi)
|
||||||
|
dn_break = np.isfinite(don_lo) & (c < don_lo)
|
||||||
|
|
||||||
|
# 3) state machine: arm breakouts only when they expand out of a recent coil.
|
||||||
|
# The thesis is that the EDGE lives in the expansion right after the coil, so
|
||||||
|
# we ride a fired breakout for HOLD_BARS then decay to flat (the coil's energy
|
||||||
|
# is spent). A fresh squeeze-armed breakout re-arms / re-times the hold. We
|
||||||
|
# exit early if price collapses back inside the channel (failed breakout).
|
||||||
|
state = np.zeros(n)
|
||||||
|
s = 0.0
|
||||||
|
age = 0 # bars since the active breakout fired
|
||||||
|
inside_count = 0 # consecutive bars back inside the channel since trigger
|
||||||
|
for i in range(n):
|
||||||
|
armed = coil_recent[i]
|
||||||
|
fired = False
|
||||||
|
if armed and up_break[i]:
|
||||||
|
s = 1.0; age = 0; inside_count = 0; fired = True
|
||||||
|
elif armed and dn_break[i]:
|
||||||
|
s = -SHORT_SCALE; age = 0; inside_count = 0; fired = (SHORT_SCALE > 0)
|
||||||
|
|
||||||
|
if not fired and s != 0.0:
|
||||||
|
age += 1
|
||||||
|
# failed-breakout guard: price back inside the prior channel
|
||||||
|
in_channel = True
|
||||||
|
if np.isfinite(don_hi[i]) and c[i] > don_hi[i]:
|
||||||
|
in_channel = False
|
||||||
|
if np.isfinite(don_lo[i]) and c[i] < don_lo[i]:
|
||||||
|
in_channel = False
|
||||||
|
inside_count = inside_count + 1 if in_channel else 0
|
||||||
|
if inside_count >= STALL_BARS or age >= HOLD_BARS:
|
||||||
|
s = 0.0; age = 0; inside_count = 0
|
||||||
|
state[i] = s
|
||||||
|
|
||||||
|
# 4) size by causal vol-targeting (shrinks into vol spikes -> caps DD).
|
||||||
|
pos = bl.vol_target(state, df, target_vol=TARGET_VOL,
|
||||||
|
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
|
||||||
|
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
"""agent_12_pivot — ANGLE: rolling support/resistance PIVOT breakout + confirmation bar.
|
||||||
|
|
||||||
|
Idea (assigned angle, family=breakout / slug=pivot):
|
||||||
|
Build dynamic SUPPORT and RESISTANCE from swing PIVOTS (fractal turning points), not
|
||||||
|
from a flat Donchian channel. A pivot HIGH at bar k is a local maximum with `LR` bars
|
||||||
|
higher-or-equal on each side; a pivot LOW the mirror. Resistance = the most recent
|
||||||
|
CONFIRMED pivot-high price; support = the most recent confirmed pivot-low price.
|
||||||
|
A BREAKOUT is close[i] printing above resistance (long) / below support (short).
|
||||||
|
We require a CONFIRMATION BAR: the breakout must hold for `CONFIRM` consecutive closes
|
||||||
|
(filters the one-bar wick fake-out) before we take the position.
|
||||||
|
|
||||||
|
CAUSALITY — the crux of a pivot signal:
|
||||||
|
A pivot at bar k can only be CONFIRMED `LR` bars later (you need the `LR` right-side bars
|
||||||
|
to know k was a local extreme). So the resistance/support level available at bar i is the
|
||||||
|
newest pivot whose confirmation bar k+LR <= i. We build the level series with a forward
|
||||||
|
scan that, at each i, only looks at pivots already confirmed by bars <= i. No future rows
|
||||||
|
enter the level at i. The breakout test then compares close[i] (known at i) to that level,
|
||||||
|
and the evaluator holds the resulting position during bar i+1. causality_ok -> true.
|
||||||
|
|
||||||
|
LONG/SHORT vs LONG/FLAT (honest tuning on split='train', both A & B equal weight):
|
||||||
|
Textbook pivot breakout is stop-and-reverse. On these two strongly up-trending curves the
|
||||||
|
SHORT leg destroys risk-adjusted value (downside pivot breaks are mostly V-bottoms / chop
|
||||||
|
that whipsaw a short). Best train Sharpe came from LONG on a confirmed resistance break,
|
||||||
|
going FLAT on a confirmed support break — keep the breakout EXIT, never flip short. Sized
|
||||||
|
with causal vol-targeting so the long shrinks into vol spikes (every crash is a vol spike),
|
||||||
|
which caps the drawdown of late / whipsaw entries.
|
||||||
|
|
||||||
|
Tuned params — broad plateau on train (both A & B), NOT an isolated peak. Sharpe_min holds
|
||||||
|
~1.30-1.36 across LR 3..4, CONFIRM 3, target_vol 0.20..0.40, vol_win 20..45 (sweep in commit
|
||||||
|
notes): the edge is structural, not a fitted corner. Chosen for the best PnL-at-low-DD balance:
|
||||||
|
LR=4 (pivot half-window), CONFIRM=3 (closes the break must hold), vol-target 30% / 30d / cap 1.
|
||||||
|
-> train combined: pnl_mean ~4.40, maxdd_worst ~0.26, sharpe_min ~1.33.
|
||||||
|
|
||||||
|
Honest note: like every breakout on a trending pair this is trend-following, not alpha. Its
|
||||||
|
value is converting a high-PnL / ~77%-DD uptrend into comparable PnL at ~26% drawdown (DD cut
|
||||||
|
~3x). The CONFIRMATION BAR is what separates it from a plain Donchian: it adds ~0.06-0.10
|
||||||
|
Sharpe and trims the DD by ignoring one-bar wick breaks of the pivot level.
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import blindlib as bl
|
||||||
|
|
||||||
|
LR = 4 # pivot half-window: local extreme vs LR bars each side
|
||||||
|
CONFIRM = 3 # breakout must hold this many consecutive closes (confirmation bar)
|
||||||
|
TARGET_VOL = 0.30
|
||||||
|
VOL_WIN_DAYS = 30
|
||||||
|
LEV_CAP = 1.0
|
||||||
|
|
||||||
|
|
||||||
|
def _pivot_levels(high, low, lr):
|
||||||
|
"""Causal nearest-confirmed-pivot resistance & support.
|
||||||
|
|
||||||
|
pivot high at k := high[k] == max(high[k-lr .. k+lr]) (>= neighbours)
|
||||||
|
It is CONFIRMED (knowable) only at bar k+lr. We emit, for every bar i, the price of
|
||||||
|
the most recent pivot high/low confirmed at a bar <= i. Pure forward scan, data <= i.
|
||||||
|
"""
|
||||||
|
n = len(high)
|
||||||
|
res = np.full(n, np.nan) # nearest confirmed pivot-HIGH price (resistance)
|
||||||
|
sup = np.full(n, np.nan) # nearest confirmed pivot-LOW price (support)
|
||||||
|
cur_res = np.nan
|
||||||
|
cur_sup = np.nan
|
||||||
|
for i in range(n):
|
||||||
|
# a pivot centred at k = i-lr becomes confirmable exactly now (its right window
|
||||||
|
# k+1..k+lr == i-lr+1..i is complete and all <= i; left window also <= i).
|
||||||
|
k = i - lr
|
||||||
|
if k - lr >= 0:
|
||||||
|
seg_h = high[k - lr:i + 1] # high[k-lr .. i] = high[k-lr .. k+lr]
|
||||||
|
seg_l = low[k - lr:i + 1]
|
||||||
|
hk = high[k]
|
||||||
|
lk = low[k]
|
||||||
|
if hk >= seg_h.max(): # k is a (weak) local max -> pivot high
|
||||||
|
cur_res = hk
|
||||||
|
if lk <= seg_l.min(): # k is a local min -> pivot low
|
||||||
|
cur_sup = lk
|
||||||
|
res[i] = cur_res
|
||||||
|
sup[i] = cur_sup
|
||||||
|
return res, sup
|
||||||
|
|
||||||
|
|
||||||
|
def signal(df):
|
||||||
|
high = df["high"].values.astype(float)
|
||||||
|
low = df["low"].values.astype(float)
|
||||||
|
c = df["close"].values.astype(float)
|
||||||
|
n = len(c)
|
||||||
|
|
||||||
|
res, sup = _pivot_levels(high, low, LR)
|
||||||
|
|
||||||
|
# raw breakout events (causal: level + close both known at i)
|
||||||
|
brk_up = c > res # close above resistance pivot
|
||||||
|
brk_dn = c < sup # close below support pivot
|
||||||
|
brk_up = np.nan_to_num(brk_up, nan=False).astype(bool)
|
||||||
|
brk_dn = np.nan_to_num(brk_dn, nan=False).astype(bool)
|
||||||
|
|
||||||
|
# CONFIRMATION BAR: require the break to hold CONFIRM consecutive closes.
|
||||||
|
if CONFIRM > 1:
|
||||||
|
up_run = pd.Series(brk_up).rolling(CONFIRM, min_periods=CONFIRM).sum().values == CONFIRM
|
||||||
|
dn_run = pd.Series(brk_dn).rolling(CONFIRM, min_periods=CONFIRM).sum().values == CONFIRM
|
||||||
|
up_run = np.nan_to_num(up_run, nan=False).astype(bool)
|
||||||
|
dn_run = np.nan_to_num(dn_run, nan=False).astype(bool)
|
||||||
|
else:
|
||||||
|
up_run, dn_run = brk_up, brk_dn
|
||||||
|
|
||||||
|
# long/flat state machine (forward scan, data <= i):
|
||||||
|
# confirmed resistance break -> long ; confirmed support break -> flat.
|
||||||
|
state = np.zeros(n)
|
||||||
|
s = 0.0
|
||||||
|
for i in range(n):
|
||||||
|
if up_run[i]:
|
||||||
|
s = 1.0
|
||||||
|
elif dn_run[i]:
|
||||||
|
s = 0.0
|
||||||
|
state[i] = s
|
||||||
|
|
||||||
|
pos = bl.vol_target(state, df, target_vol=TARGET_VOL,
|
||||||
|
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
|
||||||
|
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
"""agent_13_volbreak — ANGLE [family=breakout, slug=volbreak].
|
||||||
|
|
||||||
|
Volatility breakout: enter the trend direction when REALIZED VOL EXPANDS above its
|
||||||
|
rolling median. The thesis: a fresh expansion of realized volatility marks a regime
|
||||||
|
of large, directional moves (a breakout out of a quiet base). When vol picks up we
|
||||||
|
align with the prevailing trend and ride it; when vol is compressed / below its
|
||||||
|
rolling median we stand aside (no breakout in progress, just chop).
|
||||||
|
|
||||||
|
Mechanics (all causal — value at i uses only rows 0..i):
|
||||||
|
* VOL EXPANSION gate: annualized realized vol over a short window (RV_WIN) vs its
|
||||||
|
own rolling median over a longer lookback (MED_WIN). "Expanded" when
|
||||||
|
rv[i] > EXP_K * median(rv up to i). bl.realized_vol and pandas rolling are causal.
|
||||||
|
* TREND direction: sign of price vs a moving average (close / SMA(TREND_WIN) - 1),
|
||||||
|
decided at close[i]. This is the direction we take *only while* vol is expanded.
|
||||||
|
* STATE / persistence: once vol expands we lock onto the current trend side and
|
||||||
|
hold it (stop-and-reverse if the trend sign flips while still expanded) until vol
|
||||||
|
falls back BELOW its median (expansion over) -> flat. This rides the whole
|
||||||
|
high-vol leg instead of flickering bar to bar, keeping turnover (fees) down.
|
||||||
|
* SIZING: the +1/0 direction is vol-targeted (TP01-style) so exposure shrinks into
|
||||||
|
the very vol spikes the gate selects -> caps drawdown on violent reversals.
|
||||||
|
|
||||||
|
Tuned ONLY on split='train' (Series A and B, equal weight; broad plateau grid below).
|
||||||
|
Causality verified by the harness (signal on a prefix matches signal on the full array
|
||||||
|
over its tail).
|
||||||
|
|
||||||
|
Honest notes:
|
||||||
|
* On these strongly-trending high-vol curves the edge is essentially "be long the
|
||||||
|
trend, but ONLY when vol confirms a breakout, and shrink size into vol". Value is
|
||||||
|
RISK-ADJUSTED: comparable/positive PnL at ~3-4x less drawdown than buy&hold (which
|
||||||
|
eats ~77-79% DD here), not bigger raw PnL. Train combined Sharpe ~1.12, worst-DD
|
||||||
|
~23%, mean PnL ~1.14.
|
||||||
|
* LONG-ONLY (SHORT_SCALE=0). Shorts were dropped after tuning: on these uptrends the
|
||||||
|
down-trend + vol-expansion combo is dominated by violent V-bottom reversals, which
|
||||||
|
are terrible to short -> a short leg (full OR damped) strictly LOWERED Sharpe and
|
||||||
|
raised DD on both train curves. The short leg is not an edge here; flat is better.
|
||||||
|
* EXP_K=0.8 means we trade when rv sits at/above 0.8x its rolling median — still a
|
||||||
|
genuine vol-expansion gate (it stands aside in the lowest-vol ~30-40% of bars where
|
||||||
|
price just chops), but inclusive enough not to miss the early part of a breakout
|
||||||
|
leg. Requiring rv strictly ABOVE the median (K>=1.0) entered too late and gutted the
|
||||||
|
Series-B trend capture (Sh 1.12 -> 0.28). The plateau holds for RV 15-20, MED
|
||||||
|
100-150, K 0.78-0.85, TREND 30-60.
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import blindlib as bl
|
||||||
|
|
||||||
|
# --- tuned on split='train' (broad plateau) ---------------------------------
|
||||||
|
RV_WIN = 15 # short realized-vol window (the "current" vol)
|
||||||
|
MED_WIN = 100 # rolling-median lookback for the vol baseline
|
||||||
|
EXP_K = 0.80 # vol is "expanded" when rv > EXP_K * rolling-median(rv)
|
||||||
|
TREND_WIN = 50 # trend filter: sign of close / SMA(TREND_WIN) - 1
|
||||||
|
SHORT_SCALE = 0.0 # LONG-ONLY: down-vol-breaks here are mostly V-reversals -> shorts bleed
|
||||||
|
TARGET_VOL = 0.20
|
||||||
|
VOL_WIN_DAYS = 30
|
||||||
|
LEV_CAP = 1.5
|
||||||
|
|
||||||
|
|
||||||
|
def signal(df):
|
||||||
|
c = df["close"].values.astype(float)
|
||||||
|
n = len(c)
|
||||||
|
bpy = bl.bars_per_day(df) * 365.25
|
||||||
|
|
||||||
|
# 1) realized vol (short) and its causal rolling median baseline.
|
||||||
|
r = bl.simple_returns(c)
|
||||||
|
rv = bl.realized_vol(r, RV_WIN, bpy)
|
||||||
|
rv_med = pd.Series(rv).rolling(MED_WIN, min_periods=max(10, MED_WIN // 2)).median().values
|
||||||
|
expanded = np.isfinite(rv) & np.isfinite(rv_med) & (rv > EXP_K * rv_med)
|
||||||
|
|
||||||
|
# 2) trend direction decided at close[i] (causal).
|
||||||
|
ma = bl.sma(c, TREND_WIN)
|
||||||
|
with np.errstate(invalid="ignore", divide="ignore"):
|
||||||
|
trend = np.where(np.isfinite(ma) & (ma > 0), c / ma - 1.0, 0.0)
|
||||||
|
tsign = np.sign(trend)
|
||||||
|
|
||||||
|
# 3) state machine: while vol is expanded, hold the trend side (S&R on sign flip);
|
||||||
|
# when vol falls back below its (scaled) median the breakout is spent -> flat.
|
||||||
|
state = np.zeros(n)
|
||||||
|
s = 0.0
|
||||||
|
for i in range(n):
|
||||||
|
if expanded[i]:
|
||||||
|
if tsign[i] > 0:
|
||||||
|
s = 1.0
|
||||||
|
elif tsign[i] < 0:
|
||||||
|
s = -SHORT_SCALE
|
||||||
|
# tsign == 0 -> keep current side
|
||||||
|
else:
|
||||||
|
s = 0.0
|
||||||
|
state[i] = s
|
||||||
|
|
||||||
|
# 4) size by causal vol-targeting (shrinks into vol spikes -> caps DD).
|
||||||
|
pos = bl.vol_target(state, df, target_vol=TARGET_VOL,
|
||||||
|
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
|
||||||
|
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
"""Agent 14 — RSI reversion, trend-gated (family=meanrev, slug=rsi).
|
||||||
|
|
||||||
|
The angle (assigned): RSI reversion. Long when RSI<lo, short when RSI>hi (bl.rsi),
|
||||||
|
GATED by a longer trend filter. Tune lo/hi/win.
|
||||||
|
|
||||||
|
Reading the train curves first (both A and B, split='train'): they trend UP hard
|
||||||
|
(ann vol ~0.7-0.9, total ret +6.7x / +23x over the window). The TEXTBOOK 30/70 RSI
|
||||||
|
thresholds are dead here: in these up-curves RSI sits >70 ~11% of bars and the dips
|
||||||
|
only floor around RSI 40-45 — RSI<30 in an uptrend happens ~0.1% of the time. A naive
|
||||||
|
symmetric "short every RSI>70" rule would just short the bull and bleed. So the
|
||||||
|
mean-reversion has to be REGIME-AWARE, and the lo/hi have to be tuned to the data's
|
||||||
|
actual RSI distribution, not the textbook:
|
||||||
|
|
||||||
|
* In an UPTREND (close above a long SMA) RSI dips are BUY-THE-DIP reversion. We go
|
||||||
|
LONG when RSI drops below LO and HOLD that long (hysteresis) until RSI recovers
|
||||||
|
past a higher EXIT level — the classic RSI entry/exit pair — then flat. We do NOT
|
||||||
|
short RSI>hi here (overbought in an uptrend keeps running; that is momentum).
|
||||||
|
* In a DOWNTREND (close below the long SMA) the symmetry returns: RSI>HI is a
|
||||||
|
reversion SHORT (rips fade back down); RSI<LO we stand flat (don't knife-catch
|
||||||
|
long against a downtrend). The short side is weighted < 1 because the curves drift
|
||||||
|
up — on train it adds a touch of PnL with no DD cost but is not where the edge is.
|
||||||
|
|
||||||
|
The long trend filter does two jobs: it picks WHICH side of the RSI book is reversion
|
||||||
|
(buy dips in up-trend / sell rips in down-trend) and it suppresses the side that fights
|
||||||
|
the drift. TREND_WIN=150 is the DD sweet spot on train (DD 0.11 vs 0.16-0.21 at 100/200)
|
||||||
|
— the gate is what keeps the drawdown small. Sizing is smooth (further past the
|
||||||
|
threshold -> bigger appetite, no hard 0/1 fee-churning flips) then vol-targeted so the
|
||||||
|
two curves are risk-comparable and exposure shrinks into vol spikes (crashes are vol
|
||||||
|
spikes), bounding the drawdown.
|
||||||
|
|
||||||
|
HONEST NOTE: in a market that trends this hard, a trend-gated RSI dip-buy partially
|
||||||
|
degenerates toward trend participation — the dips it buys are shallow (RSI ~50s, not
|
||||||
|
30s) and it rides them up. The genuine reversion content is the buy-low/exit-high cycle
|
||||||
|
and the DD control from the trend gate + vol-target; the short side carries almost no
|
||||||
|
weight in the train edge. The result is an honest-but-modest combined train Sharpe ~1.1
|
||||||
|
at ~11% DD (vs long-only buy&hold's ~7-23x PnL at ~70-80% DD) — i.e. a fraction of the
|
||||||
|
buy&hold PnL but ~6-7x less drawdown.
|
||||||
|
|
||||||
|
CAUSAL: rsi() is an EWMA of past gains/losses (<= i); the SMA trend filter is trailing;
|
||||||
|
the hold-state is a forward cumulative pass over PAST bars only; vol_target uses trailing
|
||||||
|
realized vol. No shift(-k), no centered windows, no global fit. Verified by causality_ok
|
||||||
|
(max_diff 0.0).
|
||||||
|
|
||||||
|
Tuning (train only, combined A&B; coarse->fine sweep + plateau check). Chosen cell is
|
||||||
|
INTERIOR on every axis — RW in [18..25], LO in [56..62], EXIT in [75..85], TWIN=150,
|
||||||
|
TVOL [0.20..0.25] all stay sharpe_min ~1.0..1.26 at DD ~0.11..0.13, a broad plateau not
|
||||||
|
a spike. (Pushing LO/EXIT higher keeps lifting train Sharpe but only by degenerating into
|
||||||
|
buy-and-hold, so we stop at an interior dip-entry cell that is still genuinely a dip rule.)
|
||||||
|
RSI_WIN=20, LO=58, HI=68, EXIT=78, TREND_WIN=150
|
||||||
|
SHORT_W=0.5, TARGET_VOL=0.25, VOL_WIN_DAYS=35, LEV_CAP=1.5, BASE=0.6
|
||||||
|
-> train combined: pnl_mean ~0.87, maxdd_worst ~0.11, sharpe_min ~1.14
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
import blindlib as bl
|
||||||
|
|
||||||
|
RSI_WIN = 20 # RSI lookback (the "win" of the angle; 20 > textbook 14 for these trends)
|
||||||
|
LO = 58.0 # oversold/dip threshold -> reversion LONG (tuned to the curves' RSI floor)
|
||||||
|
HI = 68.0 # overbought threshold -> reversion SHORT (downtrend only)
|
||||||
|
EXIT = 78.0 # dip-long is HELD until RSI recovers past EXIT (hysteresis entry/exit pair)
|
||||||
|
TREND_WIN = 150 # long SMA: above = uptrend (buy dips), below = downtrend (sell rips). DD sweet spot.
|
||||||
|
SHORT_W = 0.5 # weight on the downtrend short side; <1 because the curves drift up
|
||||||
|
BASE = 0.6 # base long size while holding a dip (scaled up if still oversold)
|
||||||
|
TARGET_VOL = 0.25
|
||||||
|
VOL_WIN_DAYS = 35
|
||||||
|
LEV_CAP = 1.5
|
||||||
|
|
||||||
|
|
||||||
|
def signal(df):
|
||||||
|
c = df["close"].values.astype(float)
|
||||||
|
n = len(c)
|
||||||
|
rs = bl.rsi(c, RSI_WIN)
|
||||||
|
trend_up = c > bl.sma(c, TREND_WIN) # causal trailing SMA trend gate
|
||||||
|
|
||||||
|
# --- smooth reversion appetite from RSI (further past threshold -> bigger) ---
|
||||||
|
long_app = np.clip((LO - rs) / 25.0, 0.0, 1.0) # oversold -> long appetite
|
||||||
|
short_app = np.clip((rs - HI) / (100.0 - HI), 0.0, 1.0) # overbought -> short appetite
|
||||||
|
|
||||||
|
# --- trend-gated RSI reversion with hysteresis on the dip-long ---
|
||||||
|
# The forward pass below is PURE PAST-ONLY: in_long at bar i depends only on bars <= i
|
||||||
|
# (rs, trend_up are causal; the state machine never looks ahead). Causality verified.
|
||||||
|
held = np.zeros(n)
|
||||||
|
in_long = False
|
||||||
|
for i in range(n):
|
||||||
|
if in_long:
|
||||||
|
# exit the held dip-long when the trend breaks down OR RSI has recovered
|
||||||
|
if (not trend_up[i]) or (rs[i] >= EXIT):
|
||||||
|
in_long = False
|
||||||
|
else:
|
||||||
|
# enter a dip-long only in an uptrend when RSI is below LO (oversold dip)
|
||||||
|
if trend_up[i] and rs[i] < LO:
|
||||||
|
in_long = True
|
||||||
|
if in_long:
|
||||||
|
held[i] = max(BASE, long_app[i]) # ride the recovery, bigger if still oversold
|
||||||
|
else:
|
||||||
|
# when not holding a long, only the downtrend reversion-short passes through
|
||||||
|
held[i] = (-SHORT_W * short_app[i]) if (not trend_up[i]) else 0.0
|
||||||
|
|
||||||
|
pos = bl.vol_target(held, df, target_vol=TARGET_VOL,
|
||||||
|
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
|
||||||
|
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
"""Agent 15 — Bollinger-band reversion, low-vol gated (family=meanrev, slug=bbands).
|
||||||
|
|
||||||
|
The angle (assigned): fade touches of the Bollinger bands (bl.bbands), only in a
|
||||||
|
low-vol regime. Tune win, k.
|
||||||
|
|
||||||
|
What the train curves actually say (A & B, split='train', diagnosed before coding):
|
||||||
|
both trend UP hard (+6.7x / +23x, ann vol ~0.7-0.9). The TEXTBOOK symmetric band-fade
|
||||||
|
is a LOSER here and the data is blunt about why:
|
||||||
|
|
||||||
|
* UPPER-band touch -> CONTINUATION, not reversion. fwd-5bar after a close>=upper is
|
||||||
|
+3.4%/+2.7% (A/B) even when we restrict to the low-vol regime. In a bull, riding the
|
||||||
|
upper band is momentum; shorting it just bleeds against the drift. So the SHORT side
|
||||||
|
of the classic fade is dead and we do NOT take it.
|
||||||
|
* LOWER-band touch is reversion ONLY when it is a DIP IN AN UPTREND. close<=lower while
|
||||||
|
price is above a long SMA -> fwd-5bar +3.5%/+7.2% (A/B): the band stretch snaps back
|
||||||
|
up. The same lower touch in a DOWNTREND / high-vol continues DOWN (A high-vol lo-touch
|
||||||
|
fwd-5 = -3.9%): a real knife. So the reversion we keep is the buy-the-dip-in-uptrend
|
||||||
|
leg, and we gate it OFF in downtrends and in high vol.
|
||||||
|
|
||||||
|
Hence the rule is an HONEST, one-sided Bollinger reversion: LONG the lower-band touch,
|
||||||
|
but only while (a) close is above a long trend SMA and (b) realized vol is in its lower
|
||||||
|
regime (the assigned low-vol gate). %b drives a smooth appetite (deeper below the band ->
|
||||||
|
bigger), the long is HELD with hysteresis until price mean-reverts back through the mid
|
||||||
|
band, then flat. Sizing is vol-targeted so the two curves are risk-comparable and exposure
|
||||||
|
shrinks into vol spikes (which are exactly the regime where the dip-buy fails).
|
||||||
|
|
||||||
|
HONEST NOTE: in a market trending this hard a trend+lowvol-gated dip-buy partially
|
||||||
|
degenerates toward trend participation — the genuine reversion content is the buy-below-band
|
||||||
|
/ exit-at-mid cycle plus the DD control from the gates + vol-target. The symmetric short-the-
|
||||||
|
upper-band leg that "Bollinger reversion" classically implies carries NEGATIVE edge on these
|
||||||
|
curves, so taking it would only add drawdown; the result is therefore a modest-but-real
|
||||||
|
reversion edge, NOT a high-PnL alpha. A negative result for the *symmetric* fade is itself a
|
||||||
|
finding (documented above).
|
||||||
|
|
||||||
|
CAUSAL: bbands/sma/realized_vol are trailing (value at i uses bars <= i); the hold-state is
|
||||||
|
a forward cumulative pass over PAST bars only; vol_target uses trailing realized vol. No
|
||||||
|
shift(-k), no centered windows, no global fit. Verified by causality_ok (max_diff ~0).
|
||||||
|
|
||||||
|
Tuning (train only, combined A&B; coarse->fine sweep + plateau check). The chosen cell is
|
||||||
|
interior on every axis and sits on a stable plateau (neighbouring K in [1.8..2.2],
|
||||||
|
TREND_WIN in [100..150], VOL_PCT in [0.65..0.85], ENTRY_PB in [0..0.1] all give
|
||||||
|
sharpe_min ~0.43-0.48 at DD ~0.08, sharpe_mean ~0.74-0.80):
|
||||||
|
BB_WIN=20, BB_K=2.0, TREND_WIN=120, VOL_WIN=20, VOL_PCT=0.65,
|
||||||
|
ENTRY_PB=0.10 (touch lower band), EXIT_PB=0.50 (exit at the MID band),
|
||||||
|
TARGET_VOL=0.25, VOL_WIN_DAYS=30, LEV_CAP=1.5, BASE=1.0
|
||||||
|
-> train combined: pnl_mean ~0.29, maxdd_worst ~0.08, sharpe_min ~0.48 (A binds; B ~1.1).
|
||||||
|
Exiting at the mid band (not higher) is the binding choice: Series A's dips are shallow and
|
||||||
|
fizzle, so holding the reversion past mid turns Series A negative (Sharpe 0.48 -> -0.0).
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
import blindlib as bl
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
BB_WIN = 20 # Bollinger lookback ("win" of the angle)
|
||||||
|
BB_K = 2.0 # band width in std ("k" of the angle)
|
||||||
|
TREND_WIN = 120 # long SMA: dip-buy only ABOVE it (reversion lives in the uptrend)
|
||||||
|
VOL_WIN = 20 # realized-vol lookback for the low-vol gate
|
||||||
|
VOL_PCT = 0.65 # low-vol gate: only act when rolling vol is below its expanding p65
|
||||||
|
ENTRY_PB = 0.10 # enter when %b <= this (close at/below the lower band)
|
||||||
|
EXIT_PB = 0.50 # exit when %b >= this (price has mean-reverted to the MID band)
|
||||||
|
TARGET_VOL = 0.25
|
||||||
|
VOL_WIN_DAYS = 30
|
||||||
|
LEV_CAP = 1.5
|
||||||
|
BASE = 1.0 # full size while holding a dip-long (the events are sparse; ride the snap-back)
|
||||||
|
|
||||||
|
|
||||||
|
def _expanding_quantile_below(x, q):
|
||||||
|
"""Causal: at bar i, is x[i] at/below the q-quantile of x[0..i]? (expanding, no leak)."""
|
||||||
|
s = np.asarray(x, float)
|
||||||
|
thr = pd.Series(s).expanding(min_periods=30).quantile(q).values
|
||||||
|
out = s <= thr
|
||||||
|
out[~np.isfinite(thr)] = False
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def signal(df):
|
||||||
|
c = df["close"].values.astype(float)
|
||||||
|
n = len(c)
|
||||||
|
up, mid, lo = bl.bbands(c, BB_WIN, BB_K) # causal trailing bands
|
||||||
|
band_w = up - lo
|
||||||
|
# %b: 0 = at the lower band, 0.5 = at the mid band, 1 = at the upper band.
|
||||||
|
pb = np.where(np.isfinite(band_w) & (band_w > 0), (c - lo) / band_w, np.nan)
|
||||||
|
trend_up = c > bl.sma(c, TREND_WIN) # causal trend gate
|
||||||
|
|
||||||
|
r = bl.simple_returns(c)
|
||||||
|
rv = bl.realized_vol(r, VOL_WIN, 365.0) # causal trailing realized vol
|
||||||
|
low_vol = _expanding_quantile_below(rv, VOL_PCT) # causal expanding low-vol regime gate
|
||||||
|
|
||||||
|
# One-sided Bollinger reversion: buy the lower-band touch (dip) in uptrend + low-vol,
|
||||||
|
# HOLD with hysteresis until %b mean-reverts back up to the MID band, then flat. The
|
||||||
|
# symmetric upper-band SHORT is a proven loser on these curves (continuation), so flat.
|
||||||
|
# Forward pass is PURE PAST-ONLY: in_long at i depends only on bars <= i.
|
||||||
|
held = np.zeros(n)
|
||||||
|
in_long = False
|
||||||
|
for i in range(n):
|
||||||
|
if in_long:
|
||||||
|
# exit when the dip has mean-reverted to the mid band, or the trend breaks
|
||||||
|
if (not trend_up[i]) or (np.isfinite(pb[i]) and pb[i] >= EXIT_PB):
|
||||||
|
in_long = False
|
||||||
|
else:
|
||||||
|
# enter a dip-long: %b at/below the lower band, in uptrend, in low-vol regime
|
||||||
|
if trend_up[i] and low_vol[i] and np.isfinite(pb[i]) and pb[i] <= ENTRY_PB:
|
||||||
|
in_long = True
|
||||||
|
held[i] = BASE if in_long else 0.0
|
||||||
|
|
||||||
|
pos = bl.vol_target(held, df, target_vol=TARGET_VOL,
|
||||||
|
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
|
||||||
|
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
"""Agent 16 — Z-score reversion to SMA, trend-gated (family=meanrev, slug=zrev).
|
||||||
|
|
||||||
|
THE ANGLE (assigned): reversion of price to its SMA via a CAUSAL rolling z-score —
|
||||||
|
short positive extremes / long negative extremes — WITH A TREND-AGREEMENT GATE.
|
||||||
|
|
||||||
|
Why the gate is the whole story here. Naive z-reversion (short every z>+thr, long every
|
||||||
|
z<-thr against a price-vs-SMA z-score) LOSES on these two curves: both trend up ~8x/24x
|
||||||
|
over the sample, so a positive z-extreme above a medium SMA is usually momentum that keeps
|
||||||
|
going (study: z>1.5 -> next-bar +0.005/+0.008, NOT a reversal), and shorting it just fights
|
||||||
|
the trend. The reversion that actually exists is the SHORT-HORIZON pullback inside the
|
||||||
|
prevailing trend:
|
||||||
|
|
||||||
|
* In an UPTREND (price > slow SMA), a negative z-extreme (a dip below the FAST SMA) is a
|
||||||
|
pullback that bounces -> go LONG. (study: UP & z<-1 -> next-bar +0.003 .. +0.012.)
|
||||||
|
* In a DOWNTREND (price < slow SMA), a positive z-extreme (a rally above the FAST SMA) is
|
||||||
|
a dead-cat that fades -> go SHORT. (study: DOWN & z>+1 -> next-bar ~0 .. -0.004.)
|
||||||
|
* A z-extreme that DISAGREES with the trend (rally in an uptrend / dip in a downtrend) is
|
||||||
|
momentum/continuation, not reversion -> stay FLAT (those bins are where naive z-reversion
|
||||||
|
bleeds: UP & z>1 -> +0.003 continuation; you must NOT short it).
|
||||||
|
|
||||||
|
So the position is the reversion impulse (-z, clipped to extremes) FILTERED by trend
|
||||||
|
agreement: keep only longs in uptrends and shorts in downtrends. A causal vol-target then
|
||||||
|
sizes it so A and B are risk-comparable and exposure shrinks into vol spikes.
|
||||||
|
|
||||||
|
CAUSAL: zscore(c, FAST) and sma(c, SLOW) at i use only rows <= i; the trend gate and
|
||||||
|
vol_target are trailing. No shift(-k), no centered windows, no global fit. Verified by
|
||||||
|
causality_ok.
|
||||||
|
|
||||||
|
Tuning (train only, combined A&B; coarse->fine sweep). A CONTINUOUS reversion impulse
|
||||||
|
(-z, saturating) gated by the trend beats sparse extreme-only entries (more of the dips are
|
||||||
|
captured while the gate keeps the trend on your side). The chosen cell is interior on every
|
||||||
|
axis and is a plateau, not a spike: FAST 2..3, SLOW 100..150, Z_SAT 1.5..2.0 all stay in
|
||||||
|
sharpe_min ~0.6..0.8 at DD ~0.06..0.12; SHORT_W 0->0.5 only lowers sharpe_min (the downtrend
|
||||||
|
short reversion fights the structural uptrend). vol_target scales PnL<->DD linearly (sharpe
|
||||||
|
flat), so TARGET_VOL just sets the risk dial.
|
||||||
|
FAST=2, SLOW=120, Z_SAT=1.75, SHORT_W=0.0, TARGET_VOL=0.30, VOL_WIN_DAYS=30, LEV_CAP=2.0
|
||||||
|
-> train combined: pnl_mean ~0.31, maxdd_worst ~0.11, sharpe_min ~0.78
|
||||||
|
(a modest PnL at a ~10% drawdown — the reversion-in-trend captures the bounces while
|
||||||
|
sidestepping the big declines, vs long-only buy&hold's huge PnL at ~70-80% DD).
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
import blindlib as bl
|
||||||
|
|
||||||
|
FAST = 2 # short SMA for the reversion z-score (the "stretch from SMA" detector)
|
||||||
|
SLOW = 120 # slow SMA defining the trend regime for the agreement gate
|
||||||
|
Z_SAT = 1.75 # z magnitude that saturates the reversion impulse to +-1
|
||||||
|
SHORT_W = 0.0 # weight on the (gated) short leg; tuning -> 0 (long-flat best on train)
|
||||||
|
TARGET_VOL = 0.30
|
||||||
|
VOL_WIN_DAYS = 30
|
||||||
|
LEV_CAP = 2.0
|
||||||
|
|
||||||
|
|
||||||
|
def signal(df):
|
||||||
|
c = df["close"].values.astype(float)
|
||||||
|
n = len(c)
|
||||||
|
|
||||||
|
z = np.nan_to_num(bl.zscore(c, FAST), nan=0.0) # price-vs-fast-SMA, standardized (causal)
|
||||||
|
slow = bl.sma(c, SLOW) # trend regime line (causal)
|
||||||
|
uptrend = c > slow # boolean trend gate
|
||||||
|
|
||||||
|
# reversion impulse = -z: long when price is stretched BELOW its SMA (dip, z<0),
|
||||||
|
# short when stretched ABOVE (rally, z>0). Proportional, saturating at +-Z_SAT.
|
||||||
|
impulse = np.clip(-z / Z_SAT, -1.0, 1.0) # -z direction = reversion to the SMA
|
||||||
|
|
||||||
|
# TREND-AGREEMENT GATE: keep ONLY longs in an uptrend and shorts in a downtrend.
|
||||||
|
# A z-extreme that DISAGREES with the trend (rally in an uptrend / dip in a downtrend)
|
||||||
|
# is momentum/continuation, not reversion -> stay FLAT. The short leg is gated AND
|
||||||
|
# down-weighted by SHORT_W (tuning drives it to 0: both curves trend up, so the
|
||||||
|
# downtrend-short reversion only adds drawdown here).
|
||||||
|
raw = np.zeros(n)
|
||||||
|
long_ok = (impulse > 0) & uptrend # buy the dip inside an uptrend
|
||||||
|
short_ok = (impulse < 0) & (~uptrend) # fade the rally inside a downtrend
|
||||||
|
raw[long_ok] = impulse[long_ok]
|
||||||
|
raw[short_ok] = impulse[short_ok] * SHORT_W
|
||||||
|
|
||||||
|
pos = bl.vol_target(raw, df, target_vol=TARGET_VOL,
|
||||||
|
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
|
||||||
|
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
"""Agent 17 — Short-term reversal, trend-gated (family=meanrev, slug=st_reversal).
|
||||||
|
|
||||||
|
THE ANGLE (assigned): fade the last 1-3 bar move, but ONLY when the longer trend
|
||||||
|
AGREES with the fade direction. So we never fight the trend: we only take the leg of
|
||||||
|
the reversal that points the same way the slow regime already points.
|
||||||
|
|
||||||
|
* UPTREND (price > slow SMA): the trend-agreeing fade is to fade a DROP -> go LONG
|
||||||
|
the bounce. (Fading a rise here would mean shorting INTO an uptrend = fighting the
|
||||||
|
trend -> NOT allowed, stay flat on that leg.)
|
||||||
|
* DOWNTREND (price < slow SMA): the trend-agreeing fade is to fade a RISE -> go SHORT
|
||||||
|
the dead-cat. (Fading a drop here would mean longing INTO a downtrend = fighting the
|
||||||
|
trend -> NOT allowed, stay flat on that leg.)
|
||||||
|
|
||||||
|
Why this is the structure in the data (train study, both curves):
|
||||||
|
Forward 1-bar return after a 1-bar move, conditioned on the 150-SMA regime --
|
||||||
|
A UP & drop>5% -> +0.0050 (bounce) UP & rise>5% -> +0.0007 (rise gives back)
|
||||||
|
B UP & drop>5% -> +0.0115 (bounce) UP & rise>5% -> -0.0004 (rise gives back)
|
||||||
|
A DN & rise>2% -> -0.0039 (fades) DN & drop0-2% -> ~0
|
||||||
|
B DN & rise>2% -> -0.0038 (fades)
|
||||||
|
-> corr(-r, fwd) is POSITIVE in both regimes (UP ~0.03-0.08, DN ~0.15): a 1-bar move
|
||||||
|
partially reverses next bar. The trend gate keeps only the half of that reversion
|
||||||
|
that the slow trend supports, so the (gated) short leg lives only where the curve
|
||||||
|
is genuinely rolling over -- it does not bleed shorting a structural bull.
|
||||||
|
|
||||||
|
The reversal impulse is the (vol-scaled) negative of the recent move -r_k -- a CONTINUOUS,
|
||||||
|
saturating fade of the last K-bar return -- rather than sparse extreme-only entries, so
|
||||||
|
more of the small bounces are captured. We blend K=1..3 (mostly K=1, the cleanest
|
||||||
|
reversal) and normalize each move by trailing vol so the threshold is in sigma, not raw %.
|
||||||
|
|
||||||
|
CAUSAL: sma(c,SLOW), the K-bar past returns, the trailing-vol scaler, the trend gate and
|
||||||
|
vol_target at bar i all use only rows <= i. No shift(-k), no centered windows, no global
|
||||||
|
fit. Verified by causality_ok.
|
||||||
|
|
||||||
|
Tuning (train only, combined A&B, coarse->fine; interior plateau, not a spike). Series A
|
||||||
|
is the binding constraint (a weaker, deeper-pullback reversal than B); the chosen cell
|
||||||
|
maximizes A's sharpe at a controlled DD without overfitting B. Perturbations around the
|
||||||
|
center all stay in sharpe_min ~0.48..0.58 at DD ~0.14..0.16:
|
||||||
|
SLOW 125..135 (smin 0.51..0.55), Z_SAT 0.85..1.05 (smin 0.52..0.56),
|
||||||
|
SHORT_W 0..0.5 (smin 0.53..0.54 -- the gated short adds a touch), K-weights from pure
|
||||||
|
1-bar (smin 0.58, DD 0.16) to (0.5,0.3,0.2) (smin 0.53, DD 0.14). vol_target scales
|
||||||
|
PnL<->DD ~linearly (sharpe flat) so TARGET_VOL is just the risk dial; LEV_CAP is not
|
||||||
|
binding (vol-target keeps |pos|<1 on these curves).
|
||||||
|
Chosen (interior, robust): SLOW=130, K_WEIGHTS=(0.7,0.2,0.1), Z_SAT=0.95, SHORT_W=0.25,
|
||||||
|
TARGET_VOL=0.25, VOL_WIN_DAYS=30, LEV_CAP=2.0
|
||||||
|
-> train combined: pnl_mean ~0.52, maxdd_worst ~0.15, sharpe_min ~0.55
|
||||||
|
(A ~0.55 sharpe / B ~1.3 sharpe). A modest, positive PnL at a ~15% drawdown -- the
|
||||||
|
trend-gated short-term reversal harvests the in-trend bounces while sidestepping the
|
||||||
|
big declines, vs long-only buy&hold's ~6-23x PnL at ~70-80% DD.
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
import blindlib as bl
|
||||||
|
|
||||||
|
SLOW = 130 # slow SMA -> trend regime for the agreement gate
|
||||||
|
K_WEIGHTS = (0.7, 0.2, 0.1) # blend of the 1-,2-,3-bar fades (mostly the 1-bar, the cleanest)
|
||||||
|
Z_SAT = 0.95 # move size (in trailing sigma) that saturates the fade impulse to +-1
|
||||||
|
SHORT_W = 0.25 # weight on the (trend-gated) short leg; gated -> it helps a little
|
||||||
|
TARGET_VOL = 0.25
|
||||||
|
VOL_WIN_DAYS = 30
|
||||||
|
LEV_CAP = 2.0
|
||||||
|
EPS = 1e-9
|
||||||
|
|
||||||
|
|
||||||
|
def signal(df):
|
||||||
|
c = df["close"].values.astype(float)
|
||||||
|
n = len(c)
|
||||||
|
r = bl.simple_returns(c) # r[i] = c[i]/c[i-1]-1 (causal, uses <= i)
|
||||||
|
|
||||||
|
# trailing daily-vol scaler so the "size of the last move" is measured in sigma,
|
||||||
|
# not raw % (otherwise A and B, with different vols, would need different thresholds).
|
||||||
|
vol = bl.rolling_std(r, 30)
|
||||||
|
vol = np.where(np.isfinite(vol) & (vol > EPS), vol, np.nan)
|
||||||
|
# causal fill: use the last finite vol seen so far; fallback to a constant for warmup.
|
||||||
|
vol = _ffill(vol)
|
||||||
|
vol = np.where(np.isfinite(vol), vol, np.nanmedian(vol[np.isfinite(vol)]) if np.isfinite(vol).any() else 0.03)
|
||||||
|
|
||||||
|
# FADE impulse = -(recent K-bar move) / vol, blended over K=1..3 and saturated to +-1.
|
||||||
|
# Positive impulse = price just DROPPED (fade -> want long); negative = just ROSE.
|
||||||
|
impulse = np.zeros(n)
|
||||||
|
for k, w in zip((1, 2, 3), K_WEIGHTS):
|
||||||
|
mk = np.zeros(n)
|
||||||
|
mk[k:] = c[k:] / c[:-k] - 1.0 # past k-bar return ending at i (causal)
|
||||||
|
# normalize the k-bar move by sqrt(k)*vol so each horizon is on the same sigma scale
|
||||||
|
zk = -mk / (np.sqrt(k) * vol + EPS) # FADE = negative of the move
|
||||||
|
impulse += w * np.clip(zk / Z_SAT, -1.0, 1.0)
|
||||||
|
impulse = np.clip(impulse, -1.0, 1.0)
|
||||||
|
|
||||||
|
slow = bl.sma(c, SLOW) # trend regime line (causal)
|
||||||
|
uptrend = c > slow
|
||||||
|
|
||||||
|
# TREND-AGREEMENT GATE: keep ONLY the fade leg that AGREES with the slow trend.
|
||||||
|
# uptrend + impulse>0 (price dropped) -> LONG the bounce (fade agrees: up)
|
||||||
|
# downtrend+ impulse<0 (price rose) -> SHORT the dead-cat (fade agrees: down)
|
||||||
|
# The disagreeing legs (fade a rise in an uptrend = short into a bull; fade a drop in a
|
||||||
|
# downtrend = long into a bear) are momentum/continuation, not reversion -> stay FLAT.
|
||||||
|
raw = np.zeros(n)
|
||||||
|
long_ok = (impulse > 0) & uptrend
|
||||||
|
short_ok = (impulse < 0) & (~uptrend)
|
||||||
|
raw[long_ok] = impulse[long_ok]
|
||||||
|
raw[short_ok] = impulse[short_ok] * SHORT_W
|
||||||
|
|
||||||
|
pos = bl.vol_target(raw, df, target_vol=TARGET_VOL,
|
||||||
|
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
|
||||||
|
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
def _ffill(a):
|
||||||
|
"""Causal forward-fill of NaNs (each value uses only past finite values)."""
|
||||||
|
out = a.copy()
|
||||||
|
last = np.nan
|
||||||
|
for i in range(len(out)):
|
||||||
|
if np.isfinite(out[i]):
|
||||||
|
last = out[i]
|
||||||
|
else:
|
||||||
|
out[i] = last
|
||||||
|
return out
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
"""Agent 18 — Distance-from-MA reversion, trend-gated (family=meanrev, slug=dist_ma).
|
||||||
|
|
||||||
|
THE ANGLE (assigned): position = -tanh(scaled distance of price from its MA). Buy when price
|
||||||
|
is stretched BELOW its MA, sell when stretched ABOVE — a reversion-to-the-MA impulse, sized by
|
||||||
|
how far price has wandered. Tune the MA window and the tanh scale.
|
||||||
|
|
||||||
|
WHY THE PURE ANGLE LOSES, AND WHAT SURVIVES.
|
||||||
|
The naive symmetric form (-tanh(scale * (price/MA - 1)) traded both sides) is CATASTROPHIC on
|
||||||
|
these two curves: both trend up ~7x (A) / ~23x (B) over the train window, so shorting every
|
||||||
|
stretch ABOVE the MA just fights a relentless uptrend. Measured: the pure symmetric angle
|
||||||
|
returns -79%..-95% with sharpe ~ -0.5..-0.9 (it shorts the bull). A conditioning study of
|
||||||
|
next-bar return vs the normalized distance-from-MA confirms the asymmetry: the LARGEST
|
||||||
|
positive next-bar returns sit at the HIGHEST positive distance (that's momentum continuation,
|
||||||
|
NOT reversion — never short it), while the genuine reversion edge lives only on the DOWNSIDE
|
||||||
|
— when price is stretched well below its MA, the next bar bounces (+0.27%..+0.35% in the
|
||||||
|
deepest dip bin, pooled A&B). So the distance-from-MA reversion that actually exists here is
|
||||||
|
the short-horizon PULLBACK inside the prevailing trend, not a fade of the trend itself.
|
||||||
|
|
||||||
|
THE RULE.
|
||||||
|
impulse = -tanh(SCALE * z) where z = (price/SMA(MA) - 1) standardized by a trailing rolling
|
||||||
|
std (so A and B, with different vol, get comparable stretch units). impulse>0 = price below
|
||||||
|
its MA (a dip -> reversion says go long); impulse<0 = price above its MA (a rally -> short).
|
||||||
|
A TREND GATE then keeps only the reversion leg that agrees with the regime:
|
||||||
|
* UPTREND (price > SMA(SLOW)): take only the LONG impulse (buy the dip that bounces).
|
||||||
|
* DOWNTREND (price < SMA(SLOW)): take only the SHORT impulse (fade the dead-cat rally),
|
||||||
|
down-weighted by SHORT_W. Tuning drives SHORT_W -> 0: both curves trend up, so the
|
||||||
|
downtrend-short reversion only adds drawdown over this sample.
|
||||||
|
A causal vol_target sizes the impulse so the two series are risk-comparable and exposure
|
||||||
|
shrinks into vol spikes.
|
||||||
|
|
||||||
|
CAUSAL: SMA(MA), SMA(SLOW), the rolling std and vol_target at bar i use only rows <= i. No
|
||||||
|
shift(-k), no centered windows, no global fit. Verified by causality_ok (online-consistent).
|
||||||
|
|
||||||
|
TUNING (train only, combined A&B; coarse->fine, plateau not spike). A FAST MA (the distance is
|
||||||
|
a short-horizon pullback, not a slow-trend gap) is decisively better than a medium MA:
|
||||||
|
ma=3 beats ma=20+ by ~0.2 sharpe at lower DD. The chosen cell is interior on every axis:
|
||||||
|
MA 3..5 -> sharpe_min 0.69..0.81 ; SCALE 1.0..2.5 -> 0.72..0.76 (PnL rises, DD ~flat) ;
|
||||||
|
NORM_WIN 30..90 -> 0.75..0.80 ; SLOW 110..140 -> sharpe_min 0.74..0.81 (a real plateau).
|
||||||
|
SHORT_W 0->0.5 only lowers sharpe (the downtrend short fights the structural uptrend).
|
||||||
|
vol_target trades PnL<->DD ~linearly (sharpe flat), so TARGET_VOL is just the risk dial.
|
||||||
|
|
||||||
|
MA=3, NORM_WIN=60, SCALE=1.5, SLOW=130, SHORT_W=0.0, TARGET_VOL=0.30, VOL_WIN=30, LEV_CAP=2.0
|
||||||
|
-> train combined: pnl_mean ~0.70, maxdd_worst ~0.115, sharpe_min ~0.80
|
||||||
|
(a solid PnL at an ~11-12% drawdown: the reversion-in-trend harvests the pullback bounces
|
||||||
|
while sidestepping the deep declines, vs long-only buy&hold's huge PnL at ~70-80% DD.)
|
||||||
|
|
||||||
|
HONEST CAVEAT: the value here is the DROP IN DRAWDOWN (~6x lower than buy&hold), not beating
|
||||||
|
buy&hold's raw PnL on a 7x/23x bull run. The PURE assigned angle (symmetric fade) is a
|
||||||
|
loser on trending data — it only becomes positive once gated to the dip side of the trend.
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import blindlib as bl
|
||||||
|
|
||||||
|
MA = 3 # fast SMA -> the distance is a SHORT-HORIZON pullback from price
|
||||||
|
NORM_WIN = 60 # trailing window standardizing the distance (so A & B are comparable)
|
||||||
|
SCALE = 1.5 # tanh scale on the standardized distance -> reversion impulse magnitude
|
||||||
|
SLOW = 130 # trend-regime SMA for the agreement gate
|
||||||
|
SHORT_W = 0.0 # weight on the (gated) downtrend-short leg; tuning -> 0 (long-flat best)
|
||||||
|
TARGET_VOL = 0.30
|
||||||
|
VOL_WIN_DAYS = 30
|
||||||
|
LEV_CAP = 2.0
|
||||||
|
|
||||||
|
|
||||||
|
def signal(df):
|
||||||
|
c = df["close"].values.astype(float)
|
||||||
|
n = len(c)
|
||||||
|
|
||||||
|
# distance of price from its (fast) MA, standardized by a trailing rolling std (causal).
|
||||||
|
dist = c / bl.sma(c, MA) - 1.0
|
||||||
|
sd = pd.Series(dist).rolling(NORM_WIN).std().values
|
||||||
|
zd = np.nan_to_num(dist / np.where(sd > 0, sd, np.nan), nan=0.0)
|
||||||
|
|
||||||
|
# the assigned angle: reversion impulse = -tanh(scaled distance).
|
||||||
|
# zd>0 (price above MA) -> impulse<0 (short the stretch)
|
||||||
|
# zd<0 (price below MA) -> impulse>0 (long the dip)
|
||||||
|
impulse = -np.tanh(SCALE * zd)
|
||||||
|
|
||||||
|
# trend-agreement gate: keep only the reversion leg that agrees with the regime.
|
||||||
|
up = c > bl.sma(c, SLOW)
|
||||||
|
raw = np.zeros(n)
|
||||||
|
long_ok = (impulse > 0) & up # buy the dip inside an uptrend
|
||||||
|
short_ok = (impulse < 0) & (~up) # fade the rally inside a downtrend (down-weighted)
|
||||||
|
raw[long_ok] = impulse[long_ok]
|
||||||
|
raw[short_ok] = impulse[short_ok] * SHORT_W
|
||||||
|
|
||||||
|
pos = bl.vol_target(raw, df, target_vol=TARGET_VOL,
|
||||||
|
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
|
||||||
|
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
"""Agent 19 — Vol-targeted long-only / risk-parity single asset
|
||||||
|
(family=vol, slug=voltarget_lo).
|
||||||
|
|
||||||
|
The angle (assigned): NO direction call. Hold the asset LONG at all times, but size
|
||||||
|
the position by INVERSE realized volatility so the book runs at a roughly constant
|
||||||
|
target volatility: exposure[i] = clip( target_vol / realized_vol[i] , 0, cap ).
|
||||||
|
|
||||||
|
Why this anticipates anything at all, despite never predicting direction: realized
|
||||||
|
vol is PERSISTENT (today's vol forecasts tomorrow's vol far better than today's return
|
||||||
|
forecasts tomorrow's return). The big declines on these two curves are also the high-
|
||||||
|
vol regimes — a crash is a vol spike. So scaling exposure DOWN when trailing vol is
|
||||||
|
high mechanically pulls the book light right when the worst legs happen, and levers UP
|
||||||
|
in the calm grind higher. The result on a structurally up-trending curve is a long-only
|
||||||
|
book with most of buy&hold's upside but a much smaller drawdown (the risk-parity / "vol
|
||||||
|
control" effect), at modest turnover (the weight only drifts with the vol forecast).
|
||||||
|
|
||||||
|
CAUSAL: realized_vol[i] uses returns over a trailing window ending at i (rows <= i);
|
||||||
|
the position is then shifted by the evaluator (held during bar i+1). No direction is
|
||||||
|
derived from any future bar; no global fit. Verified by causality_ok (max_diff 0.0).
|
||||||
|
|
||||||
|
Tuning (split='train' only, combined A&B). The free knobs are the trailing vol window,
|
||||||
|
the target vol, and the leverage cap.
|
||||||
|
* CAP is the single most important choice. Because both curves trend up hard, a high
|
||||||
|
cap just re-levers into buy&hold and brings the drawdown right back. cap=1.0 (never
|
||||||
|
more than fully invested) is what preserves the risk-parity de-risking benefit; with
|
||||||
|
a vol-driven weight that almost always sits below 1.0 this is the whole point.
|
||||||
|
* VOL_WIN is the vol-forecast horizon. A SLOW window (~120d) gives a stabler vol
|
||||||
|
estimate, less whipsaw, lower turnover and the BEST risk-adjusted result here:
|
||||||
|
sharpe_min climbs from ~0.85 (30d) to ~0.97 (120d) and the plateau (110..200d) is
|
||||||
|
flat at sharpe 0.91..0.99 / DD ~0.42-0.44 -> 120 is a robust interior pick.
|
||||||
|
* TARGET_VOL is a pure DD/PnL dial: it scales exposure up and down but (for a long-
|
||||||
|
only inverse-vol book) leaves the Sharpe essentially flat (0.971 across 0.24..0.32).
|
||||||
|
So it is chosen for the DD/PnL trade-off, not the Sharpe.
|
||||||
|
Chosen cell, interior on every axis:
|
||||||
|
TARGET_VOL = 0.28 # DD/PnL dial; Sharpe flat across 0.24..0.32 -> balanced cell
|
||||||
|
VOL_WIN_D = 120 # slow, stable vol forecast; plateau 110..200d
|
||||||
|
LEV_CAP = 1.0 # never lever past fully-invested -> keeps the DD-cut benefit
|
||||||
|
-> train combined: pnl_mean ~2.93, maxdd_worst ~0.43, sharpe_min ~0.97.
|
||||||
|
This is a DEFENSIVE long-only book, NOT alpha. Its honest value is the drawdown: ~0.43
|
||||||
|
vs ~0.77-0.79 buy&hold at comparable PnL. Because it never shorts, its Sharpe ceiling
|
||||||
|
(~1.0) is set by the absence of any direction call -> it can avoid sizing into the big
|
||||||
|
declines but cannot profit from them. That is the inherent limit of this angle.
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
import blindlib as bl
|
||||||
|
|
||||||
|
TARGET_VOL = 0.28
|
||||||
|
VOL_WIN_D = 120
|
||||||
|
LEV_CAP = 1.0
|
||||||
|
|
||||||
|
|
||||||
|
def signal(df):
|
||||||
|
# direction = always long (+1), NO direction call. Sizing is pure inverse-vol.
|
||||||
|
direction = np.ones(len(df))
|
||||||
|
pos = bl.vol_target(direction, df, target_vol=TARGET_VOL,
|
||||||
|
vol_win_days=VOL_WIN_D, leverage_cap=LEV_CAP)
|
||||||
|
# long-only risk-parity: clip to [0, cap] (no shorts by construction)
|
||||||
|
return np.clip(np.nan_to_num(pos, nan=0.0), 0.0, LEV_CAP)
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
"""agent_20_regime_switch — ANGLE [family=vol, slug=regime_switch].
|
||||||
|
|
||||||
|
Regime switch on the realized-vol PERCENTILE (expanding / online):
|
||||||
|
|
||||||
|
* Compute short-window realized vol rv[i] at each bar.
|
||||||
|
* Rank it against its EXPANDING percentile (the causal "typical" vol seen so far) —
|
||||||
|
a self-calibrating threshold that needs no magic vol level and adapts as the series
|
||||||
|
evolves (no peeking at the full-sample distribution).
|
||||||
|
* LOW-VOL regime (rv-rank <= PCTL): TREND-FOLLOW. Quiet, orderly markets are where
|
||||||
|
momentum persists, so we ride the prevailing (multi-horizon) trend.
|
||||||
|
* HIGH-VOL regime (rv-rank > PCTL): stand aside (FLAT). High realized vol is where
|
||||||
|
trends whipsaw / V-reverse and where the big drawdowns are born; the cleanest
|
||||||
|
expression of the "regime switch" is to refuse directional exposure there.
|
||||||
|
|
||||||
|
The trend leg is a multi-horizon TSMOM SIGN blend (slow horizons ~1/2/4 months): a
|
||||||
|
single lookback is regime-fragile, the blend keeps the slow macro trend while the fast
|
||||||
|
horizon cuts exposure early into a turn. Final size is a trailing vol-target, so the
|
||||||
|
position also shrinks into vol within the low-vol regime.
|
||||||
|
|
||||||
|
CAUSAL: rv uses a trailing window; the percentile rank is EXPANDING (only past bars);
|
||||||
|
each TSMOM sign uses close[i]/close[i-H]; vol_target uses a trailing realized-vol
|
||||||
|
window. No look-ahead, no centered windows, no global fit. Verified by causality_ok
|
||||||
|
(max_diff 0.0).
|
||||||
|
|
||||||
|
Tuned ONLY on split='train' (Series A & B, equal weight). A coarse->fine sweep found a
|
||||||
|
WIDE plateau: HZ=(25,60,120), PCTL in [0.60..0.70], VW in [35..55], RV in [15..25] all
|
||||||
|
give sharpe_min ~1.25-1.30 at DD ~0.17-0.19. The chosen cell is interior on every axis
|
||||||
|
(robust, not a lucky spike):
|
||||||
|
RV_WIN=20, PCTL=0.65, HORIZONS=(25,60,120), TARGET_VOL=0.22, VOL_WIN=45, LEV_CAP=1.5
|
||||||
|
-> train combined: pnl_mean ~2.0, maxdd_worst ~0.18, sharpe_min ~1.30.
|
||||||
|
|
||||||
|
Honest notes:
|
||||||
|
* The high-vol leg is LONG-FLAT (not revert). A lightly-weighted contrarian leg in
|
||||||
|
high vol helped marginally with a single-MA trend, but once the trend is the slow
|
||||||
|
multi-horizon SIGN blend the reversion leg only added drag -> flat is strictly
|
||||||
|
better here. The value is RISK-ADJUSTED: comparable/positive PnL at ~4x less
|
||||||
|
drawdown than buy&hold (which eats ~77-79% DD on these curves), by sitting out the
|
||||||
|
high-realized-vol regime where the violent declines happen.
|
||||||
|
* Loosening the gate (PCTL ~0.65, not 0.50) is what lifts both Sharpe and PnL: the
|
||||||
|
bottom ~half of the vol distribution is too restrictive and misses the early,
|
||||||
|
still-low-vol part of the trend legs. The plateau is wide enough that the exact
|
||||||
|
percentile is not load-bearing.
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
import blindlib as bl
|
||||||
|
|
||||||
|
RV_WIN = 20 # short realized-vol window ("current" vol)
|
||||||
|
PCTL = 0.65 # expanding vol-percentile gate: trend-follow when rank <= this
|
||||||
|
HORIZONS = (25, 60, 120) # multi-horizon TSMOM sign blend (~1/2/4 months of daily bars)
|
||||||
|
TARGET_VOL = 0.22
|
||||||
|
VOL_WIN_DAYS = 45
|
||||||
|
LEV_CAP = 1.5
|
||||||
|
MIN_HIST = 60 # warmup before the expanding percentile is trusted
|
||||||
|
|
||||||
|
|
||||||
|
def _expanding_pctl_rank(x: np.ndarray, min_hist: int) -> np.ndarray:
|
||||||
|
"""rank[i] = fraction of finite x[0..i] that are <= x[i] (causal, expanding).
|
||||||
|
NaN until `min_hist` finite values have accumulated."""
|
||||||
|
n = len(x)
|
||||||
|
rank = np.full(n, np.nan)
|
||||||
|
seen: list[float] = []
|
||||||
|
for i in range(n):
|
||||||
|
v = x[i]
|
||||||
|
if np.isfinite(v):
|
||||||
|
seen.append(v)
|
||||||
|
if len(seen) >= min_hist:
|
||||||
|
rank[i] = float(np.mean(np.asarray(seen) <= v))
|
||||||
|
return rank
|
||||||
|
|
||||||
|
|
||||||
|
def _tsmom_sign(c: np.ndarray, h: int) -> np.ndarray:
|
||||||
|
"""Sign of the past-h-bar return, causal. 0 for i < h."""
|
||||||
|
out = np.zeros(len(c))
|
||||||
|
if h < len(c):
|
||||||
|
out[h:] = np.sign(c[h:] / c[:-h] - 1.0)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def signal(df):
|
||||||
|
c = df["close"].values.astype(float)
|
||||||
|
bpy = bl.bars_per_day(df) * 365.25
|
||||||
|
|
||||||
|
# 1) short-window realized vol and its EXPANDING percentile rank (causal).
|
||||||
|
rv = bl.realized_vol(bl.simple_returns(c), RV_WIN, bpy)
|
||||||
|
rank = _expanding_pctl_rank(rv, MIN_HIST)
|
||||||
|
low_vol = np.isfinite(rank) & (rank <= PCTL) # the LOW-VOL regime we trade
|
||||||
|
|
||||||
|
# 2) multi-horizon TSMOM sign blend -> graded direction in [-1, +1] (causal).
|
||||||
|
sig = np.zeros(len(c))
|
||||||
|
for h in HORIZONS:
|
||||||
|
sig += _tsmom_sign(c, h)
|
||||||
|
sig /= len(HORIZONS)
|
||||||
|
|
||||||
|
# 3) regime switch: trend-follow ONLY in the low-vol regime, else flat.
|
||||||
|
raw = np.where(low_vol, sig, 0.0)
|
||||||
|
|
||||||
|
# 4) causal vol-targeting (shrinks size into vol -> caps DD).
|
||||||
|
pos = bl.vol_target(raw, df, target_vol=TARGET_VOL,
|
||||||
|
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
|
||||||
|
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
"""agent_21_atr_ride — ANGLE: ATR-channel trend ride with an ATR trailing stop that
|
||||||
|
scales the position DOWN on adverse moves (family=vol, slug=atr_ride).
|
||||||
|
|
||||||
|
Idea (assigned angle):
|
||||||
|
* Build an ATR channel around an EMA mid-line: mid = EMA_N(close);
|
||||||
|
band half-width = K_ENTRY * ATR_M. A close above mid + K_ENTRY*ATR starts an
|
||||||
|
uptrend ride.
|
||||||
|
* Maintain an ATR TRAILING STOP (Chandelier / SuperTrend flavour): a stop line that
|
||||||
|
RATCHETS in the trade's favour and never loosens. While long, the stop is
|
||||||
|
(highest-close-since-entry - K_STOP*ATR) and only moves up. A close below it ends
|
||||||
|
the ride (flatten).
|
||||||
|
* The distinguishing twist of THIS angle (vs a binary breakout) is the SCALE-DOWN on
|
||||||
|
adverse moves. Instead of a hard on/off stop we size by the ATR "stop room":
|
||||||
|
room[i] = clip( (close[i] - stop[i]) / (K_STOP*ATR[i]) , 0, 1 )
|
||||||
|
= how much cushion (in ATR units, normalised by the stop distance) sits between the
|
||||||
|
close and the trailing stop. Exposure is proportional to that cushion, so the book
|
||||||
|
runs full deep in a healthy trend, BLEEDS OFF smoothly as price falls back toward the
|
||||||
|
stop, and goes flat once the stop breaks. We ride winners and de-risk into reversals
|
||||||
|
BEFORE the stop is hit, instead of binary all-in / all-out.
|
||||||
|
|
||||||
|
Long/flat only. Both curves trend up; the short side of an ATR ride is whipsaw on the
|
||||||
|
V-shaped bottoms (same lesson as the donchian/keltner siblings), so a stop-out goes to
|
||||||
|
FLAT, never short. The ride exposure (already in [0,1]) is then vol-targeted so the
|
||||||
|
long shrinks further into vol spikes (every crash is a vol spike) -> caps the DD.
|
||||||
|
|
||||||
|
CAUSAL: mid (EMA) and ATR are built with .shift(1) -> strictly from bars <= i-1, and the
|
||||||
|
close[i] that pierces the channel / sits above the stop is a real, tradeable event at
|
||||||
|
close[i]. The trailing-stop state machine is a forward scan using only data <= i (peak is
|
||||||
|
the running max of past closes; the stop only ratchets up). vol_target uses realized vol
|
||||||
|
up to i. No future rows, no centered windows, no global fit -> causality_ok = true
|
||||||
|
(verified: max_diff 0.0). The evaluator then holds the position during bar i+1.
|
||||||
|
|
||||||
|
TUNING (split='train' only, Series A & B equal weight; chosen cell is a plateau center):
|
||||||
|
* N_EMA x N_ATR: the (20,20) cell is the best risk-adjusted corner of the EMA/ATR grid
|
||||||
|
(sharpe_min ~1.39 vs ~1.06-1.27 at slower 30-60 windows) and its 27-cell neighbourhood
|
||||||
|
(N_EMA 18-25, N_ATR 15-25, K_STOP 2.0-3.0) holds sharpe_min in [1.16, 1.41] (median
|
||||||
|
1.30, 93% of cells > 1.2) -> a genuine plateau, not an isolated peak.
|
||||||
|
* K_ENTRY = 1.0 is the clear ridge: the K_ENTRY row 0.5->1.5 peaks sharply at 1.0
|
||||||
|
(sharpe_min jumps to ~1.3-1.4) because requiring a full ATR of breakout above the mid
|
||||||
|
filters out the chop-region false starts.
|
||||||
|
* K_STOP = 2.5 ATR: the whole K_STOP 2.0-3.5 strip at K_ENTRY=1.0 is flat-high
|
||||||
|
(sharpe_min 1.29-1.39, DD 0.22-0.28); 2.5 is the interior balance.
|
||||||
|
* TARGET_VOL is a pure PnL/DD dial with FLAT Sharpe (~1.39 across 0.20-0.30): 0.20 ->
|
||||||
|
pnl 1.75/DD 0.16 ... 0.30 -> pnl 3.23/DD 0.23 ... 0.40 -> pnl 4.81/DD 0.29. 0.30 is
|
||||||
|
the balanced cell. VOL_WIN=30 is interior and best on Sharpe (1.39 vs 1.28 at 60).
|
||||||
|
LEV_CAP=1.0 (never lever past fully invested) preserves the de-risking benefit.
|
||||||
|
|
||||||
|
Train (combined A&B): pnl_mean ~3.23, maxdd_worst ~0.23, sharpe_min ~1.39.
|
||||||
|
Honest note: this is trend-following, not alpha — its value is turning a high-PnL /
|
||||||
|
~77-79%-DD uptrend into comparable PnL at ~23% drawdown (DD cut ~3.4x). The scale-down
|
||||||
|
twist buys a slightly lower DD and steadier equity than a binary ATR breakout would, at
|
||||||
|
the cost of leaving some upside on the table in the very strongest legs (the position is
|
||||||
|
rarely pinned at 1.0). The short side was not pursued: on these up-trending curves it is
|
||||||
|
value-destroying whipsaw, the same finding as the sibling breakout angles.
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import blindlib as bl
|
||||||
|
|
||||||
|
N_EMA = 20 # ATR-channel mid-line EMA span
|
||||||
|
N_ATR = 20 # ATR window (channel half-width AND trailing-stop unit)
|
||||||
|
K_ENTRY = 1.0 # entry: close > mid + K_ENTRY*ATR -> start the ride (ridge value)
|
||||||
|
K_STOP = 2.5 # trailing stop distance in ATR (Chandelier) -> also the scale ruler
|
||||||
|
TARGET_VOL = 0.30 # PnL/DD dial; Sharpe flat across 0.20-0.30
|
||||||
|
VOL_WIN_DAYS = 30
|
||||||
|
LEV_CAP = 1.0
|
||||||
|
|
||||||
|
|
||||||
|
def _atr_ride_exposure(df):
|
||||||
|
"""Long/flat exposure in [0,1]: 0 when out of the ride; while in the ride, the value
|
||||||
|
is the ATR 'stop room' (cushion above the trailing stop, in [0,1]) so the position
|
||||||
|
scales DOWN smoothly on adverse moves and goes flat when the stop breaks."""
|
||||||
|
c = df["close"].values.astype(float)
|
||||||
|
n = len(c)
|
||||||
|
mid = pd.Series(bl.ema(c, N_EMA)).shift(1).values # EMA built strictly <= i-1
|
||||||
|
atr = pd.Series(bl.atr(df, N_ATR)).shift(1).values # ATR built strictly <= i-1
|
||||||
|
|
||||||
|
expo = np.zeros(n)
|
||||||
|
in_ride = False
|
||||||
|
peak = -np.inf # highest close since entry (drives the ratcheting stop)
|
||||||
|
for i in range(n):
|
||||||
|
m, a = mid[i], atr[i]
|
||||||
|
if not (np.isfinite(m) and np.isfinite(a) and a > 0):
|
||||||
|
continue
|
||||||
|
if not in_ride:
|
||||||
|
# entry: close pierces the upper ATR channel (full ATR above the mid)
|
||||||
|
if c[i] > m + K_ENTRY * a:
|
||||||
|
in_ride = True
|
||||||
|
peak = c[i]
|
||||||
|
if in_ride:
|
||||||
|
peak = max(peak, c[i])
|
||||||
|
stop = peak - K_STOP * a # Chandelier trailing stop (ratchets via peak)
|
||||||
|
if c[i] <= stop:
|
||||||
|
in_ride = False # stop broken -> ride over, flat
|
||||||
|
expo[i] = 0.0
|
||||||
|
peak = -np.inf
|
||||||
|
else:
|
||||||
|
# SCALE DOWN on adverse moves: cushion above the stop, normalised to [0,1].
|
||||||
|
room = (c[i] - stop) / (K_STOP * a)
|
||||||
|
expo[i] = float(np.clip(room, 0.0, 1.0))
|
||||||
|
return expo
|
||||||
|
|
||||||
|
|
||||||
|
def signal(df):
|
||||||
|
expo = _atr_ride_exposure(df) # long/flat in [0,1], already scaled by stop room
|
||||||
|
pos = bl.vol_target(expo, df, target_vol=TARGET_VOL,
|
||||||
|
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
|
||||||
|
return np.clip(np.nan_to_num(pos, nan=0.0), 0.0, LEV_CAP)
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
"""agent_22_dd_derisk — ANGLE: drawdown-state de-risking overlay (family=vol, slug=dd_derisk).
|
||||||
|
|
||||||
|
Idea (assigned angle):
|
||||||
|
Ride the up-trend, but CUT exposure as the asset's running drawdown deepens, and
|
||||||
|
RE-RISK as it recovers back toward the peak. On these two structurally up-trending
|
||||||
|
curves every large decline begins as a drawdown below the running peak; trimming
|
||||||
|
exposure while the curve bleeds below its high mechanically pulls the book light
|
||||||
|
through the worst legs and re-arms it once the high is reclaimed.
|
||||||
|
|
||||||
|
Construction (all causal / online):
|
||||||
|
* dd[i] = close[i] / running_peak(close[0..i]) - 1 in (-1, 0] -> the LIVE drawdown.
|
||||||
|
* |dd| is lightly EWMA-smoothed (span DD_SMOOTH) so the re-risk on the snap-back is
|
||||||
|
not whipsawed by single-bar wicks; the smoother is causal (ewm, adjust=False).
|
||||||
|
* A smooth de-risk multiplier maps the (smoothed) drawdown to a [W_FLOOR, 1] scale:
|
||||||
|
scale = clip( 1 - (|dd_smooth| / DD_REF) ** P , W_FLOOR, 1 )
|
||||||
|
Shallow dd -> ~full size; as |dd| approaches DD_REF the scale is bled to W_FLOOR.
|
||||||
|
W_FLOOR>0 keeps a small core position through the deep regime (re-arms instantly on
|
||||||
|
recovery) rather than fully exiting and missing the V-bottom.
|
||||||
|
* This dd-scaled LONG is then vol-targeted (inverse realized vol, slow VOL_WIN_D
|
||||||
|
window). A crash is also a vol spike, so inverse-vol sizing de-risks the same legs
|
||||||
|
from the other side — the two de-risk mechanisms stack. Long/flat only: both curves
|
||||||
|
are sharply V-bottomed, so shorting the recoveries is whipsaw; a de-risk goes toward
|
||||||
|
a light long, never short.
|
||||||
|
|
||||||
|
Why no explicit trend filter: tested, it HURTS the risk-adjusted result here. The
|
||||||
|
drawdown overlay already does the de-risking a trend gate would do, but smoothly and
|
||||||
|
without the gate's whipsaw round-trips at the V-bottoms. Pure dd-derisk + slow
|
||||||
|
inverse-vol gives the better Sharpe.
|
||||||
|
|
||||||
|
CAUSAL: running peak (left-to-right accumulate), drawdown, the EWMA smoother and the
|
||||||
|
realized-vol window at i all use rows <= i only. The evaluator shifts the position (held
|
||||||
|
during bar i+1). No future rows, no centered window, no global fit -> causality_ok=true
|
||||||
|
(verified: max_diff 0.0).
|
||||||
|
|
||||||
|
Tuning (split='train' only, A & B equal weight; buy&hold ref: A Sh0.89/DD0.77,
|
||||||
|
B Sh1.16/DD0.79). The de-risk SHAPE (DD_REF / P / W_FLOOR / DD_SMOOTH) sets the Sharpe;
|
||||||
|
TARGET_VOL is a clean DD/PnL dial (Sharpe flat ~1.10-1.14 across 0.25..0.50). Chosen cell
|
||||||
|
is interior on every axis with a flat plateau (Sharpe 1.08..1.15, DD 0.19..0.24):
|
||||||
|
DD_REF=0.20 P=1.0 W_FLOOR=0.20 DD_SMOOTH=4 VOL_WIN_D=120 TARGET_VOL=0.40
|
||||||
|
-> train combined: pnl_mean ~1.63, maxdd_worst ~0.22, sharpe_min ~1.14.
|
||||||
|
Honest read: this is a DEFENSIVE long-only book, not alpha. Its value is the DRAWDOWN —
|
||||||
|
~0.22 vs ~0.77-0.79 buy&hold (a ~3.5x cut) at comparable risk-adjusted PnL. Because it
|
||||||
|
never shorts, its Sharpe ceiling (~1.1-1.2) is set by the absence of a direction call: it
|
||||||
|
can avoid sizing into the big declines but cannot profit from them. That is the inherent
|
||||||
|
limit of the de-risk-overlay angle on these curves.
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import blindlib as bl
|
||||||
|
|
||||||
|
DD_REF = 0.20 # drawdown (fraction) at which the de-risk multiplier hits the floor
|
||||||
|
P = 1.0 # de-risk curvature (linear here; >1 keeps near-full on shallow dips)
|
||||||
|
W_FLOOR = 0.20 # minimum exposure scale in the deep regime (keeps a re-armable core)
|
||||||
|
DD_SMOOTH = 4 # EWMA span on |drawdown| -> de-whipsaw the re-risk on snap-backs
|
||||||
|
VOL_WIN_D = 120 # slow trailing realized-vol horizon (days); stable, low turnover
|
||||||
|
TARGET_VOL = 0.40 # DD/PnL dial; Sharpe flat across 0.25..0.50 -> picked for PnL/DD balance
|
||||||
|
LEV_CAP = 1.0 # long-only, never lever past fully invested -> preserves the DD cut
|
||||||
|
|
||||||
|
|
||||||
|
def _drawdown_scale(c: np.ndarray) -> np.ndarray:
|
||||||
|
"""Causal de-risk multiplier in [W_FLOOR, 1] driven by the live drawdown."""
|
||||||
|
peak = np.maximum.accumulate(c) # running peak over rows <= i (causal)
|
||||||
|
dd = c / peak - 1.0 # (-1, 0]
|
||||||
|
ad = np.abs(dd)
|
||||||
|
ad = pd.Series(ad).ewm(span=DD_SMOOTH, adjust=False).mean().values # causal smoother
|
||||||
|
depth = ad / DD_REF
|
||||||
|
return np.clip(1.0 - depth ** P, W_FLOOR, 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
def signal(df):
|
||||||
|
c = df["close"].values.astype(float)
|
||||||
|
scale = _drawdown_scale(c) # long/flat de-risk exposure in [W_FLOOR, 1]
|
||||||
|
pos = bl.vol_target(scale, df, target_vol=TARGET_VOL,
|
||||||
|
vol_win_days=VOL_WIN_D, leverage_cap=LEV_CAP)
|
||||||
|
return np.clip(np.nan_to_num(pos, nan=0.0), 0.0, LEV_CAP)
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
"""agent_23_vol_of_vol — ANGLE [family=vol, slug=vol_of_vol].
|
||||||
|
|
||||||
|
Vol-of-vol gate: trade the trend ONLY when volatility itself is STABLE; flatten when
|
||||||
|
vol is spiking erratically.
|
||||||
|
|
||||||
|
The idea (distinct from a plain vol-LEVEL gate): what kills a trend-follower is not
|
||||||
|
high volatility per se — a calm, persistently-high-vol grind still trends — but the
|
||||||
|
INSTABILITY of the vol regime. When realized volatility itself starts jumping around
|
||||||
|
(vol-of-vol spikes), the market is in a disorderly, regime-shifting state where trends
|
||||||
|
V-reverse and whipsaw, and where the violent declines are born. So:
|
||||||
|
|
||||||
|
* Compute short-window realized vol rv[i] (the "current" vol).
|
||||||
|
* Compute VOL-OF-VOL vov[i] = trailing std of the LOG-CHANGES of rv (a scale-free
|
||||||
|
measure of how erratically vol is moving — robust to the absolute vol level, which
|
||||||
|
differs across the two curves).
|
||||||
|
* Rank vov against its EXPANDING percentile (causal, self-calibrating threshold — no
|
||||||
|
magic vol-of-vol level, adapts as the series evolves, never peeks at the full sample).
|
||||||
|
* STABLE-VOL regime (vov-rank <= PCTL): TREND-FOLLOW the prevailing multi-horizon
|
||||||
|
TSMOM sign blend (~1/2/4 months).
|
||||||
|
* ERRATIC-VOL regime (vov-rank > PCTL): stand aside (FLAT) — refuse directional
|
||||||
|
exposure where vol is spiking erratically.
|
||||||
|
|
||||||
|
Final size is a trailing vol-target so exposure also shrinks into raw vol inside the
|
||||||
|
stable regime.
|
||||||
|
|
||||||
|
CAUSAL: rv uses a trailing window; the log-change std uses a trailing window; the
|
||||||
|
percentile rank is EXPANDING (only past bars); each TSMOM sign uses close[i]/close[i-H];
|
||||||
|
vol_target uses a trailing realized-vol window. No look-ahead, no centered windows, no
|
||||||
|
global fit. Verified by causality_ok (max_diff 0.0).
|
||||||
|
|
||||||
|
Tuned ONLY on split='train' (Series A & B, equal weight). A coarse->fine sweep found a
|
||||||
|
WIDE plateau and one load-bearing insight: only the TOP of the vol-of-vol distribution
|
||||||
|
hurts. Tight gates (PCTL ~0.55-0.65) are too restrictive — they sit out the early, still-
|
||||||
|
orderly part of the trend legs and DROP the Sharpe to ~0.83. Flattening only the most
|
||||||
|
ERRATIC ~20% (PCTL ~0.80) is what lifts both Sharpe and PnL. Around the chosen cell the
|
||||||
|
plateau is flat: VOV_WIN in [30..50] -> sharpe_min 1.12..1.16, PCTL in [0.76..0.84] ->
|
||||||
|
1.12..1.17, all at DD ~0.19-0.23. The chosen cell is interior on every axis:
|
||||||
|
RV_WIN=30, VOV_WIN=40, PCTL=0.80, HORIZONS=(25,60,120), TARGET_VOL=0.22, VOL_WIN=45
|
||||||
|
-> train combined: pnl_mean ~1.87, maxdd_worst ~0.20, sharpe_min ~1.16.
|
||||||
|
|
||||||
|
Honest notes:
|
||||||
|
* The erratic-vol leg is LONG-FLAT (not contrarian) — refusing exposure where vol is
|
||||||
|
unstable, not betting against the move. The value is RISK-ADJUSTED: comparable PnL
|
||||||
|
at ~4x less drawdown than buy&hold (~0.77-0.79 DD on these curves), by sitting out
|
||||||
|
the disorderly regimes where the violent declines are born.
|
||||||
|
* TARGET_VOL is a pure DD/PnL dial (Sharpe flat ~1.16 across 0.18..0.26); LEV_CAP does
|
||||||
|
not bind (the vol-target weight sits below 1.0). 0.22 is a balanced cell.
|
||||||
|
* This gate measures the STABILITY of vol (vol-of-vol), distinct from a vol-LEVEL gate:
|
||||||
|
a calm persistently-HIGH-vol grind still trends and is kept; it is the erratic,
|
||||||
|
regime-shifting vol that is flattened. The Sharpe ceiling (~1.16) is set by the
|
||||||
|
absence of a short leg — it avoids the chop but cannot profit from the declines.
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import blindlib as bl
|
||||||
|
|
||||||
|
RV_WIN = 30 # short realized-vol window ("current" vol)
|
||||||
|
VOV_WIN = 40 # trailing window for vol-of-vol (std of log-changes of rv)
|
||||||
|
PCTL = 0.80 # expanding vov-percentile gate: trend-follow when rank <= this
|
||||||
|
HORIZONS = (25, 60, 120) # multi-horizon TSMOM sign blend (~1/2/4 months of daily bars)
|
||||||
|
TARGET_VOL = 0.22
|
||||||
|
VOL_WIN_DAYS = 45
|
||||||
|
LEV_CAP = 1.5
|
||||||
|
MIN_HIST = 60 # warmup before the expanding percentile is trusted
|
||||||
|
|
||||||
|
|
||||||
|
def _expanding_pctl_rank(x: np.ndarray, min_hist: int) -> np.ndarray:
|
||||||
|
"""rank[i] = fraction of finite x[0..i] that are <= x[i] (causal, expanding).
|
||||||
|
NaN until `min_hist` finite values have accumulated."""
|
||||||
|
n = len(x)
|
||||||
|
rank = np.full(n, np.nan)
|
||||||
|
seen: list[float] = []
|
||||||
|
for i in range(n):
|
||||||
|
v = x[i]
|
||||||
|
if np.isfinite(v):
|
||||||
|
seen.append(v)
|
||||||
|
if len(seen) >= min_hist:
|
||||||
|
rank[i] = float(np.mean(np.asarray(seen) <= v))
|
||||||
|
return rank
|
||||||
|
|
||||||
|
|
||||||
|
def _tsmom_sign(c: np.ndarray, h: int) -> np.ndarray:
|
||||||
|
"""Sign of the past-h-bar return, causal. 0 for i < h."""
|
||||||
|
out = np.zeros(len(c))
|
||||||
|
if h < len(c):
|
||||||
|
out[h:] = np.sign(c[h:] / c[:-h] - 1.0)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _vol_of_vol(rv: np.ndarray, win: int) -> np.ndarray:
|
||||||
|
"""vol-of-vol: trailing std of the log-changes of realized vol (scale-free)."""
|
||||||
|
rv_s = pd.Series(rv)
|
||||||
|
logrv = np.log(rv_s.where(rv_s > 0))
|
||||||
|
dlog = logrv.diff()
|
||||||
|
return dlog.rolling(win, min_periods=max(5, win // 2)).std().values
|
||||||
|
|
||||||
|
|
||||||
|
def signal(df):
|
||||||
|
c = df["close"].values.astype(float)
|
||||||
|
bpy = bl.bars_per_day(df) * 365.25
|
||||||
|
|
||||||
|
# 1) short-window realized vol, then its vol-of-vol and EXPANDING percentile (causal).
|
||||||
|
rv = bl.realized_vol(bl.simple_returns(c), RV_WIN, bpy)
|
||||||
|
vov = _vol_of_vol(rv, VOV_WIN)
|
||||||
|
rank = _expanding_pctl_rank(vov, MIN_HIST)
|
||||||
|
stable = np.isfinite(rank) & (rank <= PCTL) # the STABLE-VOL regime we trade
|
||||||
|
|
||||||
|
# 2) multi-horizon TSMOM sign blend -> graded direction in [-1, +1] (causal).
|
||||||
|
sig = np.zeros(len(c))
|
||||||
|
for h in HORIZONS:
|
||||||
|
sig += _tsmom_sign(c, h)
|
||||||
|
sig /= len(HORIZONS)
|
||||||
|
|
||||||
|
# 3) vol-of-vol gate: trend-follow ONLY when vol is stable, else flat.
|
||||||
|
raw = np.where(stable, sig, 0.0)
|
||||||
|
|
||||||
|
# 4) causal vol-targeting (shrinks size into vol -> caps DD).
|
||||||
|
pos = bl.vol_target(raw, df, target_vol=TARGET_VOL,
|
||||||
|
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
|
||||||
|
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
"""agent_24_hhll — ANGLE: swing-structure trend (higher-high/higher-low vs lower-low/lower-high).
|
||||||
|
|
||||||
|
Idea (assigned angle, family=struct / slug=hhll):
|
||||||
|
Read the curve the way a price-action trader reads market STRUCTURE. Find the swing pivots
|
||||||
|
(fractal turning points) with a rolling left/right window, then track the sequence of
|
||||||
|
confirmed swing HIGHs and swing LOWs:
|
||||||
|
* UPTREND = a higher-high AND a higher-low (last swing high > prior swing high AND
|
||||||
|
last swing low > prior swing low) -> go LONG.
|
||||||
|
* STRUCTURE BREAK DOWN = a lower-low (last swing low < prior swing low, a confirmed
|
||||||
|
market-structure-break to the downside) -> exit to FLAT.
|
||||||
|
* Otherwise -> persist the prior state (an uptrend stays innocent through pullbacks /
|
||||||
|
single lower-highs until a swing low is actually undercut).
|
||||||
|
A slow-MA gate (price must still be above its 150-bar mean) acts as the trend-still-intact
|
||||||
|
confirmation of the structural read — an uptrend whose price has fallen below its own mean
|
||||||
|
has structurally rolled over. The position is vol-targeted, so the book shrinks into the
|
||||||
|
vol spikes that mark every real structure break, which is what caps the drawdown.
|
||||||
|
|
||||||
|
CAUSALITY — the crux of any swing/pivot signal:
|
||||||
|
A swing pivot centred at bar k is only KNOWABLE `RIGHT` bars later: you need the right-hand
|
||||||
|
window k+1..k+RIGHT to assert k was a local extreme. So at bar i we may use only pivots
|
||||||
|
whose confirmation bar k+RIGHT <= i. `_hhll_state` does a pure forward scan: at each i it
|
||||||
|
confirms the pivot centred at k=i-RIGHT (its full window k-LEFT..k+RIGHT is complete and all
|
||||||
|
indices <= i) and appends it to the running swing history. The HH/HL/LL comparison and the
|
||||||
|
MA gate at i use only data <= i. No future row ever enters the state. causality_ok -> true.
|
||||||
|
|
||||||
|
LONG/FLAT, not stop-and-reverse (tuned honestly on split='train', A & B equal weight):
|
||||||
|
Both curves trend up hard. A symmetric SHORT on every lower-low / lower-high whipsaws on
|
||||||
|
V-bottoms and destroys risk-adjusted value (sweep: short legs drop sharpe_min from ~1.2 to
|
||||||
|
~0). The structural reading is kept but the down leg is FLAT, not short. This is the right
|
||||||
|
call for a long-biased instrument: ride confirmed up-structure, stand aside when it breaks.
|
||||||
|
|
||||||
|
Tuned params — a broad plateau on train (A & B), NOT an isolated peak. sharpe_min holds
|
||||||
|
~0.95-1.17 across LR 4, MA 120..180, vol-target 0.20..0.30, vol_win 20..60 (sweeps in dev
|
||||||
|
notes). LR=4 is the peak of the pivot-window dimension; MA and target_vol move PnL/DD but not
|
||||||
|
the risk-adjusted shape. Chosen centre of the plateau:
|
||||||
|
LEFT=RIGHT=4 (pivot half-window), MA_FILT=150 (trend-intact gate), target_vol 0.25 / 30d /
|
||||||
|
cap 1 -> train combined: pnl_mean ~2.13, maxdd_worst ~0.28, sharpe_min ~1.17.
|
||||||
|
|
||||||
|
Honest note: like every structure/trend rule on a strongly up-trending pair this is
|
||||||
|
trend-following, not alpha. Ablation is candid — a plain "always-long above the 150-MA" gate
|
||||||
|
scores a slightly HIGHER train sharpe (~1.34) than this structural overlay, because the
|
||||||
|
HH/HL/LL logic stands aside during some pullbacks that later resume. The structure's value is
|
||||||
|
that it is a genuinely different, pivot-based read of the SAME trend that converts a high-PnL
|
||||||
|
/ ~77-79%-DD buy&hold into comparable PnL at ~28% drawdown (DD cut ~2.7x), with only ~33%
|
||||||
|
time in market. It is the assigned angle implemented faithfully — not a momentum rule wearing
|
||||||
|
a structure costume.
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
import blindlib as bl
|
||||||
|
|
||||||
|
LEFT = 4 # pivot left half-window
|
||||||
|
RIGHT = 4 # pivot right half-window (confirmation lag)
|
||||||
|
MA_FILT = 150 # trend-still-intact gate: price must be above this SMA to stay long
|
||||||
|
TARGET_VOL = 0.25
|
||||||
|
VOL_WIN_DAYS = 30
|
||||||
|
LEV_CAP = 1.0
|
||||||
|
|
||||||
|
|
||||||
|
def _hhll_state(high, low, close, left, right, ma_filt):
|
||||||
|
"""Causal HH/HL/LL market-structure trend state in {0, 1} (long/flat).
|
||||||
|
|
||||||
|
Forward scan: at bar i confirm the pivot centred at k=i-right (window k-left..k+right,
|
||||||
|
all <= i), update the running swing-high / swing-low history, then:
|
||||||
|
* higher-high AND higher-low -> long (clean up-structure)
|
||||||
|
* lower-low (structure break) -> flat
|
||||||
|
* else -> hold prior state
|
||||||
|
A final SMA gate forces flat if price is below its slow mean (trend rolled over).
|
||||||
|
Returns a float direction array, len(high); each value uses only data <= i.
|
||||||
|
"""
|
||||||
|
n = len(high)
|
||||||
|
state = np.zeros(n)
|
||||||
|
sh = [] # confirmed swing-high prices (chronological)
|
||||||
|
sl = [] # confirmed swing-low prices
|
||||||
|
s = 0.0
|
||||||
|
sma_c = bl.sma(close, ma_filt) if ma_filt else None
|
||||||
|
for i in range(n):
|
||||||
|
k = i - right
|
||||||
|
if k - left >= 0:
|
||||||
|
seg_h = high[k - left:i + 1] # high[k-left .. k+right], all indices <= i
|
||||||
|
seg_l = low[k - left:i + 1]
|
||||||
|
if high[k] >= seg_h.max(): # weak local max -> swing high
|
||||||
|
sh.append(high[k])
|
||||||
|
if low[k] <= seg_l.min(): # local min -> swing low
|
||||||
|
sl.append(low[k])
|
||||||
|
if len(sh) >= 2 and len(sl) >= 2:
|
||||||
|
hh = sh[-1] > sh[-2] # higher high
|
||||||
|
hl = sl[-1] > sl[-2] # higher low
|
||||||
|
ll = sl[-1] < sl[-2] # lower low = structure break down
|
||||||
|
if hh and hl:
|
||||||
|
s = 1.0
|
||||||
|
elif ll:
|
||||||
|
s = 0.0
|
||||||
|
# else: keep prior state (uptrend survives a single lower-high / pullback)
|
||||||
|
ss = s
|
||||||
|
if ma_filt and s > 0.0 and not (close[i] > sma_c[i]):
|
||||||
|
ss = 0.0 # trend-intact gate (causal)
|
||||||
|
state[i] = ss
|
||||||
|
return state
|
||||||
|
|
||||||
|
|
||||||
|
def signal(df):
|
||||||
|
high = df["high"].values.astype(float)
|
||||||
|
low = df["low"].values.astype(float)
|
||||||
|
close = df["close"].values.astype(float)
|
||||||
|
|
||||||
|
direction = _hhll_state(high, low, close, LEFT, RIGHT, MA_FILT)
|
||||||
|
pos = bl.vol_target(direction, df, target_vol=TARGET_VOL,
|
||||||
|
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
|
||||||
|
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
"""agent_25_channel_pos — ANGLE [struct/channel_pos]: position WITHIN the Donchian channel.
|
||||||
|
|
||||||
|
Idea (assigned angle): instead of a binary breakout AT the channel edge, measure WHERE the
|
||||||
|
close sits inside the rolling Donchian channel [lo, hi] as a continuous fraction
|
||||||
|
chpos = (close - lo) / (hi - lo) in [0, 1] (0.5 = mid-channel).
|
||||||
|
Then take a directional position only when location AND trend AGREE:
|
||||||
|
* LONG when chpos is in the UPPER third (>= UP_TH) AND the channel/price slope is UP,
|
||||||
|
* SHORT when chpos is in the LOWER third (<= LO_TH) AND the slope is DOWN,
|
||||||
|
* FLAT in the middle band or when slope disagrees with location.
|
||||||
|
The "slope" filter is what makes the angle anticipatory rather than a reversal: riding the
|
||||||
|
upper third while the channel is still pushing up is a continuation read; the lower-third +
|
||||||
|
down-slope short tries to catch the persistent declines (the big drawdowns the benchmark eats).
|
||||||
|
|
||||||
|
WHY a slope gate (honest tuning result):
|
||||||
|
Channel-position WITHOUT a slope gate is a mean-reversion read (buy low-in-channel) and
|
||||||
|
on these trending curves it bleeds — it fights the trend and the upper third without a
|
||||||
|
trend filter chops on every pullback. Requiring location AND slope to agree turns it into
|
||||||
|
a trend-confirmation read that holds longs through the up-leg and only shorts confirmed
|
||||||
|
down-legs. The slope is the prior-W channel-midpoint change (causal).
|
||||||
|
|
||||||
|
Sizing: the agreed direction (+1/-1/0) is vol-targeted (TP01-style, causal realized vol) so
|
||||||
|
size shrinks into vol spikes (= crashes) -> caps drawdown.
|
||||||
|
|
||||||
|
Causality: bl.donchian shifts the rolling hi/lo by one bar, so the channel at i is built from
|
||||||
|
bars STRICTLY before i. chpos[i], the slope (a backward difference of a causal EMA of close),
|
||||||
|
and the vol scaling all use only data <= i. The forward scan keeps no future state. The
|
||||||
|
evaluator then HOLDS the position during bar i+1. causality_ok -> true.
|
||||||
|
|
||||||
|
WHY the short leg is sized 0.30 (honest tuning result):
|
||||||
|
A full-size (-1.0) short bled on these up-trending curves (combined Sharpe_min 1.06, DD 0.30).
|
||||||
|
Shrinking the short leg monotonically improved risk-adjusted return; long/flat alone was best
|
||||||
|
on raw PnL/Sharpe but had a slightly fatter DD (0.256). The chosen short=0.30 keeps a genuine
|
||||||
|
lower-third+down-slope SHORT (the angle is intact) and TRIMS the drawdown (0.256 -> 0.229)
|
||||||
|
at ~no PnL cost. So the angle's short leg earns its place, just at a modest size.
|
||||||
|
|
||||||
|
Plateau (tuned on train only): broad and well-behaved around DON 35-45 / UP-LO 0.62-0.66 /
|
||||||
|
SLOPE_WIN 15-20 / short 0.15-0.35 (Sharpe_min ~1.3-1.4 throughout, not an isolated peak).
|
||||||
|
|
||||||
|
FINAL train (combined A&B): pnl_mean ~4.06, maxdd_worst ~0.229, sharpe_min ~1.34, sharpe_mean ~1.40.
|
||||||
|
Per-series: A pnl 4.88 / DD 0.226 / Sh 1.45 ; B pnl 3.22 / DD 0.193 / Sh 1.33. Turnover ~14/yr.
|
||||||
|
causality.ok = true (max_diff 0). Honest note: this is a trend-confirmation read dressed as a
|
||||||
|
channel-position rule (the slope gate makes it ride the trend, not fade it); its value is
|
||||||
|
comparable PnL to buy&hold at ~1/3 of the drawdown, NOT independent alpha.
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
import blindlib as bl
|
||||||
|
|
||||||
|
DON_WIN = 40 # Donchian window for the channel
|
||||||
|
UP_TH = 0.62 # upper-band threshold on chpos (>=) -> "upper third" (location)
|
||||||
|
LO_TH = 0.38 # lower-band threshold on chpos (<=) -> "lower third" (location)
|
||||||
|
SLOPE_WIN = 20 # bars over which we measure the price slope (trend gate)
|
||||||
|
SLOPE_EPS = 0.0 # min |slope| to count as up/down (0 = any non-zero sign)
|
||||||
|
SHORT_SIZE = 0.30 # short-leg size (lower third + down-slope). <1 by tuning: the curves
|
||||||
|
# trend up, so a full-size short bleeds; a modest short still TRIMS DD.
|
||||||
|
TARGET_VOL = 0.30
|
||||||
|
VOL_WIN_DAYS = 30
|
||||||
|
LEV_CAP = 1.0
|
||||||
|
|
||||||
|
|
||||||
|
def signal(df):
|
||||||
|
c = df["close"].values.astype(float)
|
||||||
|
n = len(c)
|
||||||
|
|
||||||
|
hi, lo = bl.donchian(df, DON_WIN) # prior-DON_WIN hi/lo (shifted, causal)
|
||||||
|
width = hi - lo
|
||||||
|
# continuous position within the channel in [0,1]; mid (0.5) where channel undefined.
|
||||||
|
with np.errstate(invalid="ignore", divide="ignore"):
|
||||||
|
chpos = (c - lo) / width
|
||||||
|
chpos = np.where(np.isfinite(chpos) & (width > 0), chpos, 0.5)
|
||||||
|
chpos = np.clip(chpos, 0.0, 1.0)
|
||||||
|
|
||||||
|
# causal slope: change of a smoothed close over SLOPE_WIN bars, normalized by price.
|
||||||
|
sm = bl.ema(c, SLOPE_WIN)
|
||||||
|
slope = np.zeros(n)
|
||||||
|
slope[SLOPE_WIN:] = (sm[SLOPE_WIN:] - sm[:-SLOPE_WIN]) / np.maximum(sm[:-SLOPE_WIN], 1e-9)
|
||||||
|
|
||||||
|
up_loc = chpos >= UP_TH
|
||||||
|
dn_loc = chpos <= LO_TH
|
||||||
|
up_slope = slope > SLOPE_EPS
|
||||||
|
dn_slope = slope < -SLOPE_EPS
|
||||||
|
|
||||||
|
direction = np.zeros(n)
|
||||||
|
direction[up_loc & up_slope] = 1.0 # upper third + rising -> long
|
||||||
|
direction[dn_loc & dn_slope] = -SHORT_SIZE # lower third + falling -> (small) short
|
||||||
|
|
||||||
|
# warmup: no channel yet -> flat
|
||||||
|
direction[:DON_WIN] = 0.0
|
||||||
|
|
||||||
|
pos = bl.vol_target(direction, df, target_vol=TARGET_VOL,
|
||||||
|
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
|
||||||
|
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
"""Agent 26 — Stochastic oscillator reversion + cross, trend-gated (family=osc, slug=stoch).
|
||||||
|
|
||||||
|
The angle (assigned): a rolling Stochastic oscillator (%K / %D). %K = where the close sits
|
||||||
|
in its rolling [min(low), max(high)] window (0..100); %D = a short SMA of %K (the signal
|
||||||
|
line). Trade the REVERSION (%K leaving an oversold extreme) timed by the %K-vs-%D CROSS,
|
||||||
|
GATED by a longer trend filter. Tune the windows.
|
||||||
|
|
||||||
|
Reading the train curves first (both A and B, split='train'): they trend UP very hard
|
||||||
|
(A 100->792, B 100->2400 over the window). UNLIKE RSI — which in these up-curves never
|
||||||
|
dips below ~40 so textbook 30/70 is dead — the Stochastic %K is normalized against its
|
||||||
|
OWN rolling high/low, so it sweeps the FULL 0..100 range even inside the bull: %K<20
|
||||||
|
~12-14% of bars, %K>80 ~24-27% of bars (measured). That is exactly the structure a
|
||||||
|
stochastic reversion rule needs, so the angle is genuinely playable here, but it still
|
||||||
|
has to be REGIME-AWARE because the curves drift up:
|
||||||
|
|
||||||
|
* In an UPTREND (close above a long SMA) %K oversold (<LO) is a BUY-THE-DIP setup, and we
|
||||||
|
require %K to CROSS BACK UP through its signal line %D — the standard stochastic long
|
||||||
|
trigger — before going LONG. That waits for the dip to actually TURN (anticipating the
|
||||||
|
bounce) instead of knife-catching while %K is still falling. We HOLD the long
|
||||||
|
(hysteresis) until %K recovers into EXIT, then go flat. We do NOT short %K>80 in an
|
||||||
|
uptrend — overbought in a bull keeps running (that is momentum, not reversion).
|
||||||
|
* In a DOWNTREND (close below the long SMA) the symmetry returns: %K overbought (>80) with
|
||||||
|
a %K cross DOWN through %D is a reversion SHORT (rips fade). %K<LO we stand flat (don't
|
||||||
|
knife-catch long under a downtrend). The short side is down-weighted (SHORT_W) because
|
||||||
|
the drift is up; on train it is marginal (see HONEST NOTE).
|
||||||
|
|
||||||
|
WHY THE CROSS MATTERS (the "anticipation" the angle asks for): entering the instant %K
|
||||||
|
prints <LO is usually early — %K is still falling. Waiting for the %K/%D up-cross times the
|
||||||
|
turn, which on train is the difference between a coin-flip dip rule and a positive one: with
|
||||||
|
the cross the dip-long sits at ~9-12% DD with a clean positive Sharpe; without it the same
|
||||||
|
thresholds bleed. The cross also cuts whipsaw turnover (~5-6 round-trips/yr, fee-cheap).
|
||||||
|
|
||||||
|
The trend gate does two jobs: it picks WHICH side of the oscillator is reversion (buy dips
|
||||||
|
in up-trend / sell rips in down-trend) and it suppresses the side that fights the drift.
|
||||||
|
Sizing is smooth (deeper oversold -> bigger appetite, floored at BASE while holding) then
|
||||||
|
VOL-TARGETED so the two curves are risk-comparable and exposure shrinks into vol spikes
|
||||||
|
(crashes are vol spikes) — that is what bounds the drawdown. Note the leverage cap never
|
||||||
|
binds here (post-vol-target appetite stays <=1), so the edge does NOT rely on leverage.
|
||||||
|
|
||||||
|
HONEST NOTE (negative findings kept): (1) the downtrend short side is essentially free but
|
||||||
|
adds nothing on train — SHORT_W=0.5 gives sharpe_min 0.51 vs 0.53 at SHORT_W=0; it is kept
|
||||||
|
small to honor the bidirectional angle, not because it earns. (2) A continuous always-on
|
||||||
|
oscillator weighting (no flat state) was tried and pushed time-in-market to ~99% and DD to
|
||||||
|
0.20-0.37 — it degenerated into buy-and-hold; the hysteresis flat state is what keeps the
|
||||||
|
DD at ~12%. (3) In a market that trends this hard, even a cross-gated dip-buy is PARTLY
|
||||||
|
trend participation (the dips it buys recover and it rides them). The genuine reversion
|
||||||
|
content is the oversold-entry / cross-timed turn / overbought-exit cycle plus the DD control
|
||||||
|
from the trend gate + vol-target. Result: an honest, MODEST combined train Sharpe ~0.5 at
|
||||||
|
~12% DD — a fraction of buy&hold's huge PnL but ~6x less drawdown (it anticipates the dip
|
||||||
|
rather than just holding the asset through every crash).
|
||||||
|
|
||||||
|
CAUSAL: %K uses trailing rolling max(high)/min(low) (<= i); %D is a trailing SMA of %K; the
|
||||||
|
cross compares (%K-%D) at i vs i-1 (past only); the hold-state is a forward cumulative pass
|
||||||
|
over PAST bars only; the SMA trend filter and vol_target use trailing data. No shift(-k), no
|
||||||
|
centered windows, no global fit. Verified by causality_ok (max_diff 0.0).
|
||||||
|
|
||||||
|
Tuning (train only, combined A&B; coarse->fine sweep + plateau check). The chosen cell sits
|
||||||
|
on a broad plateau (K in [14..20], LO in [40..50], EXIT in [55..65], D in [3..5], TREND_WIN
|
||||||
|
in [150..200] all hold sharpe_min ~0.37..0.53 at DD ~0.09..0.12 — a plateau, not a spike):
|
||||||
|
K_WIN=20, D_WIN=5, LO=50, EXIT=55, TREND_WIN=150
|
||||||
|
SHORT_W=0.5, BASE=0.7, TARGET_VOL=0.25, VOL_WIN_DAYS=35, LEV_CAP=1.5
|
||||||
|
-> train combined: pnl_mean ~0.17, maxdd_worst ~0.12, sharpe_min ~0.51
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import blindlib as bl
|
||||||
|
|
||||||
|
K_WIN = 20 # %K lookback (rolling high/low window). 20 > textbook 14 for these trends.
|
||||||
|
D_WIN = 5 # %D = SMA(%K, D_WIN): the signal line the %K crosses.
|
||||||
|
LO = 50.0 # oversold threshold below which a %K/%D up-cross is a dip-long entry.
|
||||||
|
EXIT = 55.0 # dip-long HELD until %K recovers past EXIT (hysteresis entry/exit pair).
|
||||||
|
TREND_WIN = 150 # long SMA: above = uptrend (buy dips), below = downtrend (sell rips).
|
||||||
|
SHORT_W = 0.5 # weight on the downtrend reversion-short; marginal (see HONEST NOTE).
|
||||||
|
BASE = 0.7 # base long size while holding a dip (scaled up if %K still oversold).
|
||||||
|
TARGET_VOL = 0.25
|
||||||
|
VOL_WIN_DAYS = 35
|
||||||
|
LEV_CAP = 1.5
|
||||||
|
|
||||||
|
|
||||||
|
def _stoch(df, k_win, d_win):
|
||||||
|
"""Causal Stochastic oscillator. %K[i] uses high/low/close over the trailing
|
||||||
|
k_win bars (<= i); %D[i] = SMA(%K, d_win) (trailing). No look-ahead."""
|
||||||
|
h = df["high"].values.astype(float)
|
||||||
|
l = df["low"].values.astype(float)
|
||||||
|
c = df["close"].values.astype(float)
|
||||||
|
hh = pd.Series(h).rolling(k_win, min_periods=1).max().values
|
||||||
|
ll = pd.Series(l).rolling(k_win, min_periods=1).min().values
|
||||||
|
rng = hh - ll
|
||||||
|
k = np.where(rng > 1e-12, (c - ll) / rng * 100.0, 50.0)
|
||||||
|
d = bl.sma(k, d_win)
|
||||||
|
return k, d
|
||||||
|
|
||||||
|
|
||||||
|
def signal(df):
|
||||||
|
c = df["close"].values.astype(float)
|
||||||
|
n = len(c)
|
||||||
|
k, d = _stoch(df, K_WIN, D_WIN)
|
||||||
|
trend_up = c > bl.sma(c, TREND_WIN) # causal trailing SMA trend gate
|
||||||
|
|
||||||
|
# --- %K/%D crosses (past-only: compares i vs i-1) ---
|
||||||
|
kd = k - d
|
||||||
|
kd_prev = np.concatenate(([0.0], kd[:-1]))
|
||||||
|
cross_up = (kd > 0) & (kd_prev <= 0) # %K turns up through its signal line
|
||||||
|
cross_dn = (kd < 0) & (kd_prev >= 0) # %K turns down through its signal line
|
||||||
|
|
||||||
|
# --- smooth reversion appetite from %K (further past threshold -> bigger) ---
|
||||||
|
long_app = np.clip((LO - k) / LO, 0.0, 1.0) # oversold depth -> long appetite
|
||||||
|
short_app = np.clip((k - 80.0) / 20.0, 0.0, 1.0) # overbought depth -> short appetite
|
||||||
|
|
||||||
|
# --- trend-gated stochastic reversion with cross-triggered entry + hysteresis ---
|
||||||
|
# Forward pass is PURE PAST-ONLY: in_long at bar i depends only on bars <= i.
|
||||||
|
held = np.zeros(n)
|
||||||
|
in_long = False
|
||||||
|
for i in range(n):
|
||||||
|
if in_long:
|
||||||
|
# exit the held dip-long when trend breaks down OR %K has recovered past EXIT
|
||||||
|
if (not trend_up[i]) or (k[i] >= EXIT):
|
||||||
|
in_long = False
|
||||||
|
else:
|
||||||
|
# enter a dip-long in an uptrend when %K is oversold AND turns up through %D
|
||||||
|
if trend_up[i] and (k[i] < LO) and cross_up[i]:
|
||||||
|
in_long = True
|
||||||
|
if in_long:
|
||||||
|
held[i] = max(BASE, long_app[i]) # ride the recovery, bigger if still oversold
|
||||||
|
else:
|
||||||
|
# downtrend reversion-short: overbought AND %K turning down through %D
|
||||||
|
if (not trend_up[i]) and (k[i] > 80.0) and cross_dn[i]:
|
||||||
|
held[i] = -SHORT_W * short_app[i]
|
||||||
|
else:
|
||||||
|
held[i] = 0.0
|
||||||
|
|
||||||
|
pos = bl.vol_target(held, df, target_vol=TARGET_VOL,
|
||||||
|
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
|
||||||
|
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
"""agent_27_dpo — Detrended Price Oscillator (cycle phase around a LAGGED MA).
|
||||||
|
|
||||||
|
ANGLE [family=osc, slug=dpo]: detrend price by subtracting a moving average that we
|
||||||
|
DELAY (lag) so the oscillator measures where price sits in its cycle relative to a
|
||||||
|
recent trend baseline. Trade the cycle phase — causal only.
|
||||||
|
|
||||||
|
Classic DPO is price[i] - SMA(n)[i - (n/2 + 1)]. The textbook centers that lag; here we
|
||||||
|
keep the displacement STRICTLY BACKWARD (the MA value comes from ~n/2 bars ago, fully in
|
||||||
|
the past), so the oscillator is causal/online and deployable.
|
||||||
|
|
||||||
|
What the train data says (tuned on split='train' only):
|
||||||
|
dpo = (price - lagged_baseline) / vol(gap) is a z-like CYCLE PHASE around zero.
|
||||||
|
Bucketing dpo vs the NEXT-bar return showed a clean MONOTONIC relationship: the higher
|
||||||
|
the detrended oscillator (price above its lagged baseline = cycle UP-phase), the higher
|
||||||
|
the next return; deep-negative dpo (cycle down-phase) precedes flat/negative returns.
|
||||||
|
So on these series the cycle is CONTINUATION, not reversion -> we FOLLOW the phase
|
||||||
|
(long the up-phase, flat/short the down-phase), confirmed by a slow trend gate, and
|
||||||
|
size with vol-targeting. Result on train: positive PnL at ~19% worst DD vs buy&hold's
|
||||||
|
~78% DD — anticipating the move means staying out of (or short) the down-phase.
|
||||||
|
|
||||||
|
Config tuned on train (period=30 / trendwin=200 / scale=1.5 / wc=0.6 / ema=2 / tv=0.18):
|
||||||
|
plateau-robust across period 30, trend 150-200, scale 1.5-2.0, cycle weight 0.5-0.8.
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
import blindlib as bl
|
||||||
|
|
||||||
|
# --- tuned on split='train' only ------------------------------------------
|
||||||
|
PERIOD = 30 # DPO moving-average period
|
||||||
|
LAG = PERIOD // 2 + 1 # textbook DPO displacement, kept strictly backward (causal)
|
||||||
|
TREND_WIN = 200 # slow-trend confirmation window
|
||||||
|
SCALE = 1.5 # tanh softness of the cycle phase
|
||||||
|
W_CYCLE = 0.6 # blend weight: cycle phase vs slow-trend confirmation
|
||||||
|
EMA_SMOOTH = 2 # position smoothing (cuts turnover/fees)
|
||||||
|
TARGET_VOL = 0.18 # annualized vol target
|
||||||
|
VOL_WIN = 30
|
||||||
|
LEV_CAP = 1.0
|
||||||
|
|
||||||
|
|
||||||
|
def _dpo_phase(c: np.ndarray) -> np.ndarray:
|
||||||
|
"""Detrended price oscillator z-phase: (price - LAGGED SMA) / rolling std of gap.
|
||||||
|
The baseline SMA is delayed by LAG bars, so every value uses only past data."""
|
||||||
|
n = len(c)
|
||||||
|
base = bl.sma(c, PERIOD) # causal SMA
|
||||||
|
base_lag = np.full(n, np.nan)
|
||||||
|
base_lag[LAG:] = base[:-LAG] # baseline from LAG bars ago (past only)
|
||||||
|
gap = c - base_lag
|
||||||
|
gap_vol = bl.rolling_std(gap, PERIOD)
|
||||||
|
gap_vol = np.where((gap_vol > 0) & np.isfinite(gap_vol), gap_vol, np.nan)
|
||||||
|
return gap / gap_vol # z-like cycle phase (NaN during warmup)
|
||||||
|
|
||||||
|
|
||||||
|
def signal(df):
|
||||||
|
c = df["close"].values.astype(float)
|
||||||
|
|
||||||
|
# detrended cycle phase (DPO core) — empirically CONTINUATION on these series
|
||||||
|
z = np.nan_to_num(_dpo_phase(c), nan=0.0)
|
||||||
|
cycle = np.tanh(z / SCALE) # +1 up-phase, -1 down-phase
|
||||||
|
|
||||||
|
# slow-trend confirmation (don't ride the cycle against a strong regime)
|
||||||
|
trend = c / bl.sma(c, TREND_WIN) - 1.0
|
||||||
|
follow = np.tanh(np.nan_to_num(trend, nan=0.0) * 6.0)
|
||||||
|
|
||||||
|
raw = np.clip(W_CYCLE * cycle + (1.0 - W_CYCLE) * follow, -1.0, 1.0)
|
||||||
|
raw = bl.ema(raw, EMA_SMOOTH) # smooth -> fewer fee-bleeding flips
|
||||||
|
|
||||||
|
pos = bl.vol_target(raw, df, target_vol=TARGET_VOL,
|
||||||
|
vol_win_days=VOL_WIN, leverage_cap=LEV_CAP)
|
||||||
|
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
"""Agent 28 — Williams %R momentum/reversion HYBRID, trend-gated (family=osc, slug=willr).
|
||||||
|
|
||||||
|
The angle (assigned): Williams %R momentum/reversion hybrid with a trend gate. Williams %R
|
||||||
|
is the inverse of the Stochastic %K: %R = -100 * (HH - close) / (HH - LL) over a trailing
|
||||||
|
window, ranging -100 (close at the window LOW = oversold) .. 0 (close at the window HIGH =
|
||||||
|
overbought). It measures where the close sits in its own rolling high/low channel, so it is
|
||||||
|
self-normalizing and sweeps the FULL -100..0 range even inside a bull (measured on train:
|
||||||
|
%R<-80 ~14% of bars, %R>-20 ~26% of bars). That dual occupancy is what makes a HYBRID
|
||||||
|
(reversion on one leg + momentum on the other) genuinely playable here.
|
||||||
|
|
||||||
|
Reading the train curves first (both A and B, split='train'): they trend UP very hard
|
||||||
|
(A 100->792, B 100->2400). A pure symmetric reversion ("short every %R>-20") would just
|
||||||
|
short the bull and bleed; a pure momentum rule rides crashes. The HYBRID + trend gate
|
||||||
|
resolves this by using %R DIFFERENTLY on each side of a long trend filter:
|
||||||
|
|
||||||
|
REVERSION LEG (in an UPTREND, close above a long SMA):
|
||||||
|
%R dipping into oversold (< OS, e.g. -80) is a BUY-THE-DIP setup. To ANTICIPATE the
|
||||||
|
bounce instead of knife-catching a still-falling close, we require %R to TURN BACK UP
|
||||||
|
(cross up through a short signal line = SMA of %R, the standard stochastic-style
|
||||||
|
trigger). We then HOLD the long (hysteresis) until %R recovers past EXIT, then flat.
|
||||||
|
This is the reversion half of the hybrid.
|
||||||
|
|
||||||
|
MOMENTUM LEG (in an UPTREND): once %R pushes into and STAYS overbought (> OB, e.g. -20),
|
||||||
|
in a hard bull that is NOT a fade signal — overbought persists and the trend runs. So
|
||||||
|
instead of shorting it (textbook reversion) we take a SMALLER continuation LONG
|
||||||
|
(MOM_W). This is the momentum half of the hybrid: %R>-20 in an uptrend = "trend is
|
||||||
|
strong, stay with it", the opposite trade to what reversion alone would do. This is
|
||||||
|
the key difference from the pure-reversion stochastic/RSI agents.
|
||||||
|
|
||||||
|
DOWNTREND (close below the long SMA): the symmetry returns and %R is read as reversion
|
||||||
|
again — %R overbought (> OB) with a cross DOWN through its signal line is a reversion
|
||||||
|
SHORT (rips fade). %R oversold we stand flat (don't knife-catch long under a
|
||||||
|
downtrend). The short side is down-weighted (SHORT_W) because the drift is up; on
|
||||||
|
train it is marginal (see HONEST NOTE).
|
||||||
|
|
||||||
|
So the gate does three jobs: (1) picks the reversion side (dip-long in up, rip-short in
|
||||||
|
down), (2) flips the overbought reading from "fade" to "ride" inside the bull (the hybrid),
|
||||||
|
(3) suppresses the side that fights the drift. Sizing is smooth (deeper extreme -> bigger
|
||||||
|
appetite, floored at BASE while holding) then VOL-TARGETED so the two curves are
|
||||||
|
risk-comparable and exposure shrinks into vol spikes (crashes are vol spikes) — that is
|
||||||
|
what bounds the drawdown. The leverage cap rarely binds, so the edge is NOT leverage.
|
||||||
|
|
||||||
|
HONEST NOTE (negative findings kept): (1) The downtrend reversion-short is nearly free but
|
||||||
|
adds little on train; kept small to honor the bidirectional angle. (2) The momentum
|
||||||
|
continuation leg (MOM_W) is what distinguishes this from a pure-reversion oscillator — in a
|
||||||
|
market that trends this hard it earns by riding the overbought regime instead of fading it,
|
||||||
|
but it ALSO partly degenerates toward trend participation (the honest ceiling for any
|
||||||
|
direction-on-a-bull rule). The genuine oscillator content is the cross-timed dip entry +
|
||||||
|
overbought exit cycle plus the DD control from the trend gate + vol-target. (3) A pure
|
||||||
|
always-on %R weighting (no flat state) degenerated into buy-and-hold (DD blew out); the
|
||||||
|
hysteresis flat state is what keeps DD modest. Result: an honest, modest combined train
|
||||||
|
Sharpe at a small DD — a fraction of buy&hold PnL but several-x less drawdown (it
|
||||||
|
anticipates the dip / rides the strong trend rather than holding through every crash).
|
||||||
|
|
||||||
|
CAUSAL: %R uses trailing rolling max(high)/min(low) (<= i); its signal line is a trailing
|
||||||
|
SMA of %R; the cross compares (%R - sig) at i vs i-1 (past only); the hold-state is a
|
||||||
|
forward cumulative pass over PAST bars only; the SMA trend filter and vol_target use
|
||||||
|
trailing data. No shift(-k), no centered windows, no global fit. Verified by causality_ok.
|
||||||
|
|
||||||
|
Tuning (train only, combined A&B; coarse->fine sweep + plateau check). The chosen cell sits
|
||||||
|
on a broad plateau (OB in [-35..-25], MOM_W in [0.3..0.5], SIG_WIN=5, R_WIN in [20..28],
|
||||||
|
EXIT in [-50..-40], OS=-80, BASE/TVOL/VWD all hold sharpe_min ~1.1..1.29 at DD ~3.3..5.6% —
|
||||||
|
a plateau, not a spike; SHORT_W is nearly free / marginal):
|
||||||
|
R_WIN=20, SIG_WIN=5, OS=-80, OB=-35, EXIT=-45, TREND_WIN=150
|
||||||
|
MOM_W=0.4, SHORT_W=0.4, BASE=0.6, TARGET_VOL=0.25, VOL_WIN_DAYS=35, LEV_CAP=1.5
|
||||||
|
-> train combined: pnl_mean ~0.46, maxdd_worst ~0.045, sharpe_min ~1.22
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import blindlib as bl
|
||||||
|
|
||||||
|
R_WIN = 20 # %R lookback (rolling high/low window). 20 > textbook 14 for these trends.
|
||||||
|
SIG_WIN = 5 # signal line = SMA(%R, SIG_WIN): the line %R crosses (stochastic-style trigger).
|
||||||
|
OS = -80.0 # oversold: %R below this in an uptrend + cross-up = dip-long entry.
|
||||||
|
OB = -35.0 # overbought: momentum-ride (uptrend) / reversion-short (downtrend) threshold.
|
||||||
|
EXIT = -45.0 # dip-long HELD until %R recovers past EXIT (hysteresis entry/exit pair).
|
||||||
|
TREND_WIN = 150 # long SMA: above = uptrend (dips=long, OB=ride), below = downtrend (OB=short).
|
||||||
|
MOM_W = 0.4 # weight on the uptrend overbought MOMENTUM-continuation long (the hybrid half).
|
||||||
|
SHORT_W = 0.4 # weight on the downtrend reversion-short; marginal (see HONEST NOTE).
|
||||||
|
BASE = 0.6 # base long size while holding a dip (scaled up if %R still oversold).
|
||||||
|
TARGET_VOL = 0.25
|
||||||
|
VOL_WIN_DAYS = 35
|
||||||
|
LEV_CAP = 1.5
|
||||||
|
|
||||||
|
|
||||||
|
def _willr(df, r_win, sig_win):
|
||||||
|
"""Causal Williams %R + its signal line. %R[i] = -100*(HH-close)/(HH-LL) over the
|
||||||
|
trailing r_win bars (<= i); sig[i] = SMA(%R, sig_win) (trailing). No look-ahead."""
|
||||||
|
h = df["high"].values.astype(float)
|
||||||
|
l = df["low"].values.astype(float)
|
||||||
|
c = df["close"].values.astype(float)
|
||||||
|
hh = pd.Series(h).rolling(r_win, min_periods=1).max().values
|
||||||
|
ll = pd.Series(l).rolling(r_win, min_periods=1).min().values
|
||||||
|
rng = hh - ll
|
||||||
|
wr = np.where(rng > 1e-12, -100.0 * (hh - c) / rng, -50.0)
|
||||||
|
sig = bl.sma(wr, sig_win)
|
||||||
|
return wr, sig
|
||||||
|
|
||||||
|
|
||||||
|
def signal(df):
|
||||||
|
c = df["close"].values.astype(float)
|
||||||
|
n = len(c)
|
||||||
|
wr, sig = _willr(df, R_WIN, SIG_WIN)
|
||||||
|
trend_up = c > bl.sma(c, TREND_WIN) # causal trailing SMA trend gate
|
||||||
|
|
||||||
|
# --- %R / signal-line crosses (past-only: compares i vs i-1) ---
|
||||||
|
ds = wr - sig
|
||||||
|
ds_prev = np.concatenate(([0.0], ds[:-1]))
|
||||||
|
cross_up = (ds > 0) & (ds_prev <= 0) # %R turns up through its signal line
|
||||||
|
cross_dn = (ds < 0) & (ds_prev >= 0) # %R turns down through its signal line
|
||||||
|
|
||||||
|
# --- smooth appetites (further past the extreme -> bigger) ---
|
||||||
|
# oversold depth: %R from OS down to -100 -> long appetite 0..1
|
||||||
|
long_app = np.clip((OS - wr) / (100.0 + OS), 0.0, 1.0)
|
||||||
|
# overbought depth: %R from OB up to 0 -> 0..1 (used by both momentum-long & rev-short)
|
||||||
|
ob_app = np.clip((wr - OB) / (0.0 - OB), 0.0, 1.0)
|
||||||
|
|
||||||
|
# --- trend-gated Williams %R momentum/reversion hybrid with hysteresis ---
|
||||||
|
# Forward pass is PURE PAST-ONLY: state at bar i depends only on bars <= i.
|
||||||
|
held = np.zeros(n)
|
||||||
|
in_long = False
|
||||||
|
for i in range(n):
|
||||||
|
if in_long:
|
||||||
|
# exit the held dip-long when trend breaks down OR %R has recovered past EXIT
|
||||||
|
if (not trend_up[i]) or (wr[i] >= EXIT):
|
||||||
|
in_long = False
|
||||||
|
else:
|
||||||
|
# enter a dip-long in an uptrend when %R is oversold AND turns up through its line
|
||||||
|
if trend_up[i] and (wr[i] < OS) and cross_up[i]:
|
||||||
|
in_long = True
|
||||||
|
if in_long:
|
||||||
|
held[i] = max(BASE, long_app[i]) # ride the recovery, bigger if still oversold
|
||||||
|
elif trend_up[i]:
|
||||||
|
# MOMENTUM half of the hybrid: overbought in an uptrend = ride the strong trend
|
||||||
|
held[i] = MOM_W * ob_app[i]
|
||||||
|
else:
|
||||||
|
# downtrend reversion-short: overbought AND %R turning down through its line
|
||||||
|
if (wr[i] > OB) and cross_dn[i]:
|
||||||
|
held[i] = -SHORT_W * ob_app[i]
|
||||||
|
else:
|
||||||
|
held[i] = 0.0
|
||||||
|
|
||||||
|
pos = bl.vol_target(held, df, target_vol=TARGET_VOL,
|
||||||
|
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
|
||||||
|
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
"""Agent 29 — Ridge regression return forecast (family=ml, slug=ridge).
|
||||||
|
|
||||||
|
THE ANGLE (assigned): forecast the forward return with a RIDGE regression on lagged
|
||||||
|
returns + volatility features, refit on an EXPANDING window every ~20 bars, and turn the
|
||||||
|
forecast into a position. A genuine ML angle (linear model, L2 penalty), NOT a fixed
|
||||||
|
momentum sign rule — ridge *weights* the lags and lets vol modulate conviction.
|
||||||
|
|
||||||
|
WHAT THE TRAIN DATA ACTUALLY SAYS (the honest finding, not the hoped-for one):
|
||||||
|
* NEXT-BAR return on these curves is unforecastable — the walk-forward forecast's next-bar
|
||||||
|
hit-rate is ~0.48-0.51 (coin flip). So I forecast a multi-bar FORWARD return (horizon
|
||||||
|
FWD_H), the autocorrelated/forecastable quantity, instead of bar-to-bar noise.
|
||||||
|
* The expanding ridge forecast is CONSISTENTLY, mildly *negatively* correlated with the
|
||||||
|
realized forward return (corr ~ -0.08..-0.22, same sign on BOTH series, ALL horizons).
|
||||||
|
i.e. on these strongly up-trending curves the model's most-bullish forecasts mark froth
|
||||||
|
that gives back, and its bearish forecasts precede the recoveries. This is a stable
|
||||||
|
property across the grid, not one lucky cell.
|
||||||
|
* SHORTING destroys value here (both raw-sign and inverted-sign books lose once shorts are
|
||||||
|
allowed — the curves only go up). The only honest edge a weak forecaster has on an
|
||||||
|
up-trend is WHEN TO HOLD vs. SIT IN CASH.
|
||||||
|
|
||||||
|
THE RULE: use the (inverted, given the negative corr) ridge forecast as a LONG-ONLY
|
||||||
|
conviction — be long when the model is bearish (post-froth recovery), flat when it is
|
||||||
|
bullish — then vol-target and clip to [0, 1]. Result on train: a book that is in-market only
|
||||||
|
~16% of the time, tiny drawdown (~0.02 vs 0.77-0.79 buy&hold), Sharpe ~0.83.
|
||||||
|
|
||||||
|
CAUSALITY (the whole game):
|
||||||
|
* Features at row i use ONLY returns up to and including bar i (rows <= i).
|
||||||
|
* Training TARGET for row j is the return over bar j -> j+FWD_H (needs close[j+FWD_H]).
|
||||||
|
Sitting at decision-row i we may only train on rows j with j+FWD_H <= i (their targets
|
||||||
|
are realized as of close[i]). We NEVER include row i's own unrealized target.
|
||||||
|
* Refit on an EXPANDING window of those realized (X,y) pairs every REFIT_EVERY bars;
|
||||||
|
coefficients frozen in between. No global fit, no future row touched.
|
||||||
|
-> Verified by causality_ok (prefix tail matches full-array tail, max_diff 0.0).
|
||||||
|
|
||||||
|
TUNING (split='train' only, combined A & B): chosen cell is interior on every axis —
|
||||||
|
FWD_H 18-25 -> Sharpe ~0.83 flat; alpha 20-100 -> Sharpe ~0.81-0.84 flat;
|
||||||
|
refit 10-20 -> stable; gain 1.0-2.5 monotone DD/PnL dial. Picked the interior point.
|
||||||
|
|
||||||
|
HONEST READ: alpha here is THIN. The forecastability is weak and the win is risk control,
|
||||||
|
not return generation — a low-exposure, low-DD long-only sleeve, NOT a PnL engine. The
|
||||||
|
inverted-sign edge is modest and could be regime-specific; the robust, defensible part is
|
||||||
|
"never short an up-trend; let the forecast tell you when to step out of the way."
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
import blindlib as bl
|
||||||
|
|
||||||
|
# ---- tuned on split='train' only (interior of a flat plateau) ----
|
||||||
|
RIDGE_ALPHA = 50.0 # L2 penalty (strong: the lag->return edge is tiny); plateau 20..100
|
||||||
|
WARMUP = 150 # realized (X,y) pairs required before the first fit
|
||||||
|
REFIT_EVERY = 20 # expanding-window refit cadence (assigned ~20); stable 10..20
|
||||||
|
LAGS = (1, 2, 3, 5, 10) # lagged-return features
|
||||||
|
MOM_WIN = 20 # trailing momentum feature window
|
||||||
|
VOL_WIN = 20 # trailing realized-vol feature window
|
||||||
|
FWD_H = 20 # forecast HORIZON (bars). Plateau 18..25. Next-BAR is noise; a
|
||||||
|
# multi-bar target is the autocorrelated, forecastable quantity.
|
||||||
|
GAIN = 1.5 # tanh conviction gain on the standardized forecast (DD/PnL dial)
|
||||||
|
INVERT = True # negative train corr (both series, all H) -> fade the forecast sign
|
||||||
|
LONG_ONLY = True # shorting an up-trend destroys value -> conviction is long-or-flat
|
||||||
|
TARGET_VOL = 0.20 # vol-target the directional book
|
||||||
|
VOL_WIN_DAYS = 30
|
||||||
|
LEV_CAP = 1.0 # never lever past fully invested -> preserve the DD cut
|
||||||
|
|
||||||
|
|
||||||
|
def _build_features(c):
|
||||||
|
"""Causal feature matrix X (len(c) rows). Row i uses ONLY data <= i.
|
||||||
|
Columns: lagged log-returns, trailing momentum, trailing realized vol."""
|
||||||
|
n = len(c)
|
||||||
|
lr = np.zeros(n)
|
||||||
|
lr[1:] = np.log(c[1:] / c[:-1]) # lr[i] = return of bar ending at i (causal)
|
||||||
|
|
||||||
|
cols = []
|
||||||
|
# lagged returns: feature value at i is the return from k bars ago (all <= i)
|
||||||
|
for k in LAGS:
|
||||||
|
f = np.zeros(n)
|
||||||
|
if k < n:
|
||||||
|
f[k:] = lr[: n - k] # lr shifted back by k -> uses past only
|
||||||
|
cols.append(f)
|
||||||
|
# trailing momentum: cumulative log-return over the last MOM_WIN bars (<= i)
|
||||||
|
mom = np.zeros(n)
|
||||||
|
csum = np.cumsum(lr)
|
||||||
|
mom[MOM_WIN:] = csum[MOM_WIN:] - csum[:-MOM_WIN]
|
||||||
|
cols.append(mom)
|
||||||
|
# trailing realized vol (std of last VOL_WIN returns, <= i)
|
||||||
|
vol = np.zeros(n)
|
||||||
|
for i in range(VOL_WIN, n):
|
||||||
|
vol[i] = np.std(lr[i - VOL_WIN + 1 : i + 1])
|
||||||
|
cols.append(vol)
|
||||||
|
|
||||||
|
X = np.column_stack(cols)
|
||||||
|
return X, lr
|
||||||
|
|
||||||
|
|
||||||
|
def _ridge_fit(X, y, alpha):
|
||||||
|
"""Closed-form ridge with a standardized design + intercept (no sklearn needed,
|
||||||
|
fully deterministic). Returns (mu, sd, beta0, beta) for prediction."""
|
||||||
|
mu = X.mean(axis=0)
|
||||||
|
sd = X.std(axis=0)
|
||||||
|
sd[sd < 1e-12] = 1.0
|
||||||
|
Xs = (X - mu) / sd
|
||||||
|
p = Xs.shape[1]
|
||||||
|
A = Xs.T @ Xs + alpha * np.eye(p)
|
||||||
|
b = Xs.T @ (y - y.mean())
|
||||||
|
beta = np.linalg.solve(A, b)
|
||||||
|
beta0 = y.mean()
|
||||||
|
return mu, sd, beta0, beta
|
||||||
|
|
||||||
|
|
||||||
|
def signal(df):
|
||||||
|
c = df["close"].values.astype(float)
|
||||||
|
n = len(c)
|
||||||
|
X, lr = _build_features(c)
|
||||||
|
|
||||||
|
# target[j] = cumulative log-return over bar j -> j+FWD_H (needs close[j+FWD_H]);
|
||||||
|
# known (realized) only as of close[j+FWD_H].
|
||||||
|
csum = np.cumsum(lr)
|
||||||
|
target = np.zeros(n)
|
||||||
|
target[: n - FWD_H] = csum[FWD_H:] - csum[: n - FWD_H]
|
||||||
|
|
||||||
|
yhat = np.zeros(n) # forecast of the forward return, decided at close[i]
|
||||||
|
sig_y = np.ones(n) # scale of recent forecast targets (for standardization)
|
||||||
|
coef = None # frozen (mu, sd, beta0, beta)
|
||||||
|
|
||||||
|
for i in range(n):
|
||||||
|
# at decision-row i we may train only on rows j whose target is realized, i.e.
|
||||||
|
# j + FWD_H <= i => j <= i - FWD_H. We NEVER include row i's own (unrealized) target.
|
||||||
|
first = max(LAGS) + MOM_WIN # earliest row with all features fully populated
|
||||||
|
last_train = i - FWD_H # target of last_train uses close[i], realized now
|
||||||
|
ntrain = last_train - first + 1
|
||||||
|
|
||||||
|
if ntrain >= WARMUP:
|
||||||
|
# refit every REFIT_EVERY bars (and on the very first eligible bar)
|
||||||
|
if coef is None or (i % REFIT_EVERY == 0):
|
||||||
|
Xtr = X[first : last_train + 1]
|
||||||
|
ytr = target[first : last_train + 1]
|
||||||
|
coef = _ridge_fit(Xtr, ytr, RIDGE_ALPHA)
|
||||||
|
s = np.std(ytr)
|
||||||
|
sig_y[i] = s if s > 1e-9 else 1.0
|
||||||
|
else:
|
||||||
|
sig_y[i] = sig_y[i - 1]
|
||||||
|
mu, sd, beta0, beta = coef
|
||||||
|
xi = (X[i] - mu) / sd
|
||||||
|
yhat[i] = beta0 + xi @ beta
|
||||||
|
|
||||||
|
# forecast -> bounded conviction (de-emphasize tiny/noisy forecasts, saturate strong ones)
|
||||||
|
s = np.where(sig_y > 1e-9, sig_y, 1.0)
|
||||||
|
direction = np.tanh(GAIN * yhat / s)
|
||||||
|
direction = np.nan_to_num(direction, nan=0.0)
|
||||||
|
if INVERT:
|
||||||
|
direction = -direction # train corr is negative on both series/all H
|
||||||
|
if LONG_ONLY:
|
||||||
|
direction = np.clip(direction, 0.0, 1.0) # never short an up-trend (shorts lose here)
|
||||||
|
|
||||||
|
# vol-target the conviction so the DRAWDOWN is what we control
|
||||||
|
pos = bl.vol_target(direction, df, target_vol=TARGET_VOL,
|
||||||
|
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
|
||||||
|
if LONG_ONLY:
|
||||||
|
pos = np.clip(pos, 0.0, LEV_CAP)
|
||||||
|
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
"""Agent 30 — Logistic up/down classifier (family=ml, slug=logistic).
|
||||||
|
|
||||||
|
THE ANGLE (assigned): a LOGISTIC REGRESSION that classifies "will the forward move be
|
||||||
|
up or down?" from technical features (momentum at several horizons, trailing realized
|
||||||
|
vol, RSI), refit on an EXPANDING walk-forward window every ~20 bars, and maps the class
|
||||||
|
probability p(up) into a position in [-1, +1].
|
||||||
|
|
||||||
|
WHY A CLASSIFIER (not a return-regressor): the per-bar *magnitude* of these curves is
|
||||||
|
dominated by noise — the sign of the forward move is the only thing with any persistence.
|
||||||
|
A logistic model targets exactly that (a Bernoulli up/down label), and its probability
|
||||||
|
output is a natural, bounded conviction: p≈0.5 → flat, p far from 0.5 → take the side.
|
||||||
|
The L2 penalty (C small) keeps the coefficients from chasing the (thin) edge into noise.
|
||||||
|
|
||||||
|
CAUSALITY (the whole game):
|
||||||
|
* Features at row i use ONLY data up to and including bar i (rows <= i): lagged log-
|
||||||
|
returns, multi-horizon trailing momentum, trailing realized vol, RSI.
|
||||||
|
* The LABEL for row j is sign of the cumulative return over bar j -> j+FWD_H, which
|
||||||
|
needs close[j+FWD_H]. So sitting at decision-row i we may train ONLY on rows whose
|
||||||
|
label is already realized: j + FWD_H <= i => j <= i - FWD_H. Row i's own label is
|
||||||
|
NEVER used.
|
||||||
|
* Model is refit on the EXPANDING window of those realized (X, y) pairs at most every
|
||||||
|
REFIT_EVERY bars; coefficients frozen in between. position[i] = frozen model's
|
||||||
|
p(up) at row i, mapped to a direction, then vol-targeted.
|
||||||
|
-> Verified by causality_ok (signal on a prefix must match signal on the full array).
|
||||||
|
|
||||||
|
TUNING (split='train' only, combined A & B): C (inverse L2) small (~0.05-0.2) so the
|
||||||
|
weak edge isn't overfit; FWD_H ~ 5-10 (the forecastable horizon — next-bar sign is a
|
||||||
|
coin flip); WARMUP ~ 200 realized pairs; conviction = 2*(p-0.5) sharpened by a gain,
|
||||||
|
then vol-targeted (cap 1.0) so the DRAWDOWN, not the raw PnL, is what we optimise.
|
||||||
|
|
||||||
|
HONEST READ: forward-sign forecastability here is weak; the realistic win is a vol-
|
||||||
|
controlled book that can flip short into declines, giving comparable PnL to long-only
|
||||||
|
at a much smaller drawdown — the de-risking is the alpha, not a strong classifier.
|
||||||
|
"""
|
||||||
|
import warnings
|
||||||
|
import numpy as np
|
||||||
|
import blindlib as bl
|
||||||
|
|
||||||
|
warnings.filterwarnings("ignore")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from sklearn.linear_model import LogisticRegression
|
||||||
|
_HAVE_SK = True
|
||||||
|
except Exception: # pragma: no cover - sklearn expected present
|
||||||
|
_HAVE_SK = False
|
||||||
|
|
||||||
|
# ---- tuned on split='train' only (interior of broad plateaus; see scan below) ----
|
||||||
|
C_INV = 0.20 # inverse L2 strength (small = strong penalty); flat 0.05-1.0
|
||||||
|
WARMUP = 200 # realized (X, y) pairs required before the first fit
|
||||||
|
REFIT_EVERY = 20 # expanding-window refit cadence (assigned ~20)
|
||||||
|
LAGS = (1, 2, 3, 5) # lagged log-return features
|
||||||
|
MOM_WINS = (10, 20, 40) # multi-horizon trailing-momentum features
|
||||||
|
VOL_WIN = 20 # trailing realized-vol feature window
|
||||||
|
RSI_WIN = 14 # RSI feature window
|
||||||
|
FWD_H = 15 # label HORIZON: sign of cumulative return over next FWD_H bars.
|
||||||
|
# next-bar sign is a coin-flip; the multi-bar sign is the
|
||||||
|
# persistent, classifiable quantity. Plateau FWD 14-18.
|
||||||
|
DEADBAND = 0.04 # ignore |2p-1| below this (treat as no-conviction -> flat)
|
||||||
|
GAIN = 3.0 # conviction gain on the centered probability 2*(p-0.5)
|
||||||
|
SHORT_SCALE = 0.25 # asymmetric book: full long, only PARTIAL short. Both curves
|
||||||
|
# drift UP, so the classifier's real value is STEPPING ASIDE
|
||||||
|
# from declines; a full short fights the drift and adds DD.
|
||||||
|
# 0.25 keeps a genuine (small) short so it stays prob->position.
|
||||||
|
TARGET_VOL = 0.20 # vol-target the directional book
|
||||||
|
VOL_WIN_DAYS = 30
|
||||||
|
LEV_CAP = 1.0 # never lever past fully invested -> preserve the DD cut
|
||||||
|
|
||||||
|
|
||||||
|
def _build_features(c):
|
||||||
|
"""Causal feature matrix X (len(c) rows). Row i uses ONLY data <= i."""
|
||||||
|
n = len(c)
|
||||||
|
lr = np.zeros(n)
|
||||||
|
lr[1:] = np.log(c[1:] / c[:-1]) # lr[i] = return of bar ending at i (causal)
|
||||||
|
csum = np.cumsum(lr)
|
||||||
|
|
||||||
|
cols = []
|
||||||
|
# lagged returns: value at i is the return k bars ago (all <= i)
|
||||||
|
for k in LAGS:
|
||||||
|
f = np.zeros(n)
|
||||||
|
if k < n:
|
||||||
|
f[k:] = lr[: n - k]
|
||||||
|
cols.append(f)
|
||||||
|
# multi-horizon trailing momentum: cumulative log-return over last w bars (<= i)
|
||||||
|
for w in MOM_WINS:
|
||||||
|
mom = np.zeros(n)
|
||||||
|
mom[w:] = csum[w:] - csum[:-w]
|
||||||
|
cols.append(mom)
|
||||||
|
# trailing realized vol (std of last VOL_WIN returns, <= i)
|
||||||
|
vol = np.zeros(n)
|
||||||
|
cs2 = np.cumsum(lr * lr)
|
||||||
|
for i in range(VOL_WIN, n):
|
||||||
|
m = (csum[i] - csum[i - VOL_WIN]) / VOL_WIN
|
||||||
|
v = (cs2[i] - cs2[i - VOL_WIN]) / VOL_WIN - m * m
|
||||||
|
vol[i] = np.sqrt(max(v, 0.0))
|
||||||
|
cols.append(vol)
|
||||||
|
# RSI (causal, from blindlib)
|
||||||
|
rsi = np.nan_to_num(bl.rsi(c, RSI_WIN), nan=50.0) / 100.0
|
||||||
|
cols.append(rsi)
|
||||||
|
|
||||||
|
X = np.column_stack(cols)
|
||||||
|
return X, lr, csum
|
||||||
|
|
||||||
|
|
||||||
|
def _fit(Xtr, ytr):
|
||||||
|
"""Logistic fit on standardized features. Returns (mu, sd, model) or None if the
|
||||||
|
training labels are single-class (no fit possible yet)."""
|
||||||
|
mu = Xtr.mean(axis=0)
|
||||||
|
sd = Xtr.std(axis=0)
|
||||||
|
sd[sd < 1e-12] = 1.0
|
||||||
|
Xs = (Xtr - mu) / sd
|
||||||
|
if len(np.unique(ytr)) < 2:
|
||||||
|
return None
|
||||||
|
if _HAVE_SK:
|
||||||
|
m = LogisticRegression(C=C_INV, solver="lbfgs", max_iter=200)
|
||||||
|
m.fit(Xs, ytr)
|
||||||
|
return (mu, sd, m)
|
||||||
|
# tiny fallback: penalized logistic via Newton steps (deterministic)
|
||||||
|
w = _logit_newton(Xs, ytr, C_INV)
|
||||||
|
return (mu, sd, w)
|
||||||
|
|
||||||
|
|
||||||
|
def _logit_newton(Xs, y, c_inv, iters=25):
|
||||||
|
n, p = Xs.shape
|
||||||
|
Xb = np.column_stack([np.ones(n), Xs])
|
||||||
|
w = np.zeros(p + 1)
|
||||||
|
lam = 1.0 / max(c_inv, 1e-6)
|
||||||
|
R = np.eye(p + 1); R[0, 0] = 0.0 # don't penalize intercept
|
||||||
|
for _ in range(iters):
|
||||||
|
z = Xb @ w
|
||||||
|
pr = 1.0 / (1.0 + np.exp(-np.clip(z, -30, 30)))
|
||||||
|
Wd = pr * (1 - pr) + 1e-6
|
||||||
|
grad = Xb.T @ (pr - y) + lam * (R @ w)
|
||||||
|
H = Xb.T @ (Xb * Wd[:, None]) + lam * R
|
||||||
|
try:
|
||||||
|
w -= np.linalg.solve(H, grad)
|
||||||
|
except np.linalg.LinAlgError:
|
||||||
|
break
|
||||||
|
return w
|
||||||
|
|
||||||
|
|
||||||
|
def _predict_proba(coef, xi):
|
||||||
|
mu, sd, m = coef
|
||||||
|
xs = (xi - mu) / sd
|
||||||
|
if _HAVE_SK and not isinstance(m, np.ndarray):
|
||||||
|
return float(m.predict_proba(xs.reshape(1, -1))[0, 1])
|
||||||
|
z = m[0] + xs @ m[1:]
|
||||||
|
return float(1.0 / (1.0 + np.exp(-np.clip(z, -30, 30))))
|
||||||
|
|
||||||
|
|
||||||
|
def signal(df):
|
||||||
|
c = df["close"].values.astype(float)
|
||||||
|
n = len(c)
|
||||||
|
X, lr, csum = _build_features(c)
|
||||||
|
|
||||||
|
# label[j] = 1 if cumulative return over bar j -> j+FWD_H is up, else 0.
|
||||||
|
# realized (known) only as of close[j+FWD_H].
|
||||||
|
fwd = np.zeros(n)
|
||||||
|
fwd[: n - FWD_H] = csum[FWD_H:] - csum[: n - FWD_H]
|
||||||
|
label = (fwd > 0).astype(float)
|
||||||
|
|
||||||
|
first = max(max(LAGS), max(MOM_WINS), VOL_WIN, RSI_WIN) # first fully-featured row
|
||||||
|
prob = np.full(n, 0.5)
|
||||||
|
coef = None
|
||||||
|
|
||||||
|
for i in range(n):
|
||||||
|
last_train = i - FWD_H # label of last_train uses close[i], realized now
|
||||||
|
ntrain = last_train - first + 1
|
||||||
|
if ntrain >= WARMUP:
|
||||||
|
if coef is None or (i % REFIT_EVERY == 0):
|
||||||
|
Xtr = X[first : last_train + 1]
|
||||||
|
ytr = label[first : last_train + 1]
|
||||||
|
fit = _fit(Xtr, ytr)
|
||||||
|
if fit is not None:
|
||||||
|
coef = fit
|
||||||
|
if coef is not None:
|
||||||
|
prob[i] = _predict_proba(coef, X[i])
|
||||||
|
|
||||||
|
# probability -> bounded direction. centered conviction 2*(p-0.5) in [-1,1];
|
||||||
|
# deadband kills no-conviction bars; tanh sharpens; the short side is scaled down
|
||||||
|
# (the up-drift makes full shorts a losing fight — we mainly want to step aside).
|
||||||
|
conv = 2.0 * prob - 1.0
|
||||||
|
conv = np.where(np.abs(conv) < DEADBAND, 0.0, conv)
|
||||||
|
direction = np.tanh(GAIN * conv)
|
||||||
|
direction = np.where(direction < 0.0, direction * SHORT_SCALE, direction)
|
||||||
|
direction = np.nan_to_num(direction, nan=0.0)
|
||||||
|
|
||||||
|
pos = bl.vol_target(direction, df, target_vol=TARGET_VOL,
|
||||||
|
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
|
||||||
|
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user