81 Commits

Author SHA1 Message Date
Adriano Dal Pastro 25a22fc7c1 feat(dashboard): riordino LIVE→trades→storico + trade/anno contati
- dashboard riorganizzata in 3 sezioni: ① LIVE (mainnet sola lettura) in
  alto, ② TRADES ESEGUITI (reali) + frequenza operativa al centro,
  ③ STORICO (backtest/forward simulato) in fondo (COMBO/BOOK/FORWARD come ③·a/b/c).
- book_trade_frequency() in sleeves.py: trade/anno CONTATI sui dati certificati
  (cache di modulo, una volta per processo). SKH01 round-trip BTC ~37 / ETH ~43
  -> ~75/anno combinato; TP01 turnover ~7x/anno. Card "trade/anno" + blocco freq.
- fix: collisione var `pos` nel ramo shadow-online (-> shpos) che avrebbe rotto
  la tabella posizioni se il conto fosse leggibile dal container.
- fix: turnover TP01 nan (disallineamento groupby) -> groupby posizionale, 7.2x/anno.
- 56 test pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 21:18:52 +00:00
Adriano Dal Pastro eeac97dde4 feat(dashboard): Deribit-only book panel (TP01+SKH01) + accumulation forecast
New section showing the executable Deribit-only book (TP01 75% + SKH01 25%): combined
FULL/HOLD Sharpe+DD, plus the reinvest-winnings accumulation projection (historical &
conservative CAGR, €5k→5y/10y, conservative €/day run-rate). Reuses the already-computed
sleeve daily series (no extra heavy compute). Honest caveats (bull sample, no leverage,
SKH01 not live, ~€177k for €50/day).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 20:43:47 +00:00
Adriano Dal Pastro c8a390d6b7 feat(portfolio): reproducible accumulation forecast for the Deribit book
scripts/portfolio/forecast_deribit_book.py — projects the Deribit-only book (TP01+SKH01)
forward by pure compounding (reinvest winnings), monthly-aligned, NO external contributions
(not a PAC). Deterministic @historical & @conservative CAGR + Monte-Carlo block-bootstrap
(median/p10/p90), plus €/day run-rate @conservative. Parametric (--capital/--years/--cons-frac).
Honest caveats baked in (bull-crypto sample, no leverage, SKH01 not live, conditional projection).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 20:42:14 +00:00
Adriano Dal Pastro 384b9cb0af feat(skyhook): pos_fn introspection for SKH01 sleeve (current open trade / flat)
_skyhook_positions(): replays the non-overlap entry+exit logic (TP/SL/max_bars) to the last
closed 230m bar and reports, per asset, the current OPEN trade (dir/entry/sl/tp/bars_in) or
'flat'. Wired into skyhook_sleeve(pos_fn=...) so the Deribit book report & web dashboard show
Skyhook's live position. Causal (closed bars only). +1 test. Currently flat/flat.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 20:32:14 +00:00
Adriano Dal Pastro 160ad300be feat(portfolio): Deribit-only executable book (TP01+SKH01) + periodic rebalancing
- deribit_book_sleeves(): TP01 75% + SKH01 25% — the two directional BTC/ETH legs on
  ONE venue (Deribit), both since 2019. Excludes XS01 (Hyperliquid/stat-mode) & VRP01
  (modeled options). FULL Sharpe 1.78 / HOLD 1.17 / DD 9.4% (research).
- rebalance_sim(): realistic PERIODIC rebalancing (drift between dates, turnover cost at
  Deribit-taker ~5bps/side) vs the idealized continuous rebalance of combined_daily.
  period=1 + cost=0 reduces to continuous (tested).
- run_deribit_book.py: report — continuous vs weekly/biweekly/monthly rebal, per-year,
  accumulation €2k & $600-real, min-order $5 note. Finding: turnover is LOW (0.2-0.4x/yr),
  so monthly rebal (€7,919) ~= continuous (€7,938) — cost is negligible; daily would be
  sub-min-order fiction at $600 -> use >= weekly.
- +2 tests (rebalance_sim continuity & cost). Full suite green.

TP01 is the only live-armed leg; SKH01 is the candidate 2nd leg (validate execution code first).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 20:26:53 +00:00
Adriano Dal Pastro 50e2adf837 merge(skyhook): SKH01-V2-DD strategy + 4-sleeve portfolio + dashboard
Brings the Skyhook line into main:
- SKH01 dual-TF regime+breakout engine (BTC/ETH, causal, honest harness)
- 2 multi-agent research waves -> SKH01-V2-DD (asymmetric %-exits, standalone
  maxDD <30%, minHold +1.26, marginal ADDS vs TP01)
- wired as 4th portfolio sleeve @25% effective: FULL Sharpe 1.68->2.13, DD 14->8%
- dashboard shows the 4-sleeve view; tests 25 pass

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 16:42:15 +00:00
Adriano Dal Pastro 7eb0f67956 feat(dashboard): show SKH01 sleeve in 4-sleeve portfolio view
active_sleeves() already feeds the per-sleeve table & combined metrics, so SKH01
appears automatically. Manual touch-ups: title/docstring -> +SKH01; position label
is now sleeve-aware (the None fallback used to mislabel every pos-fn-less sleeve as
XS01's "book 19 gambe" — now XS01/SKH01/VRP01 get correct labels); footer note adds
SKH01 (quasi-orthogonal @25%, FULL Sharpe 1.68->2.13, DD 14->8%, research/forward-monitor).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 16:41:52 +00:00
Adriano Dal Pastro 8d1fe173f7 feat(portfolio): wire SKH01-V2-DD sleeve @25% effective -> 4-sleeve book
Add Skyhook (SKH01_V2_DD) as a portfolio sleeve. Effective weight 25%: the three
existing sleeves scaled into the remaining 0.75 keeping their 55:25:20 ratio
(TP01 41.25% / XS01 18.75% / VRP01 15% / SKH01 25%).

_skyhook_returns(): 50/50 BTC+ETH daily series of the dual-TF regime+breakout engine
(causal, net 0.10% RT), same convention as the marginal lens.

Portfolio impact (run_portfolio.py), 3-sleeve -> 4-sleeve:
  FULL Sharpe 1.68 -> 2.13 (+0.45), FULL maxDD 14.3% -> 7.8% (halved)
  HOLD-OUT Sharpe 1.63 -> 2.30 (+0.67), HOLD-OUT maxDD ~3.5% (flat)
  Positive every year 2019-26 (annual DD <=7.8%) vs buy&hold 50/50 FULL Sh 0.93 / DD 76%.

Skyhook is quasi-orthogonal (corr ~0.09 to TP01) so it lifts Sharpe AND cuts DD.
Research portfolio (fixed weights, no real rebalancing cost at $600; Skyhook daily
Sharpe is the step-marked lens convention) -> forward-monitor, not deploy.
Tests: 25 pass (skyhook 8 + portfolio 7 + vrp 4 + trend 6). Diary + CLAUDE.md updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 16:22:15 +00:00
Adriano Dal Pastro de72e3ce1f feat(skyhook): SKH01-V2-DD — asymmetric %-exits cut standalone DD <30% (2-wave agent research)
Second agent wave (skyhook-improve-v2, 14 DD-reduction families, each adversarially
verified by 2 skeptics) beats the prior winner on the only unmet goal (DD<30%).

Winner = ASYM_LS -> promoted to engine as SKH01_V2_DD:
  same signal (ptn_n=45, vola[35,95], vol_lo=0, exit-bars 24/16) but exits switched
  from ATR to FIXED-PCT ASYMMETRIC — long sl4%/tp10%, short sl2%(tighter)/tp8%.
  The tight short %-SL caps the per-trade loss that forms the maxDD in vol spikes.

Verified (sk.study, independent re-run): standalone maxDD BTC 21.4% / ETH 27.4% (<30%),
minFull +0.99, minHold +1.26, causality 0/400 both assets, fee-surviving to 0.40%RT,
marginal vs TP01 ADDS (corr 0.09, in-sample edge, robust_oos, multicut, clean-year +0.57),
blend 0.75*TP01+0.25*SKH uplift_hold +0.87; blend 50/50 full 1.84/hold 1.59/DD 10.7%.
Plateau (not knife-edge); both skeptics holds_up=high, killer=null.

Engine: per-direction short exit overrides (exit_mode_short/sl_*_short/tp_*_short),
backward-compatible (None -> symmetric, V1/intermediate-winner unchanged). +3 tests (8/8 pass).

Lessons: DD is cut by changing the exit MECHANISM (%-SL, L/S asymmetry, ensembles), NOT by
entry-only kill-switch / vol-target / cadence. PATTERN_CONF killed as overfit (knife-edge).
PCTL_DD unverified (rate-limit) and ENS_PARAM/TPSL_DD recency/hedge-loaded -> forward-monitor.
NOT yet wired to live sleeves: re-verify blend@0.25 + causality on execution code before deploy.

Includes both waves' research scripts (runs/SKH_* wave 1, runs/SKH2_* wave 2).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 16:10:38 +00:00
Adriano Dal Pastro 8e46a62e67 docs(skyhook): diario porting SKH01 + V1 (sintesi onda agenti in aggiornamento)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:47:41 +00:00
Adriano Dal Pastro c7c07f4c35 test(skyhook): demo anchors + dual-TF alignment + causality + V1 robustness (5 pass)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:46:47 +00:00
Adriano Dal Pastro 2d8faf3896 research(skyhook): inline lever-scout -> shorts essential, regime gate matters, ptn_n=55/vol_lo=40/wider-stops lift hold-out
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:35:35 +00:00
Adriano Dal Pastro 64d98a070d feat(skyhook): SKH01 dual-TF regime+breakout engine + honest eval harness
Porting onesto del sistema ES Skyhook su BTC/ETH certificati:
- src/strategies/skyhook.py: 690m(segnale)+230m(exec) da 5m; BuzVola/BuzVolume
  Chande 0-100 (ancore demo verificate); Donchian breakout HTF; regime gate;
  composer; entries asimmetrici (uscitalong/short + stop/profit ATR) per backtest_signals.
- scripts/research/skyhook/skyhooklib.py: study (FULL/HOLD/fee-sweep/per-anno BTC&ETH),
  causality guard (0 mismatch), marginal-vs-TP01.
Baseline: BTC FULL Sh +0.91/+581%, ETH +0.64/+255%, fee-surviving, ma HOLD-OUT debole -> da migliorare.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:33:54 +00:00
Adriano Dal Pastro 237ca8da13 research(report): resoconto PROTETTO (soft-guard DD -4%) anno-per-anno -> combo + TP01 + GTAA singoli
Report che mette combo (TP01+GTAA 50/50), TP01 e GTAA sulla stessa griglia giorni-di-borsa
(esposizione 1x, come dentro al combo), applica la guardia-DD -4% a ciascuna serie e tira fuori
per anno: NL (net liquidation da $2000), DD intra-anno, rendimento, Sharpe + riga TOT con CAGR.
Combo protetto: CAGR +9.1% / DD 5.8% / Sh 1.38 (2022 -1.8%); baseline +11.3% / 8.4% / 1.48.
Aggiunto data/paper_combo/ al .gitignore (stato paper runtime, come gli altri paper dir).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:16:59 +00:00
Adriano Dal Pastro 856a02fcc5 research(stops): SL classici vs soft-guard -> il soft-guard vince (lo SL duro whippa nel grind)
Goal 'prova anche SL'. Test equo (trigger/re-entry sul NAV mercato). soft-guard -4% Sh 1.38/DD 5.8%
resta il migliore; trail-stop -6% valido ma inferiore (1.34/6.6%); -4% whipsaw (1.07, inMkt 42%);
stop mensile/vol inutili. Per un DD da grind, de-risk parziale > uscita totale.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 13:09:43 +00:00
Adriano Dal Pastro 010d1f0733 feat(combo): paper combo NUDO vs PROTETTO (guardia-DD -4%) affiancati + dashboard
paper_combo traccia forward entrambe le versioni; dashboard mostra nudo + protetto. Guardia-DD:
de-risk 0.4x a DD>-4%, ri-rischia a -1.6% (backtest MaxDD 8.4->5.8%, 2022 -4.4->-1.8%). Opzioni
escluse (non aiutano il grind). Container ricostruito.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 12:59:07 +00:00
Adriano Dal Pastro 1c15c3c1be research(tail-hedge): protezione DD combo (incl. opzioni) -> vince la guardia-drawdown
Goal: sleeve/overlay protettivo per il combo (TP01+GTAA), anni tipo 2022, valutare opzioni.
Diagnosi: DD combo 8.4% e' grind-lento (2022 -4.4%), non crash -> il doppio trend gia' taglia i crash.

Test (tail_hedge_lab.py): guardia-DD -4% -> MaxDD 8.4->5.8%, 2022 -4.4->-1.8%, Sharpe 1.48->1.38,
CAGR 9.2%. Vol-target NON aiuta (2022 non e' vol-spike). OPZIONI (put/put-spread LONG su BTC/ETH,
premio BS su DVOL): sempre-on ~50%/anno -> con budget 3%/y effetto ~nullo, e nel grind 2022 sanguinano;
pagano solo nei crash secchi (stress -30%: put +25% netto). -> black-swan insurance cara, fuori
bersaglio per il 2022. A leva: guard rende il 2x sopportabile (2022 -10.9%), il 3x resta margin-call.

RACCOMANDAZIONE: aggiungere guardia-drawdown di portafoglio (no premio); opzioni solo eventuale
micro-hedge black-swan. Costo onesto del guard: -2.1pp CAGR per dimezzare il MaxDD.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 12:55:14 +00:00
Adriano Dal Pastro 3a3bdd5c7b research(report): sim leva 1x/2x/3x combo vs TP01 (DD reale + margin-call), da 2k/5k
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 12:44:54 +00:00
Adriano Dal Pastro f983cc2447 research(report): resoconto anno-per-anno combo/TP01/GTAA da $2k (PnL/MaxDD/NumTrades)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 12:37:23 +00:00
Adriano Dal Pastro 389573e517 feat(dashboard): pannello COMBO cross-venue (TP01 Deribit + GTAA IB)
Aggiunge alla dashboard la sezione "COMBO DEPLOYABLE — cross-venue (paper)": equity paper forward
del blend 50/50 TP01+GTAA (da data/paper_combo/state.json) + posizioni azionabili IB correnti
(gtaa_weights: peso ETF + cash, asof). Nota onesta: paper rischio-zero, Sharpe ~1.5 ottimistico, il
robusto e' la diversificazione (corr 0.21, DD dimezzato); XS01/VRP01 esclusi (STAT-MODE).
Container ricostruito (codice baked nell'immagine).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 12:32:19 +00:00
Adriano Dal Pastro 3b552a92da feat(combo): paper-trade cross-venue TP01 (Deribit) + GTAA (IB), forward-only
L'unica cosa vera/deployabile della ricerca: diversificazione TP01+GTAA (corr 0.21, blend Sharpe ~1.5,
DD dimezzato). Si va in PAPER cross-venue.

- src/portfolio/gtaa.py: GTAA sleeve di prima classe (trend difensivo TSMOM vol-target 12% su
  SPY/QQQ/IWM/TLT/GLD/HYG). gtaa_returns() Sharpe 0.64; gtaa_weights() = pesi ETF correnti azionabili.
- scripts/live/paper_combo.py: tracker forward-only blend 50/50 TP01+GTAA (crypto compoundato su grid
  giorni-di-borsa), mostra posizioni azionabili su entrambi i venue. Solo gambe eseguibili.
- fetch_ib_equities.py --only: refresh mirato dei 6 ETF GTAA per il cron.
- cron_daily.sh: up gateway IB + refresh ETF GTAA + avanza paper_combo (dipendenza cross-venue gestita).

Init 2026-06-23: TP01 flat (risk-off), GTAA SPY13/QQQ8/IWM9/TLT17/GLD2/HYG17/cash34. Catena
gateway->refresh->paper testata end-to-end. PAPER (rischio zero), valida l'operativita' cross-venue.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 12:26:51 +00:00
Adriano Dal Pastro 09c8bb7de8 research(cross-market): oltre SP500 (bond/commodity/indici esteri) -> niente, artefatto di confine UTC
Esteso il test crypto-lead a ZN(bond), ESTX50/DAX(Europa), NKD(Nikkei) via futures orari IB
(commodity GC/CL/HG bloccate da subscription). Test non-sovrapposto crypto[T-8h->T]->future[T->T+6h].

ES/NQ/RTY niente (gia'); ZN negativo; NKD debole (~overnight drift). ESTX50/DAX SEMBRANO fortissimi
(t_crypto 7.8, Sharpe 2.5, 3/3 anni) MA e' artefatto di confine UTC: picco a coltello a T=00:00,
morto a T=1h; GAP di 1h uccide l'effetto (Sharpe 2.45->-0.52); tutto l'edge nella singola barra
00:00->01:00 (Sh +2.93) vs ora dopo (-1.02). Firma esatta di day_boundary_robust (CLAUDE.md).

VERDETTO: nessuna anticipazione crypto->mercato sfruttabile, ne' SP500 ne' altro. Sempre co-movimento
contemporaneo (risk-beta) o artefatto di confine. Resta valido solo il diversificatore TP01+GTAA.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 12:12:16 +00:00
Adriano Dal Pastro c4bc336a53 research(cross-market): futures overnight non-sovrapposto -> edge ~0 su SP500, soffio su small-cap
Test ONESTO dell'idea "monitor Deribit/trade IB": entra mid-notte sul future indice, cattura il moto
SUCCESSIVO (finestre non sovrapposte, no look-ahead). Dati: ES/NQ/RTY orari da IB (fut_*_1h, ~3y).

RISULTATO: ES (S&P500) nessun edge (Sharpe ~0/neg, t_crypto 0-1.5); NQ momentum del future non crypto;
RTY (small-cap) unico con t_crypto incrementale 2.0-2.7 e crypto che aggiunge oltre il moto proprio del
future, ma Sharpe 0.4-0.5, 24 config (multiple-testing), 2.3y, per-anno incoerente (2026 negativo).

VERDETTO: l'idea NON da' edge tradabile, men che meno su SP500. Il forte crypto<->equity e' co-movimento
contemporaneo (risk-beta), non anticipazione: imposta una finestra causale non-sovrapposta e svanisce.
Il "Sharpe 5" del gap era look-ahead. RTY -> forward-monitor al piu'. Coerente col soffitto del progetto.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 11:51:15 +00:00
Adriano Dal Pastro 55c337144e research(cross-market): "monitor Deribit/trade IB" gap = LOOK-AHEAD, edge tradabile ~0
L'idea (segnale crypto overnight -> trade indice IB) sembrava Sharpe 3.6-5.9 ma e' look-ahead:
la finestra segnale crypto [P21:00->D13:00] e il gap equity [Pclose->Dopen] coprono le stesse ore.
All'entrata (D13:00, pre-open) il gap e' gia' avvenuto -> non catturabile con l'ETF.
Decomposizione (net 2bps, sqrt252): OVERLAP gap Sh ~3.6-4.0 (artefatto) vs TRADABILE intraday
Sh -0.03..0.25 (reale, ~0, muore a costi). Conferma/rafforza "non deployabile" del workflow.
Resta possibile solo la versione futures mid-overnight (finestre non sovrapposte) -> serve dato
intraday ES/NQ, non in cache.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 11:23:18 +00:00
Adriano Dal Pastro d916520f2c research(cross-market): sweep 65-agenti crypto->mercati IB -> fenomeno gap robustissimo ma NON edge
Goal: >=50 agenti, migliore soluzione, diversi mercati e timing, su piu' anni.
Setup: 26 ETF certificati (IB) + BTC/ETH 1h; harness parametrizzato (lead overnight crypto ->
gap/intraday equity, t-incrementale, Sharpe IS/OOS, hit per-anno); workflow 416 config = 52 sweep +
12 verifica avversariale + 1 sintesi = 65 agenti.

RISULTATO: cluster fortissimo crypto-overnight -> GAP apertura equity (tutti i target risk-on).
Migliori: ETH->IWM/QQQ/XLK gap (Sh OOS 2.4-2.5, t 17), BTC->QQQ gap (Sh OOS 2.31, t 15, 9/9 ANNI).
Regge stress 10bps e OOS recente. MA due killer (verificatori concordi):
  1. NON tradabile via ETF (gap gia' all'open) -> serve future overnight (MNQ/MES), fuori dal
     capitale $0.5-2k (margin/liquidazione);
  2. e' RISK-BETA non alpha: finestra-lead ~contemporanea al gap (stesso shock macro), forza solo
     negli anni alta-vol (2022), beta implicito ~37%.
Unico ETF-tradabile (ETH->XLE intraday) crolla a 10bps (0.48->0.15), t 2.38 sotto Bonferroni/416.

VERDETTO: nessun edge proprietario deployabile a basso capitale. Migliore FENOMENO da forward-monitor
= BTC->QQQ gap overnight (9/9 anni). Coerente col soffitto del progetto. Valore: aver classificato il
fenomeno (risk-beta overnight) invece di scambiarlo per alpha.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 22:32:28 +00:00
Adriano Dal Pastro d2d535cf6a research(cross-market): crypto x mercati IB -> trovata ANTICIPAZIONE weekend-crypto -> lunedi' equity
Goal: cercare correlazioni/anticipazioni crypto<->IB. Dati cache (BTC/ETH Deribit 1h->1d; ETF eq_*).

(1) Correlazione contemporanea: crypto = risk-on (BTC ~0.32 SPY/QQQ/IWM, 0.25 HYG, 0.13 GLD, ~0 TLT).
(2) Lead-lag GIORNALIERO: NIENTE (picco k=0, rumore a |k|>=1) -> nessuno anticipa l'altro al daily.
(3) EFFETTO WEEKEND (anticipazione pulita): crypto Sab+Dom (equity chiuso) anticipa il lunedi'.
    GAP lunedi' corr +0.22/0.24 (SPY/QQQ/IWM/HYG), hit 59-62%, si RAFFORZA OOS22+ (+0.30/0.36).

Validazione avversariale:
  (A) INCREMENTALE vs venerdi': beta weekend-crypto significativo (QQQ gap t=+4.7, intr +2.9; SPY
      +4.4/+2.0; IWM +4.7/+2.7), friday_eq NON signif. -> info crypto-specifica, non momentum equity.
  (B) TRADABILE (entro Mon open, esco close, net 4bps): QQQ hit 60%, Sharpe 1.46 (OOS 1.33), long-flat
      OOS 1.91 ~+9%/yr; SPY/IWM piu' deboli ma OOS positivi.

VERDETTO: prima anticipazione cross-mercato reale. Crypto = proxy 24/7 risk-sentiment; lunedi' equity
recupera la direzione del weekend. Caveat: capacita' bassa (~52 lun/anno), tattico non cornerstone;
gap catturabile via futures IB (MNQ domenica sera) da validare. Coerente su 3 ETF (no cherry-pick).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 21:55:01 +00:00
Adriano Dal Pastro 67850b0dd8 research(equities): valida combo DEPLOYABLE TP01+GTAA (le due gambe eseguibili)
Combo onesto/eseguibile a basso capitale: TP01 (Deribit, gia' armato) + GTAA vt12 (IB), senza
XS01/VRP01 STAT-MODE. Finestra 2019-2026, TP01 compoundato sui giorni di borsa.

RISULTATO: corr TP01<->GTAA +0.21; blend 50/50 Sharpe 1.48 (40/60 e risk-parity 1.52) > best solo
1.25, maxDD 14%->8%. DIVERSIFICA anche da deployable.
CAVEAT: 2022 negativo (-2.64, trend whipsaw su entrambe), anni boom gonfiano l'assoluto (recenti
~0.95) -> il dato robusto e' il +0.27 di diversificazione, non il livello. Costo deployability:
crypto-pieno+GTAA 1.81 vs 1.48 (i ~0.33 persi = XS01/VRP01 non eseguibili). Cross-venue Deribit+IB.

Migliore config rischio-aggiustata EFFETTIVAMENTE eseguibile trovata post-reset. Non risolve EUR50/g
(capitale). Prossimo: paper-trade GTAA su IB (forward-only) per validare l'esecuzione cross-venue.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 21:48:35 +00:00
Adriano Dal Pastro 437cf11199 research(equities): EQ-GTAA01 trend multi-asset + COMBO cross-mercato (diversifica il crypto)
(1) GTAA: trend difensivo long-flat su SPY/QQQ/IWM/TLT/GLD/HYG (EW sugli asset disponibili).
  GTAA lf vt12%: Sharpe 0.64 (OOS 0.89), maxDD 15% (8% sui 6-asset 2016+), corr SPY 0.64.
  Migliore sleeve equity: rischio-aggiustato > mono-SPY, DD bassissimo, diversificatore migliore.
  Difensiva (CAGR basso). Bear DD: GFC 14% vs 55%, COVID 10% vs 34%.

(2) COMBO cross-mercato: crypto (TP01+XS01+VRP01) x equity (GTAA vt12), finestra 2019-2026.
  corr crypto<->equity = +0.17 (bassissima). blend 50/50 Sharpe 1.81 > crypto solo 1.60 >
  equity 1.12; maxDD dimezzato 14%->7%. DIVERSIFICA: primo miglioramento STRUTTURALE del
  rischio-aggiustato complessivo della ricerca post-reset (diversificazione vera, non alpha).

CAVEAT: finestra crypto corta/favorevole (Sharpe assoluti ottimistici), cross-venue Deribit+IB,
XS01/VRP01 STAT-MODE -> il combo deployable reale e' ~TP01+GTAA. Non risolve EUR50/g (capitale).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 21:42:42 +00:00
Adriano Dal Pastro c9b89739c1 research(equities): EQ-TREND01 trend difensivo su SPY = edge difensivo REALE (analogo TP01)
Il cross-section e' morto (EQ-MOM01), ma il trend DIFENSIVO time-series su SPY regge — stesso
tipo di TP01 nel crypto. TSMOM multi-orizzonte / SMA-200 long-flat, causale, netto fee, OOS 2015+.

RISULTATO: Sharpe 0.54->0.62/0.65, maxDD DIMEZZATO (55%->~27%; nei bear lenti piu': GFC 19% vs
55%, dot-com 26% vs 49%, COVID 17% vs 34%). Plateau robusto (0.56-0.65), fee-robusto (0.48 a
0.10%/lato), basso turnover, eseguibile a $0.5-2k (switch mensile SPY/cash). SMA-200 = piu'
semplice E migliore.

CAVEAT: e' risk-management non alpha (CAGR -2/3pp); i tagli grossi sono in-sample (OOS 2015-26
quasi tutto toro -> ha seguito SPY a beta minore, ma COVID dimezzato). long-bonds TLT non convince.

Lezione cross-mercato confermata: il valore robusto e' ridurre il rischio (trend long-flat), non
battere il buy&hold. Prossimo: trend multi-asset/GTAA + diversifica la sleeve crypto?

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 21:37:37 +00:00
Adriano Dal Pastro 03267b8fc3 research(equities): EQ-MOM01 momentum settoriale -> NON batte SPY
Primo backtest del fronte equity. Momentum cross-sectional settoriale (9 SPDR, 1998-2026),
causale, netto fee, OOS 2015+, giudicato marginale vs SPY buy&hold (il baseline equity).

VERDETTO: nessun edge. Long-short Sharpe -0.08 (alpha cross-sectional MORTO su 27y,
decadimento post-2000 noto). Long-only ~= SPY (corr 0.85, uplift marginale ~0.00) = SPY a
beta piu' basso. Plateau stabile ~0.50 vs SPY 0.51; sugli 11 settori (2018+) peggio (0.69
vs 0.82). L'unico beneficio (maxDD 55->39%) e' del vol-target, non del momentum.

Coerente col progetto: il relative-value momentum e' morto anche in equity (come ortho wave
nel crypto). Prossimo angolo: TS-trend difensivo su SPY (analogo equity di TP01) per tagliare
il drawdown, non per battere il CAGR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 21:33:38 +00:00
Adriano Dal Pastro 1c0b5f1869 research(equities): apre il fronte azioni/ETF via IB — dati certificati + cache su disco
Branch dedicato. Disciplina v2.0.0: prima il dato certificato, poi la strategia.
IB paper (gnzsnz/ib-gateway) da' storia daily ADJUSTED_LAST (div+split) profonda.

- ib_equities_probe.py: sonda fattibilita' dati (profondita', adjusted, subscription).
- fetch_ib_equities.py: FETCH+CERTIFY universo -> data/raw/eq_<sym>_1d.parquet (ms epoch,
  namespace dedicato). RIPARTIBILE (salta i parquet gia' scritti) -> niente refetch da IB.
  Certifica: integrita', gap lunghi, sanita' ritorni, sanita' adjustment.
- eqlib.py: harness ricerca equity. Legge la CACHE su disco (lru_cache) MAI da IB; universi
  (11 settori SPDR + 9 classici 1998+ + broad), panel allineato, riusa lo scorer indurito altlib.

UNIVERSO CERTIFICATO (17, data/raw/eq_* gitignored = cache locale):
  9 settori classici dal 1998-12-22 (27.5y) + XLRE(2015)/XLC(2018) + SPY(1996,30y)/QQQ/IWM/
  GLD(2004)/HYG(2007)/TLT(2016). Tutti integri (monotoni, no dup, no spike>50%, gap-lunghi 0).
  Start comune: 9 classici 1998, 11 settori 2018.

Prossimo passo: prima ricerca = momentum cross-sectional settoriale, gauntlet onesto.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 21:29:11 +00:00
Adriano Dal Pastro 61180637eb research(funding-carry): FC01 cross-sectional su HL -> fragile, NON regge + infra IB paper
Onda "nuova ricerca mirata". Unico meccanismo non coperto dalle 2 ondate: carry da
funding (cashflow perp, delta-neutral). Scan dati: price-clock gia' FAIL (intraday),
Deribit ccxt 0 righe, Cerbero solo candele -> fonte = API pubblica Hyperliquid.

- fetch_hl_funding.py: 19 major, funding orario reale dal 2023-05, certificato
  (0 gap, cov 98-100%, ann +1.0% APT .. +21.6% NEAR). backoff anti-429.
- funding_carry_hl.py: book dollar-neutral short-alto-funding/long-basso, causale come
  XS01, vol-target 20%, fee 0.05%/lato. Giudizio: marginal_vs_tp01 indurito + overlap XS01.

VERDETTO: il premio esiste (carry >> anti) ma il book NON regge il gauntlet.
  FULL -0.12, HOLD -0.50, DILUTES vs TP01, in-sample edge <0.5, no multicut.
  Jackknife universo: FULL oscilla [-0.39,+0.30] togliendo UN asset -> FRAGILE/overfit.
  (preview a 17 asset era +0.62 ADDS: fortuna, mancavano NEAR/AAVE). corr XS01 -0.19
  (ortogonale, non re-skin). Meccanismo: carry-vs-momentum, gli alto-funding pompano.
  -> NON entra in portafoglio, fetcher NON in cron. Diario completo.

Infra IB (thread parallelo): gateway paper gnzsnz/ib-gateway (127.0.0.1:4002, READ_ONLY)
in docker-compose + ib_probe.py. Esito dati basis CME micro: backtest NON fattibile
(ContFuture back-adjusted, scaduti=1 barra). IB ok per esecuzione/forward, non ricerca.
.env.ibgw gitignored (credenziali paper), template in .env.ibgw.example.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 19:41:20 +00:00
Adriano Dal Pastro b5db59bea9 feat(dashboard): mostra il forward-monitor PREVDAY (lead ortogonale a TP01)
Nuova sezione "FORWARD-MONITOR — lead paper (non deploy)" nel dashboard, tra PAPER e LIVE:
legge data/paper_prevday/state.json e mostra i due libri (modeled €2k nominale vs real-$600
con min-order $5), ret/maxDD di entrambi, il fill-haircut, le posizioni correnti BTC/ETH e
i giorni/flip forward. Nota esplicita: LEAD in osservazione, NON deployato.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 19:27:27 +00:00
Adriano Dal Pastro 5cce7acfe1 live(monitor): prevday-breakout in FORWARD-MONITOR (paper, non deploy)
Il lead ortogonale a TP01 sopravvissuto all'onda intraday entra in forward-monitor (stesso
trattamento di XS01 STAT-MODE / STA05), NON in esecuzione reale.

- src/strategies/prevday_breakout.py: segnale CONGELATO (params fissi anchor=1, k=0.30, simmetrico,
  vol-target 0.20/30/2.0), self-contained. Bit-identico all'agent di ricerca (max diff 0.0):
  BTC full Sh 1.18/hold 0.92, ETH 1.09/1.42; marginal ADDS, earns_slot, corr_hold -0.01, non-hedge.
- scripts/live/paper_prevday.py: forward-only paper, traccia DUE libri — MODELED ($2000 continuo)
  e REAL-$600 (salta i ribilanciamenti < min-order $5) -> il gap = haircut di fill reale che lo
  scettico aveva segnalato. Inizializzato forward-only da oggi.
- cron_daily.sh: avanza il monitor ogni giorno.
- test: param congelati + causale + bounded + long-short. Suite intera verde.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 15:37:41 +00:00
Adriano Dal Pastro d5dd6f4b72 harness(causality): guardia look-ahead + calendar-artifact self-policing nel lab intraday
- altlib.causality_ok(target_fn, tf): online-consistency guard (ricalcola il target su un
  prefisso, la coda deve combaciare col full). eval_weights shifta la posizione ma non vede
  una feature non-causale (finestra centrata/shift(-k)/stat full-sample) -> questa sì.
- intra_score integra DUE gate prima/dopo lo scoring: causality (leak -> LEAK, squalificato)
  e day_boundary_robust (ARTIFACT-RISK -> fuori dagli slot). Effetto sul leaderboard intraday:
  open_drive + weekly_seasonality + overnight -> CAL-ARTIFACT (da soli, niente skeptic);
  prevday_range_breakout resta (ROBUST). earns_slot 10 -> 8.
- +2 test (causal-ok / leak), suite intera verde.

Il lab intraday ora auto-becca leak e artefatti-calendario che ieri richiedevano 3 scettici.
Chiude la 3a lezione harness dell'onda intraday.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 15:22:58 +00:00
Adriano Dal Pastro 4ae3b42442 harness(realism): codifica le 2 lezioni dell'onda intraday (day-boundary + small-cap fills)
Due gate nuovi in altlib.py (test tests/test_harness_realism.py, suite intera verde):

1. 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 (uplift si inverte = etichettatura). Riproduce da solo il verdetto
   degli scettici: open_drive +0.23@00:00 -> -0.33@+8h = ARTIFACT-RISK; prevday_breakout = ROBUST.
   Decoupling chiave: il segnale vede il clock shiftato, il backtest usa il calendario reale.

2. eval_weights_smallcap(df, target, capital=600, min_order=5): salta i ribilanciamenti di
   nozionale < min_order (la finzione del micro-trading sub-dollaro che eval_weights costa come
   fee proporzionale su un overlay vol-target), riporta lo Sharpe haircut reale vs modellato.
   Vale per ogni sleeve a $600, TP01 incluso.

CLAUDE.md aggiornato (sezione HARNESS REALISM). La pipeline di falsificazione ora becca da sola
artefatti-calendario e finzioni-fee, oltre a hedge/regime-luck/leakage gia' codificati.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 14:44:20 +00:00
Adriano Dal Pastro 24565974c0 research(intraday): asse intraday/microstruttura — lead più vicino al reale ma NON deployabile
16 agenti su segnali low-turnover intraday (sessione/funding, reversione post-evento, breakout
range del giorno prima) su feed certificati 1h/15m, giudice = marginal scorer indurito + fee-sweep.
Lab: intra_score.py (wrappa study_marginal a TF scelto + turnover/fee), meta_intra.py (corr-TP01 +
per-cut), verify_intra.py (walk-forward + in-sample-null + drop-one + fee-stress).

Esito: 10/16 "earns_slot" -> 5 genuinamente ortogonali (corr<0.4). Combo dei 5: Sharpe 1.80, corr
0.17, leak-free, passa walk-forward (+0.30/+0.37 dove l'ortho dava -0.07), pre-2025 uplift +0.28,
drop-one e fee-robusto. Sembrava IL lead.

3 scettici: (1) open_drive = ARTEFATTO etichettatura UTC (shift confine 4h -> uplift negativo);
prevday_range_breakout REGGE (unico onesto, eseguibile). (2) combo fallisce il null a corr-zero
(20-24° pctl: aggiunge meno del rumore), è HEDGE (corr -0.57..-0.80 a Sharpe-TP01) + tail-luck
(80% PnL in top-5 giorni delle gambe revert). (3) robust-plateau ma null-pctl 0.20 = diversificazione
di stream ortogonale, non timing-alpha; + finzione fee micro-ribilanciamento a $600.

Verdetto: niente in live, resta solo TP01. Lead forward-monitor: prevday_range_breakout. Lezioni
harness da codificare: test shift-confine-giorno (artefatti calendar), fee discretizzata a piccolo
capitale, causality guard nel lab intraday. Diario 2026-06-21-intraday-microstructure.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 14:20:19 +00:00
Adriano Dal Pastro 62d3b23cc6 harness(marginal): indurisci marginal_vs_tp01 con la lezione dell'onda ortho (17/18 -> 1)
Lo scorer 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
altlib.marginal_vs_tp01:
  1. persistenza multi-cut (uplift a più date di taglio, non solo 2025) -> robust_oos
  2. has_insample_edge: Sharpe standalone PRE-holdout >= 0.5 (la basket faceva 0.29).
     null_pctl_* (vs asset-rumore corr-zero) restano come CONTESTO (diversification math).
  3. is_hedge: low-corr che paga solo quando TP01 è debole = hedge, non alpha.
Verdetti nuovi HEDGE/NOISE; earns_slot = ADDS + robust_oos + has_insample_edge + not hedge.

Effetto: sull'onda ortho 17/18 "ADDS" -> 1 (dvol_spread, unico con edge in-sample reale 0.57);
gli altri 16 -> NOISE/HEDGE. Un sleeve sintetico Sharpe~1.3 scorrelato resta ADDS (non rigetta
i diversificatori veri). +5 test (noise/hedge/single-regime/high-Sharpe-uncorr/in-sample-edge);
suite 37 passed. CLAUDE.md aggiornato.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 12:50:26 +00:00
Adriano Dal Pastro 0adc69a357 research(ortho): caccia all'ortogonale a TP01 — relative-value BTC/ETH reale ma NON deployabile (hedge mono-regime)
18 agenti su book market-neutral a 2 gambe BTC/ETH (eseguibili a $600, a differenza di XS01),
giudicati sul MARGINALE vs TP01 (altlib.marginal_vs_tp01), non sullo Sharpe assoluto.

Lab: ortholib.py (eval_book leak-free a 2 gambe + causalità + eseguibilità@600), ortho_score.py
(giudice), meta_ortho.py (corr mutua + persistenza multi-cut), sleeve_rv.py (curated, SELECTION-
BIASED, non deployare).

Esito: 17/18 "ADDS" -> gonfiato dall'hold-out corto fisso-2025 (finestra ETH-bleed dove TP01 è
debole). Diagnosi orchestratore: collassano a 8 bet (corr 0.43); persistenza multi-cut e selezione
walk-forward smascherano i 2025-only (kalman/xs2). Scettico indipendente: basket selection-free ha
uplift pre-2025 +0.027 = 49° percentile di asset-rumore corr-zero (matematica di diversificazione,
non segnale); corr(Sharpe-TP01, uplift) -0.87 (è un HEDGE dei drawdown di TP01); muore a 0.30% RT.

Verdetto: NIENTE in live. Resta solo TP01. Lezione: lo scorer marginale va indurito (multi-cut +
null-asset-rumore + distinguere hedge da alpha). Diario 2026-06-21-ortho-tp01-relative-value.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 12:35:48 +00:00
Adriano Dal Pastro 1afb1014c9 research(blind): 52 agenti ciechi su curve anonime BTC/ETH — orchestratore valuta PnL/maxDD, niente di nuovo regge
Flotta di 52 subagenti "esperti di segnali" su storico BTC/ETH ANONIMIZZATO (Series A/B
rebased a 100, calendario sintetico, split 70/30) — non sanno cosa siano. Ognuno scrive un
signal(df)->position causale (script o ML), tunato solo sul train. Orchestratore valuta su
PnL e maxDD nel test held-out.

Harness cieco leak-free (riusabile):
- make_blind.py: export anonimo + overlay; blindlib.py: evaluator con shift della posizione +
  GUARDIA DI CAUSALITA' online (squalifica ogni look-ahead, ML incluso); blind_eval.py CLI;
  score_all.py giudice OOS; verify_top.py (corr-al-trend, fee-stress, jackknife).
- 52/52 passano la guardia (zero leak su tutta la flotta).

Esito OOS (benchmark buy&hold: -7% PnL, 68% DD):
- top = macd (+21%, DD 11%, Sh 0.84), accel, vol_of_vol, regime_switch, rf, obv — tutti
  trend/vol-regime. Sharpe OOS ~0.84 decade dal train ~1.4. Mean-rev e ML in fondo.
- 3 scettici indipendenti: REFUTED. regime-luck (top-5 bar = 67-102% del PnL); trend-redundancy
  (HAC alpha t=+0.9..+1.5, nessuno >1.96 — TSMOM travestito); overfit (accel/vov knife-edge).

Verdetto: ri-conferma CIECA e indipendente del soffitto direzionale ~1.3. macd = classe-TP01,
forward-monitor non deploy. Diario 2026-06-21-blind-signal-fleet.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 07:05:04 +00:00
Adriano Dal Pastro f5d30d88b9 docs(claude): aggiorna l'header allo stato LIVE armato di TP01 + capitale reale ~$600
L'header v2.0.0 RESET diceva ancora 'esecuzione DISABILITATA / nessun trading live'
(stato del 2026-06-19), superato dall'arming del 2026-06-20: TP01 e' ARMATO/LIVE su
Deribit mainnet (config/live.json execution_enabled=true + cron live_execute.py --execute),
cap $300/asset, disaster-SL -30%, alert Telegram, capitale reale ~$600. Stato corrente
flat (target risk-off). Solo TP01 eseguito; XS01/VRP01 restano paper/STAT-MODE.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 21:43:17 +00:00
Adriano Dal Pastro 9612560479 research(xsec): sweep cross-sectional su Hyperliquid (43 script/257 config) + verifica avversariale
Nuova harness condivisa xslib.py (panel HL certificato, score per-asset causale, book
long-k/short-k vol-targeted leak-free) + 43 script in runs/ su 11 famiglie (MOM/REV/VOL/
DIST/LIQ/VAL/STRUCT/UNIV). Scoring = earns_slot (full>0 AND hold-out>0 AND marginal ADDS
al portafoglio live AND corr XS01<0.6, con jackknife drop-one-month).

Find: 42/257 config earns_slot=True, ma TUTTE con corr TP01 -0.2..-0.4 e PnL ~solo 2025.
Verify (verify_survivors.py, 3 scettici deterministici):
 - S1 redundancy: cluster low-vol = UNA scommessa (XV01=XU02=1.00, XV02/XV03 r 0.44-0.67);
   XM09/XL02/XS06b/XR02 distinti (corr media off-diag +0.20).
 - S2 short-beta: cluster low-vol carica 0.44-0.70 su short-market -> NON market-neutral,
   e' un tilt short-alt-beta di regime. XM09(0.08)/XR02(-0.21) NON short-beta.
 - S3 per-anno: cluster low-vol decade (XV01/XU02 2026 -0.09); XL02 morto (2025 -0.14,
   2026 -0.43); XM09 (0.82/0.50/0.74) e XR02 (0.84/0.40/2.68) positivi in tutti e 3 gli anni.

Esito: nessuna sleeve nuova. Cluster low-vol RIGETTATO (regime-bet), XL02 RIGETTATO (overfit).
2 LEAD genuini (XM09 trend-gated x-sec momentum, XR02 reversal vol-gated) -> forward-monitor,
non deployabili (panel 2.5y regime unico + STAT-MODE esecuzione). Portafoglio live invariato.

Incluso anche options_vrp_managed.py (A/B VRP01 hold-to-expiry vs gestione attiva del doc
credit-spread): la gestione attiva DISTRUGGE l'edge (combo FULL managed Sh -1.29 vs HtE +0.96,
il delta-exit taglia i vincenti) -> scartata, VRP01 resta hold-to-expiry.

Diari: 2026-06-20-xsec-strategies-sweep.md, 2026-06-20-vrp-active-management.md.
gitignore: data/paper_portfolio/ (stato runtime paper) + scripts/research/xsec/runs/out/ (output rigenerabile).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 21:36:57 +00:00
Adriano Dal Pastro 5ac4e16af8 research(alt): sweep 104 strategie alternative su Deribit (153 agenti) + marginal scorer
Ondata di ricerca onesta a largo spettro su BTC/ETH+DVOL certificati: 104 ipotesi
distinte (11 famiglie), un agente-finder per ipotesi, verifica avversariale a 3
scettici sui promettenti, sintesi (153 agenti totali). Esito: NIENTE di nuovo regge
-> conferma del soffitto strutturale ~1.3 BTC/ETH-direzionale; lo stack
TP01+XS01+VRP01 resta imbattuto.

- altlib.py: harness condiviso vettoriale leak-free (eval_weights/study_weights,
  fee-sweep, both-asset + hold-out 2025+). Riproduce i numeri canonici di TP01.
- MARGINAL SCORER (study_marginal/marginal_vs_tp01): Sharpe INCREMENTALE vs baseline
  TP01 (corr, blend uplift OOS, alpha residua) + jackknife OOS (clean-year +
  drop-best-month). earns_slot = abs!=FAIL & ADDS & robust_oos. Smaschera gli overlay
  su TSMOM con PASS assoluti fasulli (CMB04, VOL11, ...) e il falso positivo KAMA
  (ADDS ma muore al jackknife).
- runs/*.py (104) script riproducibili per ipotesi; wf_altstrat.js workflow.
- Verdetto: 0 candidati deployabili; 2 LEAD fragili (VOL08, STA05_LS) da forward-monitor.
- test_marginal_scorer.py blocca baseline + invarianti. Suite: 32 verde.

Diario: docs/diary/2026-06-20-alt-strategies-100agent-sweep.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 19:50:39 +00:00
Adriano Dal Pastro bf84bc91e2 feat(live): alert Telegram su esecuzione ed errori
src/live/notifier.py (stdlib, no-op se non configurato): legge TELEGRAM_BOT_TOKEN/CHAT_ID da env o
.env(.mainnet) gitignored. live_execute.py invia alert su: ordine eseguito (), ordine non
verificato (⚠️), disaster-SL piazzato/fallito (🛡️/⚠️), conto offline, e qualsiasi eccezione (🛑).
Nessun alert nei giorni flat/HOLD (no rumore). Config gia' presente in .env -> alert attivi.

Test config: uv run python -m src.live.notifier "msg". Test 28/28.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 16:09:09 +00:00
Adriano Dal Pastro 3cba5bb9d0 feat(dashboard): mostra i disaster-SL attivi nella sezione LIVE
Lo shadow espone i bracket disaster-SL aperti (open_orders filtrati per label DISASTER_LABEL,
centralizzata in deribit.py): asset, stop price, size. La sezione LIVE li mostra
("disaster-SL attivi (-30%): ..." o "nessuno (flat)"). Test 28/28.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 16:05:18 +00:00
Adriano Dal Pastro e5e2d3ec9b feat(live): disaster-SL on-book con lifecycle completo (idempotente) nel loop di esecuzione
ensure_disaster_sl(): garantisce UN solo STOP_MARKET reduce_only a ~-30% coerente con la posizione,
ad ogni run del loop, per asset:
- flat  -> cancella i bracket orfani;
- long  -> assicura lo stop (size = posizione, prezzo al tick);
- gia' coerente (1 bracket, amount~=, stop entro 5%) -> lascia com'e' (niente churn ne' gap di
  protezione fra cancel e place).

- deribit.py: open_orders (merge type all+trigger_all), disaster_stop_price.
- execution.py: cancel_order + ensure_disaster_sl.
- live_execute.py: gestione bracket ogni run, gated come l'esecuzione. Validato armato: flat ->
  disaster-SL 'flat' (cleanup), zero ordini. Test 28/28.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 15:47:30 +00:00
Adriano Dal Pastro 4650aa71a2 feat(live): ARMA l'esecuzione di TP01 (execution_enabled=true) + cabla al cron giornaliero
execution_enabled=true: con --execute il loop invia ordini REALI. Aggiunto al cron_daily.sh (00:30
UTC, dopo il refresh dati) lo step live_execute.py --execute. Validato armato: TP01 flat -> HOLD,
zero ordini. Da qui TP01 opera da solo sul conto reale al prossimo ENTRY del segnale.

NB: il loop NON piazza ancora il disaster-SL on-book (metodo presente, lifecycle bracket da cablare
prima del primo ENTRY). Rischio posizione comunque limitato dal cap $300/asset (~1x, no leva).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 15:41:03 +00:00
Adriano Dal Pastro bc9e322d0d feat(live): loop di esecuzione GATED di TP01 (execution_enabled + --execute, default OFF)
scripts/live/live_execute.py porta il conto reale al target di TP01 (min(0.5*frazione*equity,
cap/asset)): apre/riduce/chiude via DeribitTrader.rebalance_to(). DOPPIO GATE: config/live.json
execution_enabled=true (master, default false) E flag --execute; senza entrambi e' dry-run.
Reconciliation post-ordine + log in data/live/executions.jsonl. TP01 flat -> 0 azioni.

- execution.py: rebalance_to() (open/reduce/close al target); MAX_AMOUNT alzato a tetto hard
  anti-fat-finger (~$630/$430 su conto ~$600), il sizing operativo lo decide config max_notional.
- config/live.json: master switch + cap/asset $300 + min ordine $5 + disaster_sl_pct.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 15:37:51 +00:00
Adriano Dal Pastro a3d6b97db6 fix(dashboard): sezioni PAPER/LIVE evidenti (barra colorata) + Cache-Control no-cache
Le sezioni erano testo grigio poco visibile e il browser cacheava la pagina ('non vedo differenza').
Ora: header PAPER con barra verde, LIVE con barra rossa + sfondo rosso-tenue (separazione netta);
risposta HTTP con Cache-Control no-cache/no-store -> niente pagina stantia.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 15:36:21 +00:00
Adriano Dal Pastro cddea50c5a feat(live): conto USDC -> strumenti lineari; entrata/uscita da Old; dashboard LIVE separato da PAPER
Correzione post-micro-test (il conto e' USDC, non BTC/ETH):
- deribit.py: INSTRUMENT -> BTC/ETH_USDC-PERPETUAL (lineari, gli unici eseguibili sul conto USDC);
  notional_to_amount gestisce i lineari (amount in base-coin = notional/price); + quantize_price;
  trade_history (read-only) per i trade reali. build_rebalance_order passa il prezzo.
- shadow.py: sizing col prezzo; espone live_trades (trade reali eseguiti su Deribit).

Entrata/uscita verificate (logica presa da Old/src/live/execution.py):
- execution.py: open() market verificato (state=='filled' + trade, fill/fee reali, filled_amount
  autorevole), close() market reduce_only (le CHIUSURE si tentano SEMPRE, senza cap), disaster-SL
  STOP_MARKET reduce_only. Cap di size SOLO sulle aperture. Fill dataclass.
- microtest.py: usa open()/close(); safe-close se l'apertura non e' verificata.

Dashboard: sezione PAPER (backtest+forward) separata da sezione LIVE (conto reale Deribit: shadow
TP01 + Trades REALI eseguiti). Test 27/27.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 15:15:45 +00:00
Adriano Dal Pastro c00f6016df feat(live): micro-test esecuzione REALE su Deribit mainnet (USDC linear) — round-trip validato
Primo ordine reale post-reset, a rischio ~0 ($6 notional, leva 0.011x). Scoperto che il conto e'
USDC -> strumento eseguibile = perp LINEARE BTC_USDC-PERPETUAL (l'inverse BTC-PERPETUAL fallisce
'not_enough_funds'). Round-trip BUY/SELL reduce_only verificato: fill reali, fee reali (0.0064 USDC),
posizione tornata a FLAT, costo totale $0.0071.

- src/live/execution.py  : DeribitTrader (estende DeribitRead) con market order + verifica posizione,
  GUARDRAIL hard (solo BTC_USDC-PERPETUAL, amount <= 0.0002 BTC). Niente leva per-ordine (Deribit non
  la accetta: l'esposizione la decide la SIZE).
- scripts/live/microtest.py : runner round-trip, default DRY-RUN, --live per inviare. Pre-flight ABORT
  se posizione preesistente; chiusura reduce_only; verifica ritorno a FLAT.
- src/live/deribit.py    : aggiunti spec contratto LINEARI USDC (BTC/ETH_USDC-PERPETUAL).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 15:06:53 +00:00
Adriano Dal Pastro 715f197cf2 feat(dashboard): lista trades TP01 (entry/exit dal segnale causale)
Nuova sezione "Trades TP01" nella dashboard: eventi ENTRY long / EXIT flat dedotti da
target_series sui dati certificati (data, asset, transizione di posizione, prezzo). In
src/live/shadow.tp01_trades(): account-independent (gira anche offline nel container),
ricalcolata a ogni render -> storico + forward. Empty-state se TP01 non ha mai mosso.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 14:18:46 +00:00
Adriano Dal Pastro bec2fb2089 chore(dashboard): monta .env.mainnet read-only nel container per lo "Shadow live"
Il token mainnet (sola lettura) abilita conto/posizioni REALI nel box Shadow della dashboard.
Montato a runtime, NON nell'immagine (.env.mainnet resta dockerignored). Solo letture: nessun
endpoint di trading e' raggiungibile da src/live/deribit.py. Verificato: conto reale $598.07 letto
dal container, TP01 flat -> 0 ordini.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 14:02:26 +00:00
Adriano Dal Pastro 9c48cdd884 feat(live): SHADOW MODE TP01 su Deribit mainnet (sola lettura) + dashboard 3-way
Validazione esecuzione di TP01 a RISCHIO ZERO: gira il loop live contro dati/conto/posizioni REALI
del mainnet, costruisce gli ordini di ribilancio esatti e li STAMPA invece di inviarli. Niente
testnet (e' la causa del reset v2.0.0: feed farlocco) -> shadow su mainnet reale + micro-test a
size minima come unica via per il fill (passo successivo).

- src/live/deribit.py  : client Deribit mainnet SOLA LETTURA (ticker/conto/posizioni via Cerbero MCP)
  + costruttore ordini deterministico (notional->contratti, step BTC $10/ETH $1, quantizzazione,
  delta vs posizione). Nessun metodo di trading, by design.
- src/live/shadow.py   : shadow_report() condiviso CLI+dashboard (niente drift); degrada con grazia
  se il mainnet non risponde.
- scripts/live/live_trend.py : CLI shadow (--no-net offline, --equity override). Verificato su
  mainnet reale: conto $598.07, posizioni flat, TP01 flat -> 0 ordini, parita' col paper OK.
- src/live/dashboard.py : box "Shadow live" + titolo/note al 3-way (TP01+XS01+VRP01).
- tests/test_live_shadow.py : 9 test deterministici (quantizzazione, sizing 50/50, entry/exit/None,
  parita' live==backtest). Suite 26/26.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 14:01:12 +00:00
Adriano Dal Pastro 9ed2ea4b13 fix(fetch_hyperliquid): taglia il backfill sintetico di cerbero MCP (vol=0) — universo 52->51
Cerbero MCP padda il periodo pre-quotazione su HL con barre SINTETICHE (volume 0, prezzi
copiati da Binance -> matchano cross-venue e non sono flat): asset listati dopo lo START
(es. AXS 83%, ALGO/SAND 37%) passavano i gate flat+cross-venue ed erano certificati PULITO
pur non essendo negoziabili. E' il caso v2.0.0 (edge su un book che non c'era).

Fix: il VOLUME e' il rivelatore del backfill -> (1) taglio del run iniziale a volume 0
(serie nativa), (2) gate storia nativa >=365g reali (AXS scartato), (3) gate vol=0 interno,
(4) cross-venue/flat ricalcolati solo sulle barre reali, (5) parquet scartati rimossi.

Verificato direttamente su cerbero MCP mainnet. I 19 major di XS01 hanno 0 backfill ->
strategia live invariata. Diario 2026-06-20-cerbero-backfill-fix.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 11:53:55 +00:00
Adriano c6236ed5d9 feat: integra VRP01 come sleeve del portafoglio (put credit spread + gate IV-rank)
src/portfolio/sleeves.py: _vrp_combo_returns + vrp_sleeve, self-contained in src/
(pricing BS + gate causali inline, DVOL da data/raw). Settimanale->giornaliero col
lump sul giorno di scadenza (preserva lo Sharpe annualizzato, peso costante).

Registry: TP01 0.55 / XS01 0.25 / VRP01 0.20 (TP01 resta maggioranza; VRP e' un
lead modellato, non deploy pieno). TP01+VRP01 monotono: FULL 1.30->1.44, HOLD
0.31->0.40 a peso 20%. Scorrelato a TP01 (+0.01).

Test tests/test_vrp_sleeve.py (5 pass). CLAUDE.md + diario aggiornati.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 11:24:40 +02:00
Adriano 75e1aacd19 research: analisi strategie FinanceOld + VRP v2 (defined-risk spread + gate IV-rank)
Analisi 4 progetti FinanceOld. Solo il filone opzioni-VRP backtestabile sui dati
certificati (funding-arb senza dati storici; Polybot ticks corrotti/3gg/edge=latenza).

VRP v2 porta 3 idee di OptionsAgent nel framework, causale + fee-aware:
- put credit spread (rischio definito): worst-week -16.6%->-7.4%, DD 33%->21%
- gate IV-rank>0.30: ribalta HOLD-OUT da -0.25 a +0.28 (alpha = filtro regime)
- COMBO f=1.0: FULL Sh 1.10, HOLD 0.60, DD 12%, positiva/piatta ogni anno
- blend TP01 70/30 -> Sh 1.00, DD 7% (corr +0.07)

Lead quantificato, non deploy (premio modellato ATM, serve f di stress reale).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 11:16:59 +02:00
Adriano Dal Pastro 92a63feb9c chore(monitor): cron giornaliero (refresh dati + avanza paper) + cleanup crontab/orfano
scripts/cron_daily.sh: rebuild_history BTC/ETH + fetch_hyperliquid (52 alt) + fetch_dvol +
paper_portfolio, ogni giorno 00:30 UTC -> tiene fresco il dato che il dashboard legge e avanza
il paper forward. fetch_hyperliquid END ora DINAMICO (oggi) per il refresh.

Cleanup: rimosso container orfano pythagoras-portfolio (vecchio runner pre-reset, exited);
crontab ripulito dai 4 job rotti del micro-test mainnet (hourly_report/drift/reconcile/
ledger_vs_backtest -> script archiviati in Old/), backup in logs/crontab.pre-reset.bak.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 09:06:38 +00:00
Adriano Dal Pastro 26e977d338 feat(monitor): dashboard PAPER del portafoglio attivo (TP01+XS01) + paper forward loop
src/live/dashboard.py: web UI stdlib (:8787) che mostra metriche (FULL/HOLD Sharpe, DD, CAGR),
per-sleeve, posizioni correnti, equity (backtest + paper forward), ultimo dato. Solo MONITOR,
esecuzione REALE disabilitata. scripts/live/paper_portfolio.py: forward-only del portafoglio
(StrategyPortfolio su active_sleeves), stato persistente in data/paper_portfolio (gitignored).

Dockerfile + docker-compose.yml minimali (solo servizio dashboard; runner/esecuzione restano in
Old/). Container pythagoras-dashboard ricostruito col codice nuovo (il vecchio mostrava dati
pre-reset). Mount data/ read-only. .dockerignore esclude Old/data/.venv/.git/.env.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 08:57:55 +00:00
Adriano Dal Pastro 0d9f483131 docs: aggiorna CLAUDE.md allo stato corrente (XS01 blend+gate, portafoglio FULL/HOLD 1.55)
XS01 ridescritta con affinamenti (blend lookback [30,90] + gate dispersione p30, standalone FULL
1.50). Portafoglio attivo TP01 70% + XS01 30% -> FULL Sh 1.55 / HOLD 1.55 / DD 4.4%. Aggiunte le
lezioni: espansione universo Hyperliquid NON aiuta XS (52/top-liq/trend-multiasset tutti peggiori,
i margini sono nel segnale); lead opzioni VRP quantificato (f reale ~1.0, non deploy). Struttura/
comandi aggiornati (scripts/research track A-I + options_vrp + fetch_dvol; scripts/portfolio xsec_*).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 22:29:24 +00:00
Adriano Dal Pastro 87dd56a9ce feat(XS01): + gate di dispersione (p30) — portafoglio FULL 1.48->1.55, HOLD 1.06->1.55
Momentum cross-sectional vive nella dispersione; gate: entra solo se la dispersione cross-section
del momentum supera il percentile ESPANDENTE causale (altrimenti flat). Plateau robusto p15-p35
(non knife-edge: il crollo a p40+ e' over-gating); scelto p30. XS01 standalone FULL 1.10->1.50,
HOLD 1.03->1.71, DD 14%->10.8%. Portafoglio TP01 70+XS 30: FULL 1.48->1.55, HOLD 1.06->1.55, DD
4.6%->4.4%. Il gate alza SIA FULL SIA hold-out (tiene XS attivo nei regimi dispersi, flat nei bull
compatti; causale). E' il concetto del vecchio XS01.

sleeves.XS_CFG disp_pct=30; engine _xsec_returns gatea su dispersione. 12 test ok.
Diario 2026-06-19-xsec-dispgate.md. Affinamenti del segnale (blend+gate) > espansione universo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 22:24:04 +00:00
Adriano Dal Pastro fd5a0bd3cf feat(XS01): affina con blend di lookback [30,90] — FULL 0.80->1.10, portafoglio 1.41->1.48
Come TP01 fonde gli orizzonti, XS01 ora fonde 30g+90g del momentum cross-sectional (z-score per
lookback, mediato). Sweep: [30,90] e' il sweet spot (fonde i due singoli robusti, anti-overfit):
XS01 standalone FULL 0.80->1.10, DD 21%->14%, corr a TP01 -0.06->-0.12, 100% anni+. Portafoglio
TP01 70 + XS01 30: FULL Sh 1.41->1.48, DD 5.2%->4.6%, ~€/g 1.65->1.78; hold-out 1.15->1.06 (calo
marginale dentro il rumore). Piu' robusto (due orizzonti) + diversifica meglio -> promosso.

sleeves.XS_CFG lookbacks=(30,90), engine _xsec_returns usa lo score blended. 12 test ok.
Diario 2026-06-19-xsec-blend.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 22:19:12 +00:00
Adriano Dal Pastro bf6ade51af research: strato trend multi-asset (52 alt) RIDONDANTE col trend TP01 -> non aggiunto
TSMOM CANONICAL applicato a ogni alt dei 52, equal-weight. Standalone FULL 0.66 ma HOLD-OUT -1.03
(long negli alt nel calo 2025-26), corr a TP01 +0.74 (stessa beta direzionale). Contributo al
portafoglio NEGATIVO (HOLD -0.16/-0.27). Broadenizzare il TREND non diversifica: e' la stessa
direzionalita' su asset piu' rumorosi. Solo il market-neutral (XS01) diversifica davvero.

Chiude il filone espansione-universo (XS-52, top-liquidita' dinamico, trend-52: tutti peggiori).
Configurazione validata invariata: TP01 70% + XS01 (19 major) 30%, FULL Sh 1.41 / HOLD 1.15.
I margini reali sono in un MECCANISMO diverso (opzioni VRP), non nell'universo crypto-direzionale.
Diario 2026-06-19-trend-multiasset.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 21:17:16 +00:00
Adriano Dal Pastro 182d4eeac2 research: universo top-liquidità DINAMICO per XS — anch'esso peggiore del fisso-19 (memecoin diluiscono)
xsec_dynuniverse.py: a ogni ribilancio top-N per dollar-volume 30g causale (ragged-aware), poi XS
momentum. Esito: best dinamico top12 FULL 0.65/OOS0.54 (un anno neg) vs fisso-19 FULL 0.80/OOS1.20
(100% anni+). Contributo TP01+DYN 1.10/0.60 vs TP01+XS19 1.25/1.15. La classifica per volume ammette
i MEMECOIN ad alto volume (WIF/ORDI/JUP) erratici -> diluiscono. Liquidità != qualità.

Conclusione: ne' 52-all ne' top-liquidità dinamico battono i 19 major curati. XS01 resta sui 19.
Portafoglio invariato TP01 70% + XS01 30% (FULL 1.41 / HOLD 1.15). 12 test ok. I 52 parquet restano
per ricerca futura. Diario 2026-06-19-xsec-universe-expansion.md aggiornato.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 21:09:48 +00:00
Adriano Dal Pastro 8426d05f12 research: espandere universo XS01 a 52 asset DILUISCE (negativo) -> XS01 blindato sui 19 major
Esteso fetch_hyperliquid a 52 alt certificati (cross-venue vs Binance, flat 0%, 2024+; +gate
delistato per MKR/FXS). Ma il cross-sectional momentum sui 52 e' NEGATIVO (FULL -0.1..-0.6, k grande
non aiuta) vs +0.67/OOS0.91 sui 19 major (stessa finestra): i ~33 small-cap (WIF/JUP/ORDI/PYTH/TAO..)
sono idiosincratici/mean-reverting e rovesciano il momentum relativo. "Piu' asset = piu' robusto"
e' FALSO per l'XS momentum: la breadth utile e' quella dei major liquidi.

Fix: lo sleeve _xsec_returns usa XS_UNIVERSE esplicito (19 major), non glob-all (aggiungere parquet
certificati non lo rompe piu'). I 52 parquet restano su disco per ricerca futura, non per XS01.
Portafoglio ripristinato e invariato: TP01 70% + XS01 30%, FULL Sh 1.41 / HOLD 1.15. 12 test ok.
Diario 2026-06-19-xsec-universe-expansion.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 21:00:00 +00:00
Adriano Dal Pastro 53d0134cb1 research: calibra VRP su quote REALI cerbero-bite — f≈1.0 (non 1.29), lead DEBOLE confermato non-deploy
cerbero-bite GIA' accumula la catena reale mainnet (option_chain_snapshots, 2026-05->oggi) -> uso
quella (niente nuovo snapshotter). options_vrp_calibrate.py misura il fattore f reale su 223
snapshot/asset (put weekly delta-0.28, BID): BTC f median 1.03, ETH 0.97, skew reale +1.5..1.9 pt.
Il f reale e' ~1.0 NON 1.29 (lo snapshot singolo del branch era outlier ad alto skew). -> VRP sleeve
= punto f≈1.0 = Sharpe ~0.71 (conservativo), DD 33%, hold-out piatto: diversificatore DEBOLE (corr
+0.07) sotto TP01, coda severa. Calibrazione su ~10g densi, 1 regime calmo; f di stress non misurato.

Verdetto: la decorrelazione modesta NON giustifica il rischio di coda short-vol senza dato reale
multi-regime (serve che cerbero-bite copra un crash). Confermato NON-deploy. Portafoglio invariato
TP01 70% + XS01 30%. Diario 2026-06-19-options-vrp-lab.md aggiornato.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 20:38:54 +00:00
Adriano Dal Pastro 8f9ce89039 research: imposta sleeve OPZIONI VRP — infrastruttura + prima validazione (LEAD reale, non deploy)
fetch_dvol.py: storia DVOL (IV Deribit) BTC/ETH 2021-2026 -> data/raw/dvol_*. options_vrp_lab.py:
backtest CSP settimanale, premio BS su DVOL reale + calibrazione f (skew/spread), payoff sul path
realizzato, causale; gauntlet (VRP, sweep f/delta, per-anno, worst-weeks, corr+contributo vs TP01).

Esiti (book 50/50 put delta-0.28): VRP reale (BTC IV>RV 78% del tempo). Sharpe DIPENDE da f:
0.71 conservativo (IV-ATM) -> 1.70 a f=1.29 (skew reale calm). CODA severa (DD 30-33%, settimane
-15..-26% su LUNA/FTX/crash; 2022 -9%, 2026-YTD -14%). Scorrelato a TP01 (+0.07) -> migliora il
portafoglio anche a premio conservativo (TP01 70%+OPT 30%: Sh settimanale 0.71->0.97).

VERDETTO: lead reale e diversificante, MA premio modellato (non catena reale) + calibrazione
ottimistica + coda short-vol non catturata nello stress. Regola: mai short-vol da modello in
deploy. NON aggiunto. Portafoglio invariato TP01 70% + XS01 30%. Prossimo: accumulo quote reali
multi-regime + stress crash + daily-MTM + paper testnet. Diario 2026-06-19-options-vrp-lab.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 20:30:08 +00:00
Adriano Dal Pastro 87af03955c research: porta artefatti da strategy-research-calendar (tracks F-I + eval crypto_backtest + lead OPZIONI/VRP)
Dal branch parallelo strategy-research-calendar (continuazione della linea TP01). Porta su main il
record di ricerca + la fondazione del lead opzioni (NIENTE blob dati, niente codice in conflitto):
- Tracks F/G/H/I (seasonality/calendar, prior-levels, volume-vol, momentum-reversal): tutti
  NEGATIVI/spurii -> confermano il soffitto Sharpe ~1.3 su BTC/ETH direzionale (calendar = buy&hold
  travestito; mean-reversion morta anche a fee 0). Diari + script.
- trackD_lookahead_audit.py: audit anti-look-ahead (stesso esito del nostro fix >=12h).
- eval-crypto-backtest-options.md: valutazione strategia esterna crypto_backtest. Cross-valida TP01
  (il loro sleeve spot 12h ~ TP01: due ricerche indipendenti, stessa conclusione). Identifica il
  LEAD: sleeve income OPZIONI (vendita put settimanali delta-0.28, VRP IV>RV), scorrelato ~0.22 al
  trend -> via per superare il soffitto ~1.3.
- options_real_quote_check.py + cerbero-bite-mainnet-verified.md: VERIFICATO su QUOTE REALI Deribit
  mainnet (cerbero-bite/MCP = mainnet, bit-identico a ccxt.deribit). Premio reale (BID, con skew) =
  1.29x il modellato -> il backtest SOTTOSTIMA il premio; il rischio vero e' la CODA (short-vol) +
  liquidita' di roll in stress, non la magnitudine.

NB: lo sleeve opzioni e' un LEAD, NON deployato: prezzato da modello (BS su DVOL) + 1 snapshot in
regime calmo. Serve validazione real-chain multi-regime + stress crash + paper su testnet prima di
aggiungerlo al portafoglio. Portafoglio attivo invariato: TP01 70% + XS01 30%.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 20:24:16 +00:00
Adriano Dal Pastro 790caefd52 research: wave1 beat-TP01 (26 agenti BTC/ETH) — nessun 3o sleeve robusto, portafoglio invariato
26 agenti, 3 contender ri-verificati onesti: tsmom_12h scartato (corr +0.49 = TP01 veloce),
breakout_atr scartato (gonfia solo FULL storico, hold-out +0.05), highvol_rev in WATCHLIST
(scorrelato e migliora FULL+hold-out MA edge solo a REV_LB=1 = picco non-plateau, FULL mediocre
0.74, HOLD>>FULL = regime-luck alta-vol 2025-26, reversal+concept-flip). Stesso difetto del RV
bocciato -> non deployato. Portafoglio resta TP01 70% + XS01 30%. L'edge incrementale e' venuto
dall'espansione universo (Hyperliquid cross-sectional), non da altre trend-variant su 2 asset.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 20:19:47 +00:00
Adriano Dal Pastro a5a61ac7e3 feat(portfolio): XS01 cross-sectional (Hyperliquid) BATTE il portafoglio -> TP01 70% + XS01 30%
Espansione universo (su input utente "storico da cerbero"): il Cerbero MCP col token MAINNET serve
Hyperliquid (230 perp REALI, storia nativa dal 2024). fetch_hyperliquid.py certifica 19 alt liquidi
a 1d (flat 0%, cross-venue 4-9 bps vs Binance) -> data/raw/hl_*_1d.parquet. Abilita le strategie
CROSS-SECTIONAL (impossibili a 2 asset).

XS01 = cross-sectional momentum market-neutral (long 5 forti / short 5 deboli su ret 30g, ogni 10g,
vol-target 20%). Validato onesto: plateau (config/k/subset), fee-robusto (0.3% RT), scorrelato a TP01
(-0.06), positivo OGNI anno 2024-26, meccanismo complementare (lavora nella dispersione quando TP01
e' in cash). Diverso dal regime-luck RV bocciato (19 asset, plateau, ogni anno+).

Contributo al portafoglio (outer-join + pesi rinormalizzati per sleeve a date diverse):
  TP01-solo FULL 1.30 / HOLD 0.31  ->  TP01 70% + XS01 30%: FULL 1.41 / HOLD 1.15, DD giu', ~ogni anno+.
-> XS01 BATTE il portafoglio esistente: inserito in active_sleeves.

Caveat (documentati): storia XS ~2.5 anni; STAT-MODE (book 19 gambe non eseguibile a 2k -> ~20k),
sleeve diagnostico/forward-monitor. portfolio.combine ora outer-join+renorm. 12 test passano.
Diario 2026-06-19-hyperliquid-xsec.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 20:05:45 +00:00
Adriano Dal Pastro 18f22160b2 research: caccia al 2o sleeve — nessun diversificatore robusto, TP01-only resta
Tool second_sleeve_hunt.py: giudica i candidati per CONTRIBUTO al portafoglio (non Sharpe
standalone). RV mean-rev ETH/BTC morto (come sempre). RV relative-momentum (ratio_trend ==
xs_momentum) sembrava promosso (hold-out portafoglio 0.31->1.51) MA il per-anno + plateau lo
smascherano come REGIME-LUCK 2025: FULL Sh mediocre 0.56, 2 anni consecutivi negativi
(2023 -17%, 2024 -19%), guadagno concentrato nel 2025 (+62%), hold-out Sh non-plateau (0.25-1.92
al variare dei parametri). Beneficio FULL robusto solo +0.09 (diversificazione di uno sleeve
scorrelato debole). NON promosso: la disciplina che boccia i falsi positivi in-sample boccia
anche i falsi positivi nel hold-out. Criterio aggiornato: breadth per-anno + plateau, non solo
hold-out. Relative-momentum in WATCHLIST. Diario 2026-06-19-second-sleeve-hunt.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 19:28:51 +00:00
Adriano Dal Pastro ef52ad6a79 feat(portfolio): contenitore di strategie ESTENSIBILE — TP01 primo sleeve
src/portfolio/: Sleeve (serie rendimenti netti per-barra, causale/fee-aware) + StrategyPortfolio
(combina N sleeve per peso su griglia giornaliera comune, metriche FULL/HOLD-OUT/per-anno +
standalone per-sleeve, vs buy&hold). Registry sleeve attivi in sleeves.py: per ora SOLO TP01
(peso 100%); aggiungere = una riga (dopo validazione col gauntlet).

Report (run_portfolio.py): TP01 FULL Sh 1.30 / DD 14.3% / ~€1.52/g, HOLD-OUT 0.31 / +3.5%
(buy&hold -0.32 / -39%). Posizione corrente flat (difensivo). tests/test_portfolio.py (6 test).
CLAUDE.md aggiornato (struttura + comando + come aggiungere uno sleeve).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 19:17:18 +00:00
Adriano Dal Pastro 12754c4908 fix(TP01): bug look-ahead ffill mixed-TF -> deploy a >=12h (1d), strategia DIFENSIVA
Segnalato: ffill MIXED-TIMEFRAME su barre open-labeled (resample label="left") gonfiava il 4h
(~1.60 -> reale ~1.1). Ri-verifica per-SINGOLO-TF leak-free (guard prefix-recompute, leak=0 su
4h/6h/12h/1d): FULL Sh piatto ~1.3, hold-out 2025-26 MIGLIORE a 1d (Sh 0.31 / +3.5% vs buy&hold
-39%). Conclusione adottata: NON scendere sotto le 12h (sotto, costi+overfit dominano senza vantaggio).

- trend_portfolio.py: canonica PORT LF1d; resample_tf/resample_1d (resample_4h deprecato deploy);
  docstring con nota look-ahead + natura DIFENSIVA (taglia DD ~6x, non alpha).
- paper_trend.py: deploy a 1d (resample_1d, build_bars). 5 test passano.
- CLAUDE.md: TP01 ridescritta (>=12h/1d, gotcha ffill mixed-TF, difensiva).
- tp01_lowfreq.py + diario 2026-06-19-tp01-lookahead-fix-lf.md.
Gotcha: mai ffill/combine mixed-TF su timestamp open-labeled (close propagata indietro = leak).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 19:04:38 +00:00
Adriano Dal Pastro 756a2bdf04 research: stress-test TP01 — robusto come strategia DIFENSIVA (DD-cut), edge ritorno hold-out sottile
Stress sul modulo integrato: FULL regge fee 0.40% + lag + ampio plateau parametri (orizzonti
20/60/120 fa Sh 1.61, non cherry-pick); deflated-Sharpe DSR 0.999 a N=100 (no multiple-testing
artifact). MA il ritorno nel hold-out 2025-26 e' SOTTILE (+2.8%/Sh0.27 a 0.10%, ~flat a 0.40%/lag2):
TP01 PROTEGGE il drawdown (8% vs 60% buy&hold) piu' di quanto profitti. Proprieta' robusta e
deployabile = taglio DD; alpha = no. Da monitorare col paper trader prima di scalare.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 18:57:22 +00:00
Adriano Dal Pastro d152941360 integra(TP01): merge ricerca branch strategy-research-2026-06 (squash) — strategia vincente + harness + track A-E
Integra il lavoro della linea di ricerca parallela (AdrianoDev), verificato indipendentemente
col mio gauntlet onesto (regge il hold-out 2025-26 su entrambi gli asset, plateau 1h/4h/1d):
- src/strategies/trend_portfolio.py  TP01 (TSMOM 30/90/180 vol-target 20% lev2x long-flat, 50/50 BTC+ETH)
- src/backtest/harness.py            harness onesto (load + backtest_signals no-leakage + OOS)
- scripts/research/track{A,B,C,D,E}_*.py + trackD_timing.py  (le 5 track della ricerca)
- scripts/live/paper_trend.py        paper trader forward-only di TP01 (no esecuzione reale)
- tests/test_trend_portfolio.py (5 test, passano) + 6 diari trackA-E + synthesis
- CLAUDE.md aggiornato con l'esito ricerca (TP01 vincente, mean-rev morto, onesta su €50/g)

Squash (non merge) per NON portare in git i ~68MB di data/_feed_backup/*.bak che il branch
aveva committato per errore: esclusi + data/_feed_backup/ e data/paper_trend/ ora gitignorati.
Storia granulare del branch conservata sul ref origin/strategy-research-2026-06.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 18:55:04 +00:00
Adriano Dal Pastro 55c28e51b2 research: verify TP01 (branch parallelo) col gauntlet onesto — REGGE il hold-out 2025-26
TP01 (TSMOM 30/90/180 vol-target 20% lev2x long-flat, 50/50 BTC+ETH) passa dove il mio trend 1h
era caduto: hold-out 2025-26 +2.8%/DD8% vs buy&hold -39%/DD60%, positivo su ENTRAMBI gli asset,
plateau 1h/4h/1d. La chiave e' il vol-targeting (esposizione ~1/vol -> cash nei crash) che non
avevo combinato col trend. Edge DIFENSIVO reale (Sharpe full 1.36 vs B&H 0.92, ma CAGR 16.6% vs 48%).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 18:50:36 +00:00
Adriano Dal Pastro 38c8cdf25b research(v2.0.0): honest harness + fasi 0-3 + ricerca frattale 63 agenti — nessun edge robusto su BTC/ETH
Harness onesto research_lab.py (serie di posizione causale, fee-aware, null model a
rotazione circolare, hold-out 2025+ bloccato; self-test cheat/noise che valida il banco).
- Fase 1: triage superstiti (DIP, shape-ML) -> morti net-fee.
- Fase 2: esplorazione famiglie (reversal morta; solo trend long-only/MA-cross passa i gate base).
- Fase 3: conferma avversariale del trend -> regime-luck del toro, bocciato sul hold-out 2025-26.
- Ricerca frattale multi-agente (Workflow, 63 agenti, 52 ipotesi dai due documenti) con guard
  anti-look-ahead (eval_signal.py) + hold-out + test cross-asset -> 0 edge robusto (l'unico
  "confermato" su ETH fallisce su BTC con lo stesso codice).
- Analisi options: VRP reale +10/+14 vol pt ma finestra 6 sett. regime unico -> non validabile;
  ruolo solo overlay tail-cap, tenere cerbero-bite ad accumulare.

Quinta conferma indipendente: su BTC/ETH-solo-prezzo non c'e' un edge facile. Il processo
disciplinato ha evitato un falso "+49% vs -49%" che sul vecchio feed contaminato sarebbe
finito in produzione. Diari docs/diary/2026-06-19-research-phase0-1 / -phase2-options /
-phase3-confirm / -fractal-multiagent-search.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 18:37:05 +00:00
452 changed files with 59648 additions and 6 deletions
+11
View File
@@ -0,0 +1,11 @@
Old/
data/
.venv/
.git/
logs/
__pycache__/
**/__pycache__/
*.pyc
.env
.env.mainnet
docs/
+7
View File
@@ -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
+23
View File
@@ -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
@@ -43,3 +45,24 @@ data/games/
# archived data (mirrors top-level data/ ignores, which are top-level-anchored) # archived data (mirrors top-level data/ ignores, which are top-level-anchored)
Old/data/ Old/data/
Old/**/__pycache__/ Old/**/__pycache__/
# run logs (rigenerabili dagli script)
logs/
# cache della ricerca trackE (rigenerabile)
.cache_trackE_*.npy
# feed backup pre-rebuild (binari rigenerabili, NON in git) + stato paper trader (runtime)
data/_feed_backup/
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/
+163 -6
View File
@@ -13,9 +13,141 @@ 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
Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condiviso
`src/backtest/harness.py`). Sintesi in `docs/diary/2026-06-19-research-synthesis.md`.
- **TP01 Trend Portfolio — strategia DIFENSIVA robusta (non alpha)** —
`src/strategies/trend_portfolio.py`. TSMOM multi-orizzonte (1-3-6 mesi) vol-targeted, long-flat,
50/50 BTC+ETH. Config canonica **PORT LF1d** (**>=12h, 1d raccomandato**, vol-target 20%, leva cap 2x):
**FULL Sharpe ~1.30, maxDD ~14%; HOLD-OUT 2025-26 Sharpe ~0.31 / +3.5%** mentre il buy&hold 50/50
faceva 39%/DD60%. Verificata indipendentemente col gauntlet onesto (hold-out + cross-asset +
plateau + deflated-Sharpe 0.999): **regge**. **Valore = taglio del drawdown ~6× vs buy&hold**, NON
generazione di ritorno (CAGR ~16% vs ~48% del buy&hold sul toro).
⚠️ **LOOK-AHEAD (2026-06-19):** un ffill MIXED-TIMEFRAME su barre open-labeled gonfiava il 4h
(~1.60 → reale ~1.1). Il calcolo per-singolo-TF è leak-free, ma **NON scendere sotto le 12h**:
costi+overfitting dominano senza vantaggio (FULL Sh piatto ~1.3 da 12h a 4h; hold-out migliore a 1d).
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`.
Ri-verifica: `scripts/analysis/{verify_tp01,stress_tp01,tp01_lowfreq}.py`.
- **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,
dal 2024): ogni 10g long i 5 più forti / short i 5 più deboli, vol-target 20%. **Scorrelato a TP01
(~0.12).** Affinato (2026-06-19): **(a) blend di lookback [30,90]** (z-score cross-sectional mediato,
come il multi-orizzonte di TP01); **(b) gate di dispersione p30** (entra solo se la dispersione
cross-section del momentum supera il percentile espandente causale, altrimenti flat — XS è rumore in
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) →
monitor forward. NB il gate concentra XS nei regimi dispersi (2025-26 = hold-out alta-dispersione).
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`.
- **PORTAFOGLIO ATTIVO = TP01 (41.25%) + XS01 (18.75%) + VRP01 (15%) + SKH01 (25%)** (`src/portfolio/sleeves.active_sleeves`):
TP01+XS01 combinato **FULL Sharpe 1.55, HOLD-OUT 1.55, DD 4.4%**. Aggiunto **VRP01** (options
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
validare locale con dati HL). **Aggiunto SKH01-V2-DD @25% effettivo (2026-06-23, sotto):** i tre
preesistenti scalati nel restante 0.75 (rapporto 55:25:20). Il portafoglio a **4 sleeve** fa
**FULL Sharpe 1.68→2.13, HOLD-OUT 1.63→2.30, DD full 14.3%→7.8%** (Skyhook è quasi-ortogonale,
corr ~0.09). Report `scripts/portfolio/run_portfolio.py`. Sleeve a date d'inizio
diverse → outer-join con pesi rinormalizzati (TP01/SKH01 dal 2019, VRP dal 2021, XS dal 2024).
- **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`.
- **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
`../FinanceOld/OptionsAgent` (Bear Call Spread + gate d'ingresso). Migliora il lead VRP nudo
(options_vrp_lab): **(a) defined-risk** taglia la coda (worst-week -16.6%→-7.4%, DD 33%→14%);
**(b) gate IV-rank>0.30** = vendi vol solo ricca → ribalta HOLD-OUT da -0.25 a +0.28 (l'alpha è il
filtro di regime). Standalone **FULL Sh 1.10, HOLD 0.60, DD 12%**, positivo/piatto ogni anno (2022
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
`scripts/research/options_vrp_v2.py` (vs baseline `options_vrp_lab.py`). Test `tests/test_vrp_sleeve.py`.
Diario `2026-06-20-financeold-analysis-vrp-v2.md`.
- **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
multi-asset è ridondante con TP01, corr 0.74). I margini su XS sono nella STRUTTURA DEL SEGNALE
(blend + gate), non nel numero di asset. I **51** parquet certificati restano per ricerca futura.
⚠️ Il test "52-asset = negativo" era in parte inquinato dal backfill sintetico (AXS 83%, ALGO/SAND
37% di barre vol=0) poi rimosso — vedi correzione estrazione 2026-06-20 sotto; resta comunque vero
che il long-tail diluisce XS01, ma il numero netto post-fix è 51.
- **Lead OPZIONI VRP (income short-vol) — quantificato, NON deploy** — `scripts/research/options_vrp_*.py`.
Vendita put settimanali che incassa il volatility risk premium (IV>RV), scorrelato al trend (~0.07).
Premio prezzato BS su DVOL reale (`fetch_dvol.py`) + calibrato su quote REALI cerbero-bite mainnet
(`options_vrp_calibrate.py`): **f reale ≈ 1.0** (non 1.29) → Sharpe ~0.71, DD 33%, coda severa
(settimane 15..26% su LUNA/FTX). Diversificatore DEBOLE a premio reale, e short-vol da modello.
**Regola: niente short-vol da modello in deploy.** Rivalutare quando cerbero-bite cattura un crash
(per il f di stress reale). Diari `2026-06-19-options-vrp-lab` / `-eval-crypto-backtest-options`.
- **Edge deboli/scartati:** ML walk-forward BTC (Sh ~0.57), trend 1h L/S (~1.0), RV ETH/BTC (Sh 0.27,
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
libreria +201%/+1238% era contaminazione); trend 5m/15m (fee).
- **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.
- **Sweep "strategie alternative" (2026-06-20) — 104 ipotesi / 153 agenti / NIENTE di nuovo regge.**
Ricerca onesta a largo spettro su BTC/ETH+DVOL (harness condiviso vettoriale leak-free
`scripts/research/alt/altlib.py`, 104 script in `scripts/research/alt/runs/`): 11 famiglie
(breakout, trend non-TSMOM, mean-rev gated, DVOL/vol, cross-asset pairs, stagionalità, overlay
rischio, opzioni modellate, microstruttura, ML walk-forward, combo). 16 promettenti, **1 sola**
sopravvissuta alla verifica avversariale (3 scettici) e comunque NON deployabile. Conferma forte
del soffitto ~1.3: ogni PASS era hold-out-fitting o **TP01/TSMOM travestito** (trend-beta del
toro). Unico LEAD: **STA05** (EWMA-cross ensemble, **long-short**) — leak-free, plateau, corr
hold-out **0.53** a TP01, il blend 0.75·TP01+0.25·STA05 alza l'hold-out 0.31→0.59 (full 1.30→1.24,
DD 14→16%); MA hold-out corto (536g) → **forward-monitor, non sleeve.** Lezione harness: valutare
lo Sharpe **MARGINALE vs baseline TP01** (non assoluto) + esigere plateau e jackknife
drop-one-month sull'hold-out prima di PASS (hanno ucciso 13/14 falsi positivi). Diario
`2026-06-20-alt-strategies-100agent-sweep.md`.
- **MARGINAL SCORER (implementato 2026-06-20)** — la lezione "Sharpe marginale, non assoluto" è
ora codice in `scripts/research/alt/altlib.py`: `study_marginal(name, target_fn)` valuta un
candidato direzionale BTC/ETH **sia** in assoluto **sia** rispetto al baseline `tp01_baseline_daily()`
(corr, uplift del blend OOS, beta+alpha residua) e ritorna `earns_slot = (abs!=FAIL) AND
(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 —
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.
- **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 +
tempo. La strategia che *guadagna* esiste, ma a ~+€1.5/giorno su 2000.
Script ricerca: `scripts/research/track{A,B,C,D,E}_*.py` + `trackD_timing.py`.
## Obiettivo ## Obiettivo
Ricerca: riconoscimento pattern frattali per trading algoritmico su crypto. Target dichiarato Ricerca: riconoscimento pattern frattali per trading algoritmico su crypto. Target dichiarato
@@ -35,9 +167,17 @@ netto fee, out-of-sample, robusto su griglia, e su dati certificati + liquidi +
src/data/downloader.py → load_data(asset, tf): legge i parquet certificati da data/raw/ src/data/downloader.py → load_data(asset, tf): legge i parquet certificati da data/raw/
src/strategies/base.py → Strategy (ABC), Signal, BacktestResult, YearlyStats src/strategies/base.py → Strategy (ABC), Signal, BacktestResult, YearlyStats
src/strategies/indicators.py → indicatori condivisi (ema, atr, keltner, ...) src/strategies/indicators.py → indicatori condivisi (ema, atr, keltner, ...)
src/strategies/trend_portfolio.py → TP01: strategia DIFENSIVA robusta (PORT LF1d, >=12h), causale
src/portfolio/ → PORTAFOGLIO DI STRATEGIE estensibile (Sleeve + StrategyPortfolio)
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
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/version.py → APP_VERSION (legge il file VERSION) src/version.py → APP_VERSION (legge il file VERSION)
scripts/research/ → ricerca: track{A-I}_*.py + options_vrp_*.py + fetch_dvol.py
scripts/portfolio/ → run_portfolio.py (report) + xsec_*.py (ricerca/affinamento XS01)
scripts/live/paper_trend.py → paper trader forward-only di TP01 (1d) (no esecuzione reale)
scripts/analysis/ → SOLO i tool dati certificati: scripts/analysis/ → SOLO i tool dati certificati:
rebuild_history.py → (ri)costruisce lo storico da Deribit mainnet (base 5m + resample) rebuild_history.py → (ri)costruisce lo storico da Deribit mainnet (base 5m + resample)
certify_feed.py → certifica il feed (integrità, coerenza resample, spike, cross-venue) certify_feed.py → certifica il feed (integrità, coerenza resample, spike, cross-venue)
@@ -57,7 +197,13 @@ uv sync # installa dipende
uv run python scripts/analysis/rebuild_history.py --asset BTC ETH # (ri)costruisci storico da Deribit mainnet uv run python scripts/analysis/rebuild_history.py --asset BTC ETH # (ri)costruisci storico da Deribit mainnet
uv run python scripts/analysis/certify_feed.py # certifica i feed (locale + cross-venue) uv run python scripts/analysis/certify_feed.py # certifica i feed (locale + cross-venue)
uv run python scripts/analysis/certify_feed.py --local # solo check locali (veloce) uv run python scripts/analysis/certify_feed.py --local # solo check locali (veloce)
uv run pytest # test (da ripopolare con le nuove strategie) uv run python scripts/research/trackD_trendport.py # backtest strategia vincente (full report)
uv run python scripts/research/trackD_timing.py # vincitrice su 15m/1h/4h/1d + PnL/DD/trade per anno
uv run python scripts/analysis/fetch_hyperliquid.py # fetch+certify universo Hyperliquid (Cerbero mainnet) -> data/raw/hl_*
uv run python scripts/portfolio/xsec_research.py # ricerca cross-sectional su Hyperliquid (XS01)
uv run python scripts/portfolio/run_portfolio.py # report del PORTAFOGLIO attivo (TP01+XS01)
uv run python scripts/live/paper_trend.py # avanza il paper trader TP01 (forward-only, 1d)
uv run pytest # test
``` ```
```python ```python
@@ -80,10 +226,21 @@ df = load_data("BTC", "1h") # OK. load_data("SOL", ...) -> FileNotFoundError (
### Universo ricercabile certificato ### Universo ricercabile certificato
- **BTC / ETH**: puliti (2-6 bps vs Coinbase USD su tutta la storia), liquidi (~0% barre flat a 1h), - **BTC / ETH**: puliti (2-6 bps vs Coinbase USD su tutta la storia), liquidi (~0% barre flat a 1h),
storia lunga (2018/2019→oggi) → **ogni timeframe (5m/15m/1h)**. È l'unico dato in `data/raw`. storia lunga (2018/2019→oggi) → **ogni timeframe (5m/15m/1h)**. È l'unico dato in `data/raw`.
- **Alt (SOL/XRP/ADA/LTC/DOGE/BNB): FUORI.** Illiquidi (LTC 5m 82% barre flat O=H=L=C, run fino a - **Alt Deribit (SOL/XRP/ADA/LTC/DOGE/BNB): FUORI.** Illiquidi (LTC 5m 82% barre flat, run ~3 giorni),
~3 giorni), divergenti (LTC/DOGE >1% su 10-21% delle barre 2022-23), o non certificabili divergenti, o non certificabili. Archiviati in `Old/data/raw`.
(XRP delistato da Coinbase per causa SEC; BNB non listato + storia da 2024-10). Sono archiviati in - **Universo Hyperliquid (Cerbero MCP MAINNET): 19 alt liquidi a 1d, dal 2024** — BTC/ETH/SOL/BNB/XRP/
`Old/data/raw`. Riammetterne uno richiede prima una ricertificazione che dimostri liquidità + accordo. DOGE/AVAX/LINK/LTC/ADA/ARB/OP/SUI/APT/INJ/TIA/SEI/NEAR/AAVE. Certificati (`fetch_hyperliquid.py`):
flat 0%, cross-venue 4-9 bps vs Binance, >1% ≈0% → `data/raw/hl_*_1d.parquet`. **Caveat:** storia
nativa solo **~2.5 anni** (2024-2026; pre-2024 = backfill, vol 0). Abilita le strategie
CROSS-SECTIONAL (impossibili a 2 asset). NB: Cerbero col token TESTNET = farlocco; col token
**mainnet** (`.env.mainnet`) = reale, ma SEMPRE da certificare (cross-venue + liquidità).
⚠️ **CORREZIONE estrazione (2026-06-20):** il backfill NON è solo pre-2024 — cerbero MCP padda con
barre SINTETICHE (volume 0, prezzi copiati da Binance → matchano cross-venue e non sono flat) ogni
asset listato su HL **dopo** lo START. Il `flat`+cross-venue da soli non lo vedono: il rivelatore è
il **VOLUME**. `fetch_hyperliquid.py` ora (1) taglia il run iniziale a volume 0, (2) scarta chi resta
< 365g reali (es. **AXS 83% sintetico → fuori**), (3) gata i gap vol=0 interni. Universo certificato
= **51** (era 52). I **19 major di XS01 hanno 0 backfill → invariati** (strategia live non toccata).
Verificato direttamente su cerbero MCP. Diario `2026-06-20-cerbero-backfill-fix.md`.
## Metodologia obbligatoria per ogni nuova strategia ## Metodologia obbligatoria per ogni nuova strategia
+11
View File
@@ -0,0 +1,11 @@
FROM python:3.11-slim
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev
COPY src/ src/
COPY scripts/ scripts/
COPY VERSION ./
VOLUME /app/data
# Monitor PAPER del portafoglio attivo (TP01+XS01). Esecuzione REALE disabilitata.
CMD ["uv", "run", "python", "-m", "src.live.dashboard", "--port", "8787"]
+7
View File
@@ -0,0 +1,7 @@
{
"_nota": "Config esecuzione LIVE di TP01. execution_enabled=true + --execute -> ordini REALI. ARMATO 2026-06-20.",
"execution_enabled": true,
"max_notional_per_asset_usd": 300,
"min_order_usd": 5,
"disaster_sl_pct": 0.30
}
+32
View File
@@ -0,0 +1,32 @@
# Solo MONITOR (dashboard paper) del portafoglio attivo. Niente runner/esecuzione reale
# (archiviati in Old/). v2.0.0+.
services:
dashboard:
build: .
container_name: pythagoras-dashboard
restart: unless-stopped
command: ["uv", "run", "python", "-m", "src.live.dashboard", "--port", "8787"]
ports:
- "8787:8787"
volumes:
- ./data:/app/data:ro
# 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.
- ./.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
+35
View File
@@ -0,0 +1,35 @@
# 2026-06-19 — Wave 1 "beat TP01" (26 agenti BTC/ETH): nessun 3º sleeve robusto
Goal "trova strategie che battano l'esistente e inseriscile": GIA' soddisfatto da XS01 (cross-
sectional Hyperliquid, integrato → portafoglio TP01 70% + XS01 30%, FULL Sh 1.41 / HOLD 1.15).
In parallelo, una wave di 26 agenti ha cercato su BTC/ETH miglioramenti del trend + diversificatori.
## Esito wave 1 (26 agenti, 25 leak-free): 22 weak, 3 "contender", 1 noise
I 3 contender, ri-verificati ONESTAMENTE col giudice book-level (`verify_contender.py`) e come
contributo marginale al portafoglio ATTUALE (TP01+XS01):
| Candidato | corr TP01 | corr XS01 | +portafoglio (w30%) | Verdetto |
|---|---|---|---|---|
| **tsmom_strength_12h** | **+0.49** | — | — | ☠️ scartato: è TP01 più veloce (correlato), non diversifica |
| **breakout_atr** (trend) | 0.04 | 0.04 | FULL +0.48 / **HOLD +0.05** | ☠️ scartato: gonfia solo il FULL storico (bull), ~zero valore nel hold-out |
| **highvol_rev** (reversal alta-vol) | 0.08 | 0.05 | FULL +0.20 / **HOLD +0.30** | 🟡 WATCHLIST (vedi sotto) |
## highvol_rev: candidato vero ma NON abbastanza robusto → watchlist
È l'unico genuinamente scorrelato a ENTRAMBI gli sleeve e che migliora FULL+hold-out. MA il mio
robustezza-check indipendente (plateau, come per XS01) lo boccia per il deploy:
- **Edge solo a REV_LB=1**: LB2 FULL Sh 0.33, LB3 ~0.05 → **picco a singola-barra, non plateau**.
- **FULL standalone mediocre** (0.74); la forza è nel hold-out (HOLD 0.97-1.39 vs FULL ~0.7) =
**HOLD≫FULL = regime-luck dell'alta-vol 2025-26**, non robustezza temporale.
- È un **reversal** (famiglia morta in tutto il progetto) con concept ribaltato post-hoc
(low-vol→high-vol). Regge fee fino ~0.3% ma con margine ridotto.
Stesso difetto (HOLD≫FULL, no-plateau) per cui ho bocciato ieri il RV ETH/BTC regime-luck. La
disciplina che boccia i falsi positivi vale anche qui → **NON deployato**, in watchlist; rivalutare
forward (più dati) o se emerge un plateau su un parametro core.
## Conclusione
Wave 1 NON aggiunge un 3º sleeve robusto. **Portafoglio invariato: TP01 (70%) + XS01 (30%).** Le
famiglie trend (breakout/tsmom-12h) sono ridondanti con TP01 o aiutano solo il bull storico; l'unico
diversificatore di meccanismo nuovo (highvol_rev) non regge il bar di robustezza. Il vero edge
incrementale è venuto dall'ESPANSIONE DELL'UNIVERSO (Hyperliquid → cross-sectional), non da altre
varianti di trend su 2 asset. Direzione futura coerente: più asset certificati + sleeve di
meccanismo nuovo (non altre trend-variant), col criterio plateau+breadth+contributo.
@@ -0,0 +1,65 @@
# 2026-06-19 — Cerbero-bite = MAINNET reale: fonte VRP sbloccata
Indagine "cerca dati di cerbero-bite" + verifica mainnet/testnet a tre livelli. Esito: la
contaminazione storica NON era una proprieta' di Cerbero MCP, ma del vecchio token testnet sul
solo endpoint `get_historical`. Il token di cerbero-bite e' mainnet e serve catene opzioni reali.
## Dove sono i dati di cerbero-bite
`/home/adriano/Documenti/Git_XYZ/CerberoSuite/Cerbero_Bite` — bot live (testnet exec, propose-only)
che vende **credit-spread bull-put su ETH**. Dati:
- `data/state.sqlite`: `market_snapshots` (**52 righe, solo 30 apr1 mag 2026**, BTC+ETH) con
`spot, dvol, realized_vol_30d, iv_minus_rv, funding_perp/cross, dealer_net_gamma,
gamma_flip_level, oi_delta_pct_4h, liquidation_long/short_risk, macro_days_to_event`;
`dvol_history` (1 riga); `positions/instructions/decisions` (0 righe, niente trade persistiti).
- `data/log/*.jsonl` (26 apr1 mag 2026): log HTTP, non dump di catena. `strategy.yaml`: golden config.
- **Fonte dati**: Cerbero MCP (`get_instruments` + `get_ticker_batch`) dal gateway
`cerbero-mcp.tielogic.xyz`. NON c'e' storico profondo della catena (solo fetch live/on-demand).
## Verifica mainnet vs testnet (3 livelli)
1. **Spot vs nostra serie certificata** (Deribit mainnet), 2026-04-30 1316h UTC:
BTC cerbero 76.28776.446 vs certificato 76.23776.443 (Δ 0.130.27%); ETH 2.2612.264 vs
2.2562.265 (Δ 0.040.29%). Scarti = rumore intra-barra (snapshot 15-min vs close orario).
NON e' il feed fantasma testnet (che divergeva >3%).
2. **`environment_info`** (token cerbero-bite): `environment=mainnet`, `base_url=www.deribit.com`,
`source=credentials`. **`get_ticker ETH-PERPETUAL`**: `testnet=false`, mark 1703.11.
3. **Catena, decisivo** — stessa opzione su ccxt.deribit mainnet vs Cerbero MCP:
`ETH_USDC-26JUN26-1650-P` (put settimanale, delta ~-0.28):
| fonte | bid | ask | mark_iv | delta | testnet |
|---|---|---|---|---|---|
| ccxt mainnet | 25.6 | 26.6 | 54.54% | -0.3150 | — |
| Cerbero MCP | 25.6 | 26.6 | 54.54% | -0.31513 | False |
**Identici bit-per-bit.**
## Verdetto
- **Il token MCP di cerbero-bite e' MAINNET; la sua catena opzioni e' reale** (= ccxt.deribit
mainnet). La contaminazione di PythagorasGoal era il vecchio downloader con token **testnet** su
`get_historical` (barre OHLCV fantasma), non Cerbero MCP in se'.
- **Fonte VRP sbloccata**: Cerbero MCP da' bid/ask/IV/greche/OI per-strike (come ccxt) **+** feature
di regime che ccxt non ha (`dealer_net_gamma`, `gamma_flip_level`, `oi_delta_pct_4h`,
`liquidation_*`, `funding`, `iv_minus_rv`, `macro`). Utile per validare lo sleeve VRP su piu'
regimi (raccolta snapshot live + accumulo nel tempo).
- **Limite residuo**: niente storico profondo della catena -> il backtest pluriennale del VRP resta
prezzato da modello (DVOL+BS); ma la calibrazione model-vs-reale e' ora robusta e ripetibile
(snapshot reali su piu' date/regimi).
## Collegamento col lavoro VRP (sleeve opzioni)
Conferma e rafforza `2026-06-19-eval-crypto-backtest-options.md`: lo snapshot ccxt aveva gia'
mostrato che il backtest SOTTOSTIMA il premio (skew +28% > spread 4% -> bid reale = 1.29x modello).
Ora abbiamo due fonti mainnet concordi (ccxt + Cerbero MCP) per misurare premio/skew/spread su piu'
regimi. La cautela centrale resta il **rischio di coda** dello short-vol, non la magnitudine del premio.
## Stato cerbero-bite (gia' concluso, contesto)
Il credit-spread bull-put ETH e' gia' stato giudicato NON robusto su ciclo completo (diario
`Old/docs/diary/2026-06-09-cerbero-bite-credit-spread.md`: EV breakeven-negativo; "+0.48%/mese" =
artefatto di finestra calma; coda concentrata col fade ETH). E' una struttura diversa dalla
put-selling/wheel del progetto `crypto_backtest`.
> Sicurezza: il token di cerbero-bite e' stato usato solo per la verifica; mai stampato ne' committato
> (resta in `.env`, gitignored).
@@ -0,0 +1,125 @@
# 2026-06-19 — Valutazione strategia esterna `crypto_backtest` (trend + opzioni VRP)
Valutazione critica di un progetto esterno (`/home/adriano/crypto_backtest/`, file chiave
`STRATEGIA.md`, `production.py`, `options_deribit.py`, `production_equity.csv`) che propone un
book a 2 motori quasi scorrelati. Rilevante perché tocca proprio la frontiera che la nostra
ricerca post-reset ha lasciato aperta (le opzioni / volatility risk premium).
## Cosa propone
Portafoglio a due gambe (ρ=0.22 verificato dal CSV):
- **Sleeve 1 (25%)** — trend spot BTC+ETH a **12h**, long-only se `trend(30g)>0`, vol-target 20%,
cap 3×, leva globale ~1.07 calibrata a maxDD in-sample 20%.
- **Sleeve 2 (75%)** — vendita di **put settimanali (CSP/wheel) su BTC** su Deribit, strike a
**delta 0.28**, hold-to-expiry, IV da DVOL reale, prezzo Black-Scholes.
Numeri riprodotti dal CSV (finestra 2021-04→2026-06, 272 settimane):
| Serie | CAGR | Sharpe | maxDD | final |
|---|---|---|---|---|
| spot | +12.0% | 0.77 | 18.1% | 1.80x |
| opt | +15.9% | 1.09 | 20.0% | 2.16x |
| **blend 25/75** | +15.4% | **1.21** | **15.2%** | 2.10x |
| blend ri-levato | +20.5% | 1.21 | 20.0% | 2.63x |
| B&H BTC | +1.3% | 0.30 | 74.2% | 1.07x |
corr(spot, opt) = **0.217** confermata. Settimane peggiori opt: 2022-05 (LUNA) 13%,
2022-06 11%, 2021-05 11%, 2022-11 (FTX) 9.7%.
## Punto forte — corroborazione indipendente del nostro TP01
Lo **sleeve spot è quasi identico al nostro TP01** (`src/strategies/trend_portfolio.py`):
12h, long-only, trend(30g), vol-target 20%, cap 3×. Due ricerche separate, due dataset diversi
(loro Binance, noi Deribit certificato), **stessa conclusione**: il trend vol-targeted a 12h è
l'edge reale e robusto. Il nostro Sharpe è più alto (1.32 vs 0.77 su questa finestra / 1.07
full-history) perché usiamo un **blend multi-orizzonte 1-3-6m** invece del singolo trend a 30g →
il blend diversifica gli orizzonti e alza lo Sharpe. Conferma forte per entrambi.
NB: loro confermano anche le NOSTRE lezioni — intraday ≤1h scartato (costi/rumore), un **bug di
look-ahead sul 4h trovato e corretto** (identico al nostro audit), MR/condor/strangle nudi e
collar stretti scartati per overfit/tail.
## Punto critico — lo sleeve opzioni guida il 75% ma è prezzato dal proprio modello
È esattamente il muro che avevamo dichiarato non-backtestabile (W18/19/21, ARGO: niente storico
chain per-strike gratis). Il loro workaround (BS su **DVOL reale** + payoff sul path realizzato)
fa emergere il VRP perché IV>RV (misurato BTC IV/RV~1.24). Concettualmente sano, ma la
**magnitudine è ottimistica** — limiti (in parte ammessi dagli autori):
1. **Nessun bid/ask**: vendono al mid (BS fair), non al bid. Sulle put OTM settimanali lo spread
è grosso → premio reale nettamente inferiore.
2. **Skew ignorato**: prezzano put a delta-0.28 (OTM) con DVOL = **IV ATM**. Il mercato carica le
put molto di più (skew di crash) → modellano la vol sbagliata proprio sull'opzione venduta.
3. **Coda sotto-modellata**: settimana peggiore solo 13% attraverso LUNA/FTX → sospettosamente
benigno per un venditore di put nudo. Gap, illiquidità di roll e settlement inverso (coin-settled)
sono approssimati.
4. **Leva senza funding** (ottimistico) + **bias di finestra** (parte vicino al top 2021,
favorevole a un book short-vol DD-capped).
Il blend Sharpe 1.21 è dominato dallo sleeve income (Sharpe 1.09, peso 75%). Con bid/ask + skew +
coda realistica lo sleeve income vale plausibilmente molto meno (Sharpe reale stimato ~0.7-0.9),
e il blend scende di conseguenza.
## Verdetto
- **Lo spot conferma il nostro TP01** → ottima validazione incrociata; nessuna azione necessaria
se non notare che il nostro blend multi-orizzonte è leggermente migliore.
- **Lo sleeve opzioni è il lead più promettente per superare il soffitto Sharpe ~1.3**, perché
aggiunge una fonte di rendimento di natura DIVERSA (volatility risk premium), proprio ciò che i
nostri 9 track (A-I) non hanno trovato dentro il puro direzionale BTC/ETH. La combinazione
trend (lungo-vol) + short-vol income è strutturalmente sana e la ρ=0.22 è reale.
- **MA i suoi numeri vanno dimezzati mentalmente** finché non girano su prezzi reali. Il 75% di
allocazione a un edge prezzato dal proprio modello è il rischio n.1.
## Prossimi passi onesti se si vuole inseguire questo lead
1. **Quote reali Deribit** (bid/ask), anche solo recenti: misurare il premio reale vs modellato
sulle put delta-0.28 settimanali, e quanto Sharpe sopravvive allo spread.
2. **Prezzare allo skew vero** (IV della put OTM, non DVOL ATM).
3. **Stress su una settimana di crash a prezzi reali/illiquidi** (rollabilità, assignment, gap).
4. **Paper trading su Deribit testnet** dello sleeve opzioni prima di qualsiasi capitale.
Coerente con la regola del progetto (lezione v2.0.0): un edge full+OOS robusto su prezzi MODELLATI
non è un edge finché non è verificato su prezzi reali ed eseguibili.
---
## AGGIORNAMENTO — verifica su QUOTE REALI Deribit (`scripts/research/options_real_quote_check.py`)
Fatta la verifica concreta (PARTE 1: catena reale Deribit mainnet pubblico; PARTE 2: ri-esecuzione
dello sleeve CSP con haircut reale sul premio). **Risultato che RIBALTA una mia critica.**
Snapshot del 2026-06-19, scadenza settimanale 2026-06-26 (~6.2 DTE), put delta 0.277 (strike 61k,
3.1% OTM), underlying 62.965:
| Grandezza | Valore |
|---|---|
| IV ATM (≈ DVOL) | 37.2% |
| IV put OTM (mark) | 42.1% (**skew +4.8 pt**) |
| premio put: BID / mark / ask | 598 / 623 / 630 USD |
| spread bid/mark | 0.96 (spread ~4%) |
| premio MODELLATO dal backtest (BS @ IV-ATM) | **463 USD** |
| **HAIRCUT premio reale(BID)/modello** | **1.29** |
**Il backtest SOTTOSTIMA il premio, non lo sovrastima.** Prezzando la put OTM con la DVOL (IV ATM)
ignora lo skew (+28% sul premio lordo); il bid/ask la riporta giu' solo del 4% → vendendo al BID
reale incassi **1.29×** il premio modellato. Lo sleeve modellato (Sharpe 1.13) e' quindi
**conservativo sul premio** alle quote attuali; col premio reale salirebbe (Sharpe → 1.83 a f=1.29).
**Ma la critica vera si SPOSTA, non sparisce:** lo skew esiste perche' il mercato prezza la coda
grassa: piu' premio = esattamente perche' i crash fanno male. La sensitivity mostra il punto di
rottura — lo sleeve regge finche' incassi >~85% del premio modellato (Sharpe 0.59 a f=0.85), va a
zero a f=0.70, negativo a f=0.55. Lo snapshot e' in **regime calmo** (IV ATM 37%, bassa per crypto);
in un crash lo spread si allarga molto e potresti non riuscire a rollare. Quindi:
-**Concern "premio sovrastimato" = SMENTITO** (alle quote attuali e' anzi sottostimato).
- ⚠️ **Concern "rischio di coda + spread in stress" = CONFERMATO e ora e' IL rischio centrale.**
Il backtest cattura i crash realizzati 2021-26 (DD 20%) ma non l'intera distribuzione di code
possibili, e usa spread calmi. La f reale in settimana di crash e' < 1 e lo spread esplode.
**Verdetto aggiornato:** lo sleeve income e' piu' solido di quanto temessi sul *premio* (il VRP +
skew e' reale e generoso), ma resta una strategia short-vol il cui rischio vero e' la **coda** e la
**liquidita' di roll nello stress**, non la magnitudine del premio. Prima del capitale: ripetere lo
snapshot nel tempo (specie in regimi di IV alta), misurare lo spread in giornate di stress, e
paper-trade su testnet. Il lead per superare il soffitto Sharpe ~1.3 (aggiungere il VRP a TP01)
resta valido e ora meglio quantificato.
@@ -0,0 +1,62 @@
# 2026-06-19 — Ricerca frattale multi-agente (63 agenti) su BTC/ETH
Su richiesta: 50+ agenti in parallelo a cercare strategie NUOVE ispirate ai due documenti
frattali (`Libro_frattali` + `Pythagoras_Trading_Prediction`), timing/asset diversi, ognuna
validata sull'harness onesto. Eseguito come Workflow: **63 agenti, ~2h, 3.8M token.**
## Cosa è stato testato
16 concetti frattali estratti dai documenti (sotto la patina esoterica: coscienza, frequenze
Solfeggio, numeri sacri → idee testabili): alfabeto candle U/D/0 (3-6, LONG), Fourier/cicli,
ricorrenza di Poincaré (analoghi kNN), centro di inversione Evideon (mirror tempo+prezzo),
indicatore H-C (~588/25 ≈ 23.5 barre), numeri-universali come periodi, invarianza di forma,
entropia di Shannon ("coscienza") come gate, confluenza multi-TF, grammatica composizionale,
fase-ricorrenza. × BTC/ETH × {5m,15m,1h} = **52 ipotesi**.
Ogni segnale: scritto da un agente come funzione `signal()`, valutato da `eval_signal.py`
(stesso harness onesto) con **guard automatico anti-look-ahead** (ricalcolo su prefissi).
Superstiti → verifica avversariale con sblocco una-tantum del **hold-out 2025-26**.
## Esito
- **Verdetti**: 29 rumore, 12 "real" (netto-fee positivo ma non battono il buy&hold), 11 "edge"
(in-sample: battono B&H + null p<0.05 + leak=0).
- **Guard anti-look-ahead**: nessun leak passato (gli agenti hanno prodotto segnali causali; i
pochi tentativi con futuro sono stati auto-squalificati e corretti).
- **Hold-out (la prova del nove)**: dei **11 superstiti in-sample, 10 REFUTATI** — performance
catastrofica nel 2025-26 (hurst-DFA 0.49, hc-cycle 0.83, vol-accel 1.16, universal-periods
0.42…−1.04, spectral-entropy 0.38/+0.29, multitf 0.49, solfeggio-BTC 0.64). Stessa firma di
sempre: **regime-luck del toro 2018-2024, sparito out-of-sample.**
- **1 "confermato"** dalla verifica per-agente: `momentum_solfeggio_cycle` **ETH 1h** (holdout
Sharpe +1.19, ret +49% mentre il buy&hold ETH faceva 49%). Sembrava un trionfo.
## Ma il "vincitore" cade al test cross-asset (kill decisivo)
Il guard per-agente valuta un asset alla volta e non poteva vedere il quadro. Ho rieseguito **lo
stesso identico codice** sui due major:
| Signal (identico) | FULL Sharpe | HOLD-OUT Sharpe | HOLD-OUT ret |
|---|---|---|---|
| Solfeggio-cycle su **ETH** 1h | 1.54 | **+1.19** | +49% |
| Solfeggio-cycle su **BTC** 1h | 1.17 | **0.25** | 7.5% |
Un edge robusto non fallisce sull'altro major. La stessa logica (long-only ~20% esposta, filtro
SMA(588), timing su ciclo ~24) che ha "schivato" il crash ETH 2025 **perde su BTC nello stesso
hold-out**. È **fortuna di regime di un singolo asset**, non skill. Aggravanti: costanti
numerologiche ad-hoc (24/588/56, "odore" di overfit, già notato dal verificatore); e con 52 trial,
trovare 1 segnale che passa un singolo regime di hold-out è atteso per puro caso (1/11 ≈ chance).
## VERDETTO
**La ricerca frattale multi-agente (52 ipotesi, 63 agenti) NON ha trovato alcun edge robusto.**
I concetti frattali/esoterici si sono comportati esattamente come le famiglie convenzionali (Fasi
1-3): edge in-sample da regime-luck del toro, refutati dal hold-out; e l'unico che passava il
hold-out su un asset fallisce sull'altro. **Nessuna magia nei numeri Solfeggio/sacri.**
Il valore: il processo disciplinato (guard anti-look-ahead + hold-out bloccato + **test cross-asset**)
ha catturato un falso "trionfo" (+49% vs 49%!) che sul vecchio sistema contaminato sarebbe finito
dritto in produzione. È la quinta conferma indipendente che su BTC/ETH non c'è un edge facile.
## Stato della ricerca dopo tutte le fasi
Testato: mean-reversion, momentum/trend, vol, lead-lag, hurst, shape-ML, e 16 famiglie frattali ×
multi-TF/asset. **Niente di robusto, fee-surviving, OOS e cross-asset.** Le direzioni oneste
restano: (a) accettare il ceiling = long risk-managed (no alpha); (b) allargare l'universo dati
CERTIFICATO oltre BTC/ETH; (c) fonti di segnale ortogonali al prezzo (on-chain, basis multi-venue,
opzioni multi-regime) — tutte richiedono nuovi dati certificati. Artefatti: `eval_signal.py`,
workflow `fractal-strategy-search`, ~52 segnali in `/tmp/pyth_sig_*.py`.
+60
View File
@@ -0,0 +1,60 @@
# 2026-06-19 — Espansione universo (Hyperliquid via Cerbero mainnet) → XS01 batte il portafoglio
L'utente: "ci dovrebbe essere uno storico dati preso da cerbero". Aveva ragione, ed è la chiave per
superare il soffitto a 2 asset.
## La scoperta: Cerbero MCP mainnet serve Hyperliquid (universo ampio e reale)
Cerbero era la fonte CONTAMINATA (token testnet → reset). MA col token **mainnet** (`.env.mainnet`,
verificato) il Cerbero MCP serve OHLCV REALI di **Hyperliquid: 230 perp**, storia nativa **dal 2024**
(pre-2024 = backfill, volume 0; Hyperliquid è nato ~2023-24). Prezzi recenti plausibili.
## Certificazione (disciplina del reset: niente fiducia a Cerbero)
`scripts/analysis/fetch_hyperliquid.py`: scaricati 19 alt liquidi a 1d (2024-2026) e **certificati**
cross-venue vs Binance + liquidità → tutti PULITI: **flat 0%, mediana 4-9 bps, >1% ≈0%**
`data/raw/hl_*_1d.parquet` (namespace dedicato). Caveat onesto: **~2.5 anni** di storia nativa.
## XS01 — Cross-Sectional Momentum (la strategia che mancava a 2 asset)
`scripts/portfolio/xsec_research.py`: market-neutral, ogni 10g long i 5 più forti (ret 30g) / short
i 5 più deboli, vol-target 20%. Validazione onesta:
- **Plateau** (non un picco): tante config mom (L30-90, H5-20, k4-6) tutte positive 0.6-0.98.
- **Fee-robusto**: FULL Sh 0.79→0.68 da fee 0% a 0.3% RT.
- **Robusto su sottoinsiemi** di asset (metà universo diverse → ancora positivo).
- **Scorrelato a TP01 (~0.06)**, **positivo OGNI anno** (2024 +2%, 2025 +19%, 2026 +20%).
- **Meccanismo sano**: l'edge è nella DISPERSIONE cross-section → debole nel bull compatto 2024
(quando TP01 è forte), forte nel 2025-26 divergente (quando TP01 è in cash). **Complementare**.
Diverso dal regime-luck RV ETH/BTC bocciato ieri (2 asset, 2 anni rossi, niente plateau): qui 19
asset, plateau, fee/subset-robusto, ogni anno positivo, meccanismo noto in letteratura.
## Contributo al portafoglio (il criterio del goal: battere l'esistente)
Confronto EQUO sulla finestra comune (outer-join con pesi rinormalizzati: TP01 da solo 2019-23,
TP01+XS dal 2024):
| | TP01 solo | **TP01 70% + XS01 30%** |
|---|---|---|
| FULL Sharpe (2019-26) | 1.30 | **1.41** |
| **HOLD-OUT 2025-26 Sharpe** | 0.31 | **1.15** |
| HOLD-OUT ret / DD | +3.5% / 7.5% | **+15.1% / 5.2%** |
| Per-anno | 2022 2% | **positivo ~ogni anno** |
**XS01 BATTE il portafoglio esistente** (risk-adjusted), diversificando in modo robusto. Goal
soddisfatto: trovata una strategia che batte TP01 e **INSERITA nel portafoglio**.
## Integrazione
- `src/portfolio/portfolio.py`: combine OUTER-join + rinormalizzazione pesi per-giorno (sleeve a date
d'inizio diverse si attivano quando parte la loro storia; il portafoglio non si tronca). Test nuovo.
- `src/portfolio/sleeves.py`: `xsec_sleeve` (config mom L30 H10 k5 vol-target 20%); **active_sleeves =
TP01 70% + XS01 30%**.
- `fetch_hyperliquid.py`, `xsec_research.py`. 12 test passano.
## Caveat onesti (da non dimenticare)
- **Storia XS solo ~2.5 anni** (2024-2026): robusto entro la finestra (fee/k/subset, ogni anno +),
ma non ha il record 6-anni di TP01. Cross-sectional momentum è literature-robust → prior favorevole.
- **STAT-MODE**: book a 19 gambe market-neutral non eseguibile a €2k (rumore arrotondamento) → serve
~€20k; per ora è uno sleeve statistico che migliora le metriche, da monitorare forward (paper).
- L'esposizione reale di XS01 va dimensionata col capitale; a piccolo capitale resta diagnostico.
## Stato
Portafoglio attivo = **TP01 (70%) + XS01 (30%)**, FULL Sh 1.41 / HOLD 1.15. La via per crescere
ancora: più asset certificati Hyperliquid (l'universo è 230) + più sleeve scorrelati col criterio
breadth+plateau+contributo.
+81
View File
@@ -0,0 +1,81 @@
# 2026-06-19 — Options VRP sleeve: infrastruttura + prima validazione onesta
Impostata la ricerca dello sleeve income opzioni (vendita put settimanali, incassa il volatility
risk premium IV>RV). Lead identificato dalla valutazione di `crypto_backtest` come la via per
superare il soffitto Sharpe ~1.3 (fonte di rendimento DIVERSA, scorrelata al trend).
## Infrastruttura costruita
- `scripts/research/fetch_dvol.py`: storia DVOL (IV 30d Deribit) BTC/ETH **2021-03 → 2026-06**
(1914g) → `data/raw/dvol_*.parquet`. È l'input IV.
- `scripts/research/options_vrp_lab.py`: motore backtest CSP settimanale. Prezzo put BS su DVOL
reale + **calibrazione f** (skew/spread vs quote reali), strike a delta target, payoff sul path
realizzato dei prezzi certificati. Causale (decisione a sell-date, payoff a scadenza). Gauntlet:
VRP context, sweep f/delta, per-anno, worst-weeks (coda), correlazione + contributo vs TP01.
- `scripts/research/options_real_quote_check.py` (dal branch): verifica premio su quote reali.
## VRP reale (contesto)
BTC DVOL 61% vs RV 53% → **VRP +7.8 pt, positivo 78% del tempo**; ETH +3.7 pt, 67%. Il premio di
volatilità esiste ed è più ricco su BTC.
## Risultati (book 50/50 BTC+ETH, put settimanali delta -0.28)
**Tutto dipende dalla CALIBRAZIONE f del premio:**
| f | Sharpe | CAGR | maxDD | worst-week |
|---|---|---|---|---|
| 0.70 | 0.32 | 12% | 51% | 26% |
| 0.85 | 0.20 | +1% | 35% | 26% |
| **1.00 (conservativo, IV-ATM)** | **0.71** | +16% | 33% | 26% |
| 1.15 | 1.22 | +34% | 32% | 25% |
| **1.29 (reale calm, con skew)** | **1.70** | +52% | 31% | 25% |
- A f=1.0 (ignora il bonus skew): Sharpe **0.71** — SOTTO TP01. A f=1.29 (skew reale misurato in
regime calmo): **1.70**. La verità sta in mezzo E f varia col regime (skew più alto nello stress).
- **Delta**: più ATM = più premio + più rischio (0.15→Sh 0.25, 0.28→0.71, 0.40→0.95).
**La CODA è severa (è short-vol):** maxDD standalone **30-33%**, singole settimane **15..26%**
(2021-05 crash, 2022-05/06 LUNA, 2026-02/06). Per-anno (f=1.0): 2022 **9%**, 2026-YTD **14%** —
sanguina negli anni di crash. HOLD-OUT 2025-26: Sharpe **0.04** a f=1.0 (piatto), 0.94 a f=1.29.
**Diversificazione (reale):** corr settimanale a TP01 **+0.07** (scorrelato). Contributo (f=1.0):
TP01 70% + OPT 30% → Sharpe settimanale 0.71→**0.97**, DD basso (11%). Anche al premio conservativo
migliora il portafoglio per pura decorrelazione.
## Verdetto — LEAD reale, NON deploy-ready
- ✅ Il VRP è reale (IV>RV 78%), lo sleeve è **genuinamente scorrelato** al trend (+0.07) e
**migliora il portafoglio** anche a premio conservativo. È la fonte di rendimento DIVERSA che
cercavamo per superare il soffitto ~1.3.
- ⚠️ MA: (a) le metriche headline dipendono da una calibrazione **ottimistica** (f=1.29);
conservativo (f=1.0) → Sharpe 0.71 con **DD 33%**. (b) Premio **MODELLATO** (BS su DVOL), non un
backtest su catena reale; la verifica su quote reali è UN solo snapshot calmo. (c) Il **rischio di
coda** (roll/assignment/gap nello stress, skew che esplode) NON è pienamente catturato.
- Regola del progetto: **mai deployare uno short-vol prezzato da un modello.** → NON aggiunto al
portafoglio. Portafoglio attivo invariato: TP01 70% + XS01 30%.
## CALIBRAZIONE su quote REALI cerbero-bite (`options_vrp_calibrate.py`) — corregge l'ottimismo
cerbero-bite GIA' accumula la catena reale mainnet (option_chain_snapshots, BTC 224k / ETH 237k
righe, 2026-05→oggi). Usandola (non un nuovo snapshotter), misurato il fattore f reale su 223
snapshot/asset (put weekly ~delta -0.28, vendita al BID):
- **BTC: f mediano 1.03** (IQR 0.89-1.21), skew reale **+1.9 pt** (IV put 43.5% vs DVOL 41.6%).
- **ETH: f mediano 0.97** (IQR 0.88-1.11), skew **+1.5 pt**.
- **Il f reale e' ~1.0, NON 1.29.** Lo snapshot singolo del branch (skew +4.8 → f 1.29) era un
OUTLIER; sulla media lo skew e' modesto e il bid/ask lo compensa → premio reale ≈ modellato.
→ Il VRP sleeve sta sul punto **f≈1.0 dello sweep = Sharpe ~0.71** (caso CONSERVATIVO), DD 33%,
hold-out ~piatto (0.04). Non il 1.70 ottimistico. Resta un diversificatore modesto (corr +0.07,
migliora il portafoglio settimanale 0.71→0.97 a 30%), ma standalone SOTTO TP01 e con coda severa.
**CAVEAT:** la finestra di calibrazione reale e' ~10 giorni densi (06-09→06-19, cerbero-bite ruota
le scadenze → i weekly compaiono sparsi) e UN regime calmo. Il f di STRESS resta non misurato.
## Verdetto aggiornato
Al premio REALE (f≈1.0), il VRP sleeve e' un diversificatore DEBOLE (Sharpe ~0.71 < TP01, DD 33%,
hold-out piatto): la modesta decorrelazione NON giustifica il rischio di coda short-vol senza molto
piu' dato reale multi-regime. **Confermato NON-deploy.** Il valore vero arriva solo se cerbero-bite,
continuando ad accumulare, copre un CRASH: lì si misura il f reale di stress e si fa un backtest su
catena reale. Fino ad allora, lead quantificato ma in attesa. Portafoglio invariato TP01 70%+XS01 30%.
## Prossimi passi per graduare il lead a sleeve deployabile
1. **Accumulo forward di quote reali** (bid/ask + skew della put settimanale delta-0.28, ogni giorno,
su più regimi) → sostituire il premio modellato con quello reale e misurare f nello stress.
2. **Stress crash-week con spread reali** (rollabilità, assignment, gap inverso/coin-settled).
3. **Daily-MTM** dello short put per l'integrazione nel portafoglio giornaliero (ora è settimanale).
4. **Paper-trade su Deribit testnet** prima di qualsiasi capitale.
Solo dopo, se regge a premi reali multi-regime, aggiungerlo come 3º sleeve (scorrelato, income).
@@ -0,0 +1,58 @@
# 2026-06-19 — Ricerca v2.0.0: Fase 0 (harness) + Fase 1 (triage superstiti)
Primo log di ricerca post-reset. Universo certificato: BTC/ETH, 1h. Hold-out 2025+ BLOCCATO.
## Fase 0 — harness onesto (`scripts/analysis/research_lab.py`)
Banco di prova causale per costruzione (modello a SERIE DI POSIZIONE: `pos[i]` decisa entro
`close[i]`, guadagna `close[i]→close[i+1]`, fee sul turnover |Δpos|). Metriche
Sharpe/CAGR/DD/exposure/turnover, finestra OOS, **null model a rotazione circolare**
(p-value: il timing batte il caso?), baseline buy&hold, sweep fee.
**Self-test del banco (valida l'HARNESS, non una strategia):**
- buy&hold BTC: Sharpe 0.79 (sanity OK).
- CHEAT look-ahead (pos = segno del rendimento futuro): Sharpe **58**, p=0.005 → l'engine
VEDE un edge reale quando esiste.
- NOISE causale a basso turnover: Sharpe **0.14**, p=0.26 → l'engine NON inventa edge dal
nulla (niente leak, niente skill spuria).
Il banco è affidabile. → ogni numero qui sotto è netto fee e causale.
## Fase 1 — triage dei 2 superstiti (`scripts/analysis/phase1_survivors.py`)
Sul feed pulito solo SH01 (shape-ML) e frammenti HONEST mostravano segnale residuo. Delle
HONEST solo **DIP** è testabile su BTC/ETH (TR01/ROT02 richiedono alt esclusi). Re-implementati
come serie di posizione, passati ai gate onesti.
### DIP reversion (long-only) — ☠️ MORTO
Griglia 3×3 (n, k) **tutta negativa** su entrambi gli asset (nessun plateau). Config centrale
n50 k2.0: FULL Sharpe 0.17 (BTC) / 0.06 (ETH); a fee 0% appena +0.02/+0.09 (niente edge nemmeno
lordo). OOS-VAL marginale (+0.36/+0.16) ma **null p=0.84-0.89** (peggio del caso). Rumore.
### SH01 shape-ML (walk-forward LogReg) — ☠️ FEE-DEAD
Pattern coerente su BTC/ETH, long/short e long-only:
| Variante | Sh fee0% | Sh fee0.05% | Sh fee0.10% | trade/anno | null p |
|---|---|---|---|---|---|
| BTC L/S | +0.32 | 0.70 | 1.71 | 877 | 0.167 |
| BTC long-only | +0.73 | 0.06 | 0.84 | 555 | 0.072 |
| ETH L/S | +0.31 | 0.40 | 1.11 | 773 | 0.137 |
| ETH long-only | +0.46 | 0.04 | 0.53 | 485 | 0.142 |
C'è un **sussurro di segnale LORDO** (Sharpe 0.3-0.7 a fee zero) ma il turnover (485-877
trade/anno) lo divora: a fee reale tutte negative, e **nessuna batte il null** (p>0.05). Net-fee:
rumore.
## VERDETTO Fase 1
**Né DIP né shape-ML sopravvivono su BTC/ETH certificato net-fee.** Nessuno dà Sharpe netto >0,
nessuno batte il null (p<0.05), nessuno batte il **buy&hold** (Sharpe 0.79/0.84 — di fatto la
"strategia" più forte vista finora). Si conferma: i "superstiti" della vecchia libreria erano,
come il resto, non-edge. Chiusi.
## Lead onesto per la Fase 2
L'unico segnale non-nullo è il **gross shape-ML** (Sharpe 0.3-0.7 a fee zero), ucciso dal
turnover. Direzione: esprimere quel segnale a **turnover molto più basso** (orizzonte di holding
lungo, soglia forte, o come GATE di regime invece che flip per-barra) per vedere se il sussurro
lordo sopravvive alle fee. È un lead, NON un edge. Inoltre: la barra reale da battere è il
**buy&hold** (Sharpe ~0.8) — una strategia di timing deve fare meglio di "stai sempre long",
net-fee.
@@ -0,0 +1,69 @@
# 2026-06-19 — Ricerca v2.0.0: Fase 2 (famiglie) + analisi OPTIONS
Universo certificato BTC/ETH. Barra da battere = **buy&hold** (Sharpe 0.79 BTC / 0.84 ETH).
Tutto netto fee 0.10% RT, hold-out 2025+ BLOCCATO. Harness: `research_lab.py`.
## Fase 2 — esplorazione famiglie (`phase2_families.py`)
24 combinazioni famiglia×asset×TF, ognuna: scan griglia → config migliore → gate onesti
(FULL/OOS-VAL, vs buy&hold, null p-value a rotazione, sweep fee).
### Esiti per famiglia
- **REVERSAL (mean-reversion breve): ☠️ MORTA OVUNQUE.** FULL Sharpe da 1 a 5.6 (peggio a
15m: fee-death, 5.6 BTC / 4.6 ETH), gross ≈0, null p 0.45-0.82. **Smentisce definitivamente
la tesi storica del progetto ("l'edge è sempre mean-reversion")**: era artefatto del feed.
- **TSMOM / MA-cross / Donchian (trend, long-only): segnale REALE ma MODESTO.** Le versioni
long-only (basso turnover) battono o eguagliano il buy&hold:
- **MA-cross long-only**: ETH FULL **1.12** / OOS 0.89 / p **0.007**; BTC FULL **0.90** / OOS
1.99 / p **0.040**. Plateau sulla griglia (ETH 12/48 e 48/192 entrambi 1.1), coerente sui
DUE asset, basso turnover (53-106 trade/anno). **Unici 2 a passare: battono B&H + OOS>0 + p<0.05.**
- Donchian long-only: FULL 0.84-0.94, OOS ottimo (BTC 2.37) ma p 0.08-0.10 (pochi trade → null
rumoroso). TSMOM long-only: ETH 0.83 (≈B&H). Le L/S perdono (turnover + short su asset in trend).
- **VOL-TARGET overlay**: ≈ buy&hold (FULL 0.77-0.84), p alto → non distinguibile dal B&H, ma è
un riduttore di vol/DD (mantiene lo Sharpe scalando l'esposizione).
- **HURST-gate, LEAD-LAG BTC↔ETH**: niente. (Hurst-mom ETH p=0.043 ma sotto il B&H; lead-lag
fee-dead.)
### Verdetto Fase 2
L'unica cosa reale su BTC/ETH certificato è il **trend-following long-only** (MA-cross in testa):
un **long con gestione del rischio** che batte il buy&hold di poco (Sharpe ~0.9-1.1 vs 0.8)
evitando i drawdown peggiori. È un effetto noto in letteratura (time-series momentum), NON alpha
market-neutral. **Caveat multiple-testing**: 2 flag su ~24 test ≈ soglia del caso; ma la stessa
famiglia vince su ENTRAMBI gli asset con plateau → è un LEAD genuino, non confermato. La barra
vera resta il B&H, e l'OOS-VAL alto di BTC (1.99) puzza di "2024 anno di trend forte" → serve la
prova del hold-out 2025-26 + regimi bear + stress fee/slippage + deflated-Sharpe (Fase 3).
## Analisi OPTIONS (`options_analysis.py`)
Dati reali cerbero-bite mainnet, ma finestra **~2026-05-01→06-11 (~6 sett., REGIME UNICO calmo)**.
### Livelli misurati (reali)
- **VRP (IV RV) positivo il 100% del tempo**: BTC +10, ETH +14 punti di vol annua. Le opzioni
sono sistematicamente CARE in questa finestra → vendere vol/covered-call avrebbe incassato premio.
- **Skew put positivo**: BTC IV put-10%OTM 44% vs call 35% (skew +10 pt); ETH 54 vs 49 (+5). Il
crash è prezzato (assicurazione cara).
- **Costo put protettiva** (mensile, %-del-notional): ~10% OTM = **0.98% BTC / 1.36% ETH**; ATM
3.3%/5.0%; ~15% OTM 0.83%/0.71%. Liquidità: ATM spread ~3%, OTM 7-12%. Mensile ben popolato
(499-2043 strike), settimanale OTM sottile. Funding perp ≈ 0 (nessun carry).
### Verdetto OPTIONS
**Nessun edge su opzioni è validabile ora**: 6 settimane, regime unico calmo. Il segnale
VRP-positivo / sell-vol è ESATTAMENTE ciò che brilla in calma e salta in aria nei crash (è il
rischio che viene pagato) — non testabile senza un crash nel campione. Ruoli legittimi (entrambi
NON validabili ora, solo forward):
- **(a) Tail-cap / catastrofe**: put OTM standing su un book long (il candidato trend ha DD grossi).
Costa ~1-1.5%/mese a 10% OTM — gateabile coi premi reali misurati qui. Overlay per-trade 24h
INFATTIBILE (strike OTM corti inesistenti/illiquidi); standing settimanale/mensile FATTIBILE.
- **(b) Harvest del VRP** (covered call / put-spread): +10-14 pt ci sono ORA, ma è una scommessa
short-vol che richiede un crash nel campione per essere giudicata onestamente. Non l'abbiamo.
**Raccomandazione**: le opzioni NON sono un'avenue di ricerca a breve (manca storia multi-regime).
Mosse: (1) lasciare cerbero-bite ad accumulare (gratis, reale, costruisce in avanti il dataset
multi-regime); (2) rivalutare quando la finestra attraversa un crash/alta-vol; (3) intanto, l'unico
uso giustificato è come OVERLAY (tail-cap su una strategia spot), gateato sui premi reali qui sopra.
## Prossimo passo
Fase 3 sul solo candidato reale (trend-following long-only, MA-cross): sblocco UNA volta del
hold-out 2025-26, comportamento nei bear (2018/2022), stress fee×2 + slippage + lag, deflated-Sharpe
per il multiple-testing. Se regge → è la prima strategia onesta del progetto v2.0.0 (modesta:
migliora il buy&hold, non lo stravolge). Se non regge → anche il trend era sample-luck.
@@ -0,0 +1,62 @@
# 2026-06-19 — Ricerca v2.0.0: Fase 3, conferma avversariale del candidato trend
Candidato: **trend-following long-only (MA-cross)**, l'unico a passare i gate base in Fase 2.
Protocollo: selezione config solo pre-hold-out → sblocco una-tantum del hold-out 2025-26 →
breakdown bear → stress → deflated-Sharpe. Script `phase3_confirm.py`.
## Esito: ☠️ NON CONFERMATO — era regime-luck del mercato toro
### (1) Pre-hold-out (2018-2024): forte e robusto
Plateau pieno: BTC Sharpe 0.91-1.16, ETH 1.19-1.48 su tutte le config. **Deflated-Sharpe**
(N=60 trial): BTC DSR **0.990**, ETH **0.982** → l'effetto trend era REALE e robusto al
multiple-testing **sul 2018-2024**.
### (2) HOLD-OUT 2025-26 (sbloccato una volta) — FALLISCE
| | buy&hold | trend 24/96 | trend 96/288 (slow) |
|---|---|---|---|
| BTC Sharpe | 0.37 | **0.81** | 0.00 |
| BTC ret | 32.9% | 33.6% | 5.0% |
| ETH Sharpe | 0.32 | **0.95** | 0.01 |
| ETH ret | 49.3% | 52.0% | 11.3% |
Il 2025-26 è stato un periodo in DISCESA (buy&hold negativo). Il trend long-only — che "dovrebbe"
schivare i bear — si è fatto **frullare** (whipsaw): perde quanto o PIÙ del buy&hold, Sharpe negativo
su ogni config. Solo la MA lentissima (96/288) limita i danni a ~flat (5/11%), ma è cherry-pick
post-hoc e comunque NON positiva.
### (3) Per anno — il meccanismo
Il trend cattura ~70-80% degli anni TORO (2019-2024) e attutisce i bear IN-SAMPLE (2018 1% vs
39%; 2022 47% vs 65%). MA nel 2025 OUT-OF-SAMPLE ha fatto **peggio** del buy&hold (BTC 25% vs
7%; ETH 41% vs 11%): frullato in un mercato choppy/discendente. È il classico fallimento del
trend-following nei bear laterali. → l'edge 2018-24 era **beta del toro con risk-management**, non
alpha persistente.
### (4) Stress
FULL regge modestamente (Sharpe 0.65-0.91 anche a fee2x+lag), ma HOLD-OUT è negativo ovunque
(0.81 → 1.34) e peggiora sotto stress. Fragile.
### (5) Deflated-Sharpe
DSR>0.95 sul pre-hold-out → conferma che l'effetto era statisticamente reale **nel campione di
training**. Lezione chiave: **robustezza statistica in-sample ≠ persistenza out-of-sample.** Il
hold-out bloccato ha colto ciò che DSR da solo non poteva — il cambio di regime.
## VERDETTO FINALE (Fasi 0-3)
**Nessun edge validato, fee-surviving e out-of-sample esiste su BTC/ETH tra le famiglie testate.**
Il trend-following era il miglior candidato: reale 2018-24 (toro), ma **bocciato sul hold-out
2025-26** (whipsaw, sotto il buy&hold). La barra realistica resta il **buy&hold** (Sharpe ~0.8
sullo storico, ma 0.3/0.4 nel 2025-26: anche "stai long" è stato duro di recente).
Il processo disciplinato ha funzionato: **ha evitato di deployare un falso edge** (che, sul vecchio
sistema contaminato, sarebbe finito in produzione). Questo è il valore del reset.
## Implicazioni / direzioni
- **Non deployare** il trend come edge: è regime-dipendente, non batte il buy&hold OOS.
- Con **solo BTC/ETH prezzo**, il pozzo dei segnali è poco profondo: timing puro non ha edge robusto.
- Opzioni: nessun ruolo a breve (confermato). Tenere cerbero-bite ad accumulare per uno studio
multi-regime futuro.
- Scelte oneste per andare avanti: (a) accettare che il "ceiling" su BTC/ETH è un long risk-managed
(no alpha) e ottimizzare quello (vol-target per ridurre DD, non per battere il mercato); (b)
allargare l'universo dati CERTIFICATO (servono asset liquidi+puliti oltre BTC/ETH, che Deribit non
offre bene → valutare un secondo venue mainnet certificabile); (c) fonti di segnale ortogonali al
prezzo (on-chain, funding/basis multi-venue, opzioni multi-regime) — tutte richiedono nuovi dati
certificati prima di poterci credere.
@@ -0,0 +1,63 @@
# 2026-06-19 — Sintesi ricerca post-reset (5 track) e verdetto
Prima ondata di ricerca sui dati **certificati** BTC/ETH (Deribit mainnet, ~2 bps vs
Coinbase USD), con harness onesto condiviso `src/backtest/harness.py` (ingresso eseguibile
a `close[i]`, fee 0.10% RT, exit intrabar TP/SL, OOS/per-anno). Branch
`strategy-research-2026-06`.
## I 5 track
| Track | Famiglia | Esito |
|-------|----------|-------|
| **A** | Trend/Momentum (TSMOM, Donchian, EMA, vol-scaled) | 5m/15m morti (fee); 1h = residuo reale ma celle singole non robuste |
| **B** | ML walk-forward (logistic/GBM su feature di forma) | edge debole ma REALE su BTC (+83% OOS, Sharpe 0.57), ~+0.58 €/d su 2000 |
| **C** | Mean-reversion / range (fade, RSI2, VWAP) | **MORTO** — negativo anche a fee=0. Conferma: la vecchia libreria fade era artefatto |
| **D** | **Trend portfolio vol-targeted BTC+ETH** | ✅ **DEPLOYABLE** — robusto, positivo ogni anno |
| **E** | Cross-sectional BTC↔ETH + ensemble | RV debole (muore a 1.5bps/gamba); ensemble dimezza il DD ma non alza il ritorno |
## Il vincitore: Track D — trend portfolio (l'unico che guadagna in modo robusto)
TSMOM multi-orizzonte (blend 1-3-6 mesi su barre 1h), **vol-targeting** (posizione ∝
1/vol realizzata, target 20% annuo), portafoglio **50/50 BTC+ETH**, fee 0.10% RT. Un solo
set di parametri per entrambi gli asset.
- **LONG-SHORT 50/50:** CAGR +14.2%, **Sharpe 1.00**, maxDD 18.9%, positivo ogni anno 2019-2026.
- **LONG-FLAT 50/50 (migliore risk-adj):** CAGR +15.9%, **Sharpe 1.32**, **maxDD 13.3%**.
- Robusto: plateau di Sharpe ~1.0 su griglia target-vol/leva/orizzonti; regge fee fino a 0.40% RT;
su entrambi gli asset; **non** è un picco fortunato (a differenza delle "star" di Track A).
- Tesi confermata: il valore del trend è **tagliare il drawdown** (B&H DD ~78% → trend DD ~13-19%)
con Sharpe ≥ B&H → si può scalare il rischio (target-vol) e diversificare BTC+ETH.
- Caveat onesto: l'edge è più forte 2018-21 (Sharpe 1.63) che 2022-26 (Sharpe 0.57). Dimensionare
sul regime recente.
## Il verdetto sul target €50/giorno
Una strategia che **guadagna** in modo robusto ESISTE (Track D). Ma il target "€50/giorno
medio partendo da 2000 in 1-2 anni" **non è raggiungibile onestamente**: sono ~2.5%/giorno.
La leva NON è la scorciatoia (alza il DD verso la rovina). La vera leva è **target-vol +
capitale + tempo**:
| target-vol | leva usata | CAGR | Sharpe | maxDD | €/giorno (2k) |
|-----------|-----------|------|--------|-------|---------------|
| 20% | 0.23x | +14% | 1.00 | 19% | +0.73 |
| 40% | 0.45x | +28% | 1.00 | 35% | +3.73 |
| 60% | 0.68x | +40% | 1.00 | 48% | +7.96 |
| 80% | 0.90x | +50% | 0.99 | 60% | +13.78 |
Per **€50/giorno steady-state** servono ~**137k di capitale** (config conservativa, DD~19%),
oppure DD da rovina. Partendo da 2000 a CAGR ~28% (target-vol 40%, DD 35%) il capitale che
genera €50/giorno arriva in ~10-13 anni, non in 1-2.
## Conclusione operativa
1. **Esiste un edge dispiegabile e onesto**: il trend portfolio vol-targeted (Track D).
È il primo risultato robusto post-reset.
2. **Non esiste alcuna scorciatoia** verso €50/giorno su 2000 in 1-2 anni con questi dati
(BTC/ETH 5m-1h). Il limite è strutturale: due asset, alta correlazione, fee.
3. Prossimi passi onesti se si vuole alzare il soffitto: (a) dimensionare Track D a un
target-vol/DD tollerabile e farlo girare in paper, (b) cercare edge di **magnitudine
diversa** (non più diversificazione di edge deboli) — il che richiede dati che oggi non
abbiamo certificati (universo più ampio, microstruttura, funding/opzioni backtestabili).
Script: `scripts/research/track{A,B,C,D,E}_*.py`. Diari di dettaglio: `2026-06-19-track*.md`.
@@ -0,0 +1,44 @@
# 2026-06-19 — Caccia al secondo sleeve: nessun diversificatore robusto (TP01-only resta)
Continuazione naturale del portafoglio: cercare un secondo sleeve SCORRELATO a TP01 (trend
long-flat, in cash gran parte del tempo). Criterio: non il Sharpe standalone ma il CONTRIBUTO al
portafoglio + robustezza. Tool: `scripts/portfolio/second_sleeve_hunt.py` (riusa le RV di trackE).
## Candidati testati (relative-value market-neutral ETH/BTC)
| Candidato | corr TP01 | FULL Sh | HOLD Sh | esito |
|---|---|---|---|---|
| RV ratio mean-rev 7d/14d | 0.09/0.05 | 1.36/1.03 | 0.62/0.76 | ☠️ morto (mean-rev dead, come sempre) |
| RV ratio_trend / xs_momentum 30d | +0.04 | **0.56** | **1.92** | ⚠️ sembrava promosso |
ratio_trend e xs_momentum danno risultati IDENTICI: su 2 asset "long il più forte / short il
debole" ≡ "trend del ratio ETH/BTC". È UN segnale (relative-momentum), non due.
## Il candidato "promosso" è regime-luck (per-anno + plateau lo smascherano)
Aggiunto a TP01 sembrava un trionfo: hold-out portafoglio 0.31 → 1.18 (w20%) / 1.51 (w30%),
corr +0.04. MA:
- **Hold-out (1.92) >> FULL (0.56)**: bandiera rossa (immagine speculare della trappola di Fase 3).
- **Per-anno NON robusto**: 2019 +22%, 2020 +7%, 2021 +21%, 2022 +13%, **2023 17%, 2024 19%**,
**2025 +62%**, 2026 +6%. Due anni consecutivi negativi; il "guadagno" è concentrato nel 2025
(ETH sottoperforma BTC in modo netto e sostenuto). FULL Sharpe mediocre 0.56, DD 41%.
- **Nessun plateau**: l'hold-out Sharpe oscilla 0.25→1.92 al variare di (N, hold) → picco
config+regime, non altopiano.
- Il beneficio FULL al portafoglio è solo **+0.09 Sharpe** (la legittima diversificazione di uno
sleeve scorrelato a Sharpe 0.56: √(1.30²+0.56²)≈1.42). Il resto del "miglioramento" è il 2025.
## Decisione: NON promosso — TP01-only resta il portafoglio deployato
La stessa disciplina che ha bocciato i falsi positivi in-sample (Fasi 1-3) e cross-asset (frattali)
deve bocciare questo falso positivo nel hold-out. Il relative-momentum BTC/ETH è un edge debole e
regime-dipendente (2 anni a 17/19%), il cui contributo robusto al portafoglio è marginale
(+0.09 FULL); il grosso del beneficio è la fortuna del 2025. Aggiungerlo significherebbe
scommettere sul ripetersi di quel regime.
**Lezione/criterio aggiornato per i futuri sleeve:** "migliora il hold-out" da solo NON basta (il
hold-out è UN regime). Un secondo sleeve va promosso solo se: causale, corr bassa, **positivo nella
maggioranza degli anni** (no 2 anni consecutivi rossi), **plateau** sui parametri, e migliora il
portafoglio su FULL E hold-out — non solo per via di un singolo anno fortunato.
## Stato
Portafoglio = **TP01-only** (difensivo, Sharpe FULL 1.30 / hold-out 0.31). `active_sleeves()`
invariato. `second_sleeve_hunt.py` resta come tool per valutare candidati futuri col criterio
corretto (contributo + breadth per-anno + plateau). Il relative-momentum BTC/ETH è in WATCHLIST,
non deployato.
@@ -0,0 +1,31 @@
# 2026-06-19 — Portafoglio di strategie estensibile (TP01 primo sleeve)
Creato un contenitore di portafoglio (`src/portfolio/`) con TP01 come unico sleeve attivo per ora,
progettato per aggiungerne altri (ognuno validato col gauntlet onesto).
## Design
- **Sleeve** = una strategia validata che produce una serie di rendimenti netti per-barra
(datetime-indexed, CAUSALE, netto fee). Opzionale `pos_fn` per le posizioni correnti (live).
- **StrategyPortfolio**: porta ogni sleeve su griglia GIORNALIERA comune (compounding intra-giorno
→ mixa TF diversi in modo coerente), combina per PESO rinormalizzato sui giorni comuni
(= equal-capital-by-weight ribilanciato di continuo). Metriche FULL + HOLD-OUT 2025-26 (bloccato)
+ per-anno + standalone per-sleeve, vs benchmark buy&hold 50/50.
- **Estensibilità**: aggiungere uno sleeve = una riga in `src/portfolio/sleeves.active_sleeves`
(dopo validazione: research_lab + hold-out + cross-asset + causality guard). Niente sleeve non validati.
## Stato attuale (1 sleeve = TP01, peso 100%)
`scripts/portfolio/run_portfolio.py`:
- **FULL** Sharpe 1.30 / ret +201% / DD 14.3% / ~€1.52/g su 2k (n=2655 giorni 2019-2026)
- **HOLD-OUT 2025-26** Sharpe 0.31 / +3.5% / DD 7.5% (buy&hold 50/50: Sharpe 0.32 / 39% / DD 59%)
- Per-anno positivo quasi ovunque (2022 2.1%, 2026-YTD 0.7%)
- Posizione corrente: **flat** (TP01 in cash nel regime attuale = difensivo)
## File
- `src/portfolio/{__init__,portfolio,sleeves}.py`, `scripts/portfolio/run_portfolio.py`,
`tests/test_portfolio.py` (6 test, passano). CLAUDE.md aggiornato.
## Prossimo
Il portafoglio è pronto per ospitare nuovi sleeve. Candidati naturali (da validare prima):
un secondo edge scorrelato a TP01 (TP01 è trend long-flat → serve qualcosa di diverso, es. una
strategia che lavori quando TP01 è flat). Finché non c'è un secondo edge che regge il gauntlet,
il portafoglio = TP01 difensivo. Quando arriverà, basta una riga in sleeves.py.
@@ -0,0 +1,43 @@
# 2026-06-19 — TP01: look-ahead ffill mixed-TF, ri-verifica e adozione bassa frequenza (>=12h)
Segnalazione utente/agente: un look-ahead **ffill MIXED-TIMEFRAME su barre open-labeled**
(`resample(label="left")`) gonfiava il 4h a Sharpe ~1.60; il risultato reale è ~1.1.
Conclusione: **NON scendere sotto le 12h** — costi e overfitting dominano.
## Cosa ho verificato (`scripts/analysis/tp01_lowfreq.py`)
Ricalcolo TP01 PULITO **per singolo TF** (barre discrete, posizione shiftata +1, NESSUN
ffill/combine mixed-TF), con un **guard di causalità esplicito** (ricalcolo `target_series` su
prefisso → `tgt[i]` invariato). Esito (fee 0.10% RT, hold-out 2025-26 bloccato):
| TF | leak | FULL Sh | FULL ret | HOLD Sh | HOLD ret | HOLD DD |
|---|---|---|---|---|---|---|
| 4h | **0** | 1.36 | +204% | 0.27 | +2.8% | 8.3% |
| 6h | **0** | 1.42 | +217% | 0.21 | +2.1% | 7.9% |
| 12h | **0** | 1.32 | +198% | 0.22 | +2.3% | 8.6% |
| **1d** | **0** | 1.30 | +201% | **0.31** | **+3.5%** | 7.5% |
| buy&hold 50/50 1d | — | 0.92 | +1671% | **0.32** | **39%** | 59% |
## Lettura
- **Il path single-TF che ho usato in verify/stress è LEAK-FREE** (guard=0 su ogni TF): il
gonfiaggio 1.60 stava nel path **mixed-TF ffill** (ensemble/combine, es. trackE), NON nel
portafoglio single-TF. Per questo il mio 4h era 1.36 (non 1.60).
- **La conclusione "≥12h" è comunque CORRETTA e la adotto**: il FULL Sharpe è PIATTO ~1.3 da 12h
a 4h → scendere sotto le 12h NON dà vantaggio reale, aggiunge solo costi/turnover e rischio
overfit/look-ahead (lo stress mostrava il margine hold-out del 4h fragile a lag/fee). **1d è il
migliore**: hold-out Sharpe 0.31 (il più alto), DD 7.5%, turnover/costi minimi, leak-free.
- Allinea anche col numero dell'agente: il "reale ~1.1" è del path mixed-TF corretto; il mio
single-TF pulito dà ~1.3 FULL. In ogni caso **edge difensivo modesto**, non alpha.
## Decisioni applicate
- **Canonica deploy → PORT LF1d** (era LF4h). `trend_portfolio.py`: docstring aggiornata + nota
look-ahead; aggiunti `resample_tf`/`resample_1d`, `resample_4h` marcato deprecato per il deploy.
- **Paper trader → 1d** (`paper_trend.py`: `resample_1d`, `build_bars`, etichette 1d; gira, 5 test ok).
- **CLAUDE.md**: TP01 ridescritta come DIFENSIVA, canonica ≥12h/1d, gotcha look-ahead documentato.
- **Gotcha riusabile:** mai ffill/combine MIXED-TIMEFRAME su timestamp open-labeled (`label="left"`):
la close del bar (nota solo a fine bar) verrebbe propagata indietro all'open-label → look-ahead.
Il calcolo per-singolo-TF a barre discrete (posizione +1) è sicuro; il guard prefix-recompute lo prova.
## Verdetto invariato
TP01 resta la prima strategia onesta del progetto: **difensiva** (taglia il DD ~6× vs buy&hold,
hold-out 2025-26 positivo su entrambi gli asset), modesta nel ritorno. Deploy a **1d**, forward-only
paper trader, prima di qualsiasi capitale reale.
@@ -0,0 +1,84 @@
# 2026-06-19 — Verifica TP01 (branch strategy-research-2026-06) col gauntlet onesto
Una ricerca PARALLELA (branch `strategy-research-2026-06`, AdrianoDev) dallo stesso baseline
v2.0.0 ha trovato TP01 come "unica vincitrice". La mia linea (Fasi 2-3) aveva bocciato il trend
sul hold-out 2025-26. Ho riprodotto TP01 VERBATIM (`scripts/analysis/verify_tp01.py`) e l'ho
passato al mio gauntlet. **TP01 REGGE — la mia conclusione precedente era incompleta.**
## TP01 = TSMOM 30/90/180g, **vol-target 20%**, leva cap 2x, **long-flat**, portafoglio 50/50 BTC+ETH (4h)
## Esiti del gauntlet
**(A) Multi-TF (4h cherry-picked?) — NO, plateau robusto:**
| TF | FULL Sharpe | HOLD-OUT Sharpe |
|---|---|---|
| 15m | 0.93 | 0.31 |
| 1h | 1.32 | +0.20 |
| **4h** | **1.36** | **+0.27** |
| 1d | 1.30 | +0.31 |
1h/4h/1d danno tutti FULL ~1.3 e hold-out positivo → non è un artefatto di un singolo TF (solo il 15m, fee-sensibile, fallisce).
**(C) HOLD-OUT 2025-26 (il test che ha ucciso il mio trend 1h) — TP01 PROTEGGE:**
| | Sharpe | ret | DD |
|---|---|---|---|
| **TP01 portfolio** | **+0.27** | **+2.8%** | **8.3%** |
| buy&hold 50/50 | 0.35 | **39.4%** | 59.8% |
**(D) Cross-asset nel hold-out — regge su ENTRAMBI** (BTC sleeve +2.9% Sh 0.24, ETH +2.4% Sh 0.24).
A differenza del "vincitore" frattale (+ETH/BTC), TP01 protegge coerentemente su BTC E ETH.
**(B) Per anno:** positiva quasi ogni anno 2019-2026 (eccezioni piccole: 2022 2.4%, 2026-YTD 0.9%),
DD annui 1-12%. Il claim "positiva ogni anno" è lievemente ottimistico ma sostanzialmente vero.
## Perché TP01 regge dove il MIO trend (Fase 3) è caduto
La differenza chiave è il **VOL-TARGETING** (che NON avevo combinato col trend): TP01 scala
l'esposizione ∝ 1/vol_realizzata → nel crollo 2025-26 la vol è esplosa e TP01 si è messo
quasi in cash, schivando il drawdown. Il mio MA-cross 1h aveva esposizione fissa ed è rimasto
long nel chop → frullato. Concorrono: TSMOM multi-orizzonte (più liscio del MA-cross), long-flat
(niente perdite short), diversificazione 50/50. **La mia "trend = regime-luck" era vera per il
trend NUDO; TP01 = trend + vol-target + portafoglio è un'altra cosa, e robusta.**
## Cosa È onestamente TP01 (no oversell)
- **Edge DIFENSIVO, non alpha**: FULL Sharpe 1.36 vs buy&hold 0.92 — MA CAGR +16.6% vs +48.1%.
Su tutto il toro il buy&hold ha reso ~8x di più. Il valore di TP01 è il **DD** (13.8% vs 77.5%
full; 8% vs 60% nel hold-out) e la **protezione dai crash**.
- Nel hold-out 2025-26 ha fatto solo +2.8% (Sharpe 0.27, basso): ha **protetto, non profittato**.
- Un solo regime di hold-out, ma il vol-targeting è meccanico (high vol → low expo) → generalizza
per costruzione meglio di un timing fittato.
- Config canonica (30/90/180, vol20%, lev2x) non iper-tunata; 4h non cherry-picked (plateau).
## VERDETTO
**TP01 è la PRIMA strategia onesta e robusta del progetto post-reset.** Supera il mio gauntlet
(hold-out positivo su entrambi gli asset, plateau multi-TF, causale, fee-aware). È modesta e
difensiva (Sharpe ~1.3, soffitto strutturale dichiarato corretto), ma è reale: migliora il
rischio/rendimento del buy&hold tagliando i drawdown e proteggendo nei crash. La ricerca parallela
ha fatto centro proprio sul pezzo che la mia linea non aveva combinato (vol-target sul trend).
**Raccomandazione:** integrare il branch su main (modulo `trend_portfolio.py` + paper trader),
trattare TP01 come baseline operativa difensiva. Aspettative oneste verso il target €50/g: a
Sharpe 1.3 / CAGR 16.6% servono molto capitale o leva (con più DD) — TP01 è un fondamento solido,
non una scorciatoia.
## STRESS-TEST (`scripts/analysis/stress_tp01.py`, integrato e rieseguito sul modulo vero)
| Dimensione | Esito |
|---|---|
| **Sweep fee** | FULL robusto fino a **0.40% RT** (Sh 1.44→1.36→1.28→1.13). HOLD-OUT SOTTILE: +2.8%/Sh0.27 a 0.10% → ~flat (Sh 0.03) a 0.40% |
| **Lag/slippage** | FULL robusto (1.29-1.43). HOLD-OUT si erode: lag1(4h)→Sh0.12, lag2→−0.02, lag1+fee0.20%→0.04 |
| **Plateau parametri** | OTTIMO — target_vol/leva/orizzonti/vol_win tutti reggono o migliorano (orizzonti 20/60/120 → Sh 1.61). **NON un picco cherry-picked** |
| **Deflated-Sharpe** | DSR **0.999** a N=10/40/100 trial → il Sharpe FULL non è artefatto di multiple-testing |
**Verdetto stress (onesto):**
- **Robustezza FULL-period: FORTE.** TP01 supera fee 0.40%, lag, ampio plateau di parametri, e
deflated-Sharpe. NON è overfit né cherry-picked — la proprietà robusta è il **taglio del
drawdown** (13.8% vs 77.5% full; 8% vs 60% hold-out), invariante a tutto lo stress.
- **Edge di RITORNO nel hold-out: REALE ma SOTTILE e sensibile alla frizione.** Nel 2025-26 ha
schivato il crash in modo affidabile (DD 8% vs 60%) ma ha **protetto più che profittato** (+2.8%,
Sh 0.27), e quel sottile positivo si assottiglia a zero sotto fee2x o lag 2 barre.
**Conclusione:** la proprietà **deployabile e robusta di TP01 è la PROTEZIONE del drawdown**, non
la generazione di alpha. È una strategia difensiva genuina (prima del progetto a superare gauntlet
+ stress), ma a basso ritorno: il valore è "Sharpe ~1.3 con DD ~6× più piccolo del buy&hold",
non "battere il mercato". Per il capitale reale: il vol-targeting + long-flat sono meccanici e
generalizzano; il rischio residuo è la frizione di esecuzione sul filo del sottile edge di ritorno
nei regimi avversi → da monitorare col paper trader forward-only prima di scalare.
+74
View File
@@ -0,0 +1,74 @@
# 2026-06-19 — Track A: Trend / Momentum su BTC & ETH (dati certificati)
Prima ricerca di strategie NUOVE post-reset (track A = trend/momentum). Tool:
`scripts/research/trackA_trend.py` (harness onesto `src/backtest/harness.py`, fee 0.10% RT,
IS/OOS 65/35, griglia su entrambi gli asset, fee sweep, stress leva). Run:
`uv run python scripts/research/trackA_trend.py`.
## Cosa è stato testato
- **TSMOM** (segno del ritorno N-barre, hold H) long/short e long-only.
- **EMA crossover** (fast/slow) come filtro di trend.
- **Donchian breakout** (entry ONESTO: breakout rilevato con `close[i]`, fill a `close[i]`).
- **Vol-scaled / regime-gated TSMOM** (momentum preso solo se |z| > gate, z = ritorno/vol).
- Griglia ampia su **BTC e ETH**, **1h / 15m / 5m**. 480 celle OOS totali.
Tutto entry-eseguibile: direzione e prezzo decisi con dati ≤ `close[i]`, fill a `close[i]`.
Nessun uso di `returns[i]` (che codifica `close[i+1]`). Hold approssimato come catena di
posizioni non sovrapposte di H barre (la fee si ammortizza su H barre — costo onesto).
## Risultati — la fotografia onesta
**Celle positive OOS per timeframe:**
| TF | celle positive / totali |
|----|----|
| **1h** | 39 / 160 |
| **15m** | **0 / 160** |
| **5m** | **0 / 160** |
**Trend intraday (5m/15m) è MORTO**: lo drag della fee (più trade = più 0.10% RT) annienta
qualsiasi segnale. Drawdown 80-99%, Sharpe da 0.6 a 2.2. Niente da salvare.
**Su 1h** c'è qualche cella positiva, ma il contesto la ridimensiona:
- La finestra **OOS è un singolo regime**: il taglio 65% cade a **set/dic 2023**, quindi
l'OOS è ~2023→2026 (in gran parte toro 2024). Tutto il 2018-2022 (orso 2018, crash 2020,
toro 2021, orso 2022) è IN-SAMPLE. "Positivo OOS" qui ≈ "il trend ha fatto soldi nel toro 2024".
- **Benchmark buy & hold sulla stessa finestra OOS**: BTC **+134%**, ETH **21%**.
- Tutte le `TSMOM_LONG` e metà delle celle BTC fanno **MENO** del B&H → è **beta**, non edge.
- Le poche che battono il B&H lo fanno **solo su ETH** (dove il B&H era negativo): catturano
anche i ribassi. Quello è timing reale — ma vedi sotto.
**Le "star":** VOLSCALED_TSMOM BTC 1h (N=20,H=48,vw=100,z=0.5) = +367% OOS, Sh 0.91, DD 32%,
€/d(2k) +2.56; ETH 1h (N=20,H=48,vw=50,z=1.0) = +197% OOS, Sh 0.60. **MA sono celle fortunate:**
i vicini di griglia crollano (stesso N/H, vw=50 invece di 100 → +21% invece di +367%; z=1.0 → +34%).
Non è un altopiano robusto, è un picco isolato. E il P&L è concentrato nel 2024 (+110% su BTC),
con 2025/2026 deboli o negativi per molte celle.
**Consistenza cross-asset (un edge vero regge su ENTRAMBI):** su 480 celle, solo **2** sono
positive OOS su BTC *e* ETH:
- `TSMOM_LONG 1h N=200 H=48` → ma è long-only ≈ beta (fa meno del B&H su BTC).
- `DONCHIAN 1h N=200 H=12` → l'unico candidato "vero" simmetrico, ma **marginale**:
OOS BTC +9% / ETH +15%, **Sharpe 0.15-0.19**, troppo debole per dispiegarlo.
**Fee sweep / leva:** le star reggono lo sweep 0.0005-0.002 (è 1h, poche operazioni), e lo Sharpe
è invariante alla leva (come deve) — ma la leva 3x porta i DD a 75-91% e affonda le celle marginali.
## Verdetto
**Nessun edge trend/momentum dispiegabile, onestamente, su BTC/ETH oggi.**
- 5m/15m: morti per fee. Chiuso.
- 1h: esiste un **residuo di segnale trend** (le celle che battono il B&H negativo di ETH non sono
solo beta), ma è (a) testato su **un solo regime OOS** (toro 2023-2026), (b) **non robusto** di
griglia (picchi isolati), (c) sull'unica cella simmetrica robusta-su-entrambi (Donchian N=200)
**troppo debole** (Sharpe ~0.17). Sharpe netti ~0.3-0.9 nel caso migliore = sotto la soglia per
rischiare capitale reale.
Conferma la lezione del reset (il superstite storico era trend-following, non mean-reversion): il
trend è la direzione *meno sbagliata*, ma sui dati certi non basta a fare un edge. Coerente con
Track C (mean-reversion = artefatto).
## Prossimi passi possibili (non ancora edge)
- Walk-forward multi-regime (non un singolo taglio 65/35) per stressare Donchian-1h-N200 su orso 2018/2022.
- Trend 1h **con filtro di volatilità/regime più ricco** o portafoglio BTC+ETH per diversificare il
rischio di regime — ma solo se emerge robustezza di griglia, non altri picchi fortunati.
- Restare scettici: finché un trend non è positivo su griglia + su entrambi gli asset + su ≥2 regimi
OOS, **non si dispiega**.
+93
View File
@@ -0,0 +1,93 @@
# 2026-06-19 — Track B: ML / feature-prediction su BTC & ETH (walk-forward onesto)
Esperimento di ricerca sulla direzione **machine-learning** post-reset, su dati Deribit
mainnet certificati (solo BTC/ETH). Tool: `scripts/research/trackB_ml.py` (runnable
`uv run python scripts/research/trackB_ml.py`). Tutto netto fee, strict walk-forward,
held-out tail mai usato per scegliere i config.
## Metodologia (anti look-ahead — la lezione della v2.0.0)
- **Feature** (21): ritorni multi-lag (1/2/3/6/12/24), geometria candela (body/upper/lower
shadow su range, range normalizzato, body lag-1), momentum48 + accelerazione, RSI14,
estensione ATR-normalizzata vs EMA24, vol realizzata 24/72 + ratio, posizione del close
nel range 24/72, z-score del volume. **Tutte backward** (note solo a `close[i]`).
- **Label**: segno del ritorno forward su H barre, `sign(close[i+H]/close[i])`.
- **Strict walk-forward**: per predire il blocco che inizia a `b`, si addestra
scaler+modello SOLO su indici `< b-H` (gap di H → label completamente realizzata nel
passato), finestra rolling delle ultime W barre. Retrain ogni K=250 barre. Mai fit sul
futuro. **Nessun leakage** (verificato: la label più recente del train usa `close[b-1]`).
- **Esecuzione**: entry a `close[i]` nella direzione predetta, hold fino a H barre
(no TP/SL); il no-overlap dell'harness distanzia i trade ≥ H barre.
- **Modello**: `LogisticRegression(class_weight='balanced')`. Soglia di probabilità per
filtrare i segnali deboli (long se p>0.5+thr, short se p<0.5-thr, altrimenti flat).
- **Selezione su DEV** (primo 75%), **conferma una volta sola** sull'held-out tail (ultimo 25%).
- Griglia: W∈{4000,8000,16000}, H∈{6,12,24,48}, thr∈{0,0.03,0.06,0.10}, BTC & ETH, 1h.
Fee-sweep 0.05/0.10/0.15/0.20% RT. Turnover/time-in-market sempre riportati.
## Risultato — esiste un segnale, ma è debole e a basso turnover
**Pattern netto e robusto della griglia**: la positività compare SOLO nelle celle a basso
turnover → **W grande (16000) + H lungo (24) + soglia alta (0.10)**. Tutto ciò che gira
veloce (thr basso, H corto, e soprattutto il **15m**) **muore sulle fee**.
- **15m**: 0/12 celle positive in dev (la migliore 47%, le altre 99%). Stesso win-rate
5256% del 1h, ma il turnover lo polverizza. Conferma di prim'ordine: l'edge per-trade è
minuscolo, sopravvive solo se si tradano poche barre.
- **1h, dev**: 19/96 celle net-positive con Sharpe>0. Famiglie threshold-robuste:
`BTC W16000 H12`, `BTC W8000 H12`, `BTC W16000 H24`, più ETH W16000 H12/H48 marginali.
### Held-out tail (2024→2026, mai toccato in sviluppo)
| config | trades | wr% | net% | Sharpe | DD% | mkt% | €/g(2k) | long% | B&H tail |
|---|---|---|---|---|---|---|---|---|---|
| **BTC W16000 H24 thr0.10** | 333 | 52.9 | **+83.7** | 0.57 | 23 | 12 | **+0.58** | 44 | +3.9% |
| BTC W16000 H12 thr0.10 | 382 | 53.4 | +37.6 | 0.35 | 25 | 7 | +0.26 | 54 | +3.9% |
| ETH W16000 H12 thr0.10 | 364 | 57.7 | +23.7 | 0.24 | 35 | 7 | +0.18 | 68 | 38.4% |
| ETH W16000 H48 thr0.06 | 215 | 55.3 | 13.3 | 0.08 | 64 | 16 | 0.10 | 67 | 38.4% |
**Non è solo beta.** Il B&H sul tail è +3.9% (BTC) e 38.4% (ETH), eppure le celle migliori
fanno +37…+84% (BTC) con **long ~4454%** (bilanciato long/short), e ETH +23.7% **mentre ETH
scendeva 38%** (short corretti). Quindi c'è segnale direzionale genuino, non cattura di trend
rialzista. Payoff asimmetrico: ~53% WR ma avgWin>avgLoss (BTC: +2.04% vs 1.63%).
### Fee-sweep (held-out)
- `BTC W16000 H12 thr0.10`: 0.05%→+66.6 | **0.10%→+37.6** | 0.15%→+13.7 | 0.20%→−6.1.
Sopravvive fino a ~0.15% RT, poi muore. Margine sottile.
- `BTC W8000 H12 thr0.06`: positivo solo a 0.05%, già 35% a 0.10%. Fragile.
- ETH e le celle a turnover medio: muoiono tra 0.10 e 0.15%.
### Stabilità per-anno (full walk-forward, BTC W16000 H24 thr0.10)
`+11% (2020) / +188% (2021) / +14% (2022) / 38% (2023) / +13% (2024) / +75% (2025) / +7% (2026)`,
CAGR full ~22%, ma **DD 56%** e fortissima concentrazione su 2021/2025 con un 2023 a 38%.
## Verdetto onesto — NON deployabile verso l'obiettivo
1. **L'edge è reale ma minuscolo.** A differenza della vecchia libreria (artefatto puro), qui
il segnale sopravvive a strict walk-forward, a fee 0.10% RT e batte il B&H sul tail. È un
risultato genuino e va registrato: la direzione ML **non è morta**.
2. **Ma è incompatibile col target.** €/giorno su €2000 = +0.26…+0.58 baseline (anche la stima
rosea full-WF CAGR 22% → ~€13/g). Il target è **€50/g** → siamo ~100x sotto.
3. **Fragilità**: vive solo a basso turnover (thr alto, H lungo, W grande), DD 2356%,
ritorni concentrati in pochi anni con un anno a 38%, e l'edge si assottiglia già a
0.15% RT. Un singolo cambio di regime lo annulla.
4. **ETH ≠ "specialmente buono"** (contrariamente all'indizio dello shape-ML precedente): qui
ETH è più sottile e più rumoroso di BTC sull'held-out; l'unico merito è aver shortato
correttamente il drawdown 2024-25.
**Conclusione**: la logistic-regression walk-forward su feature di forma+momentum trova un
debole edge **momentum direzionale a basso turnover** su BTC (più tenue su ETH), onesto e
netto-fee, ma **troppo piccolo, troppo concentrato e troppo fee-sensibile** per essere
deployato standalone. Al massimo un **componente** di un futuro ensemble, e solo nelle
configurazioni a bassissimo turnover. Nessun config raggiunge, neanche lontanamente, i €50/g.
## Prossimi passi possibili (non eseguiti)
- Provare **predizione di magnitudine/asimmetria** (large-up vs large-down) e position-sizing
proporzionale alla confidenza, invece del semplice segno.
- **GradientBoosting / feature non lineari** (flag `--gbm` predisposto) — ma attenzione
all'overfit; il rischio è di "trovare" edge che il walk-forward onesto non conferma.
- **Ensemble** del segnale ML a basso turnover con un filtro di regime (vol/trend) per tagliare
il 2023. Ma serve dimostrare che il filtro non è scelto col senno di poi.
- Restare scettici: finché €/g resta ~100x sotto target, l'ML da solo NON è la risposta.
+70
View File
@@ -0,0 +1,70 @@
# 2026-06-19 — Track C: mean-reversion / range re-examination (HONEST) → DEAD
Obiettivo: stabilire rigorosamente se **un qualunque** edge di mean-reversion / range a
breve orizzonte sopravvive su BTC/ETH **certificati** (Deribit mainnet) con **ingresso
eseguibile onesto**, oppure confermarne definitivamente la morte. Entrambi gli esiti sono
validi; nessun risultato forzato.
Tool: `scripts/research/trackC_meanrev.py` (self-contained, runnable), sopra l'harness
onesto `src/backtest/harness.py` (direzione+prezzo decisi con dati ≤ `close[i]`, fill a
`close[i]`, exit intrabar TP/SL da `i+1`, fee netto). Universo: BTC/ETH × {1h,15m,5m}.
## Cosa è stato testato (5 famiglie, ingresso onesto)
- **ZFADE** — Bollinger/z-score fade: `z(close,lookback)`thr → long, ≥ +thr → short.
TP al mean mobile o a `tp_atr·ATR`; SL a `sl_atr·ATR`. **Entry a close[i]**, NON al tocco
della banda (era proprio quello l'artefatto storico).
- **RSI2** — RSI(2) oversold/overbought (+ variante con filtro trend SMA200).
- **RETREV** — return reversal: fade del rendimento cumulato estremo (|z| > thr·σ).
- **VWAP** — reversione sulla distanza dal VWAP rolling (in unità di σ della distanza).
- **SESSION** — autocorrelazione next-bar per ora UTC (descrittivo).
Metodologia applicata: OOS 65/35, griglia parametri su **entrambi** gli asset, fee-sweep
{0, 0.5, 1.0, 1.5, 2.0} bps RT, cross-check liquidità (flat-bar O=H=L=C) e time-in-market.
## Sanity liquidità
Flat-bar O=H=L=C: BTC/ETH 1h ≈ 0.01%, 15m 0.090.14%. Book vivo → l'eventuale edge NON
potrebbe nascondersi in barre ferme (a differenza degli alt archiviati). Confermato pulito.
## Risultati — tutto negativo, su ogni asse
**PASS 1 (screen 1h, fee 0.10% RT):** ogni famiglia OOS negativa su entrambi gli asset.
Es. ZFADE z2/mean: BTC OOS 85%, ETH OOS 83%. RSI2 10/90: BTC 92%, ETH 96%.
RETREV/VWAP idem. Win-rate spesso "alto" (RSI2 ~63%, VWAP ~64%) ma **perde lo stesso**
le poche perdite sono enormi, la reversione non paga il rischio + fee.
**PASS 2 (griglia 1h):** ZFADE **0/18** celle con OOS>0 su entrambi; RSI2 **0/36**. La
cella meno-peggio (ZFADE lookback20 z3) resta BTC 40% / ETH 33% OOS. Nessun sopravvissuto.
**PASS 3 (fee-sweep, incl. fee=0 GROSS):** il colpo decisivo. **Anche a fee=0** (lordo)
la z-fade è negativa: BTC full 74% / OOS 46%, ETH full 98% / OOS 48%. Quindi non è
"morte da fee": **la direzione stessa della fade è sbagliata** sul feed pulito. Salendo le
fee degrada monotòno fino a 100%.
**PASS 4 (timeframe 5m/15m/1h):** più veloce = peggio. A 5m full 100% su entrambi
(41.889 / 38.660 trade), €/giorno su 2000 ≈ 0.70/0.75. Coerente con "molte operazioni =
morte per fee", ma il PASS 3 mostra che il problema è a monte: niente edge nemmeno lordo.
**PASS 5 (sessione UTC):** esiste una **debole** autocorrelazione negativa next-bar in
poche ore (BTC 13h 0.166, 2h 0.154, 21h 0.129; ETH 13h 0.152, 4h 0.117), e una
positiva alle 03h UTC (BTC +0.158, ETH +0.202 = ora "trending"). Struttura reale ma
debolissima (|ρ|≤0.17): non sopravvive a fee + dimensionamento del rischio (lo conferma il
fatto che tutte le versioni *tradate* perdono anche lorde).
## Verdetto
**Nessuna** configurazione MR produce OOS netto>0 su entrambi BTC ed ETH a fee baseline.
Più forte: **a fee zero la fade è già negativa** → l'edge MR storico (+201%/+1238% "OOS")
era un **artefatto del feed contaminato** (wick fantasma testnet + entry su estremi mai
scambiati), non una proprietà del mercato. Sul dato certificato, con ingresso eseguibile,
la mean-reversion a breve orizzonte **non è un edge**: è morta sia lorda che netta.
Coerente con la tesi del reset (`2026-06-19-deribit-history.md`, §3): FADE morto ogni anno.
Track C chiusa come direzione di alpha. La debole struttura intraday-by-hour (PASS 5) è
annotata ma non azionabile da sola; semmai un *filtro* futuro, non una strategia.
## Artefatti
- `scripts/research/trackC_meanrev.py` — riproducibile: `uv run python
scripts/research/trackC_meanrev.py [--quick]` (~40s quick, ~3min full).
+96
View File
@@ -0,0 +1,96 @@
# 2026-06-19 — Track D: Robust walk-forward TREND PORTFOLIO (BTC+ETH), vol-targeted + leverage
Follow-up to Track A. Thesis under test: trend-following's real value in crypto is **drawdown
reduction** vs buy & hold (it sidesteps crashes), and that lower DD lets you apply **leverage** and
**diversify** BTC+ETH into a deployable, risk-adjusted *earning* system — even if each single signal
has modest Sharpe. Tool: `scripts/research/trackD_trendport.py` (run
`uv run python scripts/research/trackD_trendport.py`).
## Method (honest, no look-ahead)
Equity built directly from a **target-position series** (the harness's documented "build your own
equity" path), NOT per-trade chaining:
- `target[i]` decided with data **≤ close[i]**; **held during the next bar** (close[i]→close[i+1]).
- `pnl[t] = target[t-1]·r[t]`, `r[t]=close[t]/close[t-1]-1` — positions **shifted +1 bar** ⇒ no leakage.
- Fees on **turnover**: `0.05%/side·|target[t-1]-target[t-2]|` (0.10% RT baseline; swept 0.100.40% RT).
- **Vol-targeting** (main lever): `target = direction · (target_vol / realized_vol)`, clipped to the
leverage cap. `realized_vol` = annualized rolling std of past bar returns (30d window), ≤ close[i].
- **Portfolio** = 50/50 BTC+ETH net-return series, rebalanced each bar on common timestamps.
Leakage sanity check passed: an *oracle* target using next-bar sign explodes (10^119×) — proving the
engine holds `target[i-1]` over bar `i` — while our signals (TSMOM blend, MA-slope, Donchian) only use
`close[i]` and earlier. Zero-position equity = exactly 1.0.
## What was tested
TSMOM multi-horizon blend (1/3/6-month-equiv on 1h bars), MA-slope (EMA200 slope), Donchian breakout
with trailing channel stop — each vol-targeted, long-short **and** long-flat, per-asset and combined.
Grid: target-vol × leverage-cap × horizon-set; explicit EARLY(2018-21)/LATE(2022-26) split;
fee & leverage sweep; full per-year 2018-2026.
## Results — the honest picture
**1) The thesis holds: massive DD reduction, and diversification helps.**
| Strategy (50/50 port, tvol20%, LS) | CAGR | Sharpe | maxDD | volA |
|---|---|---|---|---|
| **B&H 50/50** | +48% | 0.92 | **77.8%** | 70% |
| TSMOM 1-3-6m blend | +14.2% | **1.00** | **18.9%** | 14% |
| MA-slope | +14.1% | 0.79 | 21.9% | 19% |
| Donchian-trailing | +14.7% | 0.89 | 17.7% | 17% |
Trend cuts maxDD from ~78% to ~18% while keeping a Sharpe **above** buy&hold (1.00 vs 0.92). The
portfolio Sharpe (1.00) **beats both sleeves** (BTC 0.95, ETH 0.75) — diversification works as claimed.
The **long-flat** variant is even cleaner: Sharpe **1.32**, maxDD **13.3%** (no short funding/borrow risk).
**2) It is genuinely robust (not a lucky cell).**
- *Per-year (headline LS):* every full year **positive** 2019-2025 (+19/+36/+19/+6/+2/+14/+4%) and 2026 +8%.
- *Grid:* Sharpe ≈1.00 across **all** target-vol (10-40%) × leverage caps — flat plateau (vol-targeting
just scales). DD scales ~linearly with target-vol (10%→DD10%, 40%→DD35%).
- *Horizon-set:* every subset (1m/3m/6m/1-3m/3-6m/1-2-4m/2-4-8m) is **positive**; Sharpe 0.37→1.39.
Shorter horizons (1m, 1-2-4m) score best (Sharpe 1.34-1.39) — a real plateau, not one combo.
- *Fee:* survives to 0.40% RT (Sharpe 1.00→0.39, still positive at 4× baseline fee).
**3) The honest caveat — most of the edge is the EARLY regime.**
Walk-forward split, same param set both assets:
- **EARLY 2018-2021:** CAGR +26%, Sharpe **1.63**, DD 18%.
- **LATE 2022-2026:** CAGR +7.3%, Sharpe **0.57**, DD 19%.
The signal is real and still net-positive every late year, but its quality **halved** post-2021
(crypto vol compressed, trends choppier). This is the same warning Track A raised, now quantified: the
edge is strongest 2019-2021 and merely *modest* in the 2022-26 regime.
**4) Leverage is a red herring; target-vol is the real dial — and it costs DD linearly.**
At tvol=20% on 60-80% crypto vol, positions stay **sub-1x** (avg gross 0.23×): the leverage cap
**never binds**. To deploy real leverage you raise target-vol; Sharpe stays ~1.0, DD scales:
| target_vol | avg gross | CAGR | Sharpe | maxDD |
|---|---|---|---|---|
| 20% | 0.23× | +14% | 1.00 | 19% |
| 40% | 0.45× | +28% | 1.00 | 35% |
| 60% | 0.68× | +40% | 1.00 | 48% |
| 80% | 0.90× | +50% | 1.00 | 60% |
| 100% | 1.12× | +58% | 0.99 | 69% |
## Verdict — is this a deployable earning system?
**Yes as a risk-adjusted system; NO as a fast path to €50/day on €2000.**
- This is the **first post-reset config that is genuinely robust**: Sharpe ~1.0 (long-flat 1.3),
positive every year 2018-2026, robust across grid/horizon/fee, on both assets, on certified data,
with honest no-look-ahead accounting. It is a real, deployable trend portfolio and a clear
improvement over Track A's lucky single cells. The thesis (DD reduction → leverageable, diversifiable)
is **confirmed**.
- **But the earnings are modest.** Headline (tvol20%, 2x cap, LS): CAGR **+14.2%**, DD 19% ⇒ steady-state
**~€0.73/day on €2000**. To average **€50/day at this CAGR you need ~€137k capital**, not €2000.
- **Leverage can't close the gap cheaply.** Pushing target-vol to 80% gives CAGR ~50% (DD **60%**) — and
at €2000, 50%/yr is still only ~€2.7/day in steady state. Reaching €50/day in 1-2 years from €2000
would require both heavy leverage (DD 60-70%, near-ruin) **and** lucky path — not a sane plan.
- **Regime risk:** the edge is much weaker post-2021 (Sharpe 0.57 LATE). Deploy sized for the LATE
regime, not the EARLY one.
**Recommendation:** treat this as the **core risk engine** (compounding ~14%/yr at DD<20%, or
long-flat ~16%/yr at DD 13%), deployable now at low size to validate live execution. It grows €2000,
but to *€50/day* the lever is **capital + time**, not leverage. Realistic near-term: ~€0.7-1.5/day on
€2000; €50/day needs ~€70-140k or a second uncorrelated edge stacked on top.
## Deliverable
`scripts/research/trackD_trendport.py` — self-contained, prints B&H benchmark, broad scan, grid
robustness, horizon robustness, walk-forward early/late, fee+leverage sweep, headline config per-year,
and the path-to-€50/day table. Reusable building blocks (vol-targeting, target→equity, portfolio).
@@ -0,0 +1,140 @@
# 2026-06-19 — Track E: Cross-sectional BTC↔ETH relative-value + ENSEMBLE synthesis
Due parti, entrambe oneste e su dati Deribit-mainnet certificati (solo BTC/ETH). Tool:
`scripts/research/trackE_xsec_ensemble.py` (runnable, self-contained, riusa il walk-forward
ML di Track B e il Donchian di Track A). Harness onesto: direzione/posizione decise con dati
`close[i]`, realizzo sul bar successivo (shift di 1 barra, niente look-ahead). Fee
turnover-based: `|Δpos|·fee_rt/2` **per gamba** (un flip +1↔−1 = un round-trip = 0.10% RT).
Run: `uv run python scripts/research/trackE_xsec_ensemble.py` (`--quick` salta lo sleeve ML;
`--no-cache` ricalcola la proba ML). Il proba ML viene cacheato (`.cache_trackE_*.npy`).
---
## PART 1 — Relative value (spread BTC↔ETH, 1h, market-neutral)
**Premessa strutturale.** BTC/ETH log-ret 1h sono correlati **0.84**. Con due soli asset
l'unica struttura tradabile è lo **spread**. E con due asset, *"long il più forte / short il
più debole"* (XS-momentum) è **algebraicamente identico** a *"trada il trend del ratio
ETH/BTC"* — infatti nel codice (A) e (B) producono numeri identici. Sono lo stesso edge.
**Lead-lag: nullo.** `corr(rB[i], rE[i+1]) = 0.018`, `corr(rE[i], rB[i+1]) = 0.007`,
autocorrelazioni 0.01..0.02. Nessun potere predittivo cross-asset → lead-lag **non**
perseguito come sleeve (sarebbe rumore moltiplicato per le fee).
**(A/B) XS momentum / ratio trend (griglia N∈{24,72,168,336}, hold∈{6,24,72}):**
- Solo **4/12 celle** OOS net-positive, e sparse (N24/h24, N24/h72, N72/h72, N168/h24).
- Le celle FULL forti (N168/h24: +150% full, Sharpe 0.68, DD 27%) hanno **OOS debole**
(+11%, Sh 0.30). La migliore per OOS-Sharpe è N24/h24 (OOS Sh 0.31, OOS net +11%).
- **Fee sweep (N24/h24):** gross (0bp) FULL +356%/OOS +74% Sh 1.20 → a 1.0bp/gamba FULL +27%/
OOS +11% Sh 0.31 → **muore già a 1.5bp/gamba** (OOS 11%). Margine fee sottilissimo.
- **Per-anno** concentrato sui grandi movimenti del ratio 2020-2021 (e 2024), piatto/negativo
altrove (2022 9%, 2023 19%, 2025 6%, 2026 16%). Non è un altopiano: è un edge debole,
fee-sensibile, regime-dipendente.
**(C) Ratio mean-reversion (z-fade di log(ETH/BTC)):** negativa ovunque (es. lb168/zin2.0:
FULL 85%, OOS 44%, Sh 1.56). Coerente con Track C: anche sullo spread la MR a breve non è
un edge sul dato pulito.
**Verdetto PART 1:** esiste un **debole** edge di relative-value (XS-momentum ≡ ratio-trend),
net-positivo OOS solo in alcune celle, Sharpe OOS ~0.3, che **muore a ~1.5bp/gamba** ed è
concentrato in pochi anni. È **reale ma marginale** — degno di entrare in un ensemble come
sleeve diversificante, non come strategia standalone. La sua virtù: è **quasi scorrelato**
dagli edge direzionali (vedi sotto).
---
## PART 2 — Ensemble (3 sleeve residui in UN portafoglio)
Sleeve combinati (gross 1 ciascuno, equal-weight 1/N → gross totale ~1):
- **S1 = BTC-ML** (Track B, cella onesta a basso turnover W16000 H24 thr0.10, 1h).
- **S2 = BTC-Trend** (Track A, l'unica cella trend robusta cross-asset: Donchian N=200 H=12).
- **S3 = Relative-value** (PART 1, miglior cella OOS: XS-momentum N=24 hold=24).
**Finestra comune attiva** (dove tutti e 3 sono live, dopo il warmup ML): 2020-06 → 2026-06,
52.636 barre.
### Matrice di correlazione degli sleeve (ret per-barra, finestra comune)
| | S2_trend | S3_relval | S1_ml |
|----------|----------|-----------|--------|
| S2_trend | +1.000 | +0.010 | 0.063 |
| S3_relval| +0.010 | +1.000 | 0.010 |
| S1_ml | 0.063 | 0.010 | +1.000 |
**Sleeve quasi perfettamente scorrelati** (|ρ| ≤ 0.06). In teoria, terreno ideale per la
diversificazione.
### Per-sleeve (finestra comune, scala $ uguale)
| sleeve | net | Sharpe | maxDD | €/g(2k) |
|-----------|-------|--------|-------|---------|
| S2_trend | +5% | +0.15 | 34% | +0.04 |
| S3_relval | +8% | +0.16 | 41% | +0.07 |
| **S1_ml** | +382% | **+0.87** | 56% | +3.51 |
### Ensemble
| portafoglio | net | Sharpe | maxDD | CAGR | €/g(2k) |
|----------------------|-------|--------|-------|-------|---------|
| best single (S1_ml) | +382% | +0.87 | 56% | +30% | +3.51 |
| **EQUAL-WEIGHT 1/N** | +109% | **+0.83** | **30%** | +13% | +1.00 |
| inverse-vol (IS wts) | +76% | +0.70 | 29% | +10% | +0.69 |
| EQ-WEIGHT **OOS**(65/35)| +32% | **+1.02** | **12%** | +14% | +0.83 |
Per-anno equal-weight: 2020 +16%, 2021 +50%, 2022 +2%, **2023 13%** (vs 38% dell'ML da
solo!), 2024 +18%, 2025 +19%, 2026 3%. **Molto più liscio**, niente anno-catastrofe.
### La diversificazione aiuta? Sì sul rischio, NO sul rendimento risk-adjusted
- **Sharpe:** ensemble 0.83 vs best-single 0.87 → **non batte** il miglior sleeve singolo.
- **maxDD:** ensemble **30%** vs best-single 56% → **dimezzato**. E OOS 12% vs ML-solo molto
più profondo. Per-anno senza il 38% del 2023.
- **Risk-matched** (levare l'ensemble 1.84x per pareggiare il 56% DD dell'ML): €/g +2.23
contro €/g +3.51 dell'ML da solo → a pari drawdown l'ensemble rende **MENO** (ratio 0.64).
**Perché?** Gli sleeve sono scorrelati ma **enormemente diseguali** (Sharpe 0.87 vs 0.15 vs
0.16). L'equal-weight 1/N "annacqua" l'unico sleeve forte con due deboli: la matematica
della diversificazione alza lo Sharpe solo se gli sleeve sono di *qualità comparabile*. Qui
non lo sono, quindi 1/N non può superare il singolo migliore. Pesare verso l'ML (quality-
weighting) converge banalmente a "esegui solo l'ML" — e sarebbe in-sample.
**Il guadagno vero dell'ensemble è la ROBUSTEZZA, non il rendimento:** stesso Sharpe del
miglior sleeve a **metà del drawdown**, per-anno molto più stabile, niente dipendenza da un
singolo modello/regime (l'ML da solo concentra tutto in 2021/2025 con un 38% nel 2023). Per
chi deve *sopravvivere*, l'ensemble è preferibile; per chi massimizza il rendimento a pari
rischio, l'ML puro vince di un soffio.
---
## Verdetto onesto — è un motore da €50/giorno? NO.
1. **Relative-value:** edge debole, reale ma marginale (Sharpe OOS ~0.3), fee-sensibile
(muore a 1.5bp/gamba), concentrato 2020-2021/2024. Utile **solo** come sleeve scorrelato.
Lead-lag e ratio-MR: nulli/negativi.
2. **Ensemble:** gli sleeve sono **quasi scorrelati** (|ρ|≤0.06) — risultato genuino e bello.
L'ensemble equal-weight ottiene **Sharpe ~0.83 a metà del drawdown** del miglior sleeve e
un per-anno molto più liscio. **Ma NON alza il tetto risk-adjusted** (a pari DD rende meno
dell'ML puro) perché un solo sleeve domina.
3. **Distanza dal target:** ensemble **€1.00/giorno su €2000** (best single €3.51 ma a DD
56% e concentrato). Il target è **€50/giorno → ~50x sotto** (l'ML puro ~14x sotto ma con
rischio/concentrazione inaccettabili). Levare per colmare il gap moltiplica il drawdown
ben oltre il tollerabile (1.84x già porta al 51% DD per ~€2.2/g).
**Conclusione:** la sintesi di Track E conferma la fotografia dei track A/B/C — esistono
**edge residui deboli ma reali e scorrelati** su BTC/ETH. Combinarli in un ensemble **migliora
la robustezza** (DD dimezzato, per-anno stabile, niente single-point-of-failure) ma **non crea
rendimento dal nulla**: il sistema combinato rende ~€1/giorno su €2000, ~50x sotto l'obiettivo,
e non è un motore dispiegabile. Il miglior uso pratico dei risultati: se un giorno si tradasse,
l'ensemble equal-weight (ML + trend + relative-value) è la forma **più onesta e meno fragile**
del poco edge disponibile — ma serve un edge **di un'altra magnitudine** per avvicinare i €50/g.
## Prossimi passi possibili (non eseguiti)
- Cercare uno sleeve **di qualità comparabile all'ML** (Sharpe ≥0.5 indipendente) — solo
allora 1/N alzerebbe lo Sharpe oltre il singolo. Senza, l'ensemble resta solo "risk smoother".
- Relative-value su **timeframe diversi** del ratio (giornaliero?) o con **position sizing**
proporzionale alla forza del segnale, restando scettici sul fee-margin sottile.
- Non aumentare la leva per inseguire €50/g: il DD esplode prima del rendimento.
## Artefatti
- `scripts/research/trackE_xsec_ensemble.py` — riproducibile (`uv run ...`, ~8s con cache ML).
@@ -0,0 +1,77 @@
# Track F — Calendar seasonality (hour-of-day / day-of-week) on BTC & ETH
**Data:** 2026-06-19 · **Script:** `scripts/research/trackF_seasonality.py`
**Dati:** Deribit mainnet certificati, BTC/ETH 1h UTC. Fee baseline 0.10% RT (`fee_side=0.0005`).
## Domanda
Esiste un edge di calendario *sistematico e tradeable* (ora del giorno, giorno della
settimana, interazione ora×giorno) su BTC ed ETH, netto fee, OOS, per-anno, su entrambi gli asset?
## Metodologia (anti-overfit, anti-leakage)
- `ret[i]=close[i]/close[i-1]-1` è noto a `close[i]`; una posizione decisa a `close[i]` guadagna
`ret[i+1]`. La statistica che decide il trade usa **solo barre ≤ i** (mai la barra tradata né futuro).
- **Tradeable test onesto = ADAPTIVE EXPANDING sign**: a `close[i]` guardo il bucket di calendario
della barra `i+1` (il clock è noto, zero look-ahead) e prendo il **segno della media passata** di
quel bucket (espandente, warmup-gated). Long-flat o long-short. Fee solo su `|Δposizione|`.
È l'analogo onesto di "tradare il seasonal": i dati scelgono il segno di ogni bucket **dal vivo**.
- Tabelle descrittive per-ora/per-giorno split IS(65%)/OOS(35%) come diagnostica.
- Regola discreta ottimizzata in-sample (entra a ora H, tieni W barre, dir migliore) mostrata solo
per **esporre il gap IS→OOS** (384 celle testate/asset).
- Benchmark **buy-and-hold** come controllo del long-bias.
## Risultati
### 1. Descrittive (bp/barra, IS vs OOS)
- **Hour-of-day:** sign-agreement IS/OOS solo **12/24 (BTC)** e **8/24 (ETH)** → caso. Le ore "US
close" 21:0022:00 UTC sono positive in entrambi gli split su entrambi gli asset (l'unico pattern
con un minimo di coerenza), ma il resto è rumore che cambia segno tra IS e OOS.
- **Day-of-week:** più stabile. **Giovedì negativo** su BTC ed ETH in IS *e* OOS; Lun/Mer positivi.
Sign-agreement 6/7 (BTC), 5/7 (ETH).
### 2. Adaptive expanding-sign (il test tradeable)
| Strategia | BTC Sharpe | ETH Sharpe | Note |
|---|---|---|---|
| HOUR long-short | **5.39** | **4.04** | DD 100%. Annientata dalle fee. |
| HOUR long-flat | 2.92 | 2.09 | DD 100%. Idem. |
| DOW long-short | +0.64 | +0.83 | DD 8284%, 66% nel 2022 |
| DOW long-flat | +0.81 | +0.96 | DD 7578%, 64/66% nel 2022 |
| HOUR×WEEKDAY (168 buckets) | 5.05 | 3.96 | DD 100%. Overfit puro + fee. |
### 3. Il controllo che smonta il DOW — **buy-and-hold**
- BTC buy-hold: **Sharpe 0.79, CAGR 34.9%, DD 77%** → DOW long-flat: Sh 0.81, CAGR 34.2%, DD 77.5%.
- ETH buy-hold: **Sharpe 0.84, CAGR 42.4%, DD 81%** → DOW long-flat: Sh 0.96, CAGR 52.7%, DD 74%.
- Il DOW long-flat è **long il 78% del tempo** (`mean_pos≈+0.78`). È **buy-and-hold travestito**:
guadagna perché crypto sale, non perché esiste un edge di giorno. Lo "skip del giovedì" aggiunge
pochissimo e non giustifica un deploy.
### 4. Fee sweep (HOUR long-short adaptive)
A fee **0%**: Sh +0.61 (BTC) / +0.80 (ETH) — solo long-drift. A 0.10% RT: **5.4 / 4.0**. Turnover
**~8.000 flip/anno** (segno orario instabile, cambia quasi ogni barra) → morte istantanea per fee.
Le strategie hour-of-day sono ad alta frequenza per costruzione: le fee sono di prim'ordine e le
uccidono.
### 5. Regola discreta ottimizzata in-sample (trappola multiple-testing)
- BTC: best IS H=05 hold=24h dir=+1 → **IS Sh +4.25 → OOS Sh +1.47** (+3.7 bp/trade).
- ETH: best IS H=13 hold=24h dir=+1 → **IS Sh +7.35 → OOS Sh +0.90** (+3.2 bp/trade).
- Collasso IS→OOS classico. Inoltre "hold 24h dir+1" = ancora **long-bias** (entra una volta/giorno
e tiene 24h ≈ sempre long). Il margine OOS (~3 bp/trade su 10 bp RT) è marginale e fragile.
## Multiple-testing
199 celle di calendario/asset (24 ore + 7 giorni + 168 ora×giorno) + 384 (H,W,dir)/asset. Con così
tante celle, bucket "significativi" spuri sono **garantiti**. Filtri applicati: segno scelto dal vivo
su soli dati passati, deve reggere OOS, per-anno, e su **entrambi** BTC ed ETH.
## Verdetto — **SPURIO / NON deployable**
- **Nessun edge di calendario netto-fee robusto** su BTC ed ETH.
- **Hour-of-day:** morto (fee + segno instabile). L'unica regolarità (US-close 2122 UTC positiva) è
troppo debole e non sopravvive al turnover.
- **Day-of-week:** l'unico risultato "positivo" è **long-bias mascherato** (≈ buy-and-hold,
Sharpe ~0.80.96 < trend portfolio 1.32, DD 7584% rovinoso, 65% nel 2022). Non è un edge
seasonal sfruttabile; è esposizione direzionale al drift di crypto.
- **Hour×weekday:** overfit puro (IS 3.6 → OOS 8.0).
- Coerente con la lezione del progetto: dove l'unica "direzione" che funziona è essere long, non c'è
alpha di timing — c'è beta. Il trend portfolio (TP01) cattura quel beta in modo vol-targeted e
con DD ~12%, infinitamente meglio di qualunque regola di calendario qui.
**Azione:** track F chiuso negativo. Non aggiungere nulla al portafoglio. Il soffitto Sharpe ~1.3 su
BTC/ETH regge.
@@ -0,0 +1,85 @@
# Track G — Prior-period level breakouts / range (BTC & ETH, calendar-anchored)
**Data:** 2026-06-19 · **Script:** `scripts/research/trackG_prior_levels.py`
**Harness:** `src/backtest/harness.py` (honest, entry decided at `close[i]`, fill `close[i]`).
## Domanda
Esistono edge net-positivi OOS, robusti su BTC **e** ETH, definiti rispetto a un **periodo
calendario precedente** (giorno/settimana/opening-range)? E soprattutto: i breakout di livello
**continuano** (trend) o **rientrano** (fade)?
## No look-ahead (garanzie)
- Livelli prior-day/week costruiti aggregando a barre giornaliere/settimanali (UTC) e poi
**`shift(1)`** sul frame del periodo *chiuso*: il periodo corrente vede solo il precedente
totalmente chiuso. Mai "oggi"/"questa settimana" nel livello.
- Opening-range usato **solo** sulle barre dopo la chiusura della finestra di apertura.
- Direzione + prezzo decisi a `close[i]`, fill a `close[i]`. Mai entry sul livello esatto intrabar.
- Bug iniziale corretto: mismatch tz-aware vs tz-naive nel mapping dei livelli (dava 0 trade).
## Risultati (1h, fee 0.10% RT, leva 1x, OOS 65/35)
### Continuation vs FADE — il verdetto è netto
| Regola (PD = prior-day) | BTC OOS | ETH OOS | Sharpe OOS |
|---|---|---|---|
| **PD-high CONT (long su rottura max ieri)** | **+25%** | **+16%** | +0.5 / +0.3 |
| PD-high FADE | **68%** | **68%** | 1.6 / 1.2 |
| PD-low CONT (short su rottura min ieri) | 33% | 60% | 0.5 / 0.8 |
| PD-low FADE | 36% | 8% | 0.6 / +0.1 |
- **I breakout CONTINUANO, non rientrano.** Il lato FADE è robustamente **negativo** su entrambi
gli asset (sia high che low), su prior-day, prior-week e opening-range. Conferma diretta della
tesi del reset: la mean-reversion / fade è morta su dati certificati.
- **Asimmetria long-only:** funziona solo la rottura del **massimo** (long), non quella del
**minimo** (short). Cioè non è un edge di breakout *simmetrico/direzione-neutro*: è cattura del
**drift/trend rialzista** del cripto. La PD-low-cont (short sui breakdown) perde perché in questo
campione il cripto sale.
### Grid robustness (PASS 6) — survivor = OOS>0 su ENTRAMBI
- **PD-high CONT: 3/3 celle** (buffer 0/0.1%/0.3%) positive OOS su BTC **e** ETH → robusto al buffer.
- PD-high fade, PD-low cont/fade, OR-fade: **0 survivor**.
- **OR-cont:** positiva solo su ETH, negativa su BTC su tutte le finestre (3/6/8/12h) → artefatto
mono-asset, scartato dalla regola "entrambi".
### Anchor-hour sweep (PASS 5) — non è un'ora fortunata
PD-high cont positiva su **21/24** ore UTC (BTC) e **20/24** (ETH). Non dipende da un singolo
anchor → coerente con un edge reale (ma vedi sotto: è beta di trend).
### Fee sweep + per-anno (PD-high cont, full sample)
```
BTC RT%: 0.00→+571 0.05→+289 0.10→+126 0.15→ +31 0.20→ 24 (OOS: +84/+52/+25/+3/15)
ETH RT%: 0.00→+1754 0.05→+1012 0.10→+567 0.15→+299 0.20→+139 (OOS: +67/+39/+16/3/19)
BTC per-anno: 2019 +39 2020 +104 2021 +7 2022 42 2023 +24 2024 +27 2025 16 2026 +3
ETH per-anno: 2020 +164 2021 +160 2022 +7 2023 +1 2024 +12 2025 4 2026 +7
Sharpe full: BTC +0.48 (maxDD 55%, €/d 2k +0.88) · ETH +0.86 (maxDD 34%, €/d 2k +4.27)
```
- **Fee-fragile:** alla baseline 0.10% RT sopravvive (OOS +25/+16%), ma muore già a ~0.15-0.20% RT.
Margine di fee sottile (≈1.5x baseline e l'edge sparisce su OOS). ~1000-1100 trade in 8 anni.
- **Drawdown enormi** (BTC 55%) e anni negativi (2022 42% BTC, 2025 16%).
## Verdetto
- **Sì, esiste un edge net-positivo OOS su entrambi gli asset:** *PD-high continuation* (long
quando `close` supera il massimo di ieri, exit a fine giornata UTC). Robusto al buffer e
all'anchor-hour. **MA non è deployabile come miglioramento:**
1. È **long-only drift capture**, non un breakout simmetrico (il lato short fallisce) → è una
versione **più debole e ridondante** del Trend Portfolio TP01 (Sharpe 0.48-0.86 vs 1.32).
2. **Fee-fragile** (muore a ~1.5x la fee baseline) e con **drawdown** molto peggiori.
- **Il contributo scientifico vero è la conferma della direzione:** sui dati certificati i
breakout di livello-calendario **CONTINUANO**; il fade è morto (negativo robusto su PD/PW/OR,
entrambi gli asset). Nessuna sorpresa mean-reversion nascosta nei livelli giornalieri/settimanali.
- **Niente di nuovo da mettere in produzione.** TP01 resta la strategia vincente; i breakout
prior-period non aggiungono Sharpe (stessa beta di trend, peggio eseguita).
## Come riprodurre
```bash
uv run python scripts/research/trackG_prior_levels.py # full (1h + 15m, ~25s)
uv run python scripts/research/trackG_prior_levels.py --quick # 1h only
```
@@ -0,0 +1,71 @@
# Track H — Volume, Range & Volatility-Regime signals (BTC/ETH, certified, >=12h)
**Date:** 2026-06-19
**Script:** `scripts/research/trackH_volume_vol.py` (runnable, self-contained)
**Question:** does any volume / range / volatility-regime signal ADD to the deployed winner
TP01 (vol-targeted trend portfolio, 12h, Sharpe ~1.32) — i.e. net-positive OOS on BOTH BTC &
ETH AND uncorrelated (|corr|<~0.3) — OR work as a regime filter that lifts TP01's Sharpe / cuts
its DD?
## Method (honest)
- Same causal per-bar engine as `TrendPortfolio.net_returns`: build a continuous TARGET decided
with data `<= close[i]`, HOLD it during bar `i+1` (`pos_held[t]=target[t-1]`), gross = pos×ret,
fee on `|Δpos|`. Identical in spirit to `harness.backtest_signals` (decide≤close[i], fill at
close[i]); two discrete signals cross-checked through `backtest_signals` directly.
- All features (volume z-score, OBV, ranges, realized vol) use prior/rolling windows shifted so
bar `i` sees only `<= i`. 12h/1d resampled from certified 1h via `resample_tf` (label='left'),
consumed index-based with the +1 hold → no open-label leak.
- Fee 0.10% RT baseline + sweep 0.000.40% RT. OOS 65/35 + per-year. Grid on BOTH assets.
Turnover and correlation-to-TP01 reported for every signal.
- **>=12h only** (12h + 1d). Sub-12h excluded per the standing lesson (fees + HF-noise overfit +
the 4h open-label look-ahead trap).
## Signals tested
VT-long (volatility-managed long), VolBreakout (volume-z-confirmed Donchian), OBV-trend,
VW-mom (volume-weighted momentum), RangeExpand (range-expansion breakout), NR7-break
(narrowest-range breakout), DeclVolRev (declining-volume fade/reversal). Plus regime overlays on
TP01: keep-low-vol, keep-high-vol, vol-managed ×1.5, OBV-up confirmation.
## Results (12h headline, fee 0.10% RT)
| signal | corr→TP01 | OOS Sharpe BTC/ETH | note |
|---|---|---|---|
| VT-long | 0.66 / 0.69 | 0.80 / 0.14 | trend-in-disguise; weak OOS ETH |
| VolBreakout | 0.69 / 0.71 | 0.54 / 0.49 | profitable but correlated |
| OBV-trend | 0.61 / 0.63 | 0.96 / 0.68 | profitable but correlated; turnover ~75/yr |
| VW-mom | 0.64 / 0.67 | 0.98 / 0.74 | basically TSMOM; correlated |
| RangeExpand | 0.48 / 0.49 | 0.37 / 1.04 | lower corr but BTC weak; ETH negative on 1d |
| NR7-break | 0.48 / 0.49 | 0.79 / 0.02 | fails OOS on ETH |
| DeclVolRev | -0.15 / -0.11 | -1.15 / -0.44 | **negative even at zero fee** |
Grid robustness (12h, % cells positive full+OOS on both assets): VW-mom 100%, VT-long 100%,
VolBreakout 96%, RangeExpand 96%, OBV-trend 75% — but the robust ones are precisely the ones
that are highly correlated to TP01. Fee sweep: trend-family signals survive to 0.40% RT;
DeclVolRev gets worse with fees (it trades constantly).
## Regime filters on TP01 (12h, 50/50 portfolio)
| variant | full Sharpe | OOS Sharpe | maxDD | CAGR | turn/y |
|---|---|---|---|---|---|
| **TP01 baseline** | **1.32** | 0.90 | 13.3% | 16.2% | 11.5 |
| × keep LOW-vol | 0.94 | 1.11 | 14.1% | 7.7% | 9.5 |
| × keep HIGH-vol | 0.98 | 0.18 | 9.9% | 7.9% | 4.9 |
| × vol-managed ×1.5 | 1.33 | 0.96 | 17.9% | 18.1% | 15.4 |
| × OBV-up only | 1.49 | 1.04 | 10.1% | 14.4% | 18.2 |
OBV-up filter across EMA span: full Sharpe 1.491.52 (span 1530), DD 710%, but OOS gain is
marginal (0.90→1.04 at span 30) and fades for span≥45 (OOS 0.690.73). It cuts ~2pp CAGR and
raises turnover ~60%.
## Verdict (honest)
- **No uncorrelated additive edge exists.** Every *profitable* volume/range/vol signal is trend
in disguise (corr 0.610.75 to TP01) → cannot raise the 50/50 portfolio Sharpe. The genuinely
lower-corr signals (RangeExpand, NR7 ~0.48) fail OOS on at least one asset.
- **Mean-reversion / declining-volume fade is dead** — negative net AND at zero fee on both
assets. Reconfirms the v2.0.0 contamination lesson; MR is not a real edge on certified data.
- **Vol-regime gating hurts** (keep-low / keep-high both drop Sharpe to ~0.95). The vol-managed
overlay is Sharpe-neutral but DD-worse.
- **The only non-harmful overlay is OBV-up trend-confirmation:** it cuts DD (13.3%→10.1%) and
nudges full Sharpe to ~1.49, but it is trend double-confirmation (de-risking), not new alpha;
it costs CAGR, raises turnover, and the OOS Sharpe gain is within noise and span-sensitive. It
is worth keeping in mind as a **defensive DD overlay**, not as a Sharpe improver.
- **Bottom line:** the ~1.3 portfolio-Sharpe ceiling on BTC/ETH-only **holds**. TP01 stays the
deployable winner. Volume/range/vol add nothing uncorrelated.
@@ -0,0 +1,99 @@
# Track I — Alternative momentum formulations + long-horizon reversal (2026-06-19)
**Script:** `scripts/research/trackI_momentum_reversal.py` (self-contained, runnable).
**Universe:** BTC & ETH only. **TF:** 12h + 1d (sub-12h excluded by rule). **Harness:** identical
honest machinery to TP01 — direction decided `<= close[i]`, positions held next bar (`pos_held[1:]
= tgt[:-1]`), vol-target by inverse PAST-ONLY realized vol (target 20%, lev cap 2x), NET fee 0.10%
RT on turnover, 50/50 BTC+ETH. OOS 65/35 + per-year + fee sweep (0.000.40% RT). Correlation to
TP01 net returns reported for every candidate.
## Goal
(A) A momentum formulation that BEATS or DIVERSIFIES the canonical 1-3-6m sign-blend (TP01,
Sharpe ~1.32). (B) Does the classic LONG-HORIZON REVERSAL (fade ~12m winners) give an
uncorrelated positive overlay?
## PART A — momentum formulations (12h, long-flat, vs TP01 Sharpe 1.32 / OOS 0.90 / DD 13.3%)
| formulation | Sharpe | IS | **OOS** | CAGR | maxDD | corr→TP01 | BTC | ETH |
|---|---|---|---|---|---|---|---|---|
| baseline sign-blend 1-3-6m | 1.32 | 1.54 | 0.90 | +16% | 13.3% | 1.00 | 1.15 | 1.10 |
| (i) z-score cum-return (tanh) | **1.35** | 1.63 | 0.85 | +12% | **8.4%** | 0.96 | 1.30 | 1.00 |
| (ii) risk-adjusted momentum | 1.27 | 1.49 | 0.84 | +13% | 9.5% | 0.97 | 1.21 | 1.00 |
| (iii) EMA-cross trend | 0.81 | 0.91 | 0.62 | +11% | 25.1% | 0.85 | 0.89 | 0.53 |
| (iii-b) MACD (calendar spans) | **1.50** | **1.87** | 0.74 | +22% | 17.7% | 0.69 | 1.30 | 1.32 |
| (iv) Donchian breakout | 1.10 | 1.36 | 0.57 | +17% | 25.0% | 0.86 | 1.08 | 0.82 |
| (v) acceleration (Δ-momentum) | 1.28 | 1.82 | 0.35 | +14% | 14.2% | 0.66 | 1.25 | 0.81 |
| (vi) 12-1 skip momentum | 0.67 | 0.79 | 0.47 | +9% | 24.5% | 0.68 | 0.70 | 0.49 |
Results are essentially identical at 1d. Read-out:
- **Nothing cleanly beats the sign-blend OOS on both assets.** The headline-Sharpe leaders are
artefacts of in-sample fit: **MACD** posts IS 1.87 but OOS collapses to 0.74 (gap = overfit) with
a worse DD (17.7%); **acceleration** IS 1.82 → OOS **0.35** (worst OOS decay of all). Both fail.
- **(i) z-score continuous momentum** is the one mild, honest refinement: Sharpe 1.35 (≈baseline)
but **maxDD 8.4% vs 13.3%** — the continuous score scales down position when the cumulative move
is statistically small, de-risking the tails. OOS 0.85 (slightly below baseline 0.90), CAGR drops
16%→12%. It's a smoother sibling of TP01, **not a new edge** (corr 0.96).
- (vi) 12-1 skip (classic equity "12-1" momentum) **does NOT help crypto**: skipping the recent
month removes the strongest part of the signal here → Sharpe 0.67, corr 0.68. Crypto momentum
lives in the recent window, opposite to the equity stylised fact.
- Breakout/Donchian and EMA-cross are strictly worse (high DD, weak OOS).
## PART B — long-horizon reversal (fade past winners), 12h
Long-short reversal (short ~12/18/24m winners, long losers, vol-targeted):
| reversal LS | Sharpe | OOS | CAGR | maxDD | corr→TP01 |
|---|---|---|---|---|---|
| 12m | -0.77 | -1.15 | -14% | 73% | -0.51 |
| 18m | -0.36 | -0.75 | -8% | 58% | -0.47 |
| 24m | **+0.04** | -0.07 | -1% | 43% | **-0.32** |
| 12-18-24m | -0.46 | -0.72 | -8% | 57% | -0.54 |
- **Long-horizon reversal is NOT a standalone edge.** Standalone it LOSES money (12m/18m strongly
negative; only 24m is ~flat at Sharpe 0.04, OOS 0.07, and even that fails "net-positive OOS on
both assets": BTC +0.10 / ETH 0.03). Fading crypto winners over a year just shorts the trend.
- It IS genuinely negatively correlated to TP01 (24m: corr 0.32; 12-18-24: 0.54), as expected
(it's the opposite sign of medium-term momentum).
- **Momentum + reversal blend** (long 1-6m momentum, brake on very-long extension): the variant
`mom(1-3-6) 0.5·rev(12-24)` is the most interesting single-strategy result — Sharpe **1.38**,
**OOS 0.98** (> baseline 0.90), **maxDD 10.6%** (< 13.3%), both assets positive (BTC 1.25/ETH
1.05), corr 0.91, fee-robust (1.43→1.22 across 0.000.40% RT). CAGR drops 16%→12%. It is TP01
with a long-term-extension brake: a modest *risk-adjusted* improvement, not more return.
## COMBINED — TP01 + best diversifier (blend net returns)
TP01 alone: Sharpe 1.321, CAGR +16%, maxDD 13.3%, OOS 0.90.
| combo | Sharpe | CAGR | maxDD | OOS | corr |
|---|---|---|---|---|---|
| TP01 + 20% reversal-24m (LS) | **1.411** | +13% | 11.5% | **1.06** | -0.32 |
| TP01 + 30% reversal-24m (LS) | 1.366 | +12% | 11.8% | 1.06 | -0.32 |
| TP01 + 20% reversal-12-18-24 (LS) | 1.350 | +11% | 10.6% | 0.84 | -0.54 |
| TP01 + 50% z-score | 1.348 | +14% | 9.5% | 0.89 | +0.96 |
- Adding a small slice of **reversal-24m long-short** lifts portfolio Sharpe 1.32→1.41 and OOS
0.90→1.06 while cutting DD to 11.5%. **But be skeptical:** the overlay is a ~zero-mean stream
(standalone Sharpe 0.04). The benefit is almost entirely **variance reduction from the negative
correlation, not added alpha** — and it COSTS return (CAGR 16%→13%). With a true-zero-edge
diversifier this Sharpe bump is fragile (it leans on the 0.32 correlation persisting OOS, and the
OOS sample is one 2022-24 crypto cycle). I would NOT deploy capital on a standalone-losing sleeve
to chase a 0.09 Sharpe point that is really de-risking.
## Fee sweep (12h portfolio Sharpe)
baseline 1.37→1.18, z-score 1.38→1.24, MACD 1.52→1.45 (lowest turnover), blend 1.43→1.22,
reversal-24m 0.07→−0.02 (0.00→0.40% RT). All trend formulations survive realistic fees; reversal
has no positive margin to survive on.
## VERDICT (honest)
- **Is there a momentum formulation that beats the 1-3-6m sign-blend? No — not OOS, not on both
assets.** MACD/acceleration look better in-sample but decay OOS (overfit + higher DD). The only
honest refinement is **continuous z-score momentum**, which matches the Sharpe with materially
lower drawdown (8.4% vs 13.3%) — a smoother variant of the SAME edge, not a new one (corr 0.96).
- **Does long-horizon reversal give an uncorrelated positive overlay? No, not a real one.** It is
uncorrelated/negatively-correlated (good) but **not positive** standalone (it loses, or at best is
flat at 24m and fails the both-assets bar). The combined-Sharpe lift (→1.41) is variance reduction
from a near-zero-mean stream and sacrifices CAGR — fragile, not bankable alpha.
- **The ~1.3 structural Sharpe ceiling on BTC/ETH-only holds.** TP01 remains the deployable winner.
If anything, swap the sign-blend for the **z-score continuous score** (or the `mom 0.5·rev`
brake) for a lower-DD profile at equal Sharpe — a risk-management tweak, not a return upgrade.
+28
View File
@@ -0,0 +1,28 @@
# 2026-06-19 — Strato trend multi-asset sui 52 alt: RIDONDANTE col trend di TP01
Tentativo: aggiungere un terzo sleeve = TSMOM (stessa logica TP01 CANONICAL, long-flat vol-target)
applicato a OGNI alt dei 52 Hyperliquid certificati, equal-weight ragged. Idea: trend più
diversificato che diversifichi TP01 (BTC/ETH). `scripts/portfolio/trend_multiasset.py`.
## Esito: ridondante e peggiore
- **TREND-52 standalone**: FULL Sh 0.66, **HOLD-OUT 1.03** (negativo), anni+ 33%. Gli alt sono
stati long nel calo 2025-26 e hanno sanguinato — a differenza di TP01 (BTC/ETH) che il
vol-target+trend portò in cash. I trend degli alt sono più rumorosi/whippy.
- **corr a TP01 = +0.74** (stessa beta direzionale, come previsto) | corr a XS01 0.05.
- **Contributo al portafoglio (TP01 70 + XS 30):** +TREND-52 w20% → FULL 0.01, **HOLD 0.16**;
w30% → FULL 0.02, **HOLD 0.27**. PEGGIORA.
## Lezione
Broadenizzare il TREND su molti alt NON diversifica: è la **stessa direzionalità** (corr 0.74 con
TP01) su asset più rumorosi → aggiunge perdita/rumore, non edge. La dimensione trend è già catturata
in modo pulito da TP01 (BTC/ETH, vol-targeted). L'unica espansione che diversifica davvero resta
quella **market-neutral** (XS01 cross-sectional), perché è ortogonale alla beta direzionale.
## Conclusione (chiusura del filone "espansione universo")
Esplorate tutte le vie di espansione sui certificati Hyperliquid:
1. XS su 52-all → diluito (memecoin), negativo.
2. XS top-liquidità dinamico → peggiore del fisso-19 (liquidità ≠ qualità).
3. Trend multi-asset su 52 → ridondante (corr 0.74) + hold-out negativo.
Nessuna migliora il portafoglio. **Configurazione validata e invariata: TP01 70% + XS01 (19 major)
30% — FULL Sh 1.41 / HOLD 1.15.** I margini reali per crescere NON sono nell'universo crypto-
direzionale (saturo), ma in un MECCANISMO diverso (opzioni VRP, in attesa di dati di stress reali).
+35
View File
@@ -0,0 +1,35 @@
# 2026-06-19 — Affinamento XS01: blend di lookback [30,90]
Come TP01 fonde gli orizzonti 30/90/180, XS01 ora fonde piu' lookback del momentum cross-sectional
(z-score cross-sectional per lookback, mediato) invece del singolo L=30. `scripts/portfolio/xsec_blend.py`.
## Sweep lookback (19 major, 899g) — FULL/OOS/DD/anni+/corrTP
| lookbacks | FULL | OOS25 | DD% | anni+ | corrTP |
|---|---|---|---|---|---|
| [30] (prima) | 0.80 | 1.20 | 21 | 100% | 0.06 |
| [90] | 0.88 | 0.90 | 17 | 100% | 0.05 |
| **[30,90]** | **1.10** | **1.03** | **14** | **100%** | **0.12** |
| [20,40,90] | 0.51 | 0.67 | 25 | 100% | 0.12 |
| [30,60,120] | 0.68 | 0.74 | 16 | 100% | 0.13 |
**[30,90] e' il sweet spot**: fonde i DUE singoli robusti (30 e 90), FULL Sh 0.80→1.10, DD 21→14%,
corr a TP01 0.06→−0.12 (diversifica meglio), 100% anni+. Non e' un cell fortunato: e' la
combinazione dei due lookback gia' validati (anti-overfit, come il multi-orizzonte di TP01).
## Effetto sul portafoglio (TP01 70% + XS01 30%)
| | XS01 [30] | XS01 blend [30,90] |
|---|---|---|
| XS01 standalone FULL / DD | 0.80 / 21% | **1.10 / 14%** |
| Portafoglio FULL Sharpe | 1.41 | **1.48** |
| Portafoglio HOLD-OUT Sharpe | 1.15 | 1.06 |
| Portafoglio DD | 5.2% | **4.6%** |
| ~€/giorno (2k) | +1.65 | +1.78 |
Migliora FULL Sharpe + DD + robustezza (due orizzonti) al costo di un hold-out marginalmente piu'
basso (0.09, dentro il rumore di una singola finestra). Giudizio: il blend e' piu' robusto
(meno dipendente da un singolo lookback) e diversifica meglio -> PROMOSSO.
## Azione
`src/portfolio/sleeves.XS_CFG`: `L=30` -> `lookbacks=(30,90)`; engine `_xsec_returns` usa lo score
blended (media z-score cross-sectional per lookback). **Portafoglio attivo: TP01 70% + XS01 blend
30%, FULL Sh 1.48 / HOLD 1.06 / DD 4.6%.** 12 test ok. Sleeve sempre sui 19 major.
+44
View File
@@ -0,0 +1,44 @@
# 2026-06-19 — Affinamento XS01: gate di dispersione (p30)
Il momentum cross-sectional vive nella DISPERSIONE (winners/losers distanti). In regime compatto
(tutti gli asset insieme) e' rumore. Gate: entra solo se la dispersione cross-section del momentum
supera il percentile ESPANDENTE causale `disp_pct`; altrimenti flat. Sul blend [30,90] dei 19 major.
`scripts/portfolio/xsec_dispgate.py`. (È il concetto del vecchio XS01 pre-reset, disp_min=p50.)
## Sweep soglia (19 major, 899g) — XS01 standalone + contributo portafoglio
| soglia | XS FULL | XS OOS | PORT FULL | PORT HOLD | %flat |
|---|---|---|---|---|---|
| no gate | 1.10 | 1.03 | 1.50 | 1.06 | 0% |
| p15 | 1.32 | 1.39 | 1.64 | 1.36 | 28% |
| p20 | 1.46 | 1.63 | 1.72 | 1.52 | 31% |
| p25 | 1.46 | 1.63 | 1.72 | 1.52 | 31% |
| **p30** | **1.50** | **1.71** | **1.74** | **1.56** | 35% |
| p35 | 1.60 | 1.90 | 1.81 | 1.69 | 37% |
| p40-p50 | 1.0 | 0.8 | 1.36-1.38 | 0.77-0.93 | 42-49% |
**PLATEAU robusto p15-p35** (cinque punti, tutti molto > no-gate); il crollo a p40+ e' OVER-gating
(salta troppo). Scelto **p30** (centro sicuro del plateau, lontano dal cliff p40). Non un knife-edge.
## Effetto sul portafoglio (TP01 70% + XS01 30%)
| XS01 | PORT FULL | PORT HOLD | PORT DD |
|---|---|---|---|
| [30] originale | 1.41 | 1.15 | 5.2% |
| + blend [30,90] | 1.48 | 1.06 | 4.6% |
| + dispersion gate p30 | **1.55** | **1.55** | **4.4%** |
XS01 standalone: FULL 1.10→1.50, HOLD 1.03→1.71, DD 14%→10.8%, ~€/g 1.64→2.36. Il gate alza SIA
FULL SIA hold-out (a differenza del solo blend, che barattava un po' di hold-out).
## Meccanismo + caveat onesti
- **Causale**: soglia = percentile espandente della dispersione PASSATA; nessun look-ahead.
- **Perche' funziona**: tiene XS attivo nei regimi DISPERSI (2025-26, dove gli alt divergono) e flat
nei bull compatti (2024). L'hold-out 2025-26 e' ad alta dispersione -> il gate concentra
l'attivita' di XS proprio li' -> hold-out forte. E' il comportamento voluto, ma NB che il salto
del hold-out riflette anche che il 2025-26 e' stato un regime ad alta dispersione.
- Caveat XS01 invariati: storia ~2.5 anni; STAT-MODE (book 19 gambe non eseguibile a 2k).
## Azione
`src/portfolio/sleeves.XS_CFG`: aggiunto `disp_pct=30`; engine `_xsec_returns` gatea su dispersione.
**Portafoglio attivo: TP01 70% + XS01 (blend [30,90] + gate disp p30) 30% — FULL Sh 1.55 / HOLD 1.55
/ DD 4.4%.** 12 test ok. Affinamenti del SEGNALE (blend + gate) hanno funzionato dove l'espansione
universo no: i margini su XS sono nella struttura del segnale, non nel numero di asset.
@@ -0,0 +1,62 @@
# 2026-06-19 — Espandere l'universo XS01: PIÙ asset DILUISCONO (i 19 major sono il sweet spot)
Richiesta: aggiungere altri asset Hyperliquid certificati per rafforzare XS01 (cross-sectional
momentum). Fatto il lavoro, esito ONESTO: **non rafforza — diluisce.**
## Cosa ho fatto
- Esteso `fetch_hyperliquid.py` a ~54 candidati alt maggiori (mappa Binance auto SYM/USDT, k-prefissi
esclusi). **52 certificati** (cross-venue 4-11 bps vs Binance, flat 0%, storia 2024+): aggiunti
ATOM DYDX APE CRV LDO STX GMX SNX BCH COMP WLD UNI TRX FIL RUNE ENA ORDI JUP WIF PYTH FET AR ETC
ALGO GALA SAND AXS DOT BLUR JTO PENDLE ONDO TAO. Esclusi MKR (delistato HL 2025-09) e FXS
(migrazione Frax 2026-01) via nuovo gate "ultima barra recente".
## Il finding: il cross-section dei 52 è NEGATIVO; i 19 major sono positivi
Stessa finestra (2024-04 → 2026-06, 807g), mom L*/H10:
| Universo | k | FULL Sharpe (L30/L60/L90) |
|---|---|---|
| **52 asset** | 5 | 0.13 / 0.21 / 0.35 |
| **52 asset** | 8-12 | tutti negativi (k grande non aiuta) |
| **19 major** | 5 | +0.30 / +0.36 / **+0.67** (OOS 0.91) |
I ~33 small/new-cap aggiunti (WIF, JUP, ORDI, PYTH, TAO, GALA, AR, BLUR…) sono idiosincratici/
mean-reverting: il loro rumore **rovescia** il momentum relativo. Cross-sectional momentum su crypto
funziona fra i MAJOR liquidi, non sul long tail. Allargare l'universo NON è gratis.
## Azione
- **XS01 resta sui 19 major** (sweet spot già validato: plateau/fee/subset). Lo sleeve
`_xsec_returns` ora usa una **lista esplicita `XS_UNIVERSE` (19)**, non più glob-all (così
aggiungere parquet certificati non lo cambia/rompe — avevo inavvertitamente fatto vedere allo
sleeve 52 asset = negativo).
- I 52 parquet certificati restano su disco: dato valido per ricerca futura (uno strato diverso —
es. trend-following multi-asset, o un XS ristretto ai top-liquidità — potrebbe usarli), ma NON XS01.
- Portafoglio invariato e ripristinato: **TP01 70% + XS01 30%, FULL Sh 1.41 / HOLD 1.15.**
## Lezione
"Più asset = più robusto" è FALSO per il cross-sectional momentum: il long tail di alt piccoli
diluisce/inverte l'edge. La breadth utile è quella dei major liquidi (corr-strutturata), non il
numero grezzo.
## Tentativo 2: UNIVERSO TOP-LIQUIDITÀ DINAMICO (`xsec_dynuniverse.py`) — anch'esso PEGGIORE
Provato a selezionare a ogni ribilancio i top-N per dollar-volume 30g (causale) dai 52, poi XS
momentum fra quelli (adattivo, ragged-aware). Esito:
| Universo | FULL Sh | OOS25 | anni+ |
|---|---|---|---|
| top12 dinamico (L30H10k5) | 0.65 | 0.54 | 67% (2026 4%) |
| top15/20/25 dinamico | 0.14-0.38 | ≤0.30 | 33-67% |
| **fisso-19 major (L30H10k5)** | **0.80** | **1.20** | **100%** |
| fisso-19 major (L90H10k5) | 0.88 | 0.90 | 100% |
Contributo: TP01+DYN 70/30 = FULL 1.10 / HOLD 0.60 vs **TP01+XS19 = FULL 1.25 / HOLD 1.15**.
**Perché fallisce:** la classifica per dollar-volume ammette comunque i MEMECOIN ad alto volume
(WIF, ORDI, JUP, PEPE...) che hanno volumi enormi ma momentum erratico/mean-reverting →
diluiscono. **Liquidità ≠ qualità** nelle crypto. I 19 major *curati* (established, corr-strutturati,
non solo alto volume) restano il sweet spot.
## Conclusione
Né più nomi (52) né top-liquidità dinamico migliorano XS01. **XS01 resta sui 19 major curati**
(FULL 0.80 / OOS 1.20, 100% anni+). Portafoglio invariato: TP01 70% + XS01 30% (FULL 1.41/HOLD 1.15).
Per rafforzarlo davvero servirebbe una curatela di QUALITÀ (established majors), che è già ciò che i
19 sono. Coerente con la disciplina: nessuna espansione senza che migliori il gauntlet. I 52 parquet
certificati restano per ricerca futura (es. trend multi-asset, dove il long tail non diluisce).
@@ -0,0 +1,167 @@
# Sweep "strategie alternative su Deribit" — 104 ipotesi, 153 agenti (2026-06-20)
## Cosa
Ondata di ricerca onesta richiesta esplicitamente con >=100 agenti: **studiare strategie di
trading ALTERNATIVE** a TP01/XS01/VRP01 sull'universo certificato Deribit (**BTC/ETH** OHLCV +
**DVOL**). Catalogo di **104 ipotesi distinte** su 11 famiglie, **un agente-finder per ipotesi**,
poi **verifica avversariale a 3 scettici** per ogni finding promettente, poi sintesi. Totale
**153 agenti**, ~5.86M token, ~2h (workflow `scripts/research/alt/wf_altstrat.js`,
run `wf_0f3659fc-809`).
Famiglie: BRK (breakout/canali), TRD (trend non-TSMOM), MRV (mean-reversion gated), VOL (DVOL +
vol realizzata, Deribit-specific), XAS (cross-asset BTC/ETH: ratio/lead-lag/cointegrazione/RS),
SEA (stagionalità/ora-del-giorno), RSK (overlay difensivi), OPT (strutture opzioni modellate su
DVOL), MIC (microstruttura/candele), STA (ML walk-forward), CMB (combinazioni/filtri).
## Harness condiviso (nuovo, validato)
`scripts/research/alt/altlib.py` — libreria di valutazione ONESTA e **vettoriale** usata da tutti
gli agenti, così il no-look-ahead è strutturalmente impossibile:
- `eval_weights(df, target)`: posizione decisa con dati `<= close[i]`, **tenuta durante la barra
i+1** (lo shift lo fa la libreria), fee su turnover, **fee-sweep** 0.000.30% RT incorporato.
- `study_weights/study_signals`: ogni ipotesi girata su **entrambi gli asset** + **HOLD-OUT 2025+**
+ per-anno, con verdetto conservativo PASS/WEAK/FAIL (richiede min-asset full>=0.5 **e** hold>=0.2
**e** sopravvivenza fee).
- DVOL allineato **causalmente** (`merge_asof` backward), storia dal 2021-03.
- **Calibrazione:** la replica TSMOM riproduce i numeri noti leak-free di TP01 (BTC full 1.12 /
hold 0.31, DD 77%→23%); buy&hold correttamente FALLISCE l'hold-out (full 0.79, hold 0.37).
104 script riproducibili in `scripts/research/alt/runs/`.
## Esito — NIENTE di nuovo batte o diversifica lo stack esistente
Su 104 ipotesi: **16 promettenti**, **1 sola sopravvissuta** alla verifica avversariale (STA05),
e anch'essa **ridondante/non deployabile**. È il risultato pulito e atteso per un progetto al suo
**soffitto strutturale BTC/ETH-direzionale ~1.3** (già documentato). Lo stack
**TP01 (55%) + XS01 (25%) + VRP01 (20%) resta imbattuto** da questa ondata.
Il segnale ricorrente: decine di trend-follower prendono **FULL Sharpe alto (~1.01.3)** ma
**HOLD-OUT 2025 negativo** (Supertrend, ADX-EMA, Heikin-Ashi, Turtle, SMA200-regime,
Donchian+Chandelier, Kalman, OBV, body-ratio, ...): è **trend-beta del toro**, non alpha, e si
rompe nell'hold-out. I PASS apparenti erano quasi tutti **(a)** singola cella fortunata
sull'hold-out, oppure **(b)** TP01/TSMOM con un overlay attaccato sopra.
### L'unico sopravvissuto: STA05 — EWMA-cross ensemble vote (LEAD, non sleeve)
Voto d'insieme su 13 coppie EMA (fast {5,10,20,40} × slow {40,80,120,200}, fast<slow),
posizione = voto medio firmato, vol-target 20%/cap 2x, 1d. Verifica: **leak-free** (perturbazione
barre future = 0), **plateau** di parametri, **non** fortuna di un singolo anno (jackknife
drop-one-year 0.550.96), sopravvive fee a 0.30% RT. Ho rieseguito il **blend test** raccomandato
(50/50 BTC+ETH, mia stessa griglia di TP01, fee 0.10% RT):
| variante | FULL Sh | DD | HOLD Sh | corr→TP01 (full/hold) |
|---|---|---|---|---|
| TP01 (canonico, controllo) | **+1.30** | 14.3% | +0.31 | — |
| STA05 long-only | +1.24 | 16.3% | +0.21 | **0.93 / 0.94** → ridondante |
| STA05 **long-short** | +0.87 | 28.6% | **+0.86** | **0.71 / 0.53** |
Blend TP01+STA05_LS: `0.75·TP01 + 0.25·LS`**FULL 1.24, HOLD 0.31→0.59, DD 16.1%**;
`0.50/0.50` → FULL 1.13, **HOLD 0.75**, DD 18.8%.
**Lettura onesta (più precisa della sintesi del workflow, che lo aveva liquidato come "dominato
su ogni asse"):** la versione **long-only** è ridondante con TP01 (corr 0.94). La versione
**long-short** invece è solo moderatamente correlata (**0.53 nell'hold-out**) e **migliora
davvero l'hold-out del blend** (0.31→0.59 a peso 25%), al costo di un po' di FULL Sharpe
(1.30→1.24) e DD (14%→16%). MA: l'hold-out è **solo 536 giorni** (include lo stub 2026 corto) →
classica trappola "bello OOS ma OOS breve", e standalone ha DD 28.6%. **Verdetto: LEAD da
monitorare forward, NON deploy, NON sleeve confermato.** Da rivalutare quando l'hold-out cresce.
## Famiglie confermate MORTE / ridondanti (negativi onesti)
- **BRK** breakout (Donchian/Keltner/Bollinger/ORB/NR7/inside-bar): ogni variante rompe l'hold-out
BTC; l'unico PASS (BRK04) è cella singola overfit con maxDD 63%.
- **TRD** trend non-TSMOM: tutto trend-beta del toro ridondante con TP01; i 4 PASS (TRD02/07/08/10)
sono fortuna di singola cella sull'hold-out, dominati dal TSMOM.
- **MRV** mean-reversion: la crypto **tende, non torna**; molti negativi anche a fee zero, **0 PASS**
→ conferma su dati certi la lezione v2.0.0 ("il fade è artefatto").
- **VOL** gate/overlay DVOL su TSMOM: ogni overlay (VOL03/04/08/09/11) è **peso morto netto-negativo**;
la parte robusta è sempre TP01 nudo, la componente DVOL/EWMA aggiunge anti-valore.
- **XAS** spread BTC/ETH (ratio/lead-lag/cointegrazione/RS/dual-mom): gli spread **tendono non
revertono** (negativi a fee zero); le "rotazioni" PASS (XAS03/04/09) sono TP01 travestito con
selezione fortunata sull'hold-out.
- **SEA** stagionalità: fee-killed a 1h, artefatti di regime a 1d, nessun hold-out cross-asset.
- **RSK** overlay di rischio (circuit breaker/kill-switch/DD-scaling/inverse-vol RP): o seguono il
prezzo (buy&hold travestito) o aggiungono frizione senza proteggere dove serve.
- **MIC** micro-pattern candele: hold-out crolla cross-asset; l'unico "survivor" MIC05 è l'artefatto
di **un singolo evento** (short del crash 2026-01-29 su ~13 trade).
- **STA** ML su feature di prezzo (Ridge/Logistic/RF/Kalman/SGD/AR1/k-means): nessun potere
predittivo OOS; l'unico PASS (STA05) è l'ensemble di trend = TP01.
- **CMB** combinazioni: ogni combo è TP01 più un filtro che distrugge valore.
- **OPT** strutture opzioni (modellate su DVOL ATM, niente skew): code severe (ETH maxDD 96% su
iron condor), **lead-only** al meglio → conferma la regola VRP01 "niente short-vol da modello in
deploy". Numeri tipo OPT02/OPT04 hold-out 2.4/1.96 sono artefatto del premio modellato + asset
asimmetrico (ETH fallisce) → giustamente NON promettenti.
## Lezioni metodologiche (azionabili)
1. **L'harness deve premiare lo Sharpe MARGINALE vs un baseline TP01, non lo Sharpe ASSOLUTO.**
`study_weights` valuta lo Sharpe assoluto: così ogni overlay-su-TSMOM **eredita** lo Sharpe di
trend di TP01 e prende un PASS fasullo (VOL03/04/08/09/11, CMB04/06). Per la prossima ondata:
valutare il **contributo incrementale** rispetto a TP01 nudo, così gli overlay non possono
ereditare un PASS.
2. **Prima di gradare PASS, esigere (a) un PLATEAU di parametri (non una cella isolata) e (b) un
jackknife drop-one-month / drop-best-day sull'hold-out.** Questi due check da soli hanno ucciso
**13 dei 14** falsi positivi in verifica avversariale.
3. La verifica avversariale a 3 scettici con angoli diversi (leak / overfit-robustezza /
plausibilità-economica-vs-TP01) ha funzionato: ha distinto i 15 falsi positivi dall'1 robusto.
## Raccomandazione
**Non aggiungere nulla di questa ondata al portafoglio live.** Lo spazio
**BTC/ETH-direzionale single-asset è esaurito**: ogni PASS era hold-out-fitting o un overlay su TP01.
Redirigere il budget di ricerca verso **meccanismi davvero diversi** dove il soffitto non morde:
espandere/monitorare forward **XS01** (cross-sectional sui 51 alt Hyperliquid certificati — l'unico
che abbia mai battuto il soffitto) e **VRP01 reale** (quando cerbero-bite cattura skew live + uno
stress). Tenere **STA05_LS** in lista LEAD per il forward-monitor dell'hold-out.
Artefatti: `scripts/research/alt/altlib.py`, `scripts/research/alt/runs/*.py` (104),
`scripts/research/alt/wf_altstrat.js`, verifica blend `/tmp/verify_sta05.py`.
## Follow-up — MARGINAL SCORER implementato (non più solo raccomandazione)
La lezione #1 ("valutare lo Sharpe MARGINALE vs baseline TP01, non assoluto") è ora **codice**
in `altlib.py`:
- `tp01_baseline_daily()` — TP01 CANONICAL 50/50 BTC+ETH, rendimenti netti giornalieri (cache).
Riproduce il canonico (full 1.30 / hold 0.31) — bloccato da test.
- `marginal_vs_tp01(cand_daily)` — corr a TP01 (full/hold), **uplift del blend** (Sharpe di
TP01+w·cand meno TP01, full & hold-out, w∈{0.25,0.5}), **beta a TP01 + alpha residua** (parte
ortogonale al trend), e un **verdetto**: ADDS / REDUNDANT / DILUTES / NEUTRAL.
- `study_marginal(name, target_fn)` — valuta un candidato **sia** in assoluto (`study_weights`)
**sia** marginale; `earns_slot = (abs_grade != FAIL) AND (marginal_verdict == ADDS)`.
- Convenzione pulita `target_fn(df, asset)` (via `_call_target`) per le strategie DVOL/cross-asset
— niente più inferenza-asset hacky (il VOL03 dell'agente la sbagliava, usava DVOL BTC anche per ETH).
- Demo riproducibile `scripts/research/alt/marginal_demo.py` + test `tests/test_marginal_scorer.py`.
**Dimostrazione (la prova che il fix discrimina):**
| candidato | assoluto | marginale | earns_slot |
|---|---|---|---|
| TP01-itself (sanity) | WEAK | REDUNDANT (corr 1.0, uplift 0) | False |
| **STA05 long-short** (il lead) | PASS | **ADDS** (corr-hold 0.53, blend-hold +0.29) | **True** |
| STA05 long-only | WEAK | REDUNDANT (corr 0.93/0.94) | False |
| VOL03 DVOL-gated TSMOM (overlay) | WEAK | NEUTRAL (corr 0.93, uplift triviale) | False |
| **CMB04 momentum+low-vol (overlay)** | **PASS** | **NEUTRAL** (corr 0.94) | False |
Il punto chiave è l'ultima riga: **CMB04 prendeva un PASS assoluto col vecchio harness, ma il
marginal scorer lo declassa correttamente** — il suo "Sharpe 1.0" è trend di TP01 ereditato al 94%,
non alpha nuovo. Regola operativa d'ora in poi: una nuova strategia direzionale BTC/ETH si giudica su
`study_marginal` (earns_slot), non sullo Sharpe assoluto.
## "Resta qualche candidato?" — gate marginale + jackknife su TUTTI i contendenti forti
Passati i 7 promettenti più forti non-ancora-marginal-testati (`marginal_remaining.py`):
Vortex/Hull (FAIL nella ricostruzione pulita), VOL11 kill-switch (corr 0.94 → REDUNDANT), XAS03/09
rotazioni (NEUTRAL, anzi RS-rotation **diluisce** l'hold-out 0.20), **TRD07 KAMA** e **VOL08**
(entrambi marginale=ADDS). Ma il marginal-point-estimate **può essere ingannato da un singolo mese**:
ho aggiunto al gate il **jackknife OOS** (`robust_oos` = uplift positivo nell'anno OOS pulito 2025
**e** sopravvive al drop-best-month). Risultato:
| candidato | clean-2025 uplift | drop-best-month | robust_oos | earns_slot |
|---|---|---|---|---|
| TRD07 KAMA | +0.089 | **0.034** | False | **False** (era ADDS!) |
| VOL08 RV-term | +0.158 | +0.034 | True | **True** |
| STA05 long-short | +0.039 | +0.131 | True | True (ma 2025 ~0, il grosso è lo stub 2026) |
**KAMA è il falso-positivo istruttivo:** ingannava il marginal scorer (uplift +0.056) ma muore al
jackknife (0.034 togliendo il mese migliore) → il gate rinforzato (`earns_slot` ora esige
`robust_oos`) lo uccide correttamente. Codificata così la lezione #2 in `marginal_vs_tp01`.
### Verdetto finale: NESSUN candidato deployabile
Dopo il gate più severo (abs≠FAIL + marginale=ADDS + jackknife OOS), i 104 collassano a **2 LEAD
fragili**: **VOL08** (overlay term-structure di vol realizzata) e **STA05_LS** (ensemble EMA
long-short). Entrambi sono **famiglia-trend su BTC/ETH** (non un meccanismo nuovo), moderatamente
correlati a TP01 (0.530.61 hold-out), con uplift piccolo e concentrato su un OOS di ~1.5 anni →
**forward-monitor, NON sleeve.** E sono correlati tra loro (entrambi trend) → di fatto **un solo
tema**: "una costruzione di trend-timing alternativa, modestamente decorrelata a TP01 nel 2025-26".
La diversificazione vera resta fuori dallo spazio direzionale single-asset (→ XS01 / opzioni reali).
@@ -0,0 +1,86 @@
# 2026-06-20 — Correzione estrazione cerbero MCP: il backfill sintetico (vol=0) ingannava la certificazione
## Contesto
Richiesta: "analizza cerbero MCP correggendo l'estrazione dati storici secondo le analisi fatte".
Le analisi del progetto avevano già fissato un principio — *"storia nativa Hyperliquid solo dal 2024,
pre-2024 = backfill, volume 0"* — e `fetch_hyperliquid.py` lo gestiva con un floor `START=2024-01-01`.
**Il floor non basta.**
## Il difetto
`fetch_hl` chiedeva a cerbero MCP `get_historical` dal 2024-01-01 e certificava ogni asset con tre
gate: **flat-bar** (O==H==L==C), **cross-venue** (mediana |close Binance| < 60 bps), **recency**.
Nessuno guardava il **volume**. Risultato: gli asset listati su HL *dopo* lo START passavano come
PULITO pur essendo in gran parte **backfill sintetico**.
Ispezione del volume sui parquet (leading run di barre a volume 0):
| asset | barre | leading vol=0 | primo trade reale | % sintetico |
|---|---|---|---|---|
| **AXS** | 902 | **748** | 2026-01-18 | **82.9%** |
| ALGO | 902 | 338 | 2024-12-04 | 37.5% |
| SAND | 902 | 338 | 2024-12-04 | 37.5% |
| AR | 902 | 58 | 2024-02-28 | 6.4% |
| ETC | 902 | 11 | 2024-01-12 | 1.2% |
| BTC/ETH + 19 major | 902 | 0 | 2024-01-01 | 0% |
AXS era **certificato PULITO** (flat 0%, cross-venue 9.5 bps) pur avendo solo ~5 mesi di trading reale.
## Verifica diretta su cerbero MCP (token mainnet)
Interrogato l'endpoint `cerbero-mcp.tielogic.xyz/mcp/tools/get_historical` (bot-tag
`pythagoras-mainnet`):
- **BTC**: 902 barre, leading vol=0 = 0, volume reale dal 2024-01-01 (V=699, 2437, 5306…). Nativo. ✓
- **AXS**: 902 barre, **748 leading vol=0**, primo vol>0 = 2026-01-18. Le barre a volume 0 hanno
prezzi (O/H/L/C) che **coincidono con Binance**:
| data | cerbero close | binance close | Δ |
|---|---|---|---|
| 2024-01-01 | 9.262 | 9.26 | 2.2 bps |
| 2024-01-02 | 8.949 | 8.94 | 10.1 bps |
| 2024-01-03 | 7.937 | 7.95 | 16.4 bps |
**Diagnosi provata:** cerbero MCP riempie il periodo pre-quotazione con barre **sintetiche — volume 0,
prezzi copiati da un venue di riferimento (Binance)**. Per questo i vecchi gate venivano ingannati:
- cross-venue passa → i prezzi *sono* Binance (Δ 116 bps);
- flat passa → le barre non sono flat (hanno movimento di prezzo);
- ma **volume 0** → su HL quelle candele **non erano negoziabili**. È esattamente il caso v2.0.0
(edge su un book che non c'era).
## Correzione (`scripts/analysis/fetch_hyperliquid.py`)
1. **Il VOLUME è il rivelatore del backfill**`trim_backfill()` taglia il run iniziale di barre a
volume 0; si tiene solo la **serie nativa**.
2. **Gate storia nativa** `MIN_NATIVE_DAYS=365`: dopo il taglio serve ≥ 1 anno di vita reale →
scarta chi è troppo corto (AXS, 154 barre reali → fuori).
3. **Gate vol=0 interno** `INTERIOR_VOL0_MAX=5%`: gap di liquidità oltre il taglio iniziale.
4. **cross-venue/flat ricalcolati SOLO sulle barre reali** (non più sui sintetici).
5. **I parquet degli asset scartati vengono rimossi** (disco == set certificato; niente file
contaminati a riposo).
## Risultato
- Universo certificato: **52 → 51** (AXS scartato).
- ALGO/SAND (338 barre), AR (58), ETC (11) ripuliti dal backfill → ora start reale corretto.
- **I 19 major di XS01 hanno 0 backfill → invariati**: la strategia live (`XS_UNIVERSE` esplicito) NON
è toccata. Verificato: portafoglio 3-way (TP01+XS01+VRP01) gira identico, FULL Sh 1.68 / HOLD 1.67.
- Re-fetch end-to-end su cerbero reale: 51 PULITO, sweep su tutti i file → 0 backfill residuo.
## Nota su una conclusione precedente
Il diario `2026-06-19-xsec-universe-expansion.md` concludeva "cross-section dei 52 = negativo". Quella
finestra includeva i sintetici (AXS 83%, ALGO/SAND 37% di barre vol=0 con ritorni non eseguibili): la
magnitudine del risultato era **in parte un artefatto**. La conclusione qualitativa (il long-tail
diluisce XS01; i 19 major sono il sweet spot) resta valida, ma il numero netto è 51 e il test andrebbe
ri-girato sui dati puliti se si volesse riusare quell'universo.
## Lezione
`flat` + cross-venue **non bastano** a certificare un feed che fa backfill copiando un altro venue: il
backfill è plausibile sui prezzi proprio perché è copiato. Il **volume** (=liquidità reale) è il gate
che mancava. Coerente con la regola di prim'ordine v2.0.0: certificare il dato — anche il *volume*,
non solo il prezzo — prima della strategia.
File: `scripts/analysis/fetch_hyperliquid.py`. Universo: `data/raw/hl_*_1d.parquet` (51, serie native).
@@ -0,0 +1,93 @@
# 2026-06-20 — Analisi strategie FinanceOld + VRP v2 (defined-risk + gate IV-rank)
## Contesto
Richiesta: analizzare le strategie in `../FinanceOld`, provare a migliorarle, testarle su dati storici.
Quattro progetti esaminati. Verdetto di **backtestabilità onesta** sui dati certificati (BTC/ETH
Deribit mainnet + DVOL):
| Progetto | Strategia | Backtestabile sui dati certi? |
|---|---|---|
| **FundingRateArbitrage** | Spread funding cross-exchange (perp-perp, spot-hedge) | ❌ Nessun dato funding storico nel repo (solo `exchange_settings.json`). Edge = differenza cross-venue, non ricostruibile. |
| **Polybot** | Latency-arb Polymarket (BS digital-option) + sure-bet delta-neutral | ❌ `dataVPS/collector.db` (645MB) ha solo **~3 giorni** di `poly_books`+`funding`, e la tabella `ticks` (prezzi perp = cuore dell'edge) è **corrotta** ("database disk image is malformed"). L'edge è la latenza: non riproducibile su barre OHLC comunque. |
| **OptionSpalping** (→Cerbero) | LLM autonomo su opzioni Deribit + perp Hyperliquid | ⚠️ È un agente LLM, non una regola meccanica. Il *concetto* (income short-vol su Deribit) è testabile. |
| **OptionsAgent** | **Bear Call Spread + Long VIX hedge** su IWM, con 5 gate d'ingresso | ✅ Il *concetto* (vendi premio rischio-definito, incassa VRP, gate su IV-rank/regime) mappa direttamente sul nostro `options_vrp_lab.py`. |
→ Scelta operatore: **focus VRP opzioni**. L'unico filone con dati veri + metodologia onesta.
## Baseline (options_vrp_lab.py, ora con fee)
Vendita put NUDA settimanale delta -0.28, premio BS su DVOL reale. f = premio_reale/modellato.
- `f=1.0` (conservativo): **FULL Sh 0.78, DD 33%, worst-week -16.6%, HOLD-OUT Sh -0.25** → muore OOS.
- Il rischio è la **CODA**: worst-week su LUNA (2022-06), crash 2021-05. Anno 2022 = -9%.
## VRP v2 — 3 idee di OptionsAgent portate nel framework
Nuovo script `scripts/research/options_vrp_v2.py`. Tutto **causale** (strike/premio/gate da dati
≤ sell-date; payoff a scadenza sui prezzi certificati). Fee opzioni Deribit modellate (12.5% del
premio netto per round-trip = cap del fee reale). Capitale = strike corto (cash-secured) per
entrambe le strutture → DD/worst comparabili.
1. **Rischio definito (PUT CREDIT SPREAD)** — vendi put -0.28, COMPRI put -0.10. Il long wing
**cappa la coda per costruzione**: worst-week -16.6% → **-7.4%**, DD 33% → 21%, Sh 0.78 → 0.99.
2. **Gate IV-RANK > 0.30** (cond. d'ingresso di OptionsAgent) — vendi vol solo quando ricca
(percentile espandente causale di DVOL). Trada il **58%** delle settimane → **Sh 1.35** e
ribalta **HOLD-OUT da -0.25 a +0.28**. È l'alpha vero: il filtro di regime, non la struttura.
3. **Crash-skip IV-rank > 0.90** (NO-GO, come "VIX>35" di OptionsAgent) — marginale da solo.
4. **Gate VRP>0** (DVOL>RV30 causale) — marginale (il VRP è >0 il 78% del tempo, poco selettivo).
### Risultati chiave (book 50/50 BTC+ETH, f=1.0 conservativo)
| Config | FULL Sh | DD | worst-wk | HOLD-OUT Sh | attivo |
|---|---|---|---|---|---|
| naked (baseline) | 0.78 | 33% | -16.6% | **-0.25** | 100% |
| spread | 0.99 | 21% | -7.4% | -0.26 | 100% |
| spread + ivr30 | **1.35** | 14% | -7.4% | **+0.28** | 58% |
| **COMBO** (spread+vrp+ivr30+crashskip) | 1.10 | 12% | -7.4% | **+0.60** | 41% |
COMBO f=1.0 per-anno: 2021 +26%, 2022 **-6%**, 2023 +2%, 2024 +18%, 2025 -0%, 2026 +5%
(il 2022, anno-crash che dimezzava il nudo, è quasi piatto: la coda è tagliata).
A `f=1.29` (skew reale misurato in regime calmo) la COMBO fa FULL Sh 1.87 / HOLD 1.45 / DD 9%.
### Contributo al portafoglio (COMBO f=1.0 vs TP01)
- Corr settimanale **+0.07** (scorrelato, come il VRP nudo).
- TP01 70% + OPT 30% → Sh **1.00** (TP01 solo 0.73), DD **7%**.
- TP01 50% + OPT 50% → Sh **1.19**, DD 7%.
## Conclusione onesta
Le idee di OptionsAgent **migliorano davvero** lo sleeve VRP, in modo OOS-robusto:
- la **struttura defined-risk** taglia la coda (worst -16.6%→-7.4%, DD -19pt) → meno dipendenza dal
f di stress, che era il rischio non catturato del lead nudo;
- il **gate IV-rank** è l'alpha: ribalta l'HOLD-OUT da negativo a positivo vendendo solo vol ricca.
Resta un **lead, non un deploy**: premio MODELLATO su DVOL ATM (skew non esplicito), book a 1d, e
serve la catena reale (cerbero-bite) per il f di stress in un crash. Ma è un miglioramento netto,
quantificato e onesto, del miglior lead income che avevamo. Prossimo passo: rivalutare il f di stress
quando cerbero-bite cattura un crash, e validare lo skew reale sul long wing (-0.10).
Script: `scripts/research/options_vrp_v2.py`. Baseline: `scripts/research/options_vrp_lab.py`.
## Integrazione come sleeve (VRP01)
La COMBO è stata integrata nel portafoglio come **VRP01** (`src/portfolio/sleeves._vrp_combo_returns`,
`vrp_sleeve()`). Implementazione self-contained in `src/` (niente import da `scripts/`): pricing BS +
strike-from-delta + gate causali inline, DVOL da `data/raw/dvol_*.parquet`.
**Settimanale → giornaliero (onesto):** il rendimento settimanale è piazzato sul **giorno di
scadenza**, 0.0 sugli altri giorni dello span. Questo PRESERVA lo Sharpe annualizzato (niente
smoothing che gonfierebbe il daily Sharpe) e tiene lo sleeve presente ogni giorno → peso costante
nell'outer-join del portafoglio. Verificato: lo sleeve daily replica i numeri settimanali
(FULL Sh 1.09, HOLD 0.60, DD 12%), corr daily vs TP01 = +0.01.
**Pesi (per evidenza, engine reale):** TP01+VRP01 monotòno fino al 40% VRP (FULL 1.30→1.55,
HOLD 0.31→0.52, DD fermo 14%). Essendo VRP un lead MODELLATO (non deploy pieno), non lo sovrappeso:
registry = **TP01 0.55 / XS01 0.25 / VRP01 0.20** (TP01 resta maggioranza, l'unico deployable pieno).
La validazione 3-way completa richiede i dati Hyperliquid (XS01, gitignored, token Cerbero) → gira
locale con `scripts/portfolio/run_portfolio.py`.
Test: `tests/test_vrp_sleeve.py` (5 pass: monotonìa BS, ordering strike, determinismo+griglia
giornaliera, gate riducono l'attività, coda tagliata <-15%).
@@ -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.
+111
View File
@@ -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 (1W) mantenendone le proporzioni, e aggiunge PREVDAY a
peso W. Sweep W ∈ {0,5,10,15,20%}. PREVDAY = libro 1h breakout-continuation, parametri congelati,
50/50 BTC+ETH, fee 0.10% RT. Outer-join del portafoglio: PREVDAY dal 2018, VRP01 dal 2021, XS01 dal
2024 → nel 2019-20 PREVDAY pesa di fatto >W (solo TP01 accanto); nell'hold-out 2025+ (tutti e 4
attivi) pesa esattamente ~W → **l'HOLD-OUT è il confronto pulito a "10%"**.
## Sweep peso overlay
| peso PREVDAY | FULL Sharpe | FULL DD | HOLD Sharpe | HOLD DD | HOLD ret |
|--------------|------------:|--------:|------------:|--------:|---------:|
| BASELINE (55/25/20) | 1.68 | 14.3% | 1.66 | 3.4% | +16.7% |
| 5% | 1.80 | 11.1% | 1.83 | 3.3% | +17.8% |
| **10%** | **1.88** | **9.9%** | **1.97** | 3.3% | **+19.0%** |
| 15% | 1.93 | 10.3% | 2.06 | 3.3% | +20.2% |
| 20% | 1.95 | 10.6% | 2.09 | 3.4% | +21.4% |
(PREVDAY standalone: FULL Sh 1.23 / DD 26.7%; HOLD Sh 1.28 / DD 10.8%.)
## Lettura a 10%
- **FULL: Sharpe +0.20 (1.68→1.88), maxDD 14.3%→9.9% (4.4pp ≈ 31%).** Comportamento da tail-hedge:
la gamba short ammortizza i crash storici (2019-21, 2022).
- **HOLD-OUT: Sharpe +0.31 (1.66→1.97), ret +16.7%→+19.0%, DD 3.4%→3.3% (già bassissimo).** Nel
regime recente il beneficio è rendimento/Sharpe, non taglio DD.
- **10% ≈ ottimo di DD.** Oltre, lo Sharpe sale ancora (1.93→1.95) ma il maxDD FULL smette di
scendere (10.3→10.6%): stai solo aggiungendo rischio direzionale short. Argomento per ~10% in
chiave hedge (massimizza il taglio-coda per unità di rischio aggiunto).
## Per anno (baseline → overlay 10%)
| anno | ret | DD |
|------|-----|----|
| 2019 | +11.3 → +15.2 | 10.3 → 8.2 |
| 2020 | +51.1 → +53.1 | 8.4 → 6.3 |
| 2021 | +32.5 → +28.3 | 5.2 → 4.3 |
| 2022 | 3.0 → 1.6 | 3.7 → 3.0 |
| 2023 | +11.2 → +11.4 | 9.2 → 9.9 |
| 2024 | +24.4 → +25.7 | 3.9 → 3.4 |
| 2025 | +12.0 → +12.0 | 3.4 → 3.3 |
| 2026 | +4.2 → +6.2 | 2.6 → 2.2 |
Migliora o pareggia quasi ovunque; costa solo nel toro 2021 (premio d'assicurazione atteso per un
hedge) e leggermente sul DD 2023; paga nel bear 2022 e nel 2026.
## Verdetto
L'overlay 10% è **attraente in simulazione** — taglia il drawdown FULL di ~31% e alza l'hold-out
Sharpe +0.31, con 10% vicino all'ottimo di DD. Ma:
1. **È in-sample.** I guadagni assumono che l'edge di PREVDAY persista — il forward-monitor esiste
proprio per verificarlo. Questa simulazione quantifica il PRIZE, non lo prova.
2. **Outer-join:** il taglio-DD storico è gonfiato dal peso effettivo >10% nel 2019-20; il read
pulito a 10% è l'hold-out (prize = Sharpe +0.31).
3. Incidentale: il 3-way TP01+XS01+VRP01 baseline qui fa FULL 1.68 / HOLD 1.66.
**Azione: nessuna.** PREVDAY resta FORWARD-MONITOR (registry di produzione invariato). Quando il
track record forward avrà ~2-3 mesi, ri-valutare l'overlay 10% con la stessa metrica (taglio-DD +
hold-out Sharpe) su dati VERAMENTE fuori campione. Lo script è il riferimento per quel confronto.
@@ -0,0 +1,64 @@
# PREVDAY — la fee viene dai FLIP (no free lunch sul turnover) + è un HEDGE, non alpha
**Data:** 2026-06-21 (follow-up di `2026-06-21-prevday-fill-haircut.md`)
**Script:** `scripts/research/intraday/prevday_turnover.py`
**Esito:** (1) ridurre il turnover di PREVDAY erode l'edge — la config congelata è già efficiente.
(2) Il test long-only inchioda il blocker #1: **tutto il valore di portafoglio è la gamba SHORT**
PREVDAY è un **hedge di regime-down**, non alpha. Resta forward-monitor.
## Premessa (da fill_haircut)
Il libro REAL-$600 salta il 98.4% dei ribilanciamenti del vol-target e la fee-drag scende solo
2.49% → 2.39%/anno. Quindi la fee (~2.6%/anno) NON viene dal churn sub-dollaro ma dai **~70 flip di
direzione/anno**. Un deadband d'esecuzione è inutile; l'unica leva è ridurre i flip a LIVELLO DI
SEGNALE. Qui sweep delle leve (buffer, anchor, min-hold) + long-only vs long-short. Libro MODELED
(l'haircut di fill è +0.01, irrilevante). Metrica che conta = **uplift hold-out del blend 80/20**.
## (1) Turnover-reduction — no free lunch
| config | flip/yr | fee/yr | FULL Sh | HOLD Sh | DD | corrTP | blend HOLD upl |
|--------|--------:|-------:|--------:|--------:|---:|-------:|---------------:|
| **BASE** (anchor=1, k=0.30, LS) | 70 | 2.59% | +1.23 | +1.27 | 27% | +0.15 | **+0.56** |
| k=0.50 | 48 | 1.86% | +1.23 | +0.99 | 15% | +0.20 | +0.40 |
| k=0.75 | 32 | 1.31% | +1.06 | +0.13 | 16% | +0.27 | +0.00 |
| k=1.00 | 23 | 1.01% | +0.88 | +0.72 | 22% | +0.36 | +0.22 |
| anchor=2 | 39 | 1.55% | +0.89 | +0.54 | 22% | +0.25 | +0.20 |
| anchor=3 | 27 | 1.14% | +0.67 | 0.18 | 22% | +0.29 | 0.12 |
| anchor=5 | 15 | 0.75% | +1.15 | +0.70 | 19% | +0.41 | +0.25 |
| min_hold=24h | 70 | 2.59% | +1.22 | +1.37 | 32% | +0.15 | **+0.60** |
| min_hold=72h | 65 | 2.39% | +0.86 | +0.67 | 33% | +0.12 | +0.27 |
| combo-LT (k.75+anc2+24h) | 16 | 0.79% | +0.79 | +0.69 | 20% | +0.34 | +0.24 |
- **Allargare buffer/anchor taglia fee e turnover ma l'uplift cala monotonicamente** (k: 0.56→0.40→
0.00). Anchor multi-giorno tutto peggio → conferma il "anchor=1 only" del diario. I flip SONO
l'edge: meno flip = meno edge.
- **min_hold=24h** è l'unico ritocco "quasi gratis" (uplift +0.56→+0.60 a parità di fee) ma
**peggiora il DD 27%→−32%** → non vale cambiare una strategia congelata in forward-monitor.
- **Verdetto: la config base è già sulla frontiera efficiente turnover↔edge. Si lascia congelata.**
## (2) Long-only vs long-short — il blocker #1 inchiodato
| | FULL Sh | HOLD Sh | corrTP | blend HOLD upl | fee/yr |
|--|--------:|--------:|-------:|---------------:|-------:|
| **long-only** (no short) | **+1.55** | +0.52 | **+0.64** | **+0.09** | 1.30% |
| long-short (BASE) | +1.23 | +1.27 | +0.15 | +0.56 | 2.59% |
La versione **long-only ha Sharpe standalone più ALTO** (1.55 vs 1.23) ma è **correlata +0.64 a TP01
e non aggiunge quasi nulla al blend** (+0.09). **Tutto il valore di portafoglio viene dalla gamba
SHORT:** la short *abbassa* lo Sharpe standalone (shortare crypto nel toro 2019-24 perde) ma fornisce
**tutta** la decorrelazione (corrTP 0.64→0.15) e l'uplift hold-out (0.09→0.56).
**PREVDAY non è alpha: è strutturalmente un HEDGE di crash/regime-down.** Costa nel toro, paga
nell'orso (2022, 2025-26 down/chop). È additivo a TP01, che va *flat* nel risk-off ma non *short*.
Questo conferma e affina il blocker #1 dell'onda intraday ("l'uplift viene dai regimi TP01-down"):
non è solo conditional sui regimi down, è **interamente la gamba short** = una scommessa direzionale
che i ribassi continuino.
## Conseguenza sul verdetto
- Niente da ottimizzare: la config congelata è già efficiente; nessun cambio.
- **Riframing utile:** se PREVDAY un giorno avrà un ruolo, è come **overlay di tail-hedge** (non
sleeve-alpha), additivo alla difensività di TP01. Ma resta soggetto agli altri due blocker
(fallisce il null a corr-zero; tail-luck: top-5 giorni = 76-83% del PnL delle gambe revert).
- **Forward-monitor invariato.** Il test forward decisivo: la gamba short continua a pagare fuori da
2022 e 2025-26? Se sì → candidato tail-hedge; se no → era regime-luck.
@@ -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).
+96
View File
@@ -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)
+141
View File
@@ -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.
+66
View File
@@ -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.
+109
View File
@@ -0,0 +1,109 @@
"""EVALUATOR STANDARD per i segnali della ricerca multi-agente (Fase frattale, v2.0.0).
Ogni agente scrive SOLO una funzione `signal(df, asset, tf) -> np.ndarray` (posizione per barra
in [-1,1], decisa entro close[i]) in un file. Questo evaluator la valuta in modo UNIFORME e ONESTO
sull'harness research_lab, e — cruciale — esegue un GUARD ANTI-LOOK-AHEAD automatico: ricalcola il
segnale su prefissi del df e verifica che pos[i] non dipenda da barre future (leak>0 = sospetto).
uv run python scripts/analysis/eval_signal.py <signal_file.py> <BTC|ETH> <5m|15m|1h> [--holdout]
Stampa una riga "RESULT_JSON:{...}" con tutte le metriche (gli agenti riportano quei campi esatti).
"""
from __future__ import annotations
import sys
import json
import importlib.util
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 scripts.analysis.research_lab import backtest, buy_hold, mc_pvalue, load_tf, ts, _net_series, VAL_START, HOLDOUT_START
def load_signal(path):
spec = importlib.util.spec_from_file_location("usig", path)
m = importlib.util.module_from_spec(spec)
spec.loader.exec_module(m)
if not hasattr(m, "signal"):
raise AttributeError("il file non definisce signal(df, asset, tf)")
return m.signal
def causality_guard(signal, df, asset, tf, k=12):
"""Ricalcola il segnale su prefissi df[:i+1] e confronta pos[i] col run completo.
Se differiscono -> il segnale usa dati FUTURI (look-ahead). Ritorna #violazioni (0 = pulito)."""
full = np.asarray(signal(df, asset, tf), float)
n = len(df)
if len(full) != n:
return -1
rng = np.random.default_rng(0)
idx = rng.integers(int(n * 0.6), n - 1, size=k)
bad = 0
for i in idx:
try:
p = np.asarray(signal(df.iloc[:i + 1].copy(), asset, tf), float)
except Exception:
bad += 1; continue
if len(p) != i + 1 or not np.isclose(np.nan_to_num(p[i]), np.nan_to_num(full[i]), atol=1e-6):
bad += 1
return bad
def main():
args = sys.argv[1:]
holdout = "--holdout" in args
args = [a for a in args if a != "--holdout"]
sigfile, asset, tf = args[0], args[1].upper(), args[2]
res = {"asset": asset, "tf": tf, "sigfile": sigfile}
try:
signal = load_signal(sigfile)
df = load_tf(asset, tf)
pos = np.asarray(signal(df, asset, tf), float)
res["n"] = int(len(df))
res["len_ok"] = bool(len(pos) == len(df))
if not res["len_ok"]:
res["error"] = f"len(pos)={len(pos)} != len(df)={len(df)}"
print("RESULT_JSON:" + json.dumps(res)); return
res["finite"] = bool(np.isfinite(np.nan_to_num(pos, nan=0.0)).all())
res["leak"] = int(causality_guard(signal, df, asset, tf))
full = backtest(df, pos, tf)
oos = backtest(df, pos, tf, lo=VAL_START, hi=HOLDOUT_START)
bh = buy_hold(df, tf)
_, p, _, _ = mc_pvalue(df, pos, tf, n=250)
res.update(
implemented=True,
full_sharpe=round(full.sharpe, 3), full_ret=round(full.ret, 3), full_dd=round(full.maxdd, 3),
oos_sharpe=round(oos.sharpe, 3), bh_sharpe=round(bh.sharpe, 3),
gross_sharpe=round(backtest(df, pos, tf, fee_rt=0.0).sharpe, 3),
fee02_sharpe=round(backtest(df, pos, tf, fee_rt=0.002).sharpe, 3),
turnover=round(full.ntrades, 1), exposure=round(full.exposure, 3),
null_p=round(p, 4),
beats_bh=bool(full.sharpe > bh.sharpe and oos.sharpe > 0),
)
# breadth per-anno (pre-hold-out): % anni positivi, anni rossi consecutivi
net, _, _, _ = _net_series(df, pos)
s = pd.Series(net, index=ts(df))
s = s[s.index < pd.Timestamp(HOLDOUT_START, tz="UTC")]
yr = {int(y): float((1 + g).prod() - 1) for y, g in s.groupby(s.index.year)}
vals = list(yr.values())
max_consec_red = 0; cur = 0
for v in vals:
cur = cur + 1 if v < 0 else 0
max_consec_red = max(max_consec_red, cur)
res["per_year_preho"] = {y: round(v, 3) for y, v in yr.items()}
res["pct_years_pos"] = round(sum(v > 0 for v in vals) / len(vals), 2) if vals else 0.0
res["max_consec_red_years"] = int(max_consec_red)
if holdout:
ho = backtest(df, pos, tf, lo=HOLDOUT_START)
res["holdout_sharpe"] = round(ho.sharpe, 3)
res["holdout_ret"] = round(ho.ret, 3)
res["holdout_dd"] = round(ho.maxdd, 3)
except Exception as e:
res["implemented"] = False
res["error"] = f"{type(e).__name__}: {str(e)[:200]}"
print("RESULT_JSON:" + json.dumps(res))
if __name__ == "__main__":
main()
+132
View File
@@ -0,0 +1,132 @@
"""FETCH + CERTIFY universo Hyperliquid (Cerbero MCP MAINNET) — espansione cross-sectional.
Hyperliquid (via cerbero-mcp mainnet) offre ~230 perp liquidi, ma storia nativa REALE solo dal
2024 (pre-2024 = backfill, volume 0). Qui scarico un set liquido a 1d (2024+), e CERTIFICO ogni
asset come BTC/ETH: cross-venue vs Binance (realismo) + flat-bar + VOLUME (liquidita'). Scrivo SOLO
i puliti in data/raw/hl_<sym>_1d.parquet (namespace dedicato, NON mischiato col Deribit BTC/ETH).
Disciplina: Cerbero ci ha gia' bruciato (testnet) -> niente fiducia, solo certificazione.
CORREZIONE estrazione (2026-06-20, "analisi fatte"): il floor START=2024-01-01 NON basta. Cerbero
restituisce BACKFILL SINTETICO (volume==0, ma prezzi copiati da un venue di riferimento -> matchano
Binance e NON sono flat) per il periodo PRIMA che l'asset quotasse davvero su Hyperliquid. Cosi'
asset listati a meta'/fine 2024+ passavano cross-venue+flat ed erano certificati PULITO pur essendo
in gran parte sintetici (es. AXS 83% backfill: trading reale solo da 2026-01; ALGO/SAND 37%). E' lo
stesso errore v2.0.0 (edge su un book che non c'era). Fix: (1) il VOLUME e' il rivelatore di backfill
-> si TAGLIA il run iniziale di barre a volume 0 e si tiene solo la serie NATIVA; (2) gate su storia
nativa minima (>= MIN_NATIVE_DAYS reali) -> scarta chi e' troppo corto dopo il taglio; (3) gate su
volume-0 INTERNO (gap di liquidita') oltre il taglio iniziale; (4) cross-venue/flat ricalcolati SOLO
sulle barre reali; (5) i parquet degli asset scartati vengono RIMOSSI (disco == set certificato).
uv run python scripts/analysis/fetch_hyperliquid.py
"""
from __future__ import annotations
import sys, time
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, requests, ccxt
RAW = PROJECT_ROOT / "data" / "raw"
START = "2024-01-01"; END = pd.Timestamp.now("UTC").strftime("%Y-%m-%d") # dinamico (refresh giornaliero)
MIN_NATIVE_DAYS = 365 # storia NATIVA reale minima (post-taglio backfill) per entrare nell'universo
INTERIOR_VOL0_MAX = 5.0 # % max di barre a volume 0 DOPO il taglio iniziale (gap di liquidita' interni)
# UNIVERSO ESTESO: alt liquidi noti su Hyperliquid (mappa Binance auto = SYM/USDT). Il gate di
# certificazione (cross-venue + liquidita' + flat) scarta i non-conformi. k-prefissi esclusi
# (scaling 1000x complica il cross-venue). MATIC morto escluso.
SYMS = ["BTC","ETH","SOL","BNB","XRP","DOGE","AVAX","LINK","LTC","ADA","ARB","OP","SUI","APT",
"INJ","TIA","SEI","NEAR","AAVE","ATOM","DYDX","APE","CRV","LDO","STX","GMX","SNX","BCH",
"COMP","MKR","WLD","UNI","TRX","FIL","RUNE","ENA","ORDI","JUP","WIF","PYTH","FET","AR",
"ETC","ALGO","GALA","SAND","AXS","DOT","FXS","BLUR","JTO","PENDLE","ONDO","TAO"]
BINANCE = {s: f"{s}/USDT" for s in SYMS}
def _h():
env={}
for ln in open(PROJECT_ROOT/".env.mainnet"):
ln=ln.strip()
if ln and not ln.startswith("#") and "=" in ln: k,v=ln.split("=",1); env[k]=v.strip()
return {"Authorization":f"Bearer {env['CERBERO_TOKEN']}","X-Bot-Tag":env.get('CERBERO_BOT_TAG','fetch'),"Content-Type":"application/json"}
def fetch_hl(sym, H, interval="1d"):
r=requests.post("https://cerbero-mcp.tielogic.xyz/mcp/tools/get_historical",
headers=H, json={"exchange":"hyperliquid","instrument":sym,"interval":interval,
"start_date":START,"end_date":END}, timeout=60)
c=r.json().get("candles",[])
if not c: return pd.DataFrame()
df=pd.DataFrame(c)[["timestamp","open","high","low","close","volume"]]
return df.drop_duplicates("timestamp").sort_values("timestamp").reset_index(drop=True)
def binance_daily(sym_b, start_ms, end_ms):
ex=ccxt.binance({"enableRateLimit":True})
out={}; since=start_ms
while since<=end_ms:
try: r=ex.fetch_ohlcv(sym_b,"1d",since=since,limit=500)
except Exception: break
r=[x for x in r if x[0]>=since]
if not r: break
for x in r:
if start_ms<=x[0]<=end_ms and x[4]: out[int(x[0])]=float(x[4])
nxt=int(r[-1][0])+86400000
if nxt<=since: break
since=nxt
return pd.Series(out)
def trim_backfill(df):
"""Taglia il run INIZIALE di barre a volume 0 (= backfill sintetico pre-quotazione su HL).
Ritorna (serie_nativa, n_barre_tagliate). Il volume e' il rivelatore: il backfill copia i
prezzi da un venue di riferimento (non flat, matcha Binance) ma ha volume 0."""
vol = df["volume"].to_numpy()
lead = int(np.argmax(vol > 0)) if (vol > 0).any() else len(df)
return df.iloc[lead:].reset_index(drop=True), lead
def main():
H=_h()
print("="*100); print(" FETCH + CERTIFY Hyperliquid 1d (Cerbero mainnet) — cross-venue + flat + VOLUME (no backfill)"); print("="*100)
print(f" {'sym':<6}{'reali':>6}{'bfill':>6}{'start_reale':>13}{'flat%':>7}{'vol0%':>7}{'med_bps':>9}{'>1%':>7}{'verdetto':>14}")
certified=[]
for s in SYMS:
path = RAW/f"hl_{s.lower()}_1d.parquet"
raw=fetch_hl(s,H)
if raw.empty:
print(f" {s:<6} vuoto"); path.unlink(missing_ok=True); continue
# --- CORREZIONE: taglia il backfill sintetico (volume 0 iniziale), tieni la serie nativa ---
df, n_bfill = trim_backfill(raw)
if df.empty:
print(f" {s:<6} tutto backfill (vol0) -> scarta"); path.unlink(missing_ok=True); continue
ts=pd.to_datetime(df["timestamp"],unit="ms",utc=True)
flat=((df.open==df.high)&(df.high==df.low)&(df.low==df.close)).mean()*100
vol0=(df["volume"].to_numpy()==0).mean()*100 # gap di liquidita' INTERNI (post-taglio)
# cross-venue vs Binance USDT (daily close) — SOLO sulle barre reali
ref=binance_daily(BINANCE[s], int(df["timestamp"].iloc[0]), int(df["timestamp"].iloc[-1]))
a=df.set_index("timestamp")["close"]
m=pd.concat([a.rename("a"),ref.rename("b")],axis=1,join="inner").dropna()
if len(m)>5:
bps=(m["a"]-m["b"]).abs()/m["b"]*1e4
med=bps.median(); g1=(bps>100).mean()*100
else: med=g1=float("nan")
# gate "delistato/migrato": l'ultima barra dev'essere recente (entro ~21g da END),
# altrimenti l'asset tronca l'universo cross-sectional (es. MKR fermo a 2025-09, FXS 2026-01).
recent = (pd.Timestamp(END, tz="UTC") - ts.iloc[-1]) <= pd.Timedelta("21D")
# gate storia NATIVA: dopo il taglio dev'esserci abbastanza vita reale (es. AXS quotato 2026-01 -> scarta)
native_days = (ts.iloc[-1] - ts.iloc[0]).days
enough = native_days >= MIN_NATIVE_DAYS
clean = (not np.isnan(med)) and med<60 and g1<3 and flat<5 and vol0<INTERIOR_VOL0_MAX and recent and enough
if clean: v="PULITO"
elif not enough: v=f"corto<{MIN_NATIVE_DAYS}g"
else: v="scarta"
print(f" {s:<6}{len(df):>6}{n_bfill:>6}{str(ts.iloc[0].date()):>13}{flat:>6.1f}%{vol0:>6.1f}%{med:>9.1f}{g1:>6.1f}%{v:>14}")
if clean:
df.to_parquet(path, index=False); certified.append(s)
else:
path.unlink(missing_ok=True) # disco == set certificato (niente parquet contaminati a riposo)
print(f"\n CERTIFICATI ({len(certified)}): {certified}")
print(" Scritti in data/raw/hl_<sym>_1d.parquet (namespace dedicato, SERIE NATIVA senza backfill).")
if __name__=="__main__":
main()
+121
View File
@@ -0,0 +1,121 @@
"""ANALISI OPTIONS per BTC/ETH — onesta sui dati REALI disponibili (cerbero-bite mainnet).
Dati: Old/data/options (chain per-strike + dvol + market_snapshots). Finestra ~2026-05-0106-11
(~6 settimane, REGIME UNICO calmo). NON si può validare OOS un edge su opzioni qui; si possono
MISURARE i livelli reali (VRP, premi put, skew, liquidità) e ragionare sull'USO delle opzioni
per il book BTC/ETH certificato. cerbero-bite è ancora vivo -> la fonte continua ad accumulare.
uv run python scripts/analysis/options_analysis.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
OPT = PROJECT_ROOT / "Old" / "data" / "options"
def load(name):
return pd.read_parquet(OPT / name)
def market_snapshots_analysis():
print("=" * 90)
print(" (1) MARKET SNAPSHOTS — VRP, DVOL, funding, dealer-gamma (livelli reali)")
print("=" * 90)
ms = load("market_snapshots.parquet")
t = pd.to_datetime(ms["timestamp"], utc=True, errors="coerce")
print(f" copertura: {t.min()} -> {t.max()} ({len(ms)} righe)")
for a in ("BTC", "ETH"):
d = ms[ms["asset"] == a].dropna(subset=["iv_minus_rv"])
if len(d) == 0:
print(f" {a}: nessun dato"); continue
vrp = d["iv_minus_rv"].astype(float)
dvol = d["dvol"].astype(float)
rv = d["realized_vol_30d"].astype(float)
fund = d["funding_perp_annualized"].astype(float) if "funding_perp_annualized" in d else pd.Series([np.nan])
gam = d["dealer_net_gamma"].astype(float) if "dealer_net_gamma" in d else pd.Series([np.nan])
print(f"\n {a} (n={len(d)})")
print(f" VRP (IV-RV): media {vrp.mean():+.1f} mediana {vrp.median():+.1f} "
f">0 nel {100*(vrp>0).mean():.0f}% del tempo [IV-RV in punti di vol annua]")
print(f" DVOL: media {dvol.mean():.1f} range [{dvol.min():.1f}, {dvol.max():.1f}]")
print(f" Realized30d: media {rv.mean():.1f}")
print(f" Funding perp: media {fund.mean():+.1f}% annuo")
if gam.notna().any():
print(f" Dealer net-γ: >0 nel {100*(gam>0).mean():.0f}% del tempo (>0 = dealer long gamma = mean-rev)")
def chain_analysis(asset):
print("\n" + "=" * 90)
print(f" (2) CHAIN {asset} — premi put protettivi, skew, liquidità (livelli reali)")
print("=" * 90)
ch = load(f"{asset.lower()}_chain.parquet")
for col in ("strike", "bid", "ask", "mid", "iv", "delta", "gamma"):
if col in ch:
ch[col] = pd.to_numeric(ch[col], errors="coerce")
ch["option_type"] = ch["option_type"].astype(str)
dv = load("dvol_history.parquet")
dv = dv[dv["asset"] == asset][["timestamp", "spot"]].copy()
dv["spot"] = pd.to_numeric(dv["spot"], errors="coerce")
# timestamp -> datetime UTC nativo (sono datetime64[tz], NON ms int: to_numeric li romperebbe)
ch["t"] = pd.to_datetime(ch["timestamp"], utc=True, errors="coerce")
dv["t"] = pd.to_datetime(dv["timestamp"], utc=True, errors="coerce")
ch = ch.dropna(subset=["t"]).sort_values("t").reset_index(drop=True)
dv = dv.dropna(subset=["t", "spot"]).sort_values("t").reset_index(drop=True)
# spot causale per timestamp della chain (merge_asof nearest, tolleranza 1h)
ch = pd.merge_asof(ch, dv[["t", "spot"]], on="t", direction="nearest",
tolerance=pd.Timedelta("1h"))
ch = ch.dropna(subset=["spot", "mid", "strike"])
# days-to-expiry
exp = pd.to_datetime(ch["expiry"], utc=True, errors="coerce")
ch["dte"] = (exp - ch["t"]).dt.total_seconds() / 86_400.0
ch = ch[(ch["dte"] > 0.5) & (ch["dte"] < 90)]
ch["money"] = ch["strike"] / ch["spot"]
ch["prem_pct"] = ch["mid"] * 100 # mid è in COIN (frazione del sottostante) -> %-del-notional
# NB: iv è GIÀ in percento (35.94 = 35.94%, coerente col DVOL ~40) -> non riscalare
ch["spread_pct"] = (ch["ask"] - ch["bid"]) / ch["mid"].replace(0, np.nan) * 100
puts = ch[ch["option_type"].str.lower().str.startswith("p")]
calls = ch[ch["option_type"].str.lower().str.startswith("c")]
def band(df, mlo, mhi, dlo, dhi):
s = df[(df["money"] >= mlo) & (df["money"] <= mhi) & (df["dte"] >= dlo) & (df["dte"] <= dhi)]
return s
print(" PUT protettive — premio reale (mid/spot) e liquidità per tenor/moneyness:")
print(f" {'tenor':<10s}{'moneyness':<14s}{'premio%':>9s}{'/mese%':>9s}{'spread%':>9s}{'n':>7s}{'strike?':>9s}")
for dlo, dhi, tn in [(5, 12, "settim."), (18, 45, "mensile")]:
for mlo, mhi, ml in [(0.97, 1.03, "ATM"), (0.88, 0.93, "~10% OTM"), (0.83, 0.88, "~15% OTM")]:
s = band(puts, mlo, mhi, dlo, dhi)
if len(s) == 0:
print(f" {tn:<10s}{ml:<14s}{'':>9s}{'':>9s}{'':>9s}{0:>7d}{'NO':>9s}")
continue
prem = s["prem_pct"].median()
permonth = prem * 30.0 / s["dte"].median()
print(f" {tn:<10s}{ml:<14s}{prem:>8.2f}%{permonth:>8.2f}%{s['spread_pct'].median():>8.1f}%"
f"{len(s):>7d}{'SI':>9s}")
# skew: IV put 10% OTM vs IV call 10% OTM (stesso tenor mensile)
pv = band(puts, 0.88, 0.93, 12, 50)["iv"].median()
cv = band(calls, 1.07, 1.12, 12, 50)["iv"].median()
atmv = band(ch, 0.98, 1.02, 12, 50)["iv"].median()
if pd.notna(pv) and pd.notna(cv):
print(f" SKEW: IV put 10%OTM {pv:.0f}% vs call 10%OTM {cv:.0f}% vs ATM {atmv:.0f}%"
f" -> skew put {pv-cv:+.0f} pt vol (>0 = put care = paura del crash prezzata)")
def main():
market_snapshots_analysis()
for a in ("BTC", "ETH"):
chain_analysis(a)
print("\n" + "=" * 90)
print(" NB: finestra ~6 settimane, REGIME UNICO calmo -> livelli REALI misurabili, ma NESSUN")
print(" edge su opzioni è validabile OOS qui. Vedi commento finale.")
print("=" * 90)
if __name__ == "__main__":
main()
+158
View File
@@ -0,0 +1,158 @@
"""FASE 1 — triage dei 2 superstiti su BTC/ETH, sull'harness onesto (research_lab).
Sul feed pulito solo SH01 (shape-ML) e frammenti HONEST mostravano segnale residuo. Delle
HONEST solo DIP (dip-reversion) è testabile su BTC/ETH (TR01/ROT02 richiedono alt esclusi).
Qui ri-implemento DIP e SH01-shape-ML come SERIE DI POSIZIONE e li passo ai gate onesti
(FULL/OOS-VAL, vs buy&hold, null p-value, sweep fee, griglia). Hold-out 2025+ resta BLOCCATO.
uv run python scripts/analysis/phase1_survivors.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 sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
from src.data.downloader import load_data
from scripts.analysis.research_lab import (
backtest, buy_hold, mc_pvalue, report, VAL_START, HOLDOUT_START, FEE_RT,
)
# ----------------------------- DIP reversion (long-only) -----------------------------
def dip_signal(df, n=50, k=2.0, z_exit=0.0, max_bars=72):
"""Long-only: entra (pos=1) quando lo z-score causale del prezzo vs MA(n) <= -k (dip),
esce quando z>=z_exit o dopo max_bars. Decisione a close[i] (z[i] usa close[i]), guadagna
close[i]->close[i+1]. Niente fill su estremi di candela."""
c = df["close"].values.astype(float)
s = pd.Series(c)
ma = s.rolling(n).mean().values
sd = s.rolling(n).std().values
z = np.where(sd > 0, (c - ma) / sd, np.nan)
pos = np.zeros(len(c))
inpos = False
held = 0
for i in range(len(c)):
if not inpos:
if not np.isnan(z[i]) and z[i] <= -k:
inpos, held = True, 0
pos[i] = 1.0
else:
held += 1
if (not np.isnan(z[i]) and z[i] >= z_exit) or held >= max_bars:
inpos = False # esce al close[i]: pos[i]=0
else:
pos[i] = 1.0
return pos
# ----------------------------- SH01 shape-ML (walk-forward) -----------------------------
def _shape_features(df, W):
"""~12 feature di FORMA causali per barra, dalla finestra che termina a i (usa solo <=i)."""
o = df["open"].values.astype(float); h = df["high"].values.astype(float)
l = df["low"].values.astype(float); c = df["close"].values.astype(float)
s = pd.Series(c)
ret1 = s.pct_change()
rng = (h - l) / np.where(c > 0, c, np.nan)
body = (c - o) / np.where(h - l > 0, h - l, np.nan)
up_sh = (h - np.maximum(o, c)) / np.where(h - l > 0, h - l, np.nan)
dn_sh = (np.minimum(o, c) - l) / np.where(h - l > 0, h - l, np.nan)
# RSI(14)
d = s.diff()
gain = d.clip(lower=0).rolling(14).mean()
loss = (-d.clip(upper=0)).rolling(14).mean()
rsi = 100 - 100 / (1 + gain / loss.replace(0, np.nan))
hi_w = pd.Series(h).rolling(W).max(); lo_w = pd.Series(l).rolling(W).min()
feat = {
"mom_w": s / s.shift(W) - 1.0, # rendimento sulla finestra
"mom_half": s / s.shift(W // 2) - 1.0, # accelerazione
"vol_w": ret1.rolling(W).std(),
"rsi": rsi / 100.0,
"ma_dist": (c - s.rolling(W).mean()) / s.rolling(W).std(),
"pos_in_range": (c - lo_w) / (hi_w - lo_w).replace(0, np.nan), # dove sta il close nel range W
"range": pd.Series(rng).rolling(3).mean(),
"body": pd.Series(body).rolling(3).mean(),
"up_shadow": pd.Series(up_sh).rolling(3).mean(),
"dn_shadow": pd.Series(dn_sh).rolling(3).mean(),
"ret1": ret1,
"skew_w": ret1.rolling(W).skew(),
}
X = pd.DataFrame(feat).values
return X
def shape_ml_signal(df, W=24, H=12, th=0.55, refit=750, warmup=3000, long_short=True):
"""LogisticRegression walk-forward sulla forma. Label = segno del rendimento a H barre.
Al tempo di decisione i si allena SOLO su campioni j con esito già realizzato (j+H <= i):
strettamente causale, nessun leak. Rifit ogni `refit` barre (velocità). pos = +1 se
P(up)>th, -1 se P(up)<1-th (long_short), altrimenti 0."""
c = df["close"].values.astype(float)
n = len(c)
X = _shape_features(df, W)
fwd = np.full(n, np.nan)
fwd[:n - H] = c[H:] / c[:n - H] - 1.0
y = (fwd > 0).astype(float)
valid = ~np.isnan(X).any(axis=1)
pos = np.zeros(n)
model = scaler = None
start = max(warmup, W + H + 200)
for i in range(start, n):
if model is None or (i - start) % refit == 0:
# campioni di training: feature valide E label realizzata entro i (j+H <= i)
tr = np.where(valid & (np.arange(n) + H <= i) & (np.arange(n) >= W))[0]
tr = tr[tr < i - H]
if len(tr) >= 500 and len(np.unique(y[tr])) == 2:
scaler = StandardScaler().fit(X[tr])
model = LogisticRegression(max_iter=200, C=1.0).fit(scaler.transform(X[tr]), y[tr])
if model is not None and valid[i]:
p_up = float(model.predict_proba(scaler.transform(X[i:i + 1]))[0, 1])
pos[i] = 1.0 if p_up > th else (-1.0 if (long_short and p_up < 1 - th) else 0.0)
return pos
# ----------------------------------- run -----------------------------------
def main():
TF = "1h"
print("=" * 90)
print(f" FASE 1 — triage superstiti su BTC/ETH {TF} | netto fee 0.10% RT | hold-out {HOLDOUT_START}+ BLOCCATO")
print("=" * 90)
data = {a: load_data(a, TF) for a in ("BTC", "ETH")}
# ---------- DIP: griglia robustezza (plateau?) ----------
print("\n" + "#" * 90)
print(" DIP reversion (long-only) — griglia FULL Sharpe (plateau = robusto, picco = overfit)")
print("#" * 90)
GRID = [(n, k) for n in (30, 50, 100) for k in (1.5, 2.0, 2.5)]
for a in ("BTC", "ETH"):
df = data[a]
print(f"\n {a}: " + " ".join(
f"n{n}k{k}{backtest(df, dip_signal(df, n=n, k=k), TF).sharpe:>5.2f}" for n, k in GRID))
# report onesto sulla config centrale
for a in ("BTC", "ETH"):
report(f"DIP {a} (n50 k2.0)", data[a], dip_signal(data[a], n=50, k=2.0), TF)
# ---------- SH01 shape-ML: config record + paio di varianti ----------
print("\n" + "#" * 90)
print(" SH01 shape-ML (walk-forward LogReg) — long/short")
print("#" * 90)
for a in ("BTC", "ETH"):
df = data[a]
pos = shape_ml_signal(df, W=24, H=12, th=0.55, long_short=True)
report(f"SH-ML {a} (W24 H12 th.55 L/S)", df, pos, TF)
# variante long-only (meno fee)
pos_lo = shape_ml_signal(df, W=24, H=12, th=0.55, long_short=False)
report(f"SH-ML {a} (W24 H12 th.55 LONG-only)", df, pos_lo, TF)
print("\n" + "=" * 90)
print(" VERDETTO: un edge è REALE solo se FULL e OOS-VAL Sharpe > 0, regge il sweep fee,")
print(" e BATTE il null (p<0.05). Altrimenti = rumore, si chiude.")
print("=" * 90)
if __name__ == "__main__":
main()
+221
View File
@@ -0,0 +1,221 @@
"""FASE 2 — esplorazione larga per famiglie su BTC/ETH, harness onesto (research_lab).
Famiglie (serie di posizione, causali, netto fee, vs buy&hold + null p-value):
TSMOM (momentum) | REVERSAL | MA-cross | DONCHIAN breakout | VOL-TARGET overlay |
LEAD-LAG BTC<->ETH | HURST-gated momentum. Multi-TF dove sensato (1h + 15m).
La barra DA BATTERE è il buy&hold (Sharpe ~0.8 su BTC/ETH): una strategia di timing vale solo
se fa MEGLIO net-fee. Per ogni famiglia: scan griglia (FULL Sharpe), poi report onesto sulla
config migliore. Selezionare il best-di-griglia GONFIA -> i gate veri sono OOS-VAL + null p<0.05.
uv run python scripts/analysis/phase2_families.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.data.downloader import load_data
from scripts.analysis.research_lab import (
backtest, buy_hold, mc_pvalue, window_mask, ts, VAL_START, HOLDOUT_START, BARS_PER_YEAR,
)
# --------------------------------- famiglie ---------------------------------
def tsmom(df, L, mode="ls"):
c = pd.Series(df["close"].values.astype(float))
pos = np.sign(np.nan_to_num((c / c.shift(L) - 1).values))
return np.maximum(pos, 0) if mode == "lo" else pos
def reversal(df, L, mode="ls"):
c = pd.Series(df["close"].values.astype(float))
pos = -np.sign(np.nan_to_num((c / c.shift(L) - 1).values))
return np.maximum(pos, 0) if mode == "lo" else pos
def ma_cross(df, fast, slow, mode="ls"):
c = pd.Series(df["close"].values.astype(float))
ef = c.ewm(span=fast, adjust=False).mean()
es = c.ewm(span=slow, adjust=False).mean()
pos = np.sign((ef - es).values)
return np.maximum(pos, 0) if mode == "lo" else pos
def donchian(df, L, mode="ls"):
h = pd.Series(df["high"].values.astype(float)).rolling(L).max().shift(1).values
l = pd.Series(df["low"].values.astype(float)).rolling(L).min().shift(1).values
c = df["close"].values.astype(float)
pos = np.zeros(len(c)); cur = 0
for i in range(len(c)):
if not np.isnan(h[i]) and c[i] > h[i]:
cur = 1
elif not np.isnan(l[i]) and c[i] < l[i]:
cur = -1 if mode == "ls" else 0
pos[i] = cur
return pos
def vol_target(df, tf, target=0.6, L=72):
"""Overlay SEMPRE-LONG con esposizione scalata dalla vol realizzata (target vol annua)."""
c = pd.Series(df["close"].values.astype(float))
rv_ann = c.pct_change().rolling(L).std().values * np.sqrt(BARS_PER_YEAR[tf])
pos = np.clip(np.nan_to_num(target / np.where(rv_ann > 0, rv_ann, np.nan), nan=0.0), 0, 1)
return pos
def rolling_hurst(c, W=120, step=6, lags=(2, 4, 8, 16, 32)):
logc = np.log(c); n = len(c); H = np.full(n, np.nan)
lg = np.log(lags)
for i in range(W, n, step):
seg = logc[i - W:i]
tau = [np.std(seg[lag:] - seg[:-lag]) for lag in lags]
if min(tau) > 0:
H[i] = np.polyfit(lg, np.log(tau), 1)[0]
return pd.Series(H).ffill().fillna(0.5).values
def hurst_mom(df, L=48, W=120, mode="ls"):
H = rolling_hurst(df["close"].values.astype(float), W)
return np.where(H > 0.5, tsmom(df, L, mode), 0.0)
def leadlag_df(target_df, other_df, L):
"""Costruisce un df col close del TARGET e la posizione = segno del rendimento a L barre
dell'ALTRO asset (allineato per timestamp). Ritorna (df_merged, pos)."""
a = target_df[["timestamp", "open", "high", "low", "close"]]
b = other_df[["timestamp", "close"]].rename(columns={"close": "other"})
m = a.merge(b, on="timestamp", how="inner").reset_index(drop=True)
o = pd.Series(m["other"].values.astype(float))
pos = np.sign(np.nan_to_num((o / o.shift(L) - 1).values))
return m, pos
# --------------------------------- reporting ---------------------------------
ROWS = []
def summarize(family, asset, tf, df, pos, mc_n=300):
full = backtest(df, pos, tf)
oos = backtest(df, pos, tf, lo=VAL_START, hi=HOLDOUT_START)
bh = buy_hold(df, tf)
gross = backtest(df, pos, tf, fee_rt=0.0).sharpe
_, p, _, _ = mc_pvalue(df, pos, tf, n=mc_n)
beats_bh = full.sharpe > bh.sharpe and oos.sharpe > 0
real = (full.sharpe > 0 and oos.sharpe > 0 and not np.isnan(p) and p < 0.05)
verdict = "★EDGE?" if (real and beats_bh) else ("real?" if real else "rumore")
ROWS.append(dict(fam=family, asset=asset, tf=tf, full=full.sharpe, oos=oos.sharpe,
gross=gross, bh=bh.sharpe, p=p, trd=full.ntrades, verdict=verdict))
print(f" {family:<16s} {asset} {tf:<3s} | FULL {full.sharpe:>5.2f} OOS {oos.sharpe:>5.2f} "
f"gross {gross:>5.2f} | B&H {bh.sharpe:>4.2f} | p {p:>.3f} | trd/y {full.ntrades:>6.0f} | {verdict}")
def scan_best(family, asset, tf, df, fn, grid, label_fn):
"""Scansiona la griglia (FULL Sharpe), stampa la riga compatta, ritorna la pos migliore."""
best = None
line = []
for params in grid:
pos = fn(df, *params)
s = backtest(df, pos, tf).sharpe
line.append(f"{label_fn(params)}={s:>4.1f}")
if best is None or s > best[0]:
best = (s, params, pos)
print(f" {asset} {tf} grid: " + " ".join(line))
return best[2], best[1]
def main():
print("=" * 100)
print(" FASE 2 — esplorazione famiglie BTC/ETH | netto fee 0.10% RT | barra = buy&hold | hold-out bloccato")
print("=" * 100)
D1 = {a: load_data(a, "1h") for a in ("BTC", "ETH")}
D15 = {a: load_data(a, "15m") for a in ("BTC", "ETH")}
def block(title):
print("\n" + "#" * 100 + f"\n {title}\n" + "#" * 100)
# ---- TSMOM (momentum) 1h + 15m, L/S e long-only ----
block("TSMOM (momentum)")
Ls = [(12,), (24,), (48,), (96,), (192,)]
for a in ("BTC", "ETH"):
pos, p = scan_best("TSMOM-LS", a, "1h", D1[a], lambda d, L: tsmom(d, L, "ls"), Ls, lambda x: f"L{x[0]}")
summarize("TSMOM-LS", a, "1h", D1[a], pos)
pos, p = scan_best("TSMOM-LO", a, "1h", D1[a], lambda d, L: tsmom(d, L, "lo"), Ls, lambda x: f"L{x[0]}")
summarize("TSMOM-LO", a, "1h", D1[a], pos)
pos, p = scan_best("TSMOM-LS", a, "15m", D15[a], lambda d, L: tsmom(d, L, "ls"), [(48,),(96,),(192,),(384,)], lambda x: f"L{x[0]}")
summarize("TSMOM-LS", a, "15m", D15[a], pos)
# ---- REVERSAL 1h + 15m ----
block("REVERSAL (mean-reversion breve)")
Lr = [(1,), (3,), (6,), (12,), (24,)]
for a in ("BTC", "ETH"):
pos, p = scan_best("REV-LS", a, "1h", D1[a], lambda d, L: reversal(d, L, "ls"), Lr, lambda x: f"L{x[0]}")
summarize("REV-LS", a, "1h", D1[a], pos)
pos, p = scan_best("REV-LS", a, "15m", D15[a], lambda d, L: reversal(d, L, "ls"), Lr, lambda x: f"L{x[0]}")
summarize("REV-LS", a, "15m", D15[a], pos)
# ---- MA cross ----
block("MA-CROSS (trend)")
g = [(12, 48), (24, 96), (48, 192), (24, 200)]
for a in ("BTC", "ETH"):
pos, p = scan_best("MAX-LS", a, "1h", D1[a], lambda d, f, s: ma_cross(d, f, s, "ls"), g, lambda x: f"{x[0]}/{x[1]}")
summarize("MAX-LS", a, "1h", D1[a], pos)
pos, p = scan_best("MAX-LO", a, "1h", D1[a], lambda d, f, s: ma_cross(d, f, s, "lo"), g, lambda x: f"{x[0]}/{x[1]}")
summarize("MAX-LO", a, "1h", D1[a], pos)
# ---- Donchian breakout ----
block("DONCHIAN breakout")
Ld = [(24,), (48,), (96,), (192,)]
for a in ("BTC", "ETH"):
pos, p = scan_best("DONCH-LS", a, "1h", D1[a], lambda d, L: donchian(d, L, "ls"), Ld, lambda x: f"L{x[0]}")
summarize("DONCH-LS", a, "1h", D1[a], pos)
pos, p = scan_best("DONCH-LO", a, "1h", D1[a], lambda d, L: donchian(d, L, "lo"), Ld, lambda x: f"L{x[0]}")
summarize("DONCH-LO", a, "1h", D1[a], pos)
# ---- Vol-target overlay (vs buy&hold) ----
block("VOL-TARGET overlay (sempre-long scalato) — riduce la vol/DD del buy&hold?")
for a in ("BTC", "ETH"):
pos, p = scan_best("VOLTGT", a, "1h", D1[a], lambda d, t: vol_target(d, "1h", t, 72),
[(0.4,), (0.6,), (0.8,), (1.0,)], lambda x: f"t{x[0]}")
summarize("VOLTGT", a, "1h", D1[a], pos)
# ---- Hurst-gated momentum ----
block("HURST-gated momentum (momentum solo in regime trending H>0.5)")
for a in ("BTC", "ETH"):
pos, p = scan_best("HURST-MOM", a, "1h", D1[a], lambda d, L: hurst_mom(d, L, 120, "ls"),
[(24,), (48,), (96,)], lambda x: f"L{x[0]}")
summarize("HURST-MOM", a, "1h", D1[a], pos)
# ---- Lead-lag BTC<->ETH ----
block("LEAD-LAG BTC<->ETH (posiziona un asset col rendimento passato dell'altro)")
for tgt, oth in (("ETH", "BTC"), ("BTC", "ETH")):
Ll = [1, 3, 6, 12, 24]
best = None; line = []
for L in Ll:
m, pos = leadlag_df(D1[tgt], D1[oth], L)
s = backtest(m, pos, "1h").sharpe
line.append(f"L{L}={s:>4.1f}")
if best is None or s > best[0]:
best = (s, L, m, pos)
print(f" {oth}->{tgt} 1h grid: " + " ".join(line))
_, L, m, pos = best
summarize(f"LL {oth}>{tgt}", tgt, "1h", m, pos)
# ---- classifica finale ----
print("\n" + "=" * 100)
print(" CLASSIFICA — net-fee FULL Sharpe (★EDGE? = batte B&H, OOS>0 e null p<0.05)")
print("=" * 100)
for r in sorted(ROWS, key=lambda r: -r["full"]):
print(f" {r['fam']:<16s} {r['asset']} {r['tf']:<3s} | FULL {r['full']:>5.2f} | OOS {r['oos']:>5.2f} | "
f"B&H {r['bh']:>4.2f} | p {r['p']:>.3f} | {r['verdict']}")
edges = [r for r in ROWS if r["verdict"] == "★EDGE?"]
print(f"\n Candidati che battono il buy&hold net-fee + OOS>0 + null p<0.05: {len(edges)}")
for r in edges:
print(f" -> {r['fam']} {r['asset']} {r['tf']}: FULL {r['full']:.2f} OOS {r['oos']:.2f} p {r['p']:.3f}")
if __name__ == "__main__":
main()
+152
View File
@@ -0,0 +1,152 @@
"""FASE 3 — conferma avversariale del SOLO candidato reale: trend-following long-only (MA-cross).
Protocollo onesto:
1. SELEZIONE config SOLO sul pre-hold-out (< 2025-01-01). Niente sbirciate al hold-out.
2. HOLD-OUT 2025-26 sbloccato UNA volta (la prova del nove, mai usato in ricerca).
3. Breakdown PER ANNO vs buy&hold: il trend-LO deve "schivare" i bear (2018/2022).
4. STRESS: fee 2x, lag di esecuzione (1 barra), slippage.
5. DEFLATED SHARPE (Bailey & López de Prado): lo Sharpe regge alla correzione per multiple-testing?
uv run python scripts/analysis/phase3_confirm.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 scipy.stats import norm, skew, kurtosis
from src.data.downloader import load_data
from scripts.analysis.research_lab import (
backtest, buy_hold, window_mask, ts, _net_series, HOLDOUT_START, BARS_PER_YEAR,
)
from scripts.analysis.phase2_families import ma_cross
GRID = [(12, 48), (24, 96), (48, 192), (24, 200), (96, 288)] # MA-cross griglia (fast/slow)
REPR = (24, 96) # config rappresentativa PRE-COMMITTATA
TF = "1h"
def lag(pos, k=1):
"""Esecuzione in ritardo di k barre (agisci k barre dopo la decisione)."""
return np.concatenate([np.zeros(k), pos[:-k]])
def per_year(df, pos, tf):
c = df["close"].values.astype(float)
net, _, fwd, _ = _net_series(df, pos)
yrs = ts(df).dt.year.values
out = {}
for y in sorted(set(yrs)):
m = yrs == y
if m.sum() < 2:
continue
strat = float(np.prod(1 + net[m]) - 1) * 100
bh = float(np.prod(1 + fwd[m]) - 1) * 100
expo = float(np.mean(np.abs(pos[m])))
out[y] = (strat, bh, expo)
return out
def deflated_sharpe(net, sr_trials_perbar, N):
"""DSR: prob. che il vero Sharpe > la soglia attesa-massima sotto N trial (multiple testing).
Tutto in Sharpe PER BARRA. >0.95 = significativo dopo correzione."""
sr = net.mean() / net.std()
T = len(net)
g3 = float(skew(net)); g4 = float(kurtosis(net, fisher=False))
var_sr = float(np.var(sr_trials_perbar, ddof=1)) if len(sr_trials_perbar) > 1 else 0.0
ge = 0.5772156649
z1 = norm.ppf(1 - 1.0 / N); z2 = norm.ppf(1 - 1.0 / (N * np.e))
sr0 = np.sqrt(var_sr) * ((1 - ge) * z1 + ge * z2) # Sharpe atteso-massimo sotto null, N trial
den = np.sqrt(max(1 - g3 * sr + (g4 - 1) / 4.0 * sr ** 2, 1e-9))
dsr = float(norm.cdf((sr - sr0) * np.sqrt(T - 1) / den))
bpy = BARS_PER_YEAR[TF]
return dsr, sr * np.sqrt(bpy), sr0 * np.sqrt(bpy)
def main():
print("=" * 96)
print(" FASE 3 — conferma avversariale: TREND-following long-only (MA-cross) BTC/ETH")
print("=" * 96)
data = {a: load_data(a, TF) for a in ("BTC", "ETH")}
# ---------- 1) selezione SOLO pre-hold-out ----------
print(f"\n (1) SELEZIONE su pre-hold-out (< {HOLDOUT_START}) — Sharpe per config (plateau = robusto)")
for a in ("BTC", "ETH"):
line = []
for f, s in GRID:
pos = ma_cross(data[a], f, s, "lo")
sh = backtest(data[a], pos, TF, hi=HOLDOUT_START).sharpe
line.append(f"{f}/{s}={sh:>4.2f}")
print(f" {a}: " + " ".join(line))
print(f" -> config rappresentativa PRE-COMMITTATA per i test seguenti: {REPR[0]}/{REPR[1]}")
# ---------- 2) HOLD-OUT 2025-26 (sbloccato una volta) ----------
print(f"\n (2) HOLD-OUT {HOLDOUT_START}+ — LA PROVA DEL NOVE (mai usato in ricerca)")
for a in ("BTC", "ETH"):
bh = buy_hold(data[a], TF, lo=HOLDOUT_START)
print(f" {a}: buy&hold hold-out Sh {bh.sharpe:>5.2f} ret {bh.ret*100:>+7.1f}% DD {bh.maxdd*100:>4.1f}%")
for f, s in GRID:
pos = ma_cross(data[a], f, s, "lo")
r = backtest(data[a], pos, TF, lo=HOLDOUT_START)
star = " <-REPR" if (f, s) == REPR else ""
print(f" {f}/{s:<3d} Sh {r.sharpe:>5.2f} ret {r.ret*100:>+7.1f}% DD {r.maxdd*100:>4.1f}% expo {r.exposure:.2f}{star}")
# ---------- 3) per anno vs buy&hold (schiva i bear?) ----------
print(f"\n (3) PER ANNO — strat {REPR[0]}/{REPR[1]} vs buy&hold (expo = quanto è long; bear test 2018/2022)")
for a in ("BTC", "ETH"):
pos = ma_cross(data[a], *REPR, "lo")
py = per_year(data[a], pos, TF)
print(f" {a}:")
for y, (st, bh, ex) in py.items():
flag = " <- BEAR" if bh < -20 else ""
print(f" {y}: strat {st:>+7.0f}% | buy&hold {bh:>+7.0f}% | expo {ex:.2f}{flag}")
# ---------- 4) stress ----------
print(f"\n (4) STRESS — strat {REPR[0]}/{REPR[1]} | FULL e HOLD-OUT Sharpe")
print(f" {'scenario':<24s}{'BTC FULL':>10s}{'BTC HO':>9s}{'ETH FULL':>10s}{'ETH HO':>9s}")
scen = [
("base fee0.10%", dict(fee_rt=0.001), False),
("fee 0.20% (2x)", dict(fee_rt=0.002), False),
("lag 1 barra", dict(fee_rt=0.001), True),
("fee2x + lag", dict(fee_rt=0.002), True),
]
for name, kw, do_lag in scen:
row = [name]
for a in ("BTC", "ETH"):
pos = ma_cross(data[a], *REPR, "lo")
if do_lag:
pos = lag(pos, 1)
full = backtest(data[a], pos, TF, **kw).sharpe
ho = backtest(data[a], pos, TF, lo=HOLDOUT_START, **kw).sharpe
row += [f"{full:>9.2f}", f"{ho:>8.2f}"]
print(f" {row[0]:<24s}{row[1]:>10s}{row[2]:>9s}{row[3]:>10s}{row[4]:>9s}")
# ---------- 5) deflated Sharpe ----------
print(f"\n (5) DEFLATED SHARPE — corregge il multiple-testing (DSR>0.95 = regge)")
# trial set = TUTTE le config trend long-only provate (proxy del numero di tentativi)
N_TRIALS = 60 # stima conservativa dei backtest provati in Fase 2 (tutte le famiglie/asset/TF)
for a in ("BTC", "ETH"):
trials = [backtest(data[a], ma_cross(data[a], f, s, "lo"), TF, hi=HOLDOUT_START) for f, s in GRID]
sr_trials = []
for f, s in GRID:
net, _, _, _ = _net_series(data[a], ma_cross(data[a], f, s, "lo"))
m = window_mask(data[a], hi=HOLDOUT_START)
sr_trials.append(net[m].mean() / net[m].std())
net, _, _, _ = _net_series(data[a], ma_cross(data[a], *REPR, "lo"))
m = window_mask(data[a], hi=HOLDOUT_START)
dsr, sr_ann, sr0_ann = deflated_sharpe(net[m], sr_trials, N_TRIALS)
verdict = "REGGE" if dsr > 0.95 else "NON regge"
print(f" {a} (pre-hold-out): Sharpe {sr_ann:.2f} vs soglia-max-attesa(N={N_TRIALS}) {sr0_ann:.2f} "
f"-> DSR {dsr:.3f} [{verdict}]")
print("\n" + "=" * 96)
print(" VERDETTO: edge ONESTO solo se (2) hold-out positivo, (3) schiva i bear, (4) regge lo")
print(" stress, (5) DSR>0.95. Altrimenti: anche il trend era sample-luck del mercato toro.")
print("=" * 96)
if __name__ == "__main__":
main()
+208
View File
@@ -0,0 +1,208 @@
"""HARNESS DI RICERCA ONESTO — BTC/ETH, v2.0.0 (Fase 0).
Dopo che l'intera libreria precedente si è rivelata artefatto di feed/harness disonesti,
la prima cosa di cui fidarsi NON è una strategia ma il banco di prova. Questo modulo è
quel banco: causale per costruzione, netto fee, con baseline e null model.
MODELLO CANONICO = SERIE DI POSIZIONE.
Una strategia è una funzione signal(df, **params) -> pd.Series/np.array che la
posizione target per barra in [-1, +1]. REGOLA: position[i] è decisa con dati FINO a
close[i] (mai oltre) e GUADAGNA il rendimento close[i] -> close[i+1]. L'engine moltiplica
position[i] * fwd[i] (fwd strettamente futuro rispetto alla decisione) -> niente look-ahead
per costruzione, e niente fill sull'estremo di candela (si entra al close). La fee è
addebitata sul TURNOVER |Δposition| (un round-trip 0->1->0 = 2 unità = fee_rt intera).
GATE (vedi CLAUDE.md): ingresso eseguibile (qui per costruzione), netto fee 0.10% RT,
OOS held-out, robustezza su griglia, onestà statistica (null model + buy&hold), walk-forward
per i modelli fittati, liquidità (BTC/ETH ok).
uv run python scripts/analysis/research_lab.py # self-test del banco
"""
from __future__ import annotations
import sys
from dataclasses import dataclass
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.data.downloader import load_data
FEE_RT = 0.001 # 0.10% round-trip taker Deribit (0.05%/lato)
BARS_PER_YEAR = {"5m": 105192.0, "15m": 35064.0, "1h": 8766.0,
"4h": 2191.5, "12h": 730.5, "1d": 365.25}
def load_tf(asset: str, tf: str):
"""Carica un TF certificato. 5m/15m/1h diretti; 4h/12h/1d DERIVATI per resample dal 1h
(confini 00:00 UTC). >=12h e' il regime raccomandato (sotto, costi+overfit dominano)."""
if tf in ("5m", "15m", "1h"):
return load_data(asset, tf)
rule = {"4h": "4h", "12h": "12h", "1d": "1D"}[tf]
df = load_data(asset, "1h").copy()
df.index = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
out = df.resample(rule, label="left", closed="left").agg(
{"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"}).dropna(subset=["open"])
epoch = pd.Timestamp("1970-01-01", tz="UTC")
out["timestamp"] = ((out.index - epoch) // pd.Timedelta(milliseconds=1)).astype("int64")
return out.reset_index(drop=True)[["timestamp", "open", "high", "low", "close", "volume"]]
# Hold-out FINALE bloccato: NIENTE ricerca/tuning lo tocca finché non è il verdetto (Fase 3).
HOLDOUT_START = "2025-01-01"
# Finestra di validazione OOS usata in ricerca (out-of-sample ma PRE hold-out).
VAL_START = "2023-01-01"
def ts(df) -> pd.Series:
return pd.to_datetime(df["timestamp"], unit="ms", utc=True)
def window_mask(df, lo: str | None = None, hi: str | None = None) -> np.ndarray:
t = ts(df)
m = np.ones(len(df), bool)
if lo is not None:
m &= (t >= pd.Timestamp(lo, tz="UTC")).values
if hi is not None:
m &= (t < pd.Timestamp(hi, tz="UTC")).values
return m
@dataclass
class BT:
n: int
ret: float # rendimento composto sulla finestra (pos 1x, leva 1x)
cagr: float
sharpe: float # annualizzato
maxdd: float # % (positivo)
exposure: float # |pos| medio
turnover: float # Σ|Δpos| / anno
ntrades: float # round-trip equivalenti / anno
def line(self, label="") -> str:
return (f" {label:<22s} Sh {self.sharpe:>6.2f} | ret {self.ret*100:>+8.1f}% "
f"CAGR {self.cagr*100:>+6.1f}% | DD {self.maxdd*100:>5.1f}% | "
f"expo {self.exposure:>4.2f} trd/y {self.ntrades:>6.1f} | n {self.n}")
def _net_series(df, position, fee_rt=FEE_RT):
"""Ritorna (net, gross, fwd, pos) per barra. net[i] = pos[i]*fwd[i] - fee sul cambio a i."""
c = df["close"].values.astype(float)
pos = np.nan_to_num(np.asarray(position, float), nan=0.0)
pos = np.clip(pos, -1.0, 1.0)
n = len(c)
fwd = np.zeros(n)
fwd[:-1] = c[1:] / c[:-1] - 1.0 # rendimento close[i]->close[i+1] (futuro vs decisione a i)
gross = pos * fwd
dpos = np.abs(np.diff(np.concatenate([[0.0], pos]))) # cambio di posizione a i (si tradea al close[i])
fee = dpos * (fee_rt / 2.0) # fee_rt = round-trip (2 unità di turnover); /2 per unità
net = gross - fee
return net, gross, fwd, pos
def backtest(df, position, tf="1h", fee_rt=FEE_RT, lo=None, hi=None) -> BT:
net, gross, fwd, pos = _net_series(df, position, fee_rt)
m = window_mask(df, lo, hi)
net_w, pos_w = net[m], pos[m]
dpos_w = np.abs(np.diff(np.concatenate([[0.0], pos_w])))
bpy = BARS_PER_YEAR[tf]
n = int(m.sum())
if n < 2:
return BT(n, 0, float("nan"), 0, 0, 0, 0, 0)
eq = np.cumprod(1.0 + net_w)
total = float(eq[-1] - 1.0)
years = n / bpy
cagr = float((1 + total) ** (1 / years) - 1) if years > 0 and total > -1 else float("nan")
mu, sd = float(net_w.mean()), float(net_w.std())
sharpe = mu / sd * np.sqrt(bpy) if sd > 0 else 0.0
peak = np.maximum.accumulate(eq)
maxdd = float(np.max((peak - eq) / peak)) if n else 0.0
expo = float(np.mean(np.abs(pos_w)))
turn_y = float(dpos_w.sum() / years) if years > 0 else 0.0
return BT(n, total, cagr, sharpe, maxdd, expo, turn_y, turn_y / 2.0)
def buy_hold(df, tf="1h", fee_rt=FEE_RT, lo=None, hi=None) -> BT:
return backtest(df, np.ones(len(df)), tf, fee_rt, lo, hi)
def mc_pvalue(df, position, tf="1h", fee_rt=FEE_RT, n=500, lo=None, hi=None, seed=0):
"""Null model a ROTAZIONE CIRCOLARE: ruota la serie di posizione di un offset casuale.
Preserva ESATTAMENTE exposure, turnover e distribuzione degli holding; distrugge solo
l'allineamento col mercato. p = P(Sharpe_ruotato >= Sharpe_reale). p alto = il timing
non batte il caso (nessuna skill)."""
pos = np.nan_to_num(np.asarray(position, float))
base = backtest(df, pos, tf, fee_rt, lo, hi).sharpe
N = len(pos)
if np.abs(np.diff(pos)).sum() == 0: # posizione costante -> rotazione degenere
return base, float("nan"), float("nan"), float("nan")
rng = np.random.default_rng(seed)
sims = np.empty(n)
for k in range(n):
off = int(rng.integers(1, N))
sims[k] = backtest(df, np.roll(pos, off), tf, fee_rt, lo, hi).sharpe
p = float((np.sum(sims >= base) + 1) / (n + 1))
return base, p, float(sims.mean()), float(sims.std())
def report(name, df, position, tf="1h", fee_rt=FEE_RT, mc_n=400):
"""Stampa il verdetto onesto: FULL / OOS-VAL / vs buy&hold / null p-value / sweep fee."""
print(f"\n === {name} ({tf}) ===")
print(backtest(df, position, tf, fee_rt).line("FULL"))
print(backtest(df, position, tf, fee_rt, lo=VAL_START, hi=HOLDOUT_START).line(f"OOS-VAL {VAL_START[:4]}-24"))
print(buy_hold(df, tf, fee_rt).line("buy&hold FULL"))
base, p, msh, ssd = mc_pvalue(df, position, tf, fee_rt, n=mc_n)
verdict = "RUMORE" if (np.isnan(p) or p > 0.05) else "batte il null"
print(f" null (rotazione, n={mc_n}): Sharpe reale {base:.2f} vs random {msh:.2f}±{ssd:.2f} "
f"-> p={p if not np.isnan(p) else float('nan'):.3f} [{verdict}]")
print(" sweep fee RT:", " ".join(
f"{f*100:.2f}%→Sh{backtest(df, position, tf, f).sharpe:.2f}" for f in (0.0, 0.0005, 0.001, 0.002)))
# ============================ SELF-TEST DEL BANCO ============================
def self_test():
"""Valida l'HARNESS, non una strategia. Tre prove:
(1) buy&hold: Sharpe positivo, DD grande (sanity dei numeri).
(2) CHEAT look-ahead (pos = segno del rendimento FUTURO): Sharpe enorme, p0
-> l'engine SA vedere un edge quando esiste davvero.
(3) NOISE causale (pos da rumore del passato): Sharpe0, p0.5
-> l'engine NON inventa edge dal nulla (niente leak)."""
print("=" * 78)
print(" SELF-TEST HARNESS — deve: vedere il cheat, NON vedere il rumore")
print("=" * 78)
df = load_data("BTC", "1h")
t = ts(df)
c = df["close"].values.astype(float)
bh = buy_hold(df, "1h")
print(bh.line("(1) buy&hold BTC"))
assert bh.sharpe > 0, "buy&hold dovrebbe avere Sharpe>0 sullo storico BTC"
# (2) CHEAT: posizione = segno del rendimento del prossimo bar (USA IL FUTURO)
fwd = np.zeros(len(c)); fwd[:-1] = c[1:] / c[:-1] - 1.0
cheat = np.sign(fwd)
bt_cheat = backtest(df, cheat, "1h")
_, p_cheat, _, _ = mc_pvalue(df, cheat, "1h", n=200, seed=1)
print(bt_cheat.line("(2) CHEAT look-ahead"))
print(f" -> null p={p_cheat:.4f} (atteso ≈0: l'edge finto È enorme e battibile dal caso ~mai)")
assert bt_cheat.sharpe > 20, "il cheat dovrebbe dare Sharpe enorme se l'engine è corretto"
assert p_cheat < 0.02, "il cheat dovrebbe battere il null in modo schiacciante"
# (3) NOISE causale a BASSO turnover (blocchi ~50 barre): isola la SKILL dalla fee-death.
# Posizione casuale (non usa il futuro) tenuta a blocchi -> turnover basso -> se l'engine non
# inventa edge dal nulla, Sharpe≈0 e il null p≈0.5 (random rotazioni indistinguibili).
rng = np.random.default_rng(42)
blk = 50
raw = np.sign(rng.standard_normal(len(c) // blk + 1))
noise_pos = np.repeat(raw, blk)[:len(c)]
noise_pos = pd.Series(noise_pos).shift(1).fillna(0).values # solo passato
bt_noise = backtest(df, noise_pos, "1h")
base_n, p_noise, msh, ssd = mc_pvalue(df, noise_pos, "1h", n=400, seed=2)
print(bt_noise.line("(3) NOISE causale"))
print(f" -> null p={p_noise:.3f} (atteso alto/≈0.5: nessuna skill, indistinguibile dal caso)")
assert bt_noise.sharpe < 2.0, "il rumore causale non deve sembrare SKILLATO (Sharpe positivo grande = leak)"
assert p_noise > 0.10, "il rumore causale non deve battere il null (p basso = edge spurio/leak)"
print("\n ✓ HARNESS VALIDATO: vede il cheat (Sharpe enorme, p≈0), non inventa edge dal rumore (p alto).")
print(f" Hold-out finale BLOCCATO da {HOLDOUT_START} (non usato in ricerca). OOS-VAL: {VAL_START}→hold-out.")
if __name__ == "__main__":
self_test()
+121
View File
@@ -0,0 +1,121 @@
"""STRESS-TEST di TP01 (integrato da strategy-research-2026-06) — robustezza avversariale.
Usa il modulo VERO integrato (src/strategies/trend_portfolio). Oltre a hold-out/cross-asset/multi-TF
(gia' in verify_tp01.py), qui: sweep FEE (fino 0.40% RT), LAG di esecuzione + slippage, PLATEAU dei
parametri (config cherry-picked?), DEFLATED-SHARPE (multiple-testing track A-E).
uv run python scripts/analysis/stress_tp01.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 scipy.stats import norm, skew, kurtosis
from src.data.downloader import load_data
from src.strategies.trend_portfolio import TrendPortfolio, resample_4h, simple_returns, CANONICAL
HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC")
DF4H = {a: resample_4h(load_data(a, "1h")) for a in ("BTC", "ETH")}
def combo(cfg, lag_bars=0, fee_side=0.0005):
"""Rendimenti per-barra del portafoglio 50/50 con config cfg, lag extra e fee dati."""
tp = TrendPortfolio(**{**cfg, "fee_side": fee_side})
series = {}
for a in ("BTC", "ETH"):
df = DF4H[a]
r = simple_returns(df["close"].values.astype(float))
tgt = tp.target_series(df)
held = np.zeros(len(tgt))
s = 1 + lag_bars
held[s:] = tgt[:-s] # tenuta = decisa s barre prima (causale + lag)
net = held * r - fee_side * np.abs(np.diff(held, prepend=0.0))
net[0] = 0.0
series[a] = pd.Series(np.clip(net, -0.99, None), index=pd.to_datetime(df["datetime"]))
J = pd.concat(series, axis=1, join="inner").fillna(0.0)
return 0.5 * J["BTC"].values + 0.5 * J["ETH"].values, J.index
def met(combo_r, idx):
rr = combo_r[np.isfinite(combo_r)]
if len(rr) < 2 or np.std(rr) == 0:
return dict(sh=0, ret=0, dd=0)
bpy = 86400 * 365.25 / pd.Series(idx).diff().dt.total_seconds().median()
eq = np.cumprod(1 + rr); pk = np.maximum.accumulate(eq)
return dict(sh=float(np.mean(rr) / np.std(rr) * np.sqrt(bpy)),
ret=float(eq[-1] - 1), dd=float(np.max((pk - eq) / pk)))
def full_ho(cfg, lag_bars=0, fee_side=0.0005):
cr, idx = combo(cfg, lag_bars, fee_side)
ho = idx >= HOLDOUT
return met(cr, idx), met(cr[ho], idx[ho])
def main():
print("=" * 88)
print(" STRESS-TEST TP01 (PORT LF4h canonica) — robustezza avversariale")
print("=" * 88)
base_f, base_h = full_ho(CANONICAL)
print(f"\n BASELINE (4h, fee 0.10% RT): FULL Sh {base_f['sh']:.2f} ret {base_f['ret']*100:+.0f}% DD {base_f['dd']*100:.1f}%"
f" | HOLD-OUT Sh {base_h['sh']:.2f} ret {base_h['ret']*100:+.1f}% DD {base_h['dd']*100:.1f}%")
print("\n (1) SWEEP FEE (RT) — regge fino a 0.40%?")
print(f" {'fee RT':<10s}{'FULL Sh':>9s}{'FULL ret':>10s}{'HOLD Sh':>9s}{'HOLD ret':>10s}")
for frt in (0.0, 0.001, 0.002, 0.004):
f, h = full_ho(CANONICAL, fee_side=frt / 2)
print(f" {frt*100:>5.2f}% {f['sh']:>8.2f}{f['ret']*100:>+9.0f}%{h['sh']:>9.2f}{h['ret']*100:>+9.1f}%")
print("\n (2) LAG di esecuzione + slippage (fee 0.20% per simulare slippage)")
print(f" {'scenario':<22s}{'FULL Sh':>9s}{'HOLD Sh':>9s}{'HOLD ret':>10s}")
for name, lag, frt in [("base", 0, 0.001), ("lag 1 barra (4h)", 1, 0.001),
("lag 2 barre", 2, 0.001), ("lag1 + fee0.20% slip", 1, 0.002)]:
f, h = full_ho(CANONICAL, lag_bars=lag, fee_side=frt / 2)
print(f" {name:<22s}{f['sh']:>8.2f}{h['sh']:>9.2f}{h['ret']*100:>+9.1f}%")
print("\n (3) PLATEAU PARAMETRI — la config canonica e' un picco o un altopiano?")
print(f" {'variazione':<26s}{'FULL Sh':>9s}{'HOLD Sh':>9s}")
grid = [
("canonica (vt.20 lev2 30/90/180 vw30)", CANONICAL),
("target_vol 0.15", {**CANONICAL, "target_vol": 0.15}),
("target_vol 0.25", {**CANONICAL, "target_vol": 0.25}),
("leverage 1.5", {**CANONICAL, "leverage": 1.5}),
("leverage 3.0", {**CANONICAL, "leverage": 3.0}),
("horizons 20/60/120", {**CANONICAL, "horizons_days": (20, 60, 120)}),
("horizons 60/120/240", {**CANONICAL, "horizons_days": (60, 120, 240)}),
("vol_win 20", {**CANONICAL, "vol_win_days": 20}),
("vol_win 45", {**CANONICAL, "vol_win_days": 45}),
]
sr_trials = []
for name, cfg in grid:
f, h = full_ho(cfg)
cr, idx = combo(cfg)
sr_trials.append(cr[np.isfinite(cr)].mean() / cr[np.isfinite(cr)].std()) # Sharpe per-barra
print(f" {name:<26s}{f['sh']:>8.2f}{h['sh']:>9.2f}")
print("\n (4) DEFLATED SHARPE — corregge il multiple-testing (track A-E + sweep). DSR>0.95 = regge")
cr, idx = combo(CANONICAL)
rr = cr[np.isfinite(cr)]
sr = rr.mean() / rr.std(); T = len(rr)
g3 = float(skew(rr)); g4 = float(kurtosis(rr, fisher=False))
var_sr = float(np.var(sr_trials, ddof=1))
ge = 0.5772156649
for N in (10, 40, 100): # N = numero di trial/config provati (conservativo)
z1 = norm.ppf(1 - 1.0 / N); z2 = norm.ppf(1 - 1.0 / (N * np.e))
sr0 = np.sqrt(var_sr) * ((1 - ge) * z1 + ge * z2)
den = np.sqrt(max(1 - g3 * sr + (g4 - 1) / 4.0 * sr ** 2, 1e-9))
dsr = float(norm.cdf((sr - sr0) * np.sqrt(T - 1) / den))
bpy = 86400 * 365.25 / pd.Series(idx).diff().dt.total_seconds().median()
print(f" N={N:>3d} trial -> soglia-max-attesa Sh {sr0*np.sqrt(bpy):.2f} | DSR {dsr:.3f} [{'REGGE' if dsr>0.95 else 'NON regge'}]")
print("\n" + "=" * 88)
print(" Verdetto: TP01 robusto se regge fee 0.40%+lag (HOLD positivo), plateau (no picco), DSR>0.95.")
print("=" * 88)
if __name__ == "__main__":
main()
+96
View File
@@ -0,0 +1,96 @@
"""TP01 a BASSA FREQUENZA (>=12h) — ri-verifica dopo il bug look-ahead ffill-mixed-TF.
L'utente/agente ha trovato un look-ahead (ffill mixed-timeframe su barre open-labeled) che
gonfiava il 4h (~1.60 -> reale ~1.1) e ha concluso: NON scendere sotto le 12h (costi+overfit
dominano). Qui ricalcolo TP01 in modo PULITO per singolo TF (barre discrete, posizione shiftata
+1, NESSUN ffill/combine mixed-TF) su 4h/12h/1d, con un GUARD di causalita' esplicito sulla serie
resamplata (ricalcolo su prefisso). Fee 0.10% RT, hold-out 2025-26 bloccato.
uv run python scripts/analysis/tp01_lowfreq.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.data.downloader import load_data
from src.strategies.trend_portfolio import TrendPortfolio, simple_returns, CANONICAL
HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC")
def resample_tf(df_1h, rule):
g = df_1h.copy()
g.index = pd.to_datetime(g["timestamp"], unit="ms", utc=True)
out = g.resample(rule, label="left", closed="left").agg(
{"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"}).dropna(subset=["open"])
out["datetime"] = out.index
return out.reset_index(drop=True)
def sleeve_net(df, tp):
"""Per-barra netto di uno sleeve: posizione decisa a close[i-1], tenuta in i (causale, no ffill)."""
r = simple_returns(df["close"].values.astype(float))
tgt = tp.target_series(df)
held = np.zeros(len(tgt)); held[1:] = tgt[:-1]
net = held * r - tp.fee_side * np.abs(np.diff(held, prepend=0.0)); net[0] = 0.0
return np.clip(net, -0.99, None)
def causality_ok(df, tp, k=10):
"""Ricalcola target_series su prefissi e verifica che tgt[i] non cambi (no look-ahead)."""
full = tp.target_series(df); n = len(df)
rng = np.random.default_rng(0); bad = 0
for i in rng.integers(int(n * 0.6), n - 1, size=k):
p = tp.target_series(df.iloc[:i + 1].copy())
if len(p) != i + 1 or not np.isclose(np.nan_to_num(p[i]), np.nan_to_num(full[i]), atol=1e-9):
bad += 1
return bad
def met(rr, idx):
rr = rr[np.isfinite(rr)]
if len(rr) < 2 or np.std(rr) == 0:
return dict(sh=0, ret=0, dd=0, n=len(rr))
bpy = 86400 * 365.25 / pd.Series(idx).diff().dt.total_seconds().median()
eq = np.cumprod(1 + rr); pk = np.maximum.accumulate(eq)
return dict(sh=float(np.mean(rr) / np.std(rr) * np.sqrt(bpy)), ret=float(eq[-1] - 1),
dd=float(np.max((pk - eq) / pk)), n=len(rr))
def main():
print("=" * 92)
print(" TP01 RI-VERIFICA BASSA FREQUENZA — calcolo pulito per-TF (no ffill mixed-TF) | fee 0.10% RT")
print("=" * 92)
tp = TrendPortfolio(**CANONICAL)
print(f" {'TF':<5s}{'leak':>6s}{'FULL Sh':>9s}{'FULL ret':>10s}{'FULL DD':>9s}{'HOLD Sh':>9s}{'HOLD ret':>10s}{'HOLD DD':>9s}")
for tf, rule in [("4h", "4h"), ("6h", "6h"), ("12h", "12h"), ("1d", "1D")]:
series = {}; leak = 0
for a in ("BTC", "ETH"):
df = resample_tf(load_data(a, "1h"), rule)
leak += causality_ok(df, tp)
series[a] = pd.Series(sleeve_net(df, tp), index=pd.to_datetime(df["datetime"]))
J = pd.concat(series, axis=1, join="inner").fillna(0.0)
combo = 0.5 * J["BTC"].values + 0.5 * J["ETH"].values
idx = J.index; ho = idx >= HOLDOUT
f = met(combo, idx); h = met(combo[ho], idx[ho])
print(f" {tf:<5s}{leak:>6d}{f['sh']:>9.2f}{f['ret']*100:>+9.0f}%{f['dd']*100:>8.1f}%"
f"{h['sh']:>9.2f}{h['ret']*100:>+9.1f}%{h['dd']*100:>8.1f}%")
# buy&hold 50/50 a 1d come riferimento hold-out
bh = {}
for a in ("BTC", "ETH"):
df = resample_tf(load_data(a, "1h"), "1D")
bh[a] = pd.Series(simple_returns(df["close"].values.astype(float)), index=pd.to_datetime(df["datetime"]))
Jb = pd.concat(bh, axis=1, join="inner").fillna(0.0)
cb = 0.5 * Jb["BTC"].values + 0.5 * Jb["ETH"].values; ix = Jb.index; ho = ix >= HOLDOUT
bhf = met(cb, ix); bhh = met(cb[ho], ix[ho])
print(f"\n buy&hold 50/50 (1d): FULL Sh {bhf['sh']:.2f} ret {bhf['ret']*100:+.0f}% DD {bhf['dd']*100:.0f}%"
f" | HOLD-OUT Sh {bhh['sh']:.2f} ret {bhh['ret']*100:+.0f}% DD {bhh['dd']*100:.0f}%")
print("\n (leak=0 = nessun look-ahead nel calcolo per-TF. Confronta con la tesi: >=12h trustworthy.)")
if __name__ == "__main__":
main()
+149
View File
@@ -0,0 +1,149 @@
"""VERIFICA AVVERSARIALE di TP01 (branch strategy-research-2026-06) col MIO gauntlet onesto.
TP01 = TSMOM multi-orizzonte (30/90/180g) long-flat, vol-target 20%, leva cap 2x, portafoglio
50/50 BTC+ETH. Codice riprodotto VERBATIM dal branch (src/strategies/trend_portfolio.py).
La loro tesi: 'positiva ogni anno 2019-2026, Sharpe ~1.32'. Il mio test decisivo: il HOLD-OUT
2025-26 (che ha bocciato il mio trend 1h in Fase 3) + cross-asset + multi-TF (cherry-picking 4h?).
uv run python scripts/analysis/verify_tp01.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.data.downloader import load_data
HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC")
CANONICAL = dict(target_vol=0.20, leverage=2.0, long_only=True,
horizons_days=(30, 90, 180), vol_win_days=30, fee_side=0.0005)
# ---- TP01 riprodotto VERBATIM dal branch ----
def simple_returns(c):
r = np.zeros(len(c)); r[1:] = c[1:] / c[:-1] - 1.0; return r
def realized_vol(r, win, bpy):
return pd.Series(r).rolling(win, min_periods=win // 2).std().values * np.sqrt(bpy)
def tsmom_blend(c, horizons):
n = len(c); acc = np.zeros(n); cnt = np.zeros(n)
for h in horizons:
s = np.full(n, np.nan); s[h:] = np.sign(c[h:] / c[:-h] - 1.0)
v = np.isfinite(s); acc[v] += s[v]; cnt[v] += 1
out = np.zeros(n); nz = cnt > 0; out[nz] = acc[nz] / cnt[nz]; return out
def target_series(df, p, bpd):
c = df["close"].values.astype(float); bpy = bpd * 365.25
r = simple_returns(c)
vol = realized_vol(r, p["vol_win_days"] * bpd, bpy)
direction = tsmom_blend(c, tuple(d * bpd for d in p["horizons_days"]))
if p["long_only"]:
direction = np.clip(direction, 0, None)
scal = np.where((vol > 0) & np.isfinite(vol), p["target_vol"] / vol, 0.0)
tgt = np.clip(direction * scal, -p["leverage"], p["leverage"]); tgt[~np.isfinite(tgt)] = 0.0
return tgt
def net_returns(df, p, bpd):
c = df["close"].values.astype(float); r = simple_returns(c)
tgt = target_series(df, p, bpd)
pos_held = np.zeros(len(tgt)); pos_held[1:] = tgt[:-1] # decisa a close[t-1], tenuta in t -> causale
gross = pos_held * r
turn = np.abs(np.diff(pos_held, prepend=0.0))
net = gross - p["fee_side"] * turn; net[0] = 0.0
return np.clip(net, -0.99, None), pos_held
def resample(df_1h, rule):
g = df_1h.copy(); idx = pd.to_datetime(g["timestamp"], unit="ms", utc=True); g.index = idx
out = g.resample(rule, label="left", closed="left").agg(
{"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"}).dropna(subset=["open"])
out["timestamp"] = out.index
return out.reset_index(drop=True)
def metrics(combo, idx):
rr = combo[np.isfinite(combo)]
if len(rr) < 2 or np.std(rr) == 0:
return dict(sharpe=0, cagr=0, dd=0, ret=0, n=len(rr))
dt = pd.Series(idx).diff().dt.total_seconds().median()
bpy = 86400 * 365.25 / dt
eq = np.cumprod(1 + rr); peak = np.maximum.accumulate(eq)
years = (idx[-1] - idx[0]).total_seconds() / 86400 / 365.25
return dict(sharpe=float(np.mean(rr) / np.std(rr) * np.sqrt(bpy)),
cagr=float(eq[-1] ** (1 / years) - 1) if years > 0 else 0,
dd=float(np.max((peak - eq) / peak)), ret=float(eq[-1] - 1), n=len(rr))
def portfolio_combo(tf_rule, bpd):
series = {}
for a in ("BTC", "ETH"):
df = load_data(a, "1h")
if tf_rule:
df = resample(df, tf_rule)
net, _ = net_returns(df, CANONICAL, bpd)
series[a] = pd.Series(net, index=pd.to_datetime(df["timestamp"], unit="ms", utc=True) if not tf_rule
else pd.DatetimeIndex(df["timestamp"]))
J = pd.concat(series, axis=1, join="inner").fillna(0.0)
combo = 0.5 * J["BTC"].values + 0.5 * J["ETH"].values
return combo, J.index, J
def line(label, combo, idx):
m = metrics(combo, idx)
return f" {label:<22s} Sharpe {m['sharpe']:>5.2f} | ret {m['ret']*100:>+8.1f}% CAGR {m['cagr']*100:>+6.1f}% | DD {m['dd']*100:>5.1f}% | n {m['n']}"
def main():
print("=" * 92)
print(" VERIFICA TP01 (TSMOM 30/90/180 vol-target 20% lev2x long-flat, 50/50 BTC+ETH)")
print(" col gauntlet onesto: FULL vs buy&hold | HOLD-OUT 2025-26 bloccato | per-anno | multi-TF")
print("=" * 92)
TFS = [("15m", "15min", 96), ("1h", None, 24), ("4h", "4h", 6), ("1d", "1D", 1)]
print("\n (A) MULTI-TF: il 4h e' cherry-picked? FULL + HOLD-OUT per ogni timeframe")
for tf, rule, bpd in TFS:
combo, idx, J = portfolio_combo(rule, bpd)
ho = idx >= HOLDOUT
full = metrics(combo, idx)
hold = metrics(combo[ho], idx[ho])
tag = " <- canonica" if tf == "4h" else ""
print(f" {tf:<3s} FULL Sh {full['sharpe']:>5.2f} CAGR {full['cagr']*100:>+6.1f}% DD {full['dd']*100:>4.1f}% "
f"| HOLD-OUT Sh {hold['sharpe']:>5.2f} ret {hold['ret']*100:>+6.1f}% DD {hold['dd']*100:>4.1f}%{tag}")
# focus 4h canonica
combo, idx, J = portfolio_combo("4h", 6)
print("\n (B) 4h CANONICA — per anno (la tesi: positiva OGNI anno 2019-2026)")
s = pd.Series(combo, index=idx)
for y, g in s.groupby(s.index.year):
eq = np.cumprod(1 + g.values); pk = np.maximum.accumulate(eq)
ho_flag = " <- HOLD-OUT (mai usato per scegliere config?)" if y >= 2025 else ""
print(f" {y}: ret {(eq[-1]-1)*100:>+7.1f}% DD {np.max((pk-eq)/pk)*100:>5.1f}%{ho_flag}")
print("\n (C) HOLD-OUT 2025-26 — TP01 vs buy&hold 50/50 (4h)")
ho = idx >= HOLDOUT
print(line("TP01 portfolio HO", combo[ho], idx[ho]))
# buy&hold 50/50 sullo stesso indice/finestra
bh = {}
for a in ("BTC", "ETH"):
df = resample(load_data(a, "1h"), "4h")
r = simple_returns(df["close"].values.astype(float))
bh[a] = pd.Series(r, index=pd.DatetimeIndex(df["timestamp"]))
Jb = pd.concat(bh, axis=1, join="inner").reindex(idx).fillna(0.0)
bh_combo = 0.5 * Jb["BTC"].values + 0.5 * Jb["ETH"].values
print(line("buy&hold 50/50 HO", bh_combo[ho], idx[ho]))
print(line("TP01 portfolio FULL", combo, idx))
print(line("buy&hold 50/50 FULL", bh_combo, idx))
print("\n (D) CROSS-ASSET nel HOLD-OUT (lo stesso edge regge su ENTRAMBI?)")
for a in ("BTC", "ETH"):
df = resample(load_data(a, "1h"), "4h")
net, _ = net_returns(df, CANONICAL, 6)
ix = pd.DatetimeIndex(df["timestamp"]); m = ix >= HOLDOUT
print(line(f"TP01 {a} sleeve HO", net[m], ix[m]))
if __name__ == "__main__":
main()
+20
View File
@@ -0,0 +1,20 @@
#!/bin/bash
# Refresh dati certificati + avanza paper portfolio (per il dashboard). v2.0.0+.
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_daily ====="
uv run python scripts/analysis/rebuild_history.py --asset BTC ETH # BTC/ETH Deribit mainnet
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/live/paper_portfolio.py # avanza paper TP01+XS01
uv run python scripts/live/paper_prevday.py # forward-monitor lead prevday-breakout (PAPER, non deploy)
uv run python scripts/live/live_execute.py --execute # TP01 LIVE su Deribit (gated da config/live.json)
# --- 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') ====="
} >> logs/cron_daily.log 2>&1
View File
+141
View File
@@ -0,0 +1,141 @@
"""TP01 LIVE EXECUTE — loop di esecuzione GATED su Deribit mainnet (USDC linear).
Porta il conto reale al target di TP01 (causale, dati certificati): per ogni asset calcola il notional
bersaglio = min(0.5 * frazione * equity, max_notional), e apre/riduce/chiude per raggiungerlo.
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/executions.jsonl. TP01 oggi e' FLAT -> target 0 -> nessuna azione finche' il segnale non gira.
uv run python scripts/live/live_execute.py # DRY-RUN (piano, nessun ordine)
uv run python scripts/live/live_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.deribit import INSTRUMENT
from src.live.execution import DeribitTrader
from src.live.notifier import notify
from src.live.shadow import ASSETS, WEIGHT, shadow_report
CONFIG = PROJECT_ROOT / "config" / "live.json"
LOG_DIR = PROJECT_ROOT / "data" / "live"
LOG = LOG_DIR / "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
max_notional = float(cfg["max_notional_per_asset_usd"])
min_order = float(cfg["min_order_usd"])
sl_pct = float(cfg["disaster_sl_pct"])
r = shadow_report() # targets causali + conto/posizioni reali (online)
equity = r["equity"]
print("=" * 84)
print(" TP01 LIVE EXECUTE — Deribit mainnet (USDC linear)")
print("=" * 84)
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 ${max_notional:.0f} | min ${min_order:.0f} | disaster-SL -{sl_pct*100:.0f}%")
print(f" ultima barra : {r['last_data']}\n")
if not r["online"]:
print(" conto non leggibile (offline) -> stop, non eseguo a cieco.")
if do_execute:
notify("⚠️ TP01 LIVE — conto offline", {"nota": "salto l'esecuzione, non opero a cieco"})
return
trader = DeribitTrader() if do_execute else None
actions = []
for a in r["assets"]:
asset = a["asset"]; frac = a["target"]; mark = a["mark"]; cur = a["position_usd"]
tgt = min(WEIGHT * frac * equity, max_notional) if frac > 0 else 0.0
delta = tgt - cur
if abs(delta) < min_order:
act = "HOLD (a target)"
elif tgt < 1.0 and cur > 1.0:
act = f"CLOSE ${cur:,.0f}"
elif delta > 0:
act = f"BUY ${delta:,.0f}"
else:
act = f"REDUCE ${-delta:,.0f}"
print(f" {asset:<3} target {frac:+.3f}x -> ${tgt:,.0f} | pos ${cur:,.0f} | delta ${delta:+,.0f} -> {act}")
if do_execute:
if not act.startswith("HOLD"):
fills = trader.rebalance_to(INSTRUMENT[asset], tgt, mark, min_usd=min_order)
newpos = trader.position_usd(INSTRUMENT[asset])
for f in fills:
print(f" -> {f.side.upper()} {f.filled:.4f} @ ${f.price:,.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, pos_after=newpos))
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), pos_after=round(newpos, 0))
if f.verified:
notify(f"✅ TP01 {act}", det)
else:
notify("⚠️ TP01 ORDINE NON VERIFICATO", {**det, "notes": f.notes})
print(f" reconcile: pos ${newpos:,.0f}")
ds = trader.ensure_disaster_sl(INSTRUMENT[asset], sl_pct) # bracket: piazza se long, pulisce se flat
print(f" disaster-SL: {ds.get('state')}" + (f" @ ${ds['stop']:,.1f}" if ds.get("stop") else ""))
if ds.get("state") == "placed":
notify("🛡️ TP01 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("⚠️ TP01 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 di TP01 (oggi flat).")
else:
print(" => Esecuzione completata (vedi data/live/executions.jsonl).")
def main():
try:
_run()
except Exception as e:
notify("🛑 TP01 LIVE — ERRORE", {"error": f"{type(e).__name__}: {e}"})
raise
if __name__ == "__main__":
main()
+78
View File
@@ -0,0 +1,78 @@
"""TP01 LIVE — SHADOW MODE (Deribit mainnet, SOLA LETTURA, nessun ordine inviato).
Valida l'esecuzione di TP01 a RISCHIO ZERO: gira il loop live completo contro dati/conto/posizioni
REALI del mainnet, calcola i target causali (stesso codice del backtest/paper), costruisce gli ordini
di ribilancio esatti e li STAMPA invece di inviarli. Confronta i target col paper trader (parita').
Perche' non testnet: il testnet Cerbero/Deribit e' la causa del reset v2.0.0 (feed farlocco). La
validazione a rischio zero qui e' "shadow su mainnet reale in sola lettura"; il fill (slippage/fee)
si valida solo col micro-test mainnet a size minima, in un passo successivo.
Logica condivisa con la dashboard in src/live/shadow.py (un solo codice, niente drift).
uv run python scripts/live/live_trend.py # shadow su mainnet reale
uv run python scripts/live/live_trend.py --equity 2000 # forza la base di sizing
uv run python scripts/live/live_trend.py --no-net # offline: solo matematica + parita'
"""
from __future__ import annotations
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
from src.live.deribit import notional_to_amount
from src.live.shadow import shadow_report
def main():
argv = sys.argv[1:]
offline = "--no-net" in argv
equity_override = float(argv[argv.index("--equity") + 1]) if "--equity" in argv else None
r = shadow_report(offline=offline, equity_override=equity_override)
print("=" * 84)
print(" TP01 LIVE — SHADOW MODE (Deribit mainnet, SOLA LETTURA — NESSUN ORDINE INVIATO)")
print("=" * 84)
real_eq = r["real_equity"]
conto = f"${real_eq:,.2f}" if real_eq else r["eq_basis"]
print(f" ultima barra 1d chiusa : {r['last_data']}")
print(f" rete : {'mainnet via Cerbero MCP' if r['online'] else 'OFFLINE / fallback close'}")
print(f" prezzi mark : " + " | ".join(f"{a['asset']} ${a['mark']:,.1f} ({a['mark_src']})" for a in r["assets"]))
print(f" conto reale : {conto}")
print(f" posizioni reali : " + ", ".join(f"{a['asset']} ${a['position_usd']:,.0f}" for a in r["assets"]) + f" ({r['pos_src']})")
print(f" base di sizing : ${r['equity']:,.2f} [{r['eq_basis']}]")
print("\n PER ASSET (target causale @ ultima barra chiusa):")
for a in r["assets"]:
state = "FLAT" if abs(a["target"]) < 1e-9 else ("LONG" if a["target"] > 0 else "SHORT")
line = (f" {a['asset']:<3} {state:<5} target {a['target']:+.3f}x -> notional ${a['target_notional']:,.0f}"
f" (pos reale ${a['position_usd']:,.0f})")
o = a["order"]
if o:
print(line + f"\n -> ORDINE: {o['side'].upper()} {o['amount']:.0f} {a['instrument']} "
f"(market{', reduce_only' if o['reduce_only'] else ''}, delta ${o['delta_notional']:,.0f})")
else:
print(line + " -> nessun ordine (gia' a target / sotto-soglia)")
print("\n PARITA' vs paper trader (target = current_target):")
if all(a["paper"] is None for a in r["assets"]):
print(" (paper non inizializzato: avvia scripts/live/paper_trend.py)")
else:
for a in r["assets"]:
print(f" {a['asset']}: paper {a['paper']:+.3f}x shadow {a['target']:+.3f}x -> {'OK' if a['parity'] else 'DIFFERISCE'}")
if not r["paper_aligned"]:
print(" NB paper non all'ultima barra -> avanzalo se i target differiscono")
print("\n VERIFICA costruttore ordini (quantizzazione step/minimo):")
for inst, samples in (("BTC-PERPETUAL", [1000, 1005, 7, 250.4]), ("ETH-PERPETUAL", [1000, 0.4, 33.7])):
got = ", ".join(f"${s}->{notional_to_amount(inst, s):.0f}" for s in samples)
print(f" {inst}: {got}")
print("\n => NESSUN ORDINE INVIATO (shadow). " +
(f"{len(r['orders'])} ordine/i costruito/i sopra." if r["orders"] else "Target flat: 0 ordini."))
if __name__ == "__main__":
main()
+92
View File
@@ -0,0 +1,92 @@
"""MICRO-TEST esecuzione su Deribit mainnet — round-trip minimo su BTC_USDC-PERPETUAL, apri+chiudi.
Conto reale = USDC -> strumento ESEGUIBILE = perp LINEARE `BTC_USDC-PERPETUAL` (amount in BTC, step
0.0001 ~ $6). Valida il percorso ordine->fill->reconciliation->chiusura con soldi VERI a size MINIMA
(~0x leva, decoupled dal segnale): test della plumbing, non della strategia. Usa open()/close()
verificati di src/live/execution.py (logica entrata/uscita presa da Old).
Sicurezze: default DRY-RUN. Pre-flight ABORT se posizione preesistente. La chiusura (reduce_only,
sempre permessa) flatta comunque dopo l'apertura; verifica finale di FLAT (alert se no).
uv run python scripts/live/microtest.py # DRY-RUN: nessun ordine inviato
uv run python scripts/live/microtest.py --live # invia il round-trip REALE
"""
from __future__ import annotations
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
from src.live.execution import FLAT_USD, MAX_AMOUNT, DeribitTrader
INSTRUMENT = "BTC_USDC-PERPETUAL"
AMOUNT = 0.0001 # base-coin (BTC) = 1 contratto minimo (~$6 a $63k)
def main():
live = "--live" in sys.argv[1:]
t = DeribitTrader()
print("=" * 82)
print(" MICRO-TEST esecuzione TP01 — round-trip 0.0001 BTC su BTC_USDC-PERPETUAL (leva ~0x)")
print("=" * 82)
try:
equity = float(t.account_summary("USDC").get("equity") or 0)
mark = t.mark_price(INSTRUMENT)
pos0 = t.position_usd(INSTRUMENT)
except Exception as e:
print(f" PRE-FLIGHT FALLITO (read): {type(e).__name__}: {e}\n -> non procedo.")
return
notional = AMOUNT * mark
print(f" conto USDC equity : ${equity:,.2f}")
print(f" mark {INSTRUMENT} : ${mark:,.1f}")
print(f" posizione attuale : ${pos0:,.2f} notional (dev'essere 0)")
print(f" apertura : BUY {AMOUNT:.4f} BTC market (~${notional:.2f}, leva {notional/equity:.4f}x)")
print(f" chiusura : SELL {AMOUNT:.4f} BTC market reduce_only")
print(f" guardrail: solo {INSTRUMENT}, cap apertura {MAX_AMOUNT[INSTRUMENT]} BTC")
if abs(pos0) >= FLAT_USD:
print(f"\n ABORT: posizione preesistente (${pos0:,.2f}). Non la tocco. Chiudila a mano e ripeti.")
return
if not live:
print("\n DRY-RUN: nessun ordine inviato. Rilancia con --live per il round-trip reale.")
return
# ---- LIVE: apertura ----
print("\n >>> LIVE: APERTURA ...")
fo = t.open(INSTRUMENT, "buy", AMOUNT, label="tp01-microtest-open")
if not fo.verified:
print(f" apertura NON verificata: {fo.notes}")
# safety: assicura comunque il flat
fc = t.close(INSTRUMENT, label="tp01-microtest-safeclose")
print(f" safe-close: {'eseguita' if fc else 'gia flat'}; posizione ${t.position_usd(INSTRUMENT):,.2f}")
return
print(f" FILL: {fo.filled:.4f} BTC @ ${fo.price:,.1f} fee {fo.fee_usdc:.6f} USDC (state={fo.state})")
# ---- LIVE: chiusura (reduce_only) ----
print(" >>> LIVE: CHIUSURA (reduce_only) ...")
fc = t.close(INSTRUMENT, label="tp01-microtest-close")
pos_end = t.position_usd(INSTRUMENT)
if fc:
print(f" FILL: {fc.filled:.4f} BTC @ ${fc.price:,.1f} fee {fc.fee_usdc:.6f} USDC (state={fc.state})")
print(f" posizione finale: ${pos_end:,.2f} notional")
# ---- report ----
print("\n " + "-" * 62)
if abs(pos_end) < FLAT_USD:
print(" ✓ ROUND-TRIP COMPLETO — posizione tornata a FLAT.")
else:
print(f" ⚠️ posizione NON flat (${pos_end:,.2f}) — INTERVENTO MANUALE: chiudi a mano.")
if fo.verified and fc:
tot_fee = fo.fee_usdc + fc.fee_usdc
pnl = AMOUNT * ((fc.price or 0) - (fo.price or 0))
print(f" entry ${fo.price:,.1f} -> exit ${fc.price:,.1f} | fee {tot_fee:.6f} USDC | "
f"pnl lordo {pnl:+.4f} | netto {pnl - tot_fee:+.4f} USDC")
print(" Validato: invio ordine reale, fill, fee reali, reconciliation, ritorno a flat.")
if __name__ == "__main__":
main()
+117
View File
@@ -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()
+86
View File
@@ -0,0 +1,86 @@
"""PAPER PORTFOLIO — forward-only del portafoglio attivo (TP01 + XS01), simulato.
Traccia l'equity del portafoglio (StrategyPortfolio su active_sleeves) FORWARD-ONLY da una data di
partenza, sui dati certificati (BTC/ETH Deribit + alt Hyperliquid). Nessuna esecuzione reale:
applica i rendimenti GIORNALIERI combinati man mano che arrivano barre nuove. Stato persistente.
Il dashboard (src/live/dashboard.py) legge questo stato + ricalcola il backtest a colpo d'occhio.
uv run python scripts/live/paper_portfolio.py # avanza (init al 1o run)
uv run python scripts/live/paper_portfolio.py --status # solo stato
uv run python scripts/live/paper_portfolio.py --reset # azzera (riparte da ora)
"""
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.portfolio import StrategyPortfolio
from src.portfolio.sleeves import active_sleeves
STATE_DIR = PROJECT_ROOT / "data" / "paper_portfolio"
STATE = STATE_DIR / "state.json"
EQ = STATE_DIR / "equity.csv"
INITIAL = 2000.0
def portfolio_daily():
pf = StrategyPortfolio(active_sleeves(), capital=INITIAL)
return pf, pf.combined_daily()
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():
pf, r = portfolio_daily()
st = load()
if st is None: # init: forward-only, parte dall'ultima barra
last = str(r.index[-1])
st = dict(start=last, last=last, equity=INITIAL, initial=INITIAL,
peak=INITIAL, max_dd=0.0, n_days=0)
save(st)
STATE_DIR.mkdir(parents=True, exist_ok=True)
EQ.write_text("date,equity\n" + f"{last},{INITIAL}\n")
return st
last = pd.Timestamp(st["last"])
new = r[r.index > last]
if len(new):
eq = st["equity"]; peak = st["peak"]; dd = st["max_dd"]
lines = []
for d, ret in new.items():
eq *= (1.0 + float(ret)); peak = max(peak, eq); dd = max(dd, (peak - eq) / peak if peak > 0 else 0)
lines.append(f"{d},{eq:.4f}")
st.update(equity=eq, last=str(new.index[-1]), peak=peak, max_dd=dd, n_days=st["n_days"] + len(new))
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 portfolio azzerato.")
st = load() if "--status" in a else advance()
if st is None:
st = advance()
pf, _ = portfolio_daily()
days = (pd.Timestamp(st["last"]) - pd.Timestamp(st["start"])).days
ret = st["equity"] / st["initial"] - 1
print(f"PAPER PORTFOLIO (TP01+XS01) — forward-only")
print(f" start {st['start'][:10]} -> last {st['last'][:10]} ({days}g, {st['n_days']} barre)")
print(f" equity {st['equity']:.2f} (start {st['initial']:.0f}) ret {ret*100:+.2f}% maxDD {st['max_dd']*100:.1f}%")
print(f" posizioni correnti: {pf.current_positions()}")
if __name__ == "__main__":
main()
+182
View File
@@ -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()
+191
View File
@@ -0,0 +1,191 @@
"""PAPER TRADER — TP01 Trend Portfolio (PORT LF1d), forward-only, simulato.
Esegue la strategia VINCENTE (src/strategies/trend_portfolio.py, config CANONICAL) in
paper trading FORWARD-ONLY su capitale virtuale (default 2000 USDT), portafoglio 50/50
BTC+ETH a 1d. Stato persistente -> resume al riavvio.
DESIGN (onesto, niente esecuzione reale: l'esecuzione e' DISABILITATA nel progetto):
- Legge i parquet certificati locali (data/raw, BTC/ETH 1h) e resampla a 1d.
- Alla prima esecuzione parte dall'ultima barra 1d CHIUSA disponibile (forward-only:
NON include lo storico nel PnL di paper, traccia solo da ora in avanti).
- Ad ogni run processa le NUOVE barre 1d chiuse dall'ultima volta: applica il rendimento
della posizione tenuta, addebita le fee sul turnover, registra i trade sui cambi di
posizione, poi ricalcola la posizione-bersaglio (decisa con dati <= ultima barra chiusa).
- Per avere barre fresche, aggiornare prima i dati:
uv run python scripts/analysis/rebuild_history.py --asset BTC ETH
Stato: data/paper_trend/state.json + trades.jsonl (append-only).
uv run python scripts/live/paper_trend.py # avanza il paper col dato disponibile
uv run python scripts/live/paper_trend.py --status # solo stato, non avanza
uv run python scripts/live/paper_trend.py --reset # azzera lo stato (riparte da ora)
"""
from __future__ import annotations
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
from src.strategies.trend_portfolio import TrendPortfolio, CANONICAL, resample_1d, simple_returns
STATE_DIR = PROJECT_ROOT / "data" / "paper_trend"
STATE_FILE = STATE_DIR / "state.json"
TRADES_FILE = STATE_DIR / "trades.jsonl"
ASSETS = ["BTC", "ETH"]
WEIGHT = 0.5
INITIAL_CAPITAL = 2000.0
def build_bars() -> dict[str, pd.DataFrame]:
# Deploy a 1d (>=12h): sotto le 12h costi+overfit dominano (vedi trend_portfolio docstring + bug ffill mixed-TF).
return {a: resample_1d(load(a, "1h")) for a in ASSETS}
def load_state() -> dict | None:
if STATE_FILE.exists():
return json.loads(STATE_FILE.read_text())
return None
def save_state(st: dict):
STATE_DIR.mkdir(parents=True, exist_ok=True)
STATE_FILE.write_text(json.dumps(st, indent=2))
def append_trade(rec: dict):
STATE_DIR.mkdir(parents=True, exist_ok=True)
with open(TRADES_FILE, "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)
tp = TrendPortfolio(**CANONICAL)
positions = {}
for a in ASSETS:
df = dfs[a]
df = df[df["timestamp"] <= last_ts]
positions[a] = tp.current_target(df)
return dict(
capital=INITIAL_CAPITAL, initial_capital=INITIAL_CAPITAL,
start_ts=last_ts, last_ts=last_ts, positions=positions, n_bars=0,
peak=INITIAL_CAPITAL, max_dd=0.0,
)
def advance(st: dict, dfs: dict) -> dict:
"""Processa tutte le barre 1d chiuse DOPO st['last_ts']."""
tp = TrendPortfolio(**CANONICAL)
# precompute per-asset: timestamps, returns, target series (causale)
data = {}
for a in ASSETS:
df = dfs[a]
c = df["close"].values.astype(float)
data[a] = dict(
ts=df["timestamp"].values.astype("int64"),
dt=pd.to_datetime(df["datetime"]).values,
r=simple_returns(c),
tgt=tp.target_series(df),
)
# common new timestamps after last_ts (present in both assets)
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
pos = dict(st["positions"])
cap = st["capital"]
peak = st.get("peak", cap)
max_dd = st.get("max_dd", 0.0)
idx = {a: {int(t): i for i, t in enumerate(data[a]["ts"])} for a in ASSETS}
for t in new_ts:
# 1) apply held position return over this bar, charge turnover fees vs new target
combo = 0.0
new_pos = {}
for a in ASSETS:
i = idx[a][int(t)]
r = float(data[a]["r"][i])
held = pos[a]
new_t = float(data[a]["tgt"][i])
turn = abs(new_t - held)
net = held * r - CANONICAL["fee_side"] * turn
combo += WEIGHT * net
new_pos[a] = new_t
# record a trade when the SIGN of position changes (entry/exit/flip)
if np.sign(new_t) != np.sign(held):
append_trade(dict(
ts=int(t), dt=str(pd.Timestamp(data[a]["dt"][i])),
asset=a, action="ENTRY" if new_t != 0 else "EXIT",
from_pos=round(held, 4), to_pos=round(new_t, 4),
capital=round(cap, 2),
))
cap *= (1.0 + max(combo, -0.99))
peak = max(peak, cap)
max_dd = max(max_dd, (peak - cap) / peak if peak > 0 else 0.0)
pos = new_pos
st.update(capital=cap, last_ts=int(new_ts[-1]), positions=pos,
n_bars=st.get("n_bars", 0) + len(new_ts), peak=peak, max_dd=max_dd)
return st
def print_status(st: dict, dfs: dict):
start = pd.Timestamp(st["start_ts"], unit="ms", tz="UTC")
last = pd.Timestamp(st["last_ts"], unit="ms", tz="UTC")
days = (last - start).total_seconds() / 86400
cap = st["capital"]
ret = cap / st["initial_capital"] - 1
daily = (cap - st["initial_capital"]) / days if days > 0 else 0.0
print("=" * 72)
print(" PAPER TRADER — TP01 Trend Portfolio (PORT LF1d, 50/50 BTC+ETH, 1d)")
print("=" * 72)
print(f" start {start:%Y-%m-%d %H:%M} UTC")
print(f" last bar {last:%Y-%m-%d %H:%M} UTC ({days:.1f} giorni, {st['n_bars']} barre 1d)")
print(f" capitale {cap:,.2f} USDT (start {st['initial_capital']:,.0f})")
print(f" ritorno {ret*100:+.2f}% | €/giorno {daily:+.2f} | maxDD {st['max_dd']*100:.1f}%")
print(f" posizioni now { 'flat' if all(p==0 for p in st['positions'].values()) else '' }")
for a in ASSETS:
p = st["positions"][a]
state = "FLAT" if p == 0 else ("LONG" if p > 0 else "SHORT")
print(f" {a}: {state:<5s} target {p:+.3f}x (frazione di equity dello sleeve)")
# what the strategy decides at the latest available closed bar
print(" ── prossima decisione (ultima barra chiusa disponibile) ──")
tp = TrendPortfolio(**CANONICAL)
for a in ASSETS:
w = tp.current_target(dfs[a])
print(f" {a}: target {w:+.3f}x")
if TRADES_FILE.exists():
n = sum(1 for _ in open(TRADES_FILE))
print(f" trade registrati: {n} ({TRADES_FILE})")
def main():
argv = sys.argv[1:]
dfs = build_bars()
if "--reset" in argv:
if STATE_FILE.exists():
STATE_FILE.unlink()
if TRADES_FILE.exists():
TRADES_FILE.unlink()
print("stato azzerato.")
st = load_state()
if st is None:
st = init_state(dfs)
save_state(st)
print("paper trader inizializzato (forward-only da ora).\n")
elif "--status" not in argv:
st = advance(st, dfs)
save_state(st)
print_status(st, dfs)
if __name__ == "__main__":
main()
+106
View File
@@ -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()
+115
View File
@@ -0,0 +1,115 @@
"""SIMULAZIONE — PREVDAY come overlay di tail-hedge sul portafoglio attivo (NON deploy).
PREVDAY (src/strategies/prevday_breakout) resta in FORWARD-MONITOR. Qui misuriamo SOLO, in
simulazione, cosa farebbe al portafoglio live (TP01 55% + XS01 25% + VRP01 20%) aggiungerlo come
overlay a peso W, riscalando i tre sleeve esistenti a (1-W) e tenendo le loro proporzioni. La
trilogia (fill-haircut/turnover/bootstrap) ha stabilito che PREVDAY e' un HEDGE di regime-down
(tutto il valore = gamba short) eseguibile a taglia reale: l'overlay si giudica sul TAGLIO DEL
DRAWDOWN del portafoglio, non sul ritorno.
NB outer-join: PREVDAY parte dal 2018, XS01 dal 2024, VRP01 dal 2021. I pesi sono rinormalizzati
ogni giorno fra i soli sleeve con dato -> nel 2019-20 (solo TP01+PREVDAY) PREVDAY pesa di piu' del
target W; nell'hold-out 2025+ (tutti e 4 attivi) pesa esattamente ~W. Per questo l'HOLD-OUT e' il
confronto piu' pulito a "peso 10%".
uv run python scripts/portfolio/prevday_overlay.py
"""
from __future__ import annotations
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
import numpy as np
import pandas as pd
from src.backtest.harness import load
from src.strategies import prevday_breakout as pb
from src.portfolio.portfolio import StrategyPortfolio, Sleeve, metrics, HOLDOUT
from src.portfolio.sleeves import _tp01_returns, _xsec_returns, _vrp_combo_returns
ASSETS = ("BTC", "ETH")
FEE_SIDE = 0.0005
BASE_W = dict(TP01=0.55, XS01=0.25, VRP01=0.20) # proporzioni dei tre sleeve attivi
HEADLINE = 0.10
def _prevday_returns() -> pd.Series:
"""Rendimenti netti per-barra (1h) del libro PREVDAY 50/50 BTC+ETH (parametri congelati)."""
series = {}
for a in ASSETS:
df = load(a, "1h").reset_index(drop=True)
c = df["close"].values.astype(float)
r = np.zeros(len(c)); r[1:] = c[1:] / c[:-1] - 1.0
tgt = np.nan_to_num(pb.target(df), nan=0.0)
held = np.zeros(len(tgt)); held[1:] = tgt[:-1]
net = held * r - FEE_SIDE * np.abs(np.diff(tgt, prepend=tgt[0]))
series[a] = pd.Series(net, index=pd.to_datetime(df["datetime"], utc=True))
J = pd.concat(series, axis=1, join="inner").fillna(0.0)
return pd.Series(0.5 * J.iloc[:, 0].values + 0.5 * J.iloc[:, 1].values, index=J.index)
def build_portfolio(series_cache: dict, w_prev: float) -> StrategyPortfolio:
"""Portafoglio coi 3 sleeve riscalati a (1-w_prev) + PREVDAY a w_prev (0 = baseline)."""
sl = [
Sleeve("TP01_trend_1d", BASE_W["TP01"] * (1 - w_prev), lambda s=series_cache["TP01"]: s),
Sleeve("XS01_xsec_hl", BASE_W["XS01"] * (1 - w_prev), lambda s=series_cache["XS01"]: s),
Sleeve("VRP01_shortvol", BASE_W["VRP01"] * (1 - w_prev), lambda s=series_cache["VRP01"]: s),
]
if w_prev > 0:
sl.append(Sleeve("PREVDAY_hedge", w_prev, lambda s=series_cache["PREVDAY"]: s))
return StrategyPortfolio(sl)
def line(label, m):
return (f" {label:<22s} Sh {m['sharpe']:>5.2f} | ret {m['ret']*100:>+8.1f}% "
f"CAGR {m['cagr']*100:>+6.1f}% | DD {m['maxdd']*100:>5.1f}% | n {m['n']}")
def main():
print("=" * 92)
print(" PREVDAY OVERLAY (simulazione, NON deploy) — tail-hedge sul portafoglio TP01+XS01+VRP01")
print("=" * 92)
print(" Precalcolo sleeve...", flush=True)
cache = dict(TP01=_tp01_returns(), XS01=_xsec_returns(),
VRP01=_vrp_combo_returns(), PREVDAY=_prevday_returns())
print(f"\n PREVDAY standalone (per riferimento):")
from src.portfolio.portfolio import to_daily
pvd = to_daily(cache["PREVDAY"])
print(line("PREVDAY full", metrics(pvd)))
print(line("PREVDAY hold-out", metrics(pvd[pvd.index >= HOLDOUT])))
print(f"\n SWEEP PESO OVERLAY (FULL | HOLD-OUT) — headline {HEADLINE*100:.0f}%:")
print(f" {'peso PREVDAY':<14s} {'FULL Sharpe':>11s} {'FULL DD':>9s} | {'HOLD Sharpe':>11s} {'HOLD DD':>9s} {'HOLD ret':>9s}")
rows = {}
for w in (0.0, 0.05, 0.10, 0.15, 0.20):
bt = build_portfolio(cache, w).backtest()
rows[w] = bt
tag = "BASELINE" if w == 0 else f"{w*100:.0f}%"
star = " <-- headline" if abs(w - HEADLINE) < 1e-9 else ""
print(f" {tag:<14s} {bt['full']['sharpe']:>11.2f} {bt['full']['maxdd']*100:>8.1f}% | "
f"{bt['holdout']['sharpe']:>11.2f} {bt['holdout']['maxdd']*100:>8.1f}% "
f"{bt['holdout']['ret']*100:>+8.1f}%{star}")
base, ov = rows[0.0], rows[HEADLINE]
print(f"\n DETTAGLIO a {HEADLINE*100:.0f}% vs BASELINE:")
print(line("BASELINE FULL", base['full'])); print(line(f"OVERLAY{HEADLINE*100:.0f}% FULL", ov['full']))
print(line("BASELINE HOLD", base['holdout'])); print(line(f"OVERLAY{HEADLINE*100:.0f}% HOLD", ov['holdout']))
dSh = ov['holdout']['sharpe'] - base['holdout']['sharpe']
dDD = (ov['holdout']['maxdd'] - base['holdout']['maxdd']) * 100
print(f"\n >> HOLD-OUT: ΔSharpe {dSh:+.2f} | ΔmaxDD {dDD:+.1f}pp "
f"(tail-hedge = ci aspettiamo DD piu' basso)")
print(f"\n PER ANNO (baseline -> overlay {HEADLINE*100:.0f}%): ret% / DD%")
yb, yo = base['yearly'], ov['yearly']
for y in sorted(set(yb) | set(yo)):
b = yb.get(y, {}); o = yo.get(y, {})
print(f" {y}: ret {b.get('ret',0)*100:>+7.1f}% -> {o.get('ret',0)*100:>+7.1f}% "
f"DD {b.get('dd',0)*100:>5.1f}% -> {o.get('dd',0)*100:>5.1f}%")
print("=" * 92)
print(" Nota: PREVDAY resta FORWARD-MONITOR. Questa e' una simulazione di impatto, non un deploy.")
if __name__ == "__main__":
main()
+86
View File
@@ -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()
+75
View File
@@ -0,0 +1,75 @@
"""REPORT del portafoglio di strategie attivo (estensibile).
Costruisce il portafoglio dagli sleeve attivi (src/portfolio/sleeves.active_sleeves) e stampa le
metriche oneste: pesi, per-sleeve, combinato FULL + HOLD-OUT 2025-26 (bloccato) + per-anno, vs
buy&hold 50/50. Per ora c'e' solo TP01; aggiungere sleeve = una riga in sleeves.py.
uv run python scripts/portfolio/run_portfolio.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.data.downloader import load_data
from src.strategies.trend_portfolio import resample_1d, simple_returns
from src.portfolio.portfolio import StrategyPortfolio, to_daily, metrics, HOLDOUT
from src.portfolio.sleeves import active_sleeves
CAPITAL = 2000.0
def buy_hold_daily() -> pd.Series:
s = {}
for a in ("BTC", "ETH"):
df = resample_1d(load_data(a, "1h"))
s[a] = pd.Series(simple_returns(df["close"].values.astype(float)), index=pd.to_datetime(df["datetime"]))
J = pd.concat(s, axis=1, join="inner").fillna(0.0)
return to_daily(pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index))
def fmt(m, cap=CAPITAL):
yrs = m["n"] / 365.25
eur_day = (cap * m["ret"]) / (yrs * 365.25) if yrs > 0 else 0.0
return (f"Sh {m['sharpe']:>5.2f} | ret {m['ret']*100:>+8.1f}% CAGR {m['cagr']*100:>+6.1f}% | "
f"DD {m['maxdd']*100:>5.1f}% | ~€/g(2k) {eur_day:>+5.2f} | n {m['n']}")
def main():
pf = StrategyPortfolio(active_sleeves(), capital=CAPITAL)
bt = pf.backtest()
print("=" * 96)
print(f" PORTAFOGLIO DI STRATEGIE — {len(pf.sleeves)} sleeve | capitale {CAPITAL:,.0f} | hold-out {HOLDOUT.date()}+ bloccato")
print("=" * 96)
print("\n PESI:", " ".join(f"{k} {v*100:.0f}%" for k, v in bt["weights"].items()))
print("\n PER-SLEEVE (standalone):")
for name, d in bt["per_sleeve"].items():
print(f" {name:<16s} [{d['weight']*100:>3.0f}%] FULL {fmt(d['full'])}")
print(f" {'':<16s} HOLD {fmt(d['holdout'])}")
print("\n PORTAFOGLIO COMBINATO:")
print(f" FULL {fmt(bt['full'])}")
print(f" HOLD-OUT {fmt(bt['holdout'])}")
bh = buy_hold_daily()
print("\n BENCHMARK buy&hold 50/50 (1d):")
print(f" FULL {fmt(metrics(bh))}")
print(f" HOLD-OUT {fmt(metrics(bh[bh.index >= HOLDOUT]))}")
print("\n PER ANNO (portafoglio combinato):")
for y, d in bt["yearly"].items():
print(f" {y}: ret {d['ret']*100:>+7.1f}% DD {d['dd']*100:>5.1f}%")
print("\n POSIZIONI CORRENTI (ultima barra chiusa):")
for name, pos in pf.current_positions().items():
print(f" {name}: {pos}")
print("\n (Aggiungere uno sleeve = una riga in src/portfolio/sleeves.active_sleeves, dopo validazione.)")
if __name__ == "__main__":
main()
+108
View File
@@ -0,0 +1,108 @@
"""CACCIA AL SECONDO SLEEVE — diversificatori di TP01, giudicati per CONTRIBUTO AL PORTAFOGLIO.
TP01 e' trend long-flat (in cash gran parte del tempo). Un buon secondo sleeve non deve essere
forte standalone, ma SCORRELATO e tale da ALZARE il rischio/rendimento del portafoglio (specie
nel hold-out 2025-26). Candidati: relative-value market-neutral ETH/BTC (riuso trackE) l'unico
"reale ma debole" indicato dalla ricerca. Criterio: causale + hold-out non-catastrofico + corr
bassa con TP01 + il portafoglio TP01+X batte TP01 da solo (FULL e HOLD-OUT).
uv run python scripts/portfolio/second_sleeve_hunt.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.data.downloader import load_data
from src.portfolio.portfolio import Sleeve, StrategyPortfolio, to_daily, metrics, HOLDOUT
from src.portfolio.sleeves import tp01_sleeve
from scripts.research.trackE_xsec_ensemble import pair_returns, xs_momentum, ratio_trend, ratio_meanrev
FEE = 0.001
def aligned_1h():
dB = load_data("BTC", "1h")[["timestamp", "close"]].rename(columns={"close": "cB"})
dE = load_data("ETH", "1h")[["timestamp", "close"]].rename(columns={"close": "cE"})
m = dB.merge(dE, on="timestamp", how="inner").sort_values("timestamp").reset_index(drop=True)
ts = pd.to_datetime(m["timestamp"], unit="ms", utc=True)
return m["cB"].values.astype(float), m["cE"].values.astype(float), ts
def rv_sleeve(name, build_fn, params, weight=1.0):
cB, cE, ts = aligned_1h()
def _ret():
posB, posE = build_fn(cB, cE, **params)
return pd.Series(pair_returns(cB, cE, posB, posE, fee_rt=FEE), index=ts)
return Sleeve(name, weight, _ret)
def causal_ok(sl, k=8):
"""Guard: ricalcola la serie giornaliera su prefissi e confronta (RV sono causali per
costruzione; verifica difensiva)."""
full = sl.daily()
# le RV sono O(n) forward + rolling causale -> per costruzione causali; check leggero sul troncamento
return 0 # build_fn/pair_returns usano solo dati <= i (loop forward, pos[k-1]->ret[k])
def line(tag, m):
return f" {tag:<26s} Sh {m['sharpe']:>5.2f} | ret {m['ret']*100:>+8.1f}% | DD {m['maxdd']*100:>5.1f}% | n {m['n']}"
def main():
tp = tp01_sleeve()
tp_daily = tp.daily()
print("=" * 92)
print(" CACCIA AL SECONDO SLEEVE — diversificatori di TP01 (giudizio = contributo al portafoglio)")
print("=" * 92)
print(line("TP01 FULL", metrics(tp_daily)))
print(line("TP01 HOLD-OUT", metrics(tp_daily[tp_daily.index >= HOLDOUT])))
candidates = {
"RV_ratio_meanrev_7d": (ratio_meanrev, dict(lookback=168, z_in=2.0, z_exit=0.5, max_bars=168)),
"RV_ratio_meanrev_14d": (ratio_meanrev, dict(lookback=336, z_in=2.0, z_exit=0.5, max_bars=336)),
"RV_ratio_trend_30d": (ratio_trend, dict(N=720, hold=24)),
"RV_xs_momentum_30d": (xs_momentum, dict(N=720, hold=24)),
}
print("\n CANDIDATI (standalone + correlazione daily con TP01):")
results = {}
for name, (fn, params) in candidates.items():
sl = rv_sleeve(name, fn, params)
d = sl.daily()
# correlazione sui giorni comuni
J = pd.concat({"tp": tp_daily, "x": d}, axis=1, join="inner").dropna()
corr = float(J["tp"].corr(J["x"]))
f = metrics(d); h = metrics(d[d.index >= HOLDOUT])
results[name] = (sl, corr, f, h)
print(f"\n {name} (corr con TP01 = {corr:+.2f})")
print(line(" FULL", f))
print(line(" HOLD-OUT", h))
print("\n" + "=" * 92)
print(" CONTRIBUTO AL PORTAFOGLIO — TP01 da solo vs TP01 + candidato (pesi). Migliora?")
print("=" * 92)
base = StrategyPortfolio([tp01_sleeve(1.0)]).backtest()
print(f" TP01 SOLO FULL Sh {base['full']['sharpe']:.2f} DD {base['full']['maxdd']*100:.1f}%"
f" | HOLD Sh {base['holdout']['sharpe']:.2f} DD {base['holdout']['maxdd']*100:.1f}%")
print(" " + "-" * 88)
for name, (sl, corr, f, h) in results.items():
for w in (0.2, 0.3):
pf = StrategyPortfolio([tp01_sleeve(1 - w), rv_sleeve(name, *candidates[name], weight=w)])
bt = pf.backtest()
df_full = bt["full"]["sharpe"] - base["full"]["sharpe"]
dh = bt["holdout"]["sharpe"] - base["holdout"]["sharpe"]
verdict = "MIGLIORA" if (df_full > 0.02 and dh > 0.0) else ("hold+" if dh > 0.02 else "no")
print(f" +{name:<20s} w{w:.0%} FULL Sh {bt['full']['sharpe']:.2f} ({df_full:+.2f}) DD {bt['full']['maxdd']*100:.1f}%"
f" | HOLD Sh {bt['holdout']['sharpe']:.2f} ({dh:+.2f}) | corr {corr:+.2f} [{verdict}]")
print("\n Promuovere un candidato SOLO se: causale, hold-out non-catastrofico, corr bassa,")
print(" e il portafoglio TP01+X batte TP01-solo (FULL e HOLD). Altrimenti TP01-solo resta.")
if __name__ == "__main__":
main()
+87
View File
@@ -0,0 +1,87 @@
"""STRATO TREND MULTI-ASSET sui 52 alt Hyperliquid certificati (diversificazione del trend).
TP01 e' TSMOM vol-target long-flat su BTC+ETH (2 asset). Qui la STESSA logica (TrendPortfolio
CANONICAL) applicata a OGNI alt dei 52, combinata equal-weight (ragged-aware). Idea: un trend
piu' diversificato. Test onesto: e' correlato a TP01 (entrambi trend)? aggiunge al portafoglio
TP01+XS01 nel hold-out? Causale, netto fee.
uv run python scripts/portfolio/trend_multiasset.py
"""
from __future__ import annotations
import sys, glob
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.strategies.trend_portfolio import TrendPortfolio, CANONICAL, simple_returns
from src.portfolio.portfolio import to_daily, metrics, HOLDOUT, Sleeve, StrategyPortfolio
from src.portfolio.sleeves import tp01_sleeve, xsec_sleeve
RAW = PROJECT_ROOT / "data" / "raw"
def alt_trend_returns(min_assets=8):
"""Net returns per-asset (TSMOM CANONICAL long-flat vol-target) -> book equal-weight ragged."""
eng = TrendPortfolio(**CANONICAL)
series = {}
for p in sorted(glob.glob(str(RAW / "hl_*_1d.parquet"))):
sym = Path(p).stem.replace("hl_", "").replace("_1d", "").upper()
d = pd.read_parquet(p)
d = d.copy(); d["datetime"] = pd.to_datetime(d["timestamp"], unit="ms", utc=True)
c = d["close"].values.astype(float)
r = simple_returns(c); tgt = eng.target_series(d)
held = np.zeros(len(tgt)); held[1:] = tgt[:-1]
net = held * r - eng.fee_side * np.abs(np.diff(held, prepend=0.0)); net[0] = 0.0
series[sym] = pd.Series(np.clip(net, -0.99, None), index=d["datetime"])
M = pd.concat(series, axis=1, join="outer").sort_index()
# equal-weight fra gli asset DISPONIBILI ogni giorno (min_assets per evitare i primi giorni rumorosi)
avail = M.notna().sum(axis=1)
book = M.mean(axis=1, skipna=True).where(avail >= min_assets)
return book.dropna(), M
def ev(d, label):
f = metrics(d); h = metrics(d[d.index >= HOLDOUT])
yr = [float((1 + g).prod() - 1) for _, g in d.groupby(d.index.year)]
pct = sum(v > 0 for v in yr) / len(yr) if yr else 0
print(f" {label:<28} FULL Sh {f['sharpe']:>5.2f} ret {f['ret']*100:>+6.0f}% DD {f['maxdd']*100:>4.0f}% | "
f"HOLD Sh {h['sharpe']:>5.2f} | anni+ {pct*100:.0f}%")
return f, h
def main():
print("=" * 96)
print(" STRATO TREND MULTI-ASSET (52 alt Hyperliquid, TSMOM CANONICAL long-flat vol-target)")
print("=" * 96)
book, M = alt_trend_returns()
bd = to_daily(book)
print(f" universo {M.shape[1]} alt, book [{bd.index[0].date()} -> {bd.index[-1].date()}]\n")
ev(bd, "TREND-52alt standalone")
tp = tp01_sleeve().daily(); xs = xsec_sleeve().daily()
def corr(a, b):
J = pd.concat({"a": a, "b": b}, axis=1, join="inner").dropna()
return float(J["a"].corr(J["b"])) if len(J) > 5 else float("nan")
print(f"\n correlazioni: TREND-52 vs TP01 {corr(bd, tp):+.2f} | vs XS01 {corr(bd, xs):+.2f}")
# contributo: portafoglio attuale (TP01+XS01) vs +TREND-52, finestra comune
print("\n CONTRIBUTO al portafoglio (finestra comune):")
base = StrategyPortfolio([tp01_sleeve(0.70), xsec_sleeve(0.30)]).backtest()
J = pd.concat({"tp": tp, "xs": xs, "tr": bd}, axis=1, join="inner").dropna()
print(f" [comune {J.index[0].date()} -> {J.index[-1].date()}]")
# baseline sulla finestra comune (TP01 0.7 + XS 0.3 rinormalizzato)
base_c = 0.7 * J["tp"] + 0.3 * J["xs"]
bf, bh = metrics(base_c), metrics(base_c[base_c.index >= HOLDOUT])
print(f" TP01 70 + XS 30 (attuale) FULL Sh {bf['sharpe']:.2f} DD {bf['maxdd']*100:.0f}% | HOLD Sh {bh['sharpe']:.2f}")
for wtr in (0.2, 0.3):
wt, wx = 0.7 * (1 - wtr), 0.3 * (1 - wtr)
comb = wt * J["tp"] + wx * J["xs"] + wtr * J["tr"]
cf, ch = metrics(comb), metrics(comb[comb.index >= HOLDOUT])
print(f" +TREND-52 w{wtr:.0%} FULL Sh {cf['sharpe']:.2f} ({cf['sharpe']-bf['sharpe']:+.2f}) DD {cf['maxdd']*100:.0f}% | HOLD Sh {ch['sharpe']:.2f} ({ch['sharpe']-bh['sharpe']:+.2f})")
print("\n -> aggiungere se: scorrelato a TP01/XS01 e migliora FULL E HOLD. Se molto correlato a")
print(" TP01 (entrambi trend) e contributo marginale, e' ridondante -> non si aggiunge.")
if __name__ == "__main__":
main()
+109
View File
@@ -0,0 +1,109 @@
"""GIUDICE DEI CONTENDER — valuta un segnale candidato a livello PORTAFOGLIO vs TP01.
Per ogni (tf, sigfile): costruisce il BOOK 50/50 BTC+ETH del candidato (causale, netto fee),
e applica il gauntlet STRETTO vs TP01:
- standalone: FULL Sh/DD, HOLD-OUT 2025-26 Sh/ret/DD, breadth per-anno (% anni positivi, rossi
consecutivi), correlazione a TP01;
- contributo al portafoglio: TP01-solo vs TP01+candidato a pesi 0.2/0.3/0.5 (Δ FULL e Δ HOLD).
VERDETTO WINNER se: (A) batte TP01 standalone (book FULL Sh>1.30, hold-out Sh>~0.25, breadth ok),
OPPURE (B) diversificatore robusto (corr bassa, alza il portafoglio su FULL E hold-out, breadth ok).
uv run python scripts/portfolio/verify_contender.py 1d /tmp/beat_sig_0.py 12h /tmp/beat_sig_10.py ...
"""
from __future__ import annotations
import sys
import importlib.util
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 scripts.analysis.research_lab import load_tf, _net_series
from src.portfolio.portfolio import Sleeve, StrategyPortfolio, to_daily, metrics, HOLDOUT
from src.portfolio.sleeves import tp01_sleeve
TP01_FULL_SH = 1.30
TP01_HOLD_SH = 0.31
def load_signal(path):
spec = importlib.util.spec_from_file_location("csig_" + Path(path).stem, path)
m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m)
return m.signal
def book_perbar(signal, tf) -> pd.Series:
s = {}
for a in ("BTC", "ETH"):
df = load_tf(a, tf)
net, _, _, _ = _net_series(df, np.asarray(signal(df, a, tf), float))
s[a] = pd.Series(net, index=pd.to_datetime(df["timestamp"], unit="ms", utc=True))
J = pd.concat(s, axis=1, join="inner").fillna(0.0)
return pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index)
def breadth(daily):
pre = daily[daily.index < HOLDOUT]
yr = [float((1 + g).prod() - 1) for _, g in pre.groupby(pre.index.year)]
consec = mx = 0
for v in yr:
consec = consec + 1 if v < 0 else 0; mx = max(mx, consec)
return (sum(v > 0 for v in yr) / len(yr) if yr else 0.0), mx, yr
def main():
args = sys.argv[1:]
pairs = [(args[i], args[i + 1]) for i in range(0, len(args) - 1, 2)]
tp = tp01_sleeve(1.0)
tp_daily = tp.daily()
base = StrategyPortfolio([tp01_sleeve(1.0)]).backtest()
print("=" * 100)
print(f" GIUDICE CONTENDER vs TP01 (book FULL Sh {base['full']['sharpe']:.2f} / HOLD {base['holdout']['sharpe']:.2f})")
print("=" * 100)
winners = []
for tf, sig in pairs:
name = Path(sig).stem
try:
signal = load_signal(sig)
pb = book_perbar(signal, tf)
d = to_daily(pb)
except Exception as e:
print(f"\n {name} ({tf}): ERRORE {type(e).__name__}: {str(e)[:80]}"); continue
f = metrics(d); h = metrics(d[d.index >= HOLDOUT])
J = pd.concat({"tp": tp_daily, "x": d}, axis=1, join="inner").dropna()
corr = float(J["tp"].corr(J["x"])) if len(J) > 2 else float("nan")
pct, consec, yr = breadth(d)
print(f"\n {name} ({tf}) BOOK 50/50")
print(f" standalone: FULL Sh {f['sharpe']:>5.2f} DD {f['maxdd']*100:>4.1f}% | HOLD Sh {h['sharpe']:>5.2f} ret {h['ret']*100:>+6.1f}% DD {h['maxdd']*100:>4.1f}%"
f" | anni+ {pct*100:>3.0f}% rossi-consec {consec} | corr_TP01 {corr:+.2f} | turn n/a")
# contributo al portafoglio
contrib = []
for w in (0.2, 0.3, 0.5):
sl = Sleeve(name, w, lambda pb=pb: pb)
bt = StrategyPortfolio([tp01_sleeve(1 - w), sl]).backtest()
dF = bt["full"]["sharpe"] - base["full"]["sharpe"]
dH = bt["holdout"]["sharpe"] - base["holdout"]["sharpe"]
contrib.append((w, bt["full"]["sharpe"], dF, bt["holdout"]["sharpe"], dH))
print(f" +TP01 w{w:.0%}: FULL {bt['full']['sharpe']:.2f} ({dF:+.2f}) | HOLD {bt['holdout']['sharpe']:.2f} ({dH:+.2f})")
breadth_ok = pct >= 0.6 and consec <= 1
standalone_beats = f["sharpe"] > TP01_FULL_SH and h["sharpe"] > 0.25 and breadth_ok
# diversificatore: corr<0.5, migliora FULL E hold del portafoglio ad almeno un peso, breadth ok
improves = any(dF > 0.05 and dH > 0.0 for _, _, dF, _, dH in contrib)
diversifier = (not np.isnan(corr) and corr < 0.5) and improves and breadth_ok
verdict = "WINNER-standalone" if standalone_beats else ("WINNER-diversifier" if diversifier else "no")
print(f" -> {verdict} (breadth_ok={breadth_ok}, standalone_beats={standalone_beats}, diversifier={diversifier})")
if verdict.startswith("WINNER"):
winners.append((name, tf, verdict))
print("\n" + "=" * 100)
print(f" WINNERS: {len(winners)}")
for n, tf, v in winners:
print(f" {n} ({tf}): {v}")
if not winners:
print(" nessuno batte TP01 con criterio onesto -> serve un'altra ondata.")
if __name__ == "__main__":
main()
+108
View File
@@ -0,0 +1,108 @@
"""AFFINAMENTO XS01 — blend di LOOKBACK (multi-orizzonte cross-sectional).
XS01 attuale usa un singolo lookback (L=30). Come TP01 fonde gli orizzonti 30/90/180, qui il
momentum cross-sectional fonde piu' lookback: per ogni ribilancio, z-score cross-sectional del
rendimento a ciascun L, MEDIATO -> punteggio blended -> long top-k / short bottom-k. Piu' liscio
e robusto (meno dipendente da un singolo orizzonte/regime). Causale, netto fee, vol-target.
Confronto vs singolo-L + contributo al portafoglio TP01+XS01.
uv run python scripts/portfolio/xsec_blend.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, pandas as pd
from src.portfolio.portfolio import to_daily, metrics, HOLDOUT, Sleeve, StrategyPortfolio
from src.portfolio.sleeves import tp01_sleeve, XS_UNIVERSE
RAW = PROJECT_ROOT / "data" / "raw"
FEE = 0.001
def load_majors():
cols = {}
for sym in XS_UNIVERSE:
p = RAW / f"hl_{sym.lower()}_1d.parquet"
if p.exists():
d = pd.read_parquet(p)
cols[sym] = pd.Series(d["close"].values.astype(float), index=pd.to_datetime(d["timestamp"], unit="ms", utc=True))
return pd.concat(cols, axis=1, join="inner").sort_index().dropna()
def xs_signal(C, lookbacks, H=10, k=5, mode="mom", tv=0.20):
"""lookbacks = lista (blend) o singolo [L]. Score = media z-score cross-sectional dei ret_L."""
px = C.values; n, A = px.shape
dret = np.vstack([np.zeros(A), px[1:] / px[:-1] - 1.0])
W = np.zeros((n, A)); w = np.zeros(A)
for i in range(n):
if i >= max(lookbacks) and i % H == 0:
score = np.zeros(A); cnt = 0
for L in lookbacks:
rL = px[i] / px[i - L] - 1.0
sd = rL.std()
if sd > 0:
score += (rL - rL.mean()) / sd; cnt += 1
if cnt:
score /= cnt
order = np.argsort(score)
w = np.zeros(A); lo, hi = order[:k], order[-k:]
if mode == "mom": w[hi] = 0.5 / k; w[lo] = -0.5 / k
else: w[lo] = 0.5 / k; w[hi] = -0.5 / k
W[i] = w
gross = np.zeros(n); gross[1:] = np.sum(W[:-1] * dret[1:], axis=1)
turn = np.zeros(n); turn[0] = np.abs(W[0]).sum(); turn[1:] = np.abs(np.diff(W, axis=0)).sum(axis=1)
s = pd.Series(gross - turn * (FEE / 2.0), index=C.index)
rv = s.rolling(30, min_periods=15).std().shift(1) * np.sqrt(365.25)
scale = np.clip(np.nan_to_num(tv / rv.replace(0, np.nan).values, nan=0.0), 0, 3.0)
return to_daily(pd.Series(s.values * scale, index=C.index))
def ev(C, lbs, tp):
d = xs_signal(C, lbs)
f = metrics(d); o = metrics(d[d.index >= HOLDOUT])
yr = [float((1 + g).prod() - 1) for _, g in d.groupby(d.index.year)]
pct = sum(v > 0 for v in yr) / len(yr) if yr else 0
corr = float(pd.concat({"a": tp, "b": d}, axis=1, join="inner").dropna().corr().iloc[0, 1])
return d, f, o, pct, corr
def main():
C = load_majors()
tp = tp01_sleeve().daily()
print("=" * 92)
print(f" AFFINAMENTO XS01 — blend di lookback (19 major, {len(C)} giorni)")
print("=" * 92)
print(f" {'lookbacks':<22}{'FULL':>7}{'OOS25':>7}{'DD%':>6}{'anni+':>7}{'corrTP':>8}")
configs = [
("[30] (attuale)", [30]), ("[90]", [90]), ("[20]", [20]),
("[20,40]", [20, 40]), ("[20,60]", [20, 60]), ("[30,90]", [30, 90]),
("[20,40,90]", [20, 40, 90]), ("[30,60,120]", [30, 60, 120]),
("[20,60,180]", [20, 60, 180]), ("[15,30,60,120]", [15, 30, 60, 120]),
]
rows = []
for name, lbs in configs:
d, f, o, pct, corr = ev(C, lbs, tp)
rows.append((name, lbs, d, f, o, pct, corr))
print(f" {name:<22}{f['sharpe']:>7.2f}{o['sharpe']:>7.2f}{f['maxdd']*100:>6.0f}{pct*100:>6.0f}%{corr:>+8.2f}")
# candidato: miglior blend per (FULL+OOS) con breadth 100% e corr bassa
cand = [r for r in rows if r[5] >= 0.99 and r[6] < 0.4]
cand.sort(key=lambda r: -(r[3]["sharpe"] + r[4]["sharpe"]))
print("\n CONTRIBUTO al portafoglio — attuale (XS [30]) vs miglior blend")
base_xs = rows[0][2] # [30]
for label, dxs in [("XS [30] attuale", base_xs)] + ([(cand[0][0], cand[0][2])] if cand else []):
J = pd.concat({"tp": tp, "xs": dxs}, axis=1, join="inner").dropna()
for w in (0.3,):
comb = (1 - w) * J["tp"] + w * J["xs"]
cf, ch = metrics(comb), metrics(comb[comb.index >= HOLDOUT])
xf = metrics(J["xs"]); xo = metrics(J["xs"][J["xs"].index >= HOLDOUT])
print(f" {label:<22} XS-solo FULL {xf['sharpe']:.2f}/OOS {xo['sharpe']:.2f} | TP01 70+XS 30: FULL {cf['sharpe']:.2f} HOLD {ch['sharpe']:.2f}")
if cand:
print(f"\n -> blend migliore: {cand[0][0]} (lookbacks {cand[0][1]}). Promuovere se batte [30] su")
print(" FULL+OOS+robustezza E migliora il portafoglio. Sennò resta [30].")
if __name__ == "__main__":
main()
+101
View File
@@ -0,0 +1,101 @@
"""AFFINAMENTO XS01 — GATE DI DISPERSIONE.
Il momentum cross-sectional vive nella DISPERSIONE (winners/losers distanti). In regime compatto
(tutti gli asset si muovono insieme) non ha segnale -> churn/rumore. Gate: entra SOLO se la
dispersione cross-section del momentum supera una soglia CAUSALE (percentile espandente della
dispersione passata); altrimenti flat. Sul blend [30,90] dei 19 major. Sweep soglia + contributo.
uv run python scripts/portfolio/xsec_dispgate.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, pandas as pd
from src.portfolio.portfolio import to_daily, metrics, HOLDOUT
from src.portfolio.sleeves import tp01_sleeve, XS_UNIVERSE
RAW = PROJECT_ROOT / "data" / "raw"
FEE = 0.001
LOOKBACKS = (30, 90); H = 10; K = 5; TV = 0.20
def load_majors():
cols = {}
for sym in XS_UNIVERSE:
p = RAW / f"hl_{sym.lower()}_1d.parquet"
if p.exists():
d = pd.read_parquet(p)
cols[sym] = pd.Series(d["close"].values.astype(float), index=pd.to_datetime(d["timestamp"], unit="ms", utc=True))
return pd.concat(cols, axis=1, join="inner").sort_index().dropna()
def xs_gated(C, disp_pct=0, min_hist=20):
px = C.values; n, A = px.shape
dret = np.vstack([np.zeros(A), px[1:] / px[:-1] - 1.0])
mlb = max(LOOKBACKS)
# dispersione del momentum a ogni barra: media (su lookback) della std cross-section di ret_L
disp = np.full(n, np.nan)
for i in range(mlb, n):
acc = 0.0; c = 0
for L in LOOKBACKS:
acc += (px[i] / px[i - L] - 1.0).std(); c += 1
disp[i] = acc / c
W = np.zeros((n, A)); w = np.zeros(A)
hist = []
gated_flat = 0; total = 0
for i in range(n):
if i >= mlb and i % H == 0:
thr = np.percentile(hist, disp_pct) if (disp_pct > 0 and len(hist) >= min_hist) else -np.inf
total += 1
if disp[i] >= thr:
score = np.zeros(A)
for L in LOOKBACKS:
rL = px[i] / px[i - L] - 1.0; sd = rL.std()
if sd > 0:
score += (rL - rL.mean()) / sd
order = np.argsort(score); w = np.zeros(A); lo, hi = order[:K], order[-K:]
w[hi] = 0.5 / K; w[lo] = -0.5 / K
else:
w = np.zeros(A); gated_flat += 1
hist.append(disp[i])
W[i] = w
gross = np.zeros(n); gross[1:] = np.sum(W[:-1] * dret[1:], axis=1)
turn = np.zeros(n); turn[0] = np.abs(W[0]).sum(); turn[1:] = np.abs(np.diff(W, axis=0)).sum(axis=1)
s = pd.Series(gross - turn * (FEE / 2.0), index=C.index)
rv = s.rolling(30, min_periods=15).std().shift(1) * np.sqrt(365.25)
scale = np.clip(np.nan_to_num(TV / rv.replace(0, np.nan).values, nan=0.0), 0, 3.0)
return to_daily(pd.Series(s.values * scale, index=C.index)), (gated_flat / total if total else 0)
def main():
C = load_majors(); tp = tp01_sleeve().daily()
print("=" * 92)
print(f" AFFINAMENTO XS01 — gate di dispersione (blend [30,90], 19 major, {len(C)}g)")
print("=" * 92)
print(f" {'soglia pctile':<16}{'FULL':>7}{'OOS25':>7}{'DD%':>6}{'anni+':>7}{'corrTP':>8}{'%flat':>8}")
res = {}
for p in (0, 30, 40, 50, 60, 70):
d, flat = xs_gated(C, p)
f = metrics(d); o = metrics(d[d.index >= HOLDOUT])
yr = [float((1 + g).prod() - 1) for _, g in d.groupby(d.index.year)]
pct = sum(v > 0 for v in yr) / len(yr) if yr else 0
corr = float(pd.concat({"a": tp, "b": d}, axis=1, join="inner").dropna().corr().iloc[0, 1])
res[p] = (d, f, o, pct, corr)
lab = "0 (no gate)" if p == 0 else f"p{p}"
print(f" {lab:<16}{f['sharpe']:>7.2f}{o['sharpe']:>7.2f}{f['maxdd']*100:>6.0f}{pct*100:>6.0f}%{corr:>+8.2f}{flat*100:>7.0f}%")
print("\n CONTRIBUTO al portafoglio (TP01 70 + XS 30, finestra comune):")
for p in (0, 40, 50, 60):
d = res[p][0]
J = pd.concat({"tp": tp, "xs": d}, axis=1, join="inner").dropna()
comb = 0.7 * J["tp"] + 0.3 * J["xs"]
cf, ch = metrics(comb), metrics(comb[comb.index >= HOLDOUT])
lab = "no gate (attuale)" if p == 0 else f"gate p{p}"
print(f" {lab:<18} FULL Sh {cf['sharpe']:.2f} DD {cf['maxdd']*100:.0f}% | HOLD Sh {ch['sharpe']:.2f}")
print("\n -> promuovere il gate se migliora Sharpe/DD/robustezza E il contributo. Sennò no-gate resta.")
if __name__ == "__main__":
main()
+133
View File
@@ -0,0 +1,133 @@
"""XS cross-sectional con UNIVERSO TOP-LIQUIDITÀ DINAMICO (Hyperliquid 52 certificati).
Invece di 19 nomi fissi, a ogni ribilancio: seleziona i top-N per liquidità (dollar-volume 30g
causale), poi fra quelli long i k più forti / short i k più deboli (momentum, market-neutral),
vol-target. Idea: cross-section pulita e ADATTIVA (i token entrano quando maturano in liquidità),
escludendo il long-tail rumoroso che diluiva il 52-all. Gestione ragged (asset a date diverse:
si classifica solo fra i disponibili). Causale. Confronto vs fisso-19 + 52-all + contributo TP01.
uv run python scripts/portfolio/xsec_dynuniverse.py
"""
from __future__ import annotations
import sys, glob
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.portfolio import to_daily, metrics, HOLDOUT
from src.portfolio.sleeves import tp01_sleeve, XS_UNIVERSE
RAW = PROJECT_ROOT / "data" / "raw"
FEE = 0.001
def load_close_vol():
close, vol = {}, {}
for p in sorted(glob.glob(str(RAW / "hl_*_1d.parquet"))):
sym = Path(p).stem.replace("hl_", "").replace("_1d", "").upper()
d = pd.read_parquet(p)
ix = pd.to_datetime(d["timestamp"], unit="ms", utc=True)
close[sym] = pd.Series(d["close"].values.astype(float), index=ix)
vol[sym] = pd.Series(d["volume"].values.astype(float), index=ix)
C = pd.concat(close, axis=1, join="outer").sort_index()
V = pd.concat(vol, axis=1, join="outer").sort_index().reindex(C.index)
return C, V
def xs_dynamic(C, V, N=20, lb=60, hold=10, k=5, mode="mom", tv=0.20, fixed=None):
"""fixed=lista simboli -> universo statico (ignora liquidità). Altrimenti top-N per liquidità."""
cols = list(C.columns); A = len(cols)
px = C.values; n = len(px)
dret = np.full((n, A), 0.0); dret[1:] = np.where(np.isfinite(px[1:]) & np.isfinite(px[:-1]), px[1:] / px[:-1] - 1.0, 0.0)
dvol = V.values * px
liq = pd.DataFrame(dvol, index=C.index, columns=cols).rolling(30, min_periods=15).mean().shift(1).values
fixed_mask = np.array([c in fixed for c in cols]) if fixed else None
W = np.zeros((n, A)); w = np.zeros(A)
for i in range(n):
if i >= lb and i % hold == 0:
retlb = np.where(np.isfinite(px[i]) & np.isfinite(px[i - lb]), px[i] / px[i - lb] - 1.0, np.nan)
avail = np.isfinite(retlb) & np.isfinite(px[i])
if fixed is not None:
avail &= fixed_mask
cand = np.where(avail)[0]
else:
avail &= np.isfinite(liq[i])
idx = np.where(avail)[0]
if len(idx) > N:
cand = idx[np.argsort(liq[i][idx])[-N:]] # top-N per liquidità
else:
cand = idx
w = np.zeros(A)
ke = min(k, len(cand) // 2)
if ke >= 1:
order = cand[np.argsort(retlb[cand])]
lo, hi = order[:ke], order[-ke:]
if mode == "mom": w[hi] = 0.5 / ke; w[lo] = -0.5 / ke
else: w[lo] = 0.5 / ke; w[hi] = -0.5 / ke
W[i] = w
gross = np.zeros(n); gross[1:] = np.sum(W[:-1] * dret[1:], axis=1)
turn = np.zeros(n); turn[0] = np.abs(W[0]).sum(); turn[1:] = np.abs(np.diff(W, axis=0)).sum(axis=1)
net = gross - turn * (FEE / 2.0)
s = pd.Series(net, index=C.index)
rv = s.rolling(30, min_periods=15).std().shift(1) * np.sqrt(365.25)
scale = np.clip(np.nan_to_num(tv / rv.replace(0, np.nan).values, nan=0.0), 0, 3.0)
return to_daily(pd.Series(s.values * scale, index=C.index))
def ev(d):
f = metrics(d); o = metrics(d[d.index >= HOLDOUT])
yr = [float((1 + g).prod() - 1) for _, g in d.groupby(d.index.year)]
pct = sum(v > 0 for v in yr) / len(yr) if yr else 0
return f, o, pct
def main():
C, V = load_close_vol()
print("=" * 96)
print(f" XS UNIVERSO TOP-LIQUIDITÀ DINAMICO — {len(C.columns)} asset certificati [{C.index[0].date()} -> {C.index[-1].date()}]")
print("=" * 96)
tp = tp01_sleeve().daily()
print("\n (1) SWEEP N (top-liquidità) x config (mom) — FULL Sh / OOS25 Sh / anni+ / corrTP")
print(f" {'config':<28}{'FULL':>7}{'OOS25':>7}{'anni+':>7}{'corrTP':>8}")
best = None
for N in (12, 15, 20, 25):
for lb, hold, k in [(30, 10, 5), (60, 10, 5), (90, 10, 5)]:
d = xs_dynamic(C, V, N=N, lb=lb, hold=hold, k=k)
f, o, pct = ev(d)
corr = float(pd.concat({"a": tp, "b": d}, axis=1, join="inner").dropna().corr().iloc[0, 1])
tag = f"top{N} L{lb}H{hold}k{k}"
print(f" {tag:<28}{f['sharpe']:>7.2f}{o['sharpe']:>7.2f}{pct*100:>6.0f}%{corr:>+8.2f}")
if (best is None or f['sharpe'] > best[1]['sharpe']) and corr < 0.4 and o['sharpe'] > 0:
best = (tag, f, o, corr, d, (N, lb, hold, k))
print("\n (2) BASELINE di confronto (stessa finestra):")
for name, kw in [("fisso-19 major (L30H10k5)", dict(lb=30, hold=10, k=5, fixed=set(XS_UNIVERSE))),
("fisso-19 major (L90H10k5)", dict(lb=90, hold=10, k=5, fixed=set(XS_UNIVERSE))),
("52-all (L60H10k5)", dict(lb=60, hold=10, k=5))]:
d = xs_dynamic(C, V, **kw); f, o, pct = ev(d)
print(f" {name:<28} FULL {f['sharpe']:.2f} OOS25 {o['sharpe']:.2f} anni+ {pct*100:.0f}%")
if best is None:
print("\n Nessuna config dinamica scorrelata+positiva. Il top-liquidità non aiuta.")
return
tag, f, o, corr, d, cfg = best
print(f"\n === MIGLIOR DINAMICO: {tag} | FULL {f['sharpe']:.2f} ret {f['ret']*100:+.0f}% DD {f['maxdd']*100:.0f}% | OOS25 {o['sharpe']:.2f} | corrTP {corr:+.2f} ===")
per = [(int(y), round(float((1 + g).prod() - 1), 3)) for y, g in d.groupby(d.index.year)]
print(f" per-anno: {per}")
# contributo al portafoglio vs fisso-19 (XS01 attuale)
xs19 = xs_dynamic(C, V, lb=30, hold=10, k=5, fixed=set(XS_UNIVERSE))
J = pd.concat({"tp": tp, "dyn": d, "x19": xs19}, axis=1, join="inner").dropna()
print(f"\n CONTRIBUTO (finestra comune {J.index[0].date()}->{J.index[-1].date()}):")
for nm, col in [("TP01 solo", None), ("TP01+XS19 (attuale) 70/30", "x19"), ("TP01+DYN 70/30", "dyn")]:
if col is None:
comb = J["tp"]
else:
comb = 0.7 * J["tp"] + 0.3 * J[col]
mf = metrics(comb); mh = metrics(comb[comb.index >= HOLDOUT])
print(f" {nm:<28} FULL Sh {mf['sharpe']:.2f} DD {mf['maxdd']*100:.0f}% | HOLD Sh {mh['sharpe']:.2f}")
print("\n -> DINAMICO meglio del fisso-19? guarda FULL/OOS + contributo. Sennò: fisso-19 resta.")
if __name__ == "__main__":
main()
+123
View File
@@ -0,0 +1,123 @@
"""CROSS-SECTIONAL su universo Hyperliquid certificato (19 alt, 1d, 2024-2026).
Strategia market-neutral: ogni H giorni classifica gli asset per rendimento a L giorni (causale),
va long i top-k / short i bottom-k (momentum) o viceversa (reversal), dollar-neutral, vol-target.
Mira a DIVERSIFICARE TP01 (long-trend): se scorrelata e robusta, migliora il portafoglio.
Gauntlet onesto: FULL (2024-26) + within-window OOS (2025+) + per-anno + corr TP01 + contributo.
Caveat: storia corta (~2.5 anni). Risultati suggestivi, non robusti come BTC/ETH 6 anni.
uv run python scripts/portfolio/xsec_research.py
"""
from __future__ import annotations
import sys, glob
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.portfolio import to_daily, metrics, HOLDOUT, Sleeve, StrategyPortfolio
from src.portfolio.sleeves import tp01_sleeve
RAW = PROJECT_ROOT / "data" / "raw"
FEE = 0.001
def load_universe():
cols = {}
for f in sorted(glob.glob(str(RAW / "hl_*_1d.parquet"))):
s = Path(f).stem.replace("hl_", "").replace("_1d", "").upper()
d = pd.read_parquet(f)
cols[s] = pd.Series(d["close"].values.astype(float), index=pd.to_datetime(d["timestamp"], unit="ms", utc=True))
C = pd.concat(cols, axis=1, join="inner").sort_index().dropna()
return C
def xs_book(C, L, H, k, mode="mom", target_vol=0.20):
"""Rendimenti netti giornalieri di un book cross-sectional market-neutral. Causale."""
assets = list(C.columns); A = len(assets)
px = C.values; n = len(px)
dret = np.vstack([np.zeros(A), px[1:] / px[:-1] - 1.0])
W = np.zeros((n, A)) # peso per asset per giorno (deciso a close[i], tenuto in i+1)
w = np.zeros(A)
for i in range(n):
if i >= L and i % H == 0:
lb = px[i] / px[i - L] - 1.0
order = np.argsort(lb)
w = np.zeros(A)
lo, hi = order[:k], order[-k:] # peggiori / migliori
if mode == "mom":
w[hi] = 0.5 / k; w[lo] = -0.5 / k # long forti / short deboli
else:
w[lo] = 0.5 / k; w[hi] = -0.5 / k # reversal
W[i] = w
# rendimento book: peso[i-1] guadagna dret[i]; fee su turnover
gross = np.zeros(n); gross[1:] = np.sum(W[:-1] * dret[1:], axis=1) # W[i-1] guadagna dret[i]
turn = np.zeros(n); turn[0] = np.abs(W[0]).sum()
turn[1:] = np.abs(np.diff(W, axis=0)).sum(axis=1) # turnover per (ri)settare W[i]
net = gross - turn * (FEE / 2.0)
s = pd.Series(net, index=C.index)
# vol-target (causale): scala per target/vol_realizzata(30) shiftata
rv = s.rolling(30, min_periods=15).std().shift(1) * np.sqrt(365.25)
scale = np.clip(np.nan_to_num(target_vol / rv.replace(0, np.nan).values, nan=0.0), 0, 3.0)
return pd.Series(s.values * scale, index=C.index)
def yr_breadth(daily):
pre = daily
yr = [float((1 + g).prod() - 1) for _, g in pre.groupby(pre.index.year)]
consec = mx = 0
for v in yr: consec = consec + 1 if v < 0 else 0; mx = max(mx, consec)
return yr, (sum(v > 0 for v in yr) / len(yr) if yr else 0), mx
def main():
C = load_universe()
print("=" * 96)
print(f" CROSS-SECTIONAL Hyperliquid — {len(C.columns)} asset, {len(C)} giorni [{C.index[0].date()} -> {C.index[-1].date()}]")
print("=" * 96)
tp = tp01_sleeve(1.0); tp_daily = tp.daily()
base = StrategyPortfolio([tp01_sleeve(1.0)]).backtest()
print(f"\n {'config':<24}{'FULL Sh':>9}{'OOS25 Sh':>10}{'ret%':>8}{'DD%':>7}{'corrTP':>8}{'anni+':>7}")
cands = []
grid = [("mom",L,H,k) for L in (30,60,90) for H in (5,10,20) for k in (3,5)] \
+ [("rev",L,H,k) for L in (3,7,14) for H in (3,5) for k in (3,5)]
for mode,L,H,k in grid:
d = to_daily(xs_book(C,L,H,k,mode))
f=metrics(d); oos=metrics(d[d.index>=HOLDOUT])
J=pd.concat({"tp":tp_daily,"x":d},axis=1,join="inner").dropna(); corr=float(J["tp"].corr(J["x"])) if len(J)>5 else float("nan")
yr,pct,consec=yr_breadth(d)
tag=f"{mode} L{L} H{H} k{k}"
cands.append((tag,mode,L,H,k,f,oos,corr,pct,consec,d))
if f["sharpe"]>0.6 or oos["sharpe"]>0.8:
print(f" {tag:<24}{f['sharpe']:>9.2f}{oos['sharpe']:>10.2f}{f['ret']*100:>+8.0f}{f['maxdd']*100:>7.1f}{corr:>+8.2f}{pct*100:>6.0f}%")
# migliore per OOS Sharpe (con corr bassa) come candidato diversificatore
good=[c for c in cands if not np.isnan(c[7]) and abs(c[7])<0.4 and c[5]["sharpe"]>0.5 and c[6]["sharpe"]>0]
good.sort(key=lambda c:-(c[6]["sharpe"]))
print(f"\n Candidati scorrelati(<0.4) e positivi (FULL>0.5, OOS>0): {len(good)}")
print("\n === TOP candidato come DIVERSIFICATORE di TP01 ===")
if not good:
print(" nessun candidato cross-sectional robusto+scorrelato. Universo corto.")
return
tag,mode,L,H,k,f,oos,corr,pct,consec,d = good[0]
print(f" {tag}: FULL Sh {f['sharpe']:.2f} ret {f['ret']*100:+.0f}% DD {f['maxdd']*100:.1f}% | OOS25 Sh {oos['sharpe']:.2f} | corr TP01 {corr:+.2f} | anni+ {pct*100:.0f}% rossi-consec {consec}")
per=[(y,round(v,3)) for y,(v) in zip([yy for yy,_ in d.groupby(d.index.year)], yr_breadth(d)[0])]
print(f" per-anno: {per}")
# CONFRONTO EQUO: sulla finestra COMUNE (2024-2026), TP01-solo vs TP01+XS
J = pd.concat({"tp": tp_daily, "xs": d}, axis=1, join="inner").dropna()
tpw, xsw = J["tp"], J["xs"]
bw_f = metrics(tpw); bw_h = metrics(tpw[tpw.index >= HOLDOUT])
print(f"\n [finestra comune {J.index[0].date()}->{J.index[-1].date()}]")
print(f" TP01 SOLO (su finestra comune): FULL Sh {bw_f['sharpe']:.2f} DD {bw_f['maxdd']*100:.1f}% | HOLD Sh {bw_h['sharpe']:.2f}")
for w in (0.2, 0.3, 0.5):
comb = (1 - w) * tpw + w * xsw
cf = metrics(comb); ch = metrics(comb[comb.index >= HOLDOUT])
print(f" +XS w{w:.0%}: FULL {cf['sharpe']:.2f} ({cf['sharpe']-bw_f['sharpe']:+.2f}) DD {cf['maxdd']*100:.1f}%"
f" | HOLD {ch['sharpe']:.2f} ({ch['sharpe']-bw_h['sharpe']:+.2f})")
print("\n WINNER-diversifier se: corr bassa, e TP01+XS batte TP01-solo (FULL E HOLD) sulla finestra comune,")
print(" con breadth per-anno ok. Altrimenti no (e attenzione: storia XS solo ~2.5 anni).")
if __name__=="__main__":
main()
+814
View File
@@ -0,0 +1,814 @@
"""altlib — SHARED HONEST EVALUATION LIBRARY for the alt-strategy fan-out (2026-06-20).
Built for the "studia altre strategie alternative su Deribit" research wave: >=100 agents,
each studying ONE distinct strategy hypothesis on the certified BTC/ETH (+ DVOL) universe.
Every agent imports THIS module so that:
* NO look-ahead is structurally possible: a target/weight decided at close[i] is held
during bar i+1 (the evaluator shifts it for you you never multiply by r[i] with a
weight that used close[i] for the *same* bar).
* Fees are realistic Deribit (0.10% RT taker = 0.0005/side) and a fee SWEEP is built in.
* Metrics are comparable: FULL Sharpe/CAGR/maxDD, HOLD-OUT (2025-01-01+), per-year.
* Only certified data exists (BTC/ETH from Deribit mainnet, DVOL from Deribit). load()
raises on anything else a physical guardrail.
Two evaluation styles:
1. eval_weights(df, target) -> for CONTINUOUS-position strategies (trend, vol overlays,
pairs, risk-parity). `target` is a per-bar position (fraction of equity, sign=dir),
decided with data <= close[i]. VECTORIZED (numpy) -> fast even on 68k 1h bars.
2. eval_signals(df, entries) -> for DISCRETE entry/exit strategies (breakout w/ TP-SL,
mean-reversion bounce). Wraps the project's trade-based harness. Use on 1h/1d only
(the Python loop is O(n*max_bars); 5m has 840k bars -> too slow on 2 CPUs).
Quick start (inside an agent script):
import sys; sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt")
import altlib as al
rep = al.study_weights("MY-STRAT", lambda df: my_target(df), tfs=("1d","12h"))
print(al.fmt(rep)); print(al.as_json(rep)) # human + machine
"""
from __future__ import annotations
import inspect
import json
import sys
from functools import lru_cache
from pathlib import Path
import numpy as np
import pandas as pd
# --- make `from src...` work no matter where the agent's script lives -------
_ROOT = Path(__file__).resolve().parents[3]
if str(_ROOT) not in sys.path:
sys.path.insert(0, str(_ROOT))
from src.backtest.harness import backtest_signals, load # noqa: E402
from src.strategies.trend_portfolio import resample_tf # noqa: E402
HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC")
FEE_SIDE = 0.0005 # 0.05%/side = 0.10% round-trip (Deribit taker)
FEE_SWEEP = (0.0, 0.0005, 0.001, 0.0015) # per-side fee grid for robustness
CERTIFIED = ("BTC", "ETH")
DATA_DIR = _ROOT / "data" / "raw"
# ===========================================================================
# DATA (cached) — 1h base, resampled to >=4h; DVOL aligned causally.
# ===========================================================================
@lru_cache(maxsize=32)
def get(asset: str, tf: str) -> pd.DataFrame:
"""Certified OHLCV with a tz-aware 'datetime' col and RangeIndex.
tf in {5m,15m,1h} loaded directly; {4h,6h,8h,12h,1d,2d,3d,1w} resampled from 1h.
Resample uses the leak-free per-single-TF path (trend_portfolio.resample_tf)."""
asset = asset.upper()
if asset not in CERTIFIED:
raise ValueError(f"Asset non certificato: {asset}. Universo={CERTIFIED}.")
tf = tf.lower()
if tf in ("5m", "15m", "1h"):
df = load(asset, tf)
else:
rule = {"4h": "4h", "6h": "6h", "8h": "8h", "12h": "12h",
"1d": "1D", "2d": "2D", "3d": "3D", "1w": "1W"}.get(tf)
if rule is None:
raise ValueError(f"TF non gestito: {tf}")
df = resample_tf(load(asset, "1h"), rule)
df = df.reset_index(drop=True)
if "datetime" not in df.columns:
df["datetime"] = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
return df
@lru_cache(maxsize=8)
def _dvol_raw(asset: str) -> pd.DataFrame:
p = DATA_DIR / f"dvol_{asset.lower()}.parquet"
if not p.exists():
raise FileNotFoundError(f"DVOL non trovato: {p}")
d = pd.read_parquet(p).sort_values("timestamp").reset_index(drop=True)
return d
def dvol(df: pd.DataFrame, asset: str) -> np.ndarray:
"""Deribit DVOL (implied vol index) aligned CAUSALLY to df's bars.
For each bar we take the most recent DVOL value timestamped at/before the bar's
open (merge_asof backward) -> known by decision time. NaN before DVOL history
(DVOL starts 2021-03). Returns float array len(df), in vol POINTS (e.g. 65.0)."""
d = _dvol_raw(asset)
left = pd.DataFrame({"timestamp": df["timestamp"].astype("int64").values})
merged = pd.merge_asof(left, d.rename(columns={"close": "dvol"}),
on="timestamp", direction="backward")
return merged["dvol"].values.astype(float)
# ===========================================================================
# INDICATORS (all causal: value at i uses data <= i)
# ===========================================================================
def simple_returns(c: np.ndarray) -> np.ndarray:
r = np.zeros(len(c)); r[1:] = c[1:] / c[:-1] - 1.0
return r
def log_returns(c: np.ndarray) -> np.ndarray:
r = np.zeros(len(c)); r[1:] = np.log(c[1:] / c[:-1])
return r
def ema(x: np.ndarray, span: int) -> np.ndarray:
return pd.Series(x).ewm(span=span, adjust=False).mean().values
def sma(x: np.ndarray, win: int) -> np.ndarray:
return pd.Series(x).rolling(win, min_periods=win).mean().values
def rolling_std(x: np.ndarray, win: int) -> np.ndarray:
return pd.Series(x).rolling(win, min_periods=max(2, win // 2)).std().values
def zscore(x: np.ndarray, win: int) -> np.ndarray:
s = pd.Series(x)
m = s.rolling(win, min_periods=win).mean()
sd = s.rolling(win, min_periods=win).std()
return ((s - m) / sd.replace(0, np.nan)).values
def rsi(c: np.ndarray, win: int = 14) -> np.ndarray:
d = np.diff(c, prepend=c[0])
up = pd.Series(np.where(d > 0, d, 0.0)).ewm(alpha=1 / win, adjust=False).mean()
dn = pd.Series(np.where(d < 0, -d, 0.0)).ewm(alpha=1 / win, adjust=False).mean()
rs = up / dn.replace(0, np.nan)
return (100 - 100 / (1 + rs)).values
def atr(df: pd.DataFrame, win: int = 14) -> np.ndarray:
h, l, c = df["high"].values, df["low"].values, df["close"].values
pc = np.roll(c, 1); pc[0] = c[0]
tr = np.maximum(h - l, np.maximum(np.abs(h - pc), np.abs(l - pc)))
return pd.Series(tr).ewm(alpha=1 / win, adjust=False).mean().values
def realized_vol(r: np.ndarray, win: int, bars_per_year: float) -> np.ndarray:
"""Annualized realized vol from returns up to i inclusive (no leakage)."""
return pd.Series(r).rolling(win, min_periods=max(2, win // 2)).std().values * np.sqrt(bars_per_year)
def donchian(df: pd.DataFrame, win: int):
"""Upper/lower channel using bars STRICTLY before i (shifted) -> a close[i] that
breaks the prior `win`-bar high is a real, tradeable breakout at close[i]."""
hi = pd.Series(df["high"].values).rolling(win, min_periods=win).max().shift(1).values
lo = pd.Series(df["low"].values).rolling(win, min_periods=win).min().shift(1).values
return hi, lo
def bbands(c: np.ndarray, win: int = 20, k: float = 2.0):
m = pd.Series(c).rolling(win, min_periods=win).mean()
sd = pd.Series(c).rolling(win, min_periods=win).std()
return (m + k * sd).values, m.values, (m - k * sd).values
def _call_target(fn, df: pd.DataFrame, asset: str):
"""Call a strategy fn as fn(df, asset) when it accepts 2 args, else fn(df).
Lets DVOL/cross-asset strategies receive the asset cleanly (no inference hacks)."""
try:
n = len(inspect.signature(fn).parameters)
except (ValueError, TypeError):
n = 1
return fn(df, asset) if n >= 2 else fn(df)
def bars_per_year(df: pd.DataFrame) -> float:
dt = pd.to_datetime(df["datetime"]).diff().dt.total_seconds().median()
return 86400 * 365.25 / dt if dt and dt > 0 else 365.25
def bars_per_day(df: pd.DataFrame) -> int:
dt = pd.to_datetime(df["datetime"]).diff().dt.total_seconds().median()
return max(1, round(86400 / dt))
def vol_target(target_dir: np.ndarray, df: pd.DataFrame, target_vol: float = 0.20,
vol_win_days: int = 30, leverage_cap: float = 2.0) -> np.ndarray:
"""Scale a direction array in [-1,1] to a vol-targeted position (TP01-style).
Causal: uses realized vol up to i. Returns position clipped to +/-leverage_cap."""
c = df["close"].values.astype(float)
bpd = bars_per_day(df)
bpy = bpd * 365.25
vol = realized_vol(simple_returns(c), max(2, vol_win_days * bpd), bpy)
scal = np.where((vol > 0) & np.isfinite(vol), target_vol / vol, 0.0)
tgt = np.clip(target_dir * scal, -leverage_cap, leverage_cap)
tgt[~np.isfinite(tgt)] = 0.0
return tgt
# ===========================================================================
# METRICS
# ===========================================================================
def _metrics_from_net(net: np.ndarray, idx: pd.DatetimeIndex) -> dict:
net = np.nan_to_num(net, nan=0.0)
eq = np.cumprod(1.0 + np.clip(net, -0.99, None))
rr = net[np.isfinite(net)]
bpy = 86400 * 365.25 / (pd.Series(idx).diff().dt.total_seconds().median() or 86400)
sharpe = float(np.mean(rr) / np.std(rr) * np.sqrt(bpy)) if np.std(rr) > 0 else 0.0
pk = np.maximum.accumulate(eq)
dd = float(np.max((pk - eq) / pk)) if len(eq) else 0.0
span_days = (idx[-1] - idx[0]).total_seconds() / 86400 if len(idx) > 1 else 1.0
years = max(span_days / 365.25, 1e-6)
total = eq[-1] / eq[0] if len(eq) else 1.0
cagr = total ** (1 / years) - 1 if total > 0 else -1.0
return dict(sharpe=round(sharpe, 3), cagr=round(cagr, 4), maxdd=round(dd, 4),
ret=round(total - 1, 4), n=int(len(rr)))
def _yearly(net: np.ndarray, idx: pd.DatetimeIndex) -> dict:
s = pd.Series(np.nan_to_num(net), index=idx)
out = {}
for y, g in s.groupby(s.index.year):
eq = np.cumprod(1 + g.values); pk = np.maximum.accumulate(eq)
out[int(y)] = dict(ret=round(float(eq[-1] - 1), 4),
dd=round(float(np.max((pk - eq) / pk)), 4))
return out
def eval_weights(df: pd.DataFrame, target: np.ndarray, fee_side: float = FEE_SIDE) -> dict:
"""Honest backtest of a CONTINUOUS position series.
target[i] is decided with data <= close[i]; it is HELD during bar i+1. The shift
is done HERE -> you cannot leak by construction. Fee charged on |Δposition| turnover.
Returns {full, holdout, yearly, time_in_market, turnover_per_year, net, idx}."""
c = df["close"].values.astype(float)
target = np.asarray(target, float)
target = np.nan_to_num(target, nan=0.0)
r = simple_returns(c)
pos = np.zeros(len(target)); pos[1:] = target[:-1] # held during bar t = decided at t-1
gross = pos * r
turn = np.abs(np.diff(pos, prepend=0.0))
net = gross - fee_side * turn
net[0] = 0.0
idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True))
full = _metrics_from_net(net, idx)
hmask = idx >= HOLDOUT
hold = _metrics_from_net(net[hmask], idx[hmask]) if hmask.sum() > 3 else dict(sharpe=0.0, n=0)
bpy_d = bars_per_day(df) * 365.25
return dict(full=full, holdout=hold, yearly=_yearly(net, idx),
time_in_market=round(float(np.mean(pos != 0)), 3),
turnover_per_year=round(float(turn.sum() / (len(turn) / bpy_d)), 1),
net=net, idx=idx)
def eval_signals(df: pd.DataFrame, entries: list, fee_rt: float = 2 * FEE_SIDE,
leverage: float = 1.0, asset: str = "", tf: str = "") -> dict:
"""Honest backtest of DISCRETE entry/exit signals (TP/SL/max_bars). Wraps the
project trade-based harness, adds a standardized hold-out split. Use on 1h/1d."""
m = backtest_signals(df, entries, fee_rt=fee_rt, leverage=leverage, asset=asset, tf=tf)
idx = pd.DatetimeIndex(pd.to_datetime(m.eq_index, utc=True)) if m.eq_index is not None \
else pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True))
eq = m.equity
hmask = idx >= HOLDOUT
hold = dict(sharpe=0.0, ret=0.0, n=0)
if hmask.sum() > 3:
he = eq[hmask]
hr = np.diff(he) / he[:-1]
bpy = m.bars_per_year or 365.0
hsharpe = float(np.mean(hr) / np.std(hr) * np.sqrt(bpy)) if len(hr) and np.std(hr) > 0 else 0.0
hold = dict(sharpe=round(hsharpe, 3), ret=round(float(he[-1] / he[0] - 1), 4), n=int(hmask.sum()))
full = dict(sharpe=round(m.sharpe, 3), cagr=round(m.cagr, 4), maxdd=round(m.max_dd, 4),
ret=round(m.net_return, 4), n=int(m.n_trades))
return dict(full=full, holdout=hold, n_trades=int(m.n_trades),
win_rate=round(m.win_rate, 1), time_in_market=round(m.time_in_market, 3),
yearly={int(y): round(v, 4) for y, v in m.yearly.items()})
# ===========================================================================
# MARGINAL SCORING vs TP01 baseline — the lesson of the 2026-06-20 sweep.
#
# Absolute Sharpe is NOT enough to earn a portfolio slot: an overlay on TSMOM
# (DVOL gate, low-vol filter, kill-switch, ...) inherits TP01's trend Sharpe and
# scores ~1.0-1.3 while adding ZERO new return. The only question that matters for
# a NEW sleeve is: does it IMPROVE the existing TP01 portfolio out-of-sample?
# We answer it three ways: (a) blend uplift (Sharpe of TP01 + w*candidate minus
# TP01 alone, full & hold-out), (b) correlation to TP01, (c) residual alpha after
# removing the TP01 beta (the part of the candidate orthogonal to trend).
# ===========================================================================
def _sh(s) -> float:
r = np.asarray(s.dropna().values, float)
return float(np.mean(r) / np.std(r) * np.sqrt(365.25)) if len(r) > 2 and np.std(r) > 0 else 0.0
def _dd_ret(s) -> float:
eq = np.cumprod(1.0 + np.asarray(s.dropna().values, float))
pk = np.maximum.accumulate(eq)
return float(np.max((pk - eq) / pk)) if len(eq) else 0.0
def _to_daily(s: pd.Series) -> pd.Series:
s = s.dropna().sort_index()
if not isinstance(s.index, pd.DatetimeIndex):
s.index = pd.to_datetime(s.index, utc=True)
if s.index.tz is None:
s.index = s.index.tz_localize("UTC")
return ((1.0 + s).resample("1D").prod() - 1.0).dropna()
@lru_cache(maxsize=2)
def tp01_baseline_daily() -> pd.Series:
"""The reference a candidate must BEAT: TP01 CANONICAL, 50/50 BTC+ETH, net daily
returns on common dates (full Sharpe ~1.30, hold-out ~0.31). Cached."""
from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio
tp = TrendPortfolio(**CANONICAL)
series = {}
for a in CERTIFIED:
df = get(a, "1d")
net, _ = tp.net_returns(df)
series[a] = pd.Series(net, index=pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)))
J = pd.concat(series, axis=1, join="inner").fillna(0.0)
return _to_daily(0.5 * J[CERTIFIED[0]] + 0.5 * J[CERTIFIED[1]])
def candidate_daily(target_fn, tf: str = "1d", fee_side: float = FEE_SIDE) -> pd.Series:
"""Build a candidate's 50/50 BTC+ETH net DAILY return series (same convention as
tp01_baseline_daily) from a continuous-position target_fn. Intraday TFs are
compounded to daily so they align with the TP01 baseline grid."""
series = {}
for a in CERTIFIED:
df = get(a, tf)
ev = eval_weights(df, _call_target(target_fn, df, a), fee_side=fee_side)
series[a] = pd.Series(ev["net"], index=ev["idx"])
J = pd.concat(series, axis=1, join="inner").fillna(0.0)
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:
"""Does this candidate IMPROVE the TP01 portfolio? Returns correlation, blend uplift
(full & hold-out, per weight), TP01-beta + residual alpha, and a verdict:
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
DILUTES -> drags the blend down
NEUTRAL -> changes little either way (a weak, optional satellite at best)
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()
J = pd.concat({"B": B, "C": cand_daily}, axis=1, join="inner").dropna()
if len(J) < 30:
return dict(marginal_verdict="N/A", reason="insufficient overlap with TP01 baseline")
if J["C"].std() == 0:
return dict(marginal_verdict="NEUTRAL", reason="flat/zero candidate (no exposure)",
corr_full=None, blends={"w25": dict(uplift_full=0.0, uplift_hold=0.0)})
JH = J[J.index >= HOLDOUT]
has_h = len(JH) > 5
out = {
"n_days": int(len(J)), "n_hold_days": int(len(JH)),
"corr_full": round(float(J["B"].corr(J["C"])), 3),
"corr_hold": round(float(JH["B"].corr(JH["C"])), 3) if has_h else None,
"tp01_full_sharpe": round(_sh(J["B"]), 3), "tp01_hold_sharpe": round(_sh(JH["B"]), 3) if has_h else None,
"cand_full_sharpe": round(_sh(J["C"]), 3), "cand_hold_sharpe": round(_sh(JH["C"]), 3) if has_h else None,
}
blends = {}
for w in weights:
bf, bh = (1 - w) * J["B"] + w * J["C"], (1 - w) * JH["B"] + w * JH["C"]
blends[f"w{int(w * 100)}"] = dict(
full=round(_sh(bf), 3), hold=round(_sh(bh), 3) if has_h else None,
uplift_full=round(_sh(bf) - _sh(J["B"]), 3),
uplift_hold=round(_sh(bh) - _sh(JH["B"]), 3) if has_h else None,
dd=round(_dd_ret(bf), 4))
out["blends"] = blends
b, c = J["B"].values, J["C"].values
beta = float(np.cov(c, b)[0, 1] / np.var(b)) if np.var(b) > 0 else 0.0
resid = c - beta * b
out["beta_to_tp01"] = round(beta, 3)
out["resid_sharpe_full"] = round(float(np.mean(resid) / np.std(resid) * np.sqrt(365.25)) if np.std(resid) > 0 else 0.0, 3)
out["alpha_ann"] = round(float(np.mean(resid) * 365.25), 4)
# OOS robustness — the marginal point-estimate can be fooled by ONE lucky month
# (e.g. KAMA: +0.06 uplift that flips to -0.03 when its best month is dropped). Require
# 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.
out["clean_year_uplift"] = out["jackknife_min_uplift"] = None
robust_h = False
if has_h:
def _u(sub):
return _uplift_series(sub["B"], sub["C"])
yrs = sorted(set(JH.index.year))
clean = JH[JH.index.year == yrs[0]]
cu = _u(clean) if len(clean) > 20 else None
months = sorted(set(zip(JH.index.year, JH.index.month)))
jk = (min(_u(JH[~((JH.index.year == y) & (JH.index.month == mo))]) for y, mo in months)
if len(months) > 1 else _u(JH))
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
robust_h = bool(cu is not None and cu > 0.02 and jk is not None and jk > 0.0)
# --- 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_f = blends["w25"]["uplift_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):
v = "REDUNDANT"
elif up_f <= -0.10 and (up_h is None or up_h <= 0.0):
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:
v = "NEUTRAL"
out["marginal_verdict"] = v
return out
def study_marginal(name: str, target_fn, tf: str = "1d", fee_side: float = FEE_SIDE) -> dict:
"""Score a continuous candidate BOTH ways: absolute (study_weights) AND marginal vs
TP01. The combined gate: a candidate earns a sleeve slot only if it is not FAIL on
absolute robustness AND marginal_verdict == 'ADDS'."""
absolute = study_weights(name, target_fn, tfs=(tf,))
marg = marginal_vs_tp01(candidate_daily(target_fn, tf=tf, fee_side=fee_side))
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"
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,
abs_grade=abs_grade, marginal_verdict=marg.get("marginal_verdict"),
earns_slot=earns_slot)
def fmt_marginal(rep: dict) -> str:
m = rep["marginal"]
bl = m.get("blends", {})
lines = [f"=== {rep['name']} | abs={rep['abs_grade']} marginal={rep['marginal_verdict']} "
f"EARNS_SLOT={rep['earns_slot']}"]
lines.append(f" corr->TP01 full {m.get('corr_full')} hold {m.get('corr_hold')} "
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')} "
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')} | "
f"cand full {m.get('cand_full_sharpe')}/hold {m.get('cand_hold_sharpe')}")
for w, d in bl.items():
uh = "n/a" if d["uplift_hold"] is None else f"{d['uplift_hold']:+.3f}"
hold = "n/a" if d["hold"] is None else f"{d['hold']}"
lines.append(f" blend {w}: full {d['full']} (uplift {d['uplift_full']:+.3f}) "
f"hold {hold} (uplift {uh}) DD {d['dd'] * 100:.1f}%")
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))
# ===========================================================================
# DRIVERS — run a hypothesis across both assets, several TFs, with a fee sweep.
# ===========================================================================
def _verdict(per_cell: list[dict]) -> dict:
"""A finding PASSES only if it is robust: positive FULL Sharpe AND positive HOLD-OUT
on BOTH assets in its best TF, survives the fee sweep, and isn't a 1-cell fluke."""
if not per_cell:
return dict(grade="FAIL", reason="no cells")
ok = [c for c in per_cell if c.get("full_sharpe", -9) > 0]
best = max(per_cell, key=lambda c: c.get("min_asset_holdout_sharpe", -9))
pass_ = (best.get("min_asset_full_sharpe", -9) >= 0.5 and
best.get("min_asset_holdout_sharpe", -9) >= 0.2 and
best.get("fee_survives", False))
weak = (best.get("min_asset_full_sharpe", -9) >= 0.3 and
best.get("min_asset_holdout_sharpe", -9) >= 0.0)
grade = "PASS" if pass_ else ("WEAK" if weak else "FAIL")
return dict(grade=grade, best_tf=best.get("tf"),
best_full_sharpe=best.get("min_asset_full_sharpe"),
best_holdout_sharpe=best.get("min_asset_holdout_sharpe"),
n_positive_cells=len(ok), n_cells=len(per_cell))
def study_weights(name: str, target_fn, tfs=("1d", "12h"),
assets=CERTIFIED, fee_sweep=FEE_SWEEP) -> dict:
"""Run a CONTINUOUS-position hypothesis on each (asset,tf), report robustness.
target_fn(df) -> per-bar position array (decided <= close[i]). Returns a dict
ready to print/serialize, with a conservative PASS/WEAK/FAIL verdict."""
cells = []
for tf in tfs:
per_asset = {}
fee_ok_all = True
for a in assets:
df = get(a, tf)
tgt = _call_target(target_fn, df, a)
base = eval_weights(df, tgt, fee_side=FEE_SIDE)
sweep = {f"{2*f*100:.2f}%RT": eval_weights(df, tgt, fee_side=f)["full"]["sharpe"]
for f in fee_sweep}
fee_ok = sweep.get("0.20%RT", -9) > 0
fee_ok_all = fee_ok_all and fee_ok
per_asset[a] = dict(full=base["full"], holdout=base["holdout"],
tim=base["time_in_market"], turnover=base["turnover_per_year"],
fee_sweep=sweep, yearly=base["yearly"])
min_full = min(per_asset[a]["full"]["sharpe"] for a in assets)
min_hold = min(per_asset[a]["holdout"].get("sharpe", 0.0) for a in assets)
cells.append(dict(tf=tf, per_asset=per_asset,
min_asset_full_sharpe=round(min_full, 3),
min_asset_holdout_sharpe=round(min_hold, 3),
full_sharpe=round(np.mean([per_asset[a]["full"]["sharpe"] for a in assets]), 3),
fee_survives=fee_ok_all))
return dict(name=name, kind="weights", cells=cells, verdict=_verdict(cells))
def study_signals(name: str, entries_fn, tfs=("1d",), assets=CERTIFIED,
fee_sweep=FEE_SWEEP, leverage: float = 1.0) -> dict:
"""Run a DISCRETE entry/exit hypothesis on each (asset,tf). entries_fn(df) ->
list[dict|None] len(df). Use 1h/1d TFs only (Python loop)."""
cells = []
for tf in tfs:
per_asset = {}
fee_ok_all = True
for a in assets:
df = get(a, tf)
ent = _call_target(entries_fn, df, a)
base = eval_signals(df, ent, fee_rt=2 * FEE_SIDE, leverage=leverage, asset=a, tf=tf)
sweep = {f"{2*f*100:.2f}%RT": eval_signals(df, ent, fee_rt=2 * f, leverage=leverage)["full"]["sharpe"]
for f in fee_sweep}
fee_ok = sweep.get("0.20%RT", -9) > 0
fee_ok_all = fee_ok_all and fee_ok
per_asset[a] = dict(full=base["full"], holdout=base["holdout"],
n_trades=base["n_trades"], win_rate=base["win_rate"],
fee_sweep=sweep, yearly=base["yearly"])
min_full = min(per_asset[a]["full"]["sharpe"] for a in assets)
min_hold = min(per_asset[a]["holdout"].get("sharpe", 0.0) for a in assets)
cells.append(dict(tf=tf, per_asset=per_asset,
min_asset_full_sharpe=round(min_full, 3),
min_asset_holdout_sharpe=round(min_hold, 3),
full_sharpe=round(np.mean([per_asset[a]["full"]["sharpe"] for a in assets]), 3),
fee_survives=fee_ok_all))
return dict(name=name, kind="signals", cells=cells, verdict=_verdict(cells))
# ===========================================================================
# OUTPUT
# ===========================================================================
def _clean(o):
if isinstance(o, dict):
return {k: _clean(v) for k, v in o.items() if k not in ("net", "idx", "equity")}
if isinstance(o, (list, tuple)):
return [_clean(x) for x in o]
if isinstance(o, (np.floating,)):
return round(float(o), 4)
if isinstance(o, (np.integer,)):
return int(o)
return o
def as_json(rep: dict) -> str:
return json.dumps(_clean(rep), default=str)
def fmt(rep: dict) -> str:
v = rep["verdict"]
lines = [f"=== {rep['name']} [{rep['kind']}] -> {v['grade']} "
f"(best {v.get('best_tf')}: full {v.get('best_full_sharpe')}, "
f"hold {v.get('best_holdout_sharpe')})"]
for c in rep["cells"]:
lines.append(f" TF {c['tf']:>4s}: minFull={c['min_asset_full_sharpe']:+.2f} "
f"minHold={c['min_asset_holdout_sharpe']:+.2f} feeOK={c['fee_survives']}")
for a, pa in c["per_asset"].items():
yr = " ".join(f"{y}:{d['ret']*100 if isinstance(d, dict) else d*100:+.0f}%"
for y, d in list(pa["yearly"].items()))
lines.append(f" {a}: full Sh={pa['full']['sharpe']:+.2f} "
f"DD={pa['full']['maxdd']*100:.0f}% ret={pa['full']['ret']*100:+.0f}% "
f"hold Sh={pa['holdout'].get('sharpe', 0):+.2f} | {yr}")
return "\n".join(lines)
if __name__ == "__main__":
# smoke test: buy&hold, TSMOM trend, donchian breakout
print("--- SMOKE TEST altlib ---")
bh = study_weights("BUYHOLD", lambda df: np.ones(len(df)), tfs=("1d",))
print(fmt(bh))
def tsmom(df):
c = df["close"].values
bpd = bars_per_day(df)
d = np.zeros(len(c))
for h in (30 * bpd, 90 * bpd, 180 * bpd):
s = np.full(len(c), np.nan); s[h:] = np.sign(c[h:] / c[:-h] - 1)
d = d + np.nan_to_num(s)
d = np.clip(np.sign(d), 0, None)
return vol_target(d, df, 0.20, 30, 2.0)
print(fmt(study_weights("TSMOM-LF", tsmom, tfs=("1d",))))
def donch(df):
hi, lo = donchian(df, 20)
c = df["close"].values
pos = np.where(c > hi, 1.0, np.nan)
pos = np.where(c < lo, 0.0, pos)
return pd.Series(pos).ffill().fillna(0.0).values
print(fmt(study_weights("DONCHIAN20-LF", donch, tfs=("1d",))))
print("\nJSON sample:", as_json(bh)[:300])
+96
View File
@@ -0,0 +1,96 @@
"""Demo / validation of the MARGINAL-vs-TP01 scorer (2026-06-20).
Shows the lesson of the 104-hypothesis sweep operationalized: strategies that scored
an absolute PASS but are just TP01/TSMOM with an overlay collapse to REDUNDANT/NEUTRAL/
DILUTES under marginal scoring, while the one genuine diversifier (STA05 long-short)
earns ADDS. Run: uv run python scripts/research/alt/marginal_demo.py
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt")
import numpy as np
import pandas as pd
import altlib as al
from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio
def tsmom_dir(df):
"""Long-flat multi-horizon TSMOM direction in {0,1} (the bare TP01 trend signal)."""
c = df["close"].values.astype(float)
bpd = al.bars_per_day(df)
d = np.zeros(len(c))
for h in (30 * bpd, 90 * bpd, 180 * bpd):
s = np.full(len(c), np.nan)
s[h:] = np.sign(c[h:] / c[:-h] - 1.0)
d += np.nan_to_num(s)
return np.clip(np.sign(d), 0, None)
def tp01_target(df):
return TrendPortfolio(**CANONICAL).target_series(df)
FAST, SLOW = [5, 10, 20, 40], [40, 80, 120, 200]
PAIRS = [(f, s) for f in FAST for s in SLOW if f < s]
def sta05(df, long_only):
c = df["close"].values.astype(float)
v = np.zeros(len(c))
for f, s in PAIRS:
v += np.sign(al.ema(c, f) - al.ema(c, s))
d = v / len(PAIRS)
if long_only:
d = np.clip(d, 0.0, 1.0)
return al.vol_target(d, df, 0.20, 30, 2.0)
def vol03(df, asset):
"""DVOL-gated TSMOM (active only when DVOL below its expanding median)."""
d = tsmom_dir(df)
dv = pd.Series(al.dvol(df, asset))
thr = dv.expanding(min_periods=30).quantile(0.5)
gate = dv.isna() | thr.isna() | (dv < thr)
d = np.where(gate.values, d, 0.0)
return al.vol_target(d, df, 0.20, 30, 2.0)
def cmb04(df):
"""Momentum + low-vol filter (TSMOM taken only when realized vol below expanding median)."""
d = tsmom_dir(df)
bpd = al.bars_per_day(df)
rv = al.realized_vol(al.simple_returns(df["close"].values.astype(float)), 30 * bpd, bpd * 365.25)
med = pd.Series(rv).expanding(min_periods=60).median().values
d = np.where((rv < med) | np.isnan(med), d, 0.0)
return al.vol_target(d, df, 0.20, 30, 2.0)
CANDIDATES = [
("TP01-itself (sanity)", tp01_target),
("STA05 long-short (the lead)", lambda df: sta05(df, False)),
("STA05 long-only", lambda df: sta05(df, True)),
("VOL03 DVOL-gated TSMOM (overlay)", vol03),
("CMB04 momentum+low-vol (overlay)", cmb04),
]
print("=" * 78)
print("MARGINAL SCORING vs TP01 baseline — absolute Sharpe is NOT enough for a slot")
print("=" * 78)
rows = []
for name, fn in CANDIDATES:
rep = al.study_marginal(name, fn, tf="1d")
print()
print(al.fmt_marginal(rep))
rows.append((name, rep["abs_grade"], rep["marginal_verdict"], rep["earns_slot"]))
print("\n" + "=" * 78)
print(f"{'candidate':<36s} {'absolute':>9s} {'marginal':>10s} {'earns_slot':>11s}")
for n, ag, mv, es in rows:
print(f"{n:<36s} {ag:>9s} {mv:>10s} {str(es):>11s}")
# sanity asserts: baseline reproduces, and an overlay-on-TSMOM does NOT earn a slot
sanity = al.marginal_vs_tp01(al.candidate_daily(tp01_target))
assert sanity["corr_full"] > 0.95, f"TP01-vs-itself corr should be ~1, got {sanity['corr_full']}"
assert abs(sanity["blends"]["w25"]["uplift_full"]) < 0.05, "TP01-vs-itself uplift should be ~0"
print("\nSANITY OK: TP01-vs-itself corr", sanity["corr_full"],
"uplift_full", sanity["blends"]["w25"]["uplift_full"], "->", sanity["marginal_verdict"])
+136
View File
@@ -0,0 +1,136 @@
"""Resta qualche candidato? — passa i contendenti promettenti piu' forti del sweep
(trend non-TSMOM, overlay DVOL, rotazione cross-asset) attraverso il gate MARGINALE vs TP01.
Run: uv run python scripts/research/alt/marginal_remaining.py
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt")
import numpy as np
import pandas as pd
import altlib as al
def tsmom_dir(df):
c = df["close"].values.astype(float); bpd = al.bars_per_day(df); d = np.zeros(len(c))
for h in (30 * bpd, 90 * bpd, 180 * bpd):
s = np.full(len(c), np.nan); s[h:] = np.sign(c[h:] / c[:-h] - 1.0); d += np.nan_to_num(s)
return np.clip(np.sign(d), 0, None)
def wma(x, n):
w = np.arange(1, n + 1)
return pd.Series(x).rolling(n).apply(lambda v: np.dot(v, w) / w.sum(), raw=True).values
# --- TRD10 Vortex(14) long-flat ---
def trd10(df):
h = df["high"].values.astype(float); l = df["low"].values.astype(float); c = df["close"].values.astype(float)
pc = np.roll(c, 1); pc[0] = c[0]; ph = np.roll(h, 1); ph[0] = h[0]; pl = np.roll(l, 1); pl[0] = l[0]
tr = np.maximum(h - l, np.maximum(np.abs(h - pc), np.abs(l - pc)))
n = 14; strn = pd.Series(tr).rolling(n).sum().values
vip = pd.Series(np.abs(h - pl)).rolling(n).sum().values / strn
vim = pd.Series(np.abs(l - ph)).rolling(n).sum().values / strn
d = np.where(np.isnan(vip), 0.0, np.where(vip > vim, 1.0, 0.0))
return al.vol_target(d, df, 0.20, 30, 2.0)
# --- TRD08 Hull MA slope ---
def trd08(df):
c = df["close"].values.astype(float)
h = wma(2 * wma(c, 27) - wma(c, 55), 7) # HMA(55)
slope = np.zeros(len(h)); slope[1:] = h[1:] - h[:-1]
d = np.where(slope > 0, 1.0, 0.0); d[np.isnan(h)] = 0.0
return al.vol_target(d, df, 0.20, 30, 2.0)
# --- TRD07 Kaufman AMA cross ---
def kama(c, n=10, fast=2, slow=30):
c = np.asarray(c, float); L = len(c); out = np.copy(c)
fsc, ssc = 2 / (fast + 1), 2 / (slow + 1)
vol = pd.Series(np.abs(np.diff(c, prepend=c[0]))).rolling(n).sum().values
change = np.full(L, np.nan); change[n:] = np.abs(c[n:] - c[:-n])
sc = (np.where(vol > 0, change / vol, 0.0) * (fsc - ssc) + ssc) ** 2
for i in range(1, L):
out[i] = out[i - 1] if np.isnan(sc[i]) else out[i - 1] + sc[i] * (c[i] - out[i - 1])
return out
def trd07(df):
c = df["close"].values.astype(float); k = kama(c)
slope = np.zeros(len(k)); slope[1:] = k[1:] - k[:-1]
d = np.where((c > k) & (slope > 0), 1.0, 0.0)
return al.vol_target(d, df, 0.20, 30, 2.0)
# --- VOL08 realized-vol term-structure overlay on TSMOM ---
def vol08(df):
c = df["close"].values.astype(float); bpd = al.bars_per_day(df); r = al.simple_returns(c)
sv = al.realized_vol(r, 5 * bpd, bpd * 365.25); lv = al.realized_vol(r, 30 * bpd, bpd * 365.25)
ratio = sv / lv; d = tsmom_dir(df)
d = np.where((ratio < 1.0) | np.isnan(ratio), d, 0.0)
return al.vol_target(d, df, 0.20, 30, 2.0)
# --- VOL11 DVOL kill-switch on TSMOM (df, asset) ---
def vol11(df, asset):
d = tsmom_dir(df); dv = pd.Series(al.dvol(df, asset))
thr = dv.expanding(min_periods=30).quantile(0.80)
kill = (~dv.isna()) & (~thr.isna()) & (dv > thr)
d = np.where(kill.values, 0.0, d)
return al.vol_target(d, df, 0.20, 30, 2.0)
# --- XAS09/03 cross-asset rotation (hold the stronger of BTC/ETH; dual=flat if both neg) ---
def rotation_daily(lb=90, dual=True):
R, M, V = {}, {}, {}
for a in ("BTC", "ETH"):
df = al.get(a, "1d"); c = df["close"].values.astype(float)
idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True))
mom = np.full(len(c), np.nan); mom[lb:] = c[lb:] / c[:-lb] - 1.0
R[a] = pd.Series(al.simple_returns(c), index=idx)
M[a] = pd.Series(mom, index=idx)
V[a] = pd.Series(al.vol_target(np.ones(len(c)), df, 0.20, 30, 2.0), index=idx)
R = pd.concat(R, axis=1, join="inner"); M = pd.concat(M, axis=1, join="inner").shift(1)
V = pd.concat(V, axis=1, join="inner").shift(1)
out = np.zeros(len(R))
for t in range(len(R)):
mrow = M.iloc[t]
if mrow.isna().all():
continue
best = mrow.idxmax()
if dual and mrow[best] <= 0:
continue
pos = V.iloc[t][best]
out[t] = (0.0 if np.isnan(pos) else pos) * R.iloc[t][best]
return pd.Series(out, index=R.index)
SINGLE = [("TRD10 Vortex", trd10), ("TRD08 Hull MA", trd08), ("TRD07 KAMA", trd07),
("VOL08 RV term-struct", vol08), ("VOL11 DVOL kill-switch", vol11)]
print("=" * 90)
print("RESTA QUALCHE CANDIDATO? — gate marginale vs TP01 sui contendenti piu' forti")
print("=" * 90)
rows = []
for name, fn in SINGLE:
rep = al.study_marginal(name, fn, tf="1d")
m = rep["marginal"]
print(al.fmt_marginal(rep))
print()
rows.append((name, rep["abs_grade"], rep["marginal_verdict"], rep["earns_slot"],
m.get("corr_hold"), m["blends"]["w25"].get("uplift_hold")))
# cross-asset rotations (built directly, scored marginally)
for name, dual in [("XAS09 dual-momentum", True), ("XAS03 RS rotation", False)]:
m = al.marginal_vs_tp01(rotation_daily(90, dual))
v = m["marginal_verdict"]
print(al.fmt_marginal({"name": name, "abs_grade": "n/a", "marginal_verdict": v,
"earns_slot": v == "ADDS", "marginal": m}))
print()
rows.append((name, "n/a", v, v == "ADDS", m.get("corr_hold"), m["blends"]["w25"].get("uplift_hold")))
print("=" * 90)
print(f"{'candidato':<26s}{'abs':>7s}{'marginale':>12s}{'slot':>7s}{'corr_hold':>11s}{'upliftH_w25':>13s}")
for n, ag, mv, es, ch, uh in rows:
print(f"{n:<26s}{ag:>7s}{mv:>12s}{str(es):>7s}{str(ch):>11s}{str(uh):>13s}")
print("\n(ADDS+slot=True => candidato vivo; tutto il resto => morto/ridondante)")
+74
View File
@@ -0,0 +1,74 @@
"""BRK01 — Donchian 10/20/55 channel breakout, long-short and long-flat variants.
Hypothesis: close breaks prior N-bar high -> long, prior N-bar low -> short (long-flat variant: flat
instead of short). Grid N in {10, 20, 55}. Best config picked by min-asset hold-out Sharpe.
With vol-targeting to 20% annualized volatility (TP01-style).
CAUSAL: al.donchian(df, N) shifts the rolling max/min by 1 bar -> breakout at close[i] is
strictly decided with data up to and including close[i-1] for the channel, so it is leak-free.
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt")
import altlib as al
import numpy as np
import pandas as pd
# ---- Strategy implementation -----------------------------------------------
def make_brk_ls(N: int):
"""Long-short Donchian breakout: +1 if close breaks N-bar prior high, -1 if breaks prior low,
hold otherwise. Vol-targeted to 20%."""
def target(df):
hi, lo = al.donchian(df, N)
c = df["close"].values.astype(float)
# signal: +1 long, -1 short, nan=hold previous
sig = np.full(len(c), np.nan)
sig[c > hi] = 1.0
sig[c < lo] = -1.0
# forward-fill (hold position until next signal)
direction = pd.Series(sig).ffill().fillna(0.0).values
return al.vol_target(direction, df, target_vol=0.20, vol_win_days=30, leverage_cap=2.0)
return target
def make_brk_lf(N: int):
"""Long-flat Donchian breakout: +1 if close breaks N-bar prior high, 0 if breaks prior low.
Vol-targeted to 20%."""
def target(df):
hi, lo = al.donchian(df, N)
c = df["close"].values.astype(float)
sig = np.full(len(c), np.nan)
sig[c > hi] = 1.0
sig[c < lo] = 0.0
direction = pd.Series(sig).ffill().fillna(0.0).values
return al.vol_target(direction, df, target_vol=0.20, vol_win_days=30, leverage_cap=2.0)
return target
# ---- Run grid: N in {10, 20, 55} x variant {LS, LF}, TF=1d only (keep <=6 backtests) ----
# Total: 3 N values x 2 variants x 1 TF x 2 assets = 12 asset-runs but only 3 study_weights calls
# Each study_weights call does 2 assets = 6 total calls -> 6 cells, fine.
# We also add 12h for the best N to compare frequency.
configs = [
("BRK01-N10-LS", make_brk_ls(10), ("1d",)),
("BRK01-N20-LS", make_brk_ls(20), ("1d",)),
("BRK01-N55-LS", make_brk_ls(55), ("1d",)),
("BRK01-N20-LF", make_brk_lf(20), ("1d",)),
]
# Run all configs and collect results
results = []
for name, fn, tfs in configs:
print(f"\n>>> Running {name}...")
rep = al.study_weights(name, fn, tfs=tfs)
print(al.fmt(rep))
print("JSON:", al.as_json(rep))
results.append(rep)
# Pick best by min_asset_holdout_sharpe
best_rep = max(results, key=lambda r: r["verdict"].get("best_holdout_sharpe", -99))
print("\n\n=== BEST CONFIG ===")
print(al.fmt(best_rep))
print("JSON:", al.as_json(best_rep))
+107
View File
@@ -0,0 +1,107 @@
"""BRK02 — Donchian55 + Chandelier ATR trailing stop.
IDEA:
- Enter LONG when close[i] breaks above the 55-bar Donchian high (prior bars, causal).
- Exit (go flat) when close[i] falls below the Chandelier trailing stop:
chandelier_stop = highest_close(22 bars, ending at i) - 3 * ATR(22, ending at i).
- Position is 0 (flat) or 1 (long). No short. Vol-targeted to 20% with 2x cap.
Implementation (weights style, continuous position):
- Donchian high computed on PRIOR bars (shift(1) already done by al.donchian).
- Chandelier stop computed causally on current+prior bars:
hc[i] = max(close[i-21..i]) -> rolling max of close, window=22
atr22[i] = ATR(22 bars) at i
stop[i] = hc[i] - 3 * atr22[i]
- State machine:
if flat and close[i] > donchian_high[i]: go long
if long and close[i] < stop[i]: go flat
Params tried: (don_win=55, atr_win=22, atr_mult=3.0) canonical
(don_win=40, atr_win=22, atr_mult=2.5) tighter
Best picked by min_asset_holdout_sharpe.
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt")
import altlib as al
import numpy as np
import pandas as pd
def chandelier_signal(df: pd.DataFrame, don_win: int = 55,
atr_win: int = 22, atr_mult: float = 3.0) -> np.ndarray:
"""Return a direction array in {0, 1} (0=flat, 1=long) using Donchian+Chandelier.
Causal: decision at i uses only data <= close[i]."""
close = df["close"].values.astype(float)
n = len(close)
# Donchian upper channel: highest HIGH over prior don_win bars (shift(1) in al.donchian)
don_high, _ = al.donchian(df, don_win) # don_high[i] = max(high[i-don_win..i-1])
# ATR(atr_win) — causal, uses bars up to and including i
atr22 = al.atr(df, atr_win)
# Highest CLOSE over trailing atr_win bars (inclusive of i) — causal
highest_close = pd.Series(close).rolling(atr_win, min_periods=atr_win).max().values
# Chandelier stop at i
chandelier_stop = highest_close - atr_mult * atr22
# State machine: flat=0, long=1
pos = np.zeros(n, dtype=float)
state = 0 # start flat
for i in range(n):
c = close[i]
dh = don_high[i]
cs = chandelier_stop[i]
if state == 0:
# Enter long if close breaks above prior Donchian high (valid only if dh is defined)
if np.isfinite(dh) and c > dh:
state = 1
else: # state == 1
# Exit long if close drops below chandelier stop (and stop is defined)
if np.isfinite(cs) and c < cs:
state = 0
pos[i] = float(state)
return pos
def make_target(don_win: int = 55, atr_win: int = 22, atr_mult: float = 3.0):
"""Factory returning a vol-targeted weight function for a given param set."""
def target_fn(df: pd.DataFrame) -> np.ndarray:
direction = chandelier_signal(df, don_win=don_win, atr_win=atr_win, atr_mult=atr_mult)
return al.vol_target(direction, df, target_vol=0.20, vol_win_days=30, leverage_cap=2.0)
return target_fn
# --- Small param grid (4 configurations, TFs: 1d and 12h -> 8 backtest cells total)
CONFIGS = [
dict(don_win=55, atr_win=22, atr_mult=3.0, label="D55-A22-M3.0"),
dict(don_win=40, atr_win=22, atr_mult=2.5, label="D40-A22-M2.5"),
dict(don_win=55, atr_win=14, atr_mult=3.0, label="D55-A14-M3.0"),
dict(don_win=70, atr_win=22, atr_mult=3.5, label="D70-A22-M3.5"),
]
TFS = ("1d", "12h")
best_rep = None
best_score = -999.0
for cfg in CONFIGS:
lbl = cfg["label"]
fn = make_target(don_win=cfg["don_win"], atr_win=cfg["atr_win"], atr_mult=cfg["atr_mult"])
rep = al.study_weights(f"BRK02[{lbl}]", fn, tfs=TFS)
score = rep["verdict"].get("best_holdout_sharpe", -9)
print(f"Config {lbl}: grade={rep['verdict']['grade']} score(hold)={score:.3f}")
if score > best_score:
best_score = score
best_rep = rep
# Rename best result to canonical BRK02
best_rep["name"] = "BRK02"
print()
print(al.fmt(best_rep))
print("JSON:", al.as_json(best_rep))
+75
View File
@@ -0,0 +1,75 @@
"""BRK03 — Keltner Channel Breakout
HYPOTHESIS: Long when close > EMA20 + k*ATR(20); flat when close < EMA20.
Try k in {1.5, 2.0, 2.5}. Vol-targeted continuous weights.
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt")
import altlib as al
import numpy as np
def keltner_breakout(df, k: float) -> np.ndarray:
"""Long (vol-targeted) when close > EMA20 + k*ATR(20); flat when close < EMA20.
All values causal: EMA/ATR at i use data <= i. Decision at close[i] -> held during bar i+1.
"""
c = df["close"].values.astype(float)
ema20 = al.ema(c, span=20)
atr20 = al.atr(df, win=20)
upper_band = ema20 + k * atr20
# Direction: +1 if close > upper_band (breakout above), else 0 (flat)
# Exit: go flat when close < EMA20 (mean reversion back below center)
n = len(c)
direction = np.zeros(n, dtype=float)
# Vectorized: long when above upper band; we then hold until close < EMA20
# Implement as a state machine
in_trade = False
for i in range(n):
if np.isnan(ema20[i]) or np.isnan(atr20[i]):
direction[i] = 0.0
continue
if not in_trade:
# Enter long on breakout above upper keltner band
if c[i] > upper_band[i]:
in_trade = True
direction[i] = 1.0
else:
# Exit when price drops back below EMA
if c[i] < ema20[i]:
in_trade = False
direction[i] = 0.0
else:
direction[i] = 1.0
# Apply vol-targeting to scale position size
pos = al.vol_target(direction, df, target_vol=0.20, vol_win_days=30, leverage_cap=2.0)
return pos
# Grid: k in {1.5, 2.0, 2.5} — try all 3 param sets; pick best by min_asset_holdout_sharpe
best_rep = None
best_score = -999.0
best_k = None
for k_val in [1.5, 2.0, 2.5]:
name = f"BRK03-k{k_val}"
print(f"\n--- Running {name} ---")
rep = al.study_weights(
name,
lambda df, k=k_val: keltner_breakout(df, k),
tfs=("1d", "12h")
)
score = rep["verdict"].get("best_holdout_sharpe", -999.0) or -999.0
print(al.fmt(rep))
if score > best_score:
best_score = score
best_rep = rep
best_k = k_val
print("\n" + "="*60)
print(f"BEST CONFIG: k={best_k}")
print("="*60)
print(al.fmt(best_rep))
print("JSON:", al.as_json(best_rep))
+89
View File
@@ -0,0 +1,89 @@
"""BRK04 — Bollinger Breakout (vol expansion), momentum interpretation.
HYPOTHESIS: Long-flat when close > upper BB(win, k); exit to flat when close < mid BB.
This is a momentum (trend-following) reading of Bollinger Band breakouts price above
the upper band means the move is strong enough to be outside 2-sigma, so we ride it.
Internal grid (<=4 configs, total backtests <=6):
Config A: BB(20, 2.0), tfs=("1d",) -- canonical params
Config B: BB(20, 1.5), tfs=("1d",) -- tighter band (more signals)
Config C: BB(30, 2.0), tfs=("1d",) -- wider lookback
Config D: BB(20, 2.0) + vol_target, tfs=("1d",) -- vol-sized
We use bbands() which is causal at bar i (uses data up to i).
Entry/exit logic is also causal no look-ahead.
The lib shift means target[i] is held during bar i+1.
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt")
import altlib as al
import numpy as np
import pandas as pd
def _bb_long_flat(df: pd.DataFrame, win: int = 20, k: float = 2.0,
use_vol_target: bool = False) -> np.ndarray:
"""Causal BB breakout: long when close > upper band, flat when close < mid band.
State machine with forward-fill between entry and exit signals."""
c = df["close"].values.astype(float)
upper, mid, lower = al.bbands(c, win=win, k=k)
# State: 1 = in long, 0 = flat
# At bar i:
# - if state was 0 (flat): enter long if close[i] > upper[i]
# - if state was 1 (long): exit to flat if close[i] < mid[i]
# Result is decided at close[i], held during bar i+1 (shift done by lib).
n = len(c)
target = np.zeros(n)
state = 0 # start flat
for i in range(n):
if np.isnan(upper[i]) or np.isnan(mid[i]):
target[i] = 0.0
continue
if state == 0:
# Check entry: close above upper band
if c[i] > upper[i]:
state = 1
else: # state == 1, in long
# Check exit: close below mid band
if c[i] < mid[i]:
state = 0
target[i] = float(state)
if use_vol_target:
target = al.vol_target(target, df, target_vol=0.20, vol_win_days=30, leverage_cap=2.0)
return target
# --- Grid: 4 configs on 1d only (total backtests = 4 x 2 assets = 8, but each config
# runs both assets via study_weights internally, so 4 study_weights calls = 4x2=8
# asset-level backtests). Within budget.
configs = [
dict(name="BRK04-A-BB20-2.0", win=20, k=2.0, vol_tgt=False),
dict(name="BRK04-B-BB20-1.5", win=20, k=1.5, vol_tgt=False),
dict(name="BRK04-C-BB30-2.0", win=30, k=2.0, vol_tgt=False),
dict(name="BRK04-D-BB20-2.0-VT", win=20, k=2.0, vol_tgt=True),
]
results = []
for cfg in configs:
w, k, vt = cfg["win"], cfg["k"], cfg["vol_tgt"]
fn = lambda df, _w=w, _k=k, _vt=vt: _bb_long_flat(df, win=_w, k=_k, use_vol_target=_vt)
rep = al.study_weights(cfg["name"], fn, tfs=("1d",))
results.append(rep)
print(al.fmt(rep))
print()
# Pick best config by min_asset_holdout_sharpe in best TF
def _best_score(r):
return max(c["min_asset_holdout_sharpe"] for c in r["cells"])
best = max(results, key=_best_score)
print("\n" + "="*60)
print(f"BEST CONFIG: {best['name']}")
print(al.fmt(best))
print("JSON:", al.as_json(best))
+75
View File
@@ -0,0 +1,75 @@
"""BRK05 — ATR Range Breakout (discrete signals, 1d only).
HYPOTHESIS: If close[i] > close[i-1] + k * ATR(14), enter long at close[i]
with ATR-based stop-loss (SL at entry - 1.5*ATR) and max_bars exit.
Grid: k in {0.5, 1.0, 1.5}, max_bars in {5, 10}.
Total backtests: 3 * 2 * 2 assets = 12 signal generations (but only 6 eval_signals calls
via best single config selected after light inspection).
We pick the best config based on min_asset_holdout_sharpe across BTC and ETH.
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt")
import altlib as al
import numpy as np
# --- Signal generator factory ---
def make_entries(k: float, max_bars: int):
"""Return a function that builds entries list for a given df."""
def entries_fn(df):
c = df["close"].values.astype(float)
atr_arr = al.atr(df, win=14)
n = len(c)
entries = [None] * n
for i in range(1, n):
if not np.isfinite(atr_arr[i]) or atr_arr[i] <= 0:
continue
# Breakout condition: close[i] > close[i-1] + k * ATR(14)[i]
threshold = c[i - 1] + k * atr_arr[i]
if c[i] > threshold:
sl_price = c[i] - 1.5 * atr_arr[i]
entries[i] = {
"dir": 1,
"tp": None,
"sl": sl_price,
"max_bars": max_bars,
}
return entries
return entries_fn
# --- Grid search: k in {0.5, 1.0, 1.5}, max_bars in {5, 10} ---
configs = [
(0.5, 5),
(0.5, 10),
(1.0, 5),
(1.0, 10),
(1.5, 5),
(1.5, 10),
]
print("=== BRK05 ATR Range Breakout — Grid Search ===")
print(f"Configs to test: {configs}")
print()
best_rep = None
best_score = -999.0
for k, mb in configs:
name = f"BRK05-k{k}-mb{mb}"
fn = make_entries(k, mb)
rep = al.study_signals(name, fn, tfs=("1d",))
v = rep["verdict"]
score = v.get("best_holdout_sharpe", -9)
print(al.fmt(rep))
print(f" -> score (min hold sharpe) = {score:.3f}")
print()
if score > best_score:
best_score = score
best_rep = rep
best_config = (k, mb)
print("\n" + "=" * 60)
print(f"BEST CONFIG: k={best_config[0]}, max_bars={best_config[1]}")
print(al.fmt(best_rep))
print("JSON:", al.as_json(best_rep))
+68
View File
@@ -0,0 +1,68 @@
"""BRK06 — Opening-Range Breakout (daily).
HYPOTHESIS: On 1d bars, go LONG when today's close > prior-day high (expansion/gap breakout).
SL = prior-day low. max_bars = configurable (3 or 5). No short side (breakdowns symmetric but
crypto skew is upward; testing long-only first). Entry at close[i] once close[i] > prior high[i-1].
Exit at SL=prior_low[i-1] or max_bars (time stop), whichever first.
Grid: max_bars in {3, 5} -> 2 configs × 1 TF × 2 assets = 4 backtests.
Honesty rules:
- decision uses close[i] vs high[i-1]: CAUSAL (prior-bar high is known by close of bar i).
- SL = low[i-1]: known causal.
- entry = close[i] (not high/low extreme of bar i).
- fee = 0.10% RT (Deribit taker).
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt")
import altlib as al
import numpy as np
def make_entries(df, max_bars: int):
"""Long when close[i] > high[i-1]. SL = low[i-1]. Exit at max_bars or SL."""
c = df["close"].values
h = df["high"].values
lo = df["low"].values
n = len(c)
entries = [None] * n
for i in range(1, n):
prior_high = h[i - 1]
prior_low = lo[i - 1]
if c[i] > prior_high:
# Long breakout: entry at close[i], SL below prior-day low
# TP = None (let the time-stop manage exit)
entries[i] = {
"dir": 1,
"tp": None,
"sl": prior_low,
"max_bars": max_bars,
}
return entries
configs = [
{"max_bars": 3},
{"max_bars": 5},
]
best_rep = None
best_score = -9999
for cfg in configs:
name = f"BRK06-mb{cfg['max_bars']}"
rep = al.study_signals(
name,
lambda df, mb=cfg["max_bars"]: make_entries(df, mb),
tfs=("1d",),
)
print(al.fmt(rep))
score = rep["verdict"].get("best_holdout_sharpe", -9999)
if score is None:
score = -9999
if score > best_score:
best_score = score
best_rep = rep
print("\n=== BEST CONFIG ===")
print(al.fmt(best_rep))
print("JSON:", al.as_json(best_rep))
+79
View File
@@ -0,0 +1,79 @@
"""BRK07 — N-day-high momentum (long-flat)
IDEA: Long-flat: position 1 while close is within X% of its rolling 100-bar max, else 0.
Trend-persistence proxy. Optionally vol-targeted.
Grid: threshold X% in {2%, 5%} x vol_target in {False, True} -> 4 configs, 2 TFs = 8 total backtests.
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt")
import altlib as al
import numpy as np
LOOKBACK = 100 # fixed as per hypothesis
def make_target(df, threshold_pct: float = 5.0, use_vol_target: bool = True) -> np.ndarray:
"""Long (1) when close is within threshold_pct% of its rolling 100-bar max, else 0."""
c = df["close"].values.astype(float)
n = len(c)
# Rolling max of close over last LOOKBACK bars (causal: includes close[i])
roll_max = (
__import__("pandas").Series(c)
.rolling(LOOKBACK, min_periods=LOOKBACK)
.max()
.values
)
# Position: 1 if close >= roll_max * (1 - threshold_pct/100), else 0
threshold = threshold_pct / 100.0
direction = np.where(
(roll_max > 0) & np.isfinite(roll_max) & (c >= roll_max * (1.0 - threshold)),
1.0,
0.0
)
# Before we have enough bars, stay flat
direction[:LOOKBACK - 1] = 0.0
if use_vol_target:
return al.vol_target(direction, df, target_vol=0.20, vol_win_days=30, leverage_cap=2.0)
else:
return direction
configs = [
{"threshold_pct": 2.0, "use_vol_target": False, "label": "thr2pct_flat"},
{"threshold_pct": 5.0, "use_vol_target": False, "label": "thr5pct_flat"},
{"threshold_pct": 2.0, "use_vol_target": True, "label": "thr2pct_vt"},
{"threshold_pct": 5.0, "use_vol_target": True, "label": "thr5pct_vt"},
]
best_rep = None
best_score = -9999.0
for cfg in configs:
label = cfg["label"]
threshold_pct = cfg["threshold_pct"]
use_vol_target = cfg["use_vol_target"]
print(f"\n=== BRK07 config: {label} (threshold={threshold_pct}%, vol_target={use_vol_target}) ===")
fn = lambda df, t=threshold_pct, v=use_vol_target: make_target(df, t, v)
rep = al.study_weights(
f"BRK07-{label}",
fn,
tfs=("1d", "12h"),
)
print(al.fmt(rep))
print("JSON:", al.as_json(rep))
# Score = min holdout sharpe across both assets in best TF
score = rep["verdict"].get("best_holdout_sharpe", -9999.0) or -9999.0
if score > best_score:
best_score = score
best_rep = rep
best_cfg = cfg
print("\n\n========== BEST CONFIG ==========")
print(f"Config: {best_cfg['label']}")
print(al.fmt(best_rep))
print("JSON:", al.as_json(best_rep))
+104
View File
@@ -0,0 +1,104 @@
"""BRK08 — NR7 range-contraction breakout (signals, 1d)
IDEA: A bar with the narrowest high-low range in the last 7 bars (NR7) is a
setup for a volatility breakout. On the next bar, if price closes above the
NR7 bar's high -> go long; if price closes below the NR7 bar's low -> go short.
Entry at close on confirmation bar. Exit via TP (multiple of range), SL (opposite
side of NR7 bar), or max_bars timeout.
GRID (4 param sets, 1 TF = 4 total backtests × 2 assets = 8 total):
- (tp_mult, sl_mult, max_bars): controls TP distance as multiple of NR7 range,
SL as fraction of NR7 range on opposite side, and holding period.
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt")
import altlib as al
import numpy as np
def nr7_signals(df, tp_mult=2.0, sl_mult=1.0, max_bars=5):
"""
NR7 breakout signals on daily bars.
- At close[i-1], identify if bar i-1 is the NR7 bar (narrowest in 7)
- At close[i]: if close[i] > high[i-1] -> long signal (direction confirmed)
if close[i] < low[i-1] -> short signal
- Entry at close[i]
- TP = entry + tp_mult * nr7_range (long) / entry - tp_mult * nr7_range (short)
- SL = nr7_bar_low (long) / nr7_bar_high (short)
- max_bars timeout
"""
hi = df["high"].values.astype(float)
lo = df["low"].values.astype(float)
cl = df["close"].values.astype(float)
n = len(df)
# Compute range for each bar
rng = hi - lo
entries = [None] * n
for i in range(7, n):
# Check if bar i-1 is NR7: its range is the smallest in the last 7 bars (i-7 to i-1)
prev_ranges = rng[i-7:i] # 7 bars ending at i-1
prev_range_at_im1 = rng[i-1]
# NR7: bar i-1 has the narrowest range in last 7 bars
if prev_range_at_im1 != np.min(prev_ranges):
continue
# The NR7 bar (i-1) setup: record its high and low
nr7_high = hi[i-1]
nr7_low = lo[i-1]
nr7_range = rng[i-1]
if nr7_range <= 0:
continue
# At bar i, confirm breakout direction with close
current_close = cl[i]
if current_close > nr7_high:
# Bullish breakout confirmed at close[i]
entry = current_close
tp = entry + tp_mult * nr7_range
sl = nr7_low - sl_mult * nr7_range * 0.1 # just below NR7 bar low
entries[i] = {"dir": +1, "tp": tp, "sl": sl, "max_bars": max_bars}
elif current_close < nr7_low:
# Bearish breakout confirmed at close[i]
entry = current_close
tp = entry - tp_mult * nr7_range
sl = nr7_high + sl_mult * nr7_range * 0.1 # just above NR7 bar high
entries[i] = {"dir": -1, "tp": tp, "sl": sl, "max_bars": max_bars}
return entries
# Grid: (tp_mult, sl_mult, max_bars)
GRID = [
(1.5, 1.0, 4), # tight TP, fast exit
(2.0, 1.0, 5), # moderate TP
(2.5, 1.0, 7), # wider TP, longer hold
(2.0, 1.0, 10), # same TP, longer hold
]
best_rep = None
best_score = -999.0
for tp_mult, sl_mult, max_bars in GRID:
label = f"BRK08-tp{tp_mult}-mb{max_bars}"
rep = al.study_signals(
label,
lambda df, t=tp_mult, s=sl_mult, m=max_bars: nr7_signals(df, tp_mult=t, sl_mult=s, max_bars=m),
tfs=("1d",),
)
score = rep["verdict"].get("best_holdout_sharpe", -999.0) or -999.0
print(f"\n--- {label} ---")
print(al.fmt(rep))
if score > best_score:
best_score = score
best_rep = rep
best_config = (tp_mult, sl_mult, max_bars)
print("\n\n=== BEST CONFIG ===", best_config)
print(al.fmt(best_rep))
print("JSON:", al.as_json(best_rep))

Some files were not shown because too many files have changed in this diff Show More