76 Commits

Author SHA1 Message Date
Adriano Dal Pastro b691f48f43 docs(diary): incident 2026-07-09 — "BOOK LIVE conto offline" = cert Traefik di default
Traefik auto-upgrade (latest+Watchtower -> 3.7.7) ha scartato il resolver ACME
per acme.json a 660 (3.x pretende 600) -> cert self-signed su tutto il VPS ->
DeribitRead SSLError -> book online=False. Zero trade persi (target flat, gate
di sicurezza OK). Fix: chmod 600 acme.json + restart; pin traefik:3.7 nel compose.
Diario con diagnosi, verifica end-to-end e runbook.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 20:09:41 +00:00
Adriano Dal Pastro 822aa1307e research(crt): video-claim "CRT top-down 74% win rate" SCARTATO — il 74% e' un knob, non un edge
Meccanizzato e falsificato il claim ICT/SMC top-down (H1 setup CRT -> M15/M5
conferma -> entry, uscita parziale+BE+runner) su BTC/ETH certificati.

- Setup H1 CRT gia' triplo-refutato (onda 2026-07-02); qui testato l'angolo nuovo:
  la gestione d'uscita e il "74% WR".
- WR reale 30-37% a RR 1.5-2, SOTTO il null gambler's-ruin 40% -> il ritest tocca
  il target meno di una moneta (conferma "il ritest e' informazione negativa").
- Il 74% si fabbrica avvicinando il target: sweep rr1 0.5->2.0 = WR sale, expectancy
  R INVARIANTE e negativa (-1.2..-3.3R netto). DSR 0.000; a fee 0 ancora negativa
  -> non morte-per-fee, l'edge lordo non c'e'.
- Non eseguibile pulito a $600 (3-4 ordini/trade, alcuni sub-min-order).
- Regola candidata: convertire ogni claim "WR X%" da schema parziale+BE in
  expectancy R netto fee (WR ~= 1/(1+rr1), non un merito).

Nessuna modifica a src/config/live/tests. Book e pesi INVARIATI.
Diario: docs/diary/2026-07-07-crt-topdown-74winrate.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 06:02:38 +00:00
Adriano Dal Pastro cf7de40dc0 feat(live): cap/asset dinamico = equity/2 — operazionalizza la decisione frontier (inerte a $600)
Il cap notional per-asset non e' piu' fisso a $300: con max_notional_per_asset_frac
in config diventa equity/2, cosi' cresce col capitale e un deposito futuro non resta
strozzato (frontiera 2026-07-03). Guardrail: dinamico SOLO con equity reale fidata;
su fallback/offline si torna al cap fisso (protezione downside quando E e' ignota).
Live dry-run: cap $299 a equity $598 -> inerte oggi. Suite 172 passed (+4 test).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 09:42:13 +00:00
Adriano Dal Pastro eef200cf14 research(frontier): frontiera onesta del book — il 6% e' un miraggio di regime, gira gia' alla vol nativa
Misura (non ricerca segnali) della frontiera CAGR/DD/P-rovina vs
target-vol, pesata per confidenza per-sleeve, a 2k/5k. 7 agenti (3
filoni + refuter fat-tail + scettico), sanity bit-exact dei due book.

Reframe decisivo: il "6% attuale" NON e' un target, e' la vol realizzata
in risk-off; a lambda=1 il book LIVE 2-sleeve gira gia' a ~11% vol/9.4%
DD -> "saltare a 10%" e' assumere non-risk-off, non aggiungere leva.

- Target-vol fidato book LIVE (TP01+SKH01, unico eseguibile <20k):
  ~10% (banda 8-11%, lambda ~0.9). Scale-invariante: stesso % a 2k/5k.
- P(rovina-50%) e' la metrica SBAGLIATA (slack a 22%); il vincolo che
  morde e' P(DD>30%), super-lineare. Muro onesto ~12% (gap-through SKH
  reale non nei rendimenti modellati; 2-sleeve 4x crash-sensibile).
- Vol differenziata-per-confidenza REFUTATA: taglia la diversificazione;
  la bassa fiducia va nel DE-MEAN (haircut media), non nella leva.
- Reward onesto (HOLD<<FULL): ~EUR 0.3-0.7/g@2k, 0.8-1.8/g@5k; warm-up
  vs 6% risk-off = ~+EUR 0.2/g@2k. EUR 50/g resta ~130k (capitale+tempo).
- UNICA azione config (proposta, NON applicata): cap $300 -> equity/2,
  altrimenti a 5k il cap raffredda paradossalmente il book sotto il punto
  fidato (6% vol ceiling).

config/live.json NON toccato. Book invariato. 168 test verdi.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 08:43:59 +00:00
Adriano Dal Pastro 116abf5203 docs(diary): snapshot stato completo del progetto (2026-07-03)
Foto d'insieme dopo il blocco di ricerca 1-3 luglio: book live
(TP01+SKH01 flat, $598), portafoglio research 5-sleeve (de-luckato
HOLD ~2.0), anchor-audit completo 4/4, edge morti da non ri-testare,
~10 gate anti-illusione codificati, mappa capital-scaling 2-5k,
aperti/prossimi passi. Punto di riferimento unico dello stato.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 07:16:17 +00:00
Adriano Dal Pastro 26f8d27a61 research(wave-0703): migliora+proteggi VRP01 — 7 filoni, 0 miglioramenti, anchor-audit VRP01 chiuso (4/4 sleeve)
Goal: migliorare la strategia short-vol (famiglia Albimarini/VRP01) e
proteggerla dai DD. Workflow 26 agenti (7 filoni + 2 lenti avversariali
+ scettico incrociato). Esito: NON migliorabile; la protezione DD si
compra SOLO con la size.

- Griglia 288 strutture: nessuna batte VRP01 (DSR 0.000; meta' griglia
  = 3a occorrenza "0-perdite/Sharpe implausibile" dopo CC01/ALB-A).
- 4 overlay DD (exit-spike/SL-MTM/ala-coda/cooldown): 4/4 REFUTED dal
  null de-levering — la protezione crash vive gia' nel gate IV-rank.
- Gate nuovi: 4o fallimento su 4 (l'alpha e' il binario IV-rank>0.30).
- Sizing: 12% deploy ~ 0.27 Kelly onesto (anti-rovina); disambiguazione
  unita' obbligatoria vs 12% peso book (0.014 Kelly, fattore 19x).
- Gate term-structure VIX/VXV su SPX (dSh +0.90, DSR 0.992) = confound
  di modello al 100% -> nuova regola: riprezzare term-structure-consistent
  prima di credere a un gate vol su strutture BS-flat.
- ANCHOR-AUDIT VRP01 CHIUSO: primo sleeve SENZA luck (fase canonica =
  peggiore delle 7 -> numeri di ammissione conservativi). Audit anchor
  ora completo su 4/4 sleeve ancorati.

Book/pesi INVARIATI. Nessun nuovo sleeve. 168 test verdi.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 07:05:19 +00:00
Adriano Dal Pastro 76120b59c2 research(wave-0702bis): ondata video-claims — Elliott 3/3 e Albimarini 2/2 scartati, mappa capital scaling 2-5k
Sei agenti, nessun sopravvissuto:
- ELL-A range-cycle: rumore (0/24 Bonferroni; nessuna cella weekly regge
  a tutte le 7 ancore). Lezione pandas: resample("7D", origin) IGNORA
  origin -> usare "168h" per le bande d'ancora weekly.
- ELL-B Fibonacci: l'edge apparente e' la POSIZIONE dei livelli, non i
  numeri (null location-matched: pctl 0.39-0.68); confluenza FAIL 4/4.
- ELL-C canale: Donchian travestito (non batte il Donchian equivalente,
  DSR 0.685, IS 1.40 -> HOLD -0.87; target 1.618 = caso; anchor-luck 4h).
- ALB-A diagonale: il condor stessa-scadenza la batte a ogni f; senza
  gate IV-rank tutte le strutture perdono (3a conferma: l'alpha del VRP
  e' il gate); fee-negativa su Deribit a qualsiasi size; 2o caso
  "0-perdite = Sharpe implausibile" dopo CC01.
- ALB-B claims: 82%/PF 5.16/"420%" consistente con zero skill (P=20-45%,
  78.6% delle finestre 6-mesi lo produce); replay con code reali =
  rovina 1998/2002/2020; la diagonale passa il 12-40% della perdita naked.
- Capital scaling 600->2-5k: unico vincolo binding = cap $300/asset
  (a 5k book al 49% del target) -> AL DEPOSITO alzare a equity/2;
  min_order $5 lasciare; XS01 ~20k confermata; aspettativa onesta
  de-luckata 2k ~EUR 0.6-0.8/g, 5k ~EUR 1.4-2/g.

Nessun nuovo sleeve, book live invariato. 168 test verdi.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 23:31:38 +00:00
Adriano Dal Pastro a74cc69583 research(anchor-audit): timing-luck confermato su XS01 e SKH01 — 3/3 sleeve ancorati, book de-luckato HOLD ~2.0
Chiude il pendente dell'ondata timing 2026-07-02. Due audit indipendenti
(sanity replica bit-exact, ancore a priori, zero tuning per-fase, bootstrap):

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

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 22:34:35 +00:00
Adriano Dal Pastro 73d74c5e53 research(wave-0702): ondata timing + CRT — 8 filoni, 0 nuovi sleeve, finding anchor timing-luck TP01
Goal: "altre strategie su Deribit con timing differenti". 8 filoni multi-agente + scettico:
- event-clock bars, expiry calendar Deribit, clock lenti/bande, regime-speed: SCARTATI
- CRT (Candle Range Theory) base/multi-TF/contesto: SCARTATA 3/3 (DSR~0, ritest =
  informazione negativa; sottoprodotto: FOLLOW>FADE sui livelli prior-day ogni anno,
  conferma il lead prevday)
- FINDING (confermato da scettico indipendente): hold-out 0.31 di TP01 = migliore delle
  24 ancore orarie (mediana 0.04, banda [-0.13,+0.30]) -> narrativa corretta in CLAUDE.md
  e docstring: l'hold-out non risolve l'edge di ritorno, regge il taglio DD a ogni ancora.
  Tranching K=2/4 = solo varianza della stima, no deploy a $600. Audit d'ancora pendente
  su XS01/SKH01. Book live e portafoglio INVARIATI. Test 168/168.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 22:12:22 +00:00
Adriano Dal Pastro e6657fcb16 feat(portfolio): GTAA01 promosso a 5° sleeve @20% — FULL 2.12→2.24, HOLD 2.21→2.46, DD 7.8→6.2%
Il diversificatore strutturale validato il 2026-06-22 (trend difensivo equity
6-ETF su IB, 30y storia, OOS 2015+ indipendente dall'hold-out crypto, corr al
book ~+0.10) era rimasto in paper_combo senza mai essere valutato come sleeve.
Valutazione onesta (r0701_gtaa_5th_sleeve): uplift positivo in-sample e su
TUTTE le finestre disgiunte (+0.05/+0.19/+0.25), multi-cut +0.21..+0.25,
plateau monotono w10-30% — passa dove EW-STR era morto. Ingresso @20% (IS-best
30%, scelta strutturale dichiarata). Convenzioni: weekend equity=0 (capitale
IB fermo, non riciclato), attivazione all'era book 2019-03. Il book live
Deribit (TP01+SKH01) NON cambia. Suite 168/168.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-01 23:31:38 +00:00
Adriano Dal Pastro 491411ac77 research(wave-0701): 6 filoni multi-agente — 0 nuovi sleeve, pesi confermati, gate weights_tilt_null
Ondata onesta su angoli non coperti: funding-TS (chiude il filone funding su 3
lati), breadth alt (non-ridondante ma DSR 0.43, rivisitabile con storia),
XS-residmom (REDUNDANT), pesi+guardia-DD (EW-STR refutato dallo scettico come
selezione-sull'hold-out di 2° ordine, firma best-of-15), VRP-refine (filone
esaurito), stagionalità-XS (morta allo step statistico).

Lezione codificata: weights_tilt_null + combine_outer in src/portfolio
(ogni cambio-pesi vs null di tilt casuali cap-respecting + delta in-sample>=0);
5 test nuovi, suite 165/165.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-01 23:21:59 +00:00
Adriano Dal Pastro ccf5e38101 feat(dashboard): banner alert operativi del book live (SKH/posizione/equity)
Il dashboard non mostrava nessuno dei tre segnali di health introdotti nei
commit 31369b3 (skh_error) e 5670469 (pos_error/eq_fallback): finivano solo su
Telegram + log cron, invisibili a chi guarda il monitor. pos_error/eq_fallback
erano gia' in shadow_report (usato dal dashboard) ma ignorati dal template;
skh_error non era nemmeno recuperato (il dashboard usa shadow_report, non
book_report).

Fix:
- _alerts_banner(): helper puro (verde "nessun alert" se pulito, rosso con una
  riga per errore) che rispecchia i tre alert di book_execute.
- build(): raccoglie pos_error/eq_fallback dallo shadow gia' fetchato + skh_error
  da book_report(offline=True) (feed certificato, nessuna rete extra).
- html(): renderizza il banner in cima alla sezione ① LIVE.
- tests/test_dashboard.py: +4 (verde, 3 alert, parziale, chiave-ignota).

Suite 160/160. Render end-to-end verde (conto sano $598.06 flat). Chiude la
copertura: i 3 pattern di errore silenzioso ora visibili su log + Telegram + dashboard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 22:47:35 +00:00
Adriano Dal Pastro 567046953d fix(book): gate fail-safe posizione + diagnostica equity — niente errori silenziosi nel path live
Terza+quarta chiusura del pattern "eccezione ingoiata -> stato safe silenzioso"
in src/live, dopo skh_error (31369b3). Emerse durante la verifica del gate SKH01.

Opzione A (gate fail-safe posizione):
- shadow._positions: se la read della posizione reale Deribit lancia, assume flat
  MA ora ritorna un pos_error esplicito (prima solo una note mai propagata).
- Propagato shadow_report -> book_report -> book_execute: se ONLINE ma posizione
  IGNOTA, l'esecutore NON opera a cieco (return + alert), come gia' per 'online'.

Opzione B (diagnostica equity, no halt):
- shadow._equity/shadow_report: se ONLINE ma equity reale non leggibile, il book
  ripiega su paper_cap (~$2000) invece del conto reale (~$598) -> sovradimensiona
  ~33%, ma l'hard-cap $300/asset limita il downside. Nuovo flag eq_fallback ->
  book_execute stampa warning + alert Telegram MA prosegue (niente gate: la scelta
  e' solo diagnostica, l'hard-cap gia' protegge).

Test: +8 (4 gate A, 4 diagnostica B). Suite 156/156. Path live pulito
(skh_error/pos_error/eq_fallback = None). Diario 2026-07-01-book-live-error-surfacing.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 22:39:59 +00:00
Adriano Dal Pastro 31369b358c fix(book): esponi skh_error nel book live — niente flat SKH silenzioso
book_report scriveva skh_error in un dict locale MAI incluso nel return ->
r.get("skh_error") era sempre None. E book_execute non lo leggeva comunque.
Risultato: se il feed SKH (fresh_5m) fallisse, il book forzava SKH a flat in
silenzio, indistinguibile da un flat legittimo -> entry SKH reale mancato,
nessun alert.

Fix:
- src/live/book.py: skh_error come variabile esplicita, esposta nel dict di
  ritorno (None normalmente). Il fail-safe (SKH->flat, TP01 indipendente sul
  suo feed certificato) resta invariato.
- scripts/live/book_execute.py: emette la riga di log + alert Telegram quando
  skh_error e' presente.
- tests: book_report cattura-e-flagga l'errore; book_execute lo fa emergere
  (log + notify). Suite 148/148.

Contesto: verifica del gate SKH01 dopo 193 run flat dal 06-23 (gate SANO —
335/341 entry storici, shorta i crash; flat legittimo: ultimo entry 06-06
chiuso ~06-08 pre-arming). Questo chiude il rischio latente scoperto durante
la verifica.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 22:26:02 +00:00
Adriano Dal Pastro 88f5f5a02a docs(memory): CLAUDE.md — completa il log dei filoni 2026-06-29 (1ª ondata A/C/D)
Aggiunge il bullet riassuntivo dei filoni della prima ondata del 29-06 (gia' nei diari
ma mancanti dal log curato in CLAUDE.md): A DVOL-direzionale=HEDGE, B intraday ERM=falso
positivo (gia' coperto dal gate), C xsec-v2 low-vol=debole/STAT-MODE, D macro-gate=ridondante.
Memoria curata ora completa per la sessione 2026-06-29.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 21:29:37 +00:00
Adriano Dal Pastro ee82e0a056 feat(dashboard): box forward-monitor STATARB-RESID (lead ortogonale+eseguibile)
Aggiunge il forward-monitor STATARB-RESID al dashboard accanto a PREVDAY (sezione
③·c FORWARD-MONITOR): carica data/paper_statarb/state.json, mostra doppio libro
MODELED/REAL-$600, ret/maxDD/fill-haircut, posizione spread corrente e giorni forward.
Warn aggiornato: LEAD sotto deflated-Sharpe -> forward per confermare l'edge, non deploy.

Smoke-test html() OK (box presente, dati live). Test 146/146.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 21:20:56 +00:00
Adriano Dal Pastro c52e0ab3f8 feat(forward): cabla STATARB-RESID nel forward-monitor PAPER (lead ortogonale ETH/BTC)
Forward-monitor del LEAD dello sweep 2026-06-29 (relative-value ETH/BTC, dollar-neutral 2 gambe),
il primo stream insieme ORTOGONALE (corr->book 0.027, beta-mkt 0.013) ED eseguibile a $600.

- scripts/live/paper_statarb.py: forward-only, doppio libro MODELED($2000)/REAL-$600 (haircut fill),
  riusa il segnale ESATTO di orthogonal_signals.py (niente reimplementazione). Config CONGELATA
  W=45 sgn=+1.
- Cablato in scripts/cron_daily.sh accanto a paper_prevday. Stato runtime in data/paper_statarb/
  (gitignored).
- test tests/test_paper_statarb.py (frozen config + advance forward/idempotente + haircut $600 basso).

Correzione di etichetta (verificata): la cella vincente e' sgn=+1 -> NON mean-reversion ma
relative-MOMENTUM sul residuo (dislocazioni ETH-vs-BTC continuano a 1d; sgn=-1 perde -1.4 IS).
Diario + CLAUDE.md aggiornati. Test 146/146. Nessun deploy, forward-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 20:58:29 +00:00
Adriano Dal Pastro cff5fa2bf5 research(sweep): 5 thread paralleli — 0 nuovi sleeve, STATARB-RESID LEAD ortogonale+eseguibile
Ricerca onesta su aree inesplorate (harness altlib+xsec_v2_nonmom, tutti i gate incl.
study_family_honest anti-selection-on-holdout). Branch main, nessun impatto live, test 143/143.

1 XSEC low-risk cousins (MAX/idio-vol/Amihud) -> 1 LEAD (IVOL), STAT-MODE, DSR 0.37<0.95
2 XSEC momentum-structure vs XS01            -> tutto REDUNDANT (sostituire XS01 distrugge hold)
3 Meta-allocazione dinamica (4 sleeve)       -> pesi fissi vincono (gia quasi risk-parity)
4 Segnali ortogonali ETH/BTC (2 gambe)       -> STATARB-RESID + DVOLSPREAD LEAD
5 1-gamba a segnale (MACD/RSI/Supertrend/...) -> 0/12 earns_slot (trend=TP01, MR morta, hedge)

LEAD principale STATARB-RESID (mean-rev residuo ETH-b*BTC, OLS rolling, 2 gambe): primo stream
INSIEME ortogonale (corr->book 0.027, beta-mkt 0.013) ED eseguibile a $600 (haircut ~0, NON
STAT-MODE) -> cadono i 2 muri di XS01/opzioni. Resta solo il muro dell'edge (Sharpe 0.84,
DSR 0.929 same-sign <0.95). Causalita+fee verificate dal coordinatore. Forward-monitor, non sleeve.

Soffitto direzionale ~1.3 riconfermato. Diario 2026-06-29-strategy-search-5threads.md, CLAUDE.md agg.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 20:50:33 +00:00
Adriano Dal Pastro 02d875b07b merge(research): equity IB screeners + 4 filoni crypto 2026-06-29 + gate selection-on-holdout
Ricerca onesta, branch separato (nessun impatto sul book live). Test 119/119 verdi.

Equity (IB/Yahoo):
- scalping 'sottoquotate' su IB + check dati dalla rete (forward, edge non provato)
- screener short 'fond/news neg ma prezzo su' (forward, edge non provato)

Crypto BTC/ETH + universo HL (harness altlib, causale, fee 0.10% RT, marginal vs TP01):
- A DVOL direzionale  -> LEAD hedge/DD-dampener, NON sleeve
- B Intraday ERM      -> falso positivo: earns_slot da selezione-sull'hold-out + coda 2026
- C Cross-sectional non-mom (low-vol HL) -> DEBOLE/forward-monitor (deflated-Sh 0.13) STAT-MODE
- D Macro regime-gate -> RIDONDANTE col trend (corr->TP01 0.989), SCARTATO

Harness indurito (LESSON 4 in altlib): deflated_sharpe + select_cell_insample +
study_family_honest -> chiude il punto cieco "selection-on-holdout".
2026-06-29 20:09:18 +00:00
Adriano Dal Pastro b4ec92734c research(intraday): de-bias del lead ERM (filone B) — falso positivo + gate selection-on-holdout
L'analisi di robustezza affonda lo "earns_slot=True" di ERM: era prodotto da
selezione-sull'hold-out + coda 2026 + multiple-testing non corretto.
  A) deflated-Sharpe FAIL: 0.00 (tutti 122 trial) / 0.16 (no-TOD) / 0.24 (solo-ERM) << 0.95
  B) selezione in-sample-only -> ALTRA cella (long-flat, corr->TP01 0.53) = NEUTRAL, no slot
  C) ensemble del plateau (no cherry-pick) -> ADDS ma robust_oos=False -> no slot
  D) uplift FULL solo +0.10, negativo 2021/2022; uplift HOLD +0.30 concentrato nel 2026
=> ERM SCARTATO come sleeve. Conferma ennesima del soffitto BTC/ETH-direzionale ~1.3.

Lezione CODIFICATA in altlib (LESSON 4, test in tests/test_harness_realism.py):
  - deflated_sharpe()       Bailey & Lopez de Prado, PASS >= 0.95
  - select_cell_insample()  scelta cella col solo Sharpe pre-HOLDOUT (no peeking)
  - study_family_honest()   gate combinato: earns_slot[cella in-sample] AND DSR>=0.95
Regola: una strategia direzionale grid-searched si giudica con study_family_honest,
non chiamando study_marginal sulla cella a max hold-out. Verificato end-to-end su ERM
(earns_slot_honest=False). Chiude il punto cieco gemello di CC01.

Diario aggiornato (verdetto downgrade), CLAUDE.md aggiornato. Test 119/119 verdi.
Nessun impatto live (branch separato).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 20:04:36 +00:00
Adriano Dal Pastro aad69f9790 research(crypto): 4 filoni 2026-06-29 — ERM lead sub-daily (forward), 3 scartati/deboli
Ricerca onesta su BTC/ETH + universo HL, branch separato (nessun impatto live).
Harness condiviso altlib (causale, fee 0.10% RT, marginal vs TP01, day-boundary,
haircut $600). Test 19/19 verdi.

- A DVOL direzionale  -> LEAD hedge/DD-dampener, NON sleeve (buy-the-fear; is_hedge).
- B Intraday ERM 8h   -> LEAD forte / forward-monitor: earns_slot=True, ADDS oltre
                         SKH01 (TP01+SKH+ERM 60/25/15 FULL 1.88/HOLD 1.46/DD 8.9%).
                         Caveat: plateau hold-out single-row, multiple-testing non
                         deflazionato, exec 8h. Controllo TOD = FAIL atteso.
- C Cross-sectional non-mom (low-vol HL) -> DEBOLE/forward-monitor (deflated-Sh 0.13,
                         storia 2.5a, non eseguibile $600) STAT-MODE.
- D Macro regime-gate -> RIDONDANTE col trend (corr->TP01 0.989), SCARTATO.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 19:48:36 +00:00
Adriano Dal Pastro a158d0e2ae research(equity): screener short 'fond/news neg ma prezzo su' (forward, edge non provato)
Goal chiarito: short su titoli con fondamentali/notizie negativi ma prezzo in salita.

Gate dati (v2.0.0): NON backtestabile -> fondamentali da rete = snapshot correnti,
applicarli a prezzi passati = look-ahead. Come la term-structure: solo screener forward.

Costruito screener da dati di rete tokenless: fondamentali strutturati Yahoo
(quoteSummary via crumb: recommendationMean/surprise/revGrowth/sell-skew) + sentiment
headline + momentum 1m/3m. SHORT cand = (fond/news neg) AND prezzo su. Run live oggi:
nessun candidato (mercato in flessione ampia -> gamba 'prezzo su' non scatta).

Intuizione chiave: shortare la FORZA combatte momentum + PEAD; il rialzo 'malgrado'
brutte notizie spesso prezza info che i fondamentali trailing non hanno -> e' la
versione contrarian/rischiosa dell'anomalia. La versione pulita = short su fondamentali
deboli quando il prezzo CONFERMA (scende), non quando diverge.

Eseguibilita': borrow/squeeze/perdita illimitata/PDT 5k/IB instabile/00 -> non deployabile.

- scripts/research/eq_fundnews_short.py (+ forward log data/raw/fundnews_short_screen.parquet)
- tests/test_eq_fundnews_short.py (scoring puro offline)
- docs/diary/2026-06-26-equity-fundnews-short.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 19:51:22 +00:00
Adriano Dal Pastro e28176efef research(equity): scalping 'sottoquotate' su IB + check dati dalla rete
Goal: comprare azioni IB quando sottoquotate, con verifica incrociata dei dati.

1) CHECK DATI DALLA RETE (il pezzo richiesto): IB certificato (eq_*, ADJUSTED) vs
   Yahoo adjclose (sorgente rete indipendente, tokenless). Tutti CONCORDE <=1.2bps
   sui rendimenti. Teaching: un confronto naif (adjusted vs grezzo) falso-allarmava
   4/6 ticker a 30-52bps = TUTTO stacco dividendo -> ogni divergenza va SPIEGATA
   prima di gridare 'feed sporco' (v2.0.0). Strumento riutilizzabile (validatore feed
   / pre-trade price-check).

2) Scalping 'sottoquotate': NON testabile (no dati intraday) ne' eseguibile (PDT rule
   5k blocca il day-trading; IB instabile). Versione testabile = swing MR (Connors
   RSI2<10 + MA200, exit MA5): Sharpe modesto (SPY 0.75/0.70) ma NON batte il buy&hold
   (B&H hold 0.81), expo 13%, CAGR 2-5%, fee-sensitive. Niente edge schierabile.

- scripts/research/eq_meanrev_ib.py
- tests/test_eq_meanrev.py (incl. network check)
- docs/diary/2026-06-26-equity-meanrev-network-check.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 19:42:14 +00:00
Adriano Dal Pastro 8d559c6f33 merge(research): gamma-scalp + cash-carry + TP01×DVOL + vol-termstructure
Analisi onesta di 5 angoli (tutti su dati certificati, leak-free, netto fee,
scoring marginale vs TP01, vincoli eseguibilita' a $600):
- gamma scalping (scalp+opzioni)  -> SCARTATO (specchio del VRP, perde ogni freq)
- funding cross-sectional (FC01)  -> gia' morto (DILUTES)
- cash-and-carry (CC01)           -> premio reale ma Sharpe artefatto, STAT-MODE -> lead
- TP01 x DVOL vol-targeting       -> non migliora (de-levering, non timing)
- calendar-vol / term-structure   -> dato storico non pubblico -> forward logger (gia' su main)

Zero impatto sul codice live: solo script di ricerca + test + diari + note CLAUDE.md.
Il forward logger era gia' stato cablato in cron su main (555977d).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 19:28:54 +00:00
Adriano Dal Pastro 555977d987 feat(research): forward logger della vol term-structure + cron giornaliero
La storia per-scadenza non e' pubblica su Deribit -> calendar-vol non backtestabile
ora (data-first gate). Unica via: costruire il dato in avanti. Aggiunto logger
idempotente (interpola ATM IV a tenor fissi {7,30,60,90,180}g -> data/raw/vol_term_*)
+ wrapper cron giornaliero. SOLO ricerca forward: non tocca il book live ne' i dati
certificati. Analisi completa (gamma scalp/cash-carry/dvol/feasibility) sul branch
research/gamma-scalp-options.

- scripts/research/log_vol_termstructure.py + probe_vol_termstructure.py
- scripts/cron_vol_term.sh
- tests/test_vol_termstructure.py

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 19:27:15 +00:00
Adriano Dal Pastro 8c3868cb31 research(vol): calendar-vol non backtestabile — data-first gate + forward logger
Angolo term-structure DVOL / calendar-vol. Scan di fattibilita' PRIMA del backtest:
la storia per-scadenza NON e' pubblica su Deribit (DVOL solo 30g; trade-history IV
solo per strumenti vivi; il front rotola/espira -> serie continua front-vs-back
irricostruibile). Per la metodologia (niente edge senza OOS su dati certi) -> STOP,
niente backtest su uno snapshot.

Unica via legittima: costruire il dato IN AVANTI. Aggiunto logger forward idempotente
che interpola l'ATM IV a tenor fissi {7,30,60,90,180}g e accumula data/raw/vol_term_*.
Seminati i primi snapshot (oggi: contango lieve su BTC/ETH). Non auto-cablato in cron.

- scripts/research/probe_vol_termstructure.py (scan fattibilita')
- scripts/research/log_vol_termstructure.py (forward dataset builder)
- tests/test_vol_termstructure.py (interpolazione offline)
- docs/diary/2026-06-26-vol-termstructure-feasibility.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 19:18:59 +00:00
Adriano Dal Pastro 9d586eeb58 research(tp01): DVOL vol-targeting non migliora TP01 (de-levering, non timing)
Angolo ESEGUIBILE (tocca il book live): DVOL (vol implicita forward-looking) come
denominatore del vol-target invece della realizzata. Finestra comune 2021-2026.

Le varianti DVOL abbassano DD (12.3->9.2%) ma anche Sharpe FULL (0.75->0.70) e CAGR.
Controllo decisivo: realized @ vol-tgt 15% eguaglia quel DD (9.4%) a Sharpe piu' alto
(0.75) -> il taglio di DD del DVOL e' solo DE-LEVERING, replicabile meglio con un
target_vol piu' basso. Hold-out +0.06 = single-window (storia DVOL <5y), sotto la
soglia multi-cut. Gate DVOL-spike ridondante col trend (TP01 gia' flat nei crash).

Lezione: per meno DD sul live la leva e' target_vol, non un overlay DVOL.

- scripts/research/tp01_dvol_overlay.py (realized/dvol/blend/max/derisk + controllo target_vol)
- tests/test_tp01_dvol_overlay.py
- docs/diary/2026-06-26-tp01-dvol-overlay.md
- CLAUDE.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 18:56:56 +00:00
Adriano Dal Pastro 69e3517f1b research(carry): cash-and-carry CC01 — premio reale, Sharpe artefatto, non deployabile
Provato l'angolo basis/cash-and-carry (l'altro proposto; il funding cross-sectional
FC01 era gia' SCARTATO 2026-06-22). Delta-neutral long-spot/short-perp -> r=funding,
zero esposizione prezzo.

Premio di funding REALE (~+8-14%/anno aggregato, positivo ogni anno in-sample,
ortogonale a TP01). MA Sharpe modellato 11-13 = ARTEFATTO: rischi di coda fuori
dal dataset (manca il 2022; procyclico +23% 2024 -> +1.7% 2026; liquidazione/slippage
non modellati). Il mark-to-market della base (premium col) sgonfia solo 13->11 ->
il basis-from-data non e' il rischio vero. NON eseguibile a $600 (spot+perp, funding
HL non Deribit) -> STAT-MODE. LEAD da rivedere a scala, non uno sleeve.

Sottoprodotto: CC01 passa ogni gate del marginal scorer -> punto cieco (manca un
gate 'Sharpe implausibile -> rischio nascosto').

- scripts/research/cash_carry_hl.py (CC-static/gated, BTC-ETH + 19-major, basis MtM, reality-check)
- tests/test_cash_carry.py (lock del fatto economico, non dello Sharpe)
- docs/diary/2026-06-26-cash-carry-hl.md
- CLAUDE.md: riga nella sintesi di ricerca

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 18:46:14 +00:00
Adriano Dal Pastro cf72e395d3 research(options): gamma scalping (long-vol) — SCARTATO
"Scalping BTC/ETH con copertura in opzioni" = gamma scalping, lo specchio
esatto del VRP01 (long straddle ATM + delta-hedge -> incassa RV-IV).

Esito: perde ogni anno / ogni variante / ogni frequenza (Sharpe -3 a -6).
Diagnostica strutturale: a 1d IV>=RV (paghi il VRP); a 1h RV>IV gross ma il
rehedge orario paga 24x la fee di hedge -> variante peggiore. Marginale vs TP01
= DILUTES, non e' nemmeno un hedge. Muro eseguibilita': opzione BTC min $5968 >> $600.

Schiacciato tra due muri: rehedge lento = premio, veloce = fee -> nessuna
frequenza vince. Il VRP01 (lato short, gated) resta l'unico edge opzioni.

- scripts/research/options_gamma_scalp.py (harness: naked/cheap/rich, 1d+1h, diagnostica RV-IV)
- tests/test_gamma_scalp.py (lock della conclusione)
- docs/diary/2026-06-26-gamma-scalp-options.md
- CLAUDE.md: riga nella sintesi di ricerca

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 18:36:48 +00:00
Adriano Dal Pastro db738bce3b feat(live): arma il BOOK DERIBIT (TP01+SKH01 nettati in software)
Estende l'esecuzione live da TP01-only al book Deribit-only completo. TP01 e SKH01
tradano lo STESSO strumento (una sola posizione netta per conto su Deribit) -> netting
in software: un solo ordine/asset verso il target netto.

- src/live/book.py: target NETTO per asset = clamp(0.5*E*(0.75*tp_frac + 0.25*skh_sign), ±cap).
  Riusa current_target (TP01, causale) + _skyhook_positions (segno L/S, book 230m) + conto reale.
- src/live/execution.py: rebalance_signed() — reconcile CON SEGNO (long/short, flip via close+open,
  reduce reduce_only). La close resta sempre permessa (si esce da qualunque posizione).
- src/live/livefeed.py: fresh_5m() — feed 5m certificato + coda recente EFFIMERA da Deribit pubblico
  (stesso simbolo inverse, in-memory, NON scrive su disco -> dati certificati intatti; fallback al
  certificato su errore). Solo SKH01 ne ha bisogno (e' a 230m); TP01 e' giornaliero.
- scripts/live/book_execute.py: executor doppio-gate (config + --execute), disaster-SL on-book sulla
  posizione netta, log in data/live/book_executions.jsonl. Feed SKH fresco (live_feed=True).
- scripts/cron_book.sh + crontab ORARIO: book idempotente ogni ora (riconcilia al netto corrente);
  rimossa la riga live_execute.py (TP01-only) dal cron daily per non far collidere i due.
- config/live.json: ARMATO (execution_enabled=true). cap/asset $300 split 75/25, disaster-SL -30%.
  Tutto flat all'arming -> nessun ordine finche' un segnale non arma.
- tests/test_book_live.py: 20 test (sizing 75/25, cap, flip/close/reduce, parita' pesi backtest,
  gate-safety, reconcile con trader fittizio, merge/dedup feed + fallback, loader onorato). 76/76 pass.

CAVEAT: exit SKH SOFTWARE (latenza fino a fine barra 230m; solo disaster-SL on-book); TP01 scende a
peso 0.75 (max $225/asset); SKH01 resta research-grade (equity daily-step, margine DD ETH sottile).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 21:43:27 +00:00
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
372 changed files with 58027 additions and 60 deletions
+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
+15
View File
@@ -6,6 +6,8 @@ build/
.venv/
.env
!.env.example
.env.ibgw
!.env.ibgw.example
.vscode/
.idea/
.DS_Store
@@ -52,3 +54,16 @@ logs/
# 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/
data/paper_statarb/
+271 -7
View File
@@ -13,7 +13,13 @@ Cosa è cambiato:
**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
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.
### Ricerca post-reset (2026-06-19) — esito
@@ -34,6 +40,14 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis
Deploy/paper a **1d**. Diari `2026-06-19-tp01-verification.md` / `-tp01-lookahead-fix-lf.md`.
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`.
⚠️ **ANCHOR TIMING-LUCK (2026-07-02, confermato da scettico):** l'hold-out ~0.31 è calcolato
sull'ancora daily 00:00 UTC, che è la **migliore delle 24 possibili** (mediana ancore 0.04, banda
[0.13,+0.30]; P~0.86 che una qualsiasi ancora mostri uno spike così per puro caso) → l'hold-out
2025-26 NON risolve l'edge di ritorno di TP01; ciò che regge a OGNI ancora è il **taglio del DD**
(7-10% vs ~60% B&H). FULL/plateau/deflated-Sharpe/gate INVARIATI (h=0 al 31° pctl su FULL).
Regola: i futuri numeri hold-out di strategie a ribilanciamento ancorato si citano con la banda
d'ancora. Diario `2026-07-02-timing-crt-wave.md`; script `scripts/research/r0702_tp01_offset.py`
+ `r0702_skeptic_offset.py`.
- **XS01 Cross-Sectional Momentum (Hyperliquid) — DIVERSIFICATORE che migliora il portafoglio** —
`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
@@ -43,13 +57,55 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis
regime compatto). Standalone FULL Sh **1.50** / HOLD 1.71 / DD 11%, plateau robusto (lookback, gate
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).
⚠️ **PHASE TIMING-LUCK (2026-07-02):** i numeri headline sono sulla fase 0 del ciclo H=10, che è
al **15° pctl di DD** (10.8% vs ~15.5% fase tipica, 29% peggiore) e 85° di FULL fra le 10 fasi
(HOLD solo 65°, non estremo); P(spike per caso)≈0.91-0.94. Lens onesta = **ensemble di fase:
FULL 1.25 / HOLD 1.31 / DD 10.9%**; a fase mediana FULL 1.08/HOLD 1.10/DD 21%. La decisione di
ammissione @15% regge (0 fasi negative, 8/10 FULL≥1.0), i numeri 1.50/1.71/11% no → citarli con
banda di fase. Ora-del-giorno NON testabile (solo 1d HL). Script `scripts/research/r0702_anchor_xs01.py`;
diario `2026-07-02-anchor-audit-xs01-skh01.md`.
Ricerca `scripts/portfolio/{xsec_research,xsec_blend,xsec_dispgate}.py`. Diari `2026-06-19-hyperliquid-xsec`
/ `-xsec-blend` / `-xsec-dispgate` / `-xsec-universe-expansion` / `-trend-multiasset`.
- **PORTAFOGLIO ATTIVO = TP01 (55%) + XS01 (25%) + VRP01 (20%)** (`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). Report `scripts/portfolio/run_portfolio.py`. Sleeve a date d'inizio
diverse → outer-join con pesi rinormalizzati (TP01 da solo 2019-20, VRP dal 2021, blend pieno dal 2024).
- **PORTAFOGLIO ATTIVO = TP01 (33%) + XS01 (15%) + VRP01 (12%) + SKH01 (20%) + GTAA01 (20%)**
(`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%. **Aggiunto SKH01-V2-DD @25% effettivo (2026-06-23, sotto)**: 4-sleeve
**FULL Sharpe 1.68→2.13, HOLD-OUT 1.63→2.30, DD full 14.3%→7.8%** (Skyhook quasi-ortogonale,
corr ~0.09). **Aggiunto GTAA01 @20% effettivo (2026-07-01, i 4 preesistenti scalati ×0.80):**
trend difensivo equity 6-ETF su IB (`src/portfolio/gtaa.py`, ~30 anni storia, validato 2026-06-22
su OOS equity 2015+ INDIPENDENTE dall'hold-out crypto, corr al book ~+0.10) → 5-sleeve
**FULL Sharpe 2.12→2.24, HOLD-OUT 2.21→2.46, DD full 7.8%→6.2%** (costo dichiarato: CAGR full
23.3→18.8%; 2022 unico anno con dSh). Uplift positivo in-sample E su tutte le finestre disgiunte
(vs EW-STR refutato lo stesso giorno). Convenzioni: weekend/festivi equity = 0.0 (capitale IB
fermo, non riciclato); attivazione nel book all'era crypto 2019-03; **il book live Deribit
(`deribit_book_sleeves` TP01+SKH01 75/25) NON lo include** (GTAA in paper_combo dal 2026-06-23).
Test `tests/test_gtaa_sleeve.py`; diario `2026-07-01-strategy-wave-6threads.md` (addendum GTAA).
Report `scripts/portfolio/run_portfolio.py`. Sleeve a date d'inizio diverse → outer-join con pesi
rinormalizzati (TP01/SKH01/GTAA dal 2019*, VRP dal 2021, XS dal 2024; *GTAA troncato all'era book).
⚠️ **ANCHOR-LUCK del book (2026-07-02):** l'HOLD 2.46 è calcolato con TUTTI gli sleeve ancorati
alla loro ancora canonica, che per TP01/XS01/SKH01 è al top della rispettiva banda (eredità di
fortuna ~+0.10/+0.17/+0.5 HOLD). **Stima de-luckata onesta: HOLD ~1.9-2.1, FULL ~2.0-2.2, DD ~6%
invariato** — il book resta positivo e diversificato a ogni ancora testata, ma 2.46 è un massimo
di configurazioni d'ancora, non la stima centrale. Diario `2026-07-02-anchor-audit-xs01-skh01.md`.
- **SKH01-V2-DD "Skyhook" — DIVERSIFICATORE quasi-ortogonale (research)** — `src/strategies/skyhook.SKH01_V2_DD`,
sleeve `src/portfolio/sleeves._skyhook_returns`. Sistema dual-TF (segnale 690m / exec 230m) regime
(BuzVola/BuzVolume tipo-Chande) AND pattern (Donchian breakout), NON trend-follower, L/S. Vincitrice
di 2 onde multi-agente (la 2ª = DD-reduction): exit a **percentuale fissa ASIMMETRICA** (long sl4%/tp10%,
short sl2%/tp8% più stretto) → standalone **maxDD BTC 21% / ETH 27% (<30%)**, minFull +0.99, minHold
+1.26, causale (0/400), fee-surviving 0.40%RT. Marginal vs TP01 **ADDS** (corr 0.09, has_insample_edge,
robust_oos multicut 7/7, is_hedge=False); blend 0.75·TP01+0.25·SKH **hold-out 0.31→1.17**. Verificato
leak-free + 2 scettici. **CAVEAT:** equity daily-step (Sharpe lens), ETH DD margine sottile, book 230m
(costi ribilanciamento da verificare a deploy) → research win, forward-monitor. Diario `2026-06-23-skyhook.md`.
⚠️ **GRID TIMING-LUCK (2026-07-02, più forte di TP01):** i numeri headline sono sull'offset 0 della
griglia 230m/690m, al **93-98° pctl dei 23 offset a priori** — minHold +1.26, blend 1.17 e book
HOLD 2.44 sono il MASSIMO dei 23 (mediane: minHold +0.39, blend 0.72, book 1.96); spike, non
plateau (±30m crolla); P(spike)≈0.70. **Il gate DD<30% (criterio di selezione di V2-DD) fallisce
in 15/23 offset** (mediana ETH 29.2%). Regge de-luckato: uplift blend positivo a TUTTE le 23 fasi
(min +0.18, med +0.42) + corr 0.05-0.11 → ADDS sopravvive ridimensionato. **LIVE (SKH=25% del book
Deribit):** path reale cron orario + exit software → book 50/50 FULL 1.46→1.19 / HOLD 1.64→1.15 /
DD 18→25%; nei crash gap-through-stop reale (sl2% modellato → 11/23% realizzato). Pesi/book
INVARIATI (ogni cambio passa weights_tilt_null); follow-up: cadenza 230m, peso live da rivedere.
Script `scripts/research/r0702_anchor_skh01.py`; diario `2026-07-02-anchor-audit-xs01-skh01.md`.
- **VRP01 Options Short-Vol — DIVERSIFICATORE da FinanceOld/OptionsAgent** — `src/portfolio/sleeves._vrp_combo_returns`.
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
@@ -59,6 +115,22 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis
crash incluso). Scorrelato a TP01 (~+0.01-0.07). **CAVEAT:** premio MODELLATO su DVOL ATM (skew non
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`.
⚠️ **ANCHOR-AUDIT CHIUSO + ondata "migliora e proteggi" (2026-07-03, 7 filoni + 2 lenti + scettico):
VRP01 NON è migliorabile e la protezione DD si compra SOLO con la size.** (a) **Anchor-luck (ciclo
settimanale, 7 fasi): PRIMO sleeve SENZA firma di luck** — la fase canonica è la PEGGIORE delle 7 su
FULL (1.09 = 7° pctl) e su DD (11.8% = 93° pctl), mediana su HOLD (0.59); spike bootstrap NEGATIVO →
i numeri di ammissione FULL 1.10/HOLD 0.60/DD 12% sono CONSERVATIVI, non gonfiati. Da ora si citano
con banda: ShFULL [1.09,1.83], ShHOLD [0.03,1.11], DD [5.7,11.8%]; edge OOS f-dipendente (f=0.8 →
HOLD~0). **Con questo l'audit anchor è completo su 4/4 sleeve ancorati.** (b) Griglia 288 strutture:
nessuna batte VRP01 (DSR 0.000; metà griglia = 3ª occorrenza "0-perdite/Sharpe implausibile" dopo
CC01/ALB-A → gate `implausible_sharpe` alzato di priorità). (c) 4 overlay DD (exit-spike/SL-MTM/
ala-coda/cooldown): 4/4 REFUTED dal null de-levering — la protezione crash vive già nel gate
d'ingresso IV-rank. (d) Gate nuovi: 4° fallimento su 4 (l'alpha è il binario IV-rank>0.30). (e)
Sizing: 12% deploy ≈ 0.27 Kelly onesto (anti-rovina); ⚠️ NON confondere col 12% di PESO del book
(~0.014 Kelly, fattore 19x). (f) Gate term-structure VIX/VXV su SPX (ΔSh +0.90, DSR 0.992) =
**confound di modello al 100%** (la var del gate coincide con l'errore BS-flat vs term-structure) →
nuova regola: riprezzare term-structure-consistent prima di credere a un gate vol su strutture
BS-flat. Book/pesi INVARIATI. Diario `2026-07-03-vrp-improve-dd.md`; script `scripts/research/r0703_vrpimp_*.py` (7 file).
Diario `2026-06-20-financeold-analysis-vrp-v2.md`.
- **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
@@ -78,6 +150,163 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis
regime-luck), calendar/seasonality (buy&hold travestito), volume/vol e momentum-reversal (negativi).
- **MORTO/confermato artefatto:** mean-reversion / fade (negativo anche a fee zero — la vecchia
libreria +201%/+1238% era contaminazione); trend 5m/15m (fee).
- **GAMMA SCALPING (long-vol) "scalping BTC/ETH con copertura in opzioni" — SCARTATO (2026-06-26)** —
`scripts/research/options_gamma_scalp.py`, test `tests/test_gamma_scalp.py`. È lo **specchio
esatto del VRP01** (long straddle ATM + delta-hedge: incassa **RVIV**, dove VRP01 incassa IVRV).
Perde **ogni anno, ogni variante, ogni frequenza** (Sharpe 3 a 6; nudo/cheap-gated/rich-skip;
rehedge 1d e 1h). Diagnostica strutturale: a 1d IV≈o>RV (BTC +4.9pp) → paghi il VRP; a 1h RV>IV
gross ma (a) gonfiata da microstruttura, (b) il rehedge orario paga **24× la fee di hedge**
variante *peggiore* (6). Marginale vs TP01 = **DILUTES**, non è nemmeno hedge (perde sia TP01-up
sia TP01-down). Muro eseguibilità: opzione BTC min $5.968 ≫ $600. Schiacciato tra due muri:
rehedge lento = premio, veloce = fee → **nessuna frequenza vince.** Regola gemella del VRP:
*niente long-vol scalp da modello in deploy*. Il VRP01 (lato short, gated) resta l'unico edge
opzioni — funziona perché sta sul lato *giusto* dello stesso premio. Diario `2026-06-26-gamma-scalp-options.md`.
- **CASH-AND-CARRY (basis trade) "CC01" — premio REALE, Sharpe ARTEFATTO, NON deployabile (2026-06-26)** —
`scripts/research/cash_carry_hl.py`, test `tests/test_cash_carry.py`. Diverso da FC01 (funding
cross-sectional, già scartato): qui delta-neutral long-spot/short-perp sullo *stesso* asset →
ritorno ≈ **+funding** (zero esposizione prezzo). Il premio di funding è **reale** (~+8-14%/anno
aggregato, positivo ogni anno in-sample, ortogonale a TP01 corr ~0.05). MA lo **Sharpe modellato
11-13 (DD 0.3%) è un ARTEFATTO**: il modello cattura solo il cashflow liscio del funding e i
rischi di coda sono **strutturalmente fuori dal dataset** — (1) storico funding dal 2023-05 →
**manca il 2022** (deleveraging, funding , basis blow-out); (2) **procyclico** (carry +23% toro
2024 → +1.7% bear 2026, si comprime quando servirebbe); (3) liquidazione short/slippage non
modellati. Il mark-to-market della base (`premium` col → r=funding−Δpremium) sgonfia lo Sharpe
solo 13→11 → il basis-from-data NON è il rischio vero. Sharpe reale di un basis-trade ~1-3 con
code brusche. **NON eseguibile a $600** (spot+perp = 4-38 gambe, funding HL non Deribit) → STAT-MODE.
**LEAD da rivedere a scala (~20k+ e venue con funding eseguibile), non uno sleeve.** Sottoprodotto:
CC01 passa OGNI gate del marginal scorer → **punto cieco** (manca un gate "Sharpe implausibile →
rischio nascosto"; prossima indurita raccomandata). Diario `2026-06-26-cash-carry-hl.md`.
- **TP01 × DVOL vol-targeting — NON migliora (2026-06-26)** — `scripts/research/tp01_dvol_overlay.py`,
test `tests/test_tp01_dvol_overlay.py`. Angolo ESEGUIBILE (tocca il book live, non STAT-MODE):
usare il DVOL (vol implicita forward-looking) come denominatore del vol-target di TP01 invece
della vol realizzata. Su finestra comune 2021-2026: le varianti DVOL abbassano il DD (12.3%→9.2%)
ma anche Sharpe FULL (0.75→0.70) e CAGR (8%→6%). **Controllo decisivo:** il realized a target_vol
RIDOTTO (15%) eguaglia quel DD (9.4%) a Sharpe **più alto** (0.75) → il taglio di DD del DVOL è
solo **de-levering**, replicabile meglio con un semplice target_vol più basso. L'unico residuo
(hold-out +0.06) è single-window (storia DVOL <5 anni) → sotto la soglia multi-cut. Il gate
DVOL-spike de-risk è **ridondante col trend** (TP01 già flat nei crash, Δ 0.00). **Lezione: per meno
DD sul live la leva è `target_vol`, non un overlay DVOL** (20% resta canonico). Diario `2026-06-26-tp01-dvol-overlay.md`.
- **Filoni 2026-06-29 (1ª ondata) — tutti scartati/forward.** (A) **DVOL-DIREZIONALE** standalone BTC/ETH
(buy-the-fear / IV-RV come segnale di livello): l'unico edge è un **HEDGE** (is_hedge=True, paga solo
quando TP01 è debole), non alpha → earns_slot=False, forward-monitor come DD-dampener (diario
`2026-06-29-dvol-directional.md`). (B) **INTRADAY ERM** (efficiency-ratio regime momentum sub-daily):
**falso positivo da selezione-sull'hold-out** → SCARTATO (vedi gate SELECTION-ON-HOLDOUT;
`2026-06-29-intraday-regime.md`). (C) **XSEC-V2 NON-MOMENTUM** su HL (reversal/idio-reversal/low-vol/BAB):
solo LOWVOL 19-major regge standalone (FULL/HOLD 1.07) ma deflated-Sharpe 0.13 + storia 2.5a → **DEBOLE/
forward STAT-MODE** (`2026-06-29-xsec-v2-nonmom.md`). (D) **MACRO regime-gate** (equity/credito/oro/tassi →
de-risk crypto): **RIDONDANTE col trend** (corr→TP01 0.989; il gate lavora solo nel 2-3% dei giorni, TP01
già flat nei crash) → SCARTATO (`2026-06-29-macro-regime-gate.md`).
- **Sweep strategie a 5 thread (2026-06-29) — 0 nuovi sleeve, 1 LEAD che rompe 2 muri su 3.** Ricerca
parallela onesta su aree inesplorate (harness `altlib`+`xsec_v2_nonmom`, tutti i gate incl. il nuovo
`study_family_honest`): (1) low-risk cross-sectional, (2) momentum-structure vs XS01, (3) meta-allocazione
dinamica, (4) segnali ortogonali ETH/BTC, (5) 1-gamba a segnale. Esito: soffitto ~1.3 riconfermato; ogni
candidato ucciso dal gate giusto (deflated-Sharpe, is_hedge, selection-on-holdout, sostituzione-XS01,
multi-cut). Niente batte/diversifica XS01 (varianti = REDUNDANT); meta-allocazione < pesi fissi (i 4
sleeve già quasi-risk-parity); 1-gamba a segnale = TP01 travestito (trend) o hedge a DSR<0.95.
**LEAD forward-monitor:** **STATARB-RESID** (relative-MOMENTUM del residuo ETH−β·BTC, β OLS rolling, 2 gambe,
cella vincente **sgn=+1**: le dislocazioni ETH-vs-BTC CONTINUANO a 1d, la MR pura sgn=1 perde) — primo
stream **insieme ortogonale (corr→book 0.027, β-mkt 0.013) ED eseguibile a $600** (haircut ~0, NON
STAT-MODE come XS01/opzioni): marginal ADDS, robust_oos, fee-survive 0.30%/gamba; resta sotto soglia solo
sull'**edge** (Sharpe 0.84, DSR 0.929 same-sign <0.95). **CABLATO in forward-monitor PAPER:**
`scripts/live/paper_statarb.py` (W=45/sgn=+1 congelati, doppio libro MODELED/REAL-$600), nel cron
giornaliero accanto a `paper_prevday`; test `tests/test_paper_statarb.py`. Se la finestra forward conferma
l'edge è *deployabile* (2 gambe BTC/ETH perp). Altri LEAD: IVOL (idio-vol XS, STAT-MODE), DVOLSPREAD
(storia DVOL corta). Diario `2026-06-29-strategy-search-5threads.md`. Script
`scripts/research/{xsec_v3_lowrisk,xsec_v3_momstruct,meta_allocation,orthogonal_signals,signal_inout_1leg}.py`.
- **Ondata 2026-07-01 (6 filoni multi-agente + scettico) — 0 edge nuovi dai filoni, 1 gate nuovo,
e 1 sleeve promosso DALL'ARCHIVIO (GTAA01, sotto nel bullet portafoglio).** Filoni su angoli non coperti dalle ondate precedenti: (1) **funding time-series**
BTC/ETH (posizionamento) = SCARTATO — FOLLOW è trend-beta ritardato, FADE shorta il toro, il gate è
TP01 travestito (DSR 0.215); **il filone funding è chiuso su 3 lati** (FC01 carry, price-clock, TS).
(2) **breadth/internals alt** (51 HL) = SCARTATO ma unico NON-ridondante col trend (corr→TP01 0.40);
muore su jackknife (uplift su 1 mese) + DSR 0.433 con ~8 mesi IS → **rivisitabile tra 1-2 anni di
storia HL nativa**. (3) **residual momentum XS** (β-hedged, 19 major) = REDUNDANT — cross-section la
residualizzazione è un no-op (lo z-score di XS01 rimuove già il mercato); l'edge resta solo nella
coppia ETH/BTC (STATARB-RESID). (4) **ri-ottimizzazione pesi + guardia-DD**: il candidato EW-STR
(TP30/XS25/VRP15/SKH30, HOLD 2.21→2.35) **refutato dallo scettico come selezione-sull'hold-out di 2°
ORDINE** — SKH01/XS01 furono ammessi/affinati *perché* forti su quell'hold-out; pre-2025 ΔSh 0.05,
finestre disgiunte 0.12/+0.06/+0.14, percentile 94-100° fra 500 tilt casuali ≈ firma best-of-15.
Guardia-DD 5%/0.5: inerte OOS (la diversificazione fa già il lavoro; solo circuit-breaker d'emergenza).
(5) **affinamento VRP01** = NON MIGLIORA (l'alpha è tutto nel gate binario IV-rank; gate TP01 =
trappola in-sample; **3° fallimento → filone "VRP dentro il modello" esaurito** fino a f di stress
reale). (6) **stagionalità cross-sectional HL** = morta allo step statistico (null permutato).
**GATE nuovo codificato: `weights_tilt_null`** in `src/portfolio/portfolio.py` (+ `combine_outer`
riusabile): ogni proposta di CAMBIO PESI si giudica vs il null dei tilt casuali cap-respecting —
gate_pass solo se delta_insample≥0 E percentile < firma best-of-k (necessario, non sufficiente);
test `tests/test_weights_tilt_null.py`. ⚠️ Lezione tecnica: `DatetimeIndex.view("int64")` su indici
tz-aware non-ns (pandas 2.x) → scala sbagliata → `merge_asof` broadcasta = **look-ahead che
`causality_ok` non vede**; usare epoca esplicita in ms (altlib verificato pulito). Diario di sintesi
`2026-07-01-strategy-wave-6threads.md` + 6 diari di filone; script `scripts/research/r0701_*.py`.
- **Ondata 2026-07-02 (TIMING + CRT, 8 filoni multi-agente + scettico) — 0 nuovi sleeve, 1 finding
strutturale (anchor timing-luck di TP01, vedi ⚠️ nel bullet TP01).** Goal: "strategie con timing
differenti". (1) **Event-clock bars** (volume/vol/range da 5m, TSMOM/Donchian/EWMA in tempo-informazione):
batte il wall-clock a pari segnale/frequenza solo in 4/45 coppie; cella best IS 1.45 → HOLD 0.46,
NEUTRAL (corr 0.74 = trend travestito) → SCARTATO: il clock non è dove vive l'edge. (2) **Calendario
scadenze Deribit** (expiry weekly/monthly/quarterly ven 08:00 UTC): 0/24 celle a Bonferroni; il drift
post-expiry monthly fallisce placebo-weekday e permutation e si INVERTE sul quarterly (dove l'OI massimo
dovrebbe amplificarlo); unico pattern robusto = gio→ven negativo, ma è day-of-week (SEA morta) a Sharpe
netto ~0 → SCARTATO. (3) **Anchor timing-luck TP01 + tranching**: finding confermato (dettagli nel
bullet TP01); tranching K=2/4 = sola riduzione della varianza della STIMA (ΔSharpe n.s., ΔDD ~0.5pt),
NO deploy a $600 (il min-order lo degenera in K=1; serve feed intraday fuori path certificato) —
rivalutare a ≥5-10k. **Audit d'ancora ESEGUITO su XS01 e SKH01 (stesso giorno): il finding si
replica su 3/3 sleeve ancorati** — vedi ⚠️ nei rispettivi bullet e la stima de-luckata del book
nel bullet portafoglio; diario `2026-07-02-anchor-audit-xs01-skh01.md`.
(4) **Clock lenti (2-7g) + bande isteresi**: fee drag di TP01 = ~0.4%/anno = tetto di ogni risparmio;
il lag costa più del risparmio (HOLD ensemble 0.34→0.11 da N=2 a 7); a $600 **il min-order $5 è GIÀ la
banda ottimale** (ordini 74% a costo ~0) → nessun cambio al book. (5) **Velocità trend
regime-condizionata** (pesi tra orizzonti 30/90/180g vs percentile vol RV/DVOL): pctl 0.71 vs null
pesi-statici-casuali = tilt-30d statico travestito (trappola EW-STR); pesi canonici 1/3 confermati →
SCARTATO. (6-8) **CRT "Candle Range Theory"** (sweep-and-reclaim 3 candele, mai coperto da MRV/MIC):
base 864 trial DSR 0.000 + anchor-flip + short "smart-money" negativo perfino in-sample; multi-TF
(4h→15m, 1h→5m, ~10k trade) expectancy negativa ovunque anche a fee zero, e **il ritest è informazione
negativa** (pattern con-ritest 40bps vs senza +52bps: aspettarlo seleziona i peggiori); contesto
(FVG/equal-highs/sessioni, 22 trial) non salva il fade, cella "Asia" = artefatto anchor-flip →
SCARTATO 3/3. Sottoprodotto: sugli stessi livelli prior-day **FOLLOW > FADE ogni anno 2019-26**
(conferma indipendente del lead prevday in forward-monitor). Lezione: il timing-luck d'ancora è
multiple-testing che il deflated-Sharpe NON conta (candidato gate futuro `anchor_luck_band`).
Diario `2026-07-02-timing-crt-wave.md`; script `scripts/research/r0702_*.py` (9 file).
- **Ondata 2026-07-02-bis ("video claims": Elliott 3 filoni + Albimarini 2 + capital scaling) — 0 nuovi
sleeve, 0 forward-monitor, 1 azione config pendente sul deposito.** Meccanizzazione onesta di claim da
video didattici: (1) **Elliott range-cycle** (onda1 compressa→onda3 ampia): rumore, 0/24 celle a
Bonferroni, nessuna cella weekly regge a tutte le 7 ancore → SCARTATO. (2) **Confluenza Fibonacci**:
vs null ingenui sembra buona (pctl 0.82-1.00), vs null **location-matched** (Fib±jitter: "0.618 vs
0.58?") crolla a 0.39-0.68 = l'apparente edge è la POSIZIONE dei livelli, non i numeri; confluenza
FAIL 4/4 → SCARTATO (il null location-matched è IL test per ogni claim su livelli "speciali").
(3) **Tecnica del canale Elliott**: Donchian travestito — non batte il Donchian a pari geometria
(corr 0.43-0.53), DSR 0.685, cella in-sample collassa in hold-out (1.40→−0.87), target 1.618 = caso
(5/6 celle), anchor-luck di nuovo (4h banda hold [0.35,1.54], 00:00 la migliore) → SCARTATO.
(4) **Albimarini double-diagonal** (short T + long T+1, deep OTM, via motore DVOL di VRP01): il
condor stessa-scadenza la batte a ogni f (la long T+1 = assicurazione di coda +12/33bps nel tail,
~1bps costo medio, non dominanza); senza gate IV-rank TUTTE le strutture perdono (3ª conferma:
l'alpha del VRP è il gate); su Deribit fee-negativa a QUALSIASI size (fee 8 gambe = 194-221% del
theta); celle deep-OTM 0-perdite/142 trade = 2° caso "Sharpe implausibile" dopo CC01 → gate
`implausible_sharpe` raccomandato con più forza. VRP01 resta superiore su tutta la banda skew →
nessun LEAD. (5) **Audit claims** (28 trade, 82% win, PF 5.16, "420%/anno"): consistente con ZERO
skill (P=20-45%; il 78.6% delle finestre 6-mesi 1996-2026 lo produce); replay con code reali =
rovina 1998/2002/2020 col sizing dichiarato; la diagonale lascia passare il 12-40% della perdita
naked. (6) **Capital scaling 600→2-5k** (`r0702_capital_scaling.py`): l'unico vincolo binding è
`max_notional_per_asset_usd=300` (a 5k il book live girerebbe al 49% del target) → **al deposito
alzare il cap a equity/2** ($1000/$1750/$2500 a 2k/3.5k/5k); min_order $5 da LASCIARE; tranching
K=2 non cablare (blocco feed intraday); opzioni ETH eseguibili da ~2.6k ma la regola no-short-vol-
da-modello non decade col capitale; XS01 ~20k confermata, CC01 fuori per struttura. Aspettativa
onesta col CAGR de-luckato (10-15%): 2k ≈ €0.6-0.8/g, 5k ≈ €1.4-2/g (€50/g resta ≈130k).
⚠️ Lezione pandas: `resample("7D", origin=...)` IGNORA origin (pandas 2.x, solo RuntimeWarning) →
bande d'ancora weekly finte; usare `"168h"`. Diario `2026-07-02-elliott-albimarini-capital.md`;
script `scripts/research/r0702_{ell_*,alb_*,capital_scaling}.py` (6 file).
- **Video-claim "CRT top-down multi-TF, 74% win rate" — SCARTATO (2026-07-07): il 74% è un KNOB, non
un edge.** `scripts/research/r0707_crt_topdown.py`. Metodo ICT/SMC top-down (H1 setup CRT → M15
struttura/FVG → M5 conferma → M1 entry) con uscita parziale-70/80%-a-1.5R + break-even + runner verso
prev-daily high/low. Il setup H1 CRT è **già triplo-refutato** (onda 2026-07-02: base DSR 0.000, MTF
expectancy neg. ovunque "il ritest è informazione negativa", contesto/FVG peggiora, fade<follow) →
qui testato l'UNICO angolo nuovo: la **gestione d'uscita** e il **claim 74%**. Su BTC/ETH certificati
(H1→5m/15m; M1 non nel feed): **WR reale 3037%** a RR 1.52 (SOTTO il null gambler's-ruin 40% =
P(+1.5R prima di 1R)=1/(1+1.5) → il ritest tocca il target MENO di una moneta), **expectancy R
negativa a ogni schema/fee/finestra/asset** (1.2…−3.3R netto). **Il 74% si fabbrica avvicinando il
target**: sweep rr1 0.5→2.0 mostra WR salire (51→32%) con expectancy R INVARIANTE (WR alto = target
vicino, non direzione). DSR 0.000 (48 trial); a **fee 0** expR 0.10/Sh 0.63 → **non è morte-per-fee,
l'edge lordo non c'è** (residuo = beta di trend dei time-exit); parziale+BE+runner = 3-4 ordini/trade,
alcuni sub-min-order a $600. **Regola candidata:** il win-rate di uno schema parziale+BE non è merito
(≈1/(1+rr1)); convertire ogni claim "WR X%" in **expectancy R netto fee** prima di crederci (helper
`winrate_is_a_knob()`). Diario `2026-07-07-crt-topdown-74winrate.md`. Book/pesi INVARIATI.
- **Soffitto strutturale BTC/ETH-direzionale ~1.3** superato SOLO espandendo a un meccanismo diverso:
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.**
@@ -100,6 +329,41 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis
(marginal==ADDS)`. **Regola: una nuova strategia direzionale si giudica su `earns_slot`, non sullo
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.
- **SELECTION-ON-HOLDOUT gate (codificato 2026-06-29, filone B intraday ERM)** — terzo gate in
`altlib.py`, test `tests/test_harness_realism.py`. Il lead ERM faceva `earns_slot=True` MA lo script
di scoperta sceglieva la cella per **`min_hold` massimo** su 60+ celle = **selezione-sull'hold-out**:
scegliendola in-sample-only ne esce un'altra (trend-beta corr→TP01 0.53, NEUTRAL) e il deflated-Sharpe
crolla (DSR 0.0-0.24 su 122 trial). `study_marginal` da solo non lo vede (giudica UNO stream, non *come*
è scelto). Tre funzioni: **`deflated_sharpe()`** (Bailey & Lopez de Prado, PASS ≥0.95), **`select_cell_insample()`**
(cella scelta col solo Sharpe pre-HOLDOUT), e il gate combinato **`study_family_honest(name, factory, grid, tfs)`**
`earns_slot_honest = earns_slot[cella in-sample] AND deflated-Sharpe≥0.95`. **Regola: una strategia
direzionale grid-searched si giudica con `study_family_honest`, non chiamando `study_marginal` sulla
cella a max hold-out.** Chiude il punto cieco gemello di CC01 ("Sharpe implausibile"). Diario
`2026-06-29-intraday-regime.md` (analisi `scripts/research/intraday_regime_analysis.py`).
- **Onestà sul target €50/giorno:** NON raggiungibile su 2000 in 1-2 anni (servono ~130k di
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.
@@ -128,7 +392,7 @@ src/strategies/indicators.py → indicatori condivisi (ema, atr, keltner, ...)
src/strategies/trend_portfolio.py → TP01: strategia DIFENSIVA robusta (PORT LF1d, >=12h), causale
src/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
sleeves.py → REGISTRY sleeve attivi: TP01 33 / XS01 15 / VRP01 12 / SKH01 20 / GTAA01 20. Aggiungere = una riga
src/fractal/ → indicatori frattali (patterns.py, indicators.py, similarity.py)
src/backtest/engine.py → engine di backtesting riusabile
src/backtest/harness.py → harness ONESTO (load BTC/ETH, backtest_signals no-leakage, OOS)
+3 -1
View File
@@ -1,7 +1,9 @@
{
"_nota": "Config esecuzione LIVE di TP01. execution_enabled=true + --execute -> ordini REALI. ARMATO 2026-06-20.",
"_nota": "Config esecuzione LIVE del BOOK DERIBIT (TP01+SKH01 nettati in software). execution_enabled=true + --execute -> ordini REALI. ARMATO 2026-06-23: esecutore scripts/live/book_execute.py via cron ORARIO scripts/cron_book.sh (SKH01 e' a 230m). disaster-SL on-book -30% sulla posizione netta. Tutto flat all'arming -> nessun ordine finche' un segnale non arma.",
"_nota_cap": "Cap notional per-asset DINAMICO (frontiera 2026-07-03): con max_notional_per_asset_frac=0.5 il cap = equity/2, cosi' cresce col capitale e un deposito non resta strozzato. A ~$600 equity/2=~$300 -> INERTE (identico al vecchio cap fisso). Il cap dinamico si usa solo con equity reale fidata; su fallback/offline si ripiega su max_notional_per_asset_usd (protezione downside). Per tornare al cap fisso: rimuovere max_notional_per_asset_frac.",
"execution_enabled": true,
"max_notional_per_asset_usd": 300,
"max_notional_per_asset_frac": 0.5,
"min_order_usd": 5,
"disaster_sl_pct": 0.30
}
+17
View File
@@ -13,3 +13,20 @@ services:
# 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
@@ -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.
+69
View File
@@ -0,0 +1,69 @@
# 2026-06-26 — Cash-and-carry (basis trade) delta-neutral su HL (CC01): premio REALE, Sharpe ARTEFATTO, NON deployabile
**Goal utente:** dopo lo scarto del gamma scalping, "provare" gli altri due angoli proposti —
funding-carry cross-sectional e basis/cash-and-carry. Il **funding cross-sectional (FC01) è già
SCARTATO** (diario 2026-06-22: DILUTES, sign-flip su un nome). L'angolo non testato è il
**cash-and-carry delta-neutral (CC01)**. Branch `research/gamma-scalp-options`,
script `scripts/research/cash_carry_hl.py`.
## Meccanismo (diverso da FC01)
FC01 pickava cross-section (short alto-funding / long basso-funding) → scommessa relativa, fragile.
CC01 **non picka**: harvesta il premio di funding AGGREGATO restando delta-neutral sullo *stesso*
asset. Long spot + short perp (stessa size): le gambe lineari nel prezzo si cancellano → ritorno
**+funding** (lo short incassa il funding quando f>0), zero esposizione direzionale. Dati: funding
orario reale HL dal 2023-05 (`hlfund_*_1h.parquet`), gli stessi 19 major di XS01.
## Risultato grezzo — e perché NON va creduto
| config | Sharpe | CAGR | maxDD | per-anno |
|---|---|---|---|---|
| BTC/ETH CC-static | **+12.9** | +13% | 0.8% | 2023:+10 2024:+23 2025:+8 2026:+1% |
| BTC/ETH CC-gated | +13.9 | +14% | 0.3% | tutti positivi |
| 19-major CC-gated | +11.6 | +16% | 0.4% | tutti positivi |
**Uno Sharpe di 13 con DD 0.3% è un ALLARME, non un edge.** È la firma di un modello che cattura
solo un cashflow liscio (il funding) e azzera la volatilità reale. Il marginal scorer dà **ADDS /
robust_oos / has_insample_edge=True / multicut_persistent** su OGNI config — ma questo **espone un
punto cieco dello scorer**: si fida della vol riportata dal candidato e non ha un gate "Sharpe
implausibile → rischio nascosto". Un Sharpe di 13 dovrebbe squalificare-e-indagare, non passare.
## Il rischio è NASCOSTO, non assente
- **Mark-to-market della base** (`premium` col → `r = funding Δpremium`): sgonfia lo Sharpe solo
13→11. La colonna premium di HL è già lisciata/clampata → a risoluzione daily il basis-vol è
minimo. **Quindi il basis-from-data NON è il rischio vero.**
- **I rischi che giustificherebbero uno Sharpe realistico (~1-3) sono ASSENTI dal dataset:**
1. **2022 mancante.** Lo storico funding parte 2023-05 → niente deleveraging LUNA/3AC/FTX, il
regime di funding profondamente negativo + basis blow-out che farebbe il vero drawdown.
2. **Procyclicità.** Funding aggregato BTC/ETH: +23% nel toro 2024 → **+1.7% nel bear 2026**
(e 19-major CC-static 2026 = **1%**). Solo 10% di giorni a funding negativo *in-sample* — ma
in un vero bear quella frazione esplode. Il carry **non è all-weather**: è un risk-on premium.
3. **Liquidazione dello short** in uno squeeze e **slippage** su spot+perp: non modellati.
- **Sharpe reale di un crypto basis-trade ≈ 1-3 con code brusche** (Mar-2020, Mag-2021, FTX), non 13.
## Eseguibilità a $600 — muro indipendente
Serve spot **+** perp per gamba: BTC/ETH = 4 posizioni, 19-major = 38. Su Deribit (dove operiamo) lo
storico funding è **bloccato** (404) e non tradiamo spot HL. **STAT-MODE**, come XS01/VRP01 ma peggio
(doppia gamba per asset). Non deployabile a questo capitale.
## Verdetto
**CC01 = premio di funding REALE (~+8-14%/anno aggregato, positivo ogni anno in-sample) e
genuinamente ortogonale a TP01 (corr ~0.05, NON direzionale).** È la "fonte di ritorno diversa" più
concreta trovata finora. MA:
- lo **Sharpe headline è fiction** (rischi di coda strutturalmente fuori dal dataset);
- è **procyclico** (si comprime/inverte nel bear — proprio quando servirebbe);
- **non eseguibile** a $600 / sul nostro venue.
**LEAD da rivedere a scala** (capitale ~20k+ e un venue con funding eseguibile), **non uno sleeve**.
Nessun impatto sul book live (branch separato).
## Sottoprodotto metodologico (raccomandazione)
CC01 ha passato ogni gate del marginal scorer pur essendo un'illusione di rischio. **Prossima
indurita dello scorer:** un gate di *plausibilità dello Sharpe* — uno standalone Sharpe ≫ 4 deve
declassare a "SUSPECT/INVESTIGATE" invece di alimentare ADDS, perché segnala vol sottostimata
(mark-to-cashflow, dati che escludono il regime di stress, leva nascosta). Documentato qui, non
implementato in questa passata per non toccare lo scorer condiviso su `main`.
@@ -0,0 +1,58 @@
# 2026-06-26 — "Short su fondamentali/notizie NEG ma prezzo SU": screener forward, edge non provato
**Goal utente (chiarito):** se i dati finanziari/notizie di un'azienda sono negativi ma la
quotazione è positiva (sale), andare SHORT — scommessa che il prezzo scenda a riallinearsi ai
fondamentali. Branch `research/equity-meanrev-ib`, script `scripts/research/eq_fundnews_short.py`.
## Gate dati (lezione v2.0.0) — NON backtestabile
I fondamentali e le notizie scaricabili dalla rete (Yahoo) sono **snapshot CORRENTI**, non
point-in-time storici. Applicarli a prezzi passati = **look-ahead** (restatement + survivorship):
esattamente la classe di errore che ha prodotto la libreria fasulla v2.0.0. Un backtest onesto
richiede un DB point-in-time (Compustat PIT, news storiche con timestamp) — **assente**. Quindi,
come per la vol term-structure: **niente backtest**, l'unica via onesta è uno **screener forward**.
## Cosa è scaricabile (feasibility, fatto)
- **Fondamentali strutturati** via Yahoo quoteSummary (flusso cookie+**crumb**, tokenless):
`recommendationMean` (1=buy..5=sell), `revenueGrowth`, `earningsHistory` surprise%,
`recommendationTrend` (skew buy/sell). ✅
- **Notizie**: Yahoo news search (headline + publisher), sentiment lessicale crudo. ✅ (proxy debole)
- **Momentum**: chart API (return 1m/3m). ✅
## Screener costruito + eseguito dal vivo
`fund_neg` ∈ [0,1] (media di: recMean→sell, surprise negative, ricavi in calo, analisti a sell) +
`news_neg` lessicale + momentum. **SHORT candidate = (fond/news neg) AND prezzo in salita** (la
divergenza). Idempotente, logga forward su `data/raw/fundnews_short_screen.parquet`.
**Run di oggi (2026-06-26): NESSUN candidato.** Quasi tutto l'universo ha momentum 1m **negativo**
(mercato in flessione ampia, coerente col bear) → la gamba "prezzo che sale" non scatta. I peggiori
per fondamentali (TSLA 0.48, BA 0.39, NKE 0.38) stanno comunque *scendendo*. Output sensato.
## L'intuizione analitica chiave (il valore vero di questa analisi)
La premessa ha una **tensione interna** che la rende la versione *rischiosa* dell'anomalia:
- **Shortare un prezzo che SALE combatte il momentum** — una delle anomalie più robuste e persistenti
(i winner continuano a vincere su 3-12 mesi). Si scommette contro di essa.
- **Il PEAD** (post-earnings drift) dice che dopo cattive notizie il prezzo continua a **scendere**;
ma qui il prezzo *sale malgrado* le cattive notizie → spesso significa che il mercato sta
**prezzando informazione** che i fondamentali *trailing* non contengono (turnaround, M&A, squeeze).
- Quindi: la versione *pulita* dell'edge "short su fondamentali deboli" funziona meglio quando il
prezzo **conferma** (fondamentali deboli + prezzo che SCENDE = momentum e fondamentali allineati),
**non** quando diverge. La richiesta "shorta il titolo che sale su brutte notizie" è il caso
contrarian, più difficile e storicamente più punito (squeeze, perdita illimitata).
## Eseguibilità (muro)
Short = **borrow** (locate + fee; hard-to-borrow caro/assente), **perdita illimitata**, **squeeze**,
**PDT $25k** per i day-trade, IB instabile qui, $600, universo single-stock (non i nostri ETF). NON
deployabile.
## Verdetto
**Non un edge schierabile, né dimostrabile ora.** Deliverable onesto = lo **screener forward**
(funzionante, da dati di rete) + il log che accumula i candidati per una validazione futura, con
la chiara avvertenza che l'edge è **non provato** e che la *direzione contrarian* della scommessa
(short sulla forza) è la parte fragile. Strumento utile (fond+news+momentum da rete in un colpo),
tesi da maneggiare con scetticismo. Nessun impatto sul book live (branch separato).
@@ -0,0 +1,57 @@
# 2026-06-26 — Scalping azioni IB "quando sottoquotate" + check dati dalla rete
**Goal utente:** su IB, comprare azioni quando "sottoquotate" (oversold/sotto fair-value), con
verifica incrociata dei dati dalla rete. Branch `research/equity-meanrev-ib`,
script `scripts/research/eq_meanrev_ib.py`. Due pezzi.
## 1) CHECK DATI DALLA RETE — il pezzo richiesto (e perfettamente in-metodo)
Confronto i dati certificati IB (`data/raw/eq_*_1d.parquet`, ADJUSTED_LAST) con una **sorgente di
rete indipendente** (Yahoo Finance chart API, tokenless). È la lezione fondante del progetto: non
fidarti di un feed solo (il disastro v2.0.0 era un feed contaminato).
**Risultato (180g, BTC… no: 6 ETF):** dopo l'allineamento corretto **tutti CONCORDE**, deviazione
sui rendimenti **≤1.2bps**, ultimo close identico (0.0bps). Il feed IB è validato.
**La lezione (errore che ho fatto e corretto):** un primo confronto naïf (nostro close *adjusted*
vs `close` *grezzo* di Yahoo) ha **falso-allarmato 4/6 ticker** a 30-52bps (SPY 30, TLT 40, HYG 52).
Le divergenze erano **tutte su singoli giorni = stacco dividendo**: confrontavo dividend-adjusted vs
non-adjusted. Usando l'`adjclose` di Yahoo (apples-to-apples) → ~0bps. **Regola: ogni divergenza va
SPIEGATA prima di gridare "feed sporco" — e prima di tradarci sopra.** Lo strumento resta come
validatore di feed riutilizzabile / template del pre-trade price-check live.
## 2) "SCALPING quando sottoquotata" — non testabile/eseguibile; testato lo swing MR
Lo **scalping intraday non è valutabile**: (a) non abbiamo dati intraday (solo `eq_*_1d`), (b) non
è eseguibile — **PDT rule**: il day-trading di azioni US sotto **$25.000** è limitato a 3
day-trade/5gg → lo scalping è *regolatoriamente bloccato* al capitale del progetto (l'analogo equity
del muro STAT-MODE a $600 sul crypto), (c) IB Gateway è instabile qui (timeout ordini diagnosticato).
Versione onesta e testabile = **swing mean-reversion** "compra quando sottoquotata": Connors RSI2<10
+ filtro trend MA200, exit a MA5. Causale (segnale ≤ close[i], entry a close[i]), netto fee, hold-out 2015+:
| ETF | FULL Sh | DD | CAGR | expo | HOLD Sh | **B&H HOLD Sh** |
|---|---|---|---|---|---|---|
| SPY | +0.75 | 15% | +5% | 13% | +0.70 | **+0.81** |
| QQQ | +0.50 | 16% | +4% | 14% | +0.56 | **+0.92** |
| IWM | +0.32 | 22% | +2% | 13% | +0.15 | +0.53 |
| DIA | +0.38 | 13% | +2% | 14% | +0.55 | +0.74 |
| EEM | +0.41 | 20% | +3% | 13% | +0.36 | +0.46 |
Fee sweep SPY: Sh 0.82 (0bps) → 0.75 (3) → 0.70 (5) → 0.58 (10bps RT).
**Lettura onesta:** la mean-reversion "buy the dip" sugli indici equity ha un edge **reale ma
modesto** (Sharpe ~0.5-0.75, investito solo ~13% dei giorni) **che NON batte il buy&hold
risk-adjusted** (B&H hold-out 0.81 su SPY vs 0.70 della MR), con CAGR piccolo (2-5%) e sensibile ai
costi. È capital-efficient in teoria (cash 87% del tempo) ma il valore aggiunto vs "tieni l'indice"
è negativo o nullo, e la frequenza *scalping* che potrebbe aggiungere valore è bloccata dalla PDT.
## Verdetto
- **Check dati dalla rete: FATTO e utile** — feed IB validato (≤1.2bps vs Yahoo); strumento
riutilizzabile. Unico vero keeper della sessione equity.
- **Scalping "sottoquotate": NON deployabile** — non testabile (no intraday), non eseguibile (PDT
$25k, IB instabile). Lo swing MR daily testabile **non batte il buy&hold** → niente edge schierabile.
Conferma il pattern del progetto: l'idea promettente sopravvive solo finché non incontra
eseguibilità + benchmark onesto. Nessun impatto sul book live crypto (branch separato).
@@ -0,0 +1,74 @@
# 2026-06-26 — "Scalping BTC/ETH con copertura in opzioni" = gamma scalping → SCARTATO
**Domanda (goal utente):** analizzare lo scalping su BTC/ETH con copertura in opzioni come
possibile nuovo sleeve. **Esito: negativo, strutturale, su tutte le frequenze. Non è uno sleeve,
non è nemmeno un hedge.** Branch `research/gamma-scalp-options`, script
`scripts/research/options_gamma_scalp.py`.
## Cos'è davvero "scalping con copertura in opzioni"
L'interpretazione rigorosa è il **gamma scalping**: compri un'opzione (la *copertura* = long
gamma), delta-hedgi il sottostante a cadenza fissa (lo *scalp*), e il P&L netto è
≈ dollar-gamma × (vol realizzata² vol implicita²). È lo **specchio esatto del VRP01** (short-vol):
VRP01 incassa IVRV (positivo in media), il gamma scalping incassa **RVIV** (negativo in media).
Modello (mirror della struttura VRP per comparabilità): long straddle ATM, tenor 7g, IV = DVOL
Deribit (la stessa fonte del VRP), delta-hedge sui prezzi certificati, fee opzioni cap 12.5% del
premio + fee perp 0.05%/lato sull'hedge turnover, return-on-notional vol-targeted 20%.
## La diagnostica strutturale (il cuore)
| asset | IV (DVOL) | RV 1d | IVRV 1d | RV 1h | IVRV 1h |
|---|---|---|---|---|---|
| BTC | 60.9% | 56.0% | **+4.9pp** | 64.5% | 3.6pp |
| ETH | 74.9% | 76.0% | 1.0pp | 81.0% | 6.1pp |
- A **rehedge giornaliero** l'IV ≈ o > la RV → il long gamma **paga il VRP** (sei sul lato sbagliato
del premio). Perde gross, prima di ogni fee.
- A **rehedge orario** la RV *supera* l'IV (BTC +3.6pp, ETH +6.1pp gross) — sembrerebbe un edge…
**ma è una trappola doppia:** (a) la RV_1h è gonfiata da microstruttura (bid-ask bounce), quindi
il "gross edge" è in parte fittizio; (b) il rehedge orario paga **24× la fee di hedge**, che
spazza via qualunque edge intraday reale.
## I numeri (ogni variante, ogni anno, ogni frequenza: PERDE)
| variante | rehedge | Sharpe | CAGR | per-anno |
|---|---|---|---|---|
| NAKED | 1d | **3.99** | 56% | 2021..2026 tutti 25..68% |
| CHEAP-GATED (IVr<0.30) | 1d | 3.05 | 47% | tutti negativi |
| RICH-SKIP (IVr>0.90) | 1d | 3.92 | 55% | tutti negativi |
| NAKED | 1h | **6.06** | 71% | tutti 38..81% (peggio: le fee) |
| CHEAP-GATED (IVr<0.30) | 1h | 4.96 | 64% | tutti negativi |
Il gate "compra vol solo quando è a sconto" (lo specchio del gate vincente del VRP, *vendi vol solo
quando è cara*) **migliora ma non salva**: 3.05 invece di 3.99. Il segno resta negativo perché
l'IV-rank basso non garantisce RV > IV su 7 giorni.
## Scoring marginale vs TP01
Tutte le varianti: **DILUTES**, `is_hedge=False`, `has_insample_edge=False` (IS Sharpe 3..4.5),
blend-25 uplift hold 1.2/1.5. Non aiuta nemmeno come hedge: perde sia quando TP01 è su
(uplift 1.2..1.6) sia quando TP01 è giù (uplift 2.5..3.2). Corr a TP01 ≈ 0 ma il drift negativo
domina la matematica di diversificazione.
## Eseguibilità a ~$600 (muro indipendente)
- **BTC**: contratto 1 BTC, min 0.1 BTC → notional minimo **$5.968** ≫ $600 → **NON eseguibile**.
- **ETH**: min 0.1 ETH → **$157** → marginalmente OK, ma un solo straddle = ~25% del conto in premio.
Anche se l'edge esistesse, BTC è fuori e ETH è troppo grosso per diversificare.
## Conclusione
Lo "scalping con copertura in opzioni" è **schiacciato tra due muri**: rehedge lento → paghi il
premio (RV < IV); rehedge veloce → paghi le fee (24×/day). **Nessuna frequenza vince.** Conferma
due lezioni di prim'ordine del progetto, ora quantificate sul long-vol:
1. **Le fee uccidono lo scalping** (il rehedge orario, vera frequenza di scalping, è la variante
*peggiore*: 6 Sharpe).
2. **Vol-da-modello è fragile in entrambe le direzioni.** Regola gemella del VRP: *niente long-vol
scalp da modello in deploy*.
**Il VRP01 (lato short, gated, rischio-definito) resta l'unico edge opzioni del progetto**
e funziona proprio perché sta sul lato *giusto* dello stesso premio che il gamma scalp paga.
SCARTATO. Script preservato come riferimento. Nessun impatto sul book live (branch separato).
@@ -0,0 +1,66 @@
# 2026-06-26 — TP01 × DVOL vol-targeting: NON migliora (il taglio di DD è solo de-levering)
**Goal utente:** "continua" ad analizzare strategie. Dopo i tre angoli di diversificazione tutti
STAT-MODE (gamma scalp scartato, funding cross-sectional già morto, cash-and-carry lead non
deployabile), ho puntato sull'angolo a **più alto valore perché ESEGUIBILE**: migliorare il
risk-sizing del book **live** TP01 (BTC/ETH perp Deribit, già armato). Branch
`research/gamma-scalp-options`, script `scripts/research/tp01_dvol_overlay.py`.
## Ipotesi
TP01 vol-targeta sulla vol **realizzata** 30g (backward-looking). Il **DVOL** (vol implicita 30g
Deribit, forward-looking, che spesso anticipa i salti di vol) come denominatore → de-risking più
tempestivo prima dei crash → DD più basso / hold-out migliore senza affondare il FULL.
Onestà: DVOL parte 2021-03 → confronto TUTTE le varianti sulla **finestra comune 2021-04 → 2026**
(1891g, perdo il toro 2019-2020 pre-DVOL). Causale, fee 0.10% RT, config canonica TP01.
## Risultati (finestra comune, hold-out 2025+)
| variante (denominatore vol) | FULL Sh | FULL DD | CAGR | HOLD Sh |
|---|---|---|---|---|
| **realized** (baseline) | **+0.75** | 12.3% | +8% | +0.30 |
| dvol (implicita) | +0.70 | 9.3% | +6% | +0.33 |
| blend 50/50 | +0.72 | 10.4% | +7% | +0.32 |
| max(realiz, dvol) | +0.70 | 9.2% | +6% | +0.36 |
| derisk (gate DVOL>p90 ×0.5) | +0.75 | 12.3% | +8% | +0.30 |
Le varianti DVOL **abbassano il DD** (12.3% → ~9.2%) ma **anche** Sharpe FULL (0.04/0.05) e CAGR
(8% → 6%). Lo Sharpe è scale-invariant → sospetto: il taglio di DD è solo *posizioni più piccole*
(l'IV > RV per il VRP → denominatore più grande → leva minore), non vero timing.
## Il controllo decisivo
Confronto le varianti DVOL col **realized a target_vol RIDOTTO** (stesso de-levering, senza DVOL):
| | FULL Sh | DD | CAGR | HOLD Sh |
|---|---|---|---|---|
| **max-DVOL** | +0.70 | 9.2% | +6% | +0.36 |
| **realized @ vol-tgt 15%** | **+0.75** | 9.4% | +6% | +0.30 |
| **realized @ vol-tgt 13%** | +0.75 | **8.1%** | +5% | +0.30 |
**Conclusivo:** il realized a 15% eguaglia il DD del max-DVOL (9.4% ≈ 9.2%) a Sharpe **più alto**
(0.75 > 0.70) e stesso CAGR; a 13% fa DD ancora più basso (8.1%), sempre a 0.75. **Il taglio di DD
del DVOL è interamente "leva minore", replicabile MEGLIO con un semplice target_vol più basso.**
## Verdetto
**DVOL vol-targeting NON migliora TP01.** Risk-adjusted è marginalmente *peggiore* (FULL 0.70 vs
0.75 a parità di DD). L'unico residuo a favore — hold-out +0.06 (max 0.36 vs 0.30) — è **single-window**
su 5 anni scarsi di storia DVOL: sotto la soglia di **persistenza multi-cut** del progetto (la lezione
che ha ucciso 13/14 falsi positivi nello sweep alt). Il gate **DVOL-spike de-risk è RIDONDANTE col
trend**: quando il DVOL esplode (crash) TP01 è già flat (momentum negativo → posizione 0) → il gate
non tocca nulla (Δ 0.00 su tutto). Niente da deployare.
## Lezioni
1. **Per meno DD sul book live, la leva giusta è `target_vol`, non un overlay DVOL.** Abbassare TP01
da vol-tgt 20% → 15% taglia il DD 12.3% → ~9.4% allo **stesso Sharpe** (0.75). Dial pulito e noto,
se mai si volesse un profilo più difensivo (oggi 20% è la scelta canonica — invariata).
2. **La vol implicita non batte la realizzata come denominatore di sizing per un trend long-flat**:
il segnale di trend gestisce già il de-risking direzionale; il denominatore fissa solo la
magnitudine, e la realizzata lo fa marginalmente meglio per unità di rischio.
3. Conferma indiretta del soffitto: anche un raffinamento "ovvio" e *eseguibile* su TP01 non produce
alpha; al massimo sposta il punto sulla stessa frontiera rischio/rendimento.
SCARTATO come miglioria. Nessun impatto sul book live (branch separato, config canonica invariata).
@@ -0,0 +1,47 @@
# 2026-06-26 — Calendar-vol / term-structure DVOL: il DATO STORICO NON ESISTE (data-first gate)
**Goal utente:** "continua" — angolo scelto: scaricare e certificare la term-structure DVOL per
testare un calendar-vol (front IV vs back IV). **Esito: bloccato a monte dal gate "il dato prima
della strategia" (lezione v2.0.0).** Branch `research/gamma-scalp-options`.
Script: `scripts/research/probe_vol_termstructure.py` (scan) + `log_vol_termstructure.py` (forward).
## Scan di fattibilità (prima di costruire qualunque backtest)
Cosa l'API pubblica Deribit espone davvero per la vol per-scadenza:
| dato | disponibile? |
|---|---|
| Snapshot CORRENTE della term-structure ATM (mark_iv per scadenza) | **SÌ** (book_summary_by_currency, tokenless) |
| DVOL storico | solo **30g** fisso — nessun indice 7g/60g/90g pubblico |
| Trade-history IV per strumento | solo per strumenti **VIVI** (gli scaduti spariscono) |
| **Serie storica continua front-vs-back IV** | **NO** — il front-month rotola/espira → irricostruibile |
**Snapshot oggi (2026-06-26), per riferimento:**
- BTC spot $59.9k: 7g 45.6% · 30g 43.2% · 60g 42.7% · 90g 42.9% · 180g 44.5% — **contango** lieve.
- ETH spot $1.58k: 7g 57.6% · 30g 56.4% · 90g 57.5% · 180g 59.2% — contango (+1.6pp slope).
- (I valori sono interpolati a tenor fissi: il front a 1-giorno grezzo, 38%/54%, è rumore escluso.)
## Verdetto
**Un calendar-vol NON è backtestabile ora su dati certificati.** La storia per-scadenza non è
pubblica su Deribit (la nostra fonte di verità), e — coerentemente con la metodologia — **nessun
edge va creduto senza backtest OOS su dati certificati**. Costruire un calendar-vol su uno snapshot
singolo o su una storia ricostruita-a-pezzi sarebbe esattamente il tipo di scorciatoia che ha
prodotto la libreria fasulla v2.0.0. Quindi: **stop, niente backtest.**
## L'unica via legittima: costruire il dato in avanti
`scripts/research/log_vol_termstructure.py` — logger forward idempotente: ogni run prende lo snapshot
ATM, lo interpola ai tenor fissi {7,30,60,90,180}g e appende una riga/asset a
`data/raw/vol_term_<asset>.parquet`. Seminate le prime righe BTC/ETH oggi. Test offline
`tests/test_vol_termstructure.py` (interpolazione pura).
**Non auto-cablato in cron** (è ricerca forward, e non tocco il cron live senza ok). Per accumulare:
aggiungere una riga giornaliera al crontab. Dopo **~6-12 mesi** → certificare (monotonia, spike,
cross-venue) e SOLO ALLORA testare front-vs-back. Prima, nulla.
## Lezione
Il data-first gate ha funzionato: ha fermato un backtest su un dato che non esiste, invece di
produrre un numero non falsificabile. La term-structure è in **contango** oggi (regime calmo) —
osservazione da forward-monitor, non un edge. Nessun impatto sul book live (branch separato).
+118
View File
@@ -0,0 +1,118 @@
# 2026-06-29 — DVOL come segnale DIREZIONALE/REGIME su BTC/ETH: l'unico edge è un HEDGE, non alpha
**Filone A — "DVOL-direzionale".** Tesi: usare la vol IMPLICITA Deribit (DVOL, `data/raw/dvol_*.parquet`,
2021-03→oggi) come **segnale direzionale o di regime** sul perp BTC/ETH (long-flat o L/S sul LIVELLO di
mercato), non come denominatore del vol-target. Script `scripts/research/dvol_directional.py`, test
`tests/test_dvol_directional.py`. Branch `research/equity-meanrev-ib` (separato, nessun impatto live).
## Cosa è diverso dal lavoro DVOL precedente
- `tp01_dvol_overlay.py` (2026-06-26): DVOL come **denominatore** del vol-target → solo de-levering, SCARTATO.
- VOL03/04/10/11 (sweep alt 100-agent): DVOL che **gata/scala un TSMOM** → eredita lo Sharpe di trend di
TP01 (il marginal scorer li boccia NEUTRAL/REDUNDANT).
- `agent_14_dvol_spread` (onda ortho): IV **relativa BTC-vs-ETH**, market-NEUTRAL 2-leg (l'unico LEAD vivo).
Qui invece: DVOL/IV-RV come segnale **direzionale STANDALONE sul livello di mercato**, per vedere se porta
alpha direzionale **ortogonale** a TP01 (non un overlay sul trend). Tutto causale (decisione ≤ `close[i]`,
tenuta in `i+1` dallo shift di `eval_weights`), fee 0.10% RT, vol-target 20%, leva cap 2x, harness onesto
condiviso `altlib`. Allineamento DVOL: `al.dvol` (merge_asof backward) → DVOL close del giorno *t* (≈
contemporaneo a `close[i]`), noto a decisione; verificato leak-free (`causality_ok` max_tail_diff 0.0).
DVOL parte 2021-03 → pre-DVOL il segnale è flat. Valuto sia **FULL** (col flat pre-2021, deflaziona lo
Sharpe) sia **ERA-only** (da 2021-10, equo per la tesi).
## 1) Probe direzionale (corr segnale[i] vs ritorno[i+1], pooled BTC+ETH, era DVOL)
| segnale | corr | long bps | flat bps | **edge bps** | frac_long |
|---|---|---|---|---|---|
| VRP-Z+ (long VRP ricco, IV-RV alto) | +0.010 | +7.8 | 0.9 | **+8.6** | 0.52 |
| VRP-Z (flip) | 0.017 | 0.9 | +7.8 | 8.6 | 0.48 |
| **DVOL-LV fear (rank>0.5 → long)** | **+0.024** | +20.4 | +0.9 | **+19.5** | 0.14 |
| DVOL-LV calm (rank<0.5 → long) | 0.013 | +0.8 | +21.2 | 20.4 | 0.86 |
| DVOL-MOM (DVOL in calo → long) | 0.041 | 10.0 | +22.9 | **32.9** | 0.59 |
| VRP>0 (quasi buy&hold) | +0.003 | +5.3 | 0.7 | +5.9 | 0.72 |
Lettura: l'unico contenuto direzionale reale è **"buy-the-fear"** (long quando il DVOL è alto nel suo
percentile espandente: edge +19.5 bps il giorno dopo). Il VRP-ricco→long è coerente ma debolissimo (+8.6).
Il "DVOL in calo → long" è **negativo** (33 bps): comprare quando la paura scende è perdente (regola
falsificata). Nota: il percentile espandente è dominato dai picchi 2021-22 → "fear" scatta solo nel 14%
delle barre (gli spike veri), non metà del tempo.
## 2) Robustezza assoluta (study_weights 1d, fee sweep 0.00-0.20% RT) — ERA-only il giudizio equo
| segnale | abs | minFull Sh | minHold Sh | feeOK | **ERA Sh** | ERA DD | ERA CAGR |
|---|---|---|---|---|---|---|---|
| **DVOL-fear q0.4 LF** | WEAK | +0.39 | +0.14 | sì | **+0.59** | **8%** | +5% |
| VRP-Z60 LF | FAIL | +0.42 | 0.39 | sì | +0.46 | 23% | +7% |
| DVOL-fear q0.5 LF | FAIL | +0.31 | 0.17 | sì | +0.40 | 10% | +3% |
| VRP-Z90 LF | FAIL | +0.32 | 0.26 | sì | +0.32 | 29% | +4% |
| VRP-Z90 L/S | FAIL | +0.25 | +0.32 | sì | +0.31 | 28% | +3% |
| DVOL-calm q0.5 LF | FAIL | +0.00 | 1.05 | no | +0.09 | 37% | 0% |
| DVOL-MOM k10/k20 LF | FAIL | 0.6/0.7 | <1 | no | 0.6/0.7 | ~40% | ~10% |
| DVOL-MOM k10 L/S | FAIL | 1.16 | 1.29 | no | 1.23 | 72% | 23% |
Nessun segnale supera lo Sharpe assoluto in **valore** (tutti < 0.6, soffitto direzionale BTC/ETH ~1.3
confermato). Unico con profilo decente: **DVOL-fear q0.4 long-flat** (ERA Sh +0.59, DD **8%**) — il DD
bassissimo perché è long solo nei picchi di paura (poche barre, mean-reversion). Le famiglie momentum e
"buy-the-calm" sono nettamente negative e non sopravvivono nemmeno alle fee.
## 3) Marginal vs TP01 (il gate vero) — DVOL-fear q0.4 long-flat
| | corr→TP01 | in-sample Sh | uplift_full w25 | uplift_hold w25 | multi-cut persistente | is_hedge | verdetto |
|---|---|---|---|---|---|---|---|
| **ERA-only** (2021-10+) | 0.37 | 0.54 (has_edge) | +0.06 | +0.17 | sì | **No** | **ADDS** (earns_slot=True) |
| **FULL** (gate ufficiale) | 0.28 | 0.64 (has_edge) | +0.03 | +0.17 | sì | **Sì** | **HEDGE** (earns_slot=**False**) |
Il candidato ha tutto ciò che serve a un nuovo sleeve **tranne una cosa**: il gate canonico full-history
lo classifica **HEDGE**, non alpha. `hedge_yearly_corr(TP01-Sh, uplift) = 0.76`, `uplift TP01-up 0.003`
/ `uplift TP01-down +0.194`**paga SOLO quando TP01 è debole**. Ha perfettamente senso: il DVOL esplode
durante i crash, proprio quando il trend di TP01 è flat/in-drawdown; comprare la paura cattura il rimbalzo
e smorza il DD del portafoglio, ma **non è un premio permanente**. Il blend 0.75·TP01 + 0.25·cand alza
l'hold-out 0.30→**0.47** (w50 → 0.62) e taglia il DD a ~11%/8% — un effetto **drawdown-dampener** reale,
ma classificato (giustamente) come hedge.
Perché ERA dice ADDS e FULL dice HEDGE: il rivelatore di hedge richiede ≥3 anni; sulla finestra ERA
(2021-10+) la `corr(TP01-Sh annuale, uplift)` è meno netta e non scatta. **Il gate full-history è quello
giusto** (più dati per il test di hedge) → mi attengo a quello: **HEDGE, non alpha.**
Gli altri tre candidati (VRP-Z60/90, DVOL-fear q0.5): **NEUTRAL o HEDGE**, mai ADDS. I VRP-Z hanno beta a
TP01 alto (0.88-0.90) → sono trend-correlati travestiti (resid Sharpe ~0), uplift hold-out negativo.
## 4) Causalità, alignment, eseguibilità, sign-falsification
- **Causale**: `causality_ok` ok=True, max_tail_diff **0.0** su tutti i candidati (rank espandente +
z-score ricalcolati su prefisso = identici → nessun future-peeking). Test dedicato `test_expanding_rank_is_causal`.
- **Alignment DVOL**: laggando il segnale di +1 giorno (extra-conservativo, usa solo DVOL di ieri) l'ERA
Sharpe del leader scende +0.59 → **+0.45**. L'edge sopravvive ma è **sensibile all'allineamento**
(parte del vantaggio è nel reagire allo spike DVOL del giorno stesso) → cautela in esecuzione.
- **Eseguibile a $600**: `eval_weights_smallcap` (min_order $5, cap $300/asset): haircut Sharpe **≈ 0.00**
su BTC ed ETH (segnale long-flat a basso turnover, ~210-270 trade su tutta la storia) → lo Sharpe netto
onesto a $600 = quello modellato. È l'unico aspetto pienamente promosso.
- **Sign-falsification**: tesi buy-the-fear ERA Sh +0.40 vs flip buy-the-calm +0.09; VRP-Z+ +0.32 vs
VRP-Z (flip) 0.14. Il segno della tesi è quello giusto (l'edge è reale, non rumore simmetrico).
## Verdetto onesto: **LEAD (hedge / drawdown-dampener), NON sleeve.** SCARTATO come alpha direzionale.
- Nessun segnale DVOL-direzionale ha Sharpe assoluto sopra ~0.6 → **conferma il soffitto BTC/ETH
direzionale ~1.3** (la via per superarlo resta il cross-sectional XS01, non un nuovo segnale a 2 asset).
- L'unico con edge in-sample reale + eseguibile + leak-free è **DVOL-fear "buy-the-fear" long-flat**, ma il
gate canonico lo bolla **HEDGE** (`is_hedge=True`, paga solo quando TP01 è in difficoltà). Per la regola
del progetto un hedge **non si arma come slot di alpha****earns_slot=False**.
- È comunque un **LEAD utile in forward-monitor** come **smorzatore di drawdown** del portafoglio: il blend
porta l'hold-out 0.30→0.47/0.62 e abbassa il DD. Se in futuro si vorrà esplicitamente un tail/DD-hedge
*eseguibile* (a differenza del tail-hedge in opzioni, non eseguibile a $600), questo è il candidato più
pulito visto finora — ma da prezzare **come hedge**, non come ritorno atteso permanente.
## Caveat
1. **Storia DVOL <5 anni** (2021-03→oggi, ~4.7 anni; era valutata 2021-10+) → multiple-testing e
finestra corta. Il multi-cut 2026 è un assurdo +1.944 (manciata di giorni, picco DVOL recente):
l'uplift hold-out è trascinato dal regime 2025-26 ad alta-paura, non da un edge stazionario.
2. **È un hedge, non alpha**: il valore è condizionato ai drawdown di TP01 (2022, ed eventuali futuri).
In un toro lungo a bassa vol il segnale resta **flat l'86% del tempo** e non contribuisce nulla.
3. **Sensibile all'allineamento** DVOL (haircut 0.14 Sharpe col lag +1g): l'esecuzione reale dovrebbe
usare il DVOL già stampato a decisione, non assumere il close contemporaneo.
4. Long-flat → cattura il rimbalzo post-paura ma **non protegge** durante la discesa (non è uno short):
è "buy-the-dip gated by fear", con tutti i rischi del comprare coltelli che cadono se il regime peggiora.
Nessun impatto sul book live (branch separato, nessuno sleeve registrato, config canonica invariata).
+145
View File
@@ -0,0 +1,145 @@
# 2026-06-29 — Filone B: INTRADAY REGIME BTC/ETH — ERM (efficiency-ratio) è un LEAD reale, forward-monitor
**Tesi.** Cercare un meccanismo **sub-daily** sui dati certificati 1h→12h BTC/ETH che sia ortogonale
**sia** a TP01 (TSMOM trend daily, long-flat) **sia** a SKH01 (regime BuzVola/BuzVolume + Donchian a
230m). SKH01 prova che il sub-daily PUÒ funzionare ed essere quasi-ortogonale: qui un MECCANISMO DIVERSO,
basato sulla **qualità del moto intraday** (efficiency-ratio / vol-expansion / thrust) come **regime** che
condiziona una posizione direzionale tenuta ~1 giorno. Script `scripts/research/intraday_regime.py`, test
`tests/test_intraday_regime.py`. Branch `research/equity-meanrev-ib` (separato, nessun impatto live).
Killer ricorrente del progetto sotto le 12h = **muro-fee 0.10% RT + overfitting**. Ricetta SKH01:
decisione sub-daily ma **hold ~1 giorno** → pochi trade → la fee non uccide. Ogni meccanismo qui è
costruito a basso turnover e giudicato col **fee-sweep alla sua frequenza reale**.
## Meccanismi provati (tutti come posizione CONTINUA decisa ≤ `close[i]`, causali)
| | meccanismo |
|---|---|
| **ERM** | **Efficiency-Ratio regime momentum** (Kaufman): ER = \|moto netto su L barre\| / \|percorso\|. Prendi la direzione del moto netto **solo quando ER ≥ soglia** (regime intraday "pulito"/trendy), altrimenti flat |
| VEM | Vol-Expansion Momentum: direzione = segno del moto, attiva solo quando vol-corta > vol-lunga |
| VBR | Volatility/thrust breakout (Larry-Williams ROLLING, no calendario): segui i moti > k·ATR |
| TOD | Time-of-day seasonality — **CONTROLLO calendario**, incluso APPOSTA per `day_boundary_robust` |
## Selezione + fee-sweep a frequenza reale (vincitori per famiglia, min-asset)
```
ERM: 8h L=2.0 thr=0.35 L/S minFull +0.88 minHold +0.99 f@.20 +0.55 feeOK=True turn/y 125
VEM: 12h Lmom=3 Ls=2 Ll=10 minFull +0.72 minHold -0.29 f@.20 +0.51 feeOK=True (hold<0 → out)
VBR: 12h k=1.0 atr=14 L/S minFull +0.33 minHold +1.20 f@.20 +0.25 feeOK=True
TOD: 1h minFull -3.99 minHold -4.49 feeOK=False (controllo → FAIL atteso)
```
Solo **ERM** e **VBR** sopravvivono al gate fee+OOS. VEM ha hold-out negativo. TOD esplode (vedi sotto).
## Il candidato: ERM 8h L=2.0 thr=0.35 (L/S) — `earns_slot=True`
```
abs=PASS marginal=ADDS EARNS_SLOT=True
standalone: cand FULL 0.919 / HOLD 1.105 (TP01 ref FULL 1.297 / HOLD 0.303)
corr->TP01 full 0.146 hold 0.057 beta 0.197 resid Sharpe 0.738 alpha/yr 0.121
robust_oos=True (clean-year +0.103, drop-best-month +0.283)
multi-cut persistence: 2020 .166 / 2021 .167 / 2022 .298 / 2023 .337 / 2024 .418 / 2025 .497 / 2026 2.861 persistent=True
has_insample_edge=True (standalone PRE-holdout Sharpe 0.874, > null-pctl 0.317)
is_hedge=False (yearly corr(TP01-Sh, uplift) -0.762; paga sia TP01-up 0.139 sia TP01-down 0.384)
blend w25: FULL 1.481 (+0.184) HOLD 0.80 (+0.497) DD 11.0%
corr con SKH01: full 0.278 hold 0.291
day_boundary=INVARIANT (spread 0.0) haircut $600 ≈ 0.00 su BTC/ETH
```
È il **primo sub-daily da SKH01** a passare TUTTI i gate del marginal scorer indurito: edge in-sample
reale (non diversification-math), persistenza multi-cut, robust_oos, NON hedge, scorrelato a TP01 (0.15).
### ERM aggiunge OLTRE SKH01? (gate #5 — il book esistente ne ha già uno sub-daily)
| blend | FULL Sh | FULL DD | HOLD Sh | HOLD DD |
|---|---|---|---|---|
| TP01 | +1.30 | 14.3% | +0.30 | 7.5% |
| TP01+SKH 75/25 | +1.78 | 9.4% | +1.17 | 6.2% |
| **TP01+SKH+ERM 60/25/15** | **+1.88** | 8.9% | **+1.46** | 5.3% |
| TP01+SKH+ERM 55/20/25 | +1.84 | 8.8% | +1.48 | 5.1% |
Sì: corr con SKH01 solo 0.28 → ERM **aggiunge oltre SKH** (FULL +0.10, HOLD +0.29 a peso 15%, DD ancora
giù). Non è SKH01 travestito.
## Il controllo TOD (calendario) — fa esattamente ciò che doveva
`TOD` (direzione per ora-del-giorno, media espandente causale) è incluso come **trappola**: è il tipo di
effetto che uccise `open_drive` (artefatto di etichettatura UTC). Esito: **FAIL** (FULL 3.99, 31.811
trade, fee-killed), marginal=DILUTES. `day_boundary_robust=INVARIANT` → l'effetto è **robustamente
negativo** a ogni offset (non un artefatto di confine giorno: è proprio che la time-of-day-direzionale non
ha edge e sanguina fee). Il controllo conferma che l'harness non si fa ingannare e che il segno
dei segnali di prezzo (ERM/VBR) è reale, non rumore di calendario.
## Causalità / eseguibilità
- **Leak-free**: `causality_ok=True`, max_tail_diff **0.0** su tutti i candidati (ER, rank espandente,
medie su prefisso = identiche → nessun future-peeking). Test dedicato in `test_intraday_regime.py`.
- **day_boundary_robust=INVARIANT** (spread 0.0) per ERM/VBR/TOD: segnali di prezzo, non di calendario.
- **Eseguibile a $600**: `eval_weights_smallcap` haircut **≈ 0.00** su BTC ed ETH. **MA** ERM a 8h fa
~3.158 trade BTC / ~2.823 ETH su tutta la storia (turnover 125/anno): haircut nullo nel modello, ma
l'esecuzione reale sub-daily sul book è operativamente più pesante di un segnale 1d (slippage/spread
intraday non interamente catturati dalla fee proporzionale).
## Caveat (perché LEAD, non sleeve)
1. **Plateau hold-out a UNA SOLA RIGA.** Il FULL è robusto su tutta la griglia L∈[2.0,3.0] (+0.6..+1.0),
ma l'**hold-out è positivo SOLO a L=2.0** (a L=2.5/3.0 crolla a 0.5..0.8). Il plateau sul full è
ampio, quello che conta — l'hold-out — è single-row. Da rinforzare prima di credere alla stazionarietà.
2. **Standalone FULL 0.92 < soffitto ~1.3.** Coerente col soffitto direzionale BTC/ETH: il valore di ERM
è **marginale/diversificante** (corr 0.15 a TP01, 0.28 a SKH01), non assoluto. Non rompe il soffitto.
3. **Multiple-testing non deflazionato.** 102 celle testate (60 ERM + 16 VEM + 24 VBR + 2 TOD) senza
deflated-Sharpe (a differenza del filone C). Il multi-cut 2026 = +2.861 è una manciata di giorni che
gonfia. Storia sub-daily certificata utile ~quanto SKH01 → finestra non lunghissima.
4. **Esecuzione 8h** = complessità operativa reale (vedi sopra), oltre il modello a haircut nullo.
## Analisi di robustezza / de-bias (`intraday_regime_analysis.py`) — il lead NON regge
I caveat #1 (plateau hold-out single-row) e #3 (multiple-testing) erano i sospetti giusti. Tre test di
de-bias li trasformano da sospetto in **bocciatura** dello slot:
| test | esito |
|---|---|
| **A) Deflated-Sharpe** (Bailey & Lopez de Prado) su 122 trial cercati | **FAIL.** DSR 0.000 (tutti) / **0.163 (escludendo i trap TOD)** / 0.241 (solo-ERM) — tutti ≪ 0.95. Lo Sharpe winner (0.92) è sotto lo Sharpe-max-atteso-null (1.162.51): il search ha trovato celle a 1.6 full / 1.7 in-sample, il winner 0.92 **non è eccezionale**. |
| **B) Selezione IN-SAMPLE-only** (scelgo la cella ERM col solo Sharpe < 2025) | **earns_slot=False.** La cella migliore pre-hold-out è un'**ALTRA** (8h L=2.0 thr=0.4 **long-flat**), con corr→TP01 **0.53** (è trend-beta travestito) → marginal=**NEUTRAL**. Il winner max-hold **non si seleziona senza guardare l'hold-out** → il suo `earns_slot=True` era **selezione-sull'hold-out**. |
| **C) Ensemble del plateau** (media 20 celle L×thr, niente cherry-pick) | **earns_slot=False.** marginal=ADDS, in-sample Sh 1.01, corr→TP01 0.18 — ma **`robust_oos=False`** (clean-year + jackknife): l'uplift hold-out è trascinato dal **2026 (+2.09 multicut)**, manciata di giorni. |
**Dove vive l'(eventuale) edge** (per-anno, blend 3-way 60/25/15 vs 2-way 75/25): uplift FULL solo **+0.10**,
**negativo nel 2021 (0.23) e 2022 (0.15)**, positivo altrove; l'uplift HOLD **+0.30 è concentrato nel
2026 (+0.46)**. corr(ERM,SKH) 0.28 full (fino a 0.42 in alcuni anni) → **parziale sovrapposizione con SKH**,
non ortogonalità piena.
**Lettura.** Il segnale efficiency-ratio non è rumore puro (l'ensemble ha in-sample Sh ~1.0, positivo nella
maggior parte degli anni), ma come **slot** fallisce ogni de-bias: il `earns_slot=True` della scoperta era
prodotto da **(1) selezione della cella sull'hold-out** + **(2) coda 2026** + **(3) multiple-testing non
corretto**. È lo stesso falso-positivo che l'alt-sweep 100-agent imparò a uccidere — qui ucciso dai gate.
## Caveat originari (ora risolti dall'analisi sopra)
1. ~~Plateau hold-out single-row~~**confermato fatale**: l'edge hold-out a L=2.0 è cell-selection.
2. ~~Multiple-testing non deflazionato~~**deflazionato: DSR FAIL** anche senza i trap.
3. Esecuzione 8h: irrilevante ormai (lo slot non c'è).
## Verdetto onesto: **NON è uno slot. Falso positivo da selezione-hold-out + coda 2026.** SCARTATO come sleeve.
Lo "earns_slot=True" della scoperta **non sopravvive** alla de-selezione: deflated-Sharpe FAIL (anche
escludendo i controlli), selezione in-sample-only → NEUTRAL su un'altra cella (trend-beta corr 0.53),
ensemble del plateau → robust_oos FAIL. **Conferma ennesima del soffitto direzionale BTC/ETH ~1.3**: un
segnale a 2 asset non lo supera; la via resta il cross-sectional (XS01). Resta al più una **curiosità in
forward-monitor** (l'efficiency-ratio ha un debole edge in-sample reale), ma da non armare come alpha.
Gli altri tre meccanismi: VBR=NOISE, VEM hold-out<0, TOD=controllo FAIL come atteso.
**Lezione harness (CODIFICATA).** Lo script di scoperta selezionava il vincitore per `min_hold` massimo
sulla griglia = **selezione-sull'hold-out**, il punto cieco che ha generato il falso PASS. Il marginal
scorer da solo non basta se la *cella* è scelta guardando l'hold-out: serve **(a)** scegliere la cella
in-sample-only (o walk-forward) **prima** di valutare il marginal, e **(b)** deflazionare per il numero di
celle/famiglie testate. Stesso buco segnalato per CC01 ("Sharpe implausibile") e per l'alt-sweep
(hold-out-fitting): qui in forma "selection-on-holdout". Ora è **codice** in `altlib` (LESSON 4): tre
funzioni nuove — `deflated_sharpe()` (Bailey & Lopez de Prado), `select_cell_insample()` (scelta cella
col solo Sharpe pre-HOLDOUT), e il gate combinato **`study_family_honest()`** (`earns_slot_honest =
earns_slot[cella in-sample] AND deflated-Sharpe≥0.95`). Verificato: su ERM il gate ritorna
`earns_slot_honest=False` (cella in-sample = trend-beta NEUTRAL, DSR 0.74). Analisi completa in
`scripts/research/intraday_regime_analysis.py`; test in `tests/test_harness_realism.py`.
**Regola nuova: una strategia direzionale grid-searched si giudica con `study_family_honest`, non
chiamando `study_marginal` sulla cella a max hold-out.**
Nessun impatto sul book live (branch separato, config canonica invariata).
@@ -0,0 +1,71 @@
# 2026-06-29 — Filone D: MACRO regime-gate sul book crypto — RIDONDANTE col trend, SCARTATO
**Tesi.** Usare segnali macro/cross-market — equity (SPY/QQQ/IWM), credito (HYG/LQD), oro (GLD/SLV),
tassi (TLT/IEF) — come **gate risk-on/risk-off** sul book BTC/ETH, per migliorare il *timing* del
drawdown di TP01: regime risk-off (credito che cede, equity sotto trend, fuga sui bond) → riduci/azzera
l'esposizione crypto; risk-on → lascia agire TP01. **È eseguibile** (gata solo BTC/ETH perp, non aggiunge
gambe). Script `scripts/research/macro_regime_gate.py`, test `tests/test_macro_regime_gate.py`. Branch
`research/equity-meanrev-ib` (separato, nessun impatto live). Dati: `data/raw/eq_*_1d.parquet` (ETF daily,
allineati causale `merge_asof backward` sul calendario SPY).
**NON è un lead-lag direzionale** (già morto: diari 2026-06-22/-23). L'angolo nuovo = un overlay
binario/continuo di **de-risk** sul book esistente.
## Il rischio noto in partenza (CLAUDE.md)
La lezione DVOL-spike (2026-06-26): un gate di de-risk rischia di essere **ridondante col trend** — TP01
è già long-flat e va a 0 nei crash (Δ 0.00). Questo script DOVEVA dimostrare che il gate aggiunge **oltre**
quel che TP01 fa da solo. **Non ci riesce.**
## Gate provati (tutti causali: SMA/ratio/z-score rolling, merge_asof backward)
`SPY>MA{50,100,150,200,250}`, `HYG>MA`, ratio di credito `HYG/LQD`, `HYG/IEF`, combo a maggioranza
(SPY+HYG+HYG/LQD, binario e continuo), flight-to-quality (`TLTup&SPYdn`, `GLDup&SPYdn`), regime di vol
equity `SPYvol_z1` (proxy "VIX-spike" senza VIX). g_off ∈ {0.0, 0.5}.
## Esiti
I gate di trend equity alzano lo Sharpe FULL della combo (SPY>MA150/200/250 → FULL ~1.22-1.25 vs TP01-solo
1.30) ma **non l'hold-out** (HOLD ~0.17-0.19) → è **de-levering**, non timing: tagliano DD spegnendo il
book in mercati equity laterali, non anticipando i crash crypto. Il miglior profilo DD è `SPYvol_z1`.
### Deep-dive `SPYvol_z1` — il controllo decisivo (ridondanza col trend)
| | exposure TP01 risk-off | exposure risk-on | giorni risk-off | **giorni in cui il gate LAVORA** | corr(1-gate, 1-expo) |
|---|---|---|---|---|---|
| BTC | 0.023 | 0.093 | 18% | **3%** | 0.278 |
| ETH | 0.013 | 0.068 | 16% | **2%** | 0.296 |
"Il gate lavora" = giorno risk-off **E** posizione TP01 non già flat. Accade nel **2-3% dei giorni**: nel
resto, quando il gate vorrebbe de-riskare, TP01 è **già flat da solo**. Il gate spegne un motore già spento.
### Marginal scorer vs TP01 (gate come candidato-sleeve)
```
abs=FAIL marginal=REDUNDANT EARNS_SLOT=False
corr->TP01 full 0.989 hold 0.984 beta 0.978 resid Sharpe 0.739
multi-cut persistent=True has_insample_edge=True (standalone 1.634) is_hedge=False
blend w25: full 1.323 (+0.026) hold 0.257 (-0.046) blend w50: full 1.348 (+0.051) hold 0.21 (-0.093)
```
corr→TP01 **0.989**, beta 0.98: lo stream gated **È** TP01. L'uplift hold-out è **negativo** (de-levering
che toglie ritorno fuori dai crash). overlay-delta (gatedsolo) come stream a sé: `verdict=NEUTRAL`.
## Causalità / eseguibilità (gli unici aspetti promossi)
- **Leak-free**: `causality_ok=True` (max_tail_diff 0.0); STRICT align (1 barra equity extra di lag) →
FULL invariato; `day_boundary_robust=INVARIANT` (spread 0.0). Allineamento macro→crypto backward-only.
- **Eseguibile a $600**: haircut `eval_weights_smallcap`**0.00** su BTC ed ETH. Fee-survivente
(0.00→0.30% RT: 1.42→1.33). Tecnicamente impeccabile — semplicemente **non aggiunge alpha**.
## Verdetto: **SCARTATO (ridondante col trend).** earns_slot=False.
Conferma esatta della lezione in CLAUDE.md: un overlay di de-risk su un book che è **già** long-flat e
già a 0 nei crash non ha quasi nulla da fare (lavora il 2-3% dei giorni, corr→TP01 0.99). Il taglio di DD
che mostra è **de-levering**, replicabile meglio con un `target_vol` più basso (stessa lezione del
TP01×DVOL overlay del 2026-06-26). Il macro-regime **non anticipa** i drawdown crypto meglio del prezzo
crypto stesso. Nessuno sleeve registrato; nessun impatto sul book live.
**Sottoprodotto utile:** il gate `redundancy_diag` (exposure risk-off vs risk-on + "quota giorni in cui il
gate lavora") è il test pulito da riusare ogni volta che si propone un overlay di de-risk — se "il gate
lavora" è basso e corr→TP01 ~1, è ridondante, a prescindere dallo Sharpe FULL che mostra.
@@ -0,0 +1,106 @@
# 2026-06-29 — Ricerca strategie a 5 thread paralleli: 0 nuovi sleeve, 1 LEAD che rompe 2 muri su 3
**Mandato.** "Cerca altre strategie" (+ "senza correlazioni, a segnale" + "a 1 gamba in/out a segnale").
Cinque ricerche parallele su aree genuinamente inesplorate, tutte sull'harness onesto condiviso
(`altlib` + `xsec_v2_nonmom`) con TUTTI i gate: causalità (prefix-check), netto fee 0.10% RT + sweep,
OOS hold-out 2025+, **deflated-Sharpe** (multiple-testing), **gate anti-selection-on-holdout**
(`study_family_honest`: cella scelta IN-SAMPLE-only), marginal vs TP01/XS01, corr vs book, haircut $600.
Branch `main`, **nessun impatto live** (solo file nuovi, nessuno sleeve registrato).
Premessa onesta (base-rate): il soffitto direzionale BTC/ETH ~1.3 è già confermato e lo sweep di 104
ipotesi (2026-06-20) non produsse nulla di nuovo che reggesse. L'attesa era "quasi tutto SCARTATO".
## Esiti per thread
| # | Thread | File | Verdetto |
|---|--------|------|----------|
| 1 | **XSEC low-risk cousins** (MAX/lottery, idio-vol, Amihud) | `xsec_v3_lowrisk.py` | 1 LEAD (IVOL), 0 sleeve |
| 2 | **XSEC momentum-structure** (risk-adj/accel/frog-in-pan/vol-managed vs XS01) | `xsec_v3_momstruct.py` | tutto REDUNDANT/SCARTATO |
| 3 | **Meta-allocazione** (allocazione dinamica tra i 4 sleeve) | `meta_allocation.py` | pesi fissi vincono |
| 4 | **Segnali ortogonali ETH/BTC** (relative-value dollar-neutral) | `orthogonal_signals.py` | 2 LEAD (STATARB, DVOLSPREAD) |
| 5 | **1-gamba a segnale** (MACD/RSI/Supertrend/Donchian/BBands/EMA) | `signal_inout_1leg.py` | 0/12 earns_slot |
**Netto: 0 nuovi sleeve.** Il soffitto regge. Test 143/143.
### Thread 1 — Low-risk cross-sectional (51 alt HL)
Tre fattori mai provati (il filone C aveva fatto total-vol e BAB, non questi). **IVOL** (idiosyncratic-vol
basso, 19-major B30 H5 k8): FULL 1.06 / HOLD **1.22** / corr ~0 a XS01 e TP01 / positivo ogni anno /
uplift portafoglio HOLD +0.42/+0.50 — l'unico LEAD di valore. **MAX** e **AMIHUD-liquido** sono lo stesso
tema "evita speculativo/illiquido/volatile" in altre vesti (corr fra loro 0.33-0.59). **AMIHUD
long-illiquido SCARTATO** (premio illiquidità invertito in crypto: vincono i major liquidi). Bocciatura
del claim forte: **deflated-Sharpe 0.30-0.37 ≪ 0.95** (96 trial), storia ~2.5 anni, book 10-16 gambe →
**STAT-MODE**. Nota di rigore: il naive best-HOLD atterrava su celle in-sample-negative (holdout-fitting) →
l'agente ha auto-aggiunto il gate has_insample_edge. → **forward-monitor IVOL**.
### Thread 2 — Momentum-structure vs XS01
4 varianti (risk-adjusted, acceleration, frog-in-pan, vol-managed). **Nessuna batte né diversifica XS01**
(standalone 1.42). Sostituire XS01 con una variante **distrugge l'hold-out** del portafoglio (0.58…−1.38)
— prova diretta che l'edge di XS01 sta nella sua struttura SPECIFICA (blend z-score [30,90] + gate
dispersione), non in varianti generiche. Deflated-Sharpe max 0.49. Tutte REDUNDANT/SCARTATO.
### Thread 3 — Meta-allocazione dinamica
Vol-parity / momentum-of-sleeves / dispersion-regime / drawdown-control vs pesi fissi, con costo di
ribilancio realistico. **Nessuno batte i pesi fissi OOS.** Vol-parity = trappola da manuale (+0.09…+0.44
sui tagli in-sample 2022-24, **0.11 sull'hold-out 2025** → intercettato dal multi-cut). Drawdown-control
RIDONDANTE (TP01 va già flat nei crash, il gate non si attiva mai). I 4 sleeve sono già quasi-scorrelati
(corr ~0.12 max) → i pesi fissi sono già vicini al risk-parity ottimo statico. **Mantenere i pesi fissi.**
### Thread 4 — Segnali ortogonali ETH/BTC (il risultato più notevole)
Relative-value dollar-neutral sul ratio log(ETH/BTC): 6 segnali, evaluator a 2 gambe (fee × 2), cella
scelta in-sample-only.
| segnale | FULL/HOLD | corr→book | β-mkt | marginal | DSR (grid/same-sign) | exec $600 | verdetto |
|---|---|---|---|---|---|---|---|
| **STATARB-RESID** (residual relative-mom ETH−β·BTC, W45 **sgn+1**) | 0.84/0.56 | **0.027** | 0.013 | **ADDS** | 0.056 / 0.929 | haircut ~0 | **LEAD** |
| DVOLSPREAD (IV relativa, W60) | 0.74/0.77 | 0.017 | 0.012 | ADDS | 0.082 / 0.907 | haircut ~0 | LEAD |
| RATIO-MOM/REV/ACCEL | 0.25-0.68 / ≤0 | ~0 | ~0 | NEUTRAL | <0.23 | ok | NEUTRAL (diversification-math) |
| VOLSPREAD | 0.24/1.47 | ~0 | 0.013 | DILUTES | — | — | SCARTATO |
**STATARB-RESID rompe 2 dei 3 muri storici:** è **ortogonale per costruzione** (corr→book 0.027, beta di
mercato 0.013 — meglio di SKH ~0.09) **ED eseguibile a $600** (book a 2 gambe BTC+ETH perp Deribit,
haircut ≈ 0, fee-survive fino a 0.30%/gamba → **NON STAT-MODE**, a differenza di XS01 e delle opzioni). Il
muro che **resta** è l'**edge**: Sharpe 0.84 / DSR 0.929 same-sign (ottimistico) ma comunque <0.95.
Verificato indipendentemente dal coordinatore: residuo causale (β OLS rolling backward, decisione a
close[i], return in i+1), fee a 2 gambe corretta. **⚠ Correzione di etichetta:** la cella vincente è
**sgn=+1**, NON sgn=1 → NON è mean-reversion ma **momentum relativo sul residuo** (long lo spread quando
ETH è ricco vs il suo fit su BTC: le dislocazioni ETH-vs-BTC *continuano* a 1d; la MR pura sgn=1 perde
1.4 in-sample). L'ortogonalità (β~0) viene dalla struttura dollar-neutral, non dal verso. → **LEAD
forward-monitor** (monitorabile a costo reale ~0), NON deploy. **CABLATO:** `scripts/live/paper_statarb.py`
(config CONGELATA W=45/sgn=+1, doppio libro MODELED/REAL-$600, riusa il segnale esatto di
`orthogonal_signals.py`), aggiunto al cron giornaliero accanto a `paper_prevday`; test `tests/test_paper_statarb.py`.
**DVOLSPREAD** ri-valida l'ex-lead `dvol_spread`: ADDS ma storia DVOL corta (2021+) → resta forward-monitor
come già noto. Secondario crypto-vs-macro = "TSMOM travestito" (corr→book 0.17-0.20), non ortogonale.
### Thread 5 — 1-gamba a segnale (eseguibile)
12 famiglie (MACD/RSI/Supertrend/Donchian/BBands/EMA, ±ADX) su 1d/12h/8h. **0/12 earns_slot_honest.**
**Eseguibilità validata** (haircut $600 = 0 ovunque; RSI-MR ~2.5 trade/anno) — ma è l'unica cosa che
regge. I trend-follower sono **TP01 travestito** (corr 0.44-0.79, full ~1.2-1.3 = il soffitto) e le celle
sub-daily scelte in-sample **collassano OOS** (dimostrazione da manuale del gate selection-on-holdout). La
mean-reversion è morta (BBands-MR has_insample_edge=False). I "low-corr interessanti" (RSI-MR, MACD-LS,
Donchian-LS) sono **HEDGE non alpha** (`is_hedge=True`, pagano solo quando TP01 è debole), e con
deflated-Sharpe <0.95 (RSI-MR 0.861; Donchian-LS passa DSR ma is_hedge). **A $600 un 1-gamba a segnale è
eseguibile ma non aggiunge nulla a TP01.**
## Sintesi / cosa ho imparato
1. **0 nuovi sleeve, soffitto ~1.3 riconfermato** dal lato direzionale e dal lato struttura-momentum. La
ricerca è onesta: ogni candidato è stato ucciso dal gate giusto (deflated-Sharpe, is_hedge,
selection-on-holdout, sostituzione-XS01, multi-cut), non da un giudizio a occhio.
2. **Il LEAD di valore è STATARB-RESID** (ETH/BTC residual mean-reversion). È il primo stream visto che è
**insieme ortogonale (β~0) ED eseguibile a 2 gambe** — cadono i due muri che bloccano XS01 (STAT-MODE)
e le opzioni. Manca solo l'edge sopra-soglia. È il candidato n.1 per il **forward-monitor**, e l'unico
che — se la finestra forward confermasse l'edge — sarebbe *deployabile* a $600 (non statistico).
3. **Forward-monitor (STAT-MODE / sub-soglia):** IVOL (idio-vol XS, 19-major), DVOLSPREAD (storia corta),
STATARB-RESID (eseguibile — il più promettente). Nessuno armato come sleeve.
4. **I gate nuovi funzionano:** `study_family_honest` (selection-on-holdout) ha intercettato i collassi
OOS dei trend 1-gamba e dei low-risk XS; il deflated-Sharpe ha tenuto sotto la soglia ogni Sharpe ~1
su storia corta. La lezione del filone B (de-bias prima di credere) è ora applicata di default.
## Caveat
- Universi/finestre: HL ~2.5 anni, DVOL dal 2021 → multiple-testing reale; tutti i LEAD sono sotto la
soglia deflazionata. Niente di questo va creduto come alpha finché una finestra forward non lo conferma.
- Nessuno sleeve registrato, config canonica invariata (TP01+XS01+VRP01+SKH01 a pesi fissi). Book live
intatto. Tutto il lavoro è statistico/forward, su `main` come ricerca.
Script: `scripts/research/{xsec_v3_lowrisk,xsec_v3_momstruct,meta_allocation,orthogonal_signals,signal_inout_1leg}.py`.
Test: i rispettivi `tests/test_*.py` (143/143 verdi).
+65
View File
@@ -0,0 +1,65 @@
# 2026-06-29 — Filone C: cross-sectional NON-momentum su Hyperliquid — DEBOLE / forward-monitor
**Tesi.** XS01 (sleeve attivo) è momentum cross-sectional sui 19 major HL. Lezione del progetto
(2026-06-19/20): **espandere il numero di asset NON aiuta il momentum** (gli small-cap diluiscono/
invertono il segnale). Quindi qui NON si ri-prova l'espansione-universo: si cerca un **meccanismo
DIVERSO dal momentum** che, market-neutral e scorrelato, possa diversificare il portafoglio. Script
`scripts/research/xsec_v2_nonmom.py`, test `tests/test_xsec_v2.py`. Branch `research/equity-meanrev-ib`.
## Meccanismi provati (tutti L/S dollar-neutral, vol-target ~20%, ribilancio periodico, CAUSALI)
| | meccanismo |
|---|---|
| REV | short-term reversal grezzo (long loser / short winner di breve) |
| IREV | reversal **idiosincratico** (sul residuo dopo aver tolto il mercato, beta-adjusted) |
| LOWVOL | factor **low-vol** (long bassa vol realizzata / short alta vol) |
| IMOM | momentum **idiosincratico** (sul residuo, ≠ raw mom) — *riferimento* |
| BAB | betting-against-beta (long basso beta / short alto beta) |
| MOM | momentum grezzo — *riferimento* |
Universi: **19 major** e **51 all** (i parquet certificati). Gate canonico di selezione: FULL>0.5,
HOLD>0, |corr→XS01|<0.6 (escluse le famiglie momentum MOM/IMOM, che sono riferimento, non candidati).
## Esiti — un solo candidato non-momentum supera il gate standalone
```
[OK] LOWVOL 19-major B30 H10 k5 FULL +1.07 HOLD +1.07 DD 19% corrXS +0.16 corrTP -0.28
[--] BAB 19-major B60 H10 k8 FULL +0.47 HOLD +1.15 DD 28% corrXS +0.20 corrTP -0.27
[--] REV/IREV (19 e 51) FULL ~0 o NEGATIVO (reversal cross-sectional non paga)
[--] LOWVOL 51-all FULL +0.42 (il long-tail diluisce, come per il momentum)
```
- **REV / IREV negativi** (FULL 0.10..0.70): il reversal cross-sectional grezzo o idiosincratico **non
è un edge** su questo universo/finestra — coerente con la morte storica del mean-reversion del progetto.
- **BAB** ha un buon hold-out ma FULL solo +0.47 (sotto soglia) e DD 28%.
- **LOWVOL 19-major** (betting-against-vol) è l'unico idoneo: FULL **1.07** / HOLD **1.07** / DD 19%,
causale (max_tail_diff 0), corr→XS01 +0.16 (scorrelato), corr→TP01 0.28.
- Espandere a **51 asset peggiora** anche il low-vol (FULL 1.07→0.42): riconferma che i margini
cross-sectional sono nella **struttura del segnale**, non nel numero di asset.
### Uplift portafoglio (4 → 5 sleeve, LOWVOL come 5° sleeve)
```
BASE (TP01+XS01+VRP01+SKH01) FULL 2.12 DD 8% | HOLD 2.25 DD 3%
+LOWVOL @10% FULL 2.17 (+0.05) | HOLD 2.57 (+0.32)
+LOWVOL @15% FULL 2.19 (+0.06) | HOLD 2.62 (+0.37)
```
## Il muro (perché NON è uno sleeve)
1. **Deflated Sharpe 0.130 ≪ 0.95** (N=216 trial globali; soglia Sharpe-max-null annualizz. 1.79). Con il
numero di celle/famiglie/universi testati, lo Sharpe 1.07 **non è significativo** dopo la correzione
per multiple-testing. Per-anno: 2024 +0.23 / 2025 +0.50 / **2026 0.09** (il 2026 è già negativo).
2. **Storia ~2.5 anni** (HL nativo 2024-2026): finestra troppo corta per un fattore cross-sectional.
3. **Non eseguibile a $600**: book L/S a molte gambe (10+ per lato) → **STAT-MODE**, mai deploy. Stesso
vincolo strutturale di XS01 (serve ~20k di capitale).
## Verdetto: **DEBOLE / forward-monitor (STAT-MODE).** Nessuno sleeve registrato.
Il **low-vol cross-sectional** (betting-against-vol sui 19 major) è l'unico meccanismo non-momentum con un
profilo decente e scorrelato sia a XS01 sia a TP01, e migliorerebbe l'hold-out del portafoglio (+0.32/+0.37).
**MA** non supera il deflated-Sharpe (multiple-testing), ha storia cortissima ed è non-eseguibile a questo
capitale. → lavoro **puramente statistico**, da tenere in forward-monitor come *secondo* segnale
cross-sectional a fianco di XS01, da rivalutare solo a scala (capitale ~20k+) e con più storia HL.
Reversal cross-sectional (REV/IREV) **confermato non-edge** su HL. Nessun impatto sul book live.
@@ -0,0 +1,91 @@
# 2026-07-01 — Breadth / market-internals del mercato ALT come segnale su BTC/ETH — SCARTATO
**Tesi.** Gli "internals" del mercato crypto — la partecipazione degli alt — come segnale
direzionale o gate di de-risk su BTC/ETH perp (2 gambe, eseguibile a ~$600): % di alt sopra la
propria SMA(N), advance/decline, % di alt che battono BTC (risk-appetite relativo), breadth-thrust.
Script `scripts/research/r0701_breadth_internals.py` (non committato per policy del filone; harness
`altlib`). Dati: i 51 parquet certificati `data/raw/hl_*_1d.parquet` → panel di **49 alt**
(esclusi hl_btc/hl_eth dalla breadth; hl_btc solo come riferimento per la famiglia RS), barre a
volume 0 mascherate (lezione backfill 2026-06-20; il fetch le aveva già ripulite: vol0=0 su tutti
i file, gate comunque attivo).
**Limite strutturale dichiarato in partenza:** l'universo HL parte dal 2024-01 → dopo il warmup
(max 120g) la finestra comune è **2024-05-05 → 2026-07-01 (~2.2 anni)** e l'in-sample pre-HOLDOUT
è **~8 mesi del toro 2024**. Qualunque esito poteva essere al massimo un lead.
## Famiglie e griglia (108 celle, tutte contate nel deflated Sharpe)
4 famiglie × N∈{20,50,100} × soglia∈{0.3,0.5,0.7} × forma∈{LS, long-flat, gate-su-TP01}:
- **FAM-MA**: % di alt validi con close > SMA(N) (breadth classica).
- **FAM-AD**: frazione di advancers (ret 1g>0), SMA(N).
- **FAM-RS**: % di alt che battono BTC sul ritorno a N giorni (≈ market-neutral, l'angolo più ortogonale).
- **FAM-TH**: breadth-thrust = Δ a N giorni della breadth MA20 (thrust/collapse), score 0.5+Δ.
Tutte causali (rolling/shift soltanto), MIN_VALID=20 asset per data (osservato: 42-49). LS/LF
vol-target 20% cap 2x (sizing TP01-style); GATE = posizione TP01 × gate binario. Fee 0.10% RT +
sweep 0-0.30%. Procedura onesta = mirror di `study_family_honest` sulla finestra comune (il
padding pre-2024 avrebbe contaminato il ranking full-history: le celle GATE con gate=1 pre-storia
erediterebbero lo Sharpe 2019-2024 di TP01): **cella scelta col SOLO Sharpe in-sample**, DSR su
tutti i trial, `marginal_vs_tp01` di altlib sulla cella scelta.
## ⚠️ Bug trovato e corretto: allineamento merge_asof su indice non-ns
La prima versione di `_align` usava `b.index.view("int64")//10**6` per i timestamp della breadth:
su un DatetimeIndex tz-aware **a risoluzione non-ns** (pandas 2.x) la scala esce sbagliata →
`merge_asof backward` matchava OGNI barra BTC/ETH all'**ultimo** valore della breadth = **il futuro
broadcast su tutta la storia**. Sintomo: decine di celle con lo stesso identico Sharpe (il segnale
era una costante). **`causality_ok` NON lo vede** — la serie breadth è un input esterno *fisso*,
quindi il target sul prefisso troncato coincide col target full per costruzione. Fix: epoca
esplicita `(idx - Timestamp(1970, tz=UTC)) // Timedelta(1ms)` (come `resample_tf`). **Lezione
harness:** ogni allineamento di un panel esterno via merge_asof va fatto con epoch-ms esplicita, e
il sintomo "molte celle con Sharpe identico" è un red-flag di allineamento rotto (gemella
dell'osservazione merge_asof del 2026-06-20).
## Esito (procedura onesta, finestra comune 2024-05+)
104/108 celle valutabili (4 degeneri flat), 47 con FULL>0. **Cella scelta in-sample** (IS 2.11,
la selezione non tocca mai l'hold-out): **TH N=100 thr=0.7 long-flat** — long solo dopo un
breadth-thrust (+0.2 di breadth MA20 in 100g), attiva il 32% dei giorni.
| metrica | valore |
|---|---|
| standalone finestra comune | IS **+2.11** → FULL **+0.99**, HOLD **+0.41** |
| assoluto (per-asset, trimmed) | **PASS** — minFull +0.68, minHold +0.38, fee-survive 0.30%RT, DD 7-11% |
| marginale vs TP01 | **ADDS** — corr full **0.401**/hold 0.358, resid Sharpe 0.87, multicut {2025:+0.076, 2026:+0.366} persistente, is_hedge=False |
| **robust_oos** | **False** — jackknife drop-best-month **0.068** (l'uplift hold-out sta in piedi su UN mese) |
| **deflated Sharpe** | **0.433** su 104 trial (null-max atteso 1.10) — lontanissimo dal PASS ≥0.95 |
| earns_slot / earns_slot_honest | **False / False** |
| causality_ok | True (post-fix) |
| smallcap $600 | haircut ≈ **0.00** (BTC 0.01, ETH +0.00; 137-152 trade) — eseguibile |
**Ridondanza col trend (il rischio n.1, lezione macro-gate):** qui NON è la modalità di morte.
corr(pos, TP01pos) 0.15-0.30 e il segnale "lavora" (off mentre TP01 non è flat) nel **24-32% dei
giorni** (il macro-gate lavorava nel 2-3%). Le forme direzionali LS/LF sono genuinamente diverse
da TP01 (corr→TP01 0.1-0.4). La forma **gate-su-TP01** invece sì: best gate = NEUTRAL a corr
0.803, e i gate a soglia bassa = REDUNDANT corr 1.0 (gate mai off → TP01 identico). **La breadth
come gate di de-risk non aggiunge nulla; come segnale direzionale è diversa dal trend ma l'edge
non è dimostrabile.**
**Il gemello in hindsight (trasparenza, NON selezionabile).** TH N=100 thr=0.7 **LS** fa FULL 1.13
/ HOLD 1.06, corr→TP01 **0.26**, marginal ADDS con robust_oos=True e jackknife +0.46. MA il suo
in-sample (1.30) è sotto quello della cella lf (2.11): sceglierlo = **selezione-sull'hold-out**
(esattamente il falso positivo ERM del 2026-06-29). E comunque **DSR 0.518 < 0.95**: anche la
lettura più caritatevole muore sulla deflazione multiple-testing.
## Verdetto: SCARTATO (niente sleeve, niente forward-monitor cablato)
1. La cella scelta onestamente è ADDS ma **robust_oos=False** (uplift a un mese) e **DSR 0.433**
su 104 trial: su ~8 mesi di in-sample (toro 2024) un IS di 2.1 è indistinguibile dal massimo
atteso per caso (1.10) e non regge.
2. Il candidato migliore col senno di poi (TH-LS) sarebbe selezione-sull'hold-out e fallisce
comunque il DSR (0.518) — il gate `study_family_honest` fa esattamente il suo lavoro.
3. Il limite è la **storia**: 2.2 anni totali, 8 mesi di in-sample. Non c'è modo onesto di
dimostrare l'edge oggi.
**Cosa resta.** (a) La famiglia **breadth-thrust (TH)** è l'unico angolo con celle OOS-positive e
bassa/negativa corr a TP01 — **rivisitabile quando il panel HL avrà ≥1-2 anni di storia nativa in
più** (in-sample multi-regime, DSR ricalcolabile); nessun cablaggio forward oggi, il costo
dell'attesa è zero. (b) Il breadth-**gate** su TP01 va in bacino con macro-gate e DVOL-gate: terza
conferma che TP01 non ha bisogno di de-risk esterno (long-flat, già flat nei crash). (c) Il fix
`_align` (epoch esplicita) è la lezione riusabile per ogni futuro panel esterno.
@@ -0,0 +1,58 @@
# 2026-07-01 — Book live: verifica gate SKH01 + emersione errori silenziosi
## Contesto
Il BOOK DERIBIT armato (TP01 0.75 + SKH01 0.25 nettati) è **flat da quando è stato armato
(2026-06-23)**: 193 run orari, tutte HOLD, zero ordini reali. Domanda: il flat è legittimo o il
gate di SKH01 è muto per un bug?
## Verifica del gate SKH01 — SANO
- **Logica viva:** `skyhook_entries` produce 335 entry (BTC, 222 long/113 short) e 341 (ETH,
226/115) su tutta la storia. Non è degenere.
- **Shorta davvero i crash:** ha aperto SHORT sulla gamba di crollo di fine maggio/inizio giugno —
BTC 05-28→06-04 ($73k→$64k), ETH 06-02→06-06 ($1865→$1555).
- **Flat da 06-23 = legittimo:** ultimo entry 06-06, `max_bars=16 × 230m ≈ 2,5g` → chiuso ~06-08/09,
**prima** dell'arming (06-23). Dal grind post-crash nessun setup breakout+regime valido → flat
corretto per design ("NON trend-follower"). Contesto prezzo al 01-07: BTC 53% dal picco, ETH
67,5%, momentum negativo su 1/3/6/12 mesi → anche TP01 (long-flat TSMOM) è a 0x.
- **Entrambi i feed valutano senza crash:** certificato e live `fresh_5m``skh_error=None`.
## Bug scoperto e corretto — pattern "eccezione ingoiata → stato safe silenzioso"
`src/live/` aveva più `try/except` che, in errore, producono uno stato safe (flat/fallback) **senza
farlo emergere**. Rischio: quando l'errore è transitorio ma reale, l'esecutore opera su dati sbagliati
credendoli legittimi.
### (1) `skh_error` — CORRETTO
`book_report` scriveva `skh_error` in un dict locale **mai incluso nel return**`r.get("skh_error")`
sempre `None`; e `book_execute` non lo leggeva comunque. Un feed SKH rotto avrebbe forzato SKH→flat in
silenzio (entry mancato, nessun alert). Fix: `skh_error` esposto nel return + `book_execute` emette
log `⚠️ SKH FEED ERRORE` + alert Telegram. (commit 31369b3)
### (2) Gate fail-safe posizione (Opzione A) — CORRETTO
`src/live/shadow.py::_positions`: se la read della posizione reale Deribit lancia, assume flat (0.0)
con una `note`, MAI propagata (`pos_src`) a `book_report`/`book_execute`. Severità mitigata a valle
da `rebalance_signed` che **rilegge la posizione fresca** (no doppia-posizione), ma resta il caso
"target≈0 + posizione aperta + read fallita → close saltato quel run" (transitorio, backstoppato dal
disaster-SL 30%). **Scelta A = gate fail-safe:** `_positions` ritorna un `pos_error` esplicito →
propagato `shadow_report → book_report → book_execute`; se presente (ONLINE ma posizione IGNOTA)
l'esecutore **NON opera a cieco** (return + alert), come già fa per `online`.
### (3) Diagnostica equity (Opzione B) — CORRETTO
`src/live/shadow.py::_equity`: se `account_summary('USDC')` e tutte le per-asset falliscono,
`real_eq=None` e `shadow_report` ripiega su `paper_cap` (~$2000 dallo state paper) mentre il conto
reale è ~$598 → il book dimensionerebbe su $2000 invece di $598 (**sovradimensiona ~33%**). Il danno
è limitato dall'**hard-cap $300/asset** in `book_net_target`. **Scelta B = SOLO DIAGNOSTICA** (niente
gate/halt, l'hard-cap già protegge il downside): `shadow_report` calcola `eq_fallback` (ONLINE +
equity reale non leggibile + no override), propagato `→ book_report → book_execute`, che stampa
`⚠️ EQUITY FALLBACK` + alert Telegram **ma prosegue** (a differenza del gate posizione, che blocca).
## Test
Suite completa **156/156** (era 148; +4 gate A + 4 diagnostica B). Dry-run reale di `book_execute`
invariato (nessuna riga d'errore, nessun ordine, conto $598.06 al target). Path live: `pos_error=None`,
`skh_error=None`, `eq_fallback=None`.
## Sintesi finale — i 3 pattern "errore silenzioso" del path live sono ora tutti tracciati
| # | Punto | Comportamento | Emersione |
|---|-------|---------------|-----------|
| 1 | `book_report` feed SKH | SKH → flat | log `⚠️ SKH FEED ERRORE` + alert |
| 2 | `_positions` read fallita | posizione → flat | **HALT** (non esegue) + alert (gate A) |
| 3 | `_equity` non leggibile | sizing → paper_cap | log `⚠️ EQUITY FALLBACK` + alert (diagnostica B) |
+114
View File
@@ -0,0 +1,114 @@
# 2026-07-01 — Funding rate come segnale TIME-SERIES direzionale su BTC/ETH (R0701): SCARTATO
## Contesto
Filone: il **funding orario Hyperliquid come proxy di POSIZIONAMENTO/sentiment** con valenza
direzionale a orizzonte giornaliero su BTC/ETH — NON carry. Angolo diverso dal prior art già
chiuso: **FC01** (carry cross-sectional delta-neutral 19 alt → LEAD fragile SCARTATO,
`2026-06-22-funding-carry-hl.md`) e **funding price-clock** intraday (drift attorno agli stamp →
FAIL, onda intraday). Qui l'ipotesi è: funding estremo = affollamento long → fade; funding in
espansione = domanda persistente → follow; funding affollato = de-risk del trend (gate).
Attrattiva a priori: 2 gambe BTC/ETH perp = **eseguibile al capitale reale ~$600** (non STAT-MODE
come XS01/opzioni) — se avesse retto sarebbe stato deployabile.
Script: `scripts/research/r0701_funding_ts.py`.
## Dati e causalità (data-first)
- `data/raw/hlfund_{btc,eth}_1h.parquet`: **2023-05-12 → 2026-06-22, 1138/1138 giorni validi,
0 gap** dopo il 2023-06 (primi ~27 giorni a cadenza 8h — la somma giornaliera degli stamp li
gestisce indifferentemente). Funding medio **+14.4%/anno BTC, +14.6% ETH**; std del funding
giornaliero ~5.5 bps.
- **Join causale** (il punto delicato): le barre 1d di `altlib.get` sono **open-labeled**
(datetime = 00:00 UTC del giorno D; il close della barra D è noto alle 00:00 di D+1). Il
feature-day D aggrega i SOLI stamp in **[D 00:00, D+24h)** — l'ultimo alle 23:00 — quindi tutto
è noto PRIMA della decisione al close della barra D; `eval_weights` shifta la posizione al
giorno D+1. Prefix-consistency check esplicito: **max diff 0.0** (nessun leak).
- **Valutazione su finestra TRONCATA** alla copertura funding, non sul frame prezzi 2018+: fuori
copertura il target sarebbe zero per costruzione e i giorni-zero (a) **gonfiano il T del
deflated-Sharpe** (anti-conservativo) e (b) diluiscono `cand_insample_sharpe` (il gate
`has_insample_edge` scatterebbe a vuoto). La logica di `study_family_honest` è replicata
ESATTAMENTE sui frame troncati coi primitivi altlib (selezione IN-SAMPLE-ONLY → gates
study_marginal → DSR sull'intera griglia). Cross-check `study_marginal` non-troncato in coda:
stessi verdetti (abs WEAK / marginal NEUTRAL / earns_slot False).
- ⚠️ **STORIA CORTA**: ~3.1 anni totali → in-sample pre-HOLDOUT ~1.6a (meno warmup z), hold-out
2025+ ~1.5a. Dichiarato: nessun numero qui ha il peso statistico di TP01 (7 anni).
## Metodo
Famiglia a griglia modesta, 1d, 50/50 BTC+ETH, vol-target 20% cap 2x (convenzione TP01), fee
0.10% RT + sweep 0.00-0.30%:
- **FADE**: z-score(funding daily, lb) ≥ thr → short; ≤ thr → long (fade dell'affollamento).
- **FOLLOW**: z ≥ thr → long; ≤ thr → short (sentiment momentum).
- **GATE**: trend long-flat TP01-like (sgn mom 30d+90d) ma FLAT quando z ≥ thr (de-risk crowding).
- **DIVERGE**: momentum prezzo 20d seguito se funding NON affollato (z ≤ thr), fatto se z ≥ thr.
Griglia: 4 forme × lb {7,14,30,60}g × thr {0.5,1.0,1.5} = **48 celle**. Selezione cella
**in-sample-only** (mai sul hold-out), deflated-Sharpe su tutte le 48, gates marginali
(`marginal_vs_tp01`), boundary-shift della finestra di aggregazione funding (0/3/6/9/12h),
`eval_weights_smallcap` a $600.
## Risultati
Best in-sample per forma: **fade 0.34, follow 0.81, gate 1.76, diverge 0.34** — il segnale
puro-funding è debole ovunque; solo la forma che CONTIENE il trend ha Sharpe.
**Cella scelta (in-sample): GATE lb=60 thr=1.5** — IS 1.76, FULL 1.16, HOLD 0.29.
| Gate | Esito |
|---|---|
| Deflated-Sharpe (48 celle) | **0.215** (null-max atteso 1.59) → **FAIL** (soglia 0.95) |
| Assoluto (frame troncati) | **WEAK** (minFull +0.97, ma minHold **+0.04** BTC); fee-survive fino a 0.30% RT |
| Marginale vs TP01 | **NEUTRAL**: corr **0.879** (hold 0.92), beta 0.77; uplift w25 hold **+0.001**; robust_oos=False (jackknife 0.074); multicut 2026 **0.423** |
| earns_slot / earns_slot_honest | **False / False** |
| Realism | prefix-diff 0.0 (causale); boundary-shift sign-stable (uplift spread 0.047); smallcap $600 haircut ~0.00 (eseguibile, irrilevante senza edge) |
**Controllo decisivo (lezione TP01-DVOL-overlay)** — lo stesso trend long-flat SENZA gate funding:
```
trend NUDO : IS 1.65 FULL 1.14 HOLD 0.37
trend+GATE : IS 1.76 FULL 1.16 HOLD 0.29 corr(gated,nudo)=0.928
delta del gate funding: FULL +0.013, HOLD 0.080
```
**tutto lo Sharpe della "vincitrice" è il trend**; il contributo del funding è ~zero in-sample
e **negativo sul hold-out**. Il gate è TP01 travestito.
**Miglior cella puro-funding** (la vera tesi del filone): FOLLOW lb=60 thr=0.5 — IS 0.81 ma
**FULL 0.28, HOLD 1.69**, marginale **DILUTES** (uplift w25 hold 0.778). L'in-sample 0.81 era
"long-il-toro-2023/24" travestito: z-alto di funding = rally già in corso; nel 2025-26 la stessa
regola è sistematicamente dalla parte sbagliata. FADE (shortare il crowding) perde sul toro e non
recupera abbastanza nel chop; DIVERGE negativa ovunque. NB: esistono celle gate con HOLD migliore
(es. gate 7/1.5 HOLD 0.96) — sceglierle sarebbe **selection-on-holdout**, il gate del 2026-06-29
esiste esattamente per questo.
## Perché fallisce (meccanismo)
1. Su asset MAJOR il funding è **quasi sempre positivo e segue il prezzo con lag**: lo z-score
alto coincide col momentum già espresso → FOLLOW è trend-beta ritardato (muore quando il regime
gira), FADE è shortare il toro. Non c'è l'anti-persistenza ("crowding → reversal") che la tesi
richiede — a 1d, su BTC/ETH, le liquidazioni-da-crowding sono già nel prezzo prima che lo
z-score giornaliero diventi estremo.
2. L'unico uso "funzionante" (gate di de-risk sul trend) è **ridondante**: TP01-like è già flat
quando serve. Stesso verdetto di macro-regime-gate (corr 0.989) e DVOL-overlay: la famiglia
"overlay prudenziale su TSMOM" è esaurita — eredita lo Sharpe del trend e non aggiunge nulla.
## Verdetto
**SCARTATO.** `earns_slot=False`, `earns_slot_honest=False` (DSR 0.215). Il funding time-series
non ha informazione direzionale propria su BTC/ETH a 1d; l'unica cella con Sharpe è trend
travestito con contributo funding OOS negativo. Niente forward-monitor: non c'è una componente
funding da monitorare. Con FC01 (carry cross-sectional) e il price-clock intraday, il filone
"funding come segnale" su dati certi è ora **chiuso su tre lati**.
## Lascito
- `scripts/research/r0701_funding_ts.py` riusabile: (a) loader funding→daily **causale** per barre
open-labeled (finestra [D, D+24h), gestione cadenza mista 8h/1h, flag validità copertura);
(b) pattern "**truncation-honest**" per dataset a copertura parziale — replica di
`study_family_honest` su finestra troncata per non gonfiare il T del DSR né diluire
`has_insample_edge` con anni di zeri (riusabile per ogni serie corta tipo DVOL/HL).
- I parquet `hlfund_*` restano certificati. Se mai si tornasse sul funding: gli estremi VERI
(squeeze) vivono sugli alt, ma è di nuovo cross-sectional → STAT-MODE a $600, soglia di prova
alta (e FC01 ha già mostrato la fragilità d'universo).
@@ -0,0 +1,174 @@
# 2026-07-01 — Pesi statici ri-ottimizzati + guardia-DD sul portafoglio a 4 sleeve
## Mandato
Migliorare il portafoglio ATTIVO (TP01 41.25 / XS01 18.75 / VRP01 15 / SKH01 25) SENZA nuove
strategie: (A) ri-ottimizzazione onesta dei pesi statici, (B) guardia-drawdown a livello
portafoglio (replica del soft-guard vincente del tail-hedge lab 2026-06-23), (C) combinazione.
NON meta-allocazione dinamica (già scartata, thread 3 del 2026-06-29).
Script: `scripts/research/r0701_portfolio_opt.py`. Solo ricerca: nessun file di src/ o live toccato,
i pesi sono PROPOSTI, non applicati.
## Baseline riprodotta
`run_portfolio.py` oggi: **FULL Sh 2.12 / HOLD 2.21 / DD full 7.8% / CAGR 23.3%** (vs 2.13/2.30
del diario Skyhook: i dati sono avanzati di una settimana, coerente). Matrice sleeve outer-join
2019-03-14 → 2026-07-01 (TP01/SKH01 dal 2019, VRP01 dal 2021-05, XS01 dal 2024-01), combine
identico a `portfolio.combined_daily` (pesi rinormalizzati per-riga sugli sleeve attivi).
## (A) Pesi statici — metodo
- Ottimizzazione SOLO in-sample (pre-cut), valutazione OOS (post-cut), multi-cut
{2024-01, 2024-07, 2025-01}. Sleeve con <120 giorni in-sample al cut (XS01 al 2024-01)
PINNED al peso corrente (l'ottimizzatore non ha dati per giudicarli).
- Criteri: MAXSH (max-Sharpe del combo simulato), RP (inverse-vol), ERC, MINVAR-R (min-var con
vincolo ritorno ≥ pesi correnti). Null: EW (25×4) e CURRENT. Vincoli 5%≤w≤60%, Σ=1.
- **Cap STRUTTURALI dichiarati** (prudenza, non statistica): **VRP01 ≤ 15%** (sleeve MODELLATO su
DVOL ATM, stress-f non catturato — "niente short-vol da modello in deploy"), **XS01 ≤ 25%**
(STAT-MODE, storia ~2.5 anni). Varianti sotto cap: MAXSH-STR (ottimizzato) e EW-STR
(EW proiettato sui cap, parameter-free → TP 30 / XS 25 / VRP 15 / SKH 30).
### Risultati (pesi ottimizzati pre-2025, valutati FULL e HOLD-OUT 2025-26)
| criterio | pesi TP/XS/VRP/SKH | FULL Sh | FULL DD | FULL CAGR | HOLD Sh | HOLD DD | HOLD CAGR | multi-cut ΔSh OOS | anni vinti |
|---|---|---|---|---|---|---|---|---|---|
| CURRENT | 41.2/18.8/15/25 | 2.12 | 7.8% | +23.3% | 2.21 | 3.5% | +16.0% | — | — |
| MAXSH | 27.6/17.5/**39.5**/15.4 | 2.17 | 6.3% | +21.8% | 2.27 | 2.8% | +12.7% | +0.24/+0.08/+0.06 | **1/8** |
| RP-invvol | 26.7/15.5/**41.6**/16.3 | 2.17 | 6.4% | +21.8% | 2.25 | 2.8% | +12.4% | +0.28/+0.07/+0.04 | — |
| ERC | 26.6/16.2/**41.6**/15.7 | 2.17 | 6.3% | +21.7% | 2.25 | 2.8% | +12.4% | +0.29/+0.07/+0.05 | — |
| MINVAR-R | 26.7/23.3/**29.6**/20.4 | 2.18 | 6.4% | +23.3% | 2.37 | 2.6% | +16.1% | +0.20/+0.10/+0.16 | 4/8 |
| MAXSH-STR | 38.6/25/15/21.4 | 2.14 | 7.5% | +23.4% | 2.28 | 3.3% | +16.8% | **0.00**/+0.03/+0.07 | 3/8 |
| EW | 25/25/25/25 | 2.14 | 7.0% | +24.5% | 2.39 | 2.8% | +18.1% | +0.16/+0.17/+0.18 | 6/8 |
| **EW-STR** | **30/25/15/30** | 2.10 | 8.5% | +25.2% | **2.35** | 3.2% | **+19.7%** | **+0.06/+0.13/+0.14** | **7/8** |
### Lettura onesta
1. **Gli OTTIMIZZATORI non aggiungono nulla — anzi overfittano.** Tutti (MAXSH/RP/ERC/MINVAR-R)
convergono sulla stessa mossa: scaricare TP01 e caricare **VRP01 al 30-42%** perché ha la vol
più bassa (7.7%/anno). Ma quella è la vol del MODELLO (premio BS su DVOL, coda di stress non
catturata): è esattamente il punto cieco tipo-CC01 ("Sharpe/vol implausibile → rischio
nascosto"). E per-anno MAXSH vince **1/8 anni** vs CURRENT — l'uplift multi-cut è smoothing di
vol modellata, non alpha di allocazione. **Dentro i cap** l'ottimizzatore (MAXSH-STR) converge
quasi sui pesi correnti (38.6/25/15/21.4) e NON è persistente (0.00 al cut 2024-01) →
**i pesi correnti sono già ~ottimi per ciò che il backtest può giudicare lecitamente**.
2. **L'unico segnale robusto è la DE-CONCENTRAZIONE parameter-free**: EW-STR (EW sotto i cap,
nessun parametro fittato) vince **7/8 anni** (unica perdita 2021 3.1pp), tutti i cut OOS
(ΔSh +0.06/+0.13/+0.14), HOLD Sh 2.21→2.35, HOLD CAGR +16.0→+19.7%. Classico "1/N batte
l'ottimizzazione" (DeMiguel). La sostanza: CURRENT dà il 41% al peggior Sharpe FULL del book
(TP01 1.30) e EW-STR sposta ~11pp da TP01 verso SKH01 (+5pp) e XS01 (+6.25pp).
3. **MA non è un free lunch risk-adjusted**: FULL Sh 2.12→**2.10** e FULL DD 7.8→**8.5%**
(più SKH01, che ha DD standalone 18%, pesa di più nei primi anni a 2 sleeve). L'uplift è un
TILT di ritorno verso gli sleeve research-grade con la storia più corta/caveat più pesanti
(SKH01 = research win, book 230m da verificare a deploy; XS01 = STAT-MODE non eseguibile a 2k).
E TP01 resta l'unico sleeve *deployed* live: il suo 41% nel report riflette una scelta di
prudenza/eseguibilità, non solo statistica.
### VERDETTO (A)
- **Ottimizzazione formale: NON MIGLIORA** (viola i cap strutturali o converge ai pesi correnti).
**Per il book reale i pesi correnti reggono.**
- **Candidato PROPOSTO (non applicato): EW-STR = TP01 30 / XS01 25 / VRP01 15 / SKH01 30** —
uplift OOS persistente e per-anno (7/8), dentro i cap, al costo di FULL DD +0.7pp e FULL Sh
0.02. È una decisione di risk-appetite (più ritorno atteso spostando peso su sleeve
research-grade), non un miglioramento dominante. Se si vuole adottare, farlo consapevoli che
aumenta la dipendenza da SKH01/XS01, e NON tocca il book Deribit eseguibile (TP75/SKH25).
### Caveat metodologici (A)
- Le 3 finestre multi-cut sono ANNIDATE (tutte contengono il 2025-26): non 3 conferme
indipendenti. Il test disgiunto vero è il per-anno (7/8 per EW-STR è il dato forte).
- EW-STR è stato costruito DOPO aver visto EW vincere (proiezione del null sui cap dichiarati):
costruzione principled e parameter-free, ma va detto — non era nella griglia iniziale.
- I pesi CURRENT stessi furono fissati (2026-06-23) conoscendo l'hold-out → il confronto
"in-sample-opt vs CURRENT su hold-out" è semmai sbilanciato A FAVORE di CURRENT; EW-STR vince
comunque.
## (B) Guardia-drawdown a livello portafoglio — metodo
Soft-guard causale sul combo 4-sleeve (pesi CURRENT): esposizione del giorno D decisa dal DD
dell'equity fino a **D1**; trigger/re-entry sul **NAV ombra** non-guardato (lezione stops_lab:
sull'equity congelata a expo=0 il DD non rientra mai); isteresi re-risk a 0.4·X come nel
tail-hedge lab. Griglia X∈{3,4,5,6}%, derisk∈{0,0.5}. Selezione cella IN-SAMPLE (max Calmar
pre-2025), verifica hold-out + multi-cut, **null de-levering** (expo costante c che pareggia il
MaxDD in-sample della guardia: Sharpe di c·r è invariante → la guardia vale solo se batte quello).
### Risultati
- Cella scelta in-sample: **X=5% / derisk=0.5** (IS Calmar 3.64 vs 3.24 baseline; IS DD 7.8→6.6%,
IS CAGR 25.3→23.9%, IS Sh 2.13→2.12). In-sample batte il null de-levering c=0.84 a pari DD
(CAGR 23.9% vs 21.0%) — il timing esiste in-sample.
- **MA OOS è INERTE**: a ogni cut la cella scelta non scatta MAI (ΔSh +0.00, ΔDD 0.0pp, ΔCAGR
0.0pp su tutti e 3 i cut) perché il DD OOS del 4-sleeve resta 3.5-4.8% < trigger 5%.
**La diversificazione a 4 sleeve fa già il lavoro della guardia** (il 2022 del book è +11.6%
con DD 3.6%: non c'è il grind da proteggere che aveva il combo TP01+GTAA del tail-hedge lab,
DD 8.4% / 2022 4.4%).
- Trigger più stretti che scattano davvero COSTANO: X=3%/d=0.5 → HOLD Sh 2.10 (< 2.21), CAGR
13.7% (< 16.0%); X=3-4%/d=0 whipsaw pieno (HOLD Sh 1.83-2.09; a X=4/d=0 il DD guardato IS
sale addirittura a 8.8% > 7.8% baseline — accumulo di perdite su cicli off/on, lo stesso
fenomeno visto in stops_lab con l'uscita totale).
### VERDETTO (B)
**NON MIGLIORA.** La guardia-DD che vinceva sul combo TP01+GTAA (DD 8.4%, 2022 4.4%) qui non ha
bersaglio: il 4-sleeve ha già DD ~3.5-4.8% OOS. Coerente col thread meta-allocazione 2026-06-29
(drawdown-control ridondante). Nota operativa: la cella X=5%/d=0.5 è *innocua* (non fa nulla
finché il DD non supera il 5%) e in-sample mostrava timing genuino sopra il null de-levering —
può valere come **circuit-breaker di emergenza sul live** (risk management, non alpha), ma non è
un miglioramento statistico e non la si adotta come tale.
## (C) Combinazione
(B) è inerte → niente da combinare. Per completezza: EW-STR + guard X=5%/d=0.5 → FULL Sh 2.08,
DD 6.7%, CAGR 23.7% | HOLD invariato 2.35/3.2%/19.7% (la guardia lima il DD 2021-2023 del tilt
a costo di 0.02 Sh FULL). Nessuna adozione proposta.
## Sintesi
| config | FULL Sh | FULL DD | FULL CAGR | HOLD Sh | HOLD DD | HOLD CAGR |
|---|---|---|---|---|---|---|
| **CURRENT (attivo)** | 2.12 | 7.8% | +23.3% | 2.21 | 3.5% | +16.0% |
| EW-STR (proposta A, non applicata) | 2.10 | 8.5% | +25.2% | 2.35 | 3.2% | +19.7% |
| CURRENT + guard 5%/0.5 (B) | 2.11 | 6.6% | +22.3% | 2.21 | 3.5% | +16.0% |
| EW-STR + guard (C) | 2.08 | 6.7% | +23.7% | 2.35 | 3.2% | +19.7% |
- **(A) NON MIGLIORA in senso forte** — i pesi correnti reggono; l'unico candidato onesto è il
tilt de-concentrante EW-STR (30/25/15/30), un trade-off di risk-appetite da decidere, non un
upgrade dominante. Gli ottimizzatori in-sample o violano i cap strutturali (VRP01 40%) o
riconvergono sui pesi correnti.
- **(B) NON MIGLIORA** — guardia inerte OOS sul 4-sleeve; utile al più come circuit-breaker.
- Nessuna modifica a `src/portfolio/sleeves.py` o al live. Test 160/160 verdi.
Script: `scripts/research/r0701_portfolio_opt.py` (~4s, vettoriale).
## Verifica avversariale (agente scettico)
Script: `scripts/research/r0701_portfolio_skeptic.py`. **VERDETTO: NON APPLICARE — CURRENT resta.**
I numeri dell'agente riproducono ESATTAMENTE (via `StrategyPortfolio.combined_daily`, path di
produzione: CURRENT 2.12/2.21, EW-STR 2.10/2.35, multi-cut +0.06/+0.13/+0.14, |diff|=0). Il claim
muore altrove: l'uplift è **selezione-sull'hold-out di 2° ordine**, non alpha di allocazione.
1. **L'uplift OOS vive TUTTO nel 2025-26 — la finestra che ha selezionato i beneficiari.**
Finestre OOS **DISGIUNTE** (i 3 cut del diario sono annidati): ΔSh EW-STR vs CURRENT =
**0.12** (2024H1) / +0.06 (2024H2) / **+0.14** (2025+). L'unica finestra OOS non toccata
dalla selezione hold-out è negativa. Sharpe standalone IS→HOLD per sleeve: TP01 1.49→**0.30**,
SKH01 1.42→**1.64**, XS01 1.22→**1.51**: EW-STR compra esattamente i due sleeve ammessi/affinati
(2026-06-23 / 2026-06-19) PERCHÉ forti sull'hold-out e vende quello che lì collassa. Meccanico.
2. **Pre-2025 (finestra non contaminata) EW-STR PERDE risk-adjusted**: ΔSh aggregato **0.05**,
Sharpe per-anno vinti **2/6** (2021 0.29, 2020 0.10, 2023/2024 negativi).
3. **"7/8 anni vinti" è un artefatto di metrica**: è RITORNO composto (un tilt a più vol/ritorno
li vince quasi per costruzione), non risk-adjusted. Su **Sharpe per-anno: 3/8**. E due dei 7
"vinti" (2023, 2024) hanno margine **+0.03pp** = tie. Bilancio materiale sul ritorno:
5 vinti / 1 perso (2021 3.1pp) / 2 pari.
4. **Plateau: liscio ma il gradiente è solo "meno TP01"**; a TP fisso, alzare SKH 25→30 è
piatto/negativo (min multi-cut a TP35: +0.07→+0.02). Scomposizione: il tilt **solo-SKH**
(+5pp, TP 36.25) da solo NON è persistente (multi-cut **0.00**/+0.05/+0.04); il tilt
**solo-XS** (TP35/XS25/VRP15/SKH25) è l'unico positivo su tutte le finestre disgiunte
(+0.05/+0.02/+0.11) — ma pre-2025 fa ΔSh 0.00 (edge ≈ zero fuori dal 2025-26) e carica lo
sleeve MENO eseguibile (STAT-MODE).
5. **Realismo**: con l'outer-join rinormalizzato EW-STR tiene SKH01 al **50% effettivo nel
2019-20 e 40% nel 2021-23** (book research 230m a metà portafoglio: da lì il FULL DD 8.5%).
Punto onesto A FAVORE: un haircut d'esecuzione a drag su SKH01(-20/30%) e SKH+XS non ribalta
il ranking hold-out (ΔSh resta +0.08..+0.13), ma i livelli assoluti crollano (HOLD Sh CURRENT
2.21→1.64 al 30% su entrambi) e il beneficio resta di CARTA: il book Deribit eseguibile
(TP75/SKH25) non è toccato dalla decisione.
6. **Forking paths quantificato**: ≥15 config valutate sull'hold-out (7 vettori pesi + 8 celle
guardia), EW-STR costruito dopo aver visto EW vincere. Su **500 tilt CASUALI cap-respecting**:
51% batte CURRENT sull'hold-out (mediana ΔSh +0.00), solo 11% sul FULL, 30% su tutti e 3 i cut.
EW-STR è al **94° percentile** dell'uplift hold-out fra i tilt casuali ≈ il **massimo atteso di
~15 tentativi** (k/(k+1)=93.8%): l'uplift osservato è indistinguibile dal "best of 15 tilt
casuali scelto sull'hold-out".
**Conclusione**: il tilt non è disonesto nei conti ma non è evidenza — è la stessa lezione del gate
SELECTION-ON-HOLDOUT applicata al livello dei PESI (2° ordine: gli sleeve stessi sono stati
selezionati su quell'hold-out). Unica direzione sopravvissuta: XS01 18.75→25 (solo-XS, positivo su
tutte le finestre disgiunte), MA edge ≈0 fuori dal 2025-26, sleeve STAT-MODE non eseguibile a $600,
e l'hold-out-strength di XS01 è a sua volta concentrata dal gate di dispersione nel regime 2025-26
→ da rivalutare SOLO con più storia forward, non ora. **CURRENT (41.25/18.75/15/25) resta.**
@@ -0,0 +1,132 @@
# 2026-07-01 — Ondata multi-agente a 6 filoni: 0 nuovi sleeve, pesi CURRENT confermati, 1 gate nuovo
**Goal utente:** "cerca con diversi agenti altre strategie di trading. Prova di tutto e arriva a
migliorare il portfolio presente. Lo scopo è sempre 50 euro days."
**Esito in una riga:** 6 filoni nuovi (scelti dopo scan dei ~65 diari per non ripetere strade morte)
+ 1 verifica avversariale → nessun candidato supera i gate onesti; il portafoglio a 4 sleeve
(41.25/18.75/15/25) **sopravvive a un attacco serio**, che è l'esito corretto per un book onesto.
Miglioramento concreto dell'ondata = metodologico: **gate `weights_tilt_null`** codificato (sotto).
## I 6 filoni (tutti su harness onesto: study_family_honest, marginal scorer, DSR≥0.95, smallcap $600)
| # | filone | verdetto | perché muore |
|---|---|---|---|
| 1 | **Funding time-series** BTC/ETH (posizionamento, non carry) — `r0701_funding_ts.py` | SCARTATO | FOLLOW = trend-beta ritardato (HOLD 1.69), FADE = shortare il toro; la cella gate è TP01 travestito (controllo senza funding = stessi numeri, corr 0.93, ΔHOLD 0.08); DSR 0.215. **Filone funding chiuso su 3 lati** (FC01 carry, price-clock, TS-signal). |
| 2 | **Breadth/internals alt** (51 HL) → BTC/ETH — `r0701_breadth_internals.py` | SCARTATO (rivisitabile) | Unico non-ridondante col trend (corr→TP01 0.40, lavora dove TP01 è attivo), assoluto PASS, marginal ADDS — ma jackknife 0.068 (uplift su UN mese) e DSR 0.433/104 celle. Con ~8 mesi di IS il 2024-toro non basta. **Rivisitare tra 1-2 anni di storia HL nativa.** |
| 3 | **Residual momentum XS** (β-hedged vs BTC, 19 major) — `r0701_xs_residmom.py` | REDUNDANT | Cross-section la residualizzazione è quasi un no-op (lo z-score di XS01 già rimuove il mercato): corr→XS01 0.54, HOLD 0.24, corr(IS,HOLD) tra le celle **0.37** (anti-predittivo = rumore). L'edge residuo-momentum vive nella coppia ETH/BTC (STATARB-RESID, resta in `paper_statarb`), non generalizza. |
| 4 | **Pesi + guardia-DD**`r0701_portfolio_opt.py` | vedi sotto | Unico candidato dell'ondata (EW-STR) → refutato dallo scettico. Guardia-DD X5%/d0.5: in-sample batte perfino il null de-levering, ma **OOS non scatta mai** (DD book 3.5-4.8% < trigger): la diversificazione fa già il lavoro. Utilizzabile solo come circuit-breaker di emergenza (risk mgmt, non alpha). |
| 5 | **Affinamento VRP01** (sizing IVRV, DVOL-mom, gate TP01) — `r0701_vrp_refine.py` | NON MIGLIORA | L'alpha è già tutto nel gate binario IV-rank; il gate TP01 è la trappola IS perfetta (schiva il 2022, ma taglia le settimane migliori dell'hold-out: multi-cut 0/5). **3° fallimento del filone "affinare VRP dentro il modello" → esaurito** finché cerbero-bite non cattura un crash reale (f di stress). |
| 6 | **Stagionalità cross-sectional** HL — `r0701_xs_seasonal.py` | SCARTATO (allo step statistico) | Nessuna persistenza split-half sopra il null permutato max-statistic (p 0.16-0.23); l'unica struttura è il canale-beta dell'effetto weekday di mercato (famiglia trackF, già morta). Turnover ~2×gross/die = fee-death comunque. |
## EW-STR: il candidato refutato (caso di scuola di selezione-sull'hold-out di 2° ORDINE)
Il filone 4 proponeva **EW-STR** (TP01 30 / XS01 25 / VRP01 15 / SKH01 30 = equal-weight proiettato
sui cap strutturali): HOLD Sh 2.21→2.35, multi-cut +0.06/+0.13/+0.14, "7/8 anni vinti" — dichiarando
onestamente i caveat. La verifica avversariale (`r0701_portfolio_skeptic.py`, addendum nel diario
pesi) lo uccide:
1. **Selezione di 2° ordine**: SKH01 fu ammesso (2026-06-23) *perché* alzava l'hold-out 2025-26;
XS01 fu affinato conoscendo l'hold-out. EW-STR vende lo sleeve che collassa OOS (TP01 IS 1.49→
HOLD 0.30) e compra i due selezionati su quell'OOS. Pre-2025: **ΔSh 0.05** (perde risk-adjusted).
2. **Su finestre OOS disgiunte**: 0.12 (2024H1) / +0.06 (2024H2) / +0.14 (2025+) — tutto l'uplift
vive nella finestra che ha selezionato gli sleeve. "7/8 anni" era sul ritorno composto; su
Sharpe è 3/8.
3. **Firma best-of-k**: su 500 tilt casuali cap-respecting, EW-STR siede al 94-100° percentile ≈
k/(k+1) con k≈15 config viste sull'hold-out → uplift indistinguibile dal *best-of-15 scelto
sull'hold-out*.
4. **Realismo**: in pesi effettivi (outer-join rinormalizzato) EW-STR = SKH01 al 40-50% del book
2019-23 (sleeve research a book 230m). Il book Deribit eseguibile (TP+SKH) non cambierebbe comunque.
Residuo onesto: il sub-tilt XS01 18.75→25 è positivo su tutte le finestre disgiunte ma edge≈0
pre-2025 e STAT-MODE → rivalutare solo con storia forward.
## GATE NUOVO codificato: `weights_tilt_null` (src/portfolio/portfolio.py)
La lezione è ora codice, come per marginal-scorer e study_family_honest:
- **`combine_outer(daily_cols, weights, lo, hi)`** — la combinazione outer-join/rinormalizzata di
`combined_daily` estratta come funzione su pesi arbitrari (riusabile per studi di tilt).
- **`weights_tilt_null(daily_cols, w_current, w_proposed, caps, floor, n, seed, k_seen)`** —
null di tilt casuali uniformi sul simplesso dentro floor/caps: riporta `delta_hold/full/insample`,
`frac_random_beat_hold`, `pctl_hold`, `bestofk_pctl=k/(k+1)` e `gate_pass`.
**Regola: un cambio-pesi si applica solo se (1) delta_insample ≥ 0 e (2) pctl_hold < firma
best-of-k** (necessario, non sufficiente: restano finestre disgiunte + realismo).
Sul caso reale EW-STR: delta_hold +0.139, delta_insample **0.054**, pctl **100°** vs best-of-15
93.75° → `gate_pass=False`. Test: `tests/test_weights_tilt_null.py` (5 test, sintetici,
deterministici; incluso lo sleeve "forte-solo-nell'hold-out" che DEVE fallire il gate).
## Lezione tecnica riusabile (dal filone 2)
`DatetimeIndex.view("int64")` su indici **tz-aware a risoluzione non-ns** (pandas 2.x) produce
timestamp in scala sbagliata → un `merge_asof` può broadcastare l'ultimo valore su tutta la storia
= **look-ahead che `causality_ok` non vede** (input fisso). Sintomo: decine di celle con Sharpe
identico. Fix: epoca esplicita in ms. Verificato: `altlib.py` non è affetto (grep pulito); dettagli
nel diario breadth.
## Onestà sul goal €50/giorno
Invariata (vedi CLAUDE.md): a capitale reale ~$600 il book onesto fa ~+€1.5/giorno atteso; la via
resta capitale + tempo + target-vol, non un tilt di pesi né leva. Questa ondata conferma che il
book attuale è vicino al massimo estraibile dai dati certificati disponibili oggi; le opzioni
reali di crescita restano (a) storia forward che promuova i lead (STATARB-RESID, breadth tra 1-2
anni), (b) capitale, (c) nuove fonti di dato (f di stress reale per VRP).
**File dell'ondata:** `scripts/research/r0701_{funding_ts,breadth_internals,xs_residmom,portfolio_opt,portfolio_skeptic,vrp_refine,xs_seasonal}.py`; diari `2026-07-01-{funding-ts-signal,alt-breadth-internals,xs-residual-momentum,portfolio-weights-ddguard,vrp-refinement,xs-seasonality}.md`; gate in `src/portfolio/portfolio.py` + `tests/test_weights_tilt_null.py`.
## ADDENDUM (stessa sera) — GTAA01 promosso a 5° SLEEVE: il miglioramento c'era, ma era in casa
Il secondo passaggio sull'archivio (non sui dati!) ha trovato il candidato che l'ondata di *ricerca*
non poteva trovare: **EQ-GTAA01** (`src/portfolio/gtaa.py` — trend difensivo equity 6-ETF, vt12%,
~30 anni di storia, netto fee IB) era stato validato il 2026-06-22 come *"primo miglioramento
STRUTTURALE del rischio-aggiustato trovato in tutta la ricerca post-reset"*, messo in paper
cross-venue (`paper_combo`, nel cron dal 2026-06-23) e **mai valutato come 5° sleeve del
portafoglio attivo** (il giorno dopo arrivò SKH01 e il filone si fermò).
### Valutazione onesta (`r0701_gtaa_5th_sleeve.py`) — perché GTAA passa dove EW-STR è morto
| gate | EW-STR (refutato) | +GTAA01 (passa) |
|---|---|---|
| uplift IN-SAMPLE (pre-2025) | **0.05** | **+0.09..+0.13** (2.134→2.23-2.26) |
| finestre OOS disgiunte | 0.12 / +0.06 / +0.14 | **+0.05 / +0.19 / +0.25** (tutte >0) |
| multi-cut (4 tagli) | solo annidate | **+0.21 / +0.21 / +0.22 / +0.25** |
| provenienza dell'edge | sleeve selezionati sull'hold-out crypto | validato su OOS equity 2015+, **indipendente** dall'hold-out crypto |
| corr al book | — | **+0.10** full, ~0.00 hold-out; is_hedge +0.30 (non-hedge) |
| plateau | gradiente generico anti-TP01 | **monotono su w 10-30%, ogni cella migliora IS/FULL/HOLD/DD** |
Convenzione di realismo cross-venue (nuova, cablata nello sleeve): nei giorni senza barra equity
(weekend/festivi US) il rendimento è **0.0** — capitale IB fermo, NON rinormalizzato sul crypto
(due conti reali non si travasano nel weekend). Numeri quasi identici alla versione outer-join
(differenze al 3° decimale) ma il modello è quello giusto. Attivazione nel book all'era crypto
(2019-03-14): senza troncatura l'outer-join farebbe partire il book nel 1996 con 23 anni di
solo-GTAA rinormalizzato al 100%.
### Decisione pesi
Cella IS-best = w=30% (bordo griglia), **ingresso @20%** per scelta strutturale dichiarata:
(a) cross-venue IB con paper operativo di soli 8 giorni — non diventa lo sleeve più grande del
book al day-one; (b) precedente: ogni sleeve nuovo è entrato ≤25% (SKH01 25%, VRP01 15%);
(c) costo CAGR (a 30%: 16.8% vs 18.8% a 20%) — e l'obiettivo €50/g vive di CAGR, non solo di
Sharpe. Tutte le celle 10-30% passano comunque ogni gate. Pesi nuovi: **TP01 33 / XS01 15 /
VRP01 12 / SKH01 20 / GTAA01 20** (i 4 preesistenti scalati ×0.80, rapporti conservati).
### Portafoglio 5-sleeve (run_portfolio, capitale 2k)
| | FULL Sh | FULL DD | FULL CAGR | HOLD Sh | HOLD DD | HOLD CAGR |
|---|---|---|---|---|---|---|
| 4-sleeve (baseline) | 2.12 | 7.8% | +23.3% | 2.21 | 3.5% | +16.0% |
| **5-sleeve (+GTAA01 20%)** | **2.24** | **6.2%** | +18.8% | **2.46** | **2.2%** | +14.6% |
Per-anno: tutti gli 8 anni positivi, 2022 +7.0% (era +11.6%: GTAA soffre il whipsaw 2022, unico
anno con dSh negativo 0.49 — dichiarato). Il trade-off è esplicito: 4.5pp di CAGR full per
+0.12/+0.25 di Sharpe e DD 7.8→6.2 / hold-out 3.5→2.2. A pesi research (XS/VRP/SKH modellati o
STAT-MODE) il CAGR era comunque di carta; lo Sharpe/DD del book è la metrica di verità.
### Cosa NON cambia
Il **book live Deribit** (`deribit_book_sleeves` TP01+SKH01 75/25, `book_execute`) è INTATTO —
GTAA è su IB, già tracciato forward da `paper_combo`. `paper_portfolio` da oggi traccia la
composizione a 5 (regime-break dichiarato qui). Test: `tests/test_gtaa_sleeve.py` (registry,
weekend-zero, attivazione, book live invariato) — suite 168/168.
### Lezione dell'addendum
Prima di una nuova ondata di *ricerca*, ri-leggere l'archivio dei **validati-ma-non-integrati**:
l'unico upgrade onesto del book in 3 ondate (104+5+6 filoni) era un edge già validato che
aspettava solo la valutazione da sleeve. La diversificazione strutturale (secondo mercato,
corr ~0.1) resta l'unica cosa che alza lo Sharpe del book senza nuovo alpha.
+98
View File
@@ -0,0 +1,98 @@
# 2026-07-01 — Affinamento VRP01 (sizing IVRV / filtro DVOL-momentum / gate TP01): NON MIGLIORA
**Script:** `scripts/research/r0701_vrp_refine.py` (solo ricerca: `src/` e `scripts/live/` non toccati).
**Esito: NON MIGLIORA — VRP01 baseline resta la config canonica.** La cella IS-best (gate TP01-skip)
è un falso positivo in-sample da manuale: IS Sharpe 2.07 ma multi-cut **0/5**, hold-out **0.14**
(vs 0.59 del baseline), DSR 0.71. Nessuna modifica a `sleeves._vrp_combo_returns`.
## Cosa testava (celle NUOVE, verificate non ripetute nei diari)
Stessa pipeline di `options_vrp_v2.py` (pricing BS su DVOL reale, payoff sul path certificato,
fee 12.5% del premio, f=1.0, tenor 7d, spread 0.28/0.10) — cambiati SOLO gate/sizing:
1. **Sizing sul gap IVRV** (carry atteso): size lineare `clip(vrp/scale,0,1)` (scale 8/12 pt) o
percentile espandente causale del VRP; in aggiunta o in sostituzione del gate binario IV-rank.
NB: il gate composito "IV-rank>0.30 AND IVRV>0" è **già il baseline** (`gate_vrp=True`).
2. **Filtro DVOL-momentum**: non vendere se `dvol[i]dvol[ik] > thr` (k∈{5,10}, thr∈{0, 5pt}).
(Diverso da `dvol_directional` 2026-06-29, dove il DVOL-mom era segnale direzionale sul perp.)
3. **Gate di regime da TP01**: skip o half-size quando TP01 è flat su entrambi gli asset (risk-off).
4. Croce completa: **105 celle**, tutte contate nel deflated-Sharpe.
NON ripetuto (già scartato): gestione attiva intra-trade (PT/SL/vol-stop/delta-exit,
`2026-06-20-vrp-active-management.md` → hold-to-expiry vince).
Metodo: selezione cella **in-sample (pre-2025)** con floor di attività IS ≥20%, hold-out 2025-26,
**multi-cut 5 tagli** (2023-01→2025-01, richiesti ≥4/5 uplift>0), `altlib.deflated_sharpe` su tutti
i 105 trial, effetto sul **portafoglio 4-sleeve** (TP01 41.25 / XS01 18.75 / VRP 15 / SKH01 25).
## Risultati (book 50/50 BTC+ETH, rendimenti settimanali)
Baseline riprodotto esattamente: **FULL Sh 1.09 / IS 1.26 / HOLD 0.59 / DD 12% / worst 7.4% / attivo 41%.**
| cella | IS Sh | FULL | HOLD | DD | worst | attivo |
|---|---|---|---|---|---|---|
| **BASELINE** bin+ivr30 | 1.26 | **1.09** | **0.59** | 12% | 7.4% | 41% |
| sizing lin 8pt (+ivr30) | 1.20 | 1.05 | 0.61 | 11% | 7.4% | 41% |
| sizing lin 12pt (+ivr30) | 1.12 | 0.98 | 0.54 | 11% | 7.4% | 41% |
| sizing rank (+ivr30) | 1.07 | 0.94 | 0.54 | 9% | 5.7% | 41% |
| sizing lin 8pt **al posto di** ivr | 0.89 | 0.58 | **0.38** | 21% | 7.4% | 86% |
| sizing rank al posto di ivr | 1.03 | 0.69 | **0.36** | 11% | 5.7% | 86% |
| mom k=5 thr=5pt | 1.24 | 1.02 | 0.37 | 9% | 6.5% | 34% |
| mom k=10 thr=5pt | 1.26 | 1.08 | 0.55 | 9% | 6.5% | 31% |
| mom k=5 thr=0 | 1.78 | 1.24 | 0.10 | 5% | 3.9% | 20% |
| mom k=10 thr=0 | 1.36 | 0.78 | **0.47** | 7% | 3.9% | 18% |
| tp01-skip | **2.05** | 1.43 | **0.14** | 7% | 4.7% | 27% |
| tp01-half | 1.72 | 1.34 | 0.27 | 7% | 4.7% | 41% |
| IS-best: lin8+tp01-skip | **2.07** | 1.43 | **0.14** | 6% | 4.2% | 27% |
**Multi-cut della IS-best vs baseline: uplift negativo in 5/5 tagli** (0.35 → 0.73, monotono
peggiorando verso il presente). **DSR** IS-best 0.709 (<0.95); baseline 0.472 dentro questa griglia
(contesto onesto: nemmeno il baseline è statisticamente distinguibile su 105 trial correlati — ma è
pre-committed, non selezionato qui). Celle che battono l'hold-out del baseline: **1/105** (lin8pt
tp-off, +0.02 — rumore; non selezionabile: sarebbe selection-on-holdout).
### Frequenza d'intervento dei gate (settimane-trade del baseline = 163)
- **TP01-skip: blocca il 32.5%** delle settimane tradabili (TP01 flat il 38% dei giorni della
finestra DVOL). NON è ridondante col baseline — lavora tanto — ma lavora **male** OOS.
- mom thr=5pt: blocca 24-32%; mom thr=0: blocca 60-64% (quasi metà del book).
### Portafoglio 4-sleeve (VRP @15%)
| VRP nello slot | FULL Sh | FULL DD | HOLD Sh | HOLD DD |
|---|---|---|---|---|
| **baseline** | **2.12** | 7.8% | **2.21** | 3.5% |
| IS-best (lin8+tp01-skip) | 2.11 | 7.8% | 2.12 | 3.5% |
Anche a livello portafoglio la variante **peggiora** l'hold-out (2.21→2.12) e non migliora nulla.
## Lettura (perché non migliora)
1. **L'alpha del VRP è già tutto nel gate IV-rank binario** ("vendi solo vol ricca"). Il sizing
proporzionale al gap IVRV è ridondante *dato* il gate (Δ ≈ ±0.05, dentro il rumore); usato al
posto del gate, riporta dentro le settimane di vol povera e distrugge l'hold-out (0.36/0.38).
Il carry atteso (IVRV) NON è un segnale di *intensità* utile oltre il suo segno+rank.
2. **Il gate TP01 è la trappola in-sample perfetta** (la lezione macro-gate al contrario): schiva il
2022 (6%→0%) gonfiando l'IS a 2.05, ma nel 2025-26 TP01 è flat proprio nei regimi di vol ricca
in cui la vendita gated di put **guadagna** — il gate taglia le settimane migliori dell'hold-out
(0.59→−0.14). Short-vol gated e trend sono complementari, non condizionabili l'uno sull'altro.
3. **Il DVOL-momentum non aggiunge protezione**: il crash-skip IV-rank>0.90 + il long wing dello
spread coprono già il run-up di vol; thr=0 è troppo aggressivo (blocca ~60%), thr=5pt è neutro
(mai meglio del baseline su hold-out).
## Onestà / caveat (invariati)
Premio MODELLATO su DVOL ATM (no skew), book 1d, f di stress reale non catturato → tutto questo
resta dentro il modello; il verdetto massimo possibile era "sleeve modellato migliorato", e non è
stato raggiunto. Il DSR sulla griglia è un limite *superiore* (celle correlate + ~20 config storiche
di options_vrp_lab/_v2 non contate).
## Azione
**Nessuna.** VRP01 resta com'è (`bin + gate_vrp + ivr30 + crash-skip 0.90`, hold-to-expiry).
Terzo tentativo di affinamento fallito (dopo active-management e — per il portafoglio — DVOL
overlay): il filone "migliorare VRP01 dentro il modello" è da considerarsi **esaurito**; il prossimo
passo utile resta quello noto: f di stress reale quando cerbero-bite cattura un crash + skew reale
sul long wing.
Script: `scripts/research/r0701_vrp_refine.py` (`--skip-portfolio` per la sola griglia).
@@ -0,0 +1,74 @@
# 2026-07-01 — Residual (idiosyncratic) momentum cross-sectional sui 19 major HL — SCARTATO/REDUNDANT
**Tesi.** STATARB-RESID (thread 4, 2026-06-29) ha mostrato che il momentum del residuo ETH−β·BTC
(β OLS rolling, **sgn=+1**: le dislocazioni continuano a 1d) passa quasi tutti i gate su 2 gambe,
fallendo solo il deflated-Sharpe (0.929<0.95). Ipotesi qui: portare lo **stesso meccanismo
cross-sectional** sui 19 major di XS01 — per ogni alt, residuo vs β·BTC (β OLS rolling B), momentum
del residuo (blend z-score [30,90] come XS01 o singolo L), long top-k / short bottom-k, vol-target
20% — sperando che la **breadth** (18 stream invece di 1) alzasse il DSR dove il 2-gambe falliva.
Script `scripts/research/r0701_xs_residmom.py` (riusa l'harness collaudato `xsec_v2_nonmom`:
load_matrix, xs_engine — uguaglianza verificata maxdiff 0 —, deflated_sharpe, StrategyPortfolio).
Distinzione dal già-testato: IREV (idio-**reversal**, sgn=1) era FALLITO nel filone C; IMOM di
xsec_v2 usava il mercato equal-weight come fattore, B=60 fisso, senza blend ed era solo riferimento.
Qui fattore = **BTC** (come STATARB-RESID, BTC escluso dal cross-section), B in griglia, blend
z-score, probe con gate di dispersione p30 (parità strutturale con XS01), **selezione in-sample-only**.
## Setup
- Universo: 19 major HL certificati (`data/raw/hl_*_1d.parquet`), 913 giorni [2024-01-01 → 2026-07-01],
barre vol=0 escluse. Fee 0.10% RT/gamba + sweep fino a 0.30%.
- Griglia (57 trial): β-window {60,90,120} × lookback {blend[30,90], 30, 90} × k {3,5} × H {5,10,20}
+ 3 probe gated (blend, k5, H10, gate dispersione p30). Cella scelta col solo Sharpe **pre-2025**;
hold-out 2025-01-01 bloccato. Causalità: prefix-check ok (max_tail_diff 0).
- Baseline (stessa finestra, sleeve attivi): XS01 FULL Sh **1.38** / HOLD **1.51** / DD 11%;
book 4-sleeve FULL 2.12 / HOLD 2.21.
## Esito — collasso OOS della cella in-sample e nessuna cella competitiva
Cella scelta in-sample: **B60 L30 k3 H20** (IS Sh 2.00):
```
FULL +0.67 | HOLD 0.24 | DD 35% | per-anno 2024 +54% / 2025 15% / 2026 +5%
corr→XS01 +0.54 | corr→TP01 0.09 | DSR 0.510 (57 trial) | fee-sweep ok (F+0.59 a 0.30%)
small-cap $600/min$5: haircut ≈ 0 (ma book multi-gamba → STAT-MODE comunque, come XS01)
```
Portafoglio (il bar del mandato):
```
BASE 4-sleeve FULL 2.12 DD 7.8% | HOLD 2.21 DD 3.5%
SOSTITUZIONE XS01 → cand FULL 2.01 (0.10) | HOLD 1.18 (1.03) ← distrugge l'hold-out
AGGIUNTA 5° sleeve @10/15% HOLD 0.31 / 0.53 ← diluisce
MARGINAL vs BOOK: corr +0.23, uplift@10% full 0.04 / hold 0.31, multi-cut 2025 0.31 / 2026 0.14
```
**Il verdetto non dipende dalla selezione** — aggregato delle 57 celle:
- FULL max **+0.94**, HOLD max **+0.82** (mediane +0.52/+0.33): *nessuna* cella si avvicina a XS01
(1.38/1.51), nemmeno cherry-pickando sull'hold-out.
- corr→XS01 in [+0.47, +0.71], mediana **+0.62**: è momentum XS01 travestito (la residualizzazione
vs β·BTC sposta poco: lo z-scoring cross-sectional di XS01 già rimuove implicitamente il mercato).
- Solo 4/57 celle passerebbero un gate "diversificatore" (|corrXS|<0.6, FULL>0.5, HOLD>0), tutte con
FULL ≤0.68 e IS incoerente.
- **corr(Sharpe in-sample, Sharpe hold-out) attraverso le celle = 0.37**: l'in-sample è
anti-predittivo dell'OOS su questa griglia — la firma del rumore, non di un edge con plateau.
## Perché la tesi è falsa (3 righe)
1. La breadth non ha alzato il DSR (0.51 ≪ 0.95) perché non c'è edge da deflazionare: il residuo
vs β·BTC sul cross-section degli alt NON eredita l'edge del 2-gambe ETH−β·BTC — quel lead vive
nella coppia specifica ETH/BTC (i due asset profondi/cointegrati), non nel long-tail degli alt.
2. Cross-sectional, la residualizzazione è quasi un no-op: demeaning z-score (XS01) ≈ togliere il
mercato → il candidato è una variante rumorosa di XS01 (corrXS mediana 0.62) con meno segnale.
3. In-sample = solo 2024 (~1 anno utile dopo il warmup): la selezione onesta atterra su una cella
2024-lucky che perde nel 2025-26 — l'ennesima conferma del gate selection-on-holdout.
## Verdetto: **SCARTATO/REDUNDANT.** Nessuno sleeve, nessun forward-monitor.
Coerente con il filone momentum-structure (2026-06-29): l'edge di XS01 sta nella sua struttura
specifica (blend + gate dispersione), e le sue varianti — incluso l'idio-momentum vs BTC — sono
REDUNDANT o peggio. Chiude anche il cerchio su STATARB-RESID: il meccanismo residuo-momentum resta
valido SOLO come coppia ETH/BTC (forward-monitor `paper_statarb`), non generalizza al cross-section.
Nessun impatto sul book live; config canonica invariata.
Script: `scripts/research/r0701_xs_residmom.py` (non committato, prefisso r0701_xs).
+69
View File
@@ -0,0 +1,69 @@
# 2026-07-01 — Stagionalità CROSS-SECTIONAL sull'universo Hyperliquid → SCARTATO
## Ipotesi
Effetti calendario **relativi tra i 19 alt major HL** (weekday tilt, turn-of-month,
pattern weekend→lunedì nel cross-section), long/short market-neutral. L'angolo è diverso
dal prior art già morto (trackF seasonality BTC/ETH = buy&hold travestito;
crypto_weekend_signal/overnight = look-ahead; open_drive = artefatto calendario ucciso da
`day_boundary_robust`): essendo il segnale **demeaned cross-section**, il buy&hold
travestito è strutturalmente escluso… *in prima battuta* (vedi diagnostica beta sotto).
## Metodo (test statistico PRIMA della strategia)
Script `scripts/research/r0701_xs_seasonal.py`. Dati `data/raw/hl_*_1d.parquet`, 19 major
XS01 (lista `src/portfolio/sleeves.XS_UNIVERSE`), **912 giorni** (2024-01-02 → 2026-07-01,
~2.5 anni), 0 barre vol=0. Ritorno relativo x[t,a] = r[t,a] media cross-section del
giorno. Per ogni bucket calendario: **tilt bucket-specifico** per asset = media di x nel
bucket tilt incondizionato dell'asset (isola il calendario dal drift relativo generico).
**Persistenza split-half**: Spearman rank-corr del vettore tilt (19 asset) H1 vs H2, per
bucket. **Null**: 2000 permutazioni delle etichette entro ciascuna metà → distribuzione del
**max-statistic** (controlla il multiple-testing sui bucket e il confound "alpha persistente
dell'asset"). Soglia severa p<0.05 sul max. Regola del mandato: senza persistenza →
**nessun backtest**.
## Numeri
| Test | Statistica | Null 95° pctl | p | Esito |
|---|---|---|---|---|
| [A] Weekday (7 bucket) | max rank-corr **+0.511** (Wed); Mon +0.38, Sun +0.37, Fri +0.04 | +0.604 | **0.159** | FAIL |
| [B] Weekend vs feriali | +0.268 | +0.428 | **0.178** | FAIL |
| [C] Turn-of-month (±2gg) | +0.221 | +0.451 | **0.226** | FAIL |
| [D] IC weekend→lunedì | FULL 0.053 (t=1.94); H1 0.006 (t=0.17) vs H2 0.100 (t=2.4) | — | non persiste | FAIL |
Con ~65 osservazioni per weekday per metà, lo std di una rank-corr su 19 asset è ~0.24 → il
max-su-7 sotto puro rumore arriva a ~0.60: il +0.51 reale è *dentro* il rumore.
## Diagnostica: perché tutte le rank-corr weekday sono positive?
Non è tilt idiosincratico, è il **canale-beta**: il mercato EW ha un pattern weekday di
*livello* che si ripete nelle due metà (Sun 42/30 bps, Sat +26/+25, Mon +35/+32) e la
dispersione di beta (0.53→1.26) lo proietta nel cross-section — rank-corr(tilt, beta):
**Sun 0.642**, Sat +0.325, feriali ~0. Cioè la poca "persistenza" visibile è l'effetto
weekday di MERCATO (dominio trackF, già scartato come buy&hold travestito) che rientra
dalla finestra via beta. Un eventuale L/S "weekday tilt" sarebbe stato in gran parte un
book short-beta-la-domenica — non un edge calendario cross-sectional.
## Note sui gate non raggiunti
- **day_boundary_robust** (obbligatorio per ogni effetto calendario): NON eseguibile sui
19 alt — i dati HL locali sono **solo 1d**, il confine giorno non è ri-tagliabile. Anche
con persistenza, il verdetto massimo sarebbe stato LEAD-forward *condizionato* a barre
orarie HL, mai sleeve. Con lo SCARTATO allo step 1 il punto è moot, ma resta il vincolo
per chiunque riapra il filone.
- **Fee**: mai arrivati al backtest; nota strutturale: un weekday-tilt L/S ribilancia ogni
giorno (weekday di domani ≠ oggi) → turnover ~2×gross/die, morte-per-fee quasi certa a
0.10% RT anche con edge lordo.
- **DSR**: non calcolato (nessuna cella backtestata — il branch strategia esiste nello
script ma non si attiva senza persistenza).
## Verdetto
**SCARTATO** allo step statistico, senza backtest. Tre motivi: (1) nessuna persistenza
split-half sopra il null permutato (p 0.160.23 su tutti i test, IC weekend→lunedì non
persiste tra le metà); (2) la sola struttura visibile è il canale-beta di un pattern
weekday di mercato — la stessa famiglia già scartata in trackF, non un effetto
cross-sectional; (3) ~2.5 anni × effetto calendario × turnover giornaliero = rumore +
morte-per-fee anche nello scenario migliore.
File: `scripts/research/r0701_xs_seasonal.py` (deterministico, seed 20260701, ~2 min).
@@ -0,0 +1,132 @@
# 2026-07-02 — Audit anchor timing-luck su XS01 e SKH01 (chiusura del pendente dell'ondata TIMING)
**Contesto.** L'ondata TIMING+CRT dello stesso giorno (`2026-07-02-timing-crt-wave.md`) ha stabilito
che l'hold-out di TP01 (0.31) è la **migliore delle 24 ancore daily possibili** (mediana 0.04,
P~0.86 di uno spike così per caso). Lo scettico ha flaggato l'audit come **pendente** sugli altri
due sleeve a ribilanciamento/griglia ancorata: **XS01** (fase del ciclo di ribilanciamento H=10
giorni) e **SKH01** (offset della griglia dual-TF 230m/690m, `origin='epoch'`). Questo diario
chiude entrambi. Due agenti indipendenti, metodologia identica allo scettico TP01
(`r0702_skeptic_offset.py`): ancore dichiarate **a priori**, parametri **identici** in ogni
fase/offset (zero tuning per-ancora), **sanity replica bit-exact** della sleeve canonica prima di
qualsiasi numero, block-bootstrap congiunto per P(spike).
**Script:** `scripts/research/r0702_anchor_xs01.py`, `scripts/research/r0702_anchor_skh01.py`.
Nessun file di produzione toccato dagli agenti (solo docstring/narrativa aggiornate a valle, qui).
---
## Verdetto in una riga
**Il finding TP01 si replica su TUTTI e tre gli sleeve ancorati — su SKH01 più forte che su TP01 —
e i numeri headline del book (HOLD 2.46) sono in parte best-of-anchors.** Ciò che sopravvive
de-luckato: XS01 resta un diversificatore ammissibile (ensemble di fase FULL 1.25 / HOLD 1.31 /
DD 11%); SKH01 resta ADDS (uplift del blend positivo a **tutte** le 23 fasi, corr ~0.08) ma i suoi
numeri headline e — cruciale — **il gate DD<30% che l'ha selezionata** non reggono alla fase
mediana. Stima onesta de-luckata del book 5-sleeve: **HOLD ~1.92.1, FULL ~2.02.2** (vs 2.46/2.24
dichiarati).
---
## XS01 — fase del ciclo di ribilanciamento H=10 (10 fasi)
Sanity: replica di `_xsec_returns` con parametro `phase`**max|Δ|=0.0 bit-exact** vs la canonica.
NB: la canonica sui dati di oggi fa FULL 1.367/HOLD 1.479/DD 10.8% (i dichiarati 1.50/1.71/11%
erano del 2026-06-19; deriva dati, confronto interno coerente).
| metrica | min | mediana | max | canonica (f0) | pctl f0 |
|---|---|---|---|---|---|
| Sh FULL | 0.41 | 1.21 | 1.64 | 1.37 | 85° |
| Sh HOLD | 0.15 | 1.17 | 1.71 | 1.48 | 65° |
| maxDD | 10.0% | 15.5% | 29.1% | 10.8% | **15°** |
- **Firma diversa da TP01: la fortuna NON è nell'hold-out** (65° pctl, non estremo) **ma nel
DD** (15° pctl: il "DD 11%" dichiarato è uno dei più bassi delle 10 fasi; la fase tipica fa
~15.5%, la peggiore 29%) e nel FULL (85°).
- **Bootstrap**: spike osservato +0.38 su hold-out; **P(una fase qualsiasi ≥ +0.38) = 0.910.94**
(block 10/20/40, B=4000) → la canonica è il massimo atteso di 10 stime correlate (corr mediana
fra fasi 0.71).
- **Ensemble 10 fasi** (lens di reporting onesta): **FULL 1.25 / HOLD 1.31 / DD 10.9%** — la
diversificazione di fase riporta il DD al livello canonico con Sharpe ≈ mediana.
- **Book 5-sleeve**: HOLD 2.44 (canonica) → 2.26 (fase mediana) → 1.75 (peggiore, marginalmente
PEGGIO del book senza XS01 = 1.77) → 2.35 (ensemble). **Eredità di fortuna ~+0.17 HOLD.**
FULL quasi insensibile (2.102.28).
- **Gate di ammissione a fase mediana**: FULL 1.08 / HOLD 1.10 / DD 20.7% — **i numeri dichiarati
non reggono** (Sharpe 0.3/0.6, DD raddoppia). **La decisione però regge**: nessuna fase
negativa, 8/10 con FULL≥1.0, book migliore a ogni fase tranne la peggiore (dove è neutro) →
slot da diversificatore @15% difendibile; cambia la NARRATIVA (banda/ensemble, non 1.50/1.71/11%).
- **Caveat**: storia ~2.5 anni (CI95 Sh HOLD canonica **[0.04, +2.72]** — nessuna stima puntuale
affidabile); dimensione **ora-del-giorno non testabile** (solo barre 1d HL: resta luck residua
10 fasi × 24 ore non certificabile); XS01 è STAT-MODE → impatto su reporting/gate, non sul book
live Deribit.
## SKH01-V2-DD — offset della griglia dual-TF 230m/690m (23 offset × 30m)
Sanity: offset 0 riproduce `_skyhook_returns()` **bit-exact** (max|Δ|=0.0 su 2666 giorni); maxDD
harness BTC 21.4%/ETH 27.4% = identici al diario di ammissione; riuso diretto di
`skyhook_entries`/`htf_features`/`merge_htf_to_ltf` con `resample(origin='epoch', offset=...)`,
confini 690 ⊂ 230 verificati a ogni offset.
| metrica (book 50/50) | min | mediana | max | off0 (canonico) | pctl off0 |
|---|---|---|---|---|---|
| Sh FULL | 0.78 | 1.05 | 1.48 | 1.46 | 93° |
| Sh HOLD | 0.44 | 0.93 | 1.64 | 1.64 | **98° (max)** |
| minFull (2 asset) | 0.35 | 0.67 | 1.01 | 0.99 | 93° |
| minHold | 0.10 | 0.39 | 1.25 | 1.25 | **98° (max)** |
| blend 0.75·TP01+0.25·SKH, HOLD | 0.49 | 0.72 | 1.16 | 1.16 | **98° (max)** |
- **È uno spike, non un plateau**: gli offset adiacenti ±30m crollano (book HOLD 1.64 → 0.88 a
off30). Firma identica all'ancora TP01, ma **più forte**: il canonico è al 9398° pctl di OGNI
metrica; minHold +1.26 e blend "0.31→1.17" sono il **massimo dei 23**.
- **Gate DD<30% — il criterio per cui V2-DD fu selezionata — FALLISCE in 15/23 offset**
(mediana DD: BTC 23.5%, ETH **29.2%** — pass per 0.8pt; peggiori 35.9%/42.1%). Il margine ETH
"già sottile" (27.4%) è in realtà **sotto la mediana di fase**: la proprietà DD<30% è in larga
parte fortuna di fase.
- **Ciò che regge de-luckato**: **uplift del blend positivo a TUTTE le 23 fasi** (min +0.18,
mediana +0.42, canonico +0.86) e corr→TP01 in [0.05, 0.11] a ogni fase → il verdetto
ADDS/diversificatore sopravvive, ridimensionato. Alla fase peggiore invece non regge
(minHold 0.10, DD fail).
- **Bootstrap**: spike blend +0.44; **P(un offset qualsiasi ≥ +0.44) ≈ 0.70** (block 10/20/40);
P(uplift≤0) = 0.030.04 → l'uplift è reale, l'ampiezza è best-of-23.
- **Book 5-sleeve**: HOLD 2.44 (off0 = max dei 23) → mediana **1.96** → ensemble 2.12; FULL 2.24 →
mediana 2.01. **Eredità di fortuna ~+0.5 HOLD** — molto più dei ~+0.10 dell'ancora TP01.
- **Rilevanza LIVE (SKH = 25% del book Deribit reale)**: il cron è orario, i confini 230m no →
ritardo uniforme {0..50} min (media 25). Re-sim del path reale (fill al close 5m del prossimo
run orario, exit SOFTWARE come da `book.py`): book 50/50 **FULL 1.46→1.19, HOLD 1.64→1.15,
DD 18.1%→24.6%**. La maggior parte del gap è l'assunzione fill-al-livello del backtest, non il
ritardo orario in sé: nei crash le barre 230m **gappano attraverso lo SL software** (short
sl2% modellato 2% → realizzato **11/23%**: ETH 2021-05-19, 2021-01-04, COVID 2020-03-13).
Il disaster-SL on-book 30% copre solo la coda estrema della posizione netta.
- **Caveat**: equity daily-step (lens Sharpe, come il canonico); l'ensemble di offset NON è
eseguibile live (gira una sola griglia) → serve solo come stima de-luckata; sim hourly senza
slippage/parziali.
## Book 5-sleeve — stima de-luckata congiunta (approssimata)
Ogni audit varia UN solo sleeve (gli altri restano canonici), quindi gli effetti non si sommano
esattamente; ordine di grandezza dell'eredità di fortuna sull'HOLD 2.46: TP01-ancora ~+0.10,
XS01-fase ~+0.17, SKH01-offset ~+0.5. **Stima onesta: HOLD ~1.92.1, FULL ~2.02.2, DD ~6%
(invariato)** — il book resta largamente positivo e diversificato a ogni combinazione testata,
ma il 2.46 è un massimo di ~24×10×23 configurazioni di ancora, non la stima centrale.
## Regole codificate in narrativa (CLAUDE.md aggiornato)
1. **I numeri headline di OGNI sleeve ancorato si citano con la banda d'ancora** (o come ensemble
di fase): vale già per TP01, ora anche per XS01 e SKH01 e per l'HOLD del book.
2. **Un gate di selezione (es. DD<30%) va verificato sulla banda di fase, non sull'ancora
canonica** — SKH01-V2-DD fu selezionata per una proprietà che tiene in 8/23 fasi.
3. Il candidato gate `anchor_luck_band()` in altlib sale di priorità: tre sleeve su tre ancorati
mostrano la stessa firma, e il deflated-Sharpe non conta questo multiple-testing.
## Follow-up (non eseguiti, da valutare)
- **Rivalutare il peso 25% di SKH01 nel book live Deribit**: l'aspettativa onesta della gamba SKH
è ≈ fase-mediana + path orario (blend HOLD ~0.72, non 1.17) con gap-through-stop reale nei
crash. L'uplift resta positivo a ogni fase → non c'è motivo di rimuoverla, ma il sizing fu
deciso su numeri best-of-23.
- Cadenza cron a 230m (o trigger sui confini di barra) per chiudere il gap di path orario.
- `anchor_luck_band()` in `altlib.py` come gate standard per candidati ancorati.
- Audit ora-del-giorno di XS01 quando esisterà un feed HL intraday certificato.
**Stato:** XS01/SKH01/pesi/book live INVARIATI (nessun cambio operativo da questo audit — è un
audit di reporting e di gate, e ogni cambio pesi dovrà passare `weights_tilt_null` +
selection-on-holdout). Test suite 168 verdi. Script audit committati.
@@ -0,0 +1,168 @@
# 2026-07-02 — Ondata "video claims": Elliott (3 filoni), Albimarini (2), capital scaling 2-5k
**Contesto.** Su richiesta: meccanizzare e testare le claim falsificabili di 4 video didattici
italiani (3 sulle onde di Elliott, 1 ricostruzione della "strategia Albimarini" — double diagonal
a credito su SPX), più l'audit dei muri di scala visto l'annuncio che il conto live verrà portato
a **2.000-5.000 $**. Sei agenti indipendenti, harness altlib, fee 0.10% RT, hold-out 2025-26 mai
usato per selezionare, banda d'ancora obbligatoria (regola dello stesso giorno). Nessun PASS →
nessuno scettico necessario. Nessun file di produzione toccato.
**Scripts:** `scripts/research/r0702_ell_{rangecycle,fibconfluence,channel}.py`,
`r0702_alb_{structure,claims}.py`, `r0702_capital_scaling.py`.
---
## Verdetto in una riga
**Elliott 3/3 SCARTATO** (range-cycle = rumore 0/24 Bonferroni; canale = Donchian travestito che
non batte il Donchian; Fibonacci = effetto posizione, non numeri magici). **Albimarini 2/2
SCARTATO** (la diagonale non domina nemmeno nel modello ed è fee-negativa su Deribit; il track
record dichiarato è consistente con zero skill al 20-45%, e va in rovina in ogni finestra storica
con una coda). **Capital scaling: l'unico vincolo vero a 2-5k è il cap $300/asset** — da alzare a
equity/2 al momento del deposito; tutto il resto si scioglie da solo o resta fuori comunque.
---
## ELL-A — "range-cycle" (onda 1 compressa → onda 3 ampia): RUMORE
Claim di Frost&Prechter/Larry Williams meccanizzato senza conteggio soggettivo: 2 definizioni di
segmento (flip TSMOM, zigzag k·ATR causale), impulso iniziale (N∈{3,5,10} barre, range/ATR
pre-segmento) vs ampiezza del seguito (claim A) e troncatura dell'ultimo terzo (claim B).
BTC/ETH 1d+1w, Spearman con null permutato (2000), Bonferroni m=24.
- **0/24 celle significative** (miglior p raw: A 0.061, B 0.058; Bonferroni 1.00 ovunque, FULL e
pre-2025). 7-9/12 celle col segno "giusto" = moneta su celle correlate.
- **Nessuna cella weekly regge a tutte e 7 le ancore** (p 0.02-0.05 di singole ancore = il
timing-luck d'ancora che da oggi non crediamo più; alcune celle flippano segno tra ancore).
- **Artefatto previsto e smascherato**: normalizzando col range POST-impulso la correlazione
diventa negativa per pura meccanica del denominatore — chi "conferma" il claim così misura
l'artefatto.
- Coerente con le famiglie di compressione già fallite nello sweep 104 (BRK08 NR7, BRK10
BB-squeeze, CMB05, VOL12). Campione strutturalmente piccolo (mediana 78 segmenti daily, ~30
weekly). Strategizzazione correttamente non eseguita (statistica prima della strategia).
⚠️ **LEZIONE PANDAS (gemella di `view("int64")` del 2026-07-01):** `resample("7D",
origin=...)` in pandas 2.x **ignora silenziosamente `origin`** ("D" non è Tick-like, solo un
RuntimeWarning) → tutte le ancore weekly uscivano identiche e la banda d'ancora sarebbe stata una
banda finta di larghezza zero. Serve **`"168h"`**. Da ricordare per ogni futura banda d'ancora
settimanale.
## ELL-B — Confluenza di Fibonacci: EFFETTO POSIZIONE, non numeri magici
Zigzag causale non-repainting (livelli usabili solo dalla conferma del pivot, causality_ok
max_tail_diff=0.0), tocco fresh intrabar, reazione ATR-normalizzata a 5/20 barre, ~24k/14k tocchi
IS a 1h. **Tre null in scala di durezza**: (A) rapporti uniformi casuali, (B) ri-sorteggiati per
swing, **(C) location-matched** (Fib ± jitter 2-8%: "0.618 è speciale vs 0.58/0.66?").
- Vs null A/B i Fib sembrano buoni (pctl 0.82-1.00) — **vs null C crollano a 0.39-0.68 = puro
caso**. L'apparente vantaggio è interamente l'effetto di *posizione* (i rapporti Fib cadono in
zone mediamente decenti del range), non dei numeri di Fibonacci.
- **Confluenza** (2 griglie entro 0.25·ATR): FAIL 4/4 celle — non reagisce più delle confluenze
di rapporti casuali. Break-rate al tocco ~46-50% (moneta); su 1h i Fib veri vengono bucati più
spesso del placebo.
- In assoluto comunque nulla da tradare: +2.6/+5.6 bps a 5 barre (fee = 10 bps), negativo a 20
barre e su 1d. Strategizzazione correttamente saltata da gate a priori.
- **Pattern di test riusabile**: il null location-matched è IL test giusto per ogni claim su
"livelli speciali" (Fib, pivot, round numbers) — separa la qualità della posizione dal numero.
## ELL-C — Tecnica del canale (Ftaonline): DONCHIAN TRAVESTITO
Implementazione onesta completa (zigzag causale, vincolo onda-2>origine, canale 0→min-2 con
parallela dal max-1, entry al close della barra fuori, target 1.618×, stop=invalidazione,
speculare short, 27 celle TF×k×varianti).
- **family_honest boccia**: cella scelta in-sample (1h k4 long, IS 1.40) → hold-out **0.87**;
DSR 0.685 < 0.95; earns_slot=False. Marginale vs TP01: uplift positivo solo 2020-21 =
beta-trend del toro (corr 0.35).
- **Il Donchian banale a pari geometria/frequenza eguaglia o batte il canale a ogni TF**
(corr 0.43-0.53): vincoli d'onda + 1.618 non aggiungono nulla. Il segnale-2 ("onda 5") è
quasi inerte (+0.00-0.07).
- **Target 1.618 = geometria, non magia**: toccato con la frequenza di distanze identiche
piazzate a caso (5/6 celle p>0.18). Claim discriminante "in canale = correttivo":
direzionalmente sensata, mai significativa (p 0.08-0.45); l'uscita dal canale è momentum da
breakout, già in casa.
- **Anchor-luck di nuovo**: la miglior cella 4h fa hold 1.54 all'ancora 00:00, la migliore delle
4 (banda [0.35, 1.54]) — quarta conferma della firma nello stesso giorno.
## ALB-A — Double diagonal "Albimarini" su BTC/ETH: NON DOMINA, niente LEAD
Portata su Deribit (che ha le scadenze giornaliere per la long T+1) col motore DVOL di VRP01
(riprodotto esatto: 1.09/0.59/11.8%), strike scalati in vol (2σ/5g ≈ il "9% su SPY"), gate
IV-rank canonico non riottimizzato, 2021-26 con LUNA/FTX/2022, banda skew f∈{0.6,0.8,1.0,1.3}.
- **Il claim strutturale del corso è falsificato**: gated, l'iron condor stessa-scadenza batte la
diagonale a ogni f (1.05 vs 0.95 a f=1.0), Δ negativo ogni anno. La long T+1 è **assicurazione
di coda quantificata** (+12/+33 bps nei 5% peggiori vs ~1 bps di costo medio), non dominanza.
- **Senza gate tutte le strutture perdono** a f≤1.0 → terza conferma indipendente: nel VRP
l'alpha è il filtro di regime, non la vendita di premio. VRP01 resta superiore su tutta la
banda f (CAGR 8.3% vs 1.4%).
- **Il meccanismo dell'illusione riprodotto in vitro**: celle z≥2.5 = 0 perdite su 142 trade,
Sharpe 5.9-∞ (il punto cieco CC01 in forma opzioni); la best in-sample (7.88) crolla a 0.51
in hold-out. Seconda occorrenza del pattern "0-perdite = Sharpe implausibile" in una settimana
**si rafforza il caso per il gate `implausible_sharpe` in altlib** (raccomandato dal filone
CC01, ora con 2 casi d'uso).
## ALB-B — Audit dei numeri dichiarati (28 trade, 82%, PF 5.16, "420% annuo"): ZERO SKILL
SPY 1d 1996-2026 (parquet già in repo dal filone GTAA) + BTC/ETH certificati; MC 200k; replay
claim-anchored calibrato per riprodurre esattamente il video.
- **L'82% è incoerente col "deep OTM" in entrambe le letture**: strike a 9% → win strutturale
99.5% (P(|move 4td|>9%) = 0.53%, fat-tail 88× la normale); 82% letterale → strike a 2.63% ≈
1.2σ, non deep. In entrambi i casi win-rate = delta venduto.
- **P(track record del video senza edge) = 20-45%**; il **78.6%** di tutte le finestre 6-mesi
1996-2026 lo avrebbe prodotto (lettura deep). Il PF ~5.1 esce per costruzione quando la coda
non è campionata.
- **Replay attraverso le code reali**: rovina nel 1998/2002/2020 col sizing 1→4 dichiarato;
perfino regalando +20% di premio, 1 contratto fisso fa CAGR +4.1% con maxDD 95%. La stessa
semestrale del video annualizza a +73-77%, non 420%. Su BTC/ETH a EV=0: +12%/DD68% e
+0%/DD88%.
- **La diagonale lascia passare il 12-40%** della perdita naked in un 10% overnight (dipende dal
vol-spike). Su Deribit la double diagonal è **fee-negativa a qualsiasi size** (fee 8 gambe =
194-221% del theta raccolto a questi strike): il capitale sposta solo il muro di margine.
- Che cosa distingue VRP01 (sopravvissuto allo stesso tipo di audit): defined-risk per
costruzione, gate IV-rank che ribalta l'hold-out, sizing fisso — l'opposto esatto su tutti e
tre gli assi.
## Capital scaling 600 → 2000/3500/5000 (r0702_capital_scaling.py)
| capitale | TP01 haircut Sh | book investibile % (cap $300) | K=2 exec-turn % | statarb haircut | spread ETH conc. | XS01 exec-turn % |
|---|---|---|---|---|---|---|
| 600 | +0.010 | 100.0% | 90.5 | 0.004 | 0 | 56.9 |
| 2000 | 0.002 | 85.9% | 95.7 | 0.000 | 3 | 87.4 |
| 3500 | 0.002 | 63.7% | 97.5 | 0.000 | 5 | 89.3 |
| 5000 | 0.002 | **48.9%** | 98.2 | 0.000 | 7 | 90.8 |
- **Il vincolo dominante è UNO: `max_notional_per_asset_usd`=300.** A 5k il book girerebbe a
metà del target (cap binding nel 39-46% delle barre). **Proposta config (da applicare AL
deposito): cap = equity/2** → $1000 a 2k, $1750 a 3.5k, $2500 a 5k. `min_order_usd`=5 va
LASCIATO (il fee drag risale solo al tetto noto ~0.4%/anno; l'isteresi artificiale costerebbe
più in lag — lezione ondata timing).
- Min-order/haircut TP01 ≈ 0 a ogni capitale (conferma indipendente del "74% ordini" a 600 →
39-43% saltati a 5k, tutti micro). Statarb: haircut 0 da 2k (il muro era l'edge, non la scala).
- Tranching K=2: matematicamente regge a 5k (98.2% turnover-tranche), ma il blocco vero resta il
feed intraday fuori path certificato + beneficio solo di varianza → **non cablare**.
- Opzioni: il muro d'ingresso ETH cade a ~2.6k (3-7 spread concorrenti a 2-5k, fee solo 5-6% del
credito grazie al cap 12.5%; rettifica vs ALB-A: margine spread ETH oggi $66-76). BTC granulare
da ~$4.7k. **La regola "niente short-vol da modello" non decade col capitale.**
- XS01: soglia ~20k confermata (a 5k il 9%+ del turnover non esegue). CC01: il capitale non è
mai stato il muro (fuori per ragioni strutturali).
- **Aspettativa onesta**: col CAGR de-luckato del book live (10-15%, audit SKH01): **2k ≈
€0.55-0.82/giorno; 5k ≈ €1.37-2.05/giorno**. Il salto di capitale cambia cosa è eseguibile,
non l'ordine di grandezza del ritorno (€50/g ≈ 130k, invariato).
## Lezioni
1. **Il null giusto smonta il livello "speciale"**: location-matched (ELL-B) per i livelli,
Donchian-equivalente (ELL-C) per i pattern, EV=0-calibrato (ALB-B) per i track record. In
tutti e tre i casi il claim sopravviveva ai null ingenui e moriva su quello giusto.
2. **`resample("7D", origin=...)` ignora origin in pandas 2.x** → usare `"168h"` per le bande
d'ancora settimanali.
3. Secondo caso "0-perdite = Sharpe implausibile" (celle deep-OTM di ALB-A, dopo CC01) → il gate
`implausible_sharpe` in altlib sale in coda ai lavori insieme ad `anchor_luck_band`.
4. L'anchor-luck è comparso spontaneamente anche in questa ondata (celle weekly ELL-A, 4h ELL-C)
— la regola della banda d'ancora sta già pagando.
**Stato:** nessun nuovo sleeve, nessun forward-monitor aggiunto, book live INVARIATO. Un'azione
operativa pende sul FUTURO deposito: alzare `max_notional_per_asset_usd` a equity/2 (decisione
utente al momento del funding). Test suite verde. 6 script committati.
+186
View File
@@ -0,0 +1,186 @@
# 2026-07-02 — Ondata timing + CRT (8 filoni multi-agente + scettico): 0 nuovi sleeve, 1 finding strutturale (anchor timing-luck di TP01)
**Obiettivo (goal utente):** "cerca su Deribit altre strategie profittevoli magari con timing
differenti oltre a quelle già valutate". Prima ondata interamente dedicata al TIMING: dopo che le
ondate precedenti hanno coperto segnali (104 famiglie alt-sweep), universi (HL cross-sectional),
opzioni (VRP/gamma) e pesi (EW-STR/weights_tilt_null), qui si attacca la dimensione rimasta: *quando*
si campiona, si decide e si esegue. Su richiesta utente, seconda tranche dedicata alla **CRT (Candle
Range Theory)**, il pattern sweep-and-reclaim a 3 candele di scuola ICT/Wyckoff.
Inventario preventivo per non rifare lavoro: SEA01-09 coprivano già hour-of-day, day-of-week,
weekend, turn-of-month, sessioni; l'onda intraday 2026-06-21 il sub-daily (con il lead prevday in
forward-monitor); `tp01_lowfreq` i TF 4h/12h/1d. Angoli NON coperti individuati e testati: clock a
eventi, calendario scadenze Deribit, ancora di ribilanciamento, clock più lenti del daily, velocità
del segnale regime-condizionata — più i 3 tagli CRT (base, multi-TF, contesto).
**Esito: 7 filoni FAIL + 1 finding strutturale confermato dallo scettico (timing-luck dell'ancora
di TP01). Nessun nuovo sleeve. Nessuna modifica al book live. Il soffitto ~1.3 direzionale
BTC/ETH resta intatto anche cambiando il clock.**
---
## Filone 1 — Event-clock bars (tempo-informazione): FAIL
`scripts/research/r0702_eventclock.py`. Volume/vol/range bars da 5m certificato (soglie causali
EWMA-90g shift(1), barre medie 4h/12h/24h), 5 strategie (TSMOM multi-orizzonte, Donchian 10/30d,
EWMA-cross), 45 celle event + 15 controlli wall-clock = 60 trial.
- Null decisivo (stesso segnale, stessa frequenza, wall-clock): event batte wall su ENTRAMBE le
finestre solo in **4/45 coppie (9%)** — rumore. Pattern incoerenti (EWMA event vince IS 9/9 ma
perde HOLD 9/9; Donchian-30d l'esatto opposto = selection-on-holdout in agguato).
- Cella scelta in-sample (volume-bars 24h, DONCH-10d): IS 1.45 → **HOLD 0.46** (2025 7.4%,
2026 18.1%). Marginal NEUTRAL (corr 0.74 a TP01 = trend travestito), earns_slot_honest=False.
- Executability comunque assente: in alta attività le barre "24h" chiudono in 3.6-4.6h (p5) → il
cron orario le eseguirebbe in ritardo; i clock 4h richiedono monitoraggio sub-orario.
**Lezione: campionare a tempo-informazione non normalizza nulla di monetizzabile sul feed
certificato; il clock non è la dimensione dove vive l'edge.**
## Filone 2 — Calendario scadenze Deribit: FAIL (0/24 celle)
`scripts/research/r0702_expiry_calendar.py`. Griglia dichiarata a priori: 4 finestre × 3 tipi expiry
(weekly ven 08:00 UTC / monthly ultimo ven / quarterly) × 2 asset, Bonferroni |t|≥3.08; 3 null
(placebo weekday, anchor-shift ±2/4h, permutation 500 calendari).
- Nessuna cella passa Bonferroni (max |t| 2.69). Il post-expiry drift monthly (+0.61/+0.77%) ha il
segno della teoria ma: non batte il placebo giovedì, permutation pctl 84-95 (non estremo), si
INVERTE sul quarterly (dove l'OI massimo dovrebbe amplificarlo) ed è guidato dal 2019 → rumore.
- Unica cella robusta ai null: weekly [-24,0) negativa (gio→ven), ma è indistinguibile dal
day-of-week (famiglia SEA già morta) e netta fee fa Sharpe ~0.08 (52 eventi/anno × 0.10% RT).
- Best-in-sample tradabile: IS +0.90 → **HOLD 0.52**; DSR 0.230 su 24 trial.
- Nota tecnica: colpito ESATTAMENTE il pitfall pandas 2.x documentato (epoca in secondi da
`date_range` su tz-aware → matching vuoto); fix con epoca ms esplicita. La lezione 2026-07-01
ha pagato.
## Filone 3 — Anchor timing-luck di TP01 + tranching: FINDING (verificato dallo scettico)
`scripts/research/r0702_tp01_offset.py` + scettico indipendente `r0702_skeptic_offset.py`
(ricostruzione bit-exact con codice diverso, zero riuso; h=0 riproduce `tp01_baseline_daily`
esattamente; vol-target ricalcolata per ancora; nessun look-ahead — verificato con troncamenti).
**Claim 1 CONFERMATO: l'hold-out 2025-26 "Sharpe 0.31" di TP01 è in larga parte fortuna
dell'ancora 00:00 UTC.** La stessa strategia CANONICAL alle altre 23 ancore orarie: HOLD mediano
**0.04**, banda **[0.13, +0.30]** — e il massimo è proprio h=0 (98° pctl; in-sample era al 10°:
sfortunato lì, fortunato qui). Spike, non plateau (h=23/h=1 fanno 0.13/0.04). Bootstrap dello
scettico: P(una qualsiasi ancora mostri uno spike ≥ così) = **0.86** — è il massimo atteso di 24
stime correlate, non un'anomalia; best-anchor per finestra annuale ruota a caso (16→23→21→12→5→0,
Spearman ≈ 0, nessuna seasonality d'ancora). Formulazione corretta (NON "il vero hold-out è 0.07",
che ha CI95 [1.2,+1.4] su 547 giorni): **l'hold-out 2025-26 non risolve l'edge di RITORNO di TP01**
(ritorno totale mediano ancore ≈ 0%); ciò che regge A OGNI ancora è il claim DIFENSIVO
(DD 6.7-10.1% vs ~60% B&H). Che è da sempre il vero valore dichiarato di TP01.
**Claim 2 (tranching multi-ancora) RIDIMENSIONATO:** K=4 ≡ esattamente EW di 4 book ancorati
(diff 1.4e-17), turnover identico (~8.3/y), haircut $600 ≈ 0. MA il "miglioramento" IS 1.49→1.56 è
per ~90% *tornare alla media delle ancore* (h=0 era sfortunato in-sample): vs ancora tipica
+0.005 mediano, P(K4≤h0)=0.18 n.s.; il taglio DD strutturale è ~0.5pt (non 2.8: la mediana delle
singole è già 12.6%). Ciò che regge: la compressione della VARIANZA DELLA STIMA (HOLD
[0.12,+0.30]→[0.01,+0.13] a K=4) → **lens di reporting, non alpha**.
**Impatto a valle (quantificato dallo scettico):**
- Blend SKH: HOLD 1.16 (h=0) → 0.97 mediana ancore, ma l'UPLIFT a mediana è +0.93 > +0.86
dichiarato → **il verdetto ADDS di SKH01 era conservativo, regge**.
- Book 5-sleeve: HOLD 2.44 → 2.34 mediana (min 2.22), FULL 2.24→2.22 → eredita **~+0.10** di
fortuna d'ancora (non +0.25: il peso di TP01 diluisce). Nessuna decisione (GTAA/SKH/pesi) cambia.
- NON toccati: tutti i FULL (h=0 al 31° pctl = normale), plateau, deflated-Sharpe, fee-sweep,
causalità, validazione GTAA (equity OOS), gate weights_tilt_null (relativo).
**Raccomandazione operativa (sottoscritta dallo scettico): cambio di narrativa e di standard di
reporting, NESSUN cambio del book live oggi.** (1) I numeri hold-out di TP01 si citano d'ora in poi
come banda d'ancora (mediana ~0.04, [0.13,+0.30]) con valore = taglio DD; (2) valutazione
anchor-agnostic (media 24 ancore) come lens di reporting per i futuri hold-out; (3) NO deploy
K=2/K=4 a $600: guadagno ≈ 0, e a $225/asset di quota TP01 i delta per-ancora (~$1-2) sono sotto il
min-order $5 → degenererebbe comunque in K=1; inoltre le ancore intraday richiederebbero un feed
fresco fuori dal path certificato (staleness ×4 finestre di guasto). **Rivalutare K=2 (0,12) a
capitale ≥ ~5-10k.** (4) ⚠️ Audit analogo RACCOMANDATO su XS01 (rebalance 10g ancorato = spazio di
luck 10g×24h, potenzialmente peggiore) e SKH01 (fase griglia 230m/690m) — TP01 è oggi l'unico
sleeve de-luckato.
## Filone 4 — Clock lenti + banded rebalancing: FAIL (negativo utile)
`scripts/research/r0702_slow_clock.py`. Fatto a monte che chiude il filone: il fee drag di TP01 a
1d è **~0.40%/anno ≈ 0.03 Sharpe** (8 turnover/y × 0.10% RT) — il tetto di QUALSIASI risparmio.
- Clock N∈{2,3,5,7}g: degrado monotono dell'ensemble di fase (HOLD 0.34→0.11 da N=2 a N=7); lo
spread TRA fasi esplode a N≥5 (HOLD 0.19…+0.37 a N=7) = timing luck pura, coerente col filone 3.
- Bande di isteresi {2.5,5,10,20}%: tagliano il 77-94% degli ORDINI ma quasi zero TURNOVER (gli
ordini di TP01 sono micro-aggiustamenti del vol-target). Cella best-IS (band 20%): hold-out
0.13 vs 0.30 baseline, e il "guadagno" IS è effetto-segnale (posizioni stantie fortunate), non
effetto-costo → fitting. Non-monotonia sulla griglia = firma di rumore.
- **Finding utile per il live: a $600 il vincolo min-order $5 È GIÀ la banda ottimale** (ordini
427→111/y a costo Sharpe ~0, banda implicita 1.67%): cattura ~100% del risparmio catturabile.
Nessun cambio al book, a nessun capitale testato (600/2k/10k).
## Filone 5 — Velocità del trend regime-condizionata: FAIL
`scripts/research/r0702_regime_speed.py`. Pesi tra gli orizzonti TSMOM 30/90/180g condizionati al
percentile espandente di vol (realized e DVOL), 16 celle, sanity = riproduzione esatta del canonico.
- Cella best-IS (alta-vol→lento, linear, rv): FULL +0.06 ma **HOLD BTC 0.31**, 50/50 +0.005 →
dominanza fallita; multi-cut a segno instabile (+0.18/0.08/+0.00).
- Null decisivo (300 pesi statici Dirichlet): la cella sta al pctl 0.71-0.72, sotto il p90 —
un peso statico casuale la batte spesso. Meccanismo smascherato: corr(peso-30d, Sharpe) = +0.93 →
le celle "regime" vincono in-sample perché tengono il tilt-30d la maggior parte dei giorni =
**tilt statico travestito da regime**, la stessa trappola di EW-STR. E il tilt statico verso il
30d NON regge su BTC hold-out (0.15). RV e DVOL indistinguibili come misura di regime (coerente
con l'esito overlay DVOL 2026-06-26). Pesi canonici 1/3-1/3-1/3 confermati.
## Filoni 6-8 — CRT "Candle Range Theory" (base / multi-TF / contesto): FAIL 3/3
Il pattern sweep-and-reclaim a 3 candele (C1 range forte; C2 rompe un estremo ma chiude dentro =
"manipolazione"; C3 ingresso contro il breakout, stop dietro lo sweep, target all'estremo opposto),
meccanizzato onestamente in tre tagli. Overlap dichiarato: MRV01-11 e MIC07 non coprivano questa
meccanica; ora è coperta.
**6. Base single-TF** (`r0702_crt_base.py`, 864 trial su 1h/4h/12h/1d, motore trade-level
conservativo SL-first cross-checkato con l'harness): **DSR 0.000** (il null best-of-grid atteso è
Sharpe 2.27, ottenuto 0.74); cella best-IS (4h long color-rule) IS 0.90 → HOLD 0.07 con 2026 a
WR 0%; anchor-shift +1/+2h flippa l'hold-out a 1.0 (artifact-risk); lo **short su sweep dell'alto
(la narrativa smart-money canonica) perde perfino in-sample (0.59)**. Autopsia: l'expectancy IS
veniva dal time-exit in trend (beta del toro ETH 2021-23), non dal target strutturale. Dettaglio
informativo: CRT batte nettamente sia il fade incondizionato (IS 0.49) sia il breakout-confermato
(IS 0.31) → il close-back-inside FILTRA davvero tossicità, ma "meno tossico del fade morto" non è
un edge.
**7. Multi-timeframe** (`r0702_crt_mtf.py`, 4h→15m e 1h→5m, ~10k trade): expectancy netta negativa
OVUNQUE (FULL e HOLD, entrambi gli asset); il MTF alza il R:R medio da ~3 a ~10-20 ma il WR collassa
da ~36% a 9-16% → "migliora" solo perdendo meno. **Refutazione strutturale: il ritest è informazione
negativa** — pattern CON ritest 40 bps, SENZA +52 bps (non tradabile: condiziona sul futuro):
aspettare il ritest per entrare seleziona sistematicamente i pattern peggiori; i buoni scappano
subito e il metodo non li prende mai. A fee ZERO l'edge lordo è ~0 (non è morte per fee: non c'è).
Stop 0.1-0.35% ineseguibili col cron orario (26-75% dei segnali già invalidati all'esecuzione).
**8. Contesto** (`r0702_crt_context.py`, 22 trial: livelli prevday/Donchian/prevweek × equal-H/L ×
FVG × sessioni): baseline incondizionata morta (8/8 celle HOLD<0); FVG semmai peggiora; sessione
EU consistentemente negativa; unica cella IS-positiva (sweep in Asia su livello prevday) uccisa 4
volte (DSR 0.001, anchor-flip a 2/4h con la firma di open_drive — la sessione "Asia" inizia dove
il livello prevday viene ricreato —, solo-ETH, morta a 0.20% fee). **Sottoprodotto prezioso: sugli
STESSI livelli prior-day, FOLLOW batte FADE ogni singolo anno 2019-2026** (follow IS/HOLD
+1.22/+1.25 vs fade 0.66/1.46, corr 0.19/0.27) → conferma indipendente e rafforzativa del lead
prevday-breakout in forward-monitor (paper_prevday).
**Verdetto CRT: concetto Wyckoff riverniciato che, spogliato della discrezionalità, non contiene
edge su BTC/ETH Deribit certificati. La direzione giusta sui livelli affollati resta il FOLLOW
(breakout), non il fade — coerente con SKH01 (breakout) vivo e mean-reversion morta.**
---
## Lezioni codificabili
1. **Il timing-luck dell'ancora è una dimensione di multiple-testing NON coperta dai gate
esistenti** (deflated-Sharpe conta i trial, non le ancore implicite). Nuova regola di reporting:
ogni metrica hold-out di una strategia a ribilanciamento ancorato si cita con la banda d'ancora
(o la media anchor-agnostic). Candidato gate futuro: `anchor_luck_band()` in altlib. Audit
pendente: XS01 (10g), SKH01 (fase 230m/690m).
2. **La famiglia day_boundary_robust / anchor-shift continua a uccidere** (CRT base, CRT-Asia,
6/24 celle expiry): ogni effetto legato a etichettatura di barre VA spostato d'ancora prima di
crederci. Confermata la regola 2026-06-21.
3. **Il fee drag di TP01 (~0.4%/anno) non è un problema da risolvere** — chiude a priori il filone
"esecuzione più furba" a questo turnover; il min-order small-cap è già la banda ottimale.
4. Il pitfall pandas tz-aware/epoca (2026-07-01) è stato evitato/gestito 2 volte su 9 agenti grazie
alla documentazione in CLAUDE.md — il costo di documentare i bug paga.
## Stato finale
- 0 nuovi sleeve; portafoglio e book live INVARIATI (5-sleeve 33/15/12/20/20; book Deribit
TP01+SKH01 75/25, flat da armamento — il segnale resta risk-off).
- Narrativa TP01 aggiornata (CLAUDE.md + docstring): hold-out = banda d'ancora, valore = difesa DD.
- Script: `scripts/research/r0702_{eventclock,expiry_calendar,tp01_offset,slow_clock,regime_speed,
crt_base,crt_mtf,crt_context,skeptic_offset}.py`. Test suite: invariata, verde.
+111
View File
@@ -0,0 +1,111 @@
# 2026-07-03 — Frontiera rischio/rendimento del book: quanto caldo correre a 2k-5k
**Contesto.** Dopo la diagnosi strategica ("€50/g è un problema di capitale e di frontiera, non di
alpha; hai costruito una macchina difensiva a DD 6% ma hai capitale piccolo e obiettivo di
crescita"), goal: **misurare** (non cercare segnali) la frontiera CAGR/DD/P-rovina in funzione del
target-vol, pesata per confidenza per-sleeve, a €2k e €5k — per decidere quanto caldo girare il book.
Workflow: 3 filoni (curva / onestà-della-coda / eseguibile+Kelly), refuter fat-tail su ciascuno,
scettico sintetizzatore. 7 agenti, 0 errori. Sanity bit-exact di entrambi i book prima di ogni numero.
**Scripts:** `scripts/research/r0703_frontier_{curve,tail,exec}.py`. Nessun file di produzione
toccato, nessun commit dagli agenti, `config/live.json` NON modificato (cap solo proposto).
---
## Verdetto in una riga
**La tesi "corri più caldo" era in gran parte un MIRAGGIO DI REGIME.** Il "~6% attuale" non è un
target scelto: è la vol *realizzata* in regime risk-off (TP01 spesso flat 0.0x → il conto è flat
*by design*). A lambda=1 il book LIVE a 2 sleeve gira **già a ~11% vol / 9.4% DD**. Quindi "saltare
da 6% a 10%" NON è aggiungere leva — è solo assumere che il book non sia risk-off. **Il numero
onesto è: gira alla vol NATIVA (~10%, banda 8-11%), non levare.** Guadagno reale vs il 6% risk-off:
**+€0.2/g @2k, +€0.5/g @5k** — reale ma modesto, comprato con un DD plausibile di ~€500 su €2k. €50/g
resta ~130k, invariato.
---
## I numeri (frontiera fidata, non full-Sharpe)
Sanity (deriva nulla): 5-sleeve FULL Sh **2.238** / HOLD 2.452 / DD 6.17% / vol nativa 7.84%;
2-sleeve live FULL Sh **1.776** / HOLD 1.162 / DD 9.42% / vol nativa 10.96%.
**Scale-invarianza confermata da tutti e 3 i refuter:** il capitale NON cambia la matematica del
target-vol (le tabelle sono in %), cambia solo l'ESEGUIBILITÀ → **stesso % a 2k e 5k.**
### Book LIVE 2-sleeve (TP01+SKH01) — l'unico eseguibile <20k
- **Target-vol raccomandato: ~10%, banda [8%, 11%], lambda ≈ 0.9.** Sopra la vol nativa non si va.
- €/giorno al punto fidato (haircut): CAGR full-sample 12.9% → €0.71@2k / €1.77@5k; ma **HOLD-onesto**
(de-luck) CAGR ~4.5% → **€0.25@2k / €0.62@5k**. Il lato-reward headline è ~3× l'onesto (HOLD≪FULL).
- **Stima centrale onesta: €0.3-0.7/g @2k, €0.8-1.8/g @5k.**
### Book PIENO 5-sleeve — ASPIRAZIONALE, non gira <20k
Frontiera fidata regge ~12-15% sotto tail estremo, ~20% sotto tail mite — ma XS01 serve ~20k e VRP01
è modellato → rilevante solo per il tracking di `paper_combo`, non per una decisione di deploy a 2-5k.
---
## I quattro insegnamenti strutturali
### 1. La rovina-50% è la metrica SBAGLIATA (trappola esposta dal curve-refuter)
P(rovina-50%|5y) resta <1% fino a ~20-22% di target-vol su ogni book — i book sono troppo lisci/
diversificati per bruciare metà capitale entro la distribuzione campionaria. **La risposta letterale
"max CAGR a P(rovina)<1%" direbbe "gira al 22%" — ed è un suicidio**: lì P(DD>30%|5y) ≈ 20-25%. Il
vincolo che MORDE è **P(DD>30%), super-lineare** (raddoppia ~ogni 2.5 punti di vol). E la rovina-50%
non è nemmeno robusta ad alta vol (block-15 vs block-20 danno 7.1% vs 0.9% a tv30%) → altro motivo
per stare bassi.
### 2. Il muro fat-tail ONESTO è ~12%, più basso della carta (~15%)
Tre ragioni che il modello sottostima: **(a) gap-through-stop SKH già osservato reale** (sl2%
modellato → 11/23% realizzato — la coda vera NON è nei rendimenti modellati; l'iniezione dei
finder usava lo *clean-stop* → ~2× troppo mite); **(b) il 2-sleeve è 4× più crash-sensibile del
5-sleeve** (+14.5pt vs +3.5pt di P(DD>30) all'iniezione, diversificazione sottile); **(c)
vol-targeting a lambda costante lagga nello spike** → sei più levato *entrando* nel crash.
**LIVE 2-sleeve: non superare ~12%; mai 15-20%** (triplica il DD-in-euro senza margine di coda).
### 3. Aggressività differenziata-per-confidenza (TP01 più caldo di VRP) — REFUTATA
La mia proposta era intuitiva ma **sbagliata, e i numeri lo dicono netto**: differenziare la vol per
confidenza PEGGIORA la frontiera (5s Sharpe 2.24→2.11, 2s 1.78→1.69) perché **taglia la
diversificazione** di VRP/XS (corr uniforme-vs-differenziato 0.98-0.99). **Regola corretta: la bassa
fiducia sull'EDGE si esprime DE-MEANando lo sleeve in aspettativa (= la frontiera haircut), NON
riducendone l'esposizione/vol.** La confidenza vive nell'haircut della media, non nella leva. Tutti
gli sleeve alla stessa leva.
### 4. La diversificazione LIMA la coda sinistra in-sample (sorpresa onesta)
Per il 5-sleeve ben diversificato il block-bootstrap dà P(DD>30) SOTTO il Gaussiano iso-vol (30%:
10.6% reale vs 21.1% gauss) — la coda in-sample è benigna. Il segnale fat-tail vive in due posti:
l'**iniezione di crash sintetico** (il campione non contiene il crash peggiore di ogni sleeve) e
il **book live sottile** (2 gambe, drift haircut) dove il bootstrap SUPERA il Gaussiano. Cioè: la
frontiera è affidabile finché il futuro somiglia al passato campionato, e fragile esattamente dove
le storie sono corte (VRP mai stressato reale, XS 2.5y, nessun 2018/pre-2018).
---
## Azione concreta (proposta, NON applicata)
1. **Nessun cambio di leva sul codice.** Il book gira già ≈ al punto fidato: LASCIARE `target_vol=20%`
di TP01 (canonico, la vol nativa del book emerge a ~8-11%). Non c'è una leva da girare su.
2. **⚠️ L'UNICO cambio che conta: `config/live.json` `max_notional_per_asset_usd` da $300 a
equity/2** ($1000@2k / $1750@3.5k / $2500@5k). Senza, il cap $300 tappa la vol raggiungibile a
13%@2k / 8%@3.5k / **6%@5k****throttling paradossale: più capitale = book più freddo**, forzato
sotto il punto fidato. Da applicare AL deposito. (config NON toccato ora.)
3. `min_order_usd=5`: LASCIARE (banda d'isteresi gratuita). Tranching K=2: NON cablare. Vol
differenziata: NON implementare (Q3).
---
## Brutalità sui limiti
- **Storie corte = il difetto strutturale.** TP01/SKH dal 2019 (no 2018), VRP dal 2021, XS **2.5
anni** (no 2022). Il block-bootstrap non può campionare un crash mai visto; l'iniezione sintetica
è l'unico proxy, e quella dei finder era ~2× troppo mite.
- **VRP01 è MODELLATO** (f-di-stress mai catturato su un crash reale) → l'haircut 50% è stima, non
misura; resta STAT-MODE anche a 5k (regola: niente short-vol da modello in deploy).
- **HOLD ≪ FULL sul book live** (1.78 vs 1.16) → il lato-reward headline è in gran parte artefatto
full-sample; citare i €/g su base HOLD-onesta.
- **€50/g NON è risolto e non lo sarà da questo.** Il warm-up compra ~€0.2/g@2k. La via resta
**capitale + tempo**; la regola "niente leva alta come base" è rispettata (la raccomandazione è
"gira alla vol nativa", non "corri caldo").
**Numero unico da portare a casa:** book LIVE a **~10% target-vol (lambda ≈ 0.9, banda 8-11%)**,
uguale a 2k e 5k, con **cap → equity/2** come unica azione config; muro invalicabile a **~12%**;
guadagno reale vs il 6% risk-off ~**+€0.2/g@2k / +€0.5/g@5k** — reale ma modesto.
+47
View File
@@ -0,0 +1,47 @@
# 2026-07-03 — Cap notional per-asset DINAMICO (= equity/2): operazionalizza la decisione frontier
**Contesto.** La frontiera del 2026-07-03 (`docs/diary/2026-07-03-book-frontier.md`) aveva identificato
**una sola** azione config con impatto reale: portare `max_notional_per_asset_usd` da $300 fisso a
**equity/2**, perché il cap fisso strozza il book man mano che si deposita (paradosso: più capitale =
book più freddo — a $5k il cap $300 fa girare il libro al ~49% del target). Il diario la lasciava
"da applicare AL deposito". Qui la **operazionalizzo in codice** rendendo il cap dinamico, così è
sempre corretta e non richiede un edit manuale al deposito.
## Cosa è cambiato
- **`src/live/book.py``_cap()`**: se in config c'è `max_notional_per_asset_frac` (opt-in), il cap
per-asset diventa **`equity × frac`** (0.5 → equity/2). Senza la chiave → comportamento vecchio
(cap fisso `max_notional_per_asset_usd`). Il call site in `book_report()` passa
`equity`, `real_equity`, `eq_fallback` a `_cap()`.
- **`config/live.json`**: aggiunto `max_notional_per_asset_frac: 0.5` (+ nota). `max_notional_per_asset_usd`
resta come **cap fisso di fallback**.
- **`tests/test_book_live.py`**: +4 test (scaling con equity fidata, fallback su eq_fallback/real=None/
equity 0, no-frac, integrazione online). Suite: **172 passed**.
## Il guardrail di sicurezza (il punto delicato)
Il cap dinamico si usa **SOLO con equity reale fidata** (letta dal conto). Se l'equity reale non è
leggibile — `eq_fallback` attivo, `real_equity=None`, offline, o equity ≤ 0 — si **torna al cap fisso
$300**. Motivo: in fallback `equity` è il paper-cap ($2000 ipotetico); calcolare `equity/2 = $1000` su
un conto di importo reale **ignoto** sizerebbe troppo. Preserva esattamente la protezione downside che
il test `test_book_execute_eq_fallback_warns_but_proceeds` verificava (l'hard-cap $/asset protegge
quando E è ignota). Regola: **equity/2 solo quando ci fidiamo di E; altrimenti $300 fisso.**
## Verifica
- **Live dry-run reale** (nessun `--execute`, nessun ordine): `cap/asset **$299**` = $598.06/2 →
**INERTE** oggi (≡ vecchio $300). Book flat, HOLD.
- Offline / `equity_override`: cap resta **$300 fisso** (nessuna equity reale da fidarsi) → il test
di parità pre-esistente regge invariato.
- A $5.000 di equity il cap diventerebbe automaticamente **$2.500** (equity/2), rimuovendo il
throttling che la frontiera aveva quantificato.
## Impatto e limiti
- **Già attivo**: il cron orario (`cron_book.sh`) esegue dal working tree → il prossimo run legge già
`frac=0.5`. Nessun deploy separato. **Inerte a $600**, si "accende" da sé quando l'equity cresce.
- **NON avvicina €50/g**: la frontiera lo aveva già detto — è una miglioria marginale (+€0.2/g @2k,
+€0.5/g @5k), il cui unico scopo è non lasciare capitale futuro strozzato. €50/g resta un problema
di **capitale + tempo** (~130k @ CAGR onesto), non di questo cap.
- Nessun cambio di **leva** (TP01 `target_vol=20%` invariato, la vol nativa ~10% emerge da sola —
coerente col verdetto frontier "gira alla vol nativa, non levare").
@@ -0,0 +1,156 @@
# 2026-07-03 — STATO COMPLETO DEL PROGETTO (snapshot)
Foto d'insieme dopo le ondate di ricerca del 1-3 luglio 2026 (timing/CRT, anchor-audit,
video-claims Elliott/Albimarini, capital-scaling, migliora-e-proteggi VRP01). Serve come punto di
riferimento unico: *dove siamo, cosa gira, cosa è provato, cosa resta aperto.*
---
## 1. In una frase
Progetto post-reset v2.0.0: su dati Deribit mainnet certificati (solo BTC/ETH) abbiamo costruito un
**portafoglio di 5 strategie diversificate** (research Sharpe ~2.2, DD ~6%), di cui **2 sono
eseguite live su Deribit** in un book nettato in software. Il conto reale è **~$598, flat** perché
il segnale trend è risk-off. Il valore delle strategie è **difensivo** (taglio del drawdown), non
generazione di ritorno: il target €50/giorno resta lontano (serve ~130k di capitale). L'onestà
metodologica è il vero asset accumulato: ~10 gate anti-illusione codificati, decine di edge
falsificati.
---
## 2. Il book LIVE (soldi reali)
- **Conto:** Deribit mainnet, USDC linear, **equity reale $598.06** (NON i €2000 nominali del paper).
- **Cosa gira:** `deribit_book_sleeves` = **TP01 (0.75) + SKH01 (0.25)** nettati in software su un
solo conto; UN ordine con segno per asset (BTC, ETH). `config/live.json`
**`execution_enabled=true`** → esecuzione ARMATA e reale.
- **Cadenza:** cron ORARIO `scripts/cron_book.sh``scripts/live/book_execute.py` (SKH01 decide su
griglia 230m; exit software, latenza fino a fine barra).
- **Guardrail:** cap **$300 notional/asset**, min order $5, **disaster-SL on-book 30%** sulla
posizione netta, alert Telegram su esecuzione/errori, gate fail-safe su posizione/equity non
leggibili (non opera a cieco).
- **Stato corrente (ultimo cron 2026-07-03 07:00Z):** BTC e ETH **flat, HOLD a target** — il target
TSMOM è risk-off (trend 0.0x) e SKH01 è flat (nessun breakout) → **zero ordini reali mai eseguiti**
da quando è armato (2026-06-23): il conto è flat *by design*, non per un errore.
- **NON nel book live:** XS01, VRP01, GTAA01 (research/paper/STAT-MODE).
## 3. Il portafoglio RESEARCH (5 sleeve) — `src/portfolio/sleeves.active_sleeves`
| Sleeve | Peso | Meccanismo | Dati | Stato |
|---|---|---|---|---|
| **TP01** | 33% | TSMOM difensivo trend BTC/ETH 1d, long-flat, vol-target 20% | 2019→ | **Deploy pieno** (nel book live) |
| **SKH01** | 20% | Dual-TF regime+breakout BTC/ETH, L/S, exit %-asimmetrici | 2019→ | Research → nel book live |
| **GTAA01** | 20% | Trend difensivo equity 6-ETF (IB), ~30 anni storia | 1996→ | Paper (paper_combo) |
| **VRP01** | 12% | Options short-vol (put credit spread + gate IV-rank) | 2021→ | LEAD modellato (STAT-MODE) |
| **XS01** | 15% | Cross-sectional momentum 19 alt Hyperliquid | 2024→ | STAT-MODE (serve ~20k) |
- **Combinato (research, ancore canoniche):** FULL Sharpe **2.24**, HOLD-OUT **2.46**, DD full **6.2%**.
- **⚠️ Stima DE-LUCKATA onesta** (dopo gli anchor-audit, vedi §5): **HOLD ~1.9-2.1, FULL ~2.0-2.2,
DD ~6%**. Il 2.46 è un massimo di configurazioni d'ancora, non la stima centrale.
- Sleeve a date d'inizio diverse → outer-join con pesi rinormalizzati (`combine_outer`).
- Report: `scripts/portfolio/run_portfolio.py`. Il book live (TP01+SKH01 75/25) è un sottoinsieme.
## 4. Paper trader / forward-monitor attivi (nessun soldo reale)
- **paper_trend** — TP01 1d, capitale nominale 2000, forward-only.
- **paper_combo** — book 5-sleeve completo (incl. GTAA/VRP/XS) dal 2026-06-22.
- **paper_statarb** — STATARB-RESID (ETH−β·BTC residual momentum, W=45/sgn=+1 congelati), doppio
libro MODELED/REAL-$600. LEAD eseguibile a $600 sotto soglia solo sull'edge (DSR 0.929).
- **paper_prevday** — FOLLOW prior-day levels (3 trade registrati); confermato indipendentemente da
più ondate (FOLLOW > FADE ogni anno 2019-26).
---
## 5. Finding strutturale del blocco 1-3 luglio: ANCHOR TIMING-LUCK (audit COMPLETO 4/4)
Le strategie a ribilanciamento/griglia ancorata hanno un'ancora "canonica" che è spesso il
**migliore** di molte configurazioni equivalenti (giorno/ora/offset) — un multiple-testing che il
deflated-Sharpe NON conta. Audit eseguito su tutti e 4 gli sleeve ancorati:
| Sleeve | Spazio d'ancora | Firma | Conseguenza |
|---|---|---|---|
| **TP01** | 24 ore (barra daily) | canonica = migliore (P=0.86) | hold-out 0.31 non risolve l'edge di ritorno; regge solo il taglio DD |
| **XS01** | 10 fasi (ciclo 10g) | canonica al 15° pctl di DD | numeri headline conservativi sul DD, non sull'hold-out |
| **SKH01** | 23 offset (griglia 230/690m) | canonica = 93-98° pctl (peggiore firma) | gate DD<30% fallisce in 15/23 offset; ~+0.5 HOLD di fortuna sul book |
| **VRP01** | 7 giorni (ciclo settimanale) | **canonica = PEGGIORE** (7° pctl) | **numeri CONSERVATIVI**, non gonfiati — unico sleeve senza luck |
**Regola codificata:** i numeri hold-out di strategie ancorate si citano **con banda d'ancora**.
Candidato gate futuro: `anchor_luck_band()` in altlib. Diari: `2026-07-02-timing-crt-wave.md`,
`2026-07-02-anchor-audit-xs01-skh01.md`, `2026-07-03-vrp-improve-dd.md`.
---
## 6. Cosa è PROVATO e MORTO (per non ri-testarlo)
- **Soffitto BTC/ETH-direzionale ~1.3** confermato da ogni ondata; superato solo dal cross-sectional
(XS01) e dalla diversificazione multi-sleeve.
- **Sweep 104 famiglie** (2026-06-20): 1 sola sopravvissuta, comunque non deployabile.
- **Opzioni long-vol** (gamma scalping): specchio del VRP, perde ogni anno/variante/frequenza.
- **Cash-and-carry (CC01):** premio reale ma Sharpe artefatto (manca il 2022, procyclico) → STAT-MODE.
- **Timing/clock alternativi** (event-clock, expiry calendar, clock lenti, regime-speed): 0 edge.
- **CRT "Candle Range Theory"** (3/3 FAIL): DSR 0.000, expectancy negativa anche a fee zero, il
ritest è informazione negativa; sui livelli prior-day FOLLOW > FADE.
- **Elliott (3/3 FAIL):** range-cycle = rumore; Fibonacci = effetto posizione non numeri magici;
canale = Donchian travestito.
- **Albimarini double-diagonal (2/2 FAIL):** non domina il condor, fee-negativa su Deribit, track
record consistente con zero skill (P=20-45%), rovina nei replay con code reali.
- **VRP01 miglioramenti (0/7 filoni):** struttura, overlay-DD, gate nuovi (4° fallimento), sizing,
gate SPX (confound di modello) — **VRP01 non migliorabile; la protezione DD si compra con la size.**
## 7. Regole/gate anti-illusione codificati (l'asset metodologico)
- **Certificazione dati** obbligatoria (solo Deribit mainnet, BTC/ETH; alt esclusi).
- **`marginal_vs_tp01` / `study_marginal`** (earns_slot) — Sharpe MARGINALE, non assoluto.
- **`study_family_honest` / `select_cell_insample` / `deflated_sharpe≥0.95`** — anti selection-on-holdout.
- **`day_boundary_robust`** — effetti calendario che si invertono spostando il confine = artefatto.
- **`eval_weights_smallcap`** — Sharpe haircut reale a capitale piccolo (sub-min-order = finzione).
- **`weights_tilt_null`** — ogni cambio pesi vs null dei tilt casuali cap-respecting.
- **Banda d'ancora** (nuova, luglio) — per ogni costrutto ancorato.
- **Candidati in coda:** `anchor_luck_band` (multiple-testing d'ancora); `implausible_sharpe`
(0-perdite / Sharpe implausibile — 3 occorrenze: CC01, ALB-A, griglia struttura VRP).
- **Lezioni pandas:** mai `DatetimeIndex.view("int64")` su tz-aware; `resample("7D", origin=)`
ignora origin (usare `"168h"`); un gate che si correla con un errore di modello è indistinguibile
da alpha finché non correggi il modello.
---
## 8. Onestà sui numeri e sul target
- **€50/giorno da €1.000-2.000 NON è raggiungibile** in 1-2 anni: serve ~130k di capitale o un DD da
rovina. La leva non è la scorciatoia (5x raddoppia il DD). La via è target-vol + capitale + tempo.
- Aspettativa onesta col CAGR de-luckato del book (10-15%): **~€0.6-0.8/giorno a 2k, ~€1.4-2/giorno
a 5k**. Il salto di capitale cambia *cosa è eseguibile*, non l'ordine di grandezza del ritorno.
- Il valore reale di TP01/del book è il **taglio del drawdown ~6× vs buy&hold**, non l'alpha.
## 9. Capitale in arrivo 2-5k — cosa cambia (audit r0702_capital_scaling)
- **Unico vincolo binding: il cap `max_notional_per_asset_usd=300`.** A 5k il book live girerebbe al
~49% del target. **Azione pendente sul deposito: alzare il cap a equity/2** ($1000/$1750/$2500 a
2k/3.5k/5k). `min_order_usd=5` da lasciare.
- Tranching K=2: matematicamente regge a 5k ma serve feed intraday fuori path certificato → non cablare.
- Opzioni ETH eseguibili da ~2.6k (spread), ma **la regola "niente short-vol da modello in deploy"
non decade col capitale**. XS01 resta fuori fino a ~20k. CC01 fuori per struttura.
- ⚠️ Disambiguazione unità: il "12%" come peso book (~0.014 Kelly) ≠ "12%" come margine/equity
(~0.27 Kelly) — **fattore 19x**.
## 10. Aperto / prossimi passi
1. **Al deposito 2-5k:** alzare il cap $/asset (proposta pronta, `config/live.json` non ancora toccato).
2. **Forward-monitor da maturare:** paper_statarb (edge sotto soglia DSR), paper_prevday (FOLLOW),
paper_combo (book completo). Se la finestra forward conferma STATARB-RESID → deployabile a 2 gambe.
3. **Gate da codificare:** `anchor_luck_band`, `implausible_sharpe`.
4. **Rivisite temporizzate:** term-structure DVOL (logger dal 2026-06-26, rivalutare a 6-12 mesi);
breadth/internals HL (1-2 anni di storia nativa); tranching a ≥5-10k; VRP stress-f su un crash reale.
5. **Ricerca strategie nuove:** il soffitto direzionale ~1.3 è saturo; i margini restano in meccanismi
ORTOGONALI (cross-sectional, relative-value, options gated) e nella diversificazione, non in nuovi
segnali direzionali BTC/ETH.
---
## 11. Stato tecnico
- **Versione:** 2.0.0 (semver in `VERSION`). **Test:** 168 verdi (28 file). **Diari:** 76.
- **Ultimi commit:** `26f8d27` (migliora+proteggi VRP01), `76120b5` (video-claims + capital-scaling),
`a74cc69` (anchor-audit XS01/SKH01), `e6657fc` (GTAA01 5° sleeve).
- **Working tree pulito, tutto pushato.** Book live sano e invariato da tutta la ricerca (research
isolata: nessun agente tocca src/config/live).
+167
View File
@@ -0,0 +1,167 @@
# 2026-07-03 — Ondata "migliora e proteggi VRP01 dai DD" (7 filoni + verifica a 2 lenti + scettico incrociato)
**Contesto.** Dopo la chiusura del dossier Albimarini (3 backtest indipendenti convergenti: premio
piccolo e reale, code non campionate, numeri del corso spiegabili al 100% dalla struttura del
payoff), goal esplicito: **migliorare la strategia short-vol** — la famiglia Albimarini → il nostro
**VRP01**, l'unica forma sopravvissuta agli audit — **e proteggerla dai drawdown**, con
orchestrazione a decine di agenti. Workflow deterministico: 7 filoni di ricerca in parallelo, 2
refuter avversariali (lente selection-bias + lente de-levering) su ogni miglioramento proposto,
scettico incrociato finale sui sopravvissuti. **26 agenti, 0 errori.**
**Scripts:** `scripts/research/r0703_vrpimp_{structgrid,ddoverlay,sizing,newgates,anchor,spxgate,stresslab}.py`.
Nessun file di produzione toccato, nessun commit dagli agenti. Motore riusato: `r0702_alb_structure.py`
(DVOL, riproduce VRP01 bit-exact: baseline bridge max|diff|=0.00e+00 su 266 settimane).
---
## Verdetto in una riga
**VRP01 non è migliorabile oltre lo status quo, e la protezione DD si compra SOLO con la size.**
Zero nuovi sleeve, zero overlay che reggono, zero gate nuovi, zero cambio di sizing. I due unici
"sopravvissuti" alla verifica **non propongono alcun cambiamento** (una conferma dello status quo +
una lente di misura). Il candidato più luccicante dell'ondata — un gate term-structure VIX su SPX
con ΔSharpe +0.90 e DSR 0.992 — è stato smontato dallo scettico come **confound di modello al
100%**. Deliverable reale = **audit anchor-luck di VRP01 CHIUSO** (ultimo sleeve ancorato): la fase
canonica è all'estremo **conservativo** della banda, quindi i numeri di ammissione reggono e da ora
si citano con banda.
---
## I 7 filoni
### 1. Griglia di struttura (288 celle) — NESSUNA batte VRP01
6 strutture (vertical put/call, condor, 3 diagonali) × distanza {1,1.5,2,2.5}σ × ali {0.5,1,2}σ ×
tenor {3,5,7,10}g, gate IV-rank canonico sempre attivo. La cella scelta col **solo in-sample**
(DIAG-PUT z=1.0 3g, ShIS 2.28) crolla in hold-out a **+0.19** vs 0.86 del bridge stesso-motore;
DSR 0.000/0.004 su 288 trial; multi-cut negativo a 3-4 cut su 4; banda d'ancora che non tiene
nemmeno il segno (ShH [0.21,+0.64]). **Metà griglia (150/288) è il pattern "0-perdite = Sharpe
implausibile"** (celle z≥2 lunghe, 0 perdite su ~97 trade, ShH fasulli 4-6.5) — **terza occorrenza
dopo CC01 e ALB-A**. Quarta conferma indipendente: nel VRP l'alpha sta nel gate di regime + nella
zona ~δ-0.28 dove la coda è campionata, non nella geometria.
### 2. Overlay di protezione DD (14 celle) — 4/4 REFUTED (2 lenti ciascuno)
(a) exit intra-settimana su spike DVOL, (b) stop-loss MTM, (c) ala di coda comprata (broken-wing),
(d) cooldown post-perdita. **Nessuno batte il null del de-levering** a pari maxDD su tutta la banda
f; nessuno passa multi-cut (max 1/4) né DSR (0.70-0.88, tutti <0.95). Esempio decisivo: SL MTM 3x fa
DD 7.9%/worst 5.7%, ma VRP01 × 0.66 (semplice riduzione di size) fa lo **stesso DD a Sharpe
invariato e worst-week migliore (4.9%)**. Meccanismo confermato: i trigger di spike scattano solo
2-3 volte in 5 anni **perché il gate IV-rank/crash-skip evita già gli ingressi vicino alle
esplosioni di vol** — la protezione crash vive nel gate d'ingresso, non serve un overlay a valle.
Coerente con TP01×DVOL (2026-06-26): "per meno DD la leva è la size, non un overlay".
### 3. Sizing anti-rovina — CONFERMA (12% ≈ quarter-Kelly), 1 candidato REFUTED
Il sizing non crea alpha (Sharpe del flusso invariante ~0.95 a ogni frazione). Frontiera onesta
CAGR-DD-P(rovina) mappata su 92 trade gated ETH 2021-26 (pooled banda-f + coda sintetica full-loss):
Kelly onesto q*=44% → il **12% del book = 0.27 Kelly, già in zona quarter-Kelly anti-rovina**
(P(rovina-50%|5y) 0.0-0.1%, P(DD>30%) 9-18%, CAGR med 13.5-17% a 2k/5k). Salire al 25% → P(DD>30%)
> 92%; Albimarini 1→4 → P(rovina) 53-55%. **Nessun cambio raccomandato.** Il candidato anti-streak
(N=2, size 12%→3% dopo 2 vittorie, l'opposto di Albimarini) era il più luccicante mai visto sullo
sleeve (FULL CAGR +19.4%/DD 6.3%/Sh 2.08, random-null 0.996, DSR 0.999) ma **auto-refutato come
artefatto di selezione**: plateau assente (solo N=2 vince, N=3/5 collassano), P(loss|streak) non
monotona, delever-null perde in 2/7 ancore — tutto l'effetto sono ~6 perdite su 15 trade a
streak==2 (binomtest p=0.033, CI contiene la base). Istruttivo: mostra come un DSR 0.999 possa
convivere con l'assenza di meccanismo.
### 4. Gate nuovi — 4° FALLIMENTO su 4 (barra alta)
(a) term-structure DVOL **non testabile oggi** (storia vol_term = 7 giorni, logger forward dal
2026-06-26 → rivalutare a 6-12 mesi); (b) vol-of-vol, (c) RV-acceleration, (d) combinazioni-AND. Il
candidato IS-best (vol-of-vol pctl>0.80) passa DSR 0.960 e il de-levering all'ancora di default ma
la **banda d'ancora lo smaschera** (mediana uplift FULL 0.07 su 7 ancore, uplift concentrato in UNA
finestra 2021-22 di 5 settimane, hold-out uplift 0.00). Bonus strutturale: i veti RV-acceleration
bloccano settimane **profittevoli** (+0.93/+1.25% vs +0.6% tenute) — dopo il gate IV-rank, la vol
che accelera è dove il premio venduto è più ricco. Conferma il verdetto 2026-07-01: l'alpha è tutto
nel binario IV-rank>0.30.
### 5. Audit anchor-luck VRP01 — CHIUSO: primo sleeve SENZA firma di luck
VRP01 è l'ultimo sleeve ancorato dopo TP01/XS01/SKH01. Ciclo settimanale → 7 ancore (giorno di
apertura). Replica bit-exact della sleeve (max|diff|=0.0). Le 7 fasi con parametri identici:
| metrica | canonica (fase-0) | mediana 7 fasi | banda | pctl canonica |
|---|---|---|---|---|
| Sh FULL | 1.09 | 1.32 | [1.09, 1.83] | **7° (peggiore)** |
| Sh HOLD | 0.59 | 0.59 | [0.03, 1.11] | 50° (mediana) |
| maxDD | 11.8% | 7.2% | [5.7, 11.8%] | **93° (peggiore)** |
**Firma OPPOSTA a TP01/XS01/SKH01**: lo spike bootstrap è NEGATIVO (g0 HOLD 0.06, FULL 0.21,
P(g0≤0) 0.54/0.74) → niente da de-luckare. La fase canonica ha beccato le settimane crash 2022
**peggio** di ogni altra (unico anno-fase negativo, 5.8%). **I numeri di ammissione FULL 1.10 /
HOLD 0.60 / DD 12% sono quindi CONSERVATIVI, non gonfiati** — il timore di "protezione fittizia" è
refutato: il DD 12% è il caso peggiore della banda, non quello fortunato. Impatto della fase sul
book 5-sleeve ≤ 0.04 Sh (banda ShFULL 2.23-2.27, ShHOLD 2.35-2.48, DD 6.2-6.3%). **Con questo
l'audit anchor-luck è completo su tutti gli sleeve ancorati (4/4).**
### 6. Gate di regime sulla diagonale SPX (l'esperimento mancante) — REFUTED, con confound istruttivo
L'esperimento che mancava ai 3 backtest Albimarini: il gate di regime sulla diagonale SPX (VIX +
VXV da FRED, motore BS sintetico con skew log-lineare). **Domanda secca: il gate IV-rank ribalta gli
anni di coda su SPX come su crypto? NO.** Il gate VIX-rank canonico *abbassa* lo Sharpe (1.36→1.19),
non ribalta nessun anno di coda, peggiora il 2018, muore sul de-levering (ΔSh 0.17). Meccanismo:
su SPX il VRP è già positivo l'**84% dei giorni** (vs ~58% delle settimane crypto) → il filtro di
rank non ha lavoro discriminante; su equity IV-rank alto = crisi, non vol ricca.
Il candidato **term-structure VIX/VXV<1 (contango)** sembrava reale — Sh 1.18→2.08, DD 3.5→1.3%,
ΔSh **+0.90** a pari DD, multicut 3/3, flippa il 2020, DSR 0.992, plateau su c∈[0.93,1.05], jackknife
robusto — **e passa la lente selection-bias pulito**. Ma la lente de-levering lo affonda:
**l'intero uplift è un confound di modello, quantificato al 100%.** La variabile del gate (VIX/VXV>1)
coincide 1:1 con la variabile dell'errore di pricing (tenor 6g prezzato a VIX-30g). Il refuter ha
riprezzato con σ(6g)=VIX·(VIX/VXV)^α term-structure-consistent (α=1.42 = estrapolazione log-lineare
zero-parametri della pendenza osservata 30g→93g) → l'edge sparisce. **Lezione: un gate che si
correla con un errore di modello è indistinguibile da alpha finché non correggi il modello** — vale
per ogni futuro test su strutture prezzate BS-flat.
### 7. Stress lab di coda — fisica dello strumento, nessun cambio
La protezione di coda del VRP01 viene per ~metà dall'**ala far-OTM** (nel replay COVID taglia ~90%
della perdita naked) e ~metà dal **gate canonico** (sulle 10 peggiori finestre 14g taglia la perdita
cumulata del 44-71% saltando i re-entry a IV-rank>0.90). L'ala T+1 "Albimarini" è assicurazione solo
contro i gap **medi** (15/20%, dove atterra ATM con un giorno di time value: +150bps) e **NON**
contro i gap profondi (k(30%)=0.98 → il de-levering la batte), con valore interamente dipendente
dal vol-spike (banda f/mult da 324 a +263bps). Worst-case onesto a sizing 12%: piccolo in
convenzione book (−€12/sett a 2k, −€31 a 5k alla cella 30%) ma **è il margine intero** se il 12%
fosse deployato fisicamente come margine (−€164..227).
---
## Scettico incrociato — verdetto e disambiguazione critica
I 2 sopravvissuti (sizing-conferma, anchor-ensemble) usano leve diverse (posizione sulla frontiera
vs varianza della stima), ma lo scettico ha trovato **tre sovrapposizioni che i refuter singoli non
potevano vedere**:
1. **Stessa dipendenza dal modello f** — entrambi flippano segno allo stesso punto (f=0.6-0.8 →
negativo). Due sopravvissuti = **un solo grado di libertà di rischio-modello** (il caveat "premio
MODELLATO su DVOL ATM" resta il collo di bottiglia dell'intera famiglia).
2. **⚠️ CONFLITTO DI UNITÀ sul "12%"** (il punto più pericoloso per la narrativa): il **12% di PESO
del book** (convenzione pnl/Ks di `sleeves.py`) = ~**0.6% margine/equity ≈ 0.014 Kelly**, mentre
il "FISSO 12%" del filone sizing = **12% margine/equity ≈ 0.27 Kelly****fattore 19x**. Entrambe
corrette internamente, ma non vanno confuse: worst-week 12.2% del conto (convenzione margine) vs
0.64% (convenzione peso book).
3. **L'ensemble-7-fasi è ridondante con il tranching TP01** (matematica di diversificazione: corr fra
fasi ρ=0.355, boost teorico ×1.50 vs osservato ×1.53, residuo alpha ≈ 0) → **non promuovere a LEAD
autonomo**; la coda strutturale è invariata (worst = 7 tranche a full-loss simultaneo), il DD 4.7%
è campionario, non un cap.
**Effetto congiunto sul book 5-sleeve: ZERO** (entrambi propongono nessun cambio; controfattuale
|ΔSh| ≤ 0.04). Il book live Deribit (TP01+SKH01) non contiene VRP01 → impatto operativo nullo.
---
## Cosa entra nella narrativa (una sola voce, status = misura research)
- **Audit anchor-luck VRP01 CHIUSO** → i numeri di ammissione reggono e si citano con banda
(ShFULL [1.09,1.83], ShHOLD [0.03,1.11], DD [5.7,11.8%]); l'edge OOS resta f-dipendente.
Con questo l'audit anchor è completo su 4/4 sleeve ancorati.
- **Sizing (conferma, con disambiguazione unità obbligatoria):** 12% deploy ≈ 0.27 Kelly onesto,
anti-rovina; NON confondere col 12% di peso book (0.014 Kelly, 19x).
- **VRP01 canonico resta lo sleeve giusto, INVARIATO** — non migliorabile per struttura, gate,
overlay o sizing; la protezione DD si compra con la size.
## Regole/candidati rafforzati
- **`implausible_sharpe` gate in altlib**: 3ª occorrenza del pattern "0-perdite = Sharpe
implausibile" (griglia struttura, dopo CC01 e ALB-A) → priorità alzata.
- **Nuova regola metodologica**: un gate che si correla con un errore di modello (term-structure vs
BS-flat) è indistinguibile da alpha finché non si corregge il modello — riprezzare
term-structure-consistent prima di credere a qualsiasi gate vol su strutture BS-flat.
- **Conferma (4ª): l'alpha del VRP è il gate IV-rank binario**, non struttura/sizing/overlay/gate
aggiuntivi.
**Stato:** book live INVARIATO, nessun nuovo sleeve, nessun forward-monitor. Test suite verde. 7
script committati.
@@ -0,0 +1,109 @@
# 2026-07-07 — Video-claim "CRT top-down multi-TF, 74% win rate": SCARTATO (il 74% è un knob, non un edge)
**Richiesta utente:** analizzare e testare una strategia da video, metodo ICT/SMC top-down
multi-timeframe con claim **74% win rate**:
1. **H1** — setup CRT: candela di displacement forte, poi candela che prende liquidità (spike oltre
il range di C1) ma richiude dentro → falso breakout → fade/reversal.
2. **M15** — struttura: swing (U a 3 candele) + imbalance che diventa inverse-imbalance (FVG mitigato).
3. **M5** — conferma: displacement direzionale + zona di protezione (order block / imbalance).
4. **M1** — entry: attende la correzione, entra con SL dietro la zona di protezione.
Uscita: **TP1 a fine zona CRT (RR ~1.52), chiude 7080% lì → resto a break-even → runner 2030%**
verso la "liquidità successiva" (previous daily high/low). Due esempi mostrati (gold, nasdaq), entrambi
vincenti. Metà finale del video = vendita academy → lead magnet.
La valutazione critica dell'utente ("occhio Milito") aveva già isolato le red flag giuste (74% non
dimostrato, cherry-picking di 2 esempi, concetti discrezionali non falsificabili, RR basso + WR alto =
fragile, funnel commerciale). Questo diario **meccanizza e falsifica** la parte testabile.
## Inventario preventivo (non rifare lavoro)
Il setup H1 CRT è **già stato meccanizzato e scartato 3 volte** nell'onda 2026-07-02:
- `r0702_crt_base` (864 trial, 1h/4h/12h/1d): **DSR 0.000**; short "smart-money" negativo perfino IS.
- `r0702_crt_mtf` (4h→15m, 1h→5m, ~10k trade): **expectancy netta negativa OVUNQUE**; refutazione
strutturale = **"il ritest è informazione negativa"** (pattern CON ritest 40bps, SENZA +52bps).
- `r0702_crt_context` (FVG/sessioni/livelli): FVG semmai peggiora; **FADE < FOLLOW ogni anno 2019-26**.
**Angolo NUOVO non coperto** = la **gestione d'uscita (parziale+BE+runner)** e il **claim 74% WR**.
È lì che ho concentrato il test: `scripts/research/r0707_crt_topdown.py` (non tocca src/config/live;
nessun file scritto). Riusa l'harness certificato (dati/atr/sharpe/DSR di `altlib`).
## Disegno
- Detection CRT C1-C2 su **H1** (causale, nota alla chiusura di C2). Direzione = fade dello sweep.
- Entry sul TF basso (**5m/15m**, proxy di M5/M1 — ⚠️ **M1 non è nel feed certificato**): ritest della
zona violata + close-back attraverso il livello ("displacement di conferma"), **SL = estremo dello
swing basso = zona di protezione**.
- Tre schemi d'uscita sugli **stessi ingressi**: `fix1.5` (full, TP 1.5R), `fix2.0` (full, TP 2.0R),
`managed` (75% a 1.5R → resto 25% a BE, runner target = prev-daily high/low o estremo opposto C1).
- Gate progetto: hold-out 2025-01-01, griglia (k, s, d, tf, scheme) con **selezione cella solo
in-sample** + **DSR su tutti i trial**, **fee sweep** 0/0.10/0.20% RT, executability $600.
- Metrica invariante = **expectancy in R netto fee** (non il win-rate).
## Esito — il 74% è un artefatto della distanza del target, non un edge
**1) Win-rate reale (fade, tutti gli schemi, pooled BTC+ETH):**
| schema | WR mediana | expectancy R (netto 10bps) |
|---|---|---|
| fix1.5 | **35.8%** | 2.38 |
| fix2.0 | 31.3% | 2.34 |
| managed | **35.8%** | 2.17 |
Il WR a RR 1.5 è **~35%**, cioè **SOTTO** il null gambler's-ruin di un random walk senza edge
(P(+1.5R prima di 1R) = 1/(1+1.5) = **40%**). Il setup tocca 1.5R **meno spesso di una moneta**
conferma indipendente che *il ritest è informazione negativa*. Lo schema `managed` **non gonfia** il
WR sopra `fix1.5` (per costruzione: "vinci" solo se raggiungi il parziale a 1.5R, che è la stessa
condizione di `fix1.5`). **In nessuna cella si avvicina al 74%.**
**2) Il win-rate È un knob — sweep del target parziale `rr1` (managed, 5m, cella tipica):**
| rr1 | WR reale | null 1/(1+rr1) | expR (fee 10bps) | expR (fee 0) |
|------|----------|----------------|------------------|--------------|
| 0.50 | 51.4% | 66.7% | **2.54** | +0.11 |
| 0.75 | 46.9% | 57.1% | 2.51 | +0.14 |
| 1.00 | 43.2% | 50.0% | 2.47 | +0.17 |
| 1.50 | 36.3% | 40.0% | 2.45 | +0.19 |
| 2.00 | 31.8% | 33.3% | 2.42 | +0.23 |
Avvicinando il target il WR sale (esattamente come previsto dalla formula gambler's-ruin): **un "74%"
si fabbrica scegliendo un parziale vicino** (~0.35R), NON perché il metodo indovina la direzione. Ma
l'**expectancy R è costante e negativa a ogni rr1**: il WR alto non porta soldi. NB anche a WR massimo
il setup resta *sotto* il null teorico ad ogni rr1 → è pure peggio di un random walk nel raggiungere
target vicini.
**3) Gate statistici (cella scelta in-sample = 15m, k1.5, s0.1, d0.15, fix2.0):**
- **DSR = 0.000** su 48 trial (null max-Sharpe atteso 4.35). FAIL.
- **Fee sweep:** a **0 bps** expR = 0.10 e Sharpe book 0.63 → **non è morte-per-fee: l'edge lordo
non esiste** (il residuo lordo marginalmente positivo su 5m è la beta di trend dei time-exit, la
stessa autopsia del CRT base). A 10bps expR 1.26; a 20bps 2.43. Stop stretti (0.130.24%) →
la fee 0.10% pesa 0.752.6 R per trade → qualunque micro-edge lordo è polverizzato.
- **Hold-out 2025-26:** WR 32%, expR 1.56 (peggio dell'IS). Nessuna reggenza fuori campione.
**4) Executability $600:** SL sul TF basso = 0.13% (5m) / 0.24% (15m). Lo schema parziale+BE+runner
richiede **34 ordini per trade**, alcuni **sotto il min-order $5** a nozionale $1200 → non eseguibile
pulito al capitale reale.
## Verdetto
**SCARTATO.** Il claim "74% win rate" è **coerente con ZERO edge**: è la scelta di un parziale vicino
(gambler's ruin), non capacità direzionale. Smontato lo schema d'uscita, resta il setup H1 CRT che
questo progetto ha già triplo-refutato (DSR 0.000, ritest = informazione negativa, fade < follow).
Sul feed certificato BTC/ETH il metodo ha **win-rate reale ~3037% e expectancy R negativa a ogni
schema, fee, finestra e asset**; lordo di fee non c'è edge; a $600 non è nemmeno eseguibile pulito.
La parte *utile* del video è il framework di lettura (CRT + imbalance + parziali), non il numero — come
aveva già concluso la valutazione dell'utente. Da trattare come lettura del mercato, non come sistema.
## Lezione codificabile (candidata)
**Il win-rate di uno schema parziale+BE non è un parametro di merito: è ~P(tocca il primo target prima
dello stop) = 1/(1+rr1) per un processo senza drift.** Ogni claim "WR X%" da schema SMC va convertito
in **expectancy R netto fee** prima di crederci; il WR alto si compra con un target vicino a costo di
un'expectancy identica (o peggiore). Prossimo giro di gate: un helper `winrate_is_a_knob()` che, dato
un book a schema parziale, riporta expectancy-R vs WR al variare del target (rende esplicito l'artefatto).
## File
- `scripts/research/r0707_crt_topdown.py` — il test (griglia + 74%-check + knob-sweep + DSR + fee + exec).
- Prior: `r0702_crt_{base,mtf,context}.py`, diario `2026-07-02-timing-crt-wave.md`.
- Nessuna modifica a src/, config/, scripts/live/, tests/. Book live e pesi INVARIATI.
@@ -0,0 +1,108 @@
# 2026-07-09 — Incident infra: "BOOK LIVE conto offline" = cert Traefik di default (non un problema del conto)
**Tipo:** incident operativo (infra VPS), non ricerca. Nessun edge, nessun cambio a strategie/pesi/config
di trading. Documentato qui perché ha **bloccato la lettura del conto del book live** e ha prodotto
l'alert Telegram `⚠️ BOOK LIVE — conto offline`.
## Sintomo
Alert dal `cron_book` orario (`scripts/live/book_execute.py --execute`):
```
⚠️ BOOK LIVE — conto offline
nota: salto l'esecuzione, non opero a cieco
```
Nel log (`logs/cron_book.log`) le run recenti mostravano `conto non leggibile (offline) -> stop, non
eseguo a cieco`. 10 run offline su 382 totali → **regressione recente**, non uno stato storico.
## Diagnosi (catena completa)
Il book legge il conto Deribit mainnet in sola lettura via **Cerbero MCP** (`DeribitRead`,
`src/live/deribit.py``https://cerbero-mcp.tielogic.xyz`). `book_report` marca `online=False` quando
`DeribitRead.mark_price(BTC)` lancia. Riproduzione diretta:
```
mark_price BTC: FAIL -> SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] self-signed certificate
```
Il proxy presentava il **cert self-signed di default di Traefik** (`CN = TRAEFIK DEFAULT CERT`,
`notBefore Jul 9 19:55 2026`) al posto del vero Let's Encrypt. `curl -k` → HTTP 401 (backend
`cerbero-mcp` healthy, routing OK): **solo la presentazione del cert TLS era rotta**. `requests`
verifica il cert → fallisce → `online=False` → il book salta l'esecuzione (comportamento di sicurezza
**corretto**: non opera a cieco).
**Causa a monte** (log di avvio Traefik, 11:37:16Z, container `traefik-traefik-1`):
```
ERR The ACME resolve is skipped from the resolvers list
error="unable to get ACME account: permissions 660 for /acme.json are too open, please use 600"
resolver=mytlschallenge
→ (a cascata) Router uses a nonexistent certificate resolver certificateResolver=mytlschallenge
routerName=cerbero-mcp@docker [+ ~14 altri router: git, vault, portfolio, budget, cerbero-bite, ...]
```
Sequenza:
1. Immagine `traefik:latest` + **Watchtower** (`com.centurylinklabs.watchtower.enable=true`) →
auto-upgrade a **Traefik 3.7.7** con restart alle 11:37 di oggi.
2. Traefik 3.x **rifiuta** `acme.json` con permessi più larghi di `600`; il file era `660`
(`-rw-rw---- root:adriano`) → **resolver `mytlschallenge` scartato**.
3. Tutti i ~15 router che lo referenziano → "nonexistent certificate resolver" → Traefik serve il
**cert di default self-signed** a ogni host HTTPS del VPS (non solo cerbero-mcp).
4. I cert veri erano **già dentro `acme.json`** (14 domini, incl. `cerbero-mcp.tielogic.xyz`) → nessuna
ri-emissione necessaria, **zero rischio rate-limit** Let's Encrypt.
## Impatto
- **VPS-wide:** ogni servizio HTTPS dietro Traefik serviva un cert invalido (warning browser, fallimento
di ogni client che verifica il TLS). Non solo il trading.
- **Trading:** **nessun trade perso.** Target del book **flat** (TP01/SKH01 risk-off) + conto reale
~$598 (non finanziato al livello nominale): anche online il book avrebbe fatto HOLD. Il doppio gate
di sicurezza ha lavorato come previsto.
## Fix
**Immediato** (eseguito dall'utente):
```bash
sudo chmod 600 /opt/docker/traefik/acme.json
docker restart traefik-traefik-1
```
Il resolver ricarica i cert già presenti in `acme.json` e li ripresenta subito.
**Durevole** (pin dell'immagine, `/opt/docker/traefik/docker-compose.yml`):
```yaml
- image: "traefik" # = traefik:latest → Watchtower salta ai major (causa dell'incident)
+ image: "traefik:3.7" # Watchtower resta su 3.7.x: patch sì, salto a 3.8+ breaking no
```
Applicato con `docker compose up -d` (container ricreato su `traefik:3.7`, 20:05:05Z).
## Verifica (end-to-end, post-fix)
| Check | Risultato |
|---|---|
| `acme.json` perms | `600` |
| Cert `cerbero-mcp.tielogic.xyz` | issuer **Let's Encrypt** (fino 2026-09-27) |
| Traefik log | nessun `ACME resolve is skipped` / `too open` |
| Container image | `traefik:3.7` (running) |
| `DeribitRead.mark_price` | BTC 63.193 / ETH 1.746 |
| `book_report.online` | **True** · `eq_basis=mainnet USDC` · `real_equity=$598.06` · `pos_error=None` |
## Lezioni
- **`traefik:latest` + Watchtower = auto-upgrade non deterministico.** Un bump di major (qui il giro
di vite sui permessi di `acme.json`) diventa un'interruzione TLS di tutto il VPS senza intervento
umano. Pinnare la minor (`3.7`) rende i restart prevedibili pur ricevendo le patch.
- **Traefik 3.x pretende `acme.json` a `600`** (non solo warning: **scarta il resolver**). Se il file
è group/other-readable, tutti i router cadono sul cert di default.
- **Il gate `online` del book ha fatto il suo lavoro:** un problema *infra di lettura* non ha prodotto
ordini a cieco. L'alert "conto offline" va letto prima come possibile problema di **connettività/cert
al Cerbero MCP**, non del conto Deribit.
- **Runbook rapido** per "BOOK LIVE conto offline":
1. `python -c "from src.live.deribit import DeribitRead; DeribitRead().mark_price('BTC-PERPETUAL')"`
→ se `SSLError`/`CERTIFICATE_VERIFY_FAILED` è il cert del proxy, non il conto.
2. `openssl s_client -connect cerbero-mcp.tielogic.xyz:443 -servername cerbero-mcp.tielogic.xyz | openssl x509 -noout -issuer`
→ se `TRAEFIK DEFAULT CERT`, Traefik non serve il cert reale.
3. `docker logs traefik-traefik-1 | grep -iE "ACME resolve is skipped|too open|nonexistent"`
→ conferma resolver scartato; controlla `stat -c %a /opt/docker/traefik/acme.json` (deve essere `600`).
Nessun file di trading toccato; `config/live.json`, sleeve e pesi **invariati**.
+14
View File
@@ -0,0 +1,14 @@
#!/bin/bash
# BOOK DERIBIT-ONLY (TP01+SKH01 nettati) — esecuzione LIVE a cadenza ORARIA. v2.0.0+.
# SKH01 decide su griglia 230m -> serve girare piu' spesso del daily; orario IDEMPOTENTE:
# riconcilia al target NETTO corrente (se non cambia nulla -> HOLD). Il feed 5m fresco per il
# segnale SKH e' preso IN MEMORIA dentro book_execute (livefeed.fresh_5m): NON tocca i dati
# certificati su disco. Esecuzione reale gated da config/live.json (execution_enabled) + --execute.
export PATH="/home/adriano/.local/bin:$PATH"
cd /opt/docker/PythagorasGoal || exit 1
mkdir -p logs
{
echo "===== $(date -u '+%Y-%m-%dT%H:%M:%SZ') cron_book ====="
uv run python scripts/live/book_execute.py --execute
echo "===== done $(date -u '+%H:%M:%SZ') ====="
} >> logs/cron_book.log 2>&1
+10 -1
View File
@@ -9,6 +9,15 @@ mkdir -p logs
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/live_execute.py --execute # TP01 LIVE su Deribit (gated da config/live.json)
uv run python scripts/live/paper_prevday.py # forward-monitor lead prevday-breakout (PAPER, non deploy)
uv run python scripts/live/paper_statarb.py # forward-monitor lead STATARB-RESID ETH/BTC ortogonale (PAPER, non deploy)
# NB: l'esecuzione Deribit e' passata al BOOK (TP01+SKH01 nettati) via scripts/cron_book.sh a
# cadenza ORARIA (SKH01 e' a 230m: il daily mancherebbe gli ingressi). live_execute.py
# (TP01-only) NON va piu' eseguito qui, sennò i due farebbero a pugni sullo stesso strumento.
# --- COMBO cross-venue (PAPER): refresh ETF IB (GTAA) + avanza paper TP01+GTAA ---
docker compose up -d ib-gateway >/dev/null 2>&1 # gateway IB paper (idempotente)
for i in $(seq 1 25); do (echo > /dev/tcp/127.0.0.1/4002) >/dev/null 2>&1 && break; sleep 6; done
uv run --with ib_async python scripts/research/fetch_ib_equities.py --only SPY,QQQ,IWM,TLT,GLD,HYG # ETF GTAA freschi
uv run python scripts/live/paper_combo.py # avanza paper combo (forward-only)
echo "===== done $(date -u '+%H:%M:%SZ') ====="
} >> logs/cron_daily.log 2>&1
+14
View File
@@ -0,0 +1,14 @@
#!/bin/bash
# LOGGER FORWARD della vol term-structure Deribit — cadenza GIORNALIERA. v2.0.0+.
# Costruisce il dataset per un futuro calendar-vol (oggi NON backtestabile: storia per-scadenza non
# pubblica — vedi docs/diary/2026-06-26-vol-termstructure-feasibility.md). Append idempotente per
# giorno su data/raw/vol_term_<asset>.parquet. SOLO ricerca forward: NON tocca il book live ne' i
# dati certificati BTC/ETH; legge l'API pubblica Deribit (tokenless) e scrive un parquet dedicato.
export PATH="/home/adriano/.local/bin:$PATH"
cd /opt/docker/PythagorasGoal || exit 1
mkdir -p logs
{
echo "===== $(date -u '+%Y-%m-%dT%H:%M:%SZ') cron_vol_term ====="
uv run python scripts/research/log_vol_termstructure.py
echo "===== done $(date -u '+%H:%M:%SZ') ====="
} >> logs/cron_vol_term.log 2>&1
+162
View File
@@ -0,0 +1,162 @@
"""BOOK DERIBIT-ONLY LIVE EXECUTE — TP01 + SKH01 NETTATI in software su un solo conto Deribit mainnet.
Porta il conto reale al target NETTO per asset (vedi src/live/book.py): per ogni asset combina la
frazione long-flat di TP01 (peso 0.75) e il segno L/S di SKH01 (peso 0.25), e manda UN ordine con
segno (long/short/flip) per raggiungerlo. Poi assicura un disaster-SL on-book sulla posizione NETTA.
DOPPIO GATE DI SICUREZZA (entrambi necessari per inviare ordini reali):
1. config/live.json -> "execution_enabled": true (master switch, default false)
2. flag CLI --execute
Senza entrambi e' un DRY-RUN (stampa il piano, NON invia). Reconciliation dopo ogni ordine; log in
data/live/book_executions.jsonl.
⚠️ CADENZA: SKH01 decide su griglia 230m -> questo script va lanciato ogni ~230 minuti con la feed
fresca all'ultima barra chiusa (NON il cron giornaliero, che mancherebbe gli ingressi). Gli exit di
SKH sono SOFTWARE (latenza fino a fine barra 230m); solo il disaster-SL (-30%) e' on-book.
uv run python scripts/live/book_execute.py # DRY-RUN (piano, nessun ordine)
uv run python scripts/live/book_execute.py --execute # esegue SOLO se execution_enabled=true
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
import pandas as pd
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
from src.live.book import book_report
from src.live.execution import DeribitTrader
from src.live.notifier import notify
CONFIG = PROJECT_ROOT / "config" / "live.json"
LOG_DIR = PROJECT_ROOT / "data" / "live"
LOG = LOG_DIR / "book_executions.jsonl"
def load_config() -> dict:
cfg = json.loads(CONFIG.read_text()) if CONFIG.exists() else {}
cfg.setdefault("execution_enabled", False)
cfg.setdefault("max_notional_per_asset_usd", 300.0)
cfg.setdefault("min_order_usd", 5.0)
cfg.setdefault("disaster_sl_pct", 0.30)
return cfg
def log_event(rec: dict):
LOG_DIR.mkdir(parents=True, exist_ok=True)
with open(LOG, "a") as f:
f.write(json.dumps(rec) + "\n")
def _run():
cfg = load_config()
want_execute = "--execute" in sys.argv[1:]
enabled = bool(cfg["execution_enabled"])
do_execute = want_execute and enabled
min_order = float(cfg["min_order_usd"])
sl_pct = float(cfg["disaster_sl_pct"])
r = book_report(live_feed=True) # target NETTO + conto/posizioni reali (feed SKH fresco)
equity = r["equity"]
print("=" * 88)
print(" BOOK DERIBIT LIVE EXECUTE — TP01(0.75)+SKH01(0.25) NETTATI — Deribit mainnet (USDC linear)")
print("=" * 88)
mode = ("ESECUZIONE REALE" if do_execute else
("ARMATO ma manca --execute" if enabled else "DRY-RUN (execution_enabled=false)"))
print(f" modo : {mode}")
print(f" gate : execution_enabled={enabled} | --execute={want_execute}")
print(f" conto reale : ${r['real_equity']:,.2f}" if r["real_equity"] else f" conto: {r['eq_basis']}")
print(f" sizing base : ${equity:,.2f} | cap/asset ${r['cap_per_asset']:.0f} | min ${min_order:.0f} | disaster-SL -{sl_pct*100:.0f}%")
print(f" ultima barra : {r['last_data']}\n")
if r.get("skh_error"): # SKH feed fallito -> book.py ha forzato flat IN SILENZIO
print(f" ⚠️ SKH FEED ERRORE (SKH forzato flat!): {r['skh_error']}")
notify("⚠️ BOOK — SKH feed fallito (sleeve forzato flat)", {"error": r["skh_error"]})
if not r["online"]:
print(" conto non leggibile (offline) -> stop, non eseguo a cieco.")
if do_execute:
notify("⚠️ BOOK LIVE — conto offline", {"nota": "salto l'esecuzione, non opero a cieco"})
return
if r.get("pos_error"): # ONLINE ma posizione IGNOTA (read fallita -> assunta flat)
print(f" 🛑 POSIZIONE NON LEGGIBILE -> NON eseguo a cieco: {r['pos_error']}")
if do_execute:
notify("🛑 BOOK LIVE — posizione non leggibile", {"error": r["pos_error"],
"nota": "salto l'esecuzione, non opero a cieco"})
return
if r.get("eq_fallback"): # equity reale non leggibile -> sizing su paper_cap
print(f" ⚠️ EQUITY FALLBACK (sizing su paper_cap, NON blocco): {r['eq_fallback']}")
if do_execute: # solo diagnostica: l'hard-cap $/asset limita il downside
notify("⚠️ BOOK LIVE — equity fallback (sizing su paper_cap)", {"nota": r["eq_fallback"]})
trader = DeribitTrader() if do_execute else None
actions = []
for a in r["assets"]:
asset, inst = a["asset"], a["instrument"]
net, cur, mark = a["net_target"], a["position_usd"], a["mark"]
sk = a["skh_state"]
sk_txt = "flat" if sk == "flat" else f"{sk['dir']}@{sk.get('entry')}"
order = a["order"]
if order is None:
act = "HOLD (a target)"
elif order.get("is_close"):
act = f"CLOSE ${cur:,.0f}"
elif order.get("needs_flip"):
act = f"FLIP -> ${net:,.0f}"
else:
act = f"{order['side'].upper()} ${order['delta']:+,.0f}"
print(f" {asset:<3} TP {a['tp_frac']:+.3f} · SKH {a['skh_sign']:+d}({sk_txt}) -> net ${net:+,.0f} "
f"| pos ${cur:+,.0f} -> {act}")
if do_execute and order is not None:
fills = trader.rebalance_signed(inst, net, mark, min_usd=min_order)
newpos = trader.position_usd(inst)
for f in fills:
print(f" -> {f.side.upper()} {f.filled:.4f} @ ${f.price or 0:,.1f} fee {f.fee_usdc:.5f} "
f"({'OK' if f.verified else 'NON VERIFICATO: ' + f.notes})")
log_event(dict(ts_utc=str(pd.Timestamp(r['last_data'])), asset=asset, action=act,
side=f.side, filled=f.filled, price=f.price, fee=f.fee_usdc,
verified=f.verified, notes=f.notes, net_target=net, pos_after=newpos,
tp_frac=a["tp_frac"], skh_sign=a["skh_sign"]))
det = dict(asset=asset, side=f.side, amount=round(f.filled, 4), price=round(f.price or 0, 1),
fee=round(f.fee_usdc, 5), net=round(net, 0), pos_after=round(newpos, 0))
notify(f"✅ BOOK {act}" if f.verified else "⚠️ BOOK ORDINE NON VERIFICATO",
det if f.verified else {**det, "notes": f.notes})
print(f" reconcile: pos ${newpos:,.0f}")
if do_execute:
ds = trader.ensure_disaster_sl(inst, sl_pct) # bracket su posizione NETTA (adatta long/short)
print(f" disaster-SL: {ds.get('state')}" + (f" @ ${ds['stop']:,.1f}" if ds.get("stop") else ""))
if ds.get("state") == "placed":
notify("🛡️ BOOK disaster-SL piazzato", {"asset": asset, "stop": round(ds.get("stop") or 0, 1),
"amount": round(ds.get("amount") or 0, 4)})
elif ds.get("state") == "place-failed":
notify("⚠️ BOOK disaster-SL FALLITO", {"asset": asset, "notes": ds.get("notes")})
actions.append(act)
print()
if not do_execute:
print(" => DRY-RUN: nessun ordine inviato." +
("" if enabled else " Per armare: config/live.json execution_enabled=true + --execute."))
elif all(x.startswith("HOLD") for x in actions):
print(" => Nessuna azione: conto gia' al target netto del book.")
else:
print(" => Esecuzione completata (vedi data/live/book_executions.jsonl).")
def main():
try:
_run()
except Exception as e:
notify("🛑 BOOK LIVE — ERRORE", {"error": f"{type(e).__name__}: {e}"})
raise
if __name__ == "__main__":
main()
+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()
+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()
+176
View File
@@ -0,0 +1,176 @@
"""FORWARD-MONITOR — STATARB-RESID (relative-value ETH/BTC, dollar-neutral 2 gambe), PAPER.
NON è esecuzione reale. È il monitoraggio forward-only del LEAD dello sweep 2026-06-29 (thread
"segnali ortogonali"): il PRIMO stream visto che è INSIEME ortogonale al book (corr→book 0.027,
beta-mkt 0.013) ED eseguibile a $600 (book a 2 gambe BTC+ETH perp, NON STAT-MODE come XS01). Resta
sotto la soglia solo sull'EDGE (Sharpe ~0.84, deflated-Sharpe <0.95 su storia ~2.5 anni). Questo
monitor accumula la finestra OUT-OF-SAMPLE VERA per decidere se l'edge regge — stesso trattamento di
PREVDAY/STA05/XS01 STAT-MODE. Nessun edge creduto prima.
ⓘ NATURA DEL SEGNALE (onesto): la cella vincente è W=45 **sgn=+1**, NON sgn=-1. Cioè il residuo di
ETH dopo β·BTC (β OLS rolling causale) viene seguito in MOMENTUM (long lo spread quando ETH è ricco
vs il suo fit su BTC), non in mean-reversion: le dislocazioni relative ETH-vs-BTC CONTINUANO a 1d
(la MR pura, sgn=-1, perde -1.4 in-sample). È relative-MOMENTUM, dollar-neutral → ortogonale al
trend assoluto di TP01.
CONFIG CONGELATA (dallo sweep, selezione IN-SAMPLE-only, mai sul max hold-out):
W=45, sgn=+1, tf=1d, vol-target 20%, leva cap 2x. Riusa il segnale ESATTO di
scripts/research/orthogonal_signals.py (nessuna reimplementazione → niente drift).
DESIGN (onesto, come paper_prevday):
- Legge BTC/ETH 1d certificati (build_joint), calcola il segnale CAUSALE su tutta la storia.
- Alla prima esecuzione fissa l'INCEPTION all'ultima barra 1d chiusa (forward-only: lo storico
NON entra nel PnL di paper).
- Ogni run processa le NUOVE barre 1d chiuse: applica il rendimento dello spread tenuto,
addebita le fee su 2 GAMBE (ogni Δpos muove ETH e BTC → 2×|Δpos|), registra i flip di segno.
- DUE libri in parallelo (onestà sull'esecuzione a piccolo capitale):
* MODELED : capitale nominale $2000, ribilanciamento continuo (fee su ogni |Δpos|).
* REAL-$600: capitale reale $600, salta i ribilanci di nozionale-per-gamba < min_order ($5)
→ il gap MODELED-REAL = l'haircut di fill reale (qui ~0: turnover bassissimo a 1d).
- Per barre fresche aggiornare prima i dati: uv run python scripts/analysis/rebuild_history.py --asset BTC ETH
Stato: data/paper_statarb/{state.json, trades.jsonl, returns.jsonl} (append-only).
uv run python scripts/live/paper_statarb.py # avanza col dato disponibile
uv run python scripts/live/paper_statarb.py --status # solo stato, non avanza
uv run python scripts/live/paper_statarb.py --reset # azzera (riparte da ora)
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
import numpy as np
import pandas as pd
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
sys.path.insert(0, str(PROJECT_ROOT / "scripts" / "research"))
# Segnale ESATTO dello sweep (nessuna reimplementazione → identità garantita col backtest).
from orthogonal_signals import build_joint, f_statarb_resid, spread_ret # noqa: E402
STATE_DIR = PROJECT_ROOT / "data" / "paper_statarb"
STATE_FILE = STATE_DIR / "state.json"
TRADES_FILE = STATE_DIR / "trades.jsonl"
RETURNS_FILE = STATE_DIR / "returns.jsonl"
# --- CONFIG CONGELATA (frozen) -----------------------------------------------------------
W_FROZEN = 45
SGN_FROZEN = +1
FEE_SIDE = 0.0005 # 0.05%/side = 0.10% round-trip (Deribit taker)
MODELED_CAPITAL = 2000.0 # nominale, ribilanciamento continuo
REAL_CAPITAL = 600.0 # capitale mainnet reale
MIN_ORDER = 5.0 # min order Deribit -> sotto, il conto vero NON ribilancia
def _signal(j: pd.DataFrame):
"""(ts, dt, pos, sr): posizione vol-targeted sullo spread (decisa <= close[i]) + ritorno spread."""
pos = np.nan_to_num(f_statarb_resid(W=W_FROZEN, sgn=SGN_FROZEN)(j))
return (j["timestamp"].values.astype("int64"),
pd.to_datetime(j["datetime"]).values, pos, spread_ret(j))
def _state_io(write: dict | None = None):
if write is not None:
STATE_DIR.mkdir(parents=True, exist_ok=True)
STATE_FILE.write_text(json.dumps(write, indent=2))
return write
return json.loads(STATE_FILE.read_text()) if STATE_FILE.exists() else None
def _append(path: Path, rec: dict):
STATE_DIR.mkdir(parents=True, exist_ok=True)
with open(path, "a") as f:
f.write(json.dumps(rec) + "\n")
def init_state(j: pd.DataFrame) -> dict:
ts, _, pos, _ = _signal(j)
last = int(ts[-1])
return dict(start_ts=last, last_ts=last, n_bars=0,
pos_modeled=float(pos[-1]), pos_real=float(pos[-1]),
cap_modeled=MODELED_CAPITAL, cap_real=REAL_CAPITAL,
peak_modeled=MODELED_CAPITAL, peak_real=REAL_CAPITAL,
dd_modeled=0.0, dd_real=0.0, n_trades=0)
def advance(st: dict, j: pd.DataFrame) -> dict:
ts, dt, pos, sr = _signal(j)
new = [i for i in range(len(ts)) if ts[i] > st["last_ts"]]
if not new:
return st
pm, pr = st["pos_modeled"], st["pos_real"] # posizioni TENUTE (decise alla barra precedente)
cm, cr = st["cap_modeled"], st["cap_real"]
pkm, pkr, ddm, ddr = st["peak_modeled"], st["peak_real"], st["dd_modeled"], st["dd_real"]
ntr = st.get("n_trades", 0)
for i in new:
s = float(sr[i])
tgt = float(pos[i]) # bersaglio deciso a close[i] (eseguito da i+1)
# MODELED: rendimento dello spread tenuto durante la barra i, poi ribilancia al target
net_m = pm * s - FEE_SIDE * 2.0 * abs(tgt - pm)
# REAL-$600: salta i ribilanci sub-min_order (nozionale per-gamba |Δpos|*cap < $5)
executed = abs(tgt - pr) * REAL_CAPITAL >= MIN_ORDER
new_pr = tgt if executed else pr
net_r = pr * s - FEE_SIDE * 2.0 * abs(new_pr - pr)
if np.sign(new_pr) != np.sign(pr):
_append(TRADES_FILE, dict(ts=int(ts[i]), dt=str(pd.Timestamp(dt[i])),
action="FLIP", from_pos=round(pr, 4), to_pos=round(new_pr, 4)))
ntr += 1
cm *= (1.0 + max(net_m, -0.99)); cr *= (1.0 + max(net_r, -0.99))
pkm = max(pkm, cm); pkr = max(pkr, cr)
ddm = max(ddm, (pkm - cm) / pkm if pkm > 0 else 0.0)
ddr = max(ddr, (pkr - cr) / pkr if pkr > 0 else 0.0)
pm, pr = tgt, new_pr
_append(RETURNS_FILE, dict(ts=int(ts[i]), dt=str(pd.Timestamp(dt[i])),
net_modeled=round(net_m, 6), net_real=round(net_r, 6),
pos=round(new_pr, 4), cap_modeled=round(cm, 2), cap_real=round(cr, 2)))
st.update(last_ts=int(ts[new[-1]]), n_bars=st.get("n_bars", 0) + len(new),
pos_modeled=pm, pos_real=pr, cap_modeled=cm, cap_real=cr,
peak_modeled=pkm, peak_real=pkr, dd_modeled=ddm, dd_real=ddr, n_trades=ntr)
return st
def print_status(st: dict, j: pd.DataFrame):
last_data = int(j["timestamp"].iloc[-1])
days = (last_data - st["start_ts"]) / 86400_000
rm = st["cap_modeled"] / MODELED_CAPITAL - 1
rr = st["cap_real"] / REAL_CAPITAL - 1
print("\n STATARB-RESID forward-monitor (PAPER — relative-value ETH/BTC, ortogonale, NON deploy)")
print(f" config CONGELATA: W={W_FROZEN} sgn={SGN_FROZEN:+d} (residual relative-momentum, dollar-neutral)")
print(f" forward da {pd.Timestamp(st['start_ts'], unit='ms', tz='UTC').date()} "
f"({st['n_bars']} barre 1d ~{days:.0f}g) flip di segno: {st['n_trades']}")
print(f" posizione spread corrente (long ETH / short BTC per unità): {st['pos_real']:+.3f}")
print(f" MODELED ($2000 nominale): {rm*100:+6.2f}% eq ${st['cap_modeled']:.2f} maxDD {st['dd_modeled']*100:.1f}%")
print(f" REAL-$600 (min-order $5) : {rr*100:+6.2f}% eq ${st['cap_real']:.2f} maxDD {st['dd_real']*100:.1f}%")
print(f" -> fill-haircut MODELED-REAL: {(rm-rr)*100:+.2f} pp (atteso ~0: turnover basso a 1d)")
print(f" log: {RETURNS_FILE}\n")
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--status", action="store_true")
ap.add_argument("--reset", action="store_true")
args = ap.parse_args()
j = build_joint("1d")
if args.reset:
for p in (STATE_FILE, TRADES_FILE, RETURNS_FILE):
if p.exists():
p.unlink()
st = init_state(j); _state_io(st)
print("forward-monitor STATARB-RESID inizializzato (forward-only da ora).")
print_status(st, j); return
st = _state_io()
if st is None:
st = init_state(j); _state_io(st)
print("forward-monitor STATARB-RESID inizializzato (forward-only da ora).")
print_status(st, j); return
if not args.status:
st = advance(st, j); _state_io(st)
print_status(st, j)
if __name__ == "__main__":
main()
+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()
+329 -11
View File
@@ -29,12 +29,14 @@ from __future__ import annotations
import inspect
import json
import math
import sys
from functools import lru_cache
from pathlib import Path
import numpy as np
import pandas as pd
from scipy.stats import norm
# --- make `from src...` work no matter where the agent's script lives -------
_ROOT = Path(__file__).resolve().parents[3]
@@ -334,14 +336,56 @@ def candidate_daily(target_fn, tf: str = "1d", fee_side: float = FEE_SIDE) -> pd
return _to_daily(0.5 * J[CERTIFIED[0]] + 0.5 * J[CERTIFIED[1]])
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 -> meaningfully lifts the OOS blend and is not just leverage-of-trend
ADDS -> lifts the blend, PERSISTENTLY (multi-cut), beats the zero-corr noise
null, in BOTH TP01-up and TP01-down regimes
HEDGE -> low corr but only pays when TP01 is WEAK (a drawdown dampener, not a
standing premium): real, but price it as a hedge, not as alpha
NOISE -> uplift indistinguishable from a random zero-corr asset (diversification
math, not a signal)
REDUNDANT -> ~identical to TP01 (corr high, ~zero uplift): a re-skin, no slot
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."""
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:
@@ -378,12 +422,10 @@ def marginal_vs_tp01(cand_daily: pd.Series, weights=(0.25, 0.5)) -> dict:
# the blend uplift to be positive in the earliest CLEAN hold-out year AND to survive a
# drop-one-month jackknife. This is lesson #2 of the 2026-06-20 sweep, in code.
out["clean_year_uplift"] = out["jackknife_min_uplift"] = None
out["robust_oos"] = False
robust_h = False
if has_h:
ww = 0.25
def _u(sub):
return _sh((1 - ww) * sub["B"] + ww * sub["C"]) - _sh(sub["B"])
return _uplift_series(sub["B"], sub["C"])
yrs = sorted(set(JH.index.year))
clean = JH[JH.index.year == yrs[0]]
cu = _u(clean) if len(clean) > 20 else None
@@ -392,17 +434,79 @@ def marginal_vs_tp01(cand_daily: pd.Series, weights=(0.25, 0.5)) -> dict:
if len(months) > 1 else _u(JH))
out["clean_year_uplift"] = round(cu, 3) if cu is not None else None
out["jackknife_min_uplift"] = round(jk, 3) if jk is not None else None
out["robust_oos"] = bool(cu is not None and cu > 0.02 and jk is not None and jk > 0.0)
# verdict (weight 0.25 = a satellite slot; hold-out is what the defensive stack cares about)
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_h is not None and up_h >= 0.05 and up_f > -0.15 and ch < 0.85:
v = "ADDS"
elif up_f <= -0.10 and (up_h is None or up_h <= 0.0):
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
@@ -416,8 +520,12 @@ def study_marginal(name: str, target_fn, tf: str = "1d", fee_side: float = FEE_S
absolute = study_weights(name, target_fn, tfs=(tf,))
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("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)
@@ -432,6 +540,13 @@ def fmt_marginal(rep: dict) -> str:
f"beta {m.get('beta_to_tp01')} resid Sharpe {m.get('resid_sharpe_full')} alpha/yr {m.get('alpha_ann')}")
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():
@@ -442,6 +557,209 @@ def fmt_marginal(rep: dict) -> str:
return "\n".join(lines)
# ===========================================================================
# HARNESS REALISM — two gates codified from the 2026-06-21 intraday wave.
#
# LESSON 1 (day-boundary): open_drive ("first 8h UTC predicts rest-of-day") scored a
# +0.23 uplift but INVERTED to -0.10 when the UTC day start was shifted 4h — a calendar-
# LABELING artifact, not an intraday effect. A real hour/session/day edge degrades
# gracefully under a boundary shift; an artifact flips sign.
#
# LESSON 2 (small-cap fills): eval_weights charges fee on EVERY |Δposition|, incl. the
# thousands of sub-dollar rebalances a vol-target overlay produces. At ~$600 real capital a
# $0.03 trade can't execute — the modeled proportional fee is a continuous-rebalancing
# fiction. eval_weights_smallcap skips changes below min_order and reports the Sharpe haircut.
# ===========================================================================
def _shift_calendar(df: pd.DataFrame, offset_hours: int) -> pd.DataFrame:
"""Relabel the clock the SIGNAL sees by +offset_hours (datetime & timestamp), leaving
prices/returns untouched -> the signal's .dt.hour / day-grouping shifts, the backtest
does not. (get() is cached; copy so we never mutate the shared frame.)"""
d = df.copy()
dt = pd.to_datetime(d["datetime"], utc=True) + pd.Timedelta(hours=offset_hours)
d["datetime"] = dt
if "timestamp" in d:
d["timestamp"] = d["timestamp"].astype("int64") + int(offset_hours * 3600 * 1000)
return d
def day_boundary_robust(target_fn, tf: str = "1h",
offsets=(0, 3, 6, 9, 12, 15, 18, 21), w: float = 0.25) -> dict:
"""Is a candidate's marginal uplift ROBUST to shifting the UTC day boundary? For each
offset we relabel the calendar the signal sees, recompute its 50/50 BTC+ETH daily series
and the blend uplift vs TP01. A datetime-independent signal is INVARIANT (spread ~0); a
calendar signal that stays positive is ROBUST; one whose uplift flips sign is ARTIFACT-RISK
(open_drive). Run this on ANY hour/session/day-of-week signal before believing it."""
B = tp01_baseline_daily()
per = {}
for off in offsets:
series = {}
for a in CERTIFIED:
df0 = get(a, tf) # ORIGINAL bars/dates
tgt = _call_target(target_fn, _shift_calendar(df0, off), a) # signal sees shifted clock
ev = eval_weights(df0, tgt) # backtest on the real calendar
series[a] = pd.Series(ev["net"], index=ev["idx"])
J = pd.concat(series, axis=1, join="inner").fillna(0.0)
cand = _to_daily(0.5 * J[CERTIFIED[0]] + 0.5 * J[CERTIFIED[1]])
JJ = pd.concat({"B": B, "C": cand}, axis=1, join="inner").dropna()
per[int(off)] = round(_sh((1 - w) * JJ["B"] + w * JJ["C"]) - _sh(JJ["B"]), 3) if len(JJ) > 30 else None
ups = [v for v in per.values() if v is not None]
if not ups:
return dict(per_offset=per, verdict="N/A", reason="no evaluable offsets")
spread = round(max(ups) - min(ups), 3)
calendar_sensitive = spread > 0.02
robust = min(ups) > 0
verdict = ("INVARIANT" if not calendar_sensitive else ("ROBUST" if robust else "ARTIFACT-RISK"))
return dict(per_offset=per, base=per[offsets[0]], min=min(ups), max=max(ups),
spread=spread, calendar_sensitive=calendar_sensitive,
robust_to_boundary=robust, verdict=verdict)
def eval_weights_smallcap(df: pd.DataFrame, target, capital: float = 600.0,
min_order: float = 5.0, fee_side: float = FEE_SIDE) -> dict:
"""Honest net at SMALL capital. A desired position change whose notional |Δw|*capital is
below min_order is NOT executed (held -> tracking error, no trade) — removing the
continuous-rebalancing fiction. Returns realistic vs modeled metrics, the Sharpe haircut,
and the number of trades that actually execute. (Applies to ANY sleeve at this capital,
TP01 included.)"""
c = df["close"].values.astype(float)
tgt = np.clip(np.nan_to_num(np.asarray(target, float)), -10, 10)
held = np.empty(len(tgt)); cur = 0.0; n_tr = 0
for i in range(len(tgt)):
if abs(tgt[i] - cur) * capital >= min_order:
cur = tgt[i]; n_tr += 1
held[i] = cur
r = simple_returns(c)
pos = np.zeros(len(held)); pos[1:] = held[:-1]
turn = np.abs(np.diff(pos, prepend=0.0))
net = pos * r - fee_side * turn; net[0] = 0.0
idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True))
real = _metrics_from_net(net, idx)
modeled = eval_weights(df, tgt, fee_side=fee_side)["full"]
bpy_d = bars_per_day(df) * 365.25
return dict(realistic=real, modeled=modeled,
sharpe_haircut=round(modeled["sharpe"] - real["sharpe"], 3),
n_executed_trades=int(n_tr),
executed_turnover_per_year=round(float(turn.sum() / (len(turn) / bpy_d)), 1))
def causality_ok(target_fn, tf: str = "1h", assets=CERTIFIED,
tail: int = 80, tol: float = 1e-3) -> dict:
"""Online-consistency / LOOK-AHEAD guard for a continuous target_fn(df) [or (df, asset)].
eval_weights SHIFTS the position so you cannot leak by multiplying a weight by the SAME
bar's return — but it does NOT verify the FEATURE construction is causal: a centered
window, a .shift(-k), or a full-sample statistic would pass eval_weights yet peek at the
future. Here we recompute the target on a TRUNCATED prefix and require its tail to MATCH
target(full)[:cut] (the bars a deployable signal would have emitted in real time). Any
future-peeking diverges. Run this in every altlib-based lab (blind/ortho already do)."""
worst = 0.0; bad = False; checked = 0
for a in assets:
df = get(a, tf)
full = np.nan_to_num(np.asarray(_call_target(target_fn, df, a), float))
n = len(df)
for cut in (int(n * 0.80), int(n * 0.92)):
if cut <= tail + 5 or cut >= n:
continue
sub = df.iloc[:cut].reset_index(drop=True)
s = np.nan_to_num(np.asarray(_call_target(target_fn, sub, a), float))
if len(s) != cut:
bad = True
continue
d = np.abs(s[cut - tail:cut] - full[cut - tail:cut])
worst = max(worst, float(np.max(d)) if len(d) else 0.0)
checked += 1
return dict(ok=bool((not bad) and worst <= tol),
max_tail_diff=round(worst, 8), checked=checked,
reason=("length-mismatch on prefix" if bad else None))
# ===========================================================================
# SELECTION-ON-HOLDOUT GATE — codified 2026-06-29 from filone B (intraday ERM).
#
# LESSON 3: intraday_regime.py picked its "winner" cell by MAX hold-out Sharpe over a ~60-cell
# grid, then ran study_marginal on THAT cell -> earns_slot=True. But the slot was an artifact of
# SELECTING THE CELL ON THE HOLD-OUT: picking the cell IN-SAMPLE-ONLY (no peeking) lands on a
# DIFFERENT, TP01-correlated cell that scores NEUTRAL, and the standalone Sharpe deflates to
# DSR~0.0-0.24 over the trials searched. study_marginal alone can't catch this — it judges ONE
# stream and never sees how the cell was chosen. The fix is two-fold and lives here:
# (1) choose the cell IN-SAMPLE-ONLY (or walk-forward) BEFORE scoring the marginal, and
# (2) DEFLATE the standalone Sharpe for the number of cells/families searched.
# Twin of the CC01 ("implausible Sharpe -> hidden risk") and alt-sweep ("hold-out-fitting") blind
# spots, in its "selection-on-holdout" form.
# ===========================================================================
def deflated_sharpe(sr_ann, all_sr_ann, daily_ret, dpy: float = 365.25):
"""Deflated Sharpe Ratio (Bailey & Lopez de Prado): P(true Sharpe > the MAX Sharpe expected
under the null of N independent trials). Penalizes multiple-testing — a standalone Sharpe ~1
over a 100+ cell grid is routinely NOT significant once deflated. sr_ann = annualized Sharpe
of the CHOSEN config; all_sr_ann = the Sharpe of EVERY cell searched; daily_ret = the chosen
config's daily returns (for skew/kurt/T). Returns (DSR, expected_null_max_sharpe_ann);
PASS if DSR >= 0.95."""
r = np.asarray(pd.Series(daily_ret).dropna().values, float)
T = len(r)
if T < 30 or np.std(r) == 0:
return float("nan"), float("nan")
sr = sr_ann / math.sqrt(dpy)
trials = np.asarray([s / math.sqrt(dpy) for s in all_sr_ann if np.isfinite(s)], float)
N = max(len(trials), 2)
var_tr = float(np.var(trials, ddof=1)) if N > 1 else 0.0
emc = 0.5772156649
z1 = norm.ppf(1 - 1.0 / N)
z2 = norm.ppf(1 - 1.0 / (N * math.e))
sr0 = math.sqrt(var_tr) * ((1 - emc) * z1 + emc * z2)
sk = float(pd.Series(r).skew())
ku = float(pd.Series(r).kurt()) + 3.0 # pandas kurt = excess
den = math.sqrt(max(1e-9, 1 - sk * sr + (ku - 1) / 4.0 * sr ** 2))
dsr = float(norm.cdf((sr - sr0) * math.sqrt(T - 1) / den))
return dsr, sr0 * math.sqrt(dpy)
def select_cell_insample(factory, grid, tfs, fee_side: float = FEE_SIDE) -> dict:
"""Pick a config WITHOUT looking at the hold-out: rank grid cells by IN-SAMPLE (pre-HOLDOUT)
standalone Sharpe of the 50/50 candidate. `factory(tf=..., **params)` -> target_fn; each grid
item is a dict of factory kwargs (besides tf). Returns the in-sample-best cell, all rows
(sorted), and EVERY cell's FULL Sharpe (for deflated_sharpe). This is the honest replacement
for picking the max-hold-out cell."""
rows = []
for tf in tfs:
for params in grid:
try:
daily = candidate_daily(factory(tf=tf, **params), tf=tf, fee_side=fee_side)
except Exception:
continue
ins = daily[daily.index < HOLDOUT]
is_sh = _sh(ins) if len(ins) > 60 else float("nan")
rows.append(dict(tf=tf, params=params, insample_sharpe=round(is_sh, 3),
full_sharpe=round(_sh(daily), 3)))
valid = [r for r in rows if np.isfinite(r["insample_sharpe"])]
chosen = max(valid, key=lambda r: r["insample_sharpe"]) if valid else None
return dict(chosen=chosen,
rows=sorted(valid, key=lambda r: r["insample_sharpe"], reverse=True),
all_full_sharpe=[r["full_sharpe"] for r in rows])
def study_family_honest(name: str, factory, grid, tfs, fee_side: float = FEE_SIDE,
dsr_min: float = 0.95) -> dict:
"""HARDENED family gate. A grid-searched directional candidate earns a slot ONLY if, picking
the cell IN-SAMPLE-ONLY (no hold-out peeking), it STILL earns_slot via study_marginal AND its
standalone Sharpe survives deflation for the WHOLE grid searched. Use this INSTEAD of
cherry-picking the max-hold cell and calling study_marginal on it."""
sel = select_cell_insample(factory, grid, tfs, fee_side=fee_side)
ch = sel["chosen"]
if ch is None:
return dict(name=name, chosen=None, earns_slot_honest=False,
reason="no valid in-sample cell")
fn = factory(tf=ch["tf"], **ch["params"])
sm = study_marginal(f"{name} ISpick {ch['params']}", fn, tf=ch["tf"], fee_side=fee_side)
daily = candidate_daily(fn, tf=ch["tf"], fee_side=fee_side)
dsr, sr0 = deflated_sharpe(_sh(daily), sel["all_full_sharpe"], daily)
dsr_pass = bool(np.isfinite(dsr) and dsr >= dsr_min)
return dict(name=name, n_cells=len(sel["all_full_sharpe"]), chosen=ch, rows=sel["rows"],
marginal=sm, earns_slot_marginal=bool(sm["earns_slot"]),
deflated_sharpe=round(dsr, 3) if np.isfinite(dsr) else None,
expected_null_max=round(sr0, 3) if np.isfinite(sr0) else None,
dsr_pass=dsr_pass,
earns_slot_honest=bool(sm["earns_slot"] and dsr_pass))
# ===========================================================================
# DRIVERS — run a hypothesis across both assets, several TFs, with a fee sweep.
# ===========================================================================
@@ -0,0 +1,13 @@
{
"oos_benchmark_buyhold": {"pnl": -0.07, "maxdd": 0.68, "sharpe": 0.22},
"top_survivors_oos": {
"agent_04_macd": {"pnl_A": 0.23, "pnl_B": 0.19, "maxdd": 0.11, "sharpe_min": 0.84, "corr_to_trend": 0.52},
"agent_06_accel": {"pnl_A": 0.40, "pnl_B": 0.22, "maxdd": 0.12, "sharpe_min": 0.79, "corr_to_trend": 0.50},
"agent_23_vol_of_vol":{"pnl_A": 0.30, "pnl_B": 0.32, "maxdd": 0.21, "sharpe_min": 0.69, "corr_to_trend": 0.46},
"agent_44_obv": {"pnl_A": 0.22, "pnl_B": 0.20, "maxdd": 0.16, "sharpe_min": 0.60, "corr_to_trend": 0.31}
},
"skeptic_regime_luck": "REFUTED x3 - top-5 of ~800 OOS bars supply 67-102% of PnL; drop-10 turns negative; accel COMB final-third Sharpe -1.21; A & B disagree on WHEN it works.",
"skeptic_trend_redundancy": "REFUTED x4 - Newey-West HAC alpha t-stats +0.92..+1.51 (none > 1.96); corr-to-trend 0.34-0.74, beta 0.45-0.73; residual mean +0.05-0.08/yr = noise. Better-tuned TSMOM, not orthogonal alpha.",
"skeptic_overfit": "MACD not-refuted (genuine one-axis plateau, OOS Sh 0.84 not train 1.40); ACCEL REFUTED (acceleration term HURTS OOS, LAG knife-edge -63% on -20%); VOV REFUTED (PCTL 0.80->0.60 destroys 73% of OOS Sharpe).",
"verdict": "52 blind agents, orchestrator scored all on OOS PnL & maxDD. NOTHING new survives. All winners are trend-beta of two up-trending curves; OOS Sharpe ceiling ~0.84 (decayed from train ~1.4); no statistically distinguishable alpha vs TSMOM. Independent BLIND re-confirmation of the project's ~1.3 directional ceiling. macd = least-bad, TP01-class, forward-monitor not deploy."
}
+225
View File
@@ -0,0 +1,225 @@
"""Adversarial parameter-perturbation harness for the 3 blind survivors.
Re-implements each signal parameterized; perturbs each key param +/-25% (and larger
jumps), re-evaluates OOS (test slice, A & B) and train. Reports min/median/max OOS
Sharpe across the grid and the train->test Sharpe decay. Also a fee bump to 0.20% RT.
"""
import sys
import numpy as np
import pandas as pd
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/blind")
import blindlib as bl
FEE_BASE = 0.0005 # 0.10% RT
FEE_BUMP = 0.001 # 0.20% RT
def _masks(series):
df = bl.load(series, "full")
cut = bl.split_cut(series)
test = np.zeros(len(df), bool); test[cut:] = True
train = np.zeros(len(df), bool); train[:cut] = True
return df, train, test
# ---------------- agent_04 MACD ----------------
def macd_signal(df, FAST=26, SLOW=52, SIGNAL=9, SLOPE_W=0.20, SHORT_W=0.5,
TARGET_VOL=0.20, VOL_WIN=30, LEV_CAP=1.0):
c = df["close"].values.astype(float)
macd = bl.ema(c, FAST) - bl.ema(c, SLOW)
signal_line = bl.ema(macd, SIGNAL)
hist = macd - signal_line
base = np.where(np.sign(hist) == np.sign(macd), np.sign(macd), 0.0)
slope = np.sign(np.diff(hist, prepend=hist[0]))
raw = (1.0 - SLOPE_W) * base + SLOPE_W * slope
raw = np.clip(raw, -1.0, 1.0)
raw = np.where(raw < 0, raw * SHORT_W, raw)
raw = np.nan_to_num(raw, nan=0.0)
pos = bl.vol_target(raw, df, target_vol=TARGET_VOL, vol_win_days=VOL_WIN,
leverage_cap=LEV_CAP)
return np.clip(pos, -1.0, 1.0)
# ---------------- agent_06 accel ----------------
def _lagged_diff(x, lag):
out = np.zeros(len(x))
if lag < len(x):
out[lag:] = x[lag:] - x[:-lag]
return out
def accel_signal(df, FAST=28, LAG=30, Z_WIN=200, KV=1.5, KA=1.5, W_VEL=0.4,
W_ACC=0.6, SHORT_W=0.0, TARGET_VOL=0.27, VOL_WIN=25, LEV_CAP=1.5):
c = df["close"].values.astype(float)
lr = np.zeros(len(c)); lr[1:] = np.log(c[1:] / c[:-1])
vel = bl.ema(lr, FAST)
acc = _lagged_diff(vel, LAG)
zv = np.nan_to_num(bl.zscore(vel, Z_WIN), nan=0.0)
za = np.nan_to_num(bl.zscore(acc, Z_WIN), nan=0.0)
raw = W_VEL * np.tanh(KV * zv) + W_ACC * np.tanh(KA * za)
raw = np.clip(raw, -1.0, 1.0)
raw = np.where(raw >= 0.0, raw, raw * SHORT_W)
pos = bl.vol_target(raw, df, target_vol=TARGET_VOL, vol_win_days=VOL_WIN,
leverage_cap=LEV_CAP)
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
# ---------------- agent_23 vol_of_vol ----------------
def _expanding_pctl_rank(x, min_hist):
n = len(x); rank = np.full(n, np.nan); seen = []
for i in range(n):
v = x[i]
if np.isfinite(v):
seen.append(v)
if len(seen) >= min_hist:
rank[i] = float(np.mean(np.asarray(seen) <= v))
return rank
def _tsmom_sign(c, h):
out = np.zeros(len(c))
if h < len(c):
out[h:] = np.sign(c[h:] / c[:-h] - 1.0)
return out
def _vol_of_vol(rv, win):
rv_s = pd.Series(rv)
logrv = np.log(rv_s.where(rv_s > 0))
dlog = logrv.diff()
return dlog.rolling(win, min_periods=max(5, win // 2)).std().values
def vov_signal(df, RV_WIN=30, VOV_WIN=40, PCTL=0.80, HORIZONS=(25, 60, 120),
TARGET_VOL=0.22, VOL_WIN=45, LEV_CAP=1.5, MIN_HIST=60):
c = df["close"].values.astype(float)
bpy = bl.bars_per_day(df) * 365.25
rv = bl.realized_vol(bl.simple_returns(c), RV_WIN, bpy)
vov = _vol_of_vol(rv, VOV_WIN)
rank = _expanding_pctl_rank(vov, MIN_HIST)
stable = np.isfinite(rank) & (rank <= PCTL)
sig = np.zeros(len(c))
for h in HORIZONS:
sig += _tsmom_sign(c, h)
sig /= len(HORIZONS)
raw = np.where(stable, sig, 0.0)
pos = bl.vol_target(raw, df, target_vol=TARGET_VOL, vol_win_days=VOL_WIN,
leverage_cap=LEV_CAP)
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
def score(sig_fn, kwargs, fee=FEE_BASE):
"""Return dict of train & test sharpe/pnl, averaged over A&B (min/mean)."""
out = {}
for s in ("A", "B"):
df, train, test = _masks(s)
tgt = sig_fn(df, **kwargs)
rtr = bl.eval_target(df, tgt, fee_side=fee, metric_mask=train)
rte = bl.eval_target(df, tgt, fee_side=fee, metric_mask=test)
out[s] = dict(tr_sh=rtr["sharpe"], tr_pnl=rtr["pnl"],
te_sh=rte["sharpe"], te_pnl=rte["pnl"], te_dd=rte["maxdd"])
# combined: min across A,B (the agents tuned on sharpe_min)
te_sh_min = min(out["A"]["te_sh"], out["B"]["te_sh"])
tr_sh_min = min(out["A"]["tr_sh"], out["B"]["tr_sh"])
te_sh_mean = 0.5 * (out["A"]["te_sh"] + out["B"]["te_sh"])
te_pnl_mean = 0.5 * (out["A"]["te_pnl"] + out["B"]["te_pnl"])
return dict(out=out, te_sh_min=te_sh_min, tr_sh_min=tr_sh_min,
te_sh_mean=te_sh_mean, te_pnl_mean=te_pnl_mean)
def perturb_grid(sig_fn, base, grid):
"""grid: {param: [values]}. Sweep one param at a time around base."""
base_sc = score(sig_fn, base)
rows = []
for p, vals in grid.items():
for v in vals:
kw = dict(base); kw[p] = v
sc = score(sig_fn, kw)
rows.append(dict(param=p, val=v, te_sh_min=sc["te_sh_min"],
te_sh_mean=round(sc["te_sh_mean"], 3),
te_pnl_mean=round(sc["te_pnl_mean"], 3),
tr_sh_min=sc["tr_sh_min"]))
return base_sc, rows
if __name__ == "__main__":
import json
pd.set_option("display.width", 160)
pd.set_option("display.max_rows", 300)
print("="*70)
print("AGENT 04 — MACD")
print("="*70)
base04 = dict(FAST=26, SLOW=52, SIGNAL=9, SLOPE_W=0.20, SHORT_W=0.5,
TARGET_VOL=0.20, VOL_WIN=30, LEV_CAP=1.0)
b, rows = perturb_grid(macd_signal, base04, dict(
FAST=[20, 22, 26, 30, 32, 39], # +/-25% + bigger
SLOW=[39, 45, 52, 60, 65, 78],
SIGNAL=[5, 7, 9, 11, 13, 18],
SLOPE_W=[0.10, 0.15, 0.20, 0.25, 0.30, 0.40],
SHORT_W=[0.0, 0.25, 0.375, 0.5, 0.625, 0.75, 1.0],
VOL_WIN=[15, 22, 30, 38, 45, 60],
TARGET_VOL=[0.15, 0.20, 0.25, 0.30],
))
print("BASE:", json.dumps({k: b[k] for k in ("tr_sh_min","te_sh_min","te_sh_mean","te_pnl_mean")}))
print(" per-series base:", b["out"])
print(pd.DataFrame(rows).to_string(index=False))
print("\n" + "="*70)
print("AGENT 06 — ACCEL")
print("="*70)
base06 = dict(FAST=28, LAG=30, Z_WIN=200, KV=1.5, KA=1.5, W_VEL=0.4,
W_ACC=0.6, SHORT_W=0.0, TARGET_VOL=0.27, VOL_WIN=25, LEV_CAP=1.5)
b, rows = perturb_grid(accel_signal, base06, dict(
FAST=[21, 24, 28, 32, 35, 42],
LAG=[20, 26, 30, 36, 40, 50],
Z_WIN=[140, 160, 200, 240, 260, 320],
KV=[1.0, 1.2, 1.5, 1.8, 2.0, 3.0],
KA=[1.0, 1.2, 1.5, 1.8, 2.0, 3.0],
W_ACC=[0.3, 0.45, 0.6, 0.75, 0.9, 1.0],
TARGET_VOL=[0.18, 0.22, 0.27, 0.32],
VOL_WIN=[18, 22, 25, 30, 35],
))
print("BASE:", json.dumps({k: b[k] for k in ("tr_sh_min","te_sh_min","te_sh_mean","te_pnl_mean")}))
print(" per-series base:", b["out"])
print(pd.DataFrame(rows).to_string(index=False))
print("\n" + "="*70)
print("AGENT 23 — VOL_OF_VOL")
print("="*70)
base23 = dict(RV_WIN=30, VOV_WIN=40, PCTL=0.80, HORIZONS=(25, 60, 120),
TARGET_VOL=0.22, VOL_WIN=45, LEV_CAP=1.5, MIN_HIST=60)
b, rows = perturb_grid(vov_signal, base23, dict(
RV_WIN=[22, 26, 30, 34, 38, 45],
VOV_WIN=[30, 35, 40, 45, 50, 60],
PCTL=[0.60, 0.70, 0.76, 0.80, 0.84, 0.90, 1.00],
TARGET_VOL=[0.18, 0.22, 0.26, 0.30],
VOL_WIN=[34, 40, 45, 55, 60],
MIN_HIST=[40, 60, 90],
))
print("BASE:", json.dumps({k: b[k] for k in ("tr_sh_min","te_sh_min","te_sh_mean","te_pnl_mean")}))
print(" per-series base:", b["out"])
# horizons sweep separately (tuple param)
hz_rows = []
for hz in [(20,50,100),(25,60,120),(30,70,140),(20,40,80),(40,90,180),(15,30,60)]:
kw = dict(base23); kw["HORIZONS"] = hz
sc = score(vov_signal, kw)
hz_rows.append(dict(param="HORIZONS", val=str(hz), te_sh_min=sc["te_sh_min"],
te_sh_mean=round(sc["te_sh_mean"],3),
te_pnl_mean=round(sc["te_pnl_mean"],3), tr_sh_min=sc["tr_sh_min"]))
print(pd.DataFrame(rows + hz_rows).to_string(index=False))
# ---- FEE BUMP to 0.20% RT, base params ----
print("\n" + "="*70)
print("FEE BUMP 0.10% -> 0.20% RT (base params)")
print("="*70)
for name, fn, base in [("MACD", macd_signal, base04),
("ACCEL", accel_signal, base06),
("VOV", vov_signal, base23)]:
lo = score(fn, base, fee=FEE_BASE)
hi = score(fn, base, fee=FEE_BUMP)
print(f"{name:6s} te_sh_min {lo['te_sh_min']:+.3f} -> {hi['te_sh_min']:+.3f} | "
f"te_sh_mean {lo['te_sh_mean']:+.3f} -> {hi['te_sh_mean']:+.3f} | "
f"te_pnl_mean {lo['te_pnl_mean']:+.3f} -> {hi['te_pnl_mean']:+.3f}")
print(f" per-series @0.20%: A te_sh {score(fn,base,fee=FEE_BUMP)['out']['A']['te_sh']} "
f"B te_sh {score(fn,base,fee=FEE_BUMP)['out']['B']['te_sh']}")
@@ -0,0 +1,31 @@
"""TEMPLATE for a blind-signal agent. COPY this, rename, implement `signal`.
You are given two anonymized, overlaid price curves ("A" and "B"), rebased to 100.
You do NOT know what they are. Find a way to ANTICIPATE the next move.
Rules (enforced automatically — break them and you are disqualified):
* `signal(df)` returns float array len(df). position[i] in [-1,+1] = how much of
equity to hold during the NEXT bar (sign=long/short, 0=flat). The evaluator
shifts it -> you trade bar i+1 with a decision made at close[i].
* CAUSAL/ONLINE only: position[i] uses ONLY rows 0..i. No .shift(-k), no centered
windows, no fitting a model on the whole df then predicting the whole df.
If you train a model, use an EXPANDING/WALK-FORWARD scheme (refit using only
past rows) or fit once on an EARLY fixed warmup and freeze.
* Tune ONLY on split='train'. The held-out tail is scored by the orchestrator.
Score it:
uv run python scripts/research/blind/blind_eval.py --module <this file> --split train
Make sure the output has "causality": {"ok": true, ...}.
"""
import numpy as np
import blindlib as bl
def signal(df):
c = df["close"].values.astype(float)
# --- EXAMPLE: vol-targeted dual-timescale momentum (replace with your idea) ---
fast = c / bl.sma(c, 20) - 1.0
slow = c / bl.sma(c, 100) - 1.0
raw = np.sign(fast) * 0.5 + np.sign(slow) * 0.5 # -1..1 direction
pos = bl.vol_target(raw, df, target_vol=0.20, vol_win_days=30, leverage_cap=1.0)
return np.clip(pos, -1.0, 1.0)
@@ -0,0 +1,44 @@
"""agent_00_sma_trend — ANGLE: trend / single long SMA (long/flat).
Idea (assigned angle): go LONG only while price is meaningfully above a single long
simple moving average, otherwise FLAT. The long SMA defines the macro trend; staying
flat below it is what cuts the asset's ~77% buy&hold drawdown to ~1/3.
Tuned on split='train' only (both Series A and B, equal weight):
* window W = 150 (canonical long SMA; sits on a wide robust plateau W=135..165)
* band B = 0.02 (require close > 1.02*SMA -> avoids whipsaw chop near the line)
* vol-target the long exposure to 35% ann vol (vol_win=30d, cap 1.0). This is what
actually controls drawdown: long size shrinks when realized vol spikes (every
crypto-like crash is a vol spike), so we're never full-size into the worst bars.
Everything is causal: SMA(close[..i]), realized vol(returns[..i]). No future rows.
The evaluator shifts position by one bar (decision at close[i] -> held bar i+1).
Train (combined A&B): pnl_mean ~ 5.4, maxdd_worst ~ 0.30, sharpe_min ~ 1.36.
Honest note: this is a DEFENSIVE trend filter, not alpha — its value is converting a
high-PnL/high-DD uptrend into comparable risk-adjusted PnL at a MUCH smaller drawdown.
"""
import numpy as np
import blindlib as bl
W = 150 # single long SMA window
BAND = 0.02 # long only when close > (1+BAND)*SMA(W)
TARGET_VOL = 0.35
VOL_WIN_DAYS = 30
LEV_CAP = 1.0
def signal(df):
c = df["close"].values.astype(float)
sma = bl.sma(c, W) # causal SMA up to i
# long/flat gate vs the single long SMA, with a band to dodge whipsaw near the line
long_gate = np.where(c > sma * (1.0 + BAND), 1.0, 0.0)
long_gate[:W] = 0.0 # no signal before the SMA is defined
long_gate[~np.isfinite(sma)] = 0.0
# size the long with causal vol-targeting (shrinks into vol spikes -> cuts DD)
pos = bl.vol_target(long_gate, df, target_vol=TARGET_VOL,
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
@@ -0,0 +1,40 @@
"""Agent 01 — Dual EMA crossover (family=trend, slug=ema_cross).
The angle: long/short on the sign of (fast EMA - slow EMA). The two spans are the
core tuned knobs. One refinement that survived a plateau check on split='train':
the two anonymized curves are strongly up-trending, so a SYMMETRIC short is pure
drag (it shorts the dips of a bull market). We keep the long/short crossover but
size the SHORT side down by `SHORT_W` — still a genuine long/short EMA cross, just
risk-asymmetric. Direction is then vol-targeted (causal trailing window) so the two
curves are sized comparably and the drawdown stays bounded.
Tuning (train only): a broad plateau f in [18..30], s in [40..50], SHORT_W in
[0.1..0.3] all give sharpe_min ~1.3 / DD ~0.23. f=25, s=40, SHORT_W=0.25 sits in
the plateau interior (not on a grid edge) -> robust, not a lucky cell.
CAUSAL: ema(c, span) is an online recursion (value at i uses rows 0..i only);
vol_target uses a trailing vol window. No look-ahead, no centered windows, no
global fit. Verified by causality_ok (max_diff 0.0).
"""
import numpy as np
import blindlib as bl
# --- tuned ONLY on split='train' (plateau interior) ---
FAST_SPAN = 25
SLOW_SPAN = 40
SHORT_W = 0.25 # short side sized down (asymmetric L/S); 0 -> long-flat
TARGET_VOL = 0.20
VOL_WIN = 30
LEV_CAP = 1.0
def signal(df):
c = df["close"].values.astype(float)
fast = bl.ema(c, FAST_SPAN)
slow = bl.ema(c, SLOW_SPAN)
# +1 when fast above slow, -SHORT_W when below: genuine EMA-cross direction,
# short side de-weighted because the curves are persistently up-trending.
raw = np.where(fast >= slow, 1.0, -SHORT_W)
pos = bl.vol_target(raw, df, target_vol=TARGET_VOL,
vol_win_days=VOL_WIN, leverage_cap=LEV_CAP)
return np.clip(pos, -1.0, 1.0)
@@ -0,0 +1,72 @@
"""Agent 02 — TSMOM multi-horizon (family=trend, slug=tsmom_multi).
The angle (assigned): time-series momentum over several lookback horizons. For each
horizon H in {~30, ~90, ~180} bars take the SIGN of the past-H-bar return (is the
asset up or down vs H bars ago?), average the three signs into a -1..+1 direction,
then size it with a causal vol-target so the two curves are risk-comparable and the
drawdown stays bounded.
Why multi-horizon: a single lookback is regime-fragile (whipsaws when its window
straddles a chop). Averaging 1/3/6-month TSMOM signs is the classic TP01 trick —
the slow horizon carries the macro trend, the fast ones cut exposure early into a
turn. On these two persistently up-trending curves the net effect is to stay long
through the bull and de-risk (toward flat / light short) into the big declines,
turning a ~77-79% buy&hold drawdown into a much smaller one at comparable PnL.
Long-short vs long-flat: a symmetric short bleeds in a structural bull (it shorts
the dips). Tuned on split='train', a lightly de-weighted short (SHORT_W<1) beats both
pure long-flat (misses the protection of going short the worst legs) and a symmetric
long-short (too much drag). SHORT_W=0.25 sits in the interior of a flat plateau.
CAUSAL: each horizon return uses close[i]/close[i-H] (rows <= i only); vol_target
uses a trailing realized-vol window. No look-ahead, no centered windows, no global
fit. Verified by causality_ok (max_diff 0.0).
Tuning (train only, combined A&B). A coarse->fine sweep found a WIDE plateau around
slow horizons ~ (1.5, 4.5, 8 months): the whole block H1 in [40..55], H2 in [120..130],
H3 = 240 gives sharpe_min 1.25..1.41 at DD 0.16..0.21. The chosen cell is interior on
every axis (all 8 H-neighbors, sw, vw within the plateau) -> robust, not a lucky spike:
horizons = (45, 130, 240) # ~1.5 / 4.5 / 8 months of daily bars
SHORT_W = 0.25 # asymmetric L/S; plateau sw in [0.0..0.5]
TARGET_VOL=0.30, VOL_WIN=45d, LEV_CAP=1.5
-> train combined: pnl_mean ~3.2, maxdd_worst ~0.21, sharpe_min ~1.37.
A single fast lookback (e.g. 30) is regime-fragile here; the slow multi-horizon blend
is what both lifts the Sharpe and roughly halves the buy&hold (~77-79%) drawdown.
"""
import numpy as np
import blindlib as bl
HORIZONS = (45, 130, 240) # ~1.5/4.5/8 months of daily bars (multi-horizon TSMOM)
SHORT_W = 0.25 # de-weight the short side (curves trend up); 0 -> long-flat
TARGET_VOL = 0.30
VOL_WIN_DAYS = 45
LEV_CAP = 1.5
def _tsmom_sign(c: np.ndarray, h: int) -> np.ndarray:
"""Sign of the past-h-bar return, causal. mom[i] = sign(c[i]/c[i-h] - 1).
Undefined (0) for i < h."""
out = np.zeros(len(c))
if h < len(c):
past = c[:-h]
cur = c[h:]
out[h:] = np.sign(cur / past - 1.0)
return out
def signal(df):
c = df["close"].values.astype(float)
# average the SIGN of TSMOM over the three horizons -> direction in [-1, +1]
sig = np.zeros(len(c))
for h in HORIZONS:
sig += _tsmom_sign(c, h)
sig /= len(HORIZONS)
# asymmetric long-short: keep the long full size, de-weight the short side
raw = np.where(sig >= 0.0, sig, sig * SHORT_W)
# causal vol-targeting: shrinks size into vol spikes (every crash is a vol spike)
pos = bl.vol_target(raw, df, target_vol=TARGET_VOL,
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
@@ -0,0 +1,68 @@
"""Agent 03 — MA ribbon (family=trend, slug=ma_ribbon).
The angle: a quad-EMA "ribbon" (fast -> slow). The position is the FRACTION of the
ribbon that is in the correct trend order. When the ribbon is perfectly stacked
bullish (each faster EMA above the next slower one) the trend is clean and aligned
-> position +1. Perfectly stacked bearish -> -1. A tangled ribbon (MAs crossing,
no clear order) -> small / flat: we only press the position when the whole trend
structure agrees. This is a GRADED-conviction trend filter, not a binary cross.
Construction (all causal — value at i uses rows 0..i only):
* ribbon = 4 EMAs with spans SPANS (monotone fast->slow), the canonical "quad".
* For each adjacent pair (k, k+1) score +1 if ema_k > ema_{k+1} (bullish step),
-1 if below. ribbon score = mean of the K-1 step signs -> in [-1, +1]:
exactly "fraction of MAs in correct order" mapped to a signed conviction
(all-bullish -> +1, all-bearish -> -1, tangled half/half -> ~0).
* The two anonymized curves are persistently up-trending, so a symmetric short of
every partial-ribbon dip is pure drag. We de-weight the short side by SHORT_W
(still a genuine ribbon long/short, just risk-asymmetric). SHORT_W>0 helps a
little: a small short into a stacked-bearish ribbon trims the drawdown.
* Size with causal vol-targeting so Series A & B are risk-comparable and the
drawdown stays bounded (long size shrinks into vol spikes = every crash).
Tuning (ONLY split='train', both A & B equal weight). The chosen cell sits in the
interior of a broad plateau, not on a grid edge:
* SPANS base in {5,6,7} x(2 ratio) -> sharpe_min 1.32-1.37 (6 is the interior).
* VOL_WIN 20-25 best; 25 interior. * SHORT_W 0.1-0.25 flat at sharpe_min ~1.37,
DD falling 0.26->0.24 as SHORT_W rises; 0.2 interior.
Train combined: pnl_mean ~3.20, maxdd_worst ~0.241, sharpe_min ~1.37, turnover ~11/yr.
Fee-robust: sharpe_min 1.39 at 0% RT -> 1.30 at 0.40% RT (low turnover = fee-insensitive).
CAUSAL: ema is an online recursion, vol_target uses a trailing window -> no
look-ahead, no centered windows, no global fit. Verified by causality_ok (max_diff 0).
Honest note: this is a DEFENSIVE trend filter (value = converting a high-PnL/~50-67%-DD
uptrend into comparable PnL at ~24% DD), not standalone alpha — like every long-biased
trend overlay it inherits the bull-market beta of the curves.
"""
import numpy as np
import blindlib as bl
# --- tuned ONLY on split='train' (plateau interior, not a grid edge) ---
SPANS = (6, 12, 24, 48) # quad ribbon, fast -> slow (monotone)
SHORT_W = 0.2 # short side de-weighted (asymmetric L/S); 0 -> long/flat
TARGET_VOL = 0.25
VOL_WIN_DAYS = 25
LEV_CAP = 1.0
def _ribbon_score(c: np.ndarray) -> np.ndarray:
"""Signed fraction of adjacent ribbon steps in bullish order, in [-1, +1]."""
emas = [bl.ema(c, s) for s in SPANS]
steps = []
for k in range(len(emas) - 1):
# +1 where the faster EMA is above the next slower one (bullish step)
steps.append(np.where(emas[k] > emas[k + 1], 1.0, -1.0))
score = np.mean(np.vstack(steps), axis=0) # mean of K-1 step signs in [-1,1]
score[: SPANS[-1]] = 0.0 # ribbon undefined before slowest span
return score
def signal(df):
c = df["close"].values.astype(float)
score = _ribbon_score(c)
# graded conviction: keep the full long fraction, de-weight the short fraction
raw = np.where(score >= 0.0, score, SHORT_W * score)
pos = bl.vol_target(raw, df, target_vol=TARGET_VOL,
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
@@ -0,0 +1,75 @@
"""Agent 04 — MACD (family=trend, slug=macd).
The angle: MACD = EMA(fast) - EMA(slow); signal line = EMA(MACD, signal_span);
histogram = MACD - signal. Direction comes from the histogram SIGN reinforced by
its SLOPE, exactly as the angle prescribes. Concretely:
* BASE direction = +1/-1 only when the histogram sign AGREES with the MACD-line
sign (MACD above its signal line AND above zero -> uptrend), else flat. Requiring
agreement kills the histogram-sign whipsaw that bleeds the naive 12/26/9 to fees
(turnover ~24/yr -> ~15/yr) and roughly halves the drawdown.
* SLOPE confirmation = sign of the histogram's backward diff (histogram rising =
momentum accelerating). Blended in at weight SLOPE_W; it trims the drawdown
further (~0.18 -> ~0.12) by stepping aside while momentum is decelerating.
Refinements that survived a plateau check on split='train':
* Both anonymized curves are persistently up-trending, so a symmetric short bleeds
(it shorts the dips of a bull). We keep a genuine long/short MACD but size the
SHORT side down (SHORT_W=0.5).
* Direction is vol-targeted (causal trailing window) so the two curves are sized
comparably and the drawdown stays bounded.
Tuning (train only) — broad plateau, chosen cell is the interior, not a grid edge:
fast in [24..28], slow in [50..56], signal=9, SHORT_W in [0.5..0.6],
SLOPE_W in [0.2..0.35], VOL_WIN in [20..60] all give sharpe_min ~1.35-1.45 at
DD ~0.10-0.13. Picked fast=26, slow=52, signal=9, SHORT_W=0.5, SLOPE_W=0.20.
Fee-robust: sharpe_min only 1.40 -> 1.29 as round-trip fee goes 0.10% -> 0.30%.
Benchmark: long-only buy&hold on train is pnl ~6.7/23.0 but maxDD ~0.77/0.79
(sharpe ~0.89/1.16). This MACD anticipates the trend at a MUCH smaller drawdown
(~0.12) with a higher risk-adjusted return (sharpe_min ~1.40).
CAUSAL: ema(c, span) is an online recursion (value at i uses rows 0..i only); the
histogram slope is a backward diff; vol_target uses a trailing vol window. No
look-ahead, no centered windows, no global fit. Verified by causality_ok (max_diff 0).
"""
import numpy as np
import blindlib as bl
# --- tuned ONLY on split='train' (plateau interior) ---
FAST_SPAN = 26
SLOW_SPAN = 52
SIGNAL_SPAN = 9
SLOPE_W = 0.20 # weight of histogram-slope confirmation in the direction
SHORT_W = 0.5 # short side sized down (asymmetric L/S in a bull); 0 -> long-flat
TARGET_VOL = 0.20
VOL_WIN = 30
LEV_CAP = 1.0
def _macd(c, fast, slow, sig):
macd = bl.ema(c, fast) - bl.ema(c, slow)
signal_line = bl.ema(macd, sig)
hist = macd - signal_line
return macd, signal_line, hist
def signal(df):
c = df["close"].values.astype(float)
macd, signal_line, hist = _macd(c, FAST_SPAN, SLOW_SPAN, SIGNAL_SPAN)
# base direction: take a side only when the histogram sign and the MACD-line
# sign AGREE (MACD vs signal AND MACD vs zero point the same way), else flat.
base = np.where(np.sign(hist) == np.sign(macd), np.sign(macd), 0.0)
# slope confirmation: is the histogram rising or falling (causal backward diff)?
slope = np.sign(np.diff(hist, prepend=hist[0]))
raw = (1.0 - SLOPE_W) * base + SLOPE_W * slope
raw = np.clip(raw, -1.0, 1.0)
# de-weight the short side (persistent up-trend -> symmetric short is drag)
raw = np.where(raw < 0, raw * SHORT_W, raw)
raw = np.nan_to_num(raw, nan=0.0)
pos = bl.vol_target(raw, df, target_vol=TARGET_VOL,
vol_win_days=VOL_WIN, leverage_cap=LEV_CAP)
return np.clip(pos, -1.0, 1.0)
@@ -0,0 +1,79 @@
"""Agent 05 — Momentum z-score (family=trend, slug=momz).
The angle (assigned): take the N-bar return as a momentum signal, STANDARDIZE it with a
CAUSAL rolling z-score, then squash with tanh into a position in [-1,+1]. Tune N.
Why z-score the momentum (not the raw return): the magnitude of an N-bar return drifts
with the volatility regime — a +5% N-bar move means "strong" in a calm market and mere
"noise" in a wild one. Dividing by the trailing std of that same N-bar momentum makes the
signal regime-stationary: the position grows when momentum is unusually strong vs its own
recent distribution and shrinks toward 0 when it is merely typical. tanh(K*z) gives a
smooth, saturating long/short sizing (no hard sign flips -> less turnover/fee churn than a
sign rule) that is already bounded in [-1,1].
Single N is regime-fragile here (a lone lookback's sharpe_min ricochets 0.4..1.1 across N
on the two train curves). The cure, staying true to the z-score angle, is to BLEND THE
Z-SCORES of a few momentum horizons (fast/mid/slow N) — the distinguishing feature is the
standardization; multi-horizon is just averaging the standardized momentum, the same trick
that stabilizes TSMOM. The blended z is the direction; a causal vol-target then sizes it so
the two curves are risk-comparable and the drawdown stays bounded (every crash is a vol
spike -> exposure shrinks into it).
Long-flat, not long-short: the two curves trend up structurally and a tuning sweep on
split='train' is monotone — every bit of short weight ONLY adds drag and drawdown here
(SHORT_W 0->1 takes sharpe_min from ~1.4 down to ~0.85 and DD 0.17->0.33). So SHORT_W=0:
go long when blended momentum-z is positive, flat otherwise. (The short side is kept as a
parameter, not hard-removed, so the rule is explicit and re-tunable on a different regime.)
CAUSAL: mom[i] = close[i]/close[i-N]-1 uses rows <= i; zscore uses a trailing window;
vol_target uses trailing realized vol. No shift(-k), no centered windows, no global fit.
Verified by causality_ok (max_diff 0.0).
Tuning (train only, combined A&B; coarse->fine sweep). The chosen cell is INTERIOR on every
axis — all horizon-set neighbors, ZW in [200..280], VW in [30..40], K in [2.5..4] stay in
sharpe_min ~1.2..1.45 at DD ~0.16..0.24, so it's a plateau, not a lucky spike:
HORIZONS=(40,120,220) # ~fast/mid/slow N-bar momentum
Z_WIN=250 # window standardizing each N-bar momentum
K=3.0 # tanh gain (near-saturating; >=2.5 is flat)
SHORT_W=0.0 # long-flat (short only added drag here)
TARGET_VOL=0.25, VOL_WIN_DAYS=35, LEV_CAP=1.5
-> train combined: pnl_mean ~2.77, maxdd_worst ~0.17, sharpe_min ~1.39
(vs long-only buy&hold's ~7-23x PnL at ~70-80% DD — the z-momentum keeps a healthy
PnL while cutting the drawdown ~4-5x by de-risking into the big declines).
"""
import numpy as np
import blindlib as bl
HORIZONS = (40, 120, 220) # N-bar momentum lookbacks (fast/mid/slow) — the "N" of the angle
Z_WIN = 250 # causal window standardizing each N-bar momentum
K = 3.0 # tanh gain on the blended z-score (near-saturating)
SHORT_W = 0.0 # de-weight the short side; 0 -> long-flat (best on train)
TARGET_VOL = 0.25
VOL_WIN_DAYS = 35
LEV_CAP = 1.5
def _mom(c: np.ndarray, n: int) -> np.ndarray:
"""Causal N-bar return. mom[i] = c[i]/c[i-n] - 1, undefined (0) for i < n."""
out = np.zeros(len(c))
if n < len(c):
out[n:] = c[n:] / c[:-n] - 1.0
return out
def signal(df):
c = df["close"].values.astype(float)
# blend the z-scores of several momentum horizons -> regime-stationary direction
zsum = np.zeros(len(c))
for n in HORIZONS:
z = bl.zscore(_mom(c, n), Z_WIN) # standardize vs own trailing distribution
zsum += np.nan_to_num(z, nan=0.0)
z = zsum / len(HORIZONS)
raw = np.tanh(K * z) # smooth, saturating direction in [-1, 1]
raw = np.where(raw >= 0.0, raw, raw * SHORT_W) # de-weight short side (0 = long-flat)
pos = bl.vol_target(raw, df, target_vol=TARGET_VOL,
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
@@ -0,0 +1,97 @@
"""Agent 06 — Acceleration / momentum-of-momentum (family=trend, slug=accel).
The angle (assigned): 2nd difference / momentum-of-momentum. Go WITH an accelerating
trend, cut (de-risk toward flat) when the trend is decelerating.
Construction (all causal):
1. velocity v[i] = EMA(log-return, FAST) — a smoothed 1st derivative of log-price
(the local trend "speed", sign = up/down).
2. acceleration a[i] = v[i] - v[i-LAG] — the momentum-OF-momentum (discrete 2nd
difference of log-price). a>0 = the up-move is speeding up / a down-move is
bottoming; a<0 = the up-move is rolling over / a down-move is accelerating.
3. Standardize BOTH v and a with a causal rolling z-score so they are regime-
stationary (a "fast" velocity in a calm tape is "slow" in a wild one).
4. Direction = the trend you ride GATED by acceleration:
dir = sign-ish(velocity) * gate(acceleration)
where the gate OPENS exposure when momentum is accelerating in the trend's
direction and CLOSES it (toward 0) when it decelerates. Concretely we combine
a velocity term (ride the trend) with an acceleration term (the angle's edge):
raw = tanh(KV * zv) * 0.5 + tanh(KA * za) * 0.5
then de-weight the short side (these curves trend up structurally so a full
symmetric short bleeds shorting the dips) and vol-target so A and B are
risk-comparable and every crash (a vol spike) shrinks size into itself.
Why acceleration adds over plain momentum: plain TSMOM is fully long through a long
top-formation and gives the gains back on the way down. The 2nd difference turns
NEGATIVE while price is still high but rolling over (momentum decelerating) — it cuts
risk EARLY, before the level-based trend flips. Symmetrically it re-engages when a
decline starts decelerating (bottoming). That earlier turn is the whole point of the
angle: comparable PnL to buy&hold at a much smaller drawdown.
CAUSAL: EMA, rolling z-score, the v[i]-v[i-LAG] difference and vol_target all use rows
<= i only. No shift(-k), no centered windows, no global fit. Verified by causality_ok.
Tuning (train only, combined A&B): a coarse->fine sweep over (FAST, LAG, weights, KV/KA,
short_w, Z_WIN, vol-target) picked a WIDE interior plateau, not a spike. The chosen cell
(FAST=28, LAG=30, Z_WIN=200, KV=KA=1.5, W_VEL=0.4/W_ACC=0.6, SHORT_W=0, vol25) is interior
on EVERY axis: FAST in [22..36] -> sh_min 1.50..1.52; LAG in [26..40] -> 1.41..1.52
(peak 30); Z_WIN in [160..220] -> 1.52..1.56; W_ACC/KA/KV/vol all smooth & monotone.
-> train combined: pnl_mean ~2.3, maxdd_worst ~0.20, sharpe_min ~1.52.
SHORT_W=0 (long-flat) beat every short weight on train (sh_min collapses 1.31->0.43 as the
short side is turned on) — the deceleration gate ALREADY de-risks to flat at the top, so a
symmetric short just shorts the dips of a structural bull. The acceleration term is what
earns the carry over plain velocity: W_ACC=0 drops pnl_mean to ~0.6 (it ducks risk too
early); W_ACC~0.6 keeps the early de-risk while staying invested through the accelerating
legs. DD ~0.20 vs a ~77-79% buy&hold drawdown.
"""
import numpy as np
import blindlib as bl
FAST = 28 # EMA span for the velocity (smoothed log-return / local slope)
LAG = 30 # horizon of the 2nd difference: accel = v[i] - v[i-LAG]
Z_WIN = 200 # causal window to standardize velocity & acceleration
KV = 1.5 # tanh gain on the velocity z (ride the trend)
KA = 1.5 # tanh gain on the acceleration z (the angle's edge)
W_VEL = 0.4 # weight on the velocity (trend) term
W_ACC = 0.6 # weight on the acceleration (momentum-of-momentum) term
SHORT_W = 0.0 # long-flat: the de-celeration gate already cuts to flat; a
# symmetric short only bleeds shorting the dips of a structural
# up-trend (train sweep: sh_min 1.31@0.0 -> 0.43@1.0). 0 = flat.
TARGET_VOL = 0.27
VOL_WIN_DAYS = 25
LEV_CAP = 1.5
def _lagged_diff(x: np.ndarray, lag: int) -> np.ndarray:
"""Causal discrete derivative: out[i] = x[i] - x[i-lag], 0 for i < lag."""
out = np.zeros(len(x))
if lag < len(x):
out[lag:] = x[lag:] - x[:-lag]
return out
def signal(df):
c = df["close"].values.astype(float)
lr = np.zeros(len(c))
lr[1:] = np.log(c[1:] / c[:-1]) # causal log returns
# 1) velocity: smoothed 1st derivative of log-price (local trend speed)
vel = bl.ema(lr, FAST)
# 2) acceleration: momentum-of-momentum = 2nd difference of the trend
acc = _lagged_diff(vel, LAG)
# 3) standardize both vs their own trailing distribution (regime-stationary)
zv = np.nan_to_num(bl.zscore(vel, Z_WIN), nan=0.0)
za = np.nan_to_num(bl.zscore(acc, Z_WIN), nan=0.0)
# 4) ride the trend, GATED/boosted by acceleration (the angle's edge)
raw = W_VEL * np.tanh(KV * zv) + W_ACC * np.tanh(KA * za)
raw = np.clip(raw, -1.0, 1.0)
# asymmetric long-short: full long, de-weighted short (structural up-trend)
raw = np.where(raw >= 0.0, raw, raw * SHORT_W)
# causal vol-targeting: shrink size into vol spikes (every crash is a vol spike)
pos = bl.vol_target(raw, df, target_vol=TARGET_VOL,
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
@@ -0,0 +1,115 @@
"""Agent 07 — KAMA / Kaufman efficiency ratio (family=trend, slug=kama_eff).
The angle (assigned): an ADAPTIVE moving average driven by Kaufman's Efficiency
Ratio (ER). ER over a window of n bars is
ER[i] = |close[i] - close[i-n]| / sum_{k=i-n+1..i} |close[k] - close[k-1]|
i.e. net displacement / total path length, in [0, 1]. ER -> 1 when the move is a
clean straight trend (worth following); ER -> 0 in chop (the path wanders, net
displacement is small -> stay out). KAMA turns ER into an adaptive smoothing
constant SC = (ER*(fast-slow)+slow)^2 so the average snaps to price in a trend and
freezes in chop:
KAMA[i] = KAMA[i-1] + SC[i] * (close[i] - KAMA[i-1])
DIRECTION: sign of the KAMA slope (KAMA[i] vs KAMA[i-k]) — KAMA is up-sloping in an
up-trend, flat/down in a decline. GATE: the efficiency ratio itself. We only take a
position when ER exceeds a causal, expanding-quantile threshold (trend is efficient
ENOUGH right now relative to this curve's own history); otherwise flat. This is the
literal statement of the angle: "trend-follow when efficiency high, flat when choppy".
LONG-SHORT: the curves trend up structurally, so a full symmetric short bleeds
(it shorts the dips). We keep the long full size and de-weight the short side
(SHORT_W < 1) — the short is there to protect the big efficient DECLINES (which is
where flat-only leaves the worst drawdown on the table), not to fade every wiggle.
SIZING: causal vol-target so A and B are risk-comparable and the drawdown stays
bounded (every crash is a vol spike -> exposure auto-shrinks).
CAUSAL: ER, KAMA (a recursive EWMA-like filter), the slope, the expanding ER
threshold, and vol_target all use rows <= i only. No shift(-k), no centered window,
no global fit. Verified by causality_ok (max_diff ~0).
Tuning (train only, combined A&B, coarse->fine). ER window ~ a month, KAMA fast/slow
the canonical (2,30), slope over a few bars, ER gate at an expanding quantile. A WIDE
interior plateau (every 1-axis neighbor holds sharpe_min 1.25-1.54 at dd 0.18-0.33,
no spike) sits around:
ER_WIN=30, FAST=2, SLOW=30, SLOPE=5, ER_Q=0.30 (expanding causal quantile),
SHORT_W=0.20, TARGET_VOL=0.30, VOL_WIN=35d, LEV_CAP=1.5
-> train combined: pnl_mean ~4.75, maxdd_worst ~0.19, sharpe_min ~1.43 (causality.ok).
Notes: LEV_CAP is non-binding here (vol_target keeps |pos|<1 on these vol levels);
the ER gate is what de-risks chop, the de-weighted short protects the efficient
declines, and vol_target turns the ~77-79% buy&hold drawdown into ~19%.
"""
import numpy as np
import pandas as pd
import blindlib as bl
ER_WIN = 30 # efficiency-ratio lookback (~1 month of daily bars)
FAST = 2 # KAMA fast EMA constant
SLOW = 30 # KAMA slow EMA constant
SLOPE = 5 # bars to measure KAMA slope (direction)
ER_Q = 0.30 # expanding-quantile gate: trade only when ER above its own history
WARMUP = 60 # min bars before the expanding gate is trusted
SHORT_W = 0.20 # de-weight the short side (curves trend up); 0 -> long-flat
TARGET_VOL = 0.30
VOL_WIN_DAYS = 35
LEV_CAP = 1.5
def _efficiency_ratio(c: np.ndarray, n: int) -> np.ndarray:
"""Kaufman efficiency ratio over n bars, causal. ER[i] uses close[i-n..i]."""
change = np.zeros(len(c))
change[n:] = np.abs(c[n:] - c[:-n])
d = np.abs(np.diff(c, prepend=c[0])) # |close[k]-close[k-1]|
volatility = pd.Series(d).rolling(n, min_periods=n).sum().values
er = np.where(volatility > 0, change / volatility, 0.0)
er[:n] = 0.0
return np.nan_to_num(er, nan=0.0)
def _kama(c: np.ndarray, er: np.ndarray, fast: int, slow: int) -> np.ndarray:
"""Kaufman Adaptive Moving Average. SC = (ER*(fast_sc-slow_sc)+slow_sc)^2.
Recursive (only uses past) -> fully causal."""
fast_sc = 2.0 / (fast + 1.0)
slow_sc = 2.0 / (slow + 1.0)
sc = (er * (fast_sc - slow_sc) + slow_sc) ** 2
kama = np.empty(len(c))
kama[0] = c[0]
for i in range(1, len(c)):
kama[i] = kama[i - 1] + sc[i] * (c[i] - kama[i - 1])
return kama
def _expanding_quantile(x: np.ndarray, q: float, warmup: int) -> np.ndarray:
"""Causal expanding quantile: thr[i] = q-quantile of x[0..i]. For i<warmup the
gate is impassable (we don't trust an early sample) so we stay flat early."""
s = pd.Series(x)
thr = s.expanding(min_periods=warmup).quantile(q).values
return thr
def signal(df):
c = df["close"].values.astype(float)
n = len(c)
er = _efficiency_ratio(c, ER_WIN)
kama = _kama(c, er, FAST, SLOW)
# DIRECTION: sign of the KAMA slope over SLOPE bars
slope = np.zeros(n)
slope[SLOPE:] = kama[SLOPE:] - kama[:-SLOPE]
direction = np.sign(slope)
# GATE: only trade when efficiency is high relative to this curve's own past
thr = _expanding_quantile(er, ER_Q, WARMUP)
active = np.where(np.isfinite(thr) & (er >= thr), 1.0, 0.0)
raw = direction * active
# asymmetric long-short: keep long full size, de-weight the short side
raw = np.where(raw >= 0.0, raw, raw * SHORT_W)
pos = bl.vol_target(raw, df, target_vol=TARGET_VOL,
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
@@ -0,0 +1,95 @@
"""Agent 08 — Sign-vote momentum ensemble (family=trend, slug=signvote).
The angle (assigned): a SIGN-VOTE ENSEMBLE of momentum across MANY lookbacks. For a
dense ladder of horizons H in {10, 20, ..., 250} bars, each horizon casts a binary
vote: +1 if the asset is up vs H bars ago (close[i] > close[i-H]), -1 if down. The
raw direction is the MEAN of all the votes, a smooth number in [-1, +1]:
+1.0 = every horizon agrees the trend is up (full long)
0.0 = the ladder is split (no agreement) (flat)
-1.0 = every horizon agrees the trend is down (full short)
Why a dense vote-ladder beats a single (or 3-horizon) momentum:
* Robustness. No single lookback is special; the verdict is a consensus, so a chop
that whipsaws one window is outvoted by the others. The committee de-risks
GRADUALLY as horizons flip one by one — it doesn't lurch from full-long to
full-short on one window crossing a threshold.
* Anticipation. Near a top the FAST horizons flip down first while the slow ones
are still up, so the mean vote slides from +1 toward 0 BEFORE the slow trend
rolls over — exposure is cut into the turn, not after it. That is the whole point
of the assignment: "anticipate the next move".
Long-short asymmetry: both curves trend up over the visible window, so a full-size
symmetric short bleeds (it shorts every dip). A de-weighted short side (SHORT_W < 1)
keeps the protection of going short the genuine, broad-consensus declines without the
drag of fighting every pullback. SHORT_W=0.35 sits in the interior of a flat plateau.
Sizing: the consensus direction is fed to a causal vol-target so the two curves are
risk-comparable and exposure shrinks into vol spikes (every crash is a vol spike) —
this is what turns the ~77-79% buy&hold drawdown into a far smaller one at comparable
PnL.
CAUSAL: every vote uses close[i]/close[i-H] (rows <= i only); the vol-target uses a
trailing realized-vol window. No .shift(-k), no centered windows, no global fit.
Verified by causality_ok (max_diff 0.0).
Tuning (split='train' only, combined A&B). A coarse->fine sweep over the ladder span,
the step, SHORT_W, and the vol-target block found a WIDE plateau:
* Ladder = 10..250 step 10 (25 horizons). Denser steps or a different top move
sharpe_min by <0.05 -> the result is the consensus, not one cell.
* SHORT_W plateau 0.10..0.30; TARGET_VOL trades PnL<->DD monotonically (0.22->DD .16,
0.28->DD .21) at ~constant Sharpe; VOL_WIN=60 is the interior best (50/75 ~-0.05 Sh);
LEV_CAP doesn't bind (vol-target rarely reaches the cap at these target vols).
Chosen cell (interior on every axis -> robust, not a lucky spike):
SHORT_W=0.15, TARGET_VOL=0.25, VOL_WIN=60, LEV_CAP=1.5
-> train combined: pnl_mean ~1.68, maxdd_worst ~0.187, sharpe_min ~1.17.
TARGET_VOL=0.25 is the balanced pick: vs the 0.30 cell it keeps the Sharpe (~1.18) and
most of the PnL while cutting the worst drawdown 0.24->0.19 — the assignment's goal
("comparable PnL at a MUCH smaller drawdown"). A single fast lookback is regime-fragile
here; the dense sign-vote consensus both lifts the risk-adjusted return and roughly
thirds the ~77-79% buy&hold drawdown.
"""
import numpy as np
import blindlib as bl
# Dense ladder of momentum lookbacks (daily bars): 10, 20, ..., 250 -> 25 horizons.
LOOKBACKS = tuple(range(10, 251, 10))
SHORT_W = 0.15 # de-weight the short side (curves trend up); 0 -> long-flat
TARGET_VOL = 0.25
VOL_WIN_DAYS = 60
LEV_CAP = 1.5
def _vote(c: np.ndarray, h: int) -> np.ndarray:
"""Binary momentum vote of horizon h, causal. +1 if up vs h bars ago, -1 if down.
Undefined (0) for i < h (not enough history to vote)."""
out = np.zeros(len(c))
if h < len(c):
out[h:] = np.sign(c[h:] / c[:-h] - 1.0)
return out
def signal(df):
c = df["close"].values.astype(float)
n = len(c)
# MEAN of the sign-votes across the whole ladder -> consensus direction in [-1,1].
# Each horizon that has enough history contributes its +/-1 vote; we average only
# over the horizons that are actually defined at bar i, so early bars (where the
# long horizons can't vote yet) still produce a sensible consensus of the short
# horizons rather than being diluted toward 0 by undefined long votes.
vote_sum = np.zeros(n)
vote_cnt = np.zeros(n)
for h in LOOKBACKS:
if h >= n:
continue
vote_sum[h:] += np.sign(c[h:] / c[:-h] - 1.0)
vote_cnt[h:] += 1.0
sig = np.where(vote_cnt > 0, vote_sum / np.maximum(vote_cnt, 1.0), 0.0)
# asymmetric long-short: keep the long full size, de-weight the short side
raw = np.where(sig >= 0.0, sig, sig * SHORT_W)
# causal vol-targeting: shrinks size into vol spikes (every crash is a vol spike)
pos = bl.vol_target(raw, df, target_vol=TARGET_VOL,
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
@@ -0,0 +1,65 @@
"""agent_09_donchian — ANGLE: Donchian channel breakout (long / flat).
Idea (assigned angle): a classic Donchian / turtle breakout trend-follower. ENTER LONG
when the close prints above the prior N-bar HIGH (an upside breakout) and EXIT (go FLAT)
when it prints below the prior X-bar LOW (a downside breakout). Hold the long between
those two events. Tune N (entry) and X (exit) on split='train' only.
WHY LONG/FLAT, NOT LONG/SHORT (honest tuning result):
The textbook donchian is stop-and-reverse (short below the prior low). I tested it.
On BOTH series the SHORT leg is purely value-destroying: every short_size > 0 raised
the drawdown AND lowered Sharpe (the pair trends up, so downside breakouts are mostly
V-shaped bottoms / chop where the short gets whipsawed). So the breakout *exit* is
kept (a low-channel break flattens us, turtle-style), but we never flip short. The
donchian breakout EVENT is still what drives every entry and exit — the angle is intact.
Tuned on split='train' (both Series A and B, equal weight) — broad plateau Nin 25..36 /
Xout 18..20, Sharpe_min ~1.20-1.27 throughout (not an isolated peak):
* N_ENTRY = 36 bars (prior-N high that defines an upside breakout)
* N_EXIT = 18 bars (shorter prior-low channel -> exit faster than we enter)
* vol-target the long to 30% ann vol (vol_win=30d, cap 1.0): long size shrinks into
vol spikes (every crash is a vol spike) -> caps the drawdown of late/whipsaw entries.
Causality: bl.donchian shifts the rolling max/min by one bar, so the channel at i is
built from bars STRICTLY before i; a close[i] that breaks it is a real, tradeable event
at close[i]. The evaluator then holds the position during bar i+1. No future rows; the
state machine is a forward scan (uses only data <= i). causality_ok -> true.
Train (combined A&B): pnl_mean ~3.43, maxdd_worst ~0.31, sharpe_min ~1.27.
Honest note: Donchian is pure trend-following, not alpha. Its value here is converting a
high-PnL / ~74%-DD uptrend into comparable PnL at ~31% drawdown (DD cut ~2.4x). The full
long/short donchian was MUCH worse (Sharpe_min ~0.2, DD ~74%); the edge is the FLAT side.
"""
import numpy as np
import blindlib as bl
N_ENTRY = 36 # Donchian entry: long on break of prior N_ENTRY-bar high
N_EXIT = 18 # Donchian exit: flat on break of prior N_EXIT-bar low
TARGET_VOL = 0.30
VOL_WIN_DAYS = 30
LEV_CAP = 1.0
def signal(df):
c = df["close"].values.astype(float)
hi_entry, _ = bl.donchian(df, N_ENTRY) # prior N_ENTRY-bar high (shifted, causal)
_, lo_exit = bl.donchian(df, N_EXIT) # prior N_EXIT-bar low (shifted, causal)
up = c > hi_entry # upside breakout -> enter/stay long
dn = c < lo_exit # downside breakout -> exit to flat
# turtle long/flat state machine (forward scan, uses only data <= i)
n = len(c)
state = np.zeros(n)
s = 0.0
for i in range(n):
if up[i]:
s = 1.0
elif dn[i]:
s = 0.0
state[i] = s
# size the long with causal vol-targeting (shrinks into vol spikes -> caps DD)
pos = bl.vol_target(state, df, target_vol=TARGET_VOL,
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
@@ -0,0 +1,87 @@
"""agent_10_keltner — ANGLE: Keltner channel breakout (long / flat).
Idea (assigned angle): a Keltner channel is an EMA mid-line wrapped by an ATR band,
upper[i] = EMA_N(close)[i-1] + K * ATR_M[i-1]
lower[i] = EMA_N(close)[i-1] - K_EXIT * ATR_M[i-1]
Ride breakouts: go LONG when close[i] pierces the prior-bar UPPER band (an upside
breakout out of the channel); EXIT to FLAT when close[i] pierces the prior-bar LOWER
band. Hold the long between those two events (a turtle-style state machine) so we stay
in persistent trends and keep turnover (fees) low. Tune N, M, K, K_EXIT on train only.
WHY LONG/FLAT, NOT LONG/SHORT (honest tuning result on split='train'):
The textbook Keltner breakout is stop-and-reverse (short below the lower band). I
tuned both. Long/SHORT tops out at sharpe_min ~1.04 (maxdd ~0.39); switching the short
leg to FLAT lifts sharpe_min to ~1.56 and cuts maxdd to ~0.28. On BOTH series the short
leg is value-destroying: the pair trends up, so downside breakouts are mostly V-shaped
bottoms / chop where a short gets whipsawed. So the breakout *exit* is kept (a lower-
band break flattens us) but we never flip short. The Keltner breakout EVENT still drives
every entry and exit — the angle is intact.
Tuned on split='train' (Series A & B, equal weight). Broad plateau: 59/340 nearby cells
keep sharpe_min > 1.40, so the chosen point is a plateau CENTER, not an isolated peak:
* N_EMA = 20 (Keltner mid-line EMA span)
* N_ATR = 30 (ATR window for the band half-width)
* K = 1.0 (entry band multiplier: close above EMA + 1.0*ATR -> upside breakout)
* K_EXIT = 0.5 (exit band multiplier: close below EMA - 0.5*ATR -> flatten; tighter
than entry so we exit a failing trend faster than we re-enter)
* vol-target the long to 30% ann vol (vol_win=30d, cap 1.0): the long size shrinks into
vol spikes (every crash is a vol spike) -> caps the drawdown of late/whipsaw entries.
Sharpe is ~flat (1.55-1.56) across target_vol 0.20-0.40; target_vol only trades PnL
for DD (0.20 -> pnl 2.7/DD 0.19 ... 0.40 -> pnl 9.2/DD 0.34). 0.30 is the balance.
Causality: the channel that close[i] is tested against is EMA/ATR evaluated at i-1 (one-
bar lag via .shift(1)), so it is built from bars STRICTLY before i; a close[i] that
pierces it is a real, tradeable event at close[i]. The state machine is a forward scan
(uses only data <= i). The evaluator then holds the position during bar i+1. No future
rows -> causality_ok = true.
Train (combined A&B): pnl_mean ~5.55, maxdd_worst ~0.28, sharpe_min ~1.56.
Honest note: Keltner breakout is pure trend-following, not alpha. Its value here is
converting a high-PnL / ~77-79%-DD uptrend into comparable PnL at ~28% drawdown (DD cut
~2.7x). The full long/short Keltner was MUCH worse (sharpe_min ~1.04, DD ~0.39) — the
edge that matters is the FLAT side, exactly as for the sibling donchian breakout.
"""
import numpy as np
import pandas as pd
import blindlib as bl
N_EMA = 20 # Keltner mid-line EMA span
N_ATR = 30 # ATR window for the band half-width
K = 1.0 # entry band multiplier: break of EMA + K*ATR -> long
K_EXIT = 0.5 # exit band multiplier: break of EMA - K_EXIT*ATR -> flat
TARGET_VOL = 0.30
VOL_WIN_DAYS = 30
LEV_CAP = 1.0
def _keltner_band(df, n_ema, n_atr, k):
"""Lagged Keltner upper/lower at multiplier k: EMA[i-1] +/- k*ATR[i-1]."""
c = df["close"].values.astype(float)
mid = pd.Series(bl.ema(c, n_ema)).shift(1).values # EMA built <= i-1
band = pd.Series(bl.atr(df, n_atr)).shift(1).values # ATR built <= i-1
return mid + k * band, mid - k * band
def signal(df):
c = df["close"].values.astype(float)
upper, _ = _keltner_band(df, N_EMA, N_ATR, K) # entry channel (wider)
_, lower = _keltner_band(df, N_EMA, N_ATR, K_EXIT) # exit channel (tighter)
up = c > upper # upside breakout -> enter / stay long (tradeable at close[i])
dn = c < lower # downside breakout of tighter band -> exit to flat
# turtle long/flat state machine (forward scan, uses only data <= i).
n = len(c)
state = np.zeros(n)
s = 0.0
for i in range(n):
if np.isfinite(upper[i]) and up[i]:
s = 1.0
elif np.isfinite(lower[i]) and dn[i]:
s = 0.0
state[i] = s
# size the long with causal vol-targeting (shrinks into vol spikes -> caps DD).
pos = bl.vol_target(state, df, target_vol=TARGET_VOL,
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
@@ -0,0 +1,134 @@
"""agent_11_squeeze — ANGLE [family=breakout, slug=squeeze].
Range-compression (NR / Bollinger-squeeze) THEN expansion: after a low-volatility
"coil", price tends to break out and run. We (1) detect the squeeze causally, (2) wait
for the breakout out of the coil, (3) enter in the breakout direction, vol-targeted.
Mechanics (all causal — value at i uses only rows 0..i):
* SQUEEZE detector: Bollinger bandwidth = (BB_upper - BB_lower) / mid, using a
rolling window ending at i. A bar is "coiled" when its bandwidth sits in the low
tail of its own EXPANDING history (causal percentile, no future). This is the
classic Bollinger-squeeze / NR proxy: bands pinch when realized vol compresses.
* BREAKOUT trigger: a Donchian channel built STRICTLY from bars < i (bl.donchian
shifts by 1). When close[i] pierces the prior N-bar high -> upside expansion;
pierces the prior N-bar low -> downside expansion. The break is only ARMED if we
were recently in a squeeze (coil within the last LOOKBACK bars) — that is the
whole thesis: expansion out of compression, not a random breakout.
* STATE machine: once a squeeze-armed breakout fires, carry that side (stop-and-
reverse on the opposite squeeze-armed breakout) so we ride the post-coil
expansion and keep turnover low. Decay to flat if the move stalls back inside
the channel for a while (the coil's energy is spent).
* SIZING: the +/-1 direction is vol-targeted (TP01-style) so exposure shrinks into
vol spikes -> caps drawdown on whipsaws / failed breakouts.
Tuned ONLY on split='train' (Series A and B, equal weight). Causality verified by the
harness (signal on a prefix matches signal on the full array over its tail).
Honest notes:
* Squeeze-breakout is trend-following with a regime filter. On these trending curves
it captures up-legs with ~3x less drawdown than buy&hold (DD ~29% vs ~70-80%) at
only ~25-33% time-in-market; the cost is failed-breakout whipsaws after a fake-out
coil. Value is risk-adjusted, not raw PnL.
* Shorts were dropped (SHORT_SCALE=0): on both train curves the downside-breakout leg
was a net loser (coils on an uptrend mostly fake out down -> V-bottoms), so the
long/flat version is strictly better on Sharpe AND drawdown.
* ABLATION CAVEAT: a pure Donchian breakout with the SAME hold/exit logic but NO coil
gate scores marginally HIGHER on train (Sh ~1.05 / PnL ~1.34) than the coil-gated
version. The squeeze gate trims turnover and DD but is NOT the source of the edge
here — the edge is the breakout + vol-target. Kept the coil gate because the
assigned angle is *squeeze*; it is a mild, honest improvement on risk, not magic.
"""
import numpy as np
import pandas as pd
import blindlib as bl
# --- tuned on split='train' (broad plateau, see header / grid in commit) ------
BB_WIN = 20 # Bollinger window for bandwidth
BB_K = 2.0 # Bollinger multiplier
SQ_PCTL = 0.45 # bandwidth below this expanding-percentile = coil (sub-median
# compression; tighter pctl over-filters and loses good breaks)
DON_WIN = 25 # Donchian breakout lookback
ARM_LOOKBACK = 15 # breakout must occur within this many bars of a coil
HOLD_BARS = 40 # ride the post-coil expansion for ~this many bars, then decay
STALL_BARS = 12 # if price falls back inside the channel this long, exit early
SHORT_SCALE = 0.0 # downside-breakout sizing (0 = long/flat; coils on these
# uptrends mostly fake out to the downside, so shorts bleed)
TARGET_VOL = 0.20
VOL_WIN_DAYS = 30
LEV_CAP = 1.0
def _expanding_pctl_rank(x: np.ndarray, min_n: int = 60) -> np.ndarray:
"""Causal expanding percentile rank of x[i] within x[0..i]. rank in [0,1].
rank = fraction of past (<=i) values that are <= x[i]. Uses only rows 0..i."""
n = len(x)
out = np.full(n, np.nan)
# incremental sorted insertion would be O(n log n); n~2000 so an O(n^2) pass is
# fine (<30s). Keep it simple and obviously causal.
for i in range(n):
xi = x[i]
if not np.isfinite(xi):
continue
window = x[: i + 1]
valid = window[np.isfinite(window)]
if len(valid) < min_n:
continue
out[i] = float(np.mean(valid <= xi))
return out
def signal(df):
c = df["close"].values.astype(float)
n = len(c)
# 1) Bollinger bandwidth (causal) -> squeeze when bandwidth is in its low tail.
upper, mid, lower = bl.bbands(c, BB_WIN, BB_K)
with np.errstate(invalid="ignore", divide="ignore"):
bw = (upper - lower) / np.where(np.abs(mid) > 0, mid, np.nan)
bw_rank = _expanding_pctl_rank(bw, min_n=max(60, BB_WIN * 2))
coil = np.nan_to_num(bw_rank, nan=1.0) <= SQ_PCTL # True where compressed
# "recently coiled" = a coil within the last ARM_LOOKBACK bars (causal).
coil_recent = (
pd.Series(coil.astype(float)).rolling(ARM_LOOKBACK, min_periods=1).max().values > 0
)
# 2) Donchian breakout (prior-bar channel; bl.donchian already shifts by 1).
don_hi, don_lo = bl.donchian(df, DON_WIN)
up_break = np.isfinite(don_hi) & (c > don_hi)
dn_break = np.isfinite(don_lo) & (c < don_lo)
# 3) state machine: arm breakouts only when they expand out of a recent coil.
# The thesis is that the EDGE lives in the expansion right after the coil, so
# we ride a fired breakout for HOLD_BARS then decay to flat (the coil's energy
# is spent). A fresh squeeze-armed breakout re-arms / re-times the hold. We
# exit early if price collapses back inside the channel (failed breakout).
state = np.zeros(n)
s = 0.0
age = 0 # bars since the active breakout fired
inside_count = 0 # consecutive bars back inside the channel since trigger
for i in range(n):
armed = coil_recent[i]
fired = False
if armed and up_break[i]:
s = 1.0; age = 0; inside_count = 0; fired = True
elif armed and dn_break[i]:
s = -SHORT_SCALE; age = 0; inside_count = 0; fired = (SHORT_SCALE > 0)
if not fired and s != 0.0:
age += 1
# failed-breakout guard: price back inside the prior channel
in_channel = True
if np.isfinite(don_hi[i]) and c[i] > don_hi[i]:
in_channel = False
if np.isfinite(don_lo[i]) and c[i] < don_lo[i]:
in_channel = False
inside_count = inside_count + 1 if in_channel else 0
if inside_count >= STALL_BARS or age >= HOLD_BARS:
s = 0.0; age = 0; inside_count = 0
state[i] = s
# 4) size by causal vol-targeting (shrinks into vol spikes -> caps DD).
pos = bl.vol_target(state, df, target_vol=TARGET_VOL,
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
@@ -0,0 +1,116 @@
"""agent_12_pivot — ANGLE: rolling support/resistance PIVOT breakout + confirmation bar.
Idea (assigned angle, family=breakout / slug=pivot):
Build dynamic SUPPORT and RESISTANCE from swing PIVOTS (fractal turning points), not
from a flat Donchian channel. A pivot HIGH at bar k is a local maximum with `LR` bars
higher-or-equal on each side; a pivot LOW the mirror. Resistance = the most recent
CONFIRMED pivot-high price; support = the most recent confirmed pivot-low price.
A BREAKOUT is close[i] printing above resistance (long) / below support (short).
We require a CONFIRMATION BAR: the breakout must hold for `CONFIRM` consecutive closes
(filters the one-bar wick fake-out) before we take the position.
CAUSALITY — the crux of a pivot signal:
A pivot at bar k can only be CONFIRMED `LR` bars later (you need the `LR` right-side bars
to know k was a local extreme). So the resistance/support level available at bar i is the
newest pivot whose confirmation bar k+LR <= i. We build the level series with a forward
scan that, at each i, only looks at pivots already confirmed by bars <= i. No future rows
enter the level at i. The breakout test then compares close[i] (known at i) to that level,
and the evaluator holds the resulting position during bar i+1. causality_ok -> true.
LONG/SHORT vs LONG/FLAT (honest tuning on split='train', both A & B equal weight):
Textbook pivot breakout is stop-and-reverse. On these two strongly up-trending curves the
SHORT leg destroys risk-adjusted value (downside pivot breaks are mostly V-bottoms / chop
that whipsaw a short). Best train Sharpe came from LONG on a confirmed resistance break,
going FLAT on a confirmed support break — keep the breakout EXIT, never flip short. Sized
with causal vol-targeting so the long shrinks into vol spikes (every crash is a vol spike),
which caps the drawdown of late / whipsaw entries.
Tuned params — broad plateau on train (both A & B), NOT an isolated peak. Sharpe_min holds
~1.30-1.36 across LR 3..4, CONFIRM 3, target_vol 0.20..0.40, vol_win 20..45 (sweep in commit
notes): the edge is structural, not a fitted corner. Chosen for the best PnL-at-low-DD balance:
LR=4 (pivot half-window), CONFIRM=3 (closes the break must hold), vol-target 30% / 30d / cap 1.
-> train combined: pnl_mean ~4.40, maxdd_worst ~0.26, sharpe_min ~1.33.
Honest note: like every breakout on a trending pair this is trend-following, not alpha. Its
value is converting a high-PnL / ~77%-DD uptrend into comparable PnL at ~26% drawdown (DD cut
~3x). The CONFIRMATION BAR is what separates it from a plain Donchian: it adds ~0.06-0.10
Sharpe and trims the DD by ignoring one-bar wick breaks of the pivot level.
"""
import numpy as np
import pandas as pd
import blindlib as bl
LR = 4 # pivot half-window: local extreme vs LR bars each side
CONFIRM = 3 # breakout must hold this many consecutive closes (confirmation bar)
TARGET_VOL = 0.30
VOL_WIN_DAYS = 30
LEV_CAP = 1.0
def _pivot_levels(high, low, lr):
"""Causal nearest-confirmed-pivot resistance & support.
pivot high at k := high[k] == max(high[k-lr .. k+lr]) (>= neighbours)
It is CONFIRMED (knowable) only at bar k+lr. We emit, for every bar i, the price of
the most recent pivot high/low confirmed at a bar <= i. Pure forward scan, data <= i.
"""
n = len(high)
res = np.full(n, np.nan) # nearest confirmed pivot-HIGH price (resistance)
sup = np.full(n, np.nan) # nearest confirmed pivot-LOW price (support)
cur_res = np.nan
cur_sup = np.nan
for i in range(n):
# a pivot centred at k = i-lr becomes confirmable exactly now (its right window
# k+1..k+lr == i-lr+1..i is complete and all <= i; left window also <= i).
k = i - lr
if k - lr >= 0:
seg_h = high[k - lr:i + 1] # high[k-lr .. i] = high[k-lr .. k+lr]
seg_l = low[k - lr:i + 1]
hk = high[k]
lk = low[k]
if hk >= seg_h.max(): # k is a (weak) local max -> pivot high
cur_res = hk
if lk <= seg_l.min(): # k is a local min -> pivot low
cur_sup = lk
res[i] = cur_res
sup[i] = cur_sup
return res, sup
def signal(df):
high = df["high"].values.astype(float)
low = df["low"].values.astype(float)
c = df["close"].values.astype(float)
n = len(c)
res, sup = _pivot_levels(high, low, LR)
# raw breakout events (causal: level + close both known at i)
brk_up = c > res # close above resistance pivot
brk_dn = c < sup # close below support pivot
brk_up = np.nan_to_num(brk_up, nan=False).astype(bool)
brk_dn = np.nan_to_num(brk_dn, nan=False).astype(bool)
# CONFIRMATION BAR: require the break to hold CONFIRM consecutive closes.
if CONFIRM > 1:
up_run = pd.Series(brk_up).rolling(CONFIRM, min_periods=CONFIRM).sum().values == CONFIRM
dn_run = pd.Series(brk_dn).rolling(CONFIRM, min_periods=CONFIRM).sum().values == CONFIRM
up_run = np.nan_to_num(up_run, nan=False).astype(bool)
dn_run = np.nan_to_num(dn_run, nan=False).astype(bool)
else:
up_run, dn_run = brk_up, brk_dn
# long/flat state machine (forward scan, data <= i):
# confirmed resistance break -> long ; confirmed support break -> flat.
state = np.zeros(n)
s = 0.0
for i in range(n):
if up_run[i]:
s = 1.0
elif dn_run[i]:
s = 0.0
state[i] = s
pos = bl.vol_target(state, df, target_vol=TARGET_VOL,
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
@@ -0,0 +1,93 @@
"""agent_13_volbreak — ANGLE [family=breakout, slug=volbreak].
Volatility breakout: enter the trend direction when REALIZED VOL EXPANDS above its
rolling median. The thesis: a fresh expansion of realized volatility marks a regime
of large, directional moves (a breakout out of a quiet base). When vol picks up we
align with the prevailing trend and ride it; when vol is compressed / below its
rolling median we stand aside (no breakout in progress, just chop).
Mechanics (all causal — value at i uses only rows 0..i):
* VOL EXPANSION gate: annualized realized vol over a short window (RV_WIN) vs its
own rolling median over a longer lookback (MED_WIN). "Expanded" when
rv[i] > EXP_K * median(rv up to i). bl.realized_vol and pandas rolling are causal.
* TREND direction: sign of price vs a moving average (close / SMA(TREND_WIN) - 1),
decided at close[i]. This is the direction we take *only while* vol is expanded.
* STATE / persistence: once vol expands we lock onto the current trend side and
hold it (stop-and-reverse if the trend sign flips while still expanded) until vol
falls back BELOW its median (expansion over) -> flat. This rides the whole
high-vol leg instead of flickering bar to bar, keeping turnover (fees) down.
* SIZING: the +1/0 direction is vol-targeted (TP01-style) so exposure shrinks into
the very vol spikes the gate selects -> caps drawdown on violent reversals.
Tuned ONLY on split='train' (Series A and B, equal weight; broad plateau grid below).
Causality verified by the harness (signal on a prefix matches signal on the full array
over its tail).
Honest notes:
* On these strongly-trending high-vol curves the edge is essentially "be long the
trend, but ONLY when vol confirms a breakout, and shrink size into vol". Value is
RISK-ADJUSTED: comparable/positive PnL at ~3-4x less drawdown than buy&hold (which
eats ~77-79% DD here), not bigger raw PnL. Train combined Sharpe ~1.12, worst-DD
~23%, mean PnL ~1.14.
* LONG-ONLY (SHORT_SCALE=0). Shorts were dropped after tuning: on these uptrends the
down-trend + vol-expansion combo is dominated by violent V-bottom reversals, which
are terrible to short -> a short leg (full OR damped) strictly LOWERED Sharpe and
raised DD on both train curves. The short leg is not an edge here; flat is better.
* EXP_K=0.8 means we trade when rv sits at/above 0.8x its rolling median — still a
genuine vol-expansion gate (it stands aside in the lowest-vol ~30-40% of bars where
price just chops), but inclusive enough not to miss the early part of a breakout
leg. Requiring rv strictly ABOVE the median (K>=1.0) entered too late and gutted the
Series-B trend capture (Sh 1.12 -> 0.28). The plateau holds for RV 15-20, MED
100-150, K 0.78-0.85, TREND 30-60.
"""
import numpy as np
import pandas as pd
import blindlib as bl
# --- tuned on split='train' (broad plateau) ---------------------------------
RV_WIN = 15 # short realized-vol window (the "current" vol)
MED_WIN = 100 # rolling-median lookback for the vol baseline
EXP_K = 0.80 # vol is "expanded" when rv > EXP_K * rolling-median(rv)
TREND_WIN = 50 # trend filter: sign of close / SMA(TREND_WIN) - 1
SHORT_SCALE = 0.0 # LONG-ONLY: down-vol-breaks here are mostly V-reversals -> shorts bleed
TARGET_VOL = 0.20
VOL_WIN_DAYS = 30
LEV_CAP = 1.5
def signal(df):
c = df["close"].values.astype(float)
n = len(c)
bpy = bl.bars_per_day(df) * 365.25
# 1) realized vol (short) and its causal rolling median baseline.
r = bl.simple_returns(c)
rv = bl.realized_vol(r, RV_WIN, bpy)
rv_med = pd.Series(rv).rolling(MED_WIN, min_periods=max(10, MED_WIN // 2)).median().values
expanded = np.isfinite(rv) & np.isfinite(rv_med) & (rv > EXP_K * rv_med)
# 2) trend direction decided at close[i] (causal).
ma = bl.sma(c, TREND_WIN)
with np.errstate(invalid="ignore", divide="ignore"):
trend = np.where(np.isfinite(ma) & (ma > 0), c / ma - 1.0, 0.0)
tsign = np.sign(trend)
# 3) state machine: while vol is expanded, hold the trend side (S&R on sign flip);
# when vol falls back below its (scaled) median the breakout is spent -> flat.
state = np.zeros(n)
s = 0.0
for i in range(n):
if expanded[i]:
if tsign[i] > 0:
s = 1.0
elif tsign[i] < 0:
s = -SHORT_SCALE
# tsign == 0 -> keep current side
else:
s = 0.0
state[i] = s
# 4) size by causal vol-targeting (shrinks into vol spikes -> caps DD).
pos = bl.vol_target(state, df, target_vol=TARGET_VOL,
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
@@ -0,0 +1,100 @@
"""Agent 14 — RSI reversion, trend-gated (family=meanrev, slug=rsi).
The angle (assigned): RSI reversion. Long when RSI<lo, short when RSI>hi (bl.rsi),
GATED by a longer trend filter. Tune lo/hi/win.
Reading the train curves first (both A and B, split='train'): they trend UP hard
(ann vol ~0.7-0.9, total ret +6.7x / +23x over the window). The TEXTBOOK 30/70 RSI
thresholds are dead here: in these up-curves RSI sits >70 ~11% of bars and the dips
only floor around RSI 40-45 — RSI<30 in an uptrend happens ~0.1% of the time. A naive
symmetric "short every RSI>70" rule would just short the bull and bleed. So the
mean-reversion has to be REGIME-AWARE, and the lo/hi have to be tuned to the data's
actual RSI distribution, not the textbook:
* In an UPTREND (close above a long SMA) RSI dips are BUY-THE-DIP reversion. We go
LONG when RSI drops below LO and HOLD that long (hysteresis) until RSI recovers
past a higher EXIT level — the classic RSI entry/exit pair — then flat. We do NOT
short RSI>hi here (overbought in an uptrend keeps running; that is momentum).
* In a DOWNTREND (close below the long SMA) the symmetry returns: RSI>HI is a
reversion SHORT (rips fade back down); RSI<LO we stand flat (don't knife-catch
long against a downtrend). The short side is weighted < 1 because the curves drift
up — on train it adds a touch of PnL with no DD cost but is not where the edge is.
The long trend filter does two jobs: it picks WHICH side of the RSI book is reversion
(buy dips in up-trend / sell rips in down-trend) and it suppresses the side that fights
the drift. TREND_WIN=150 is the DD sweet spot on train (DD 0.11 vs 0.16-0.21 at 100/200)
— the gate is what keeps the drawdown small. Sizing is smooth (further past the
threshold -> bigger appetite, no hard 0/1 fee-churning flips) then vol-targeted so the
two curves are risk-comparable and exposure shrinks into vol spikes (crashes are vol
spikes), bounding the drawdown.
HONEST NOTE: in a market that trends this hard, a trend-gated RSI dip-buy partially
degenerates toward trend participation — the dips it buys are shallow (RSI ~50s, not
30s) and it rides them up. The genuine reversion content is the buy-low/exit-high cycle
and the DD control from the trend gate + vol-target; the short side carries almost no
weight in the train edge. The result is an honest-but-modest combined train Sharpe ~1.1
at ~11% DD (vs long-only buy&hold's ~7-23x PnL at ~70-80% DD) — i.e. a fraction of the
buy&hold PnL but ~6-7x less drawdown.
CAUSAL: rsi() is an EWMA of past gains/losses (<= i); the SMA trend filter is trailing;
the hold-state is a forward cumulative pass over PAST bars only; vol_target uses trailing
realized vol. No shift(-k), no centered windows, no global fit. Verified by causality_ok
(max_diff 0.0).
Tuning (train only, combined A&B; coarse->fine sweep + plateau check). Chosen cell is
INTERIOR on every axis — RW in [18..25], LO in [56..62], EXIT in [75..85], TWIN=150,
TVOL [0.20..0.25] all stay sharpe_min ~1.0..1.26 at DD ~0.11..0.13, a broad plateau not
a spike. (Pushing LO/EXIT higher keeps lifting train Sharpe but only by degenerating into
buy-and-hold, so we stop at an interior dip-entry cell that is still genuinely a dip rule.)
RSI_WIN=20, LO=58, HI=68, EXIT=78, TREND_WIN=150
SHORT_W=0.5, TARGET_VOL=0.25, VOL_WIN_DAYS=35, LEV_CAP=1.5, BASE=0.6
-> train combined: pnl_mean ~0.87, maxdd_worst ~0.11, sharpe_min ~1.14
"""
import numpy as np
import blindlib as bl
RSI_WIN = 20 # RSI lookback (the "win" of the angle; 20 > textbook 14 for these trends)
LO = 58.0 # oversold/dip threshold -> reversion LONG (tuned to the curves' RSI floor)
HI = 68.0 # overbought threshold -> reversion SHORT (downtrend only)
EXIT = 78.0 # dip-long is HELD until RSI recovers past EXIT (hysteresis entry/exit pair)
TREND_WIN = 150 # long SMA: above = uptrend (buy dips), below = downtrend (sell rips). DD sweet spot.
SHORT_W = 0.5 # weight on the downtrend short side; <1 because the curves drift up
BASE = 0.6 # base long size while holding a dip (scaled up if still oversold)
TARGET_VOL = 0.25
VOL_WIN_DAYS = 35
LEV_CAP = 1.5
def signal(df):
c = df["close"].values.astype(float)
n = len(c)
rs = bl.rsi(c, RSI_WIN)
trend_up = c > bl.sma(c, TREND_WIN) # causal trailing SMA trend gate
# --- smooth reversion appetite from RSI (further past threshold -> bigger) ---
long_app = np.clip((LO - rs) / 25.0, 0.0, 1.0) # oversold -> long appetite
short_app = np.clip((rs - HI) / (100.0 - HI), 0.0, 1.0) # overbought -> short appetite
# --- trend-gated RSI reversion with hysteresis on the dip-long ---
# The forward pass below is PURE PAST-ONLY: in_long at bar i depends only on bars <= i
# (rs, trend_up are causal; the state machine never looks ahead). Causality verified.
held = np.zeros(n)
in_long = False
for i in range(n):
if in_long:
# exit the held dip-long when the trend breaks down OR RSI has recovered
if (not trend_up[i]) or (rs[i] >= EXIT):
in_long = False
else:
# enter a dip-long only in an uptrend when RSI is below LO (oversold dip)
if trend_up[i] and rs[i] < LO:
in_long = True
if in_long:
held[i] = max(BASE, long_app[i]) # ride the recovery, bigger if still oversold
else:
# when not holding a long, only the downtrend reversion-short passes through
held[i] = (-SHORT_W * short_app[i]) if (not trend_up[i]) else 0.0
pos = bl.vol_target(held, df, target_vol=TARGET_VOL,
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
@@ -0,0 +1,108 @@
"""Agent 15 — Bollinger-band reversion, low-vol gated (family=meanrev, slug=bbands).
The angle (assigned): fade touches of the Bollinger bands (bl.bbands), only in a
low-vol regime. Tune win, k.
What the train curves actually say (A & B, split='train', diagnosed before coding):
both trend UP hard (+6.7x / +23x, ann vol ~0.7-0.9). The TEXTBOOK symmetric band-fade
is a LOSER here and the data is blunt about why:
* UPPER-band touch -> CONTINUATION, not reversion. fwd-5bar after a close>=upper is
+3.4%/+2.7% (A/B) even when we restrict to the low-vol regime. In a bull, riding the
upper band is momentum; shorting it just bleeds against the drift. So the SHORT side
of the classic fade is dead and we do NOT take it.
* LOWER-band touch is reversion ONLY when it is a DIP IN AN UPTREND. close<=lower while
price is above a long SMA -> fwd-5bar +3.5%/+7.2% (A/B): the band stretch snaps back
up. The same lower touch in a DOWNTREND / high-vol continues DOWN (A high-vol lo-touch
fwd-5 = -3.9%): a real knife. So the reversion we keep is the buy-the-dip-in-uptrend
leg, and we gate it OFF in downtrends and in high vol.
Hence the rule is an HONEST, one-sided Bollinger reversion: LONG the lower-band touch,
but only while (a) close is above a long trend SMA and (b) realized vol is in its lower
regime (the assigned low-vol gate). %b drives a smooth appetite (deeper below the band ->
bigger), the long is HELD with hysteresis until price mean-reverts back through the mid
band, then flat. Sizing is vol-targeted so the two curves are risk-comparable and exposure
shrinks into vol spikes (which are exactly the regime where the dip-buy fails).
HONEST NOTE: in a market trending this hard a trend+lowvol-gated dip-buy partially
degenerates toward trend participation — the genuine reversion content is the buy-below-band
/ exit-at-mid cycle plus the DD control from the gates + vol-target. The symmetric short-the-
upper-band leg that "Bollinger reversion" classically implies carries NEGATIVE edge on these
curves, so taking it would only add drawdown; the result is therefore a modest-but-real
reversion edge, NOT a high-PnL alpha. A negative result for the *symmetric* fade is itself a
finding (documented above).
CAUSAL: bbands/sma/realized_vol are trailing (value at i uses bars <= i); the hold-state is
a forward cumulative pass over PAST bars only; vol_target uses trailing realized vol. No
shift(-k), no centered windows, no global fit. Verified by causality_ok (max_diff ~0).
Tuning (train only, combined A&B; coarse->fine sweep + plateau check). The chosen cell is
interior on every axis and sits on a stable plateau (neighbouring K in [1.8..2.2],
TREND_WIN in [100..150], VOL_PCT in [0.65..0.85], ENTRY_PB in [0..0.1] all give
sharpe_min ~0.43-0.48 at DD ~0.08, sharpe_mean ~0.74-0.80):
BB_WIN=20, BB_K=2.0, TREND_WIN=120, VOL_WIN=20, VOL_PCT=0.65,
ENTRY_PB=0.10 (touch lower band), EXIT_PB=0.50 (exit at the MID band),
TARGET_VOL=0.25, VOL_WIN_DAYS=30, LEV_CAP=1.5, BASE=1.0
-> train combined: pnl_mean ~0.29, maxdd_worst ~0.08, sharpe_min ~0.48 (A binds; B ~1.1).
Exiting at the mid band (not higher) is the binding choice: Series A's dips are shallow and
fizzle, so holding the reversion past mid turns Series A negative (Sharpe 0.48 -> -0.0).
"""
import numpy as np
import blindlib as bl
import pandas as pd
BB_WIN = 20 # Bollinger lookback ("win" of the angle)
BB_K = 2.0 # band width in std ("k" of the angle)
TREND_WIN = 120 # long SMA: dip-buy only ABOVE it (reversion lives in the uptrend)
VOL_WIN = 20 # realized-vol lookback for the low-vol gate
VOL_PCT = 0.65 # low-vol gate: only act when rolling vol is below its expanding p65
ENTRY_PB = 0.10 # enter when %b <= this (close at/below the lower band)
EXIT_PB = 0.50 # exit when %b >= this (price has mean-reverted to the MID band)
TARGET_VOL = 0.25
VOL_WIN_DAYS = 30
LEV_CAP = 1.5
BASE = 1.0 # full size while holding a dip-long (the events are sparse; ride the snap-back)
def _expanding_quantile_below(x, q):
"""Causal: at bar i, is x[i] at/below the q-quantile of x[0..i]? (expanding, no leak)."""
s = np.asarray(x, float)
thr = pd.Series(s).expanding(min_periods=30).quantile(q).values
out = s <= thr
out[~np.isfinite(thr)] = False
return out
def signal(df):
c = df["close"].values.astype(float)
n = len(c)
up, mid, lo = bl.bbands(c, BB_WIN, BB_K) # causal trailing bands
band_w = up - lo
# %b: 0 = at the lower band, 0.5 = at the mid band, 1 = at the upper band.
pb = np.where(np.isfinite(band_w) & (band_w > 0), (c - lo) / band_w, np.nan)
trend_up = c > bl.sma(c, TREND_WIN) # causal trend gate
r = bl.simple_returns(c)
rv = bl.realized_vol(r, VOL_WIN, 365.0) # causal trailing realized vol
low_vol = _expanding_quantile_below(rv, VOL_PCT) # causal expanding low-vol regime gate
# One-sided Bollinger reversion: buy the lower-band touch (dip) in uptrend + low-vol,
# HOLD with hysteresis until %b mean-reverts back up to the MID band, then flat. The
# symmetric upper-band SHORT is a proven loser on these curves (continuation), so flat.
# Forward pass is PURE PAST-ONLY: in_long at i depends only on bars <= i.
held = np.zeros(n)
in_long = False
for i in range(n):
if in_long:
# exit when the dip has mean-reverted to the mid band, or the trend breaks
if (not trend_up[i]) or (np.isfinite(pb[i]) and pb[i] >= EXIT_PB):
in_long = False
else:
# enter a dip-long: %b at/below the lower band, in uptrend, in low-vol regime
if trend_up[i] and low_vol[i] and np.isfinite(pb[i]) and pb[i] <= ENTRY_PB:
in_long = True
held[i] = BASE if in_long else 0.0
pos = bl.vol_target(held, df, target_vol=TARGET_VOL,
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
@@ -0,0 +1,78 @@
"""Agent 16 — Z-score reversion to SMA, trend-gated (family=meanrev, slug=zrev).
THE ANGLE (assigned): reversion of price to its SMA via a CAUSAL rolling z-score —
short positive extremes / long negative extremes — WITH A TREND-AGREEMENT GATE.
Why the gate is the whole story here. Naive z-reversion (short every z>+thr, long every
z<-thr against a price-vs-SMA z-score) LOSES on these two curves: both trend up ~8x/24x
over the sample, so a positive z-extreme above a medium SMA is usually momentum that keeps
going (study: z>1.5 -> next-bar +0.005/+0.008, NOT a reversal), and shorting it just fights
the trend. The reversion that actually exists is the SHORT-HORIZON pullback inside the
prevailing trend:
* In an UPTREND (price > slow SMA), a negative z-extreme (a dip below the FAST SMA) is a
pullback that bounces -> go LONG. (study: UP & z<-1 -> next-bar +0.003 .. +0.012.)
* In a DOWNTREND (price < slow SMA), a positive z-extreme (a rally above the FAST SMA) is
a dead-cat that fades -> go SHORT. (study: DOWN & z>+1 -> next-bar ~0 .. -0.004.)
* A z-extreme that DISAGREES with the trend (rally in an uptrend / dip in a downtrend) is
momentum/continuation, not reversion -> stay FLAT (those bins are where naive z-reversion
bleeds: UP & z>1 -> +0.003 continuation; you must NOT short it).
So the position is the reversion impulse (-z, clipped to extremes) FILTERED by trend
agreement: keep only longs in uptrends and shorts in downtrends. A causal vol-target then
sizes it so A and B are risk-comparable and exposure shrinks into vol spikes.
CAUSAL: zscore(c, FAST) and sma(c, SLOW) at i use only rows <= i; the trend gate and
vol_target are trailing. No shift(-k), no centered windows, no global fit. Verified by
causality_ok.
Tuning (train only, combined A&B; coarse->fine sweep). A CONTINUOUS reversion impulse
(-z, saturating) gated by the trend beats sparse extreme-only entries (more of the dips are
captured while the gate keeps the trend on your side). The chosen cell is interior on every
axis and is a plateau, not a spike: FAST 2..3, SLOW 100..150, Z_SAT 1.5..2.0 all stay in
sharpe_min ~0.6..0.8 at DD ~0.06..0.12; SHORT_W 0->0.5 only lowers sharpe_min (the downtrend
short reversion fights the structural uptrend). vol_target scales PnL<->DD linearly (sharpe
flat), so TARGET_VOL just sets the risk dial.
FAST=2, SLOW=120, Z_SAT=1.75, SHORT_W=0.0, TARGET_VOL=0.30, VOL_WIN_DAYS=30, LEV_CAP=2.0
-> train combined: pnl_mean ~0.31, maxdd_worst ~0.11, sharpe_min ~0.78
(a modest PnL at a ~10% drawdown — the reversion-in-trend captures the bounces while
sidestepping the big declines, vs long-only buy&hold's huge PnL at ~70-80% DD).
"""
import numpy as np
import blindlib as bl
FAST = 2 # short SMA for the reversion z-score (the "stretch from SMA" detector)
SLOW = 120 # slow SMA defining the trend regime for the agreement gate
Z_SAT = 1.75 # z magnitude that saturates the reversion impulse to +-1
SHORT_W = 0.0 # weight on the (gated) short leg; tuning -> 0 (long-flat best on train)
TARGET_VOL = 0.30
VOL_WIN_DAYS = 30
LEV_CAP = 2.0
def signal(df):
c = df["close"].values.astype(float)
n = len(c)
z = np.nan_to_num(bl.zscore(c, FAST), nan=0.0) # price-vs-fast-SMA, standardized (causal)
slow = bl.sma(c, SLOW) # trend regime line (causal)
uptrend = c > slow # boolean trend gate
# reversion impulse = -z: long when price is stretched BELOW its SMA (dip, z<0),
# short when stretched ABOVE (rally, z>0). Proportional, saturating at +-Z_SAT.
impulse = np.clip(-z / Z_SAT, -1.0, 1.0) # -z direction = reversion to the SMA
# TREND-AGREEMENT GATE: keep ONLY longs in an uptrend and shorts in a downtrend.
# A z-extreme that DISAGREES with the trend (rally in an uptrend / dip in a downtrend)
# is momentum/continuation, not reversion -> stay FLAT. The short leg is gated AND
# down-weighted by SHORT_W (tuning drives it to 0: both curves trend up, so the
# downtrend-short reversion only adds drawdown here).
raw = np.zeros(n)
long_ok = (impulse > 0) & uptrend # buy the dip inside an uptrend
short_ok = (impulse < 0) & (~uptrend) # fade the rally inside a downtrend
raw[long_ok] = impulse[long_ok]
raw[short_ok] = impulse[short_ok] * SHORT_W
pos = bl.vol_target(raw, df, target_vol=TARGET_VOL,
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
@@ -0,0 +1,115 @@
"""Agent 17 — Short-term reversal, trend-gated (family=meanrev, slug=st_reversal).
THE ANGLE (assigned): fade the last 1-3 bar move, but ONLY when the longer trend
AGREES with the fade direction. So we never fight the trend: we only take the leg of
the reversal that points the same way the slow regime already points.
* UPTREND (price > slow SMA): the trend-agreeing fade is to fade a DROP -> go LONG
the bounce. (Fading a rise here would mean shorting INTO an uptrend = fighting the
trend -> NOT allowed, stay flat on that leg.)
* DOWNTREND (price < slow SMA): the trend-agreeing fade is to fade a RISE -> go SHORT
the dead-cat. (Fading a drop here would mean longing INTO a downtrend = fighting the
trend -> NOT allowed, stay flat on that leg.)
Why this is the structure in the data (train study, both curves):
Forward 1-bar return after a 1-bar move, conditioned on the 150-SMA regime --
A UP & drop>5% -> +0.0050 (bounce) UP & rise>5% -> +0.0007 (rise gives back)
B UP & drop>5% -> +0.0115 (bounce) UP & rise>5% -> -0.0004 (rise gives back)
A DN & rise>2% -> -0.0039 (fades) DN & drop0-2% -> ~0
B DN & rise>2% -> -0.0038 (fades)
-> corr(-r, fwd) is POSITIVE in both regimes (UP ~0.03-0.08, DN ~0.15): a 1-bar move
partially reverses next bar. The trend gate keeps only the half of that reversion
that the slow trend supports, so the (gated) short leg lives only where the curve
is genuinely rolling over -- it does not bleed shorting a structural bull.
The reversal impulse is the (vol-scaled) negative of the recent move -r_k -- a CONTINUOUS,
saturating fade of the last K-bar return -- rather than sparse extreme-only entries, so
more of the small bounces are captured. We blend K=1..3 (mostly K=1, the cleanest
reversal) and normalize each move by trailing vol so the threshold is in sigma, not raw %.
CAUSAL: sma(c,SLOW), the K-bar past returns, the trailing-vol scaler, the trend gate and
vol_target at bar i all use only rows <= i. No shift(-k), no centered windows, no global
fit. Verified by causality_ok.
Tuning (train only, combined A&B, coarse->fine; interior plateau, not a spike). Series A
is the binding constraint (a weaker, deeper-pullback reversal than B); the chosen cell
maximizes A's sharpe at a controlled DD without overfitting B. Perturbations around the
center all stay in sharpe_min ~0.48..0.58 at DD ~0.14..0.16:
SLOW 125..135 (smin 0.51..0.55), Z_SAT 0.85..1.05 (smin 0.52..0.56),
SHORT_W 0..0.5 (smin 0.53..0.54 -- the gated short adds a touch), K-weights from pure
1-bar (smin 0.58, DD 0.16) to (0.5,0.3,0.2) (smin 0.53, DD 0.14). vol_target scales
PnL<->DD ~linearly (sharpe flat) so TARGET_VOL is just the risk dial; LEV_CAP is not
binding (vol-target keeps |pos|<1 on these curves).
Chosen (interior, robust): SLOW=130, K_WEIGHTS=(0.7,0.2,0.1), Z_SAT=0.95, SHORT_W=0.25,
TARGET_VOL=0.25, VOL_WIN_DAYS=30, LEV_CAP=2.0
-> train combined: pnl_mean ~0.52, maxdd_worst ~0.15, sharpe_min ~0.55
(A ~0.55 sharpe / B ~1.3 sharpe). A modest, positive PnL at a ~15% drawdown -- the
trend-gated short-term reversal harvests the in-trend bounces while sidestepping the
big declines, vs long-only buy&hold's ~6-23x PnL at ~70-80% DD.
"""
import numpy as np
import blindlib as bl
SLOW = 130 # slow SMA -> trend regime for the agreement gate
K_WEIGHTS = (0.7, 0.2, 0.1) # blend of the 1-,2-,3-bar fades (mostly the 1-bar, the cleanest)
Z_SAT = 0.95 # move size (in trailing sigma) that saturates the fade impulse to +-1
SHORT_W = 0.25 # weight on the (trend-gated) short leg; gated -> it helps a little
TARGET_VOL = 0.25
VOL_WIN_DAYS = 30
LEV_CAP = 2.0
EPS = 1e-9
def signal(df):
c = df["close"].values.astype(float)
n = len(c)
r = bl.simple_returns(c) # r[i] = c[i]/c[i-1]-1 (causal, uses <= i)
# trailing daily-vol scaler so the "size of the last move" is measured in sigma,
# not raw % (otherwise A and B, with different vols, would need different thresholds).
vol = bl.rolling_std(r, 30)
vol = np.where(np.isfinite(vol) & (vol > EPS), vol, np.nan)
# causal fill: use the last finite vol seen so far; fallback to a constant for warmup.
vol = _ffill(vol)
vol = np.where(np.isfinite(vol), vol, np.nanmedian(vol[np.isfinite(vol)]) if np.isfinite(vol).any() else 0.03)
# FADE impulse = -(recent K-bar move) / vol, blended over K=1..3 and saturated to +-1.
# Positive impulse = price just DROPPED (fade -> want long); negative = just ROSE.
impulse = np.zeros(n)
for k, w in zip((1, 2, 3), K_WEIGHTS):
mk = np.zeros(n)
mk[k:] = c[k:] / c[:-k] - 1.0 # past k-bar return ending at i (causal)
# normalize the k-bar move by sqrt(k)*vol so each horizon is on the same sigma scale
zk = -mk / (np.sqrt(k) * vol + EPS) # FADE = negative of the move
impulse += w * np.clip(zk / Z_SAT, -1.0, 1.0)
impulse = np.clip(impulse, -1.0, 1.0)
slow = bl.sma(c, SLOW) # trend regime line (causal)
uptrend = c > slow
# TREND-AGREEMENT GATE: keep ONLY the fade leg that AGREES with the slow trend.
# uptrend + impulse>0 (price dropped) -> LONG the bounce (fade agrees: up)
# downtrend+ impulse<0 (price rose) -> SHORT the dead-cat (fade agrees: down)
# The disagreeing legs (fade a rise in an uptrend = short into a bull; fade a drop in a
# downtrend = long into a bear) are momentum/continuation, not reversion -> stay FLAT.
raw = np.zeros(n)
long_ok = (impulse > 0) & uptrend
short_ok = (impulse < 0) & (~uptrend)
raw[long_ok] = impulse[long_ok]
raw[short_ok] = impulse[short_ok] * SHORT_W
pos = bl.vol_target(raw, df, target_vol=TARGET_VOL,
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
def _ffill(a):
"""Causal forward-fill of NaNs (each value uses only past finite values)."""
out = a.copy()
last = np.nan
for i in range(len(out)):
if np.isfinite(out[i]):
last = out[i]
else:
out[i] = last
return out
@@ -0,0 +1,89 @@
"""Agent 18 — Distance-from-MA reversion, trend-gated (family=meanrev, slug=dist_ma).
THE ANGLE (assigned): position = -tanh(scaled distance of price from its MA). Buy when price
is stretched BELOW its MA, sell when stretched ABOVE — a reversion-to-the-MA impulse, sized by
how far price has wandered. Tune the MA window and the tanh scale.
WHY THE PURE ANGLE LOSES, AND WHAT SURVIVES.
The naive symmetric form (-tanh(scale * (price/MA - 1)) traded both sides) is CATASTROPHIC on
these two curves: both trend up ~7x (A) / ~23x (B) over the train window, so shorting every
stretch ABOVE the MA just fights a relentless uptrend. Measured: the pure symmetric angle
returns -79%..-95% with sharpe ~ -0.5..-0.9 (it shorts the bull). A conditioning study of
next-bar return vs the normalized distance-from-MA confirms the asymmetry: the LARGEST
positive next-bar returns sit at the HIGHEST positive distance (that's momentum continuation,
NOT reversion — never short it), while the genuine reversion edge lives only on the DOWNSIDE
— when price is stretched well below its MA, the next bar bounces (+0.27%..+0.35% in the
deepest dip bin, pooled A&B). So the distance-from-MA reversion that actually exists here is
the short-horizon PULLBACK inside the prevailing trend, not a fade of the trend itself.
THE RULE.
impulse = -tanh(SCALE * z) where z = (price/SMA(MA) - 1) standardized by a trailing rolling
std (so A and B, with different vol, get comparable stretch units). impulse>0 = price below
its MA (a dip -> reversion says go long); impulse<0 = price above its MA (a rally -> short).
A TREND GATE then keeps only the reversion leg that agrees with the regime:
* UPTREND (price > SMA(SLOW)): take only the LONG impulse (buy the dip that bounces).
* DOWNTREND (price < SMA(SLOW)): take only the SHORT impulse (fade the dead-cat rally),
down-weighted by SHORT_W. Tuning drives SHORT_W -> 0: both curves trend up, so the
downtrend-short reversion only adds drawdown over this sample.
A causal vol_target sizes the impulse so the two series are risk-comparable and exposure
shrinks into vol spikes.
CAUSAL: SMA(MA), SMA(SLOW), the rolling std and vol_target at bar i use only rows <= i. No
shift(-k), no centered windows, no global fit. Verified by causality_ok (online-consistent).
TUNING (train only, combined A&B; coarse->fine, plateau not spike). A FAST MA (the distance is
a short-horizon pullback, not a slow-trend gap) is decisively better than a medium MA:
ma=3 beats ma=20+ by ~0.2 sharpe at lower DD. The chosen cell is interior on every axis:
MA 3..5 -> sharpe_min 0.69..0.81 ; SCALE 1.0..2.5 -> 0.72..0.76 (PnL rises, DD ~flat) ;
NORM_WIN 30..90 -> 0.75..0.80 ; SLOW 110..140 -> sharpe_min 0.74..0.81 (a real plateau).
SHORT_W 0->0.5 only lowers sharpe (the downtrend short fights the structural uptrend).
vol_target trades PnL<->DD ~linearly (sharpe flat), so TARGET_VOL is just the risk dial.
MA=3, NORM_WIN=60, SCALE=1.5, SLOW=130, SHORT_W=0.0, TARGET_VOL=0.30, VOL_WIN=30, LEV_CAP=2.0
-> train combined: pnl_mean ~0.70, maxdd_worst ~0.115, sharpe_min ~0.80
(a solid PnL at an ~11-12% drawdown: the reversion-in-trend harvests the pullback bounces
while sidestepping the deep declines, vs long-only buy&hold's huge PnL at ~70-80% DD.)
HONEST CAVEAT: the value here is the DROP IN DRAWDOWN (~6x lower than buy&hold), not beating
buy&hold's raw PnL on a 7x/23x bull run. The PURE assigned angle (symmetric fade) is a
loser on trending data — it only becomes positive once gated to the dip side of the trend.
"""
import numpy as np
import pandas as pd
import blindlib as bl
MA = 3 # fast SMA -> the distance is a SHORT-HORIZON pullback from price
NORM_WIN = 60 # trailing window standardizing the distance (so A & B are comparable)
SCALE = 1.5 # tanh scale on the standardized distance -> reversion impulse magnitude
SLOW = 130 # trend-regime SMA for the agreement gate
SHORT_W = 0.0 # weight on the (gated) downtrend-short leg; tuning -> 0 (long-flat best)
TARGET_VOL = 0.30
VOL_WIN_DAYS = 30
LEV_CAP = 2.0
def signal(df):
c = df["close"].values.astype(float)
n = len(c)
# distance of price from its (fast) MA, standardized by a trailing rolling std (causal).
dist = c / bl.sma(c, MA) - 1.0
sd = pd.Series(dist).rolling(NORM_WIN).std().values
zd = np.nan_to_num(dist / np.where(sd > 0, sd, np.nan), nan=0.0)
# the assigned angle: reversion impulse = -tanh(scaled distance).
# zd>0 (price above MA) -> impulse<0 (short the stretch)
# zd<0 (price below MA) -> impulse>0 (long the dip)
impulse = -np.tanh(SCALE * zd)
# trend-agreement gate: keep only the reversion leg that agrees with the regime.
up = c > bl.sma(c, SLOW)
raw = np.zeros(n)
long_ok = (impulse > 0) & up # buy the dip inside an uptrend
short_ok = (impulse < 0) & (~up) # fade the rally inside a downtrend (down-weighted)
raw[long_ok] = impulse[long_ok]
raw[short_ok] = impulse[short_ok] * SHORT_W
pos = bl.vol_target(raw, df, target_vol=TARGET_VOL,
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
@@ -0,0 +1,58 @@
"""Agent 19 — Vol-targeted long-only / risk-parity single asset
(family=vol, slug=voltarget_lo).
The angle (assigned): NO direction call. Hold the asset LONG at all times, but size
the position by INVERSE realized volatility so the book runs at a roughly constant
target volatility: exposure[i] = clip( target_vol / realized_vol[i] , 0, cap ).
Why this anticipates anything at all, despite never predicting direction: realized
vol is PERSISTENT (today's vol forecasts tomorrow's vol far better than today's return
forecasts tomorrow's return). The big declines on these two curves are also the high-
vol regimes — a crash is a vol spike. So scaling exposure DOWN when trailing vol is
high mechanically pulls the book light right when the worst legs happen, and levers UP
in the calm grind higher. The result on a structurally up-trending curve is a long-only
book with most of buy&hold's upside but a much smaller drawdown (the risk-parity / "vol
control" effect), at modest turnover (the weight only drifts with the vol forecast).
CAUSAL: realized_vol[i] uses returns over a trailing window ending at i (rows <= i);
the position is then shifted by the evaluator (held during bar i+1). No direction is
derived from any future bar; no global fit. Verified by causality_ok (max_diff 0.0).
Tuning (split='train' only, combined A&B). The free knobs are the trailing vol window,
the target vol, and the leverage cap.
* CAP is the single most important choice. Because both curves trend up hard, a high
cap just re-levers into buy&hold and brings the drawdown right back. cap=1.0 (never
more than fully invested) is what preserves the risk-parity de-risking benefit; with
a vol-driven weight that almost always sits below 1.0 this is the whole point.
* VOL_WIN is the vol-forecast horizon. A SLOW window (~120d) gives a stabler vol
estimate, less whipsaw, lower turnover and the BEST risk-adjusted result here:
sharpe_min climbs from ~0.85 (30d) to ~0.97 (120d) and the plateau (110..200d) is
flat at sharpe 0.91..0.99 / DD ~0.42-0.44 -> 120 is a robust interior pick.
* TARGET_VOL is a pure DD/PnL dial: it scales exposure up and down but (for a long-
only inverse-vol book) leaves the Sharpe essentially flat (0.971 across 0.24..0.32).
So it is chosen for the DD/PnL trade-off, not the Sharpe.
Chosen cell, interior on every axis:
TARGET_VOL = 0.28 # DD/PnL dial; Sharpe flat across 0.24..0.32 -> balanced cell
VOL_WIN_D = 120 # slow, stable vol forecast; plateau 110..200d
LEV_CAP = 1.0 # never lever past fully-invested -> keeps the DD-cut benefit
-> train combined: pnl_mean ~2.93, maxdd_worst ~0.43, sharpe_min ~0.97.
This is a DEFENSIVE long-only book, NOT alpha. Its honest value is the drawdown: ~0.43
vs ~0.77-0.79 buy&hold at comparable PnL. Because it never shorts, its Sharpe ceiling
(~1.0) is set by the absence of any direction call -> it can avoid sizing into the big
declines but cannot profit from them. That is the inherent limit of this angle.
"""
import numpy as np
import blindlib as bl
TARGET_VOL = 0.28
VOL_WIN_D = 120
LEV_CAP = 1.0
def signal(df):
# direction = always long (+1), NO direction call. Sizing is pure inverse-vol.
direction = np.ones(len(df))
pos = bl.vol_target(direction, df, target_vol=TARGET_VOL,
vol_win_days=VOL_WIN_D, leverage_cap=LEV_CAP)
# long-only risk-parity: clip to [0, cap] (no shorts by construction)
return np.clip(np.nan_to_num(pos, nan=0.0), 0.0, LEV_CAP)
@@ -0,0 +1,100 @@
"""agent_20_regime_switch — ANGLE [family=vol, slug=regime_switch].
Regime switch on the realized-vol PERCENTILE (expanding / online):
* Compute short-window realized vol rv[i] at each bar.
* Rank it against its EXPANDING percentile (the causal "typical" vol seen so far) —
a self-calibrating threshold that needs no magic vol level and adapts as the series
evolves (no peeking at the full-sample distribution).
* LOW-VOL regime (rv-rank <= PCTL): TREND-FOLLOW. Quiet, orderly markets are where
momentum persists, so we ride the prevailing (multi-horizon) trend.
* HIGH-VOL regime (rv-rank > PCTL): stand aside (FLAT). High realized vol is where
trends whipsaw / V-reverse and where the big drawdowns are born; the cleanest
expression of the "regime switch" is to refuse directional exposure there.
The trend leg is a multi-horizon TSMOM SIGN blend (slow horizons ~1/2/4 months): a
single lookback is regime-fragile, the blend keeps the slow macro trend while the fast
horizon cuts exposure early into a turn. Final size is a trailing vol-target, so the
position also shrinks into vol within the low-vol regime.
CAUSAL: rv uses a trailing window; the percentile rank is EXPANDING (only past bars);
each TSMOM sign uses close[i]/close[i-H]; vol_target uses a trailing realized-vol
window. No look-ahead, no centered windows, no global fit. Verified by causality_ok
(max_diff 0.0).
Tuned ONLY on split='train' (Series A & B, equal weight). A coarse->fine sweep found a
WIDE plateau: HZ=(25,60,120), PCTL in [0.60..0.70], VW in [35..55], RV in [15..25] all
give sharpe_min ~1.25-1.30 at DD ~0.17-0.19. The chosen cell is interior on every axis
(robust, not a lucky spike):
RV_WIN=20, PCTL=0.65, HORIZONS=(25,60,120), TARGET_VOL=0.22, VOL_WIN=45, LEV_CAP=1.5
-> train combined: pnl_mean ~2.0, maxdd_worst ~0.18, sharpe_min ~1.30.
Honest notes:
* The high-vol leg is LONG-FLAT (not revert). A lightly-weighted contrarian leg in
high vol helped marginally with a single-MA trend, but once the trend is the slow
multi-horizon SIGN blend the reversion leg only added drag -> flat is strictly
better here. The value is RISK-ADJUSTED: comparable/positive PnL at ~4x less
drawdown than buy&hold (which eats ~77-79% DD on these curves), by sitting out the
high-realized-vol regime where the violent declines happen.
* Loosening the gate (PCTL ~0.65, not 0.50) is what lifts both Sharpe and PnL: the
bottom ~half of the vol distribution is too restrictive and misses the early,
still-low-vol part of the trend legs. The plateau is wide enough that the exact
percentile is not load-bearing.
"""
import numpy as np
import blindlib as bl
RV_WIN = 20 # short realized-vol window ("current" vol)
PCTL = 0.65 # expanding vol-percentile gate: trend-follow when rank <= this
HORIZONS = (25, 60, 120) # multi-horizon TSMOM sign blend (~1/2/4 months of daily bars)
TARGET_VOL = 0.22
VOL_WIN_DAYS = 45
LEV_CAP = 1.5
MIN_HIST = 60 # warmup before the expanding percentile is trusted
def _expanding_pctl_rank(x: np.ndarray, min_hist: int) -> np.ndarray:
"""rank[i] = fraction of finite x[0..i] that are <= x[i] (causal, expanding).
NaN until `min_hist` finite values have accumulated."""
n = len(x)
rank = np.full(n, np.nan)
seen: list[float] = []
for i in range(n):
v = x[i]
if np.isfinite(v):
seen.append(v)
if len(seen) >= min_hist:
rank[i] = float(np.mean(np.asarray(seen) <= v))
return rank
def _tsmom_sign(c: np.ndarray, h: int) -> np.ndarray:
"""Sign of the past-h-bar return, causal. 0 for i < h."""
out = np.zeros(len(c))
if h < len(c):
out[h:] = np.sign(c[h:] / c[:-h] - 1.0)
return out
def signal(df):
c = df["close"].values.astype(float)
bpy = bl.bars_per_day(df) * 365.25
# 1) short-window realized vol and its EXPANDING percentile rank (causal).
rv = bl.realized_vol(bl.simple_returns(c), RV_WIN, bpy)
rank = _expanding_pctl_rank(rv, MIN_HIST)
low_vol = np.isfinite(rank) & (rank <= PCTL) # the LOW-VOL regime we trade
# 2) multi-horizon TSMOM sign blend -> graded direction in [-1, +1] (causal).
sig = np.zeros(len(c))
for h in HORIZONS:
sig += _tsmom_sign(c, h)
sig /= len(HORIZONS)
# 3) regime switch: trend-follow ONLY in the low-vol regime, else flat.
raw = np.where(low_vol, sig, 0.0)
# 4) causal vol-targeting (shrinks size into vol -> caps DD).
pos = bl.vol_target(raw, df, target_vol=TARGET_VOL,
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
@@ -0,0 +1,108 @@
"""agent_21_atr_ride — ANGLE: ATR-channel trend ride with an ATR trailing stop that
scales the position DOWN on adverse moves (family=vol, slug=atr_ride).
Idea (assigned angle):
* Build an ATR channel around an EMA mid-line: mid = EMA_N(close);
band half-width = K_ENTRY * ATR_M. A close above mid + K_ENTRY*ATR starts an
uptrend ride.
* Maintain an ATR TRAILING STOP (Chandelier / SuperTrend flavour): a stop line that
RATCHETS in the trade's favour and never loosens. While long, the stop is
(highest-close-since-entry - K_STOP*ATR) and only moves up. A close below it ends
the ride (flatten).
* The distinguishing twist of THIS angle (vs a binary breakout) is the SCALE-DOWN on
adverse moves. Instead of a hard on/off stop we size by the ATR "stop room":
room[i] = clip( (close[i] - stop[i]) / (K_STOP*ATR[i]) , 0, 1 )
= how much cushion (in ATR units, normalised by the stop distance) sits between the
close and the trailing stop. Exposure is proportional to that cushion, so the book
runs full deep in a healthy trend, BLEEDS OFF smoothly as price falls back toward the
stop, and goes flat once the stop breaks. We ride winners and de-risk into reversals
BEFORE the stop is hit, instead of binary all-in / all-out.
Long/flat only. Both curves trend up; the short side of an ATR ride is whipsaw on the
V-shaped bottoms (same lesson as the donchian/keltner siblings), so a stop-out goes to
FLAT, never short. The ride exposure (already in [0,1]) is then vol-targeted so the
long shrinks further into vol spikes (every crash is a vol spike) -> caps the DD.
CAUSAL: mid (EMA) and ATR are built with .shift(1) -> strictly from bars <= i-1, and the
close[i] that pierces the channel / sits above the stop is a real, tradeable event at
close[i]. The trailing-stop state machine is a forward scan using only data <= i (peak is
the running max of past closes; the stop only ratchets up). vol_target uses realized vol
up to i. No future rows, no centered windows, no global fit -> causality_ok = true
(verified: max_diff 0.0). The evaluator then holds the position during bar i+1.
TUNING (split='train' only, Series A & B equal weight; chosen cell is a plateau center):
* N_EMA x N_ATR: the (20,20) cell is the best risk-adjusted corner of the EMA/ATR grid
(sharpe_min ~1.39 vs ~1.06-1.27 at slower 30-60 windows) and its 27-cell neighbourhood
(N_EMA 18-25, N_ATR 15-25, K_STOP 2.0-3.0) holds sharpe_min in [1.16, 1.41] (median
1.30, 93% of cells > 1.2) -> a genuine plateau, not an isolated peak.
* K_ENTRY = 1.0 is the clear ridge: the K_ENTRY row 0.5->1.5 peaks sharply at 1.0
(sharpe_min jumps to ~1.3-1.4) because requiring a full ATR of breakout above the mid
filters out the chop-region false starts.
* K_STOP = 2.5 ATR: the whole K_STOP 2.0-3.5 strip at K_ENTRY=1.0 is flat-high
(sharpe_min 1.29-1.39, DD 0.22-0.28); 2.5 is the interior balance.
* TARGET_VOL is a pure PnL/DD dial with FLAT Sharpe (~1.39 across 0.20-0.30): 0.20 ->
pnl 1.75/DD 0.16 ... 0.30 -> pnl 3.23/DD 0.23 ... 0.40 -> pnl 4.81/DD 0.29. 0.30 is
the balanced cell. VOL_WIN=30 is interior and best on Sharpe (1.39 vs 1.28 at 60).
LEV_CAP=1.0 (never lever past fully invested) preserves the de-risking benefit.
Train (combined A&B): pnl_mean ~3.23, maxdd_worst ~0.23, sharpe_min ~1.39.
Honest note: this is trend-following, not alpha — its value is turning a high-PnL /
~77-79%-DD uptrend into comparable PnL at ~23% drawdown (DD cut ~3.4x). The scale-down
twist buys a slightly lower DD and steadier equity than a binary ATR breakout would, at
the cost of leaving some upside on the table in the very strongest legs (the position is
rarely pinned at 1.0). The short side was not pursued: on these up-trending curves it is
value-destroying whipsaw, the same finding as the sibling breakout angles.
"""
import numpy as np
import pandas as pd
import blindlib as bl
N_EMA = 20 # ATR-channel mid-line EMA span
N_ATR = 20 # ATR window (channel half-width AND trailing-stop unit)
K_ENTRY = 1.0 # entry: close > mid + K_ENTRY*ATR -> start the ride (ridge value)
K_STOP = 2.5 # trailing stop distance in ATR (Chandelier) -> also the scale ruler
TARGET_VOL = 0.30 # PnL/DD dial; Sharpe flat across 0.20-0.30
VOL_WIN_DAYS = 30
LEV_CAP = 1.0
def _atr_ride_exposure(df):
"""Long/flat exposure in [0,1]: 0 when out of the ride; while in the ride, the value
is the ATR 'stop room' (cushion above the trailing stop, in [0,1]) so the position
scales DOWN smoothly on adverse moves and goes flat when the stop breaks."""
c = df["close"].values.astype(float)
n = len(c)
mid = pd.Series(bl.ema(c, N_EMA)).shift(1).values # EMA built strictly <= i-1
atr = pd.Series(bl.atr(df, N_ATR)).shift(1).values # ATR built strictly <= i-1
expo = np.zeros(n)
in_ride = False
peak = -np.inf # highest close since entry (drives the ratcheting stop)
for i in range(n):
m, a = mid[i], atr[i]
if not (np.isfinite(m) and np.isfinite(a) and a > 0):
continue
if not in_ride:
# entry: close pierces the upper ATR channel (full ATR above the mid)
if c[i] > m + K_ENTRY * a:
in_ride = True
peak = c[i]
if in_ride:
peak = max(peak, c[i])
stop = peak - K_STOP * a # Chandelier trailing stop (ratchets via peak)
if c[i] <= stop:
in_ride = False # stop broken -> ride over, flat
expo[i] = 0.0
peak = -np.inf
else:
# SCALE DOWN on adverse moves: cushion above the stop, normalised to [0,1].
room = (c[i] - stop) / (K_STOP * a)
expo[i] = float(np.clip(room, 0.0, 1.0))
return expo
def signal(df):
expo = _atr_ride_exposure(df) # long/flat in [0,1], already scaled by stop room
pos = bl.vol_target(expo, df, target_vol=TARGET_VOL,
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
return np.clip(np.nan_to_num(pos, nan=0.0), 0.0, LEV_CAP)
@@ -0,0 +1,75 @@
"""agent_22_dd_derisk — ANGLE: drawdown-state de-risking overlay (family=vol, slug=dd_derisk).
Idea (assigned angle):
Ride the up-trend, but CUT exposure as the asset's running drawdown deepens, and
RE-RISK as it recovers back toward the peak. On these two structurally up-trending
curves every large decline begins as a drawdown below the running peak; trimming
exposure while the curve bleeds below its high mechanically pulls the book light
through the worst legs and re-arms it once the high is reclaimed.
Construction (all causal / online):
* dd[i] = close[i] / running_peak(close[0..i]) - 1 in (-1, 0] -> the LIVE drawdown.
* |dd| is lightly EWMA-smoothed (span DD_SMOOTH) so the re-risk on the snap-back is
not whipsawed by single-bar wicks; the smoother is causal (ewm, adjust=False).
* A smooth de-risk multiplier maps the (smoothed) drawdown to a [W_FLOOR, 1] scale:
scale = clip( 1 - (|dd_smooth| / DD_REF) ** P , W_FLOOR, 1 )
Shallow dd -> ~full size; as |dd| approaches DD_REF the scale is bled to W_FLOOR.
W_FLOOR>0 keeps a small core position through the deep regime (re-arms instantly on
recovery) rather than fully exiting and missing the V-bottom.
* This dd-scaled LONG is then vol-targeted (inverse realized vol, slow VOL_WIN_D
window). A crash is also a vol spike, so inverse-vol sizing de-risks the same legs
from the other side — the two de-risk mechanisms stack. Long/flat only: both curves
are sharply V-bottomed, so shorting the recoveries is whipsaw; a de-risk goes toward
a light long, never short.
Why no explicit trend filter: tested, it HURTS the risk-adjusted result here. The
drawdown overlay already does the de-risking a trend gate would do, but smoothly and
without the gate's whipsaw round-trips at the V-bottoms. Pure dd-derisk + slow
inverse-vol gives the better Sharpe.
CAUSAL: running peak (left-to-right accumulate), drawdown, the EWMA smoother and the
realized-vol window at i all use rows <= i only. The evaluator shifts the position (held
during bar i+1). No future rows, no centered window, no global fit -> causality_ok=true
(verified: max_diff 0.0).
Tuning (split='train' only, A & B equal weight; buy&hold ref: A Sh0.89/DD0.77,
B Sh1.16/DD0.79). The de-risk SHAPE (DD_REF / P / W_FLOOR / DD_SMOOTH) sets the Sharpe;
TARGET_VOL is a clean DD/PnL dial (Sharpe flat ~1.10-1.14 across 0.25..0.50). Chosen cell
is interior on every axis with a flat plateau (Sharpe 1.08..1.15, DD 0.19..0.24):
DD_REF=0.20 P=1.0 W_FLOOR=0.20 DD_SMOOTH=4 VOL_WIN_D=120 TARGET_VOL=0.40
-> train combined: pnl_mean ~1.63, maxdd_worst ~0.22, sharpe_min ~1.14.
Honest read: this is a DEFENSIVE long-only book, not alpha. Its value is the DRAWDOWN —
~0.22 vs ~0.77-0.79 buy&hold (a ~3.5x cut) at comparable risk-adjusted PnL. Because it
never shorts, its Sharpe ceiling (~1.1-1.2) is set by the absence of a direction call: it
can avoid sizing into the big declines but cannot profit from them. That is the inherent
limit of the de-risk-overlay angle on these curves.
"""
import numpy as np
import pandas as pd
import blindlib as bl
DD_REF = 0.20 # drawdown (fraction) at which the de-risk multiplier hits the floor
P = 1.0 # de-risk curvature (linear here; >1 keeps near-full on shallow dips)
W_FLOOR = 0.20 # minimum exposure scale in the deep regime (keeps a re-armable core)
DD_SMOOTH = 4 # EWMA span on |drawdown| -> de-whipsaw the re-risk on snap-backs
VOL_WIN_D = 120 # slow trailing realized-vol horizon (days); stable, low turnover
TARGET_VOL = 0.40 # DD/PnL dial; Sharpe flat across 0.25..0.50 -> picked for PnL/DD balance
LEV_CAP = 1.0 # long-only, never lever past fully invested -> preserves the DD cut
def _drawdown_scale(c: np.ndarray) -> np.ndarray:
"""Causal de-risk multiplier in [W_FLOOR, 1] driven by the live drawdown."""
peak = np.maximum.accumulate(c) # running peak over rows <= i (causal)
dd = c / peak - 1.0 # (-1, 0]
ad = np.abs(dd)
ad = pd.Series(ad).ewm(span=DD_SMOOTH, adjust=False).mean().values # causal smoother
depth = ad / DD_REF
return np.clip(1.0 - depth ** P, W_FLOOR, 1.0)
def signal(df):
c = df["close"].values.astype(float)
scale = _drawdown_scale(c) # long/flat de-risk exposure in [W_FLOOR, 1]
pos = bl.vol_target(scale, df, target_vol=TARGET_VOL,
vol_win_days=VOL_WIN_D, leverage_cap=LEV_CAP)
return np.clip(np.nan_to_num(pos, nan=0.0), 0.0, LEV_CAP)
@@ -0,0 +1,120 @@
"""agent_23_vol_of_vol — ANGLE [family=vol, slug=vol_of_vol].
Vol-of-vol gate: trade the trend ONLY when volatility itself is STABLE; flatten when
vol is spiking erratically.
The idea (distinct from a plain vol-LEVEL gate): what kills a trend-follower is not
high volatility per se — a calm, persistently-high-vol grind still trends — but the
INSTABILITY of the vol regime. When realized volatility itself starts jumping around
(vol-of-vol spikes), the market is in a disorderly, regime-shifting state where trends
V-reverse and whipsaw, and where the violent declines are born. So:
* Compute short-window realized vol rv[i] (the "current" vol).
* Compute VOL-OF-VOL vov[i] = trailing std of the LOG-CHANGES of rv (a scale-free
measure of how erratically vol is moving — robust to the absolute vol level, which
differs across the two curves).
* Rank vov against its EXPANDING percentile (causal, self-calibrating threshold — no
magic vol-of-vol level, adapts as the series evolves, never peeks at the full sample).
* STABLE-VOL regime (vov-rank <= PCTL): TREND-FOLLOW the prevailing multi-horizon
TSMOM sign blend (~1/2/4 months).
* ERRATIC-VOL regime (vov-rank > PCTL): stand aside (FLAT) — refuse directional
exposure where vol is spiking erratically.
Final size is a trailing vol-target so exposure also shrinks into raw vol inside the
stable regime.
CAUSAL: rv uses a trailing window; the log-change std uses a trailing window; the
percentile rank is EXPANDING (only past bars); each TSMOM sign uses close[i]/close[i-H];
vol_target uses a trailing realized-vol window. No look-ahead, no centered windows, no
global fit. Verified by causality_ok (max_diff 0.0).
Tuned ONLY on split='train' (Series A & B, equal weight). A coarse->fine sweep found a
WIDE plateau and one load-bearing insight: only the TOP of the vol-of-vol distribution
hurts. Tight gates (PCTL ~0.55-0.65) are too restrictive — they sit out the early, still-
orderly part of the trend legs and DROP the Sharpe to ~0.83. Flattening only the most
ERRATIC ~20% (PCTL ~0.80) is what lifts both Sharpe and PnL. Around the chosen cell the
plateau is flat: VOV_WIN in [30..50] -> sharpe_min 1.12..1.16, PCTL in [0.76..0.84] ->
1.12..1.17, all at DD ~0.19-0.23. The chosen cell is interior on every axis:
RV_WIN=30, VOV_WIN=40, PCTL=0.80, HORIZONS=(25,60,120), TARGET_VOL=0.22, VOL_WIN=45
-> train combined: pnl_mean ~1.87, maxdd_worst ~0.20, sharpe_min ~1.16.
Honest notes:
* The erratic-vol leg is LONG-FLAT (not contrarian) — refusing exposure where vol is
unstable, not betting against the move. The value is RISK-ADJUSTED: comparable PnL
at ~4x less drawdown than buy&hold (~0.77-0.79 DD on these curves), by sitting out
the disorderly regimes where the violent declines are born.
* TARGET_VOL is a pure DD/PnL dial (Sharpe flat ~1.16 across 0.18..0.26); LEV_CAP does
not bind (the vol-target weight sits below 1.0). 0.22 is a balanced cell.
* This gate measures the STABILITY of vol (vol-of-vol), distinct from a vol-LEVEL gate:
a calm persistently-HIGH-vol grind still trends and is kept; it is the erratic,
regime-shifting vol that is flattened. The Sharpe ceiling (~1.16) is set by the
absence of a short leg — it avoids the chop but cannot profit from the declines.
"""
import numpy as np
import pandas as pd
import blindlib as bl
RV_WIN = 30 # short realized-vol window ("current" vol)
VOV_WIN = 40 # trailing window for vol-of-vol (std of log-changes of rv)
PCTL = 0.80 # expanding vov-percentile gate: trend-follow when rank <= this
HORIZONS = (25, 60, 120) # multi-horizon TSMOM sign blend (~1/2/4 months of daily bars)
TARGET_VOL = 0.22
VOL_WIN_DAYS = 45
LEV_CAP = 1.5
MIN_HIST = 60 # warmup before the expanding percentile is trusted
def _expanding_pctl_rank(x: np.ndarray, min_hist: int) -> np.ndarray:
"""rank[i] = fraction of finite x[0..i] that are <= x[i] (causal, expanding).
NaN until `min_hist` finite values have accumulated."""
n = len(x)
rank = np.full(n, np.nan)
seen: list[float] = []
for i in range(n):
v = x[i]
if np.isfinite(v):
seen.append(v)
if len(seen) >= min_hist:
rank[i] = float(np.mean(np.asarray(seen) <= v))
return rank
def _tsmom_sign(c: np.ndarray, h: int) -> np.ndarray:
"""Sign of the past-h-bar return, causal. 0 for i < h."""
out = np.zeros(len(c))
if h < len(c):
out[h:] = np.sign(c[h:] / c[:-h] - 1.0)
return out
def _vol_of_vol(rv: np.ndarray, win: int) -> np.ndarray:
"""vol-of-vol: trailing std of the log-changes of realized vol (scale-free)."""
rv_s = pd.Series(rv)
logrv = np.log(rv_s.where(rv_s > 0))
dlog = logrv.diff()
return dlog.rolling(win, min_periods=max(5, win // 2)).std().values
def signal(df):
c = df["close"].values.astype(float)
bpy = bl.bars_per_day(df) * 365.25
# 1) short-window realized vol, then its vol-of-vol and EXPANDING percentile (causal).
rv = bl.realized_vol(bl.simple_returns(c), RV_WIN, bpy)
vov = _vol_of_vol(rv, VOV_WIN)
rank = _expanding_pctl_rank(vov, MIN_HIST)
stable = np.isfinite(rank) & (rank <= PCTL) # the STABLE-VOL regime we trade
# 2) multi-horizon TSMOM sign blend -> graded direction in [-1, +1] (causal).
sig = np.zeros(len(c))
for h in HORIZONS:
sig += _tsmom_sign(c, h)
sig /= len(HORIZONS)
# 3) vol-of-vol gate: trend-follow ONLY when vol is stable, else flat.
raw = np.where(stable, sig, 0.0)
# 4) causal vol-targeting (shrinks size into vol -> caps DD).
pos = bl.vol_target(raw, df, target_vol=TARGET_VOL,
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
@@ -0,0 +1,109 @@
"""agent_24_hhll — ANGLE: swing-structure trend (higher-high/higher-low vs lower-low/lower-high).
Idea (assigned angle, family=struct / slug=hhll):
Read the curve the way a price-action trader reads market STRUCTURE. Find the swing pivots
(fractal turning points) with a rolling left/right window, then track the sequence of
confirmed swing HIGHs and swing LOWs:
* UPTREND = a higher-high AND a higher-low (last swing high > prior swing high AND
last swing low > prior swing low) -> go LONG.
* STRUCTURE BREAK DOWN = a lower-low (last swing low < prior swing low, a confirmed
market-structure-break to the downside) -> exit to FLAT.
* Otherwise -> persist the prior state (an uptrend stays innocent through pullbacks /
single lower-highs until a swing low is actually undercut).
A slow-MA gate (price must still be above its 150-bar mean) acts as the trend-still-intact
confirmation of the structural read — an uptrend whose price has fallen below its own mean
has structurally rolled over. The position is vol-targeted, so the book shrinks into the
vol spikes that mark every real structure break, which is what caps the drawdown.
CAUSALITY — the crux of any swing/pivot signal:
A swing pivot centred at bar k is only KNOWABLE `RIGHT` bars later: you need the right-hand
window k+1..k+RIGHT to assert k was a local extreme. So at bar i we may use only pivots
whose confirmation bar k+RIGHT <= i. `_hhll_state` does a pure forward scan: at each i it
confirms the pivot centred at k=i-RIGHT (its full window k-LEFT..k+RIGHT is complete and all
indices <= i) and appends it to the running swing history. The HH/HL/LL comparison and the
MA gate at i use only data <= i. No future row ever enters the state. causality_ok -> true.
LONG/FLAT, not stop-and-reverse (tuned honestly on split='train', A & B equal weight):
Both curves trend up hard. A symmetric SHORT on every lower-low / lower-high whipsaws on
V-bottoms and destroys risk-adjusted value (sweep: short legs drop sharpe_min from ~1.2 to
~0). The structural reading is kept but the down leg is FLAT, not short. This is the right
call for a long-biased instrument: ride confirmed up-structure, stand aside when it breaks.
Tuned params — a broad plateau on train (A & B), NOT an isolated peak. sharpe_min holds
~0.95-1.17 across LR 4, MA 120..180, vol-target 0.20..0.30, vol_win 20..60 (sweeps in dev
notes). LR=4 is the peak of the pivot-window dimension; MA and target_vol move PnL/DD but not
the risk-adjusted shape. Chosen centre of the plateau:
LEFT=RIGHT=4 (pivot half-window), MA_FILT=150 (trend-intact gate), target_vol 0.25 / 30d /
cap 1 -> train combined: pnl_mean ~2.13, maxdd_worst ~0.28, sharpe_min ~1.17.
Honest note: like every structure/trend rule on a strongly up-trending pair this is
trend-following, not alpha. Ablation is candid — a plain "always-long above the 150-MA" gate
scores a slightly HIGHER train sharpe (~1.34) than this structural overlay, because the
HH/HL/LL logic stands aside during some pullbacks that later resume. The structure's value is
that it is a genuinely different, pivot-based read of the SAME trend that converts a high-PnL
/ ~77-79%-DD buy&hold into comparable PnL at ~28% drawdown (DD cut ~2.7x), with only ~33%
time in market. It is the assigned angle implemented faithfully — not a momentum rule wearing
a structure costume.
"""
import numpy as np
import blindlib as bl
LEFT = 4 # pivot left half-window
RIGHT = 4 # pivot right half-window (confirmation lag)
MA_FILT = 150 # trend-still-intact gate: price must be above this SMA to stay long
TARGET_VOL = 0.25
VOL_WIN_DAYS = 30
LEV_CAP = 1.0
def _hhll_state(high, low, close, left, right, ma_filt):
"""Causal HH/HL/LL market-structure trend state in {0, 1} (long/flat).
Forward scan: at bar i confirm the pivot centred at k=i-right (window k-left..k+right,
all <= i), update the running swing-high / swing-low history, then:
* higher-high AND higher-low -> long (clean up-structure)
* lower-low (structure break) -> flat
* else -> hold prior state
A final SMA gate forces flat if price is below its slow mean (trend rolled over).
Returns a float direction array, len(high); each value uses only data <= i.
"""
n = len(high)
state = np.zeros(n)
sh = [] # confirmed swing-high prices (chronological)
sl = [] # confirmed swing-low prices
s = 0.0
sma_c = bl.sma(close, ma_filt) if ma_filt else None
for i in range(n):
k = i - right
if k - left >= 0:
seg_h = high[k - left:i + 1] # high[k-left .. k+right], all indices <= i
seg_l = low[k - left:i + 1]
if high[k] >= seg_h.max(): # weak local max -> swing high
sh.append(high[k])
if low[k] <= seg_l.min(): # local min -> swing low
sl.append(low[k])
if len(sh) >= 2 and len(sl) >= 2:
hh = sh[-1] > sh[-2] # higher high
hl = sl[-1] > sl[-2] # higher low
ll = sl[-1] < sl[-2] # lower low = structure break down
if hh and hl:
s = 1.0
elif ll:
s = 0.0
# else: keep prior state (uptrend survives a single lower-high / pullback)
ss = s
if ma_filt and s > 0.0 and not (close[i] > sma_c[i]):
ss = 0.0 # trend-intact gate (causal)
state[i] = ss
return state
def signal(df):
high = df["high"].values.astype(float)
low = df["low"].values.astype(float)
close = df["close"].values.astype(float)
direction = _hhll_state(high, low, close, LEFT, RIGHT, MA_FILT)
pos = bl.vol_target(direction, df, target_vol=TARGET_VOL,
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
@@ -0,0 +1,91 @@
"""agent_25_channel_pos — ANGLE [struct/channel_pos]: position WITHIN the Donchian channel.
Idea (assigned angle): instead of a binary breakout AT the channel edge, measure WHERE the
close sits inside the rolling Donchian channel [lo, hi] as a continuous fraction
chpos = (close - lo) / (hi - lo) in [0, 1] (0.5 = mid-channel).
Then take a directional position only when location AND trend AGREE:
* LONG when chpos is in the UPPER third (>= UP_TH) AND the channel/price slope is UP,
* SHORT when chpos is in the LOWER third (<= LO_TH) AND the slope is DOWN,
* FLAT in the middle band or when slope disagrees with location.
The "slope" filter is what makes the angle anticipatory rather than a reversal: riding the
upper third while the channel is still pushing up is a continuation read; the lower-third +
down-slope short tries to catch the persistent declines (the big drawdowns the benchmark eats).
WHY a slope gate (honest tuning result):
Channel-position WITHOUT a slope gate is a mean-reversion read (buy low-in-channel) and
on these trending curves it bleeds — it fights the trend and the upper third without a
trend filter chops on every pullback. Requiring location AND slope to agree turns it into
a trend-confirmation read that holds longs through the up-leg and only shorts confirmed
down-legs. The slope is the prior-W channel-midpoint change (causal).
Sizing: the agreed direction (+1/-1/0) is vol-targeted (TP01-style, causal realized vol) so
size shrinks into vol spikes (= crashes) -> caps drawdown.
Causality: bl.donchian shifts the rolling hi/lo by one bar, so the channel at i is built from
bars STRICTLY before i. chpos[i], the slope (a backward difference of a causal EMA of close),
and the vol scaling all use only data <= i. The forward scan keeps no future state. The
evaluator then HOLDS the position during bar i+1. causality_ok -> true.
WHY the short leg is sized 0.30 (honest tuning result):
A full-size (-1.0) short bled on these up-trending curves (combined Sharpe_min 1.06, DD 0.30).
Shrinking the short leg monotonically improved risk-adjusted return; long/flat alone was best
on raw PnL/Sharpe but had a slightly fatter DD (0.256). The chosen short=0.30 keeps a genuine
lower-third+down-slope SHORT (the angle is intact) and TRIMS the drawdown (0.256 -> 0.229)
at ~no PnL cost. So the angle's short leg earns its place, just at a modest size.
Plateau (tuned on train only): broad and well-behaved around DON 35-45 / UP-LO 0.62-0.66 /
SLOPE_WIN 15-20 / short 0.15-0.35 (Sharpe_min ~1.3-1.4 throughout, not an isolated peak).
FINAL train (combined A&B): pnl_mean ~4.06, maxdd_worst ~0.229, sharpe_min ~1.34, sharpe_mean ~1.40.
Per-series: A pnl 4.88 / DD 0.226 / Sh 1.45 ; B pnl 3.22 / DD 0.193 / Sh 1.33. Turnover ~14/yr.
causality.ok = true (max_diff 0). Honest note: this is a trend-confirmation read dressed as a
channel-position rule (the slope gate makes it ride the trend, not fade it); its value is
comparable PnL to buy&hold at ~1/3 of the drawdown, NOT independent alpha.
"""
import numpy as np
import blindlib as bl
DON_WIN = 40 # Donchian window for the channel
UP_TH = 0.62 # upper-band threshold on chpos (>=) -> "upper third" (location)
LO_TH = 0.38 # lower-band threshold on chpos (<=) -> "lower third" (location)
SLOPE_WIN = 20 # bars over which we measure the price slope (trend gate)
SLOPE_EPS = 0.0 # min |slope| to count as up/down (0 = any non-zero sign)
SHORT_SIZE = 0.30 # short-leg size (lower third + down-slope). <1 by tuning: the curves
# trend up, so a full-size short bleeds; a modest short still TRIMS DD.
TARGET_VOL = 0.30
VOL_WIN_DAYS = 30
LEV_CAP = 1.0
def signal(df):
c = df["close"].values.astype(float)
n = len(c)
hi, lo = bl.donchian(df, DON_WIN) # prior-DON_WIN hi/lo (shifted, causal)
width = hi - lo
# continuous position within the channel in [0,1]; mid (0.5) where channel undefined.
with np.errstate(invalid="ignore", divide="ignore"):
chpos = (c - lo) / width
chpos = np.where(np.isfinite(chpos) & (width > 0), chpos, 0.5)
chpos = np.clip(chpos, 0.0, 1.0)
# causal slope: change of a smoothed close over SLOPE_WIN bars, normalized by price.
sm = bl.ema(c, SLOPE_WIN)
slope = np.zeros(n)
slope[SLOPE_WIN:] = (sm[SLOPE_WIN:] - sm[:-SLOPE_WIN]) / np.maximum(sm[:-SLOPE_WIN], 1e-9)
up_loc = chpos >= UP_TH
dn_loc = chpos <= LO_TH
up_slope = slope > SLOPE_EPS
dn_slope = slope < -SLOPE_EPS
direction = np.zeros(n)
direction[up_loc & up_slope] = 1.0 # upper third + rising -> long
direction[dn_loc & dn_slope] = -SHORT_SIZE # lower third + falling -> (small) short
# warmup: no channel yet -> flat
direction[:DON_WIN] = 0.0
pos = bl.vol_target(direction, df, target_vol=TARGET_VOL,
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
@@ -0,0 +1,134 @@
"""Agent 26 — Stochastic oscillator reversion + cross, trend-gated (family=osc, slug=stoch).
The angle (assigned): a rolling Stochastic oscillator (%K / %D). %K = where the close sits
in its rolling [min(low), max(high)] window (0..100); %D = a short SMA of %K (the signal
line). Trade the REVERSION (%K leaving an oversold extreme) timed by the %K-vs-%D CROSS,
GATED by a longer trend filter. Tune the windows.
Reading the train curves first (both A and B, split='train'): they trend UP very hard
(A 100->792, B 100->2400 over the window). UNLIKE RSI — which in these up-curves never
dips below ~40 so textbook 30/70 is dead — the Stochastic %K is normalized against its
OWN rolling high/low, so it sweeps the FULL 0..100 range even inside the bull: %K<20
~12-14% of bars, %K>80 ~24-27% of bars (measured). That is exactly the structure a
stochastic reversion rule needs, so the angle is genuinely playable here, but it still
has to be REGIME-AWARE because the curves drift up:
* In an UPTREND (close above a long SMA) %K oversold (<LO) is a BUY-THE-DIP setup, and we
require %K to CROSS BACK UP through its signal line %D — the standard stochastic long
trigger — before going LONG. That waits for the dip to actually TURN (anticipating the
bounce) instead of knife-catching while %K is still falling. We HOLD the long
(hysteresis) until %K recovers into EXIT, then go flat. We do NOT short %K>80 in an
uptrend — overbought in a bull keeps running (that is momentum, not reversion).
* In a DOWNTREND (close below the long SMA) the symmetry returns: %K overbought (>80) with
a %K cross DOWN through %D is a reversion SHORT (rips fade). %K<LO we stand flat (don't
knife-catch long under a downtrend). The short side is down-weighted (SHORT_W) because
the drift is up; on train it is marginal (see HONEST NOTE).
WHY THE CROSS MATTERS (the "anticipation" the angle asks for): entering the instant %K
prints <LO is usually early — %K is still falling. Waiting for the %K/%D up-cross times the
turn, which on train is the difference between a coin-flip dip rule and a positive one: with
the cross the dip-long sits at ~9-12% DD with a clean positive Sharpe; without it the same
thresholds bleed. The cross also cuts whipsaw turnover (~5-6 round-trips/yr, fee-cheap).
The trend gate does two jobs: it picks WHICH side of the oscillator is reversion (buy dips
in up-trend / sell rips in down-trend) and it suppresses the side that fights the drift.
Sizing is smooth (deeper oversold -> bigger appetite, floored at BASE while holding) then
VOL-TARGETED so the two curves are risk-comparable and exposure shrinks into vol spikes
(crashes are vol spikes) — that is what bounds the drawdown. Note the leverage cap never
binds here (post-vol-target appetite stays <=1), so the edge does NOT rely on leverage.
HONEST NOTE (negative findings kept): (1) the downtrend short side is essentially free but
adds nothing on train — SHORT_W=0.5 gives sharpe_min 0.51 vs 0.53 at SHORT_W=0; it is kept
small to honor the bidirectional angle, not because it earns. (2) A continuous always-on
oscillator weighting (no flat state) was tried and pushed time-in-market to ~99% and DD to
0.20-0.37 — it degenerated into buy-and-hold; the hysteresis flat state is what keeps the
DD at ~12%. (3) In a market that trends this hard, even a cross-gated dip-buy is PARTLY
trend participation (the dips it buys recover and it rides them). The genuine reversion
content is the oversold-entry / cross-timed turn / overbought-exit cycle plus the DD control
from the trend gate + vol-target. Result: an honest, MODEST combined train Sharpe ~0.5 at
~12% DD — a fraction of buy&hold's huge PnL but ~6x less drawdown (it anticipates the dip
rather than just holding the asset through every crash).
CAUSAL: %K uses trailing rolling max(high)/min(low) (<= i); %D is a trailing SMA of %K; the
cross compares (%K-%D) at i vs i-1 (past only); the hold-state is a forward cumulative pass
over PAST bars only; the SMA trend filter and vol_target use trailing data. No shift(-k), no
centered windows, no global fit. Verified by causality_ok (max_diff 0.0).
Tuning (train only, combined A&B; coarse->fine sweep + plateau check). The chosen cell sits
on a broad plateau (K in [14..20], LO in [40..50], EXIT in [55..65], D in [3..5], TREND_WIN
in [150..200] all hold sharpe_min ~0.37..0.53 at DD ~0.09..0.12 — a plateau, not a spike):
K_WIN=20, D_WIN=5, LO=50, EXIT=55, TREND_WIN=150
SHORT_W=0.5, BASE=0.7, TARGET_VOL=0.25, VOL_WIN_DAYS=35, LEV_CAP=1.5
-> train combined: pnl_mean ~0.17, maxdd_worst ~0.12, sharpe_min ~0.51
"""
import numpy as np
import pandas as pd
import blindlib as bl
K_WIN = 20 # %K lookback (rolling high/low window). 20 > textbook 14 for these trends.
D_WIN = 5 # %D = SMA(%K, D_WIN): the signal line the %K crosses.
LO = 50.0 # oversold threshold below which a %K/%D up-cross is a dip-long entry.
EXIT = 55.0 # dip-long HELD until %K recovers past EXIT (hysteresis entry/exit pair).
TREND_WIN = 150 # long SMA: above = uptrend (buy dips), below = downtrend (sell rips).
SHORT_W = 0.5 # weight on the downtrend reversion-short; marginal (see HONEST NOTE).
BASE = 0.7 # base long size while holding a dip (scaled up if %K still oversold).
TARGET_VOL = 0.25
VOL_WIN_DAYS = 35
LEV_CAP = 1.5
def _stoch(df, k_win, d_win):
"""Causal Stochastic oscillator. %K[i] uses high/low/close over the trailing
k_win bars (<= i); %D[i] = SMA(%K, d_win) (trailing). No look-ahead."""
h = df["high"].values.astype(float)
l = df["low"].values.astype(float)
c = df["close"].values.astype(float)
hh = pd.Series(h).rolling(k_win, min_periods=1).max().values
ll = pd.Series(l).rolling(k_win, min_periods=1).min().values
rng = hh - ll
k = np.where(rng > 1e-12, (c - ll) / rng * 100.0, 50.0)
d = bl.sma(k, d_win)
return k, d
def signal(df):
c = df["close"].values.astype(float)
n = len(c)
k, d = _stoch(df, K_WIN, D_WIN)
trend_up = c > bl.sma(c, TREND_WIN) # causal trailing SMA trend gate
# --- %K/%D crosses (past-only: compares i vs i-1) ---
kd = k - d
kd_prev = np.concatenate(([0.0], kd[:-1]))
cross_up = (kd > 0) & (kd_prev <= 0) # %K turns up through its signal line
cross_dn = (kd < 0) & (kd_prev >= 0) # %K turns down through its signal line
# --- smooth reversion appetite from %K (further past threshold -> bigger) ---
long_app = np.clip((LO - k) / LO, 0.0, 1.0) # oversold depth -> long appetite
short_app = np.clip((k - 80.0) / 20.0, 0.0, 1.0) # overbought depth -> short appetite
# --- trend-gated stochastic reversion with cross-triggered entry + hysteresis ---
# Forward pass is PURE PAST-ONLY: in_long at bar i depends only on bars <= i.
held = np.zeros(n)
in_long = False
for i in range(n):
if in_long:
# exit the held dip-long when trend breaks down OR %K has recovered past EXIT
if (not trend_up[i]) or (k[i] >= EXIT):
in_long = False
else:
# enter a dip-long in an uptrend when %K is oversold AND turns up through %D
if trend_up[i] and (k[i] < LO) and cross_up[i]:
in_long = True
if in_long:
held[i] = max(BASE, long_app[i]) # ride the recovery, bigger if still oversold
else:
# downtrend reversion-short: overbought AND %K turning down through %D
if (not trend_up[i]) and (k[i] > 80.0) and cross_dn[i]:
held[i] = -SHORT_W * short_app[i]
else:
held[i] = 0.0
pos = bl.vol_target(held, df, target_vol=TARGET_VOL,
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
@@ -0,0 +1,68 @@
"""agent_27_dpo — Detrended Price Oscillator (cycle phase around a LAGGED MA).
ANGLE [family=osc, slug=dpo]: detrend price by subtracting a moving average that we
DELAY (lag) so the oscillator measures where price sits in its cycle relative to a
recent trend baseline. Trade the cycle phase — causal only.
Classic DPO is price[i] - SMA(n)[i - (n/2 + 1)]. The textbook centers that lag; here we
keep the displacement STRICTLY BACKWARD (the MA value comes from ~n/2 bars ago, fully in
the past), so the oscillator is causal/online and deployable.
What the train data says (tuned on split='train' only):
dpo = (price - lagged_baseline) / vol(gap) is a z-like CYCLE PHASE around zero.
Bucketing dpo vs the NEXT-bar return showed a clean MONOTONIC relationship: the higher
the detrended oscillator (price above its lagged baseline = cycle UP-phase), the higher
the next return; deep-negative dpo (cycle down-phase) precedes flat/negative returns.
So on these series the cycle is CONTINUATION, not reversion -> we FOLLOW the phase
(long the up-phase, flat/short the down-phase), confirmed by a slow trend gate, and
size with vol-targeting. Result on train: positive PnL at ~19% worst DD vs buy&hold's
~78% DD — anticipating the move means staying out of (or short) the down-phase.
Config tuned on train (period=30 / trendwin=200 / scale=1.5 / wc=0.6 / ema=2 / tv=0.18):
plateau-robust across period 30, trend 150-200, scale 1.5-2.0, cycle weight 0.5-0.8.
"""
import numpy as np
import blindlib as bl
# --- tuned on split='train' only ------------------------------------------
PERIOD = 30 # DPO moving-average period
LAG = PERIOD // 2 + 1 # textbook DPO displacement, kept strictly backward (causal)
TREND_WIN = 200 # slow-trend confirmation window
SCALE = 1.5 # tanh softness of the cycle phase
W_CYCLE = 0.6 # blend weight: cycle phase vs slow-trend confirmation
EMA_SMOOTH = 2 # position smoothing (cuts turnover/fees)
TARGET_VOL = 0.18 # annualized vol target
VOL_WIN = 30
LEV_CAP = 1.0
def _dpo_phase(c: np.ndarray) -> np.ndarray:
"""Detrended price oscillator z-phase: (price - LAGGED SMA) / rolling std of gap.
The baseline SMA is delayed by LAG bars, so every value uses only past data."""
n = len(c)
base = bl.sma(c, PERIOD) # causal SMA
base_lag = np.full(n, np.nan)
base_lag[LAG:] = base[:-LAG] # baseline from LAG bars ago (past only)
gap = c - base_lag
gap_vol = bl.rolling_std(gap, PERIOD)
gap_vol = np.where((gap_vol > 0) & np.isfinite(gap_vol), gap_vol, np.nan)
return gap / gap_vol # z-like cycle phase (NaN during warmup)
def signal(df):
c = df["close"].values.astype(float)
# detrended cycle phase (DPO core) — empirically CONTINUATION on these series
z = np.nan_to_num(_dpo_phase(c), nan=0.0)
cycle = np.tanh(z / SCALE) # +1 up-phase, -1 down-phase
# slow-trend confirmation (don't ride the cycle against a strong regime)
trend = c / bl.sma(c, TREND_WIN) - 1.0
follow = np.tanh(np.nan_to_num(trend, nan=0.0) * 6.0)
raw = np.clip(W_CYCLE * cycle + (1.0 - W_CYCLE) * follow, -1.0, 1.0)
raw = bl.ema(raw, EMA_SMOOTH) # smooth -> fewer fee-bleeding flips
pos = bl.vol_target(raw, df, target_vol=TARGET_VOL,
vol_win_days=VOL_WIN, leverage_cap=LEV_CAP)
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
@@ -0,0 +1,145 @@
"""Agent 28 — Williams %R momentum/reversion HYBRID, trend-gated (family=osc, slug=willr).
The angle (assigned): Williams %R momentum/reversion hybrid with a trend gate. Williams %R
is the inverse of the Stochastic %K: %R = -100 * (HH - close) / (HH - LL) over a trailing
window, ranging -100 (close at the window LOW = oversold) .. 0 (close at the window HIGH =
overbought). It measures where the close sits in its own rolling high/low channel, so it is
self-normalizing and sweeps the FULL -100..0 range even inside a bull (measured on train:
%R<-80 ~14% of bars, %R>-20 ~26% of bars). That dual occupancy is what makes a HYBRID
(reversion on one leg + momentum on the other) genuinely playable here.
Reading the train curves first (both A and B, split='train'): they trend UP very hard
(A 100->792, B 100->2400). A pure symmetric reversion ("short every %R>-20") would just
short the bull and bleed; a pure momentum rule rides crashes. The HYBRID + trend gate
resolves this by using %R DIFFERENTLY on each side of a long trend filter:
REVERSION LEG (in an UPTREND, close above a long SMA):
%R dipping into oversold (< OS, e.g. -80) is a BUY-THE-DIP setup. To ANTICIPATE the
bounce instead of knife-catching a still-falling close, we require %R to TURN BACK UP
(cross up through a short signal line = SMA of %R, the standard stochastic-style
trigger). We then HOLD the long (hysteresis) until %R recovers past EXIT, then flat.
This is the reversion half of the hybrid.
MOMENTUM LEG (in an UPTREND): once %R pushes into and STAYS overbought (> OB, e.g. -20),
in a hard bull that is NOT a fade signal — overbought persists and the trend runs. So
instead of shorting it (textbook reversion) we take a SMALLER continuation LONG
(MOM_W). This is the momentum half of the hybrid: %R>-20 in an uptrend = "trend is
strong, stay with it", the opposite trade to what reversion alone would do. This is
the key difference from the pure-reversion stochastic/RSI agents.
DOWNTREND (close below the long SMA): the symmetry returns and %R is read as reversion
again — %R overbought (> OB) with a cross DOWN through its signal line is a reversion
SHORT (rips fade). %R oversold we stand flat (don't knife-catch long under a
downtrend). The short side is down-weighted (SHORT_W) because the drift is up; on
train it is marginal (see HONEST NOTE).
So the gate does three jobs: (1) picks the reversion side (dip-long in up, rip-short in
down), (2) flips the overbought reading from "fade" to "ride" inside the bull (the hybrid),
(3) suppresses the side that fights the drift. Sizing is smooth (deeper extreme -> bigger
appetite, floored at BASE while holding) then VOL-TARGETED so the two curves are
risk-comparable and exposure shrinks into vol spikes (crashes are vol spikes) — that is
what bounds the drawdown. The leverage cap rarely binds, so the edge is NOT leverage.
HONEST NOTE (negative findings kept): (1) The downtrend reversion-short is nearly free but
adds little on train; kept small to honor the bidirectional angle. (2) The momentum
continuation leg (MOM_W) is what distinguishes this from a pure-reversion oscillator — in a
market that trends this hard it earns by riding the overbought regime instead of fading it,
but it ALSO partly degenerates toward trend participation (the honest ceiling for any
direction-on-a-bull rule). The genuine oscillator content is the cross-timed dip entry +
overbought exit cycle plus the DD control from the trend gate + vol-target. (3) A pure
always-on %R weighting (no flat state) degenerated into buy-and-hold (DD blew out); the
hysteresis flat state is what keeps DD modest. Result: an honest, modest combined train
Sharpe at a small DD — a fraction of buy&hold PnL but several-x less drawdown (it
anticipates the dip / rides the strong trend rather than holding through every crash).
CAUSAL: %R uses trailing rolling max(high)/min(low) (<= i); its signal line is a trailing
SMA of %R; the cross compares (%R - sig) at i vs i-1 (past only); the hold-state is a
forward cumulative pass over PAST bars only; the SMA trend filter and vol_target use
trailing data. No shift(-k), no centered windows, no global fit. Verified by causality_ok.
Tuning (train only, combined A&B; coarse->fine sweep + plateau check). The chosen cell sits
on a broad plateau (OB in [-35..-25], MOM_W in [0.3..0.5], SIG_WIN=5, R_WIN in [20..28],
EXIT in [-50..-40], OS=-80, BASE/TVOL/VWD all hold sharpe_min ~1.1..1.29 at DD ~3.3..5.6%
a plateau, not a spike; SHORT_W is nearly free / marginal):
R_WIN=20, SIG_WIN=5, OS=-80, OB=-35, EXIT=-45, TREND_WIN=150
MOM_W=0.4, SHORT_W=0.4, BASE=0.6, TARGET_VOL=0.25, VOL_WIN_DAYS=35, LEV_CAP=1.5
-> train combined: pnl_mean ~0.46, maxdd_worst ~0.045, sharpe_min ~1.22
"""
import numpy as np
import pandas as pd
import blindlib as bl
R_WIN = 20 # %R lookback (rolling high/low window). 20 > textbook 14 for these trends.
SIG_WIN = 5 # signal line = SMA(%R, SIG_WIN): the line %R crosses (stochastic-style trigger).
OS = -80.0 # oversold: %R below this in an uptrend + cross-up = dip-long entry.
OB = -35.0 # overbought: momentum-ride (uptrend) / reversion-short (downtrend) threshold.
EXIT = -45.0 # dip-long HELD until %R recovers past EXIT (hysteresis entry/exit pair).
TREND_WIN = 150 # long SMA: above = uptrend (dips=long, OB=ride), below = downtrend (OB=short).
MOM_W = 0.4 # weight on the uptrend overbought MOMENTUM-continuation long (the hybrid half).
SHORT_W = 0.4 # weight on the downtrend reversion-short; marginal (see HONEST NOTE).
BASE = 0.6 # base long size while holding a dip (scaled up if %R still oversold).
TARGET_VOL = 0.25
VOL_WIN_DAYS = 35
LEV_CAP = 1.5
def _willr(df, r_win, sig_win):
"""Causal Williams %R + its signal line. %R[i] = -100*(HH-close)/(HH-LL) over the
trailing r_win bars (<= i); sig[i] = SMA(%R, sig_win) (trailing). No look-ahead."""
h = df["high"].values.astype(float)
l = df["low"].values.astype(float)
c = df["close"].values.astype(float)
hh = pd.Series(h).rolling(r_win, min_periods=1).max().values
ll = pd.Series(l).rolling(r_win, min_periods=1).min().values
rng = hh - ll
wr = np.where(rng > 1e-12, -100.0 * (hh - c) / rng, -50.0)
sig = bl.sma(wr, sig_win)
return wr, sig
def signal(df):
c = df["close"].values.astype(float)
n = len(c)
wr, sig = _willr(df, R_WIN, SIG_WIN)
trend_up = c > bl.sma(c, TREND_WIN) # causal trailing SMA trend gate
# --- %R / signal-line crosses (past-only: compares i vs i-1) ---
ds = wr - sig
ds_prev = np.concatenate(([0.0], ds[:-1]))
cross_up = (ds > 0) & (ds_prev <= 0) # %R turns up through its signal line
cross_dn = (ds < 0) & (ds_prev >= 0) # %R turns down through its signal line
# --- smooth appetites (further past the extreme -> bigger) ---
# oversold depth: %R from OS down to -100 -> long appetite 0..1
long_app = np.clip((OS - wr) / (100.0 + OS), 0.0, 1.0)
# overbought depth: %R from OB up to 0 -> 0..1 (used by both momentum-long & rev-short)
ob_app = np.clip((wr - OB) / (0.0 - OB), 0.0, 1.0)
# --- trend-gated Williams %R momentum/reversion hybrid with hysteresis ---
# Forward pass is PURE PAST-ONLY: state at bar i depends only on bars <= i.
held = np.zeros(n)
in_long = False
for i in range(n):
if in_long:
# exit the held dip-long when trend breaks down OR %R has recovered past EXIT
if (not trend_up[i]) or (wr[i] >= EXIT):
in_long = False
else:
# enter a dip-long in an uptrend when %R is oversold AND turns up through its line
if trend_up[i] and (wr[i] < OS) and cross_up[i]:
in_long = True
if in_long:
held[i] = max(BASE, long_app[i]) # ride the recovery, bigger if still oversold
elif trend_up[i]:
# MOMENTUM half of the hybrid: overbought in an uptrend = ride the strong trend
held[i] = MOM_W * ob_app[i]
else:
# downtrend reversion-short: overbought AND %R turning down through its line
if (wr[i] > OB) and cross_dn[i]:
held[i] = -SHORT_W * ob_app[i]
else:
held[i] = 0.0
pos = bl.vol_target(held, df, target_vol=TARGET_VOL,
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
@@ -0,0 +1,158 @@
"""Agent 29 — Ridge regression return forecast (family=ml, slug=ridge).
THE ANGLE (assigned): forecast the forward return with a RIDGE regression on lagged
returns + volatility features, refit on an EXPANDING window every ~20 bars, and turn the
forecast into a position. A genuine ML angle (linear model, L2 penalty), NOT a fixed
momentum sign rule — ridge *weights* the lags and lets vol modulate conviction.
WHAT THE TRAIN DATA ACTUALLY SAYS (the honest finding, not the hoped-for one):
* NEXT-BAR return on these curves is unforecastable — the walk-forward forecast's next-bar
hit-rate is ~0.48-0.51 (coin flip). So I forecast a multi-bar FORWARD return (horizon
FWD_H), the autocorrelated/forecastable quantity, instead of bar-to-bar noise.
* The expanding ridge forecast is CONSISTENTLY, mildly *negatively* correlated with the
realized forward return (corr ~ -0.08..-0.22, same sign on BOTH series, ALL horizons).
i.e. on these strongly up-trending curves the model's most-bullish forecasts mark froth
that gives back, and its bearish forecasts precede the recoveries. This is a stable
property across the grid, not one lucky cell.
* SHORTING destroys value here (both raw-sign and inverted-sign books lose once shorts are
allowed — the curves only go up). The only honest edge a weak forecaster has on an
up-trend is WHEN TO HOLD vs. SIT IN CASH.
THE RULE: use the (inverted, given the negative corr) ridge forecast as a LONG-ONLY
conviction — be long when the model is bearish (post-froth recovery), flat when it is
bullish — then vol-target and clip to [0, 1]. Result on train: a book that is in-market only
~16% of the time, tiny drawdown (~0.02 vs 0.77-0.79 buy&hold), Sharpe ~0.83.
CAUSALITY (the whole game):
* Features at row i use ONLY returns up to and including bar i (rows <= i).
* Training TARGET for row j is the return over bar j -> j+FWD_H (needs close[j+FWD_H]).
Sitting at decision-row i we may only train on rows j with j+FWD_H <= i (their targets
are realized as of close[i]). We NEVER include row i's own unrealized target.
* Refit on an EXPANDING window of those realized (X,y) pairs every REFIT_EVERY bars;
coefficients frozen in between. No global fit, no future row touched.
-> Verified by causality_ok (prefix tail matches full-array tail, max_diff 0.0).
TUNING (split='train' only, combined A & B): chosen cell is interior on every axis —
FWD_H 18-25 -> Sharpe ~0.83 flat; alpha 20-100 -> Sharpe ~0.81-0.84 flat;
refit 10-20 -> stable; gain 1.0-2.5 monotone DD/PnL dial. Picked the interior point.
HONEST READ: alpha here is THIN. The forecastability is weak and the win is risk control,
not return generation — a low-exposure, low-DD long-only sleeve, NOT a PnL engine. The
inverted-sign edge is modest and could be regime-specific; the robust, defensible part is
"never short an up-trend; let the forecast tell you when to step out of the way."
"""
import numpy as np
import blindlib as bl
# ---- tuned on split='train' only (interior of a flat plateau) ----
RIDGE_ALPHA = 50.0 # L2 penalty (strong: the lag->return edge is tiny); plateau 20..100
WARMUP = 150 # realized (X,y) pairs required before the first fit
REFIT_EVERY = 20 # expanding-window refit cadence (assigned ~20); stable 10..20
LAGS = (1, 2, 3, 5, 10) # lagged-return features
MOM_WIN = 20 # trailing momentum feature window
VOL_WIN = 20 # trailing realized-vol feature window
FWD_H = 20 # forecast HORIZON (bars). Plateau 18..25. Next-BAR is noise; a
# multi-bar target is the autocorrelated, forecastable quantity.
GAIN = 1.5 # tanh conviction gain on the standardized forecast (DD/PnL dial)
INVERT = True # negative train corr (both series, all H) -> fade the forecast sign
LONG_ONLY = True # shorting an up-trend destroys value -> conviction is long-or-flat
TARGET_VOL = 0.20 # vol-target the directional book
VOL_WIN_DAYS = 30
LEV_CAP = 1.0 # never lever past fully invested -> preserve the DD cut
def _build_features(c):
"""Causal feature matrix X (len(c) rows). Row i uses ONLY data <= i.
Columns: lagged log-returns, trailing momentum, trailing realized vol."""
n = len(c)
lr = np.zeros(n)
lr[1:] = np.log(c[1:] / c[:-1]) # lr[i] = return of bar ending at i (causal)
cols = []
# lagged returns: feature value at i is the return from k bars ago (all <= i)
for k in LAGS:
f = np.zeros(n)
if k < n:
f[k:] = lr[: n - k] # lr shifted back by k -> uses past only
cols.append(f)
# trailing momentum: cumulative log-return over the last MOM_WIN bars (<= i)
mom = np.zeros(n)
csum = np.cumsum(lr)
mom[MOM_WIN:] = csum[MOM_WIN:] - csum[:-MOM_WIN]
cols.append(mom)
# trailing realized vol (std of last VOL_WIN returns, <= i)
vol = np.zeros(n)
for i in range(VOL_WIN, n):
vol[i] = np.std(lr[i - VOL_WIN + 1 : i + 1])
cols.append(vol)
X = np.column_stack(cols)
return X, lr
def _ridge_fit(X, y, alpha):
"""Closed-form ridge with a standardized design + intercept (no sklearn needed,
fully deterministic). Returns (mu, sd, beta0, beta) for prediction."""
mu = X.mean(axis=0)
sd = X.std(axis=0)
sd[sd < 1e-12] = 1.0
Xs = (X - mu) / sd
p = Xs.shape[1]
A = Xs.T @ Xs + alpha * np.eye(p)
b = Xs.T @ (y - y.mean())
beta = np.linalg.solve(A, b)
beta0 = y.mean()
return mu, sd, beta0, beta
def signal(df):
c = df["close"].values.astype(float)
n = len(c)
X, lr = _build_features(c)
# target[j] = cumulative log-return over bar j -> j+FWD_H (needs close[j+FWD_H]);
# known (realized) only as of close[j+FWD_H].
csum = np.cumsum(lr)
target = np.zeros(n)
target[: n - FWD_H] = csum[FWD_H:] - csum[: n - FWD_H]
yhat = np.zeros(n) # forecast of the forward return, decided at close[i]
sig_y = np.ones(n) # scale of recent forecast targets (for standardization)
coef = None # frozen (mu, sd, beta0, beta)
for i in range(n):
# at decision-row i we may train only on rows j whose target is realized, i.e.
# j + FWD_H <= i => j <= i - FWD_H. We NEVER include row i's own (unrealized) target.
first = max(LAGS) + MOM_WIN # earliest row with all features fully populated
last_train = i - FWD_H # target of last_train uses close[i], realized now
ntrain = last_train - first + 1
if ntrain >= WARMUP:
# refit every REFIT_EVERY bars (and on the very first eligible bar)
if coef is None or (i % REFIT_EVERY == 0):
Xtr = X[first : last_train + 1]
ytr = target[first : last_train + 1]
coef = _ridge_fit(Xtr, ytr, RIDGE_ALPHA)
s = np.std(ytr)
sig_y[i] = s if s > 1e-9 else 1.0
else:
sig_y[i] = sig_y[i - 1]
mu, sd, beta0, beta = coef
xi = (X[i] - mu) / sd
yhat[i] = beta0 + xi @ beta
# forecast -> bounded conviction (de-emphasize tiny/noisy forecasts, saturate strong ones)
s = np.where(sig_y > 1e-9, sig_y, 1.0)
direction = np.tanh(GAIN * yhat / s)
direction = np.nan_to_num(direction, nan=0.0)
if INVERT:
direction = -direction # train corr is negative on both series/all H
if LONG_ONLY:
direction = np.clip(direction, 0.0, 1.0) # never short an up-trend (shorts lose here)
# vol-target the conviction so the DRAWDOWN is what we control
pos = bl.vol_target(direction, df, target_vol=TARGET_VOL,
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
if LONG_ONLY:
pos = np.clip(pos, 0.0, LEV_CAP)
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
@@ -0,0 +1,189 @@
"""Agent 30 — Logistic up/down classifier (family=ml, slug=logistic).
THE ANGLE (assigned): a LOGISTIC REGRESSION that classifies "will the forward move be
up or down?" from technical features (momentum at several horizons, trailing realized
vol, RSI), refit on an EXPANDING walk-forward window every ~20 bars, and maps the class
probability p(up) into a position in [-1, +1].
WHY A CLASSIFIER (not a return-regressor): the per-bar *magnitude* of these curves is
dominated by noise — the sign of the forward move is the only thing with any persistence.
A logistic model targets exactly that (a Bernoulli up/down label), and its probability
output is a natural, bounded conviction: p≈0.5 → flat, p far from 0.5 → take the side.
The L2 penalty (C small) keeps the coefficients from chasing the (thin) edge into noise.
CAUSALITY (the whole game):
* Features at row i use ONLY data up to and including bar i (rows <= i): lagged log-
returns, multi-horizon trailing momentum, trailing realized vol, RSI.
* The LABEL for row j is sign of the cumulative return over bar j -> j+FWD_H, which
needs close[j+FWD_H]. So sitting at decision-row i we may train ONLY on rows whose
label is already realized: j + FWD_H <= i => j <= i - FWD_H. Row i's own label is
NEVER used.
* Model is refit on the EXPANDING window of those realized (X, y) pairs at most every
REFIT_EVERY bars; coefficients frozen in between. position[i] = frozen model's
p(up) at row i, mapped to a direction, then vol-targeted.
-> Verified by causality_ok (signal on a prefix must match signal on the full array).
TUNING (split='train' only, combined A & B): C (inverse L2) small (~0.05-0.2) so the
weak edge isn't overfit; FWD_H ~ 5-10 (the forecastable horizon — next-bar sign is a
coin flip); WARMUP ~ 200 realized pairs; conviction = 2*(p-0.5) sharpened by a gain,
then vol-targeted (cap 1.0) so the DRAWDOWN, not the raw PnL, is what we optimise.
HONEST READ: forward-sign forecastability here is weak; the realistic win is a vol-
controlled book that can flip short into declines, giving comparable PnL to long-only
at a much smaller drawdown — the de-risking is the alpha, not a strong classifier.
"""
import warnings
import numpy as np
import blindlib as bl
warnings.filterwarnings("ignore")
try:
from sklearn.linear_model import LogisticRegression
_HAVE_SK = True
except Exception: # pragma: no cover - sklearn expected present
_HAVE_SK = False
# ---- tuned on split='train' only (interior of broad plateaus; see scan below) ----
C_INV = 0.20 # inverse L2 strength (small = strong penalty); flat 0.05-1.0
WARMUP = 200 # realized (X, y) pairs required before the first fit
REFIT_EVERY = 20 # expanding-window refit cadence (assigned ~20)
LAGS = (1, 2, 3, 5) # lagged log-return features
MOM_WINS = (10, 20, 40) # multi-horizon trailing-momentum features
VOL_WIN = 20 # trailing realized-vol feature window
RSI_WIN = 14 # RSI feature window
FWD_H = 15 # label HORIZON: sign of cumulative return over next FWD_H bars.
# next-bar sign is a coin-flip; the multi-bar sign is the
# persistent, classifiable quantity. Plateau FWD 14-18.
DEADBAND = 0.04 # ignore |2p-1| below this (treat as no-conviction -> flat)
GAIN = 3.0 # conviction gain on the centered probability 2*(p-0.5)
SHORT_SCALE = 0.25 # asymmetric book: full long, only PARTIAL short. Both curves
# drift UP, so the classifier's real value is STEPPING ASIDE
# from declines; a full short fights the drift and adds DD.
# 0.25 keeps a genuine (small) short so it stays prob->position.
TARGET_VOL = 0.20 # vol-target the directional book
VOL_WIN_DAYS = 30
LEV_CAP = 1.0 # never lever past fully invested -> preserve the DD cut
def _build_features(c):
"""Causal feature matrix X (len(c) rows). Row i uses ONLY data <= i."""
n = len(c)
lr = np.zeros(n)
lr[1:] = np.log(c[1:] / c[:-1]) # lr[i] = return of bar ending at i (causal)
csum = np.cumsum(lr)
cols = []
# lagged returns: value at i is the return k bars ago (all <= i)
for k in LAGS:
f = np.zeros(n)
if k < n:
f[k:] = lr[: n - k]
cols.append(f)
# multi-horizon trailing momentum: cumulative log-return over last w bars (<= i)
for w in MOM_WINS:
mom = np.zeros(n)
mom[w:] = csum[w:] - csum[:-w]
cols.append(mom)
# trailing realized vol (std of last VOL_WIN returns, <= i)
vol = np.zeros(n)
cs2 = np.cumsum(lr * lr)
for i in range(VOL_WIN, n):
m = (csum[i] - csum[i - VOL_WIN]) / VOL_WIN
v = (cs2[i] - cs2[i - VOL_WIN]) / VOL_WIN - m * m
vol[i] = np.sqrt(max(v, 0.0))
cols.append(vol)
# RSI (causal, from blindlib)
rsi = np.nan_to_num(bl.rsi(c, RSI_WIN), nan=50.0) / 100.0
cols.append(rsi)
X = np.column_stack(cols)
return X, lr, csum
def _fit(Xtr, ytr):
"""Logistic fit on standardized features. Returns (mu, sd, model) or None if the
training labels are single-class (no fit possible yet)."""
mu = Xtr.mean(axis=0)
sd = Xtr.std(axis=0)
sd[sd < 1e-12] = 1.0
Xs = (Xtr - mu) / sd
if len(np.unique(ytr)) < 2:
return None
if _HAVE_SK:
m = LogisticRegression(C=C_INV, solver="lbfgs", max_iter=200)
m.fit(Xs, ytr)
return (mu, sd, m)
# tiny fallback: penalized logistic via Newton steps (deterministic)
w = _logit_newton(Xs, ytr, C_INV)
return (mu, sd, w)
def _logit_newton(Xs, y, c_inv, iters=25):
n, p = Xs.shape
Xb = np.column_stack([np.ones(n), Xs])
w = np.zeros(p + 1)
lam = 1.0 / max(c_inv, 1e-6)
R = np.eye(p + 1); R[0, 0] = 0.0 # don't penalize intercept
for _ in range(iters):
z = Xb @ w
pr = 1.0 / (1.0 + np.exp(-np.clip(z, -30, 30)))
Wd = pr * (1 - pr) + 1e-6
grad = Xb.T @ (pr - y) + lam * (R @ w)
H = Xb.T @ (Xb * Wd[:, None]) + lam * R
try:
w -= np.linalg.solve(H, grad)
except np.linalg.LinAlgError:
break
return w
def _predict_proba(coef, xi):
mu, sd, m = coef
xs = (xi - mu) / sd
if _HAVE_SK and not isinstance(m, np.ndarray):
return float(m.predict_proba(xs.reshape(1, -1))[0, 1])
z = m[0] + xs @ m[1:]
return float(1.0 / (1.0 + np.exp(-np.clip(z, -30, 30))))
def signal(df):
c = df["close"].values.astype(float)
n = len(c)
X, lr, csum = _build_features(c)
# label[j] = 1 if cumulative return over bar j -> j+FWD_H is up, else 0.
# realized (known) only as of close[j+FWD_H].
fwd = np.zeros(n)
fwd[: n - FWD_H] = csum[FWD_H:] - csum[: n - FWD_H]
label = (fwd > 0).astype(float)
first = max(max(LAGS), max(MOM_WINS), VOL_WIN, RSI_WIN) # first fully-featured row
prob = np.full(n, 0.5)
coef = None
for i in range(n):
last_train = i - FWD_H # label of last_train uses close[i], realized now
ntrain = last_train - first + 1
if ntrain >= WARMUP:
if coef is None or (i % REFIT_EVERY == 0):
Xtr = X[first : last_train + 1]
ytr = label[first : last_train + 1]
fit = _fit(Xtr, ytr)
if fit is not None:
coef = fit
if coef is not None:
prob[i] = _predict_proba(coef, X[i])
# probability -> bounded direction. centered conviction 2*(p-0.5) in [-1,1];
# deadband kills no-conviction bars; tanh sharpens; the short side is scaled down
# (the up-drift makes full shorts a losing fight — we mainly want to step aside).
conv = 2.0 * prob - 1.0
conv = np.where(np.abs(conv) < DEADBAND, 0.0, conv)
direction = np.tanh(GAIN * conv)
direction = np.where(direction < 0.0, direction * SHORT_SCALE, direction)
direction = np.nan_to_num(direction, nan=0.0)
pos = bl.vol_target(direction, df, target_vol=TARGET_VOL,
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)

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