130 Commits

Author SHA1 Message Date
Adriano 7b34e11476 docs: update CLAUDE.md with post-research state (TP01 winner + research outcome) 2026-06-19 20:36:01 +02:00
Adriano ae7f3d17f2 deploy: TP01 trend portfolio (PORT LF4h) module + paper trader
- src/strategies/trend_portfolio.py: canonical winner, causal/no-leakage,
  reproduces CAGR +16.5% Sharpe 1.36 maxDD 13.8%
- scripts/live/paper_trend.py: forward-only paper trader, persistent state, resume
- tests/test_trend_portfolio.py: 5 tests (causality, profitability, long-only, paper parity)
2026-06-19 20:35:28 +02:00
Adriano 3b6ff02197 fix: resolution-safe timestamp in trackD_timing resample (pandas 2.x datetime64[ms])
- combination study: PORT LF4h (BTC+ETH) Sharpe 1.32 DD 12.3% remains best
- RV ETH/BTC market-neutral sleeve is genuinely uncorrelated (~0.05) but too weak
  (Sharpe 0.27) to raise portfolio Sharpe; combining the two TF configs is redundant
  (same-asset cross-config corr 0.80)
2026-06-19 20:18:03 +02:00
Adriano 8dbdadd509 research: add per-year trade counts + turnover to trackD_timing 2026-06-19 20:09:32 +02:00
Adriano 33267584d9 research: Track D winner across timeframes (15m-1d) + per-year PnL/DD
- trackD_timing.py: same TSMOM 1-3-6m blend config sampled at 15m/1h/4h/1d
- robust plateau across all TFs; 4h marginally best (LF Sharpe 1.36, DD 13.8%)
- per-year PnL and per-year max drawdown tables
2026-06-19 19:39:02 +02:00
Adriano dc2b5697da research wave 1: 5 honest tracks on certified BTC/ETH + synthesis
- trackA trend, trackB ML, trackC mean-rev, trackD trend-portfolio, trackE xsec/ensemble
- VERDICT: Track D vol-targeted BTC+ETH trend portfolio is the one robust deployable
  earner (Sharpe 1.0-1.32, DD 13-19%, positive every year 2019-2026)
- mean-reversion confirmed dead on clean data; weak-but-real ML/trend residuals
- honest: EUR50/day on 2000 in 1-2y is not reachable (needs ~137k capital or ruinous DD)
2026-06-19 19:14:53 +02:00
Adriano 6b9c469832 research: rebuild certified BTC/ETH feed + honest backtest harness
- rebuilt BTC/ETH from Deribit mainnet (certified 1.7-1.9bps vs Coinbase)
- archived contaminated alt data to Old/data/raw
- add src/backtest/harness.py: leakage-free, fee-aware signal engine
  (entry at close[i], intrabar TP/SL, CAGR/Sharpe/DD/per-year/OOS)
2026-06-19 18:41:15 +02:00
Adriano Dal Pastro 14522262e6 chore(reset): v2.0.0 — storico certificato Deribit mainnet, ripartenza pulita
Reset del progetto su fondamenta verificate dopo la scoperta che l'intera
libreria "validata OOS" era artefatto di feed contaminato (print fantasma del
feed Cerbero TESTNET + storico Binance/USDT).

- Storico ricostruito da Deribit MAINNET (ccxt pubblico, tokenless) e
  CERTIFICATO (certify_feed.py): BTC/ETH puliti su TUTTA la storia
  (mediana 2-6 bps vs Coinbase USD), integrita' OHLC + coerenza resample
  (maxΔ 0.00) + cross-venue OK. Alt esclusi (illiquidi/divergenti: LTC/DOGE
  50-82% barre flat; XRP/BNB non certificabili).
- Verdetto sul feed pulito: FADE / PAIRS / XS01 / TSM01 morti (ogni
  portafoglio Sharpe -2.3..-3.0, DD ~40%); solo SH01 e frammenti HONEST
  con segnale residuo, da ri-validare in isolamento.
- Cleanup "restart pulito": strategie, stack live (src/live, src/portfolio,
  runner/executor, yml, docker), ~100 script ricerca/gate, waste/games/
  portfolios, dati non certificati + cache e 60+ diari -> archiviati in Old/
  (preservati, non cancellati). Diario consolidato in un unico documento.
- Skeleton ricerca tenuto: Strategy ABC + indicatori + src/fractal +
  src/backtest/engine + load_data; tool dati certificati (rebuild_history,
  certify_feed, audit_feed, multi_source_check).
- Universo dati ATTIVO: solo BTC/ETH (5m/15m/1h); guardrail fisico
  (load_data su alt -> FileNotFoundError). Esecuzione DISABILITATA, conto flat.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 15:20:59 +00:00
Adriano Dal Pastro 8401a280b9 chore: bump 1.1.34 — GRID_BTC paper sleeve (Price Ladder shadow stage 1)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 16:19:48 +00:00
Adriano Dal Pastro b4d4961d77 merge: Price Ladder research + GRID_BTC paper sleeve (shadow stage 1)
Porta su main: pulizia feed (clean_feed, 254 spike-print Binance), ricerca Price Ladder
(ladder_search/regate/sltp), GridWorker paper + validazione, wiring runner kind=grid +
paper_extra (PORT06 canonico INTATTO: 19 sleeve), GRID_BTC paper in portfolios.yml.
Tutto additive + validato; nessun ordine reale (grid sim-only).
2026-06-18 16:19:48 +00:00
Adriano Dal Pastro 264b9200ea feat(live): Stage 2 — GridWorker (Price Ladder) come PAPER sleeve nel runner
Wire del Price Ladder come sleeve PAPER (sim-only, fuori dal pool €500, NESSUN ordine reale):
- runner: kind="grid" -> GridWorker in build_worker_for; _spec_assets_tf grid; tick branch
  (w.tick(res[asset])); meccanismo PAPER_EXTRA (sleeve paper letti da overrides.paper_extra,
  NON da _defs.py -> NON entrano nel backtest canonico/regression-lock: PORT06 resta 19 sleeve).
  Parsing difensivo (un errore non crasha il runner mainnet). Loop dati estesi a paper_extra.
- GridWorker: bootstrap storia FULL (start fisso, come SH01) + mappatura capitale forward dal
  deploy (capital = initial*eq[-1]/base_norm) -> niente salti da finestra mobile; base_norm
  persistito (resume). grid_mtm robusto al df live (timestamp senza datetime; param df).
- portfolios.yml: GRID_BTC in paper_extra (regime range1.5, rd0.20/ru0.06, L6, sl0.10/tp0.03,
  position_size 0.15 PINNATO). Gira in data/portfolio_paper_stats/GRID_BTC/.
Validazione (validate_grid_worker.py): [A] logica n_trades==backtest, [B] forward-tracking
esatto, [C] resume esatto. Dry-test integrazione runner: import OK, build OK, tick OK, pos 0.15.
SICUREZZA: kind=grid mai eseguito reale (runner avvia ordini solo per single/ml); €500 intatti.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 16:18:53 +00:00
Adriano Dal Pastro b3d4ab7150 feat(live): GridWorker (Price Ladder) SIM/PAPER + validazione replay==backtest — shadow stage 1
Stage 1 dello shadow per il Price Ladder (config finale: BTC 1h range1.5 rd0.20 ru0.06 L6
sl0.10 tp0.03). GridWorker (src/live/grid_worker.py) gira sul feed LIVE e contabilizza
l'equity mark-to-market col motore CANONICO grid_mtm (parita' col backtest per costruzione),
SENZA piazzare ordini reali (sim/paper). Stato persistente + resume. grid_mtm esteso con
param df=None (retro-compatibile: il feed live passa il df; None = _load come prima, gate
invariato — BTC ladder 10.8/5.9, PORT06 base 8.18 identici). Validazione
validate_grid_worker.py: [A] full-tick == grid_mtm esatto, [B] replay incrementale converge
esatto, [C] resume entro la persistenza (4 dec) -> VALIDAZIONE OK.

NB SICUREZZA: nessuna modifica a runner/portfolios.yml/_defs -> il sistema mainnet (€500
reali) e' INTATTO; il worker e' inerte finche' non wirato. L'esecuzione REALE (griglia di
LIMIT resting su Deribit, fill parziali/episodi) e' lo stage 2-3, dietro testnet +
autorizzazione esplicita. Il runner avvia ordini reali solo per kind in (single,ml);
kind=grid resta sim per costruzione.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 14:58:11 +00:00
Adriano Dal Pastro 587fbc0f61 feat(analysis): ladder_sltp_study — 3 passi pre-deploy + SL/TP, config finale ladder
I 3 passi: [1] valutazione 2018-INCLUSIVE (il gate IDX2021+ e' cieco al 2018) -> il tail
vero del BTC ladder e' il 2018 (-27.7% regime-gated / -50% none); [2] fill maker 0% vs
taker 0.10% (Deribit = LIMIT = maker) -> maker leggermente MIGLIORE, harness conservativa,
nessun fee-cliff; [3] half-size (la coda 2018 si dimezza sul book). STUDIO SL/TP: sweet spot
sl_buf=0.10/tp_buf=0.03 cappa il 2018 a -23.5% (da -27.7) senza intaccare l'edge (oos 5.06).
Lezione (conferma prior progetto): SL troppo stretto PEGGIORA (redeploy nel coltello = falso
negativo MR), SL da solo senza regime-gate e' erratico -> il regime-gate e' il controllo
PRIMARIO della coda, il SL moderato fine-tuna. Config finale: BTC 1h range1.5 rd0.20 ru0.06
L6 sl0.10 tp0.03 half-size, PROMOSSO (OOS 10.86->11.0, corr 0.195). Unico passo residuo:
shadow ledger reale (operativo). Diario sez. 8.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 14:48:39 +00:00
Adriano Dal Pastro 8abeb7a83f docs(diary)+feat(analysis): re-gate Price Ladder su dati puliti — tail vero = 2018
ladder_regate_clean.py ri-valuta i top candidati dopo clean_feed.py con gate PORT06 +
stress close_only + DD per anno. Esiti (sez. 7 diario): (1) l'obiezione "coda artefatto"
CADE -- il 54% BTC era spike-print 2024, ora DD gate (2021+) ~11-15%, tutti PROMOSSO half,
corr 0.22-0.29, reggono fee2x = candidati VERI; (2) emerge il tail REALE = 2018 (-44/-52%
standalone) che il gate NON vede (IDX parte dal 2021) -> lacuna metodologica generale; il
regime-gate stretto (BTC rd0.2 L6 range1.5) lo dimezza a -27.7%; (3) l'edge dipende dai fill
INTRABAR (close_only crolla 4.7->0.24): legittimo per LIMIT ma serve shadow ledger.
Verdetto: miglior candidato BTC 1h rd0.20 ru0.06 L6 range1.5 (PROMOSSO, corr 0.23, miglior
coda 2018); prima del deploy servono gate 2018-inclusive + shadow fill + half-size.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 14:42:18 +00:00
Adriano Dal Pastro b9e3388f76 feat(data): clean_feed — ripara spike-print del feed Deribit coi dati reali Binance
La ricerca Price Ladder ha rivelato spike-print nei parquet storici (es. BTC 2024-02-13
low 38.580 / high fasulli, BTC reale ~50k) = stesso problema TP_PHANTOM/feed testnet di
CLAUDE.md, che avvelena i backtest (stop/entry su prezzi mai avvenuti) e gonfia le code.
Tool conservativo: DETECT (wick >15% oltre il cluster close locale) -> ARBITRA con Binance
spot (ccxt, gia' cablato): sostituisce O/H/L/C solo se Binance dissente >2% su high/low (un
wick VERO confermato da Binance resta intatto), backup in data/_feed_backup/ + scrittura
atomica + validazione. RIPARATE 254 barre (BTC 132, ETH 122) su 8 file BTC/ETH x TF; 2 wick
reali confermati da Binance e TENUTI (ETH 30m/1h flash-crash veri). Impatto validato: il
BTC ladder che dava FULL DD 53.69% (artefatto) ora ne da 10.8% (la "coda di trend" era spike
fasulli); PORT06 baseline FULL Sharpe 8.13->8.18, DD/OOS invariati (canonici stabili).
data/raw e' gitignored: e' committato il TOOL, non i parquet (rigenerabili + backup locale).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 14:25:34 +00:00
Adriano Dal Pastro e631e1b89c docs(diary): price-ladder search 2026-06-18 — verdetto RETTIFICATO (nessun deploy)
Caccia multi-agente (34 agenti, 200 celle) a Price Ladder che passi il gate PORT06 su
Deribit. La sintesi iniziale trovava candidati "deployabili" (ETH 1h range-gated half-size,
OOS 10.86->11.0; BTC half-size OOS->11.34). Il critico + verifica dati ROVESCIANO il verdetto
(sez. 6): (1) la "coda BTC ~54%" e' DATI CORROTTI da spike-print (37 barre >20% intrabar, feb-2024
wick fasullo -23%, min 22.7k/max 104.7k vs BTC reale ~47-57k = stesso problema TP_PHANTOM/feed
testnet di CLAUDE.md) -> BTC non affidabile, lo stress-2022 non toccherebbe la coda vera;
(2) il +0.14 OOS Sharpe e' rumore al peso deployabile (~2.7% half, 592g regime calmo, no
significativita'); (3) il finalista ETH e' corr 0.305 (vicino allo 0.40 che fece bocciare la
griglia originale) e il regime_mask condivide il filtro trend con le fade ETH live; (4) FULL DD
standalone mascherata dal compounding (31x); (5) close_only disponibile ma non girato sui
finalisti. VERDETTO: nessun Price Ladder deployabile dimostrato; il multi-agente+critico ha
evitato un deploy ingiustificato. Config live INVARIATA. Harness validata resta come record.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 14:16:54 +00:00
Adriano Dal Pastro d733196564 feat(analysis): ladder_search — harness caccia Price Ladder che passi il gate PORT06
Goal "decine di agenti a cercare strategie Price Ladder" (Deribit). Riusa il motore
grid_mtm mark-to-market ONESTO (SL gap-aware, flat-skip, fee 0.10% RT taker = CONSERVATIVO
su Deribit, dove i fill ai livelli LIMIT sono maker ~0%) ed espone:
  - eval  <asset tf rd ru levels sl_buf tp_buf max_bars [regime] [trend_max]>
  - scan  <asset tf [regime] [trend_max]>  (sub-griglia struttura, gate PORT06, baseline
    cachata) -> top celle con verdetto gate + max_corr coi 19 sleeve + FULL DD.
Leva NUOVA: regime-gate `range` (deploy_mask in grid_mtm, retro-compatibile) = deploya la
griglia SOLO in regime di range (|close-EMA200|/ATR < trend_max), dove la griglia vive, e
non in trend dove muore. Contesto della ricerca: la griglia ETH del gioco e' BOCCIATA
(ridondante, corr 0.40); i ladder BTC sono meno correlati (~0.18) e passano il gate, MA il
nodo e' la FULL DD (coda di trend 2021/22, ~60% standalone) che il verdetto del gate NON
controlla -> la harness la espone (full_dd standalone + full_full_dd di portafoglio).
grid_mtm: aggiunto param deploy_mask (None = comportamento storico, parita' col gate).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 12:29:25 +00:00
Adriano Dal Pastro 57758ffcb1 feat(analysis): leverage_sweep — sweep leva 1-10, stress 2022, leva max raccomandata
Risponde a "se porto a leva 10 cosa succede?". Sweep leva 1->10 su PORT06
(modello daily lineare == live) FULL/OOS + anno per anno (2021-2026), con le
non-linearita' che il modello daily NASCONDE: volatility drag/Kelly, rovina
intraday vs daily. Stress con shock REALI 2022 (BTC/ETH storici: LUNA -29/-36%,
3AC-giugno -44/-52%, FTX -26/-32%) sull'esposizione netta del book -> leva di
rovina per net-long beta. Verdetto: leva 10 NON sopravvive a un 2022-repeat
(rovina gia' a ~5-9x se net-long). Leva MAX raccomandata ~3x (DD<=50% recuperabile,
beta=0.6) == config live attuale. Non modifica nulla del live.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 09:48:33 +00:00
Adriano Dal Pastro c8b956ab56 fix(dashboard): PnL totale dal ledger, non init hardcoded 2000
La KPI "PnL totale" sottraeva un capitale iniziale fisso di 2000 (retaggio
testnet) -> sul micro-test mainnet da 500 mostrava -1499 (-74.95%) invece di
+0.95 (+0.19%). Nuovo helper _ledger_pnl(): legge pnl_total dall'ultima riga
di equity.jsonl (il ledger lo scrive come equity-initial_capital) e ne deriva
l'iniziale -> la dashboard combacia col ledger per costruzione, qualunque sia
il capitale (500 mainnet / 2000 testnet), niente piu' valori fissi da
aggiornare a mano. Fallback: primo punto equity, poi l'equity stessa.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 09:48:33 +00:00
Adriano Dal Pastro 3b60289cbd fix(gitignore): commento su riga propria per data/funds_watch.json
Il commento inline rendeva il pattern non-matchante (gitignore non
supporta i commenti a fine riga).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 21:16:13 +00:00
Adriano Dal Pastro e328836614 feat(live): switch a MAINNET — micro-test fade-only €500 + funds-watch
Conversione testnet -> mainnet Deribit (soldi veri). Vedi
docs/specs/mainnet-microtest-plan.md, Fase 1.

- portfolios.yml: total_capital 500, leva 3, weighting equal; eseguono
  REALE solo i 7 single-leg (6 fade MR01/02/07 x BTC/ETH 15m + DIP01 1h);
  pairs/SH01/multi-asset -> paper (rumore arrotondamento a €500); real_truth.
- docker-compose.yml: portfolio+dashboard caricano anche .env.mainnet
  (token mainnet, prevale su .env; .env.mainnet resta gitignored).
- reconcile_account.py: watermark FONDI (compute_funds_change) — rileva
  aumenti di capitale (deposito/top-up) e cali anomali (prelievo) sul
  balance USDC, alert Telegram FUNDS_INCREASE/FUNDS_DECREASE; soglia
  max(€25, 5%). Stato in data/funds_watch.json (host-writable).
- .gitignore: ignora data/funds_watch.json (stato runtime).

Ledger testnet archiviato in data/_reset_backup/pre_mainnet_*.tgz e azzerato.
Crons host ri-puntati al conto reale (sourcing .env + .env.mainnet).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 21:15:55 +00:00
Adriano Dal Pastro a0842a61cc feat(analysis): trades_status — PnL live per posizione aperta (entry reale vs mark USDC)
Riusa la convenzione della dashboard (get_ticker_batch USDC perp, real_entry vs mark;
pairs a 2 gambe). Mostra unrealized per trade + Σ REALE, e segnala che il ledger
unrealized e' sul feed sim-decisione testnet DISLOCATO (lo scarto e' dislocazione,
non soldi). Fix sys.path: import src.* anche eseguito come script file.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 15:56:22 +00:00
Adriano Dal Pastro 3a66907f43 feat(analysis): trades_status.py — report stato trades con POOL reale vs PAPER-STATS separati
La dir e' la fonte di verita': portfolio_paper/ = pool (ledger/equity), portfolio_paper_stats/
= TR01/ROT02/TSM01/XS01 solo statistica. Niente piu' glob su portfolio_paper* (matchava
entrambi -> +8.15 XS01 sim attribuiti per sbaglio all'equity il 2026-06-17). Mostra realizzato
POOL + unrealized aperti e riconcilia con Δequity del ledger. Caveat + comando in CLAUDE.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 15:17:50 +00:00
Adriano Dal Pastro aee53161c3 docs: Fase 0 smoke mainnet VERDE (ETH->USDC limit) + aggiorna piano micro-test
Catena d'esecuzione validata su mainnet con soldi veri a costo ~0: LIMIT SELL ETH_USDC
0.0138 @ floor 1776.3 -> filled@1778.1 (=bid), fee 0, USDC 25->49.54. Consolidato l'ETH
stranded in margine USDC. Funding parziale ~$49.54 (sotto il min EUR500) -> Fase 1 in
attesa di top-up a ~EUR1000. Diario + STATO/checklist del piano aggiornati.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 15:48:38 +00:00
Adriano Dal Pastro 7b3e554424 release: v1.1.33 2026-06-16 14:16:22 +00:00
Adriano Dal Pastro b576ee66ac feat(worker): guard TP-invertito — sopprime l'entry quando il TP e' gia' sfondato (wick-print)
Un wick transitorio fa calcolare un tp dal lato sbagliato dell'entry (donchian: segnale
su barra wickata, entry al prezzo recuperato oltre il proprio tp) -> l'exit intrabar
scatta a bars_held=0 in perdita (16-06: 8 giri MR02_BTC 15m, sim -17.9 / reale -2.3).
TP_PHANTOM non lo prende (niente resting oracle, prezzo oltre il livello). Gate
zero-parametri in StrategyWorker._open_position, solo path live. Test + diario + CLAUDE.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 14:16:13 +00:00
Adriano Dal Pastro ef797f1ff0 release: v1.1.32 2026-06-15 08:44:42 +00:00
Adriano Dal Pastro 4c184d5cbc feat(runner): gate feed CONGELATO — sospende gli sleeve ETH-leg finche' il feed non si sblocca
Audit "stato ordini": il feed ETH-PERPETUAL congelato a 1661.95 (36h+) generava
perdite REALI (SH01_ETH -2.83 reali vs -0.09 sim su un close + riapertura; 4 pairs
con gamba ETH entrati su z-score spuri -3/-5/+5.6 = artefatto del log-ratio con ETH
pinnato mentre gli alt si muovono).

_frozen_assets + _feed_gated_sids: quando il feed di decisione 1h di un asset e'
congelato, gli sleeve concentrati (single/ml/pairs) che ne dipendono saltano il tick
(entry+exit) finche' non si sblocca (come outage; disaster-SL on-book = coda).
Auto-guarente: rilascio alla prima barra COMPLETA non-flat (NON l'entry-guard
post-flat bocciata). Detector guasto-vs-illiquido: conta la run di close INVARIATE
(ETH/BNB/DOGE run 40-64, 1-4 val/48h = morti; SOL/LTC/ADA run <=12, 5-31 val = vivi).
Soglia feed_freeze_gate_bars=24 -> gatea le 9 gambe ETH esatte, PR_BTCLTC e i
multi-asset restano attivi. Alert Telegram FEED_FROZEN_GATE GATED/RIPRESO.

Test test_freeze_gate.py (6, detector+scope+rilascio). Suite portfolio 140/140.
Cerotto testnet: il fix vero e' mainnet. Diario 2026-06-15-frozen-feed-gate.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 08:44:39 +00:00
Adriano Dal Pastro c21e1dc635 docs: timing-sweep pairs/honest (NO deploy) + stato mainnet + vincolo feed v2
- CLAUDE: feed ETH ancora congelato 36h+; vincolo Cerbero v2 (serve SOLO
  5m/15m/1h, 30m/10m -> 400, legacy 404; _SUBHOURLY "30m" speculativo mai
  testato); esito timing-sweep (5m non conviene: regime recente peggiore +
  flat ETH 29%; gate full+OOS necessario ma non sufficiente); stato mainnet
  (token in .env.mainnet verificato is_mainnet=True, conto VUOTO = blocco)
- spec mainnet-microtest: blocco STATO 2026-06-14, .env.mainnet separato +
  servizio dedicato, checklist aggiornata (token done, funding = blocco)
- nuovo diario 2026-06-14-timing-sweep-pairs-honest.md + harness riusabile
  scripts/analysis/timing_sweep_pairs_honest.py
- .gitignore: .env.mainnet (token mainnet mai in git)

Nessuna modifica a codice/config live: PORT06 invariato (19 sleeve).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 20:19:56 +00:00
Adriano Dal Pastro 47e92a0425 docs(CLAUDE): aggiorna memoria — dashboard, report ledger-vs-backtest, feed ETH congelato, micro-test mainnet
Fatti di sessione: dashboard web (porta 8787), cron ledger-vs-backtest (gate scaling),
caveat feed testnet ETH-PERPETUAL congelato (spiega 0-trade fade ETH 15m; fix=mainnet),
piano micro-test mainnet + token Cerbero env-configurabile (CERBERO_TOKEN).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 22:46:29 +00:00
Adriano Dal Pastro faf08b3988 docs(spec): piano micro-test mainnet + token Cerbero env-configurabile
Piano operativo per validare l'edge su mainnet con poco denaro reale (il gate per
scalare). Fasi: smoke -> fade-only €1000 2-4 sett -> verdetto ledger-vs-backtest ->
espansione. Sizing motivato (fades €1000 = rumore arrotondamento 2.6% BTC; pairs
esclusi: 30% a quella taglia). Token ora da env CERBERO_TOKEN (default testnet
invariato) -> switch mainnet = solo .env, niente codice. is_mainnet() helper.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 22:36:26 +00:00
Adriano Dal Pastro 3ac8a46e6a feat(dashboard): trade attivi con data/ora ingresso + tempo in posizione
Nuove colonne 'Ingresso (UTC)' (entry_time) e 'In posizione' (durata dall'ingresso,
formattata m/h/g) con flag stantio. Sostituisce la colonna Eta (staleness) col
tempo di holding effettivo.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 22:24:22 +00:00
Adriano Dal Pastro 7ee5b1767f feat(dashboard): solo dati REALI nei trade attivi + valore mercato reale (anche pairs) + attivi sotto il grafico
- entry/mark/unrealized SEMPRE reali (entry di esecuzione vs mark USDC live); il
  feed sim/decisione (testnet inverse dislocato) non e' piu' mostrato come prezzo
- pairs: valore di mercato reale di ENTRAMBE le gambe + PnL non realizzato reale a
  2 gambe (prima 'pairs (z)' senza valore)
- tabella attivi spostata subito sotto il grafico equity

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 22:21:22 +00:00
Adriano Dal Pastro d5a83075f8 fix(dashboard): unrealized usa entry REALE (non sim) per coerenza col mark reale
Le posizioni eseguite mostravano l'entry SIM (feed decisione testnet inverse,
dislocato ~1.3% dal lineare USDC) confrontato col mark REALE -> unrealized gonfiato
(SH01_ETH -1.36% invece di ~-0.25%) e due trade ETH con lo stesso entry sim 1661.95.
Ora entry = real_entry_price/real_entry_a se eseguito; unrealized vs mark reale su
base coerente. Indicatore '⚠sim' quando il feed di decisione diverge dal reale.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 22:13:48 +00:00
Adriano Dal Pastro 183209dbd4 feat(dashboard): grafico equity per famiglia + tabella strategie raggruppata per famiglia
- nuovo grafico multi-linea 'Equity per famiglia' (PnL cumulato realizzato di ogni
  famiglia su asse-tempo comune, colori per famiglia, legenda col totale)
- tabella strategie reali raggruppata per famiglia con intestazioni colorate +
  PnL di famiglia; attive prima delle ritirate dentro ogni gruppo
- backend: fam_curves (step-wise cumulato per famiglia dai CLOSE reali), tag fam
  sui trade chiusi

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 21:48:56 +00:00
Adriano Dal Pastro e311d00fe4 fix(dashboard): tooltip equity allineato al mouse (responsive canvas)
Forzare l'altezza del canvas via CSS !important con maintainAspectRatio default
sfalsava la risoluzione interna vs quella mostrata -> hit-test tooltip offset dal
mouse. Fix canonico: canvas in contenitore .chartbox ad altezza fissa + chart
responsive:true/maintainAspectRatio:false + interaction index su tutti e 3 i grafici
(equity, modal, paper).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 21:35:15 +00:00
Adriano Dal Pastro 37ffe92498 docs(report): strategie_attive.html aggiornato a config live (fade 15m, leva 3x)
Era fermo al 2026-06-12 06:17 (pre-swap): diceva fade 'intraday 1h' e 'leva 2x'.
Rigenerato -> numeri post-swap (FULL Sh 8.13/DD 2.47%, OOS 10.86/2.09%) + fix
etichette hardcoded nel generatore (header FADE 1h->15m, leva 2x->3x, nota tabella).
E' la scheda linkata dal modal della dashboard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 21:30:28 +00:00
Adriano Dal Pastro f6287fcd36 feat(dashboard): area PAPER distinta con equity separata
I 4 multi-asset (TR01/ROT02/TSM01/XS01, solo statistica fuori dal pool reale) ora
in una zona dedicata con la PROPRIA equity (capitale nozionale + PnL cumulato del
book paper) e tabella separata. La tabella/fam_pnl/closed dell'area principale
diventano SOLO reali. Curva paper Chart.js (ocra), cliccabile -> stesso modal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 21:25:17 +00:00
Adriano Dal Pastro b6b465878a feat(dashboard): scheda per strategia (modal: curva PnL reale vs sim + trade + link grafici) + versione sistema/strategia + equity restyling
- click su una strategia -> modal con descrizione, versione di creazione/modifica,
  curva PnL CUMULATO reale vs sim (mostra il leakage), trade, e link alla scheda
  dettagliata strategie_attive.html (servita, docs/report montato ro)
- versione di sistema (APP_VERSION) nell'header; per ogni strategia la versione/
  origine documentata (mappa VERSIONS da CLAUDE.md)
- equity restyling: gradiente verde/rosso secondo segno, tooltip con EUR e delta,
  valore+delta in testata, assi in EUR, piu' alto. /api/strategy con guard
  path-traversal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 21:20:33 +00:00
Adriano Dal Pastro 22af19c7f9 feat(dashboard): badge strategie RITIRATE (staleness) + descrizioni per strategia
Le fade 1h sostituite dallo swap 15m sono marcate RITIRATA→15m (rilevate per
status.json stantio >30min, ordinate in fondo, riga barrata). Descrizioni concise
per ogni strategia (da strategie_attive.html) come tooltip + sezione legenda.
Fix rilevazione paper: per directory (portfolio_paper_stats), non per chiave
'weights' (TR01/TSM01/XS01 non ce l'hanno) -> ora i 4 multi-asset sono paper.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 21:12:42 +00:00
Adriano Dal Pastro 31f08ddf32 feat(dashboard): frontend web PORT06 — stato live, PnL totale/per-strategia, grafici, trade
Server stdlib http.server (zero dipendenze nuove) che legge data/: KPI equity/PnL/DD,
grafico equity (Chart.js CDN + fallback), PnL per-strategia (barre, realizzato reale),
trade attivi in TEMPO REALE (mark Cerbero best-effort, PnL non realizzato, barre, eta
stantio) e chiusi (ultimi 50). Servizio docker-compose 'dashboard' porta 8787, stessa
immagine, monta data/, restart unless-stopped + healthcheck. Nessuna auth -> rete interna.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 19:46:47 +00:00
Adriano Dal Pastro dc22256e0e feat(report): ledger reale vs backtest — il gate per scalare il capitale
Per gli sleeve eseguiti sim==backtest per costruzione -> reale vs backtest =
fuga di esecuzione (slippage + fee + netting/phantom/sim_fallback). Misura
LEAKAGE sim-reale per-trade, slippage ingressi/uscite, fee reali, sim_fallback,
ledger per-sleeve; verdetto verde/giallo/rosso. Clean-start --since 2026-06-13
(la finestra mobile includerebbe l'incidente testnet pre-fix). Cron host
giornaliero 08:30 UTC --telegram. Read-only, niente rete.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 11:24:19 +00:00
Adriano Dal Pastro 6fe45867f0 docs(CLAUDE): stato live v1.1.31 — leva 3x, swap fade 15m, lineage, stale-guard, ribilancio conservativo
Aggiorna il doc canonico: default PORT06 ora leva 3x + fade 15m (FULL 8.13/OOS
10.86), e i fix di sessione: INIT_LINEAGE, STALE_REAL_POSITION, conservazione
equity al ribilancio. TF sweep (1m/2m/5m/10m) annotato.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 10:21:27 +00:00
Adriano Dal Pastro ab9899b0c4 release: v1.1.31 2026-06-13 10:09:57 +00:00
Adriano Dal Pastro 432c1ad790 fix(ledger): ribilancio conserva l'equity (no doppio conteggio in-position)
Bug: i flat si dividevano l'INTERO total includendo il capitale dei worker in
posizione, che lo trattenevano in piu' -> equity gonfiata di Sigma(capital-alloc)
sugli in-pos. Caso MR02_BTC 15m seedato (181.19) e in posizione al ribilancio
00:01: +4.77 fantasma. allocate(weights, reserved={sid:cap}): gli in-pos
trattengono il loro, i flat si dividono total-Sigma(reserved) per peso
rinormalizzato -> Sigma alloc == total sempre. Parita' runner intatta
(5.8e-08). Test conservazione (ledger + runner). Suite verde.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 10:09:36 +00:00
Adriano Dal Pastro 85043bf2d3 feat(reconcile): guard STALE_REAL_POSITION — rileva posizioni reali non gestite
Caso MR02_BTC 1h (2026-06-13): ritirato dallo swap a 15m mentre short reale ->
short nudo (TP perso nel netting), reconciler cieco perche' lo status fermo
contava ancora come libro. compute_stale_real_positions: worker con
real_in_position e status.json fermo >15min = non gestito -> alert
STALE_REAL_POSITION. Discriminante = staleness (venue-agnostico: cattura
ritirati-da-swap/crashati). Gira dal cron host :40 (no rebuild). 3 test nuovi,
suite 132 passed. Orfano chiuso a mano (testnet, +0.85 netto).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 09:43:47 +00:00
Adriano Dal Pastro 643ff7f943 feat(live): INIT_LINEAGE — eredita' capitale al cambio timeframe + sweep migliorie serale
Worker nuovo (no status.json) eredita capital/real_capital dal gemello piu'
recente stessa strategia+asset (mai la posizione): niente piu' seed manuale
al prossimo swap. 3 test nuovi, suite 129 passed.
Ricerca: gate 10m ADD bocciato (OOS Sh 10.86->10.76, watchlist chiusa);
XSEC breadth 8->14 Deribit BOCCIATO (gambe flat 91-99% corrompono il ranking)
ma promettente su dati HL reali (-6%->+9% stessa finestra) -> sblocco = routing
dati Hyperliquid quando la storia sara' sufficiente.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 21:18:30 +00:00
Adriano Dal Pastro 12e71d4c8b research: fade TF sweep 1m/2m/5m/10m/30m — 15m confermato, 10m in watchlist, 1m/2m chiusi
Frontiera Sharpe monotona al scendere del tf ma margine fee si assottiglia:
MR02_BTC muore a fee2x a 5m (-1.70); MR02 sotto i 15m e' fee-death nel regime
corrente (1m -64%). 1m: flat share ETH 25.6% + niente storia full -> chiuso.
Corr col 15m live: 5m 0.46 / 10m 0.53. Fix resample unit-safe (pandas 2.x ms).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 20:56:15 +00:00
Adriano Dal Pastro 378ec7fb43 release: v1.1.30 2026-06-12 20:47:45 +00:00
Adriano Dal Pastro f78380596a feat(live): SWAP fade 1h -> 15m su PORT06 (scelta utente, gate fade15m_port06_gate)
FADE specs tf=15m (sid invariati -> pesi/alloc intatti; DIP01 resta 1h);
builder canonico allineato (combine_portfolio.FADE_TF) -> parita' delle due
facce; regression-lock aggiornato al nuovo baseline (FULL Sh 7.34->8.13
DD 3.46->2.47, OOS Sh 10.07->10.86 DD 1.48->2.09). Runner invariato (fetch
15m gia' esistente dal blend pairs). Suite 126 passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 20:47:44 +00:00
Adriano Dal Pastro 82a1c60e08 research(gate): fade 15m PROMOSSI al gate PORT06 (ADD: OOS Sh 10.07->10.48, DD giu' su entrambe le finestre)
Parita' builder esatta; corr 15m-1h media 0.26 (vera diversificazione);
flat-entry-skip OK su ETH (edge reale, non stale-print). Raccomandata ADD.
Deploy plumbing elencato nel diario (non eseguito).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 20:37:53 +00:00
Adriano Dal Pastro 515193a203 docs(research): XEX — discordanze Deribit testnet vs Hyperliquid
Spread D/H mean-reverting (half-life 3-7h); su BTC/ETH inverse il BOOK stesso
e' dislocato 1-2% (reale, eseguibile), su DOGE/SOL e' stale-print illusion
(book allineato a HL, 87%/35% barre flat). Edge testnet-only: non deployato,
record + telemetria. Vedi diario.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 20:29:11 +00:00
Adriano Dal Pastro 612f2bfced feat(live): reconcile resting + orphan single-leg + circuit-breaker venue-lock + FEED_BOOK_GAP
Codice della tornata v1.1.27/28 (gia' in produzione, mai committato):
- reconcile_account: estensione ordini RESTING (FILLED_UNBOOKED/MISSING/STALE,
  caso MR02_BTC: TP fillato di notte scoperto ore dopo) + expected_resting in books
- strategy_worker: orphan_legs su REAL_CLOSE_PARTIAL anche single-leg, persistito
- execution: circuit-breaker su venue-lock admin (stop ordini dopo errori ripetuti)
- runner/hourly_report: alert FEED_BOOK_GAP + timestamp closed trades
- cerbero_client: get_open_orders (merge all + trigger_all)
Test: 12 nuovi, suite completa 126 passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 20:29:02 +00:00
Adriano Dal Pastro a2d581691a release: v1.1.29 2026-06-12 20:23:51 +00:00
Adriano Dal Pastro 5fe53841e9 feat(live): leva 2x -> 3x su PORT06 (scelta utente, frontiera ACCEL50)
PAIRS position_size_family 0.20 -> 0.13 per conservare l'esposizione validata
~0.40 (gate 2026-06-07): la leva accelera le famiglie con stop, non i no-stop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 20:23:43 +00:00
Adriano Dal Pastro e0257c6c88 docs(research): ACCEL50 — frontiera di leva PORT06 + probe fade 15m verso i 50 EUR/giorno
Leva 2->3/4 dimezza i tempi (OOS CAGR 111->206/343%, DD full 3.5->5.2/6.9%).
Fade 15m passa il probe (6/6 sleeve OOS+, fee 2x OK, BTC 15m > 1h a meta' DD).
Pairs nuove e PAXG bocciati: stale-print illusion (gambe alt 88-98% barre flat).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 20:21:51 +00:00
Adriano Dal Pastro 6f294e3889 docs(diary): chiusura incidente testnet — flatten + orfani risolti, reconciler OK
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 13:04:56 +00:00
Adriano Dal Pastro 1a0b601fab release: v1.1.28 2026-06-12 12:35:49 +00:00
Adriano Dal Pastro 2b9adaeea3 docs(diary): reconcile resting + FEED_BOOK_GAP + epoca v1.1.26 (tornata v1.1.27)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 12:24:37 +00:00
Adriano Dal Pastro e7fd286fe0 release: v1.1.27 2026-06-12 12:22:10 +00:00
Adriano Dal Pastro 003875f2c3 docs(report): strategie_attive.html rigenerato su dati freschi (tutti gli 8 asset al 2026-06-12)
- parquet rinfrescati post-fix downloader: BTC+ETH 1h/15m/5m completi via v2,
  alt 1h incrementali (erano fermi al 28-29/05, panel multi-asset incoerenti)
- make_strategy_doc.yearly_stats: clamp di j (l'engine live-path lascia j>=n
  per il trade aperto al bordo serie -> IndexError coi dati freschi)
- header v1.1.26, card XS01 con phase-tranching, metodologia con verita'
  d'esecuzione/netting. Backtest canonico STABILE su +2 settimane di dati:
  FULL Sharpe 7.34 / DD 3.46 — OOS 10.07 / 1.48 (identico al 2026-06-11)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 06:17:39 +00:00
Adriano Dal Pastro e18adba4a6 fix(data): downloader su endpoint v2 + guard anti-troncamento (post-mortem refresh notturno)
Il refresh notturno e' rimasto 10h appeso e ha TRONCATO i parquet BTC al 2018:
cerbero-mcp ha rimosso l'endpoint legacy /mcp-deribit/tools/get_historical (404),
il downloader skippava in silenzio OGNI chunk (3 retry + sleep esponenziale x
migliaia di chunk) e a fine giro scriveva comunque il file con la sola Fase 1
storica. Aggravante: TIMEFRAMES includeva '1m' (1 giorno/richiesta = ~3000
richieste/asset). ETH salvato in tempo (kill prima della sovrascrittura);
BTC ripristinato via v2 (1h/15m/5m completi 2018->oggi in ~30 min).

- _fetch_deribit -> endpoint v2 /mcp/tools/get_historical (lo stesso del runner)
- guard chunk: >50% skippati = endpoint rotto -> RuntimeError, niente parziali
- guard anti-regressione in download_asset: mai sovrascrivere un parquet con
  dati che finiscono PRIMA dell'esistente
- '1m' fuori da TIMEFRAMES (refresh torna 5m/15m/1h; il 1m ad-hoc se serve)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 06:08:42 +00:00
Adriano Dal Pastro 36ac2426a1 release: v1.1.26 2026-06-11 21:36:30 +00:00
Adriano Dal Pastro 82c05f6f81 fix(exec): code-review serale — guard anti-posizione-nuda sul netting + verità per-frazione
7 finder paralleli sul diff della giornata (8adf388..HEAD), fix dei confermati:

CRITICI (money-path):
- close_amount: GUARD stato-stantio sul fallback netting — il residuo
  non-reduce-only e' consentito solo fino al gap (conto reale - libri degli
  altri worker - orfani) nella direzione della chiusura (src/live/books.py =
  fonte unica, usata anche dal reconciler). Senza guard, un close su stato
  stantio (DSL scattato in outage, flatten manuale) APRIVA una posizione nuda
  a taglia piena bookata come 'chiusura verificata'. Fail-safe se il gap non
  e' calcolabile. Check polvere PRIMA di _quantize_step (che clampa al lotto
  minimo: un residuo 1e-17 diventava un ordine nudo da un lotto).
- _real_close: market_amt = filled_amount anche a merged verified=False (i
  contratti chiusi dal reduce-only non si buttano se il leg netting fallisce);
  REAL_CLOSE_PARTIAL non piu' gateato su verified (era soppresso proprio nel
  caso parziale reale).
- pairs: verita' per-FRAZIONE di gamba (gross proporzionale al fillato, orfano
  = solo il residuo — prima falsava reconciler e real_capital della parte gia'
  chiusa); REAL_OPEN_PAIR booka filled_amount; docstring applied corretta.
- open_pair unwind: chiude il FILLATO, non il richiesto (senza il cap silenzioso
  del reduce-only avrebbe mangiato quota altrui).
- place_tp_limit: quantize CONSERVATIVO (sell=floor/buy=ceil) — il rounding al
  tick piu' vicino poteva mettere il resting oltre il TP sim -> tocco genuino
  classificato phantom sistematicamente.

ROBUSTEZZA/OSSERVABILITA':
- runner: WORKER_ERROR_STREAK a 5 e poi ogni 50 poll + recovery RIPRESO +
  flag real_in_position nell'alert (prima: one-shot a n==5, poi silenzio).
- _tp_phantom: TTL 120s sul verdetto (era ~50 HTTP/h per worker a barra
  fantasma); merge notes con entrambi gli order_id (audit-trail).
- reset_flatten: _quantize_step Decimal (round float produceva amount che
  Deribit rifiuta); hourly_report: book XS01 con gambe SHORT visibili (abs).
- validate_xsec_worker: POS/LEV importati (non hardcoded); xs01_tranche:
  regression-lock ESEGUITO vs xsec_sim; reconcile su src/live/books;
  drift_monitor: rolling vettoriale + exit code 1 su warn.

Test: +4 guard/dust, fixture filled_amount -> 114 passed.
Deferiti (TODO): resting esposti al netting, lifecycle orphan_legs, finestra
trade-history TP_PHANTOM, validazione feed a monte, dedup minori.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 21:36:30 +00:00
Adriano Dal Pastro 3f1feb1a6f docs(report): generatore aggiornato — tranching XS01 + verità esecuzione/netting in metodologia
- card XS01: paragrafo phase-tranching v1.1.21 (3 sub-book sfasati, gate plateau
  K=2/K=3, PORT06 OOS 10.07->10.15 DD 1.48->1.38)
- metodologia: bullet v1.1.23-25 (TP_PHANTOM conferma dal book reale, ledger su
  amount fillato, chiusure netting-aware, reconciler ACCOUNT_DRIFT, drift-monitor)

L'HTML rigenerato segue in un commit separato (in coda dietro il refresh parquet).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 21:07:06 +00:00
Adriano Dal Pastro cfdd8f6a72 release: v1.1.25 2026-06-11 20:54:44 +00:00
Adriano Dal Pastro 844a9d0e22 feat(exec): netting delle chiusure market — il residuo reduce-only cappato/respinto va in market puro
close_amount ora: (1) tenta il market reduce-only (sicurezza storica: un bug di
stato filla 0 invece di aprire posizioni); (2) il residuo cappato/respinto dal
netting di conto (worker in direzioni opposte sullo stesso strumento) viene
rieseguito in MARKET PURO con label '|net' — muove il conto esattamente del
delta del libro = netta contro le quote opposte. Niente piu' gambe pairs orfane
ne' close cappati per costruzione (close_pair passa da close_amount).

- _merge_close_fills: il chiamante riceve UN Fill (prezzo medio pesato sui fill,
  fee sommate, filled_amount totale, verified se copre il richiesto, notes
  'netting' quando il fallback scatta)
- worker single-leg + pairs: evento NET_CLOSE (log jsonl + Telegram) a ogni
  fallback — osservabilita' della frequenza dei conflitti di netting
- sicurezza persa sul residuo coperta dal reconciler orario (ACCOUNT_DRIFT);
  orphan_legs/REAL_CLOSE_PARTIAL restano come ultima difesa
- 4 test nuovi (full r-o senza fallback, cappato, respinto, doppio fail) -> 110 passed

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 20:54:43 +00:00
Adriano Dal Pastro 22b77668f1 feat(ops): reconciler read-only conto vs libri worker (primo passo verso il position-manager)
scripts/analysis/reconcile_account.py: per strumento USDC confronta l'atteso
(somma quote reali dai status.json: single-leg + pairs 2 gambe + orphan_legs
registrati = drift spiegato) col conto reale (get_positions, size/mark -> coin).
Tolleranza 1.5x step contratto, anti-race (ricontrollo a 10s), alert Telegram
ACCOUNT_DRIFT con --telegram. SOLO lettura, gira dall'host (cron orario :40,
nessun deploy). Al primo run: vero positivo su BTC (libro MR02 short 0.0028 vs
conto flat — TP fillato dallo spike reale, si riconcilia alla chiusura sim).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 20:43:17 +00:00
Adriano Dal Pastro 521447fce6 release: v1.1.24 2026-06-11 20:05:43 +00:00
Adriano Dal Pastro 429fa01c97 fix(exec): verità contabile sul netting — filled_amount, gambe orfane, tick isolato
Da audit live (3 indagini parallele, diario 2026-06-11-system-audit.md): il modello
quote-per-worker reduce-only si rompe con direzioni opposte sullo stesso strumento
(pairs long ETH vs fade short ETH) — close cappati bookati pieni, 3 gambe pairs mai
eseguite col PnL sim nel ledger reale, conto short 0.027 ETH oltre i libri
(riallineato a mano + DSL orfano cancellato).

- Fill.filled_amount (order.filled_amount): TUTTI i ledger usano il fillato reale
- REAL_CLOSE_PARTIAL (log+alert) su close cappato; residuo orfano dichiarato
- pairs: PnL solo per gambe verificate; gamba respinta -> orphan_legs persistito
  + alert PAIR_LEG_ORPHAN; applied solo con entrambe le gambe (else sim_fallback)
- REAL_DIVERGENCE anche su jsonl (prima solo Telegram)
- runner: tick isolato per-worker + WORKER_ERROR_STREAK a 5 fail consecutivi

Strutturale APERTO (decisione utente, opzioni nel diario): position-manager
centrale / sotto-conti per famiglia / status-quo monitorato.

Test: +2 (partial-close, orphan-leg), fixture filled_amount -> 106 passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 20:05:42 +00:00
Adriano Dal Pastro 8a2b065dd7 chore(analysis): dedup engine gate PORT06 + drift monitor giornaliero + impact bfill
- _port06_gate_common.py: build_trades_variant/equity_from_trades/port_metrics/dd
  fattorizzati dai 3 gate exit16/trendmax/dip01 (-214 righe duplicate). Nessun
  copy-drift trovato; versione promossa = trendmax (superset con hurst_mask).
  Output dei 3 gate verificato BYTE-IDENTICO prima/dopo. dip_trades resta nel suo
  script (sibling deliberato long-only/orig_gap, non una copia).
- drift_monitor.py: rolling-return per famiglia vs distribuzione storica propria
  (warn sotto p5; oggi: FADE 120g al p2). In crontab host giornaliero 07:15 UTC
  con report Telegram. Osservabilita', non filtro di trading.
- daily_equity_bfill_impact.py: bug bfill _daily_equity QUANTIFICATO -> non
  materiale (OOS invariato per costruzione, FULL DD 3.46->3.67 col fix, nessun
  verdetto gate a rischio). Lasciato documentato in TODO, niente fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 20:04:23 +00:00
Adriano Dal Pastro 51baed22e4 release: v1.1.23 2026-06-11 19:32:24 +00:00
Adriano Dal Pastro 12625b3315 feat(exec): gate TP_PHANTOM — il tocco TP va confermato dal book reale, non dal feed
Il feed testnet stampa wick che 'toccano' il TP intrabar senza che il prezzo
abbia mai scambiato al livello: il sim bookava +4% fantasma a bars_held=0 e
_real_close chiudeva A MERCATO una posizione col resting TP a zero fill
(-fee/spread a giro, 14 giri l'11-06). Ora StrategyWorker._tp_phantom sopprime
l'exit take_profit quando: tocco sim + resting LIMIT zero-fill + prezzo corrente
che non ha raggiunto il livello (il limit sul book reale e' l'oracolo del tocco).
Zero parametri (verita' d'esecuzione, non filtro di strategia); SL close-confirm
e max_bars restano attivi nello stesso tick; fill parziale/prezzo oltre il
livello/worker non eseguito/errore rete -> comportamento storico. Log TP_PHANTOM
dedup per barra + alert Telegram una tantum. 5 test nuovi (104 passed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 19:32:24 +00:00
Adriano Dal Pastro 15520dda70 release: v1.1.22 2026-06-11 19:18:55 +00:00
Adriano Dal Pastro 2ae6d7afe6 fix(report): conta i CHIUSI col flag win del worker (PnL REALE), non col net_return sim
Col real-truth ledger net_return resta il numero SIM diagnostico: sui TP fantasma
da spike-print testnet (ETH, bars_held=0: lo spike genera il segnale E tocca il TP
intrabar; il reale chiude a mercato pagando fee/spread) il report Telegram diceva
26 positivi / 0 negativi mentre il reale era 11/15. Ora collect() usa ev.win
(real-truth-aware, fallback nr>0 per eventi storici); i PnL per-motivo erano gia'
reali. TODO: nota monitor sul churn da spike (niente filtri anti-spike nei segnali:
artefatto testnet, real-truth gia' contabilizza giusto).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 19:18:55 +00:00
Adriano Dal Pastro b208417f2d release: v1.1.21 2026-06-11 13:29:25 +00:00
Adriano Dal Pastro 9d15506b05 feat(stability): sweep stabilità — fix TR01 mean(rets), XS01 phase-tranching K=3, z-stop pairs bocciato
Audit anti-overfit su tutte le 19 sleeve (diario 2026-06-11-stability-sweep.md):

- FIX BasketTrendWorker: mean(rets) sui soli asset in posizione sovrappesava N/k
  a paniere parziale (1 long = 0.45 del capitale invece di 0.09) -> replay -44%
  vs ref +42%. Ora sum(rets)/N (convenzione canonica 1/N): replay +32% vs +42%
  (residuo = convenzione dichiarata). Solo statistica PAPER.
- XS01 PHASE-TRANCHING (gate xs01_tranche_gate: plateau K=2 E K=3 promossi,
  PORT06 OOS Sh 10.07->10.15 DD 1.48->1.38, FULL pari): la fase del roll e'
  timing-luck (Sharpe daily 1.52-2.33, DD 13.8-33% sulle 12 fasi). Worker con
  param tranches (default 1), 3 sub-book sfasati hold/3 su capitale comune,
  migrazione status legacy, last_bar_ts solo-avanti; runner forward del param;
  _defs tranches=3; hourly_report aggrega i sub-book; validatore esteso e
  PASSATO (K=1 == xsec_sim esatto, K=3 == unione fasi esatto).
- Disaster-cap z sui pairs: pre-registrato e BOCCIATO su tutti i criteri (coda
  OOS peggiora 4/6 coppie, Sharpe -10..-49%, plateau solo del danno; 5a conferma
  stop-su-MR). Record pairs_zstop_research.py; pairs restano senza stop.
- Audit drift: regression-lock trendmax OK (parita' 1.00000, plateau 2.5/3.0/3.5
  confermato), correlazioni cross-famiglia ~0 invariate; PORT06 rolling al
  19-28mo pct (normale) ma FADE 120g al 2o percentile storico -> monitor in TODO
  (nessun ritocco parametri).
- TODO: forming-bar ROT02/TSM01 era gia' fixato (v1.1.10), item chiuso.

Test: pytest 99 passed; validate_honest_workers OK; validate_xsec_worker OK.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 13:29:14 +00:00
Adriano Dal Pastro ba5ec7bd6b docs: aggiorna strategie_attive.html + CLAUDE/TODO a v1.1.20 (XS01, real-truth, 19 sleeve)
- make_strategy_doc: nuova famiglia XSEC — card XS01 (meccanismo, dispersion-gate
  disp_min=0.0313, gate PORT06 OOS 10.07->10.37) con grafico book+gate e tabella
  per anno (engine canonico ungated); pesi aggiornati a 19 sleeve (5.69/5.26/2.94%);
  header con real-truth ledger e 4 book paper; bullet metodologia real-truth;
  ripuliti i riferimenti 'oggi' (EXIT-16 su DIP01, fix punto-10 SH01)
- strategie_attive.html rigenerato: v1.1.20, backtest canonico FULL Sharpe 7.34 /
  DD 3.46% — OOS Sharpe 10.07 / DD 1.48% (19 sleeve, XS01 incluso)
- CLAUDE.md: sezione XS01 (famiglia XSEC, gate, dispersion-gate live-only, FC01
  scartata), copertura reale 15/19, varianti gioco options/session/grid in struttura
- TODO.md: stato esecuzione aggiornato (6 pairs, XS01 paper, real-truth)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 10:00:48 +00:00
Adriano Dal Pastro 8d69a0cef5 feat(games): sessioni 2-3 Blind Traders (opzioni/session/grid) + gate PORT06 e tooling reset
- Gioco GRID TRADERS (sessione 3, regola STRATEGIA_GRIGLIA.md): grid_engine
  (backtest causale fee-aware della griglia geometrica), grid_brief (digest
  anonimo per dimensionare la griglia), grid_arena (torneo 100 agenti);
  diario docs/diary/2026-06-10-grid-traders-game3.md
- Gioco OPZIONI: options_engine (BS + skew fittato + DVOL storica),
  options_arena, opt_calibrate (superficie premi REALE da cerbero-bite)
- Gioco SESSION: session_engine/session_arena (pattern orari intraday)
- arena: vincolo GAME_NO_LIVE=1 (vieta pairs e fade zscore/breakout/momentum
  gia' live, coercizione a trend/ma_cross) + normalize del candidato PRIMA
  della valutazione nel hill-climb
- Gate: grid_game_gate (griglia ETH vincitrice vs PORT06, mark-to-market),
  pairs30m_gate (ETH/BTC 30m ridondante col 15m gia' deployato?)
- reset_flatten: flatten one-shot del conto testnet per il reset portafoglio
- .gitignore: data/portfolio_paper_stats/ (stato runtime sleeve paper-only)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 09:49:17 +00:00
Adriano Dal Pastro 8adf388e86 release: v1.1.20 2026-06-10 21:42:12 +00:00
Adriano Dal Pastro e25d5db6ad feat(xsec): dispersion-gate XS01 live (disp_min=0.0313) — Sharpe 3.46, PORT06 OOS 10.07->10.37; FC01 funding-carry scartata
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 21:42:12 +00:00
Adriano Dal Pastro d1180ef25b release: v1.1.19 2026-06-10 21:24:00 +00:00
Adriano Dal Pastro 61fcc29c5b fix(xsec): cast numpy->Python in _save/_close_book — int64 rompeva json.dumps e bloccava il runner in error-streak
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 21:23:52 +00:00
Adriano Dal Pastro 5e557fe998 release: v1.1.18 2026-06-10 12:23:58 +00:00
Adriano Dal Pastro cc39c36c08 feat(live): REAL-TRUTH ledger — capital aggiornato dal PnL dei fill reali, sim ridotto a diagnostica
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 12:23:52 +00:00
Adriano Dal Pastro d2948d582b release: v1.1.17 2026-06-09 21:38:12 +00:00
Adriano Dal Pastro a85289d7c7 feat(xsec): XS01 reversione cross-sectional (8 asset) -> PORT06 PAPER
Famiglia NUOVA trovata in sessione (dopo aver scartato trend/breakout/seasonal/
opzioni/funding come rumore): ogni 12h long i perdenti relativi / short i vincenti
su 8 asset, market-neutral. Scorrelata (~0) da pairs e fade -> diversificatore.

- engine canonico scripts/strategies/XS01_cross_sectional.py (no look-ahead, plateau
  OOS Sharpe 2-3.9, 5/5 anni+, edge concentrato 2025, cost-sensitive ~0.35% RT).
- src/live/xsec_worker.py CrossSectionalWorker: validate_xsec_worker == backtest ESATTO
  (4993/1427 trade). Mirror della cadenza engine (entry-to-entry = hold+1).
- gate PORT06: +XS01 -> OOS Sharpe 9.66->10.07, FULL DD 3.68->3.46 (OOS DD +0.17pp,
  risk-contrib 2.2%). xsec_port06_gate.py.
- wiring: _defs XSEC in PORT06 (19 sleeve, family XSEC), build_everything, runner
  kind=xsec, asset_days da supported (fix fetch alt anche per paper sleeves), paper.
- 8 gambe -> niente exec reale -> gira PAPER. Regression-lock 18->19, FULL 7.20->7.34,
  OOS 9.66->10.07. 93 test verdi. Diario 2026-06-09-xs01-cross-sectional.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 21:38:05 +00:00
Adriano Dal Pastro d3dab57532 feat(games): sessione 2 del gioco Blind Traders su timing diversi (30m/2h/4h)
- engine: resampling (_RESAMPLE) per 30m/2h/4h/1d + TF_BPM esteso -> nuovi timing.
- arena/run_game: TIMEFRAMES estesi, out_name e GAME_SPECS_DIR/GAME_OUT parametrizzati
  (game 1 non sovrascritto).
- Risultato: 10 finalisti tutti 30m pairs ETH/BTC (vincitore #36: OOS Sh 12.3, 43 tr/mese).
  La regola >=10 trade/mese filtra i tf lunghi (4h: 4/33 qualificati). Conferma la
  frontiera frequenza-vs-edge. Diario 2026-06-09-blind-traders-game2.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 13:01:34 +00:00
Adriano Dal Pastro abc1c1cb80 docs(diary): analisi robustezza ETH/BTC 15m — robusto come segnale, fragile sui costi
Plateau 16/16, 8/9 anni+, OOS Sh 17.6, non-artefatto-flat (83% Sharpe), corr 0.37.
MA fee-sweep: il 15m va negativo a ~4x i costi mentre il 1h regge ~6x (frequenza 5x,
margine di costo sottile). Conferma mezza-size e NO allo swap; giudice = ledger reale.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 12:34:08 +00:00
Adriano Dal Pastro 1a9032feaa docs(diary): statistiche per-anno delle 15 sleeve attive in real
Snapshot post-BLEND 15m (v1.1.16): PnL%/n-trade per anno + maxDD per ogni sleeve
che esegue reale (6 fade + DIP01 + 6 pairs + 2 SH01), engine path-live, fee incluse.
+ aggregato PORT06 per anno. Esclusi i 3 book PAPER (TR01/ROT02/TSM01).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 12:12:53 +00:00
Adriano Dal Pastro 90c4726a31 docs: aggiorna CLAUDE.md + strategie_attive.html col BLEND ETH/BTC 15m
- CLAUDE.md: default PORT06 (FULL 7.20/OOS 9.66, 18 sleeve), paragrafo BLEND 15m
  flat-skip (origine gioco Blind Traders, gate, validazione, caveat slippage),
  copertura reale ~83% (6 pairs), scripts/games/ + pairs15m_* nella struttura.
- make_strategy_doc.py: header dinamico dal backtest, colonna ETH/BTC·15m nella
  tabella pairs, card PR01 col blend, conteggi sleeve aggiornati -> rigenerato HTML.
- pairs_sim_flat: ritorna yearly_n (parita' con pairs_sim, usato dal doc).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 11:59:36 +00:00
Adriano Dal Pastro be72b157cf release: v1.1.16 2026-06-09 11:48:26 +00:00
Adriano Dal Pastro d25d897fd1 feat(pairs): attiva ETH/BTC 15m flat-skip in PORT06 (BLEND, mezza size)
Origine: gioco "Blind Traders" (100 agenti ciechi su BTC/ETH anonimizzati) ->
vincitore = spread ETH/BTC reversion a 15m. Testato sul serio col gate PORT06:
non duplicato (corr 1h vs 15m = 0.37), robusto (16/16 celle Sharpe>1), edge NON
artefatto delle candele flat ETH 15m (filtrandole resta l'83% dello Sharpe).

Percorso live costruito e validato:
- pairs_research.pairs_sim_flat: engine generalizzato con exit LIVE-REALIZABLE
  (arma exit_ready, esce alla 1a barra pulita); regression-lock a pairs_sim.
- PairsWorker: flat_skip + exit_ready + rilevamento flat da OHLC (1h byte-exact).
- runner: fetch diretto dei timeframe sub-orari + override position_size per-sleeve.
- validate_worker_pairs: replay worker == backtest a 15m (8452 vs 8453 trade).
- _defs/build_everything: sleeve PR_ETHBTC_15M (mezza size, pos 0.10) -> PORT06
  FULL 6.43->7.20, OOS 8.58->9.66, DD giu'. Rischio bilanciato col 1h.
- smoke live: Cerbero serve candele 15m fresche; worker ticca.

Diari docs/diary/2026-06-09-*. Caveat slippage: mezza size = blend-tilt prudente.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 11:48:15 +00:00
Adriano Dal Pastro 5d45f4ef6e research(cerbero-bite): validazione edge credit-spread su prezzi reali
Backtest dell'edge credit-spread ETH di cerbero-bite con la chain reale
(data/options/). Esito: NON edge robusto su ciclo completo. Entry cw reale
0.106 (short 9.4% OTM, max-loss/credito 8.4x); hold-to-expiry EV -1.0 cr/trade
7/9 anni negativi; managed (skew) EV -0.02 cr win-rate 37%. Il "+0.48%/mese"
era artefatto di finestra calma; coda concentrata col fade ETH. TODO aperto:
calibrazione esatta credito (bid/ask + griglia) per EV managed definitivo.
Script riprendibile + diario.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 08:08:53 +00:00
Adriano Dal Pastro cfc48cdef4 release: v1.1.15 2026-06-09 07:43:09 +00:00
Adriano Dal Pastro aa4361ddde feat(live): MR02/ETH stop-largo sl_atr 3.0 (miglioria gated, regola sl=0 rispettata)
Esito ricerca sostituto/miglioria MR02/ETH: nessun sostituto batte il live
EXIT-16; la miglioria deployabile e' allargare lo stop close-confirm da
sl_atr 2.0->3.0 sul solo MR02_ETH. Gate PORT06 (swap MR02_ETH): FULL Sh
6.73->6.77 DD 3.67->3.30, OOS 8.80->8.82 DD 1.23 (domina il live). Cattura
il beneficio del no-SL tenendo lo stop di catastrofe: worst-trade -50% vs
-65% del no-SL -> regola "mai sl=0" rispettata. Override scoped a MR02/ETH
(_fade_params); altri 5 fade invariati. 93/93 test ok.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 07:31:55 +00:00
Adriano Dal Pastro c997bce00e feat(options): attach_market() panel helper (regime panel, causal, no look-ahead)
attach_market(df, asset): merge_asof causale del pannello market_snapshots
(spot/VRP/funding/net-GEX/gamma-flip/liquidation) sul prezzo, pronto per
regime_lab. Fix look-ahead: merge su datetime tz-aware (ns-aligned), MAI
astype(int64) su datetime (darebbe ns -> match all'ultimo snapshot = leak).
Copertura reale documentata: net-GEX denso solo da ~2026-05-01 (~5-6 sett,
singolo regime) -> infrastruttura pronta, edge validabile solo forward.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 07:24:44 +00:00
Adriano Dal Pastro df8bfbceaa research(mr02eth): ricerca sostituto + integrazione dati opzioni cerbero-bite
Ricerca sostituto/miglioria a MR02/ETH (127 strategie + 18 overlay opzioni,
verifica avversariale, gate PORT06). Esito: miglioria = no-SL (gia' ~catturata
da EXIT-16 live); tail-hedge opzioni NO-GO empirico su prezzi reali.

Infrastruttura opzioni REALE (muro ARGO/GEX caduto):
- options_fetcher.py: importa lo storico per-strike + market_snapshots da
  cerbero-bite -> data/options/ (chain bid/ask/IV/greche/OI + pannello regime
  con net-GEX dealer/gamma-flip/VRP/funding/liquidation).
- options_chain.py: loader + skew_curve/premium_levels (aggregati reali) +
  quote() causale + load_market() (pannello regime).
- option_overlay_lab.py: overlay opzioni BS su DVOL (pricing sintetico).
- mr02eth_port06_gate.py / eth_collar_gate.py: gate swap-sleeve e collar.
- mr02eth_search/options.workflow.js: i 2 workflow.

Numeri reali: skew put 10%OTM ~1.1 (liquido), premio ~1.0%/mese; niente strike
10%OTM a 24h (overlay per-trade infattibile); collar standing paga nei crash ma
net-negativo a PORT06 (alza OOS DD). Diario + CLAUDE.md aggiornati.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 07:05:00 +00:00
Adriano Dal Pastro cff40283f4 docs(report): PR01 + SH01 in esecuzione reale, pool real-only 14 sleeve
Aggiorna strategie_attive.html (e il generatore make_strategy_doc.py) allo
stato post v1.1.12/1.1.13/1.1.14:
- PR01 (5 coppie) e SH01 (BTC/ETH): badge SIMULATO -> ESECUZIONE REALE
- header: pool live real-only 14 sleeve (3 multi-asset in statistica)
- metodologia: esecuzione reale ~81% (fade+DIP single-leg, pairs 2 gambe,
  SH01 a orizzonte); restano simulati solo TR01/ROT02/TSM01

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 15:08:30 +00:00
Adriano Dal Pastro ce25be0475 release: v1.1.14 2026-06-08 11:29:00 +00:00
Adriano Dal Pastro 0e8ad9c5f0 feat(live): portafoglio REALE-only — i 2000EUR ai soli 14 sleeve eseguiti, paper fuori dal pool
Decisione utente: il conto reale deve mostrare il risultato puro. I 2000 si dividono
SOLO tra i 14 sleeve reali (6 fade + DIP01 + 5 pairs + SH01). I 3 multi-asset
(TR01/ROT02/TSM01), non eseguibili in reale, escono da pool/pesi/ledger e girano solo
per statistica.

- portfolios.yml: paper_sleeves [TR01,ROT02,TSM01].
- runner: live_specs (pool/ledger) vs paper_specs (capitale nozionale fisso = total/N,
  data/portfolio_paper_stats/, ticcati ma MAI in update_equity).
- hourly_report: sezione PAPER da portfolio_paper_stats, etichettata 'fuori dal conto reale'.

Pesi reale-only (14, cap): non-cappati 0.087, PAIRS 0.33, SHAPE 0.0588. Costo misurato
e accettato: FULL DD 3.96->5.34%, OOS DD 1.36->1.70% (perde i diversificatori PAPER,
corr 0.07); Sharpe ~invariato. Test 93/93. Diario docs/diary/2026-06-08-real-only-portfolio.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 11:29:00 +00:00
Adriano Dal Pastro 3d3ebd85d2 release: v1.1.13 2026-06-08 11:18:06 +00:00
Adriano Dal Pastro 0b900a5420 feat(live): SH01 in esecuzione reale shadow (diversificatore decorrelato)
L'infrastruttura no-TP esisteva gia': _place_real_tp ritorna subito senza TP,
_real_close chiude tutta la quota a market reduce-only (exit a orizzonte H=12).
Bastava: (1) _exec_for accetta kind 'ml', (2) SH01 in execution.sleeves.
Disaster-bracket on-book = unica protezione di coda di SH01 (esce a H=12 ben prima
del -30%). Motivo: SH01 e' il diversificatore piu' decorrelato (corr 0.07) — senza
i 5 sleeve PAPER il DD del portafoglio sale 3.96->5.35%, OOS Sharpe 8.58->8.27.

Test: SH01 open/close reale senza TP + disaster bracket (93/93). Copertura reale
ora ~81% (resta paper solo TR01/ROT02/TSM01, bloccati dal capitale).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 11:18:06 +00:00
Adriano Dal Pastro c6970d96c9 release: v1.1.12 2026-06-08 10:26:23 +00:00
Adriano Dal Pastro 2da19c3a7e config(pairs): attiva pairs_enabled (esecuzione reale 2 gambe) a conto flat
Smoke testnet end-to-end verificato (v1.1.11). Conto Deribit flat su tutti gli
strumenti pair confermato in tempo reale prima dell'accensione. I 5 pairs ora
eseguono reale shadow a 2 gambe; shadow pulito dalla prossima apertura pair.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 10:26:23 +00:00
Adriano Dal Pastro 59901e236b release: v1.1.11 2026-06-08 10:19:56 +00:00
Adriano Dal Pastro c655604131 feat(live): executor a 2 gambe per i pairs (PairsExecutionClient, shadow) — pronto, spento
PairsExecutionClient (compone ExecutionClient): open_pair (2 market long A/short B,
verifica per-gamba, LEG-RISK unwind se una sola filla), close_pair (2 reduce-only via
close_amount, MAI close_position), register_contract (fetch spec USDC). Spec LTC/ADA/SOL
aggiunti. PairsWorker: ledger reale shadow a 2 gambe resume-safe (_real_open_pair/
_real_close_pair, PnL per gamba dir A=+d/B=-d, doppio arrotondamento riportato). Runner:
pairs_executor gated su execution.pairs_enabled (false di default).

Validazione: test 92/92 (open/close, leg-risk unwind, resume) + smoke testnet
end-to-end (open 2 gambe verificate, close reduce-only, PnL reale -0.039 vs sim -0.036,
conto flat). Smoke ora aborta se ci sono posizioni di produzione + usa solo close_amount.

NB incidente testnet documentato (diario): pulizia manuale con close_position ha flattato
le quote shadow dei fade sul conto condiviso -> auto-riconciliazione al prossimo close.
Lezione: mai close_position su strumenti condivisi.

pairs_enabled resta FALSE: accendere con finestra a conto flat + osservazione.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 10:19:56 +00:00
Adriano Dal Pastro a2f1b960ec research: gate PORT06 index_comp_disp — promosso marginale, documentato e rimandato
Decorrela bene (corr 0.06 col MASTER, smentisce il timore ridondanza) ma beneficio
OOS nullo (Sharpe 8.58->8.56, DD 1.36->1.40); migliora solo FULL DD 3.96->3.73. Non
deployato (wiring + simulato per guadagno OOS nel rumore). Gate riusabile committato.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 10:06:34 +00:00
Adriano Dal Pastro d277d02cf5 release: v1.1.10 2026-06-08 10:00:16 +00:00
Adriano Dal Pastro f509f51543 fix(live): ROT02/TSM01 scartano la barra 1d in formazione (forming-bar in _panel)
Code-review follow-up: i worker daily valutavano momentum/regime e bookavano il
return sulla candela 1d PARZIALE al primo poll dopo mezzanotte UTC, poi last_bar_ts
bloccava la rivalutazione a giorno chiuso (stessa classe del bug gia' fixato su
fade/TR01/Pairs). Fix in _panel (un punto solo -> ROT02 e TSM01 lo ereditano) via
src.live.bars.last_bar_is_forming. Replay honest invariato (dati storici: nessuna
barra in formazione). Test: 88/88 + parity drop forming daily bar.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 10:00:16 +00:00
Adriano Dal Pastro 7f4378190e research: ricerca dispersion/correlation index (165 agenti) — 2 edge su 60, nessun nuovo motore
Workflow 60 celle (15 famiglie x 4 finestre), verifica avversariale 2-skeptic.
Esito: 2 edge confermati causali (index_comp_disp W168, rel_idio_fade W24), ma
entrambi fade-BTC del residuo idiosincratico -> sovrapposti alle MR esistenti
(P migliora-PORT06 ~20%, lezione FR01). 13/15 famiglie rumore (overfit/regime,
non look-ahead). Edge preservati in scripts/analysis/dispersion_edges/; harness
riusabile dispersion_lab.py (gia' committato). Diario + TODO follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 03:32:31 +00:00
Adriano Dal Pastro cb122683fe feat(research): harness condiviso dispersion/correlation index (feature causali + cache)
Feature realized causali (avg pairwise corr, cross-sectional dispersion, beta vs
indice EW, componente idiosincratica) su universo 8-asset, finestra comune dal
2022-07. NO implied (opzioni non backtestabili). Check no-look-ahead OK. Cache su
disco per il fan-out di ricerca. Base per la ricerca multi-agente dispersion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 21:16:10 +00:00
Adriano Dal Pastro 9275eab4af docs: TODO follow-up post-sweep (executor 2-gambe pairs, SH01 reale, capitale, code-review minori)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 20:57:28 +00:00
Adriano Dal Pastro 2a97294d67 docs: statistiche per anno (trd/PnL%/maxDD) per strategia e mercato nel doc HTML
Ogni card ora include la tabella anno x mercato:
- fade MR01/02/07 (BTC+ETH) e DIP01: calcolate col PATH LIVE (EXIT-16 + trend 3.0),
  trades/PnL per anno di entry, DD per anno su equity compounding pos 0.15 + DD totale
- PR01: matrice anno x 5 coppie, cella = PnL% (n trade, DD anno) + riga TOT
  (pairs_sim espone ora anche yearly_n, modifica non-breaking)
- TR01/ROT02/TSM01: ret%/DD% per anno dall'equity canonica daily 2021+
- SH01: per anno dal walk-forward EXPANDING (regime validato e ora live)
Nota di convenzione su ogni tabella (leva 3x test vs 2x live, fee incluse) +
caveat: finestra canonica dal 2021, anni 2018-2020 mostrati per onesta' storica.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 17:32:00 +00:00
Adriano Dal Pastro bc0ad3e86f docs: documento HTML strategie attive PORT06 (descrizioni + grafici da episodi reali)
scripts/analysis/make_strategy_doc.py genera docs/report/strategie_attive.html
(autocontenuto, PNG base64): le 6 strategie attive per famiglia con tesi,
config live, badge sim/reale e 11 grafici costruiti su EPISODI REALI dai
parquet locali — fade con bande/canale/z e trade annotato (entry/TP/SL),
explainer EXIT-16 (wick che buca lo SL senza conferma sul close), DIP01,
TR01 EMA 4h, ROT02 (gate regime + ranking momentum), PR01 (gambe normalizzate
+ z-score del ratio con soglie), TSM01 (risk-off + heatmap consenso),
SH01 (finestra forma + schema walk-forward), pesi cap del portafoglio.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 16:55:45 +00:00
Adriano Dal Pastro cc9a380a63 release: v1.1.9 2026-06-07 16:48:22 +00:00
Adriano Dal Pastro 8c5372c487 feat(SH01): bootstrap storia full-history + last_block_only (punto-10: regime live non robusto)
Ri-validazione sh01_trainwindow_validate.py (rolling train_window == regime live):
- tw=8760 (live 365g): BTC FULL +82% ma fee-2x -42% (6/9 anni), ETH Sharpe -0.02,
  trade-rate 21.7-26.4% vs 9.8% validato -> NON robusto. Diagnosi sweep confermata
  (LogReg over-confident su train corto, th 0.58 inerte).
- progressione MONOTONA con la memoria (2y: Sh 2.05; 3y: 3.09; expanding: ROBUSTO
  8/9) -> l'edge di SH01 e' la memoria lunga.
- sweep soglia nel regime corto: instabile/incoerente fra asset -> NON ri-tunare.

Fix (decisione utente): ripristinare in live il regime validato.
- ml_wf_entries(last_block_only=True): fitta/predice solo l'ultimo blocco del WF
  (confini deterministici start+k*retrain) -> entries IDENTICHE per costruzione
  al tail del WF completo (parity test esatto); 0.6s/tick su 73k barre vs ~140 fit.
- runner._with_history: tick degli sleeve ml su parquet locale + feed live
  (dedup timestamp, gap-guard con WARN una-tantum se parquet stantio).
- _defs.py: params {last_block_only: true} sugli sleeve SHAPE (solo path live;
  il backtest canonico resta WF completo).

Effetto atteso live: trade-rate SH01 ~25% -> ~10% delle barre, selettivita'
ripristinata. Manutenzione: parquet fresco via download_all (oggi 2026-05-28,
margine ~11 mesi col feed 365g).

Test: 87/87 (4 nuovi: parity last-block esatta, merge storia, gap fallback).
Diario docs/diary/2026-06-07-sh01-trainwindow.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 16:48:22 +00:00
Adriano Dal Pastro be85a454e0 release: v1.1.8 2026-06-07 16:19:25 +00:00
Adriano Dal Pastro ed2a9013aa feat(DIP01): EXIT-16 close-confirm SL (gate gap-aware 36/36 BTC, PORT06 promosso)
Punto 9 roadmap sweep. DIP01 era l'unico sleeve BTC con esecuzione reale
round-trip ancora sul branch SL intrabar wick-sensitive.

Gate dip01_exit16_impact.py (parita' canonica 1.00000; engine GAP-AWARE: orig
filla lo SL a worse(livello, open) per rimuovere il bias pro-stop-intrabar
dell'engine canonico sui gap-through):
- grid 3x3x2 BTC: EXIT-16 >= orig in 36/36 (train E oos), OOS Sharpe ~2-4x
  (canonica 1.47->3.48); ETH 35/36 (robustezza).
- plateau buffer piatto 0.4-1.0 (OOS DD 6.4% identico) -> 0.5 come le fade.
- PORT06: FULL Sh 6.43->6.61 DD 3.96->3.58 | OOS Sh 8.58->8.77 DD 1.36->1.34.
5a conferma del principio EXIT-16 (wick-stop = falsi negativi per mean-reversion).
hurst_max non valutato (ridondante-dannoso post-EXIT-16, gate trendmax).

Deploy: "sl_confirm_atr": 0.5 nei params DIP01_BTC (il worker legge gia').

Trappola di metodo documentata: la prima parita' falliva per l'ancora bfill di
_daily_equity (la serie a punti-trade reindexata su IDX aggancia il primo valore
al PRIMO trade in finestra, non al capitale portato avanti) — replicata per
parita'; finding aperto perche' tocca le metriche canoniche di tutti gli sleeve
a punti-trade. Diario docs/diary/2026-06-07-dip01-exit16.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 16:19:25 +00:00
Adriano Dal Pastro bf1cada33f release: v1.1.7 2026-06-07 14:59:58 +00:00
Adriano Dal Pastro f5173fba06 fix(live): 4 hardening da code-review — REAL_DIVERGENCE, outage su feed vuoto, bars.py condiviso, DSL cancel retry
Review multi-agente 7-angoli su 8c4e1cd..HEAD + check trades live:

1. Alert Telegram REAL_DIVERGENCE quando |slippage sim/reale| >= 100bps a
   open/close. Causa scatenante: spike print testnet 10:37 (candela 10:00
   H=65618, O/C ~62400) -> 3 fade BTC short su close fantasma 65266.5, reale
   fillato a 62395 (-440bps), sim +2.26 mai esistiti — passato in silenzio.
2. FEED_OUTAGE anche su feed degradato SENZA eccezione (HTTP 200 + candles
   vuote: i worker saltavano il tick in silenzio, streak a 0). Helper unico
   _outage_tick; chiavi payload uniformate (minuti su start e RIPRESO).
3. src/live/bars.py: detection forming-bar unificata (bar_ms_of /
   last_bar_is_forming / last_settled_idx) — era copiata in 4 punti
   (strategy_worker, basket, pairs, _check_stale_feed hardcoded 1h).
   E' l'invariante di sicurezza EXIT-16: ora una sola implementazione testata.
4. DSL cancel hardening in _real_close: retry su errore transitorio + alert
   REAL_DSL_CANCEL_FAIL se lo stop resta forse orfano sul book (prima l'id
   veniva dimenticato in silenzio); order_not_found = probabile trigger in
   outage -> solo log (il close a valle esce gia' verified=False).

Refutato il finding top dei finder ("stop_market senza trigger"): cerbero-mcp
traduce price->trigger_price+mark, e in produzione 2 DSL armati + 1 ciclo
completo pulito (MR07_BTC).

Test: 83/83 (9 nuovi: bars helper + DSL/divergence con executor finto).
Smoke testnet 4 scenari verdi, conto flat, zero falsi allarmi.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 14:59:58 +00:00
Adriano Dal Pastro 7bf8b197e0 release: v1.1.6 2026-06-07 10:44:15 +00:00
Adriano Dal Pastro 6097205694 feat(pairs): position_size per-famiglia, PAIRS 0.5->0.20 (gate PORT06)
Punto 8 roadmap sweep. La famiglia pairs (SENZA stop, validata a pos 0.15 lev 3 =
esposizione 0.45) girava col position_size globale 0.5 a leva 2 = esposizione 1.0,
~2.2x il validato (ETH/BTC DD grezzo 78% a quella taglia; live: ADA/ETH -4.26%
sleeve in un trade il 2026-06-05).

Gate pairspos_port06_impact.py (pairs_sim alla leva live 2x, parita' canonica
1.00000 5/5, PORT06 pesi cap): griglia pos {0.5, 0.25, 0.20, 0.15}. Il gate
meccanico boccia ogni riduzione (costa OOS Sharpe — il compounding in-sample a 0.5
e' fantasia: +2.6e9% ETH/BTC), ma 0.20 e' il ginocchio del trade-off: OOS DD
3.40->1.26% (meglio anche di 0.15), famiglia DD 14->5.8%, costo OOS Sharpe
9.05->8.43. Decisione assicurativa (utente), come il precedente cap SHAPE.

- runner.pos_for_spec(sid, global, family_overrides): override per-famiglia
  (chiave weighting.family_of), plumbato in build_worker_for.
- portfolios.yml: position_size_family {PAIRS: 0.20} (globale 0.5 invariato).
- Test: pos_for_spec mapping + PairsWorker.position_size (74/74 verdi).

Nota transizione: la posizione LTC/ETH aperta verra' chiusa col pos nuovo
(one-time). Diario docs/diary/2026-06-07-pairspos-gate.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 10:44:15 +00:00
Adriano Dal Pastro 0aaab08612 release: v1.1.5 2026-06-07 10:29:57 +00:00
Adriano Dal Pastro e3bb622b90 feat(fade): swap filtro live hurst->trend_max 3.0 (gate PORT06 sul path live)
Punto 7 roadmap sweep. Gate trendmax_port06_impact.py (engine exit16_port06_impact
riusato, parita' canonica 1.00000; maschera hurst IDENTICA al live via
fade_base.hurst_skip_mask; PORT06 pesi cap, path live EXIT-16):

- CANDIDATO (hurst+trend) BOCCIATO: over-filtering (FULL Sharpe 7.23->7.11,
  meta' dei trade) nonostante DD 2.68->2.06.
- SCOPERTA: il loss-guard Hurst e' ridondante-DANNOSO post-EXIT-16 (NESSUNO
  batte LIVE: FULL Sh 8.07 vs 7.23). EXIT-16 ha eliminato i wick-stop che hurst
  evitava -> gli ingressi saltati (66% delle barre) sono tornati vincenti.
  Il test che promosse hurst (2026-06-02) era sull'engine PRE-EXIT-16.
- TREND-ONLY domina LIVE su tutte le metriche (FULL Sh 7.89 DD 2.46, OOS Sh 9.91
  DD 1.20) ed e' la config che la ricerca EXIT-16 aveva davvero promosso (entries
  trend-filtrate, no hurst) mai eseguita dal live. Plateau 2.5/3.0/3.5 robusto.

Decisione (utente): SWAP. _defs.py: trend_max=3.0 + ema_long=200 nelle 6 fade,
hurst_max rimosso (hurst_skip_mask resta in fade_base). hourly_report: monitor
stop-rate per epoca PRE->HURST->TREND (verdetto a n>=30). CLAUDE.md aggiornato
(paragrafo hurst marcato storico). Diario docs/diary/2026-06-07-trendmax-gate.md.

Lezione: ri-gateare ogni filtro quando cambia l'exit engine.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 10:29:57 +00:00
Adriano Dal Pastro 11ace196c7 release: v1.1.4 2026-06-07 09:36:17 +00:00
Adriano Dal Pastro 62e272255b feat(live): disaster-bracket on-book sui fade reali + alert FEED_OUTAGE + osservabilita' multi-asset
Punti 5-6 dell'improvement-sweep 2026-06-06 (protezione capitale + osservabilita'):

Punto 5 — disaster bracket:
- ExecutionClient.place_disaster_sl: STOP_MARKET reduce-only a ~-30% dall'ingresso
  (trigger mark price), piazzato a ogni REAL_OPEN (MR01/MR02/MR07/DIP01) e
  cancellato in _real_close. Assicurazione outage: il poll-loop in except lascia
  le posizioni reali senza valutazione exit (ETH gap max storico 33%/1h). In
  operativita' normale non scatta mai -> 0 costo Sharpe. real_dsl_order_id
  persistito (resume-safe). Config overrides.execution.disaster_sl_pct (0.30).
- NB: set_stop_loss di cerbero-mcp e' un private/edit Deribit (solo ordini APERTI)
  -> non usabile su market fillati; il bracket e' un trigger order autonomo via
  place_order(type=stop_market). Cancel di un trigger order risponde 'untriggered'
  (= successo, verificato testnet: re-cancel -> order_not_found).
- Runner: alert Telegram FEED_OUTAGE dopo 5 poll falliti consecutivi (elenco
  posizioni reali aperte) + notifica RIPRESO con durata.

Punto 6 — osservabilita':
- in_position nei _save() di TR01/ROT02/TSM01; hourly_report: sezione MULTI-ASSET
  (book | ultimo flip | freschezza status) — prima i 3 worker erano invisibili
  (collect() filtra su event/in_position che non emettevano); esclusi dalla
  tabella IN CORSO (assume entry/bars single-leg).
- live_shadow_smoke esteso: scenari C/D SHORT (TP-resting BUY mai esercitato
  prima) + disaster bracket in tutti gli scenari.

Verifiche: 72/72 test; smoke testnet 4 scenari verdi (DSL piazzato/cancellato due
lati, zero ordini orfani sul book, conto flat); multi_asset_section renderizza sui
dati live. Diario docs/diary/2026-06-07-sweep-fixes.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 09:36:11 +00:00
Adriano Dal Pastro bd5f1a6ea0 release: v1.1.3 2026-06-07 08:23:25 +00:00
Adriano Dal Pastro 3909646028 fix(live): parita' worker multi-asset — TR01 fee x leva, forming-bar su TR01/Pairs, WARN panel corto
Punti 2-4 dell'improvement-sweep 2026-06-06 (fix di parita', nessun cambio di strategia):

- TR01 (basket_trend_worker): fee per flip = POS * LEV * fee_rt/2 come il reference
  honest_improve2._tr_basket_daily:150 (mancava il fattore leva -> fee sotto-caricate 2x,
  bias ottimistico che gonfiava total_capital al ribilancio).
- TR01: crossover EMA e booking del return valutati SOLO su barre 4h COMPLETE
  (la riga -1 e' la candela in corso finche' non e' trascorsa la sua durata) — lezione
  EXIT-16; evidenza live: flip SOL 0->1->0 in 59min nella stessa finestra 4h.
- PairsWorker: entry ED exit valutati sul close di barra COMPLETA, come il backtest
  pairs_research (close settled). Stesso pattern bar_ms di strategy_worker.tick.
- TSM01/ROT02: il silent return su panel inner-join troncato sotto il lookback ora
  emette WARN log + Telegram PANEL_SHORT (helper _warn_panel_short condiviso), gated
  su "era gia' operativo" (no falsi positivi al cold-start), una notifica per episodio.

Test: 72/72 verdi (7 nuovi: forming-bar TR01/pairs + controllo positivo, fee con leva,
PANEL_SHORT warn/dedup/cold-start). Replay parity: validate_worker_pairs ESATTO
(ETH/BTC e BTC/LTC == backtest); validate_honest_workers invariato (TR01 -44% vs ref
+42% IDENTICO pre/post fix: e' la divergenza di convenzione capitale-unico vs
media-equity gia' documentata, da rivisitare a parte).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 08:23:19 +00:00
Adriano Dal Pastro 9ce43031ee fix(PORT06): ripristina cap SHAPE 0.0588 in portfolios.yml (P0 improvement-sweep)
L'override caps in portfolios.yml SOSTITUISCE interamente il dict di _defs.py
(setattr in base.py:load_active_portfolio) -> il cap SHAPE introdotto in v1.1.2
era inerte e SH01 (senza SL by-design) girava a 2x peso. Ridichiarato il dict
completo {PAIRS: 0.33, SHAPE: 0.0588} + commento sul footgun.

Verificato live post-restart: SH_BTC+SH_ETH = 0.0588, non-cappati a 0.0647,
sum = 1.0 (== backtest canonico FULL 6.43 / OOS 8.58).

Include il diario dello sweep multi-agente 2026-06-06 (106 agenti, 37 finding
validi): fonte dei P0/P1 in roadmap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 08:07:00 +00:00
418 changed files with 25502 additions and 1668 deletions
+18
View File
@@ -18,7 +18,10 @@ data/processed/
notebooks/.ipynb_checkpoints/
data/paper_trades/
data/portfolio_paper/
data/portfolio_paper_stats/
data/portfolios/
# watermark fondi del reconciler (stato runtime, contiene il balance)
data/funds_watch.json
# stato locale di tooling (non condiviso)
.claude/
@@ -26,3 +29,18 @@ data/portfolios/
# dati regime (DVOL/funding/feature cache, rigenerabili)
data/regime/
_disp_scratch/
data/regime/dispersion_features.parquet
# storico catena opzioni importato da cerbero-bite (rigenerabile: options_fetcher.py)
data/options/
data/_reset_backup/
# game artifacts (log/json di scripts/games e gate)
data/games/
.env.mainnet
# archived data (mirrors top-level data/ ignores, which are top-level-anchored)
Old/data/
Old/**/__pycache__/
.cache_trackE_*.npy
+131 -350
View File
@@ -1,383 +1,164 @@
# PythagorasGoal — Istruzioni per agenti
## Panoramica
## Stato del progetto — v2.0.0 RESET (2026-06-19)
Progetto di ricerca: riconoscimento pattern frattali per trading algoritmico su criptovalute (BTC, ETH). L'obiettivo è arrivare a €50/giorno di profitto partendo da €1.000, entro 68 mesi.
**LEGGERE PRIMA DI TUTTO.** Il progetto è stato resettato dopo aver scoperto che l'intera
libreria di strategie "validata OOS" (FADE, PAIRS, DIP01, TR01, ROT02, TSM01, XS01, SH01) era
**artefatto di uno storico contaminato** — print fantasma del feed Cerbero **testnet** + storico
**Binance/USDT**. Ri-testate sul feed reale, tutte perdono ogni anno (vedi
`docs/diary/2026-06-19-deribit-history.md`, il documento di fondazione).
Cosa è cambiato:
- Lo storico è stato **ricostruito da Deribit mainnet** e **certificato**. Universo affidabile =
**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.**
- Si riparte dalla ricerca di strategie NUOVE, su dati certi, con la metodologia qui sotto.
### Ricerca post-reset (2026-06-19) — esito
Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condiviso
`src/backtest/harness.py`). Sintesi in `docs/diary/2026-06-19-research-synthesis.md`.
- **VINCITRICE (l'unica robusta e profittevole): TP01 Trend Portfolio** —
`src/strategies/trend_portfolio.py`. TSMOM multi-orizzonte (1-3-6 mesi) vol-targeted,
50/50 BTC+ETH. Config canonica **PORT LF4h** (4h, long-flat, vol-target 20%, leva cap 2x):
**CAGR ~16.6%, Sharpe ~1.32-1.36, maxDD ~12-14%, positiva ogni anno 2019-2026**.
Robusta su tutti i TF (15m-1d), regge fee fino a 0.40% RT, su entrambi gli asset.
Paper trader: `scripts/live/paper_trend.py`. Test: `tests/test_trend_portfolio.py`.
- **Edge deboli ma reali** (NON standalone, NON migliorano il portafoglio): ML walk-forward
su BTC (Sharpe ~0.57), trend 1h long-short (Sharpe ~1.0), relative-value market-neutral
ETH/BTC (scorrelato ~0.05 ma Sharpe solo 0.27 → troppo debole per alzare lo Sharpe).
- **MORTO/confermato artefatto:** mean-reversion / fade (negativo anche a fee zero su dati
certi — la vecchia libreria +201%/+1238% era pura contaminazione); trend 5m/15m (fee).
- **Soffitto strutturale:** con i soli BTC/ETH lo Sharpe di portafoglio si ferma a **~1.3**.
Combinare TF o aggiungere la RV non aiuta (ridondanza/edge troppo debole).
- **Onestà sul target €50/giorno:** NON raggiungibile su 2000 in 1-2 anni (servono ~130k di
capitale o un DD da rovina). La leva non è la scorciatoia; la via è target-vol + capitale +
tempo. La strategia che *guadagna* esiste, ma a ~+€1.5/giorno su 2000.
Script ricerca: `scripts/research/track{A,B,C,D,E}_*.py` + `trackD_timing.py`.
## Obiettivo
Ricerca: riconoscimento pattern frattali per trading algoritmico su crypto. Target dichiarato
€50/giorno partendo da €1.000. **Onestà prima di tutto**: nessun numero va creduto finché non è
netto fee, out-of-sample, robusto su griglia, e su dati certificati + liquidi + eseguibili.
## Stack
- **Linguaggio:** Python 3.11+
- **Package manager:** uv (dipendenze in `pyproject.toml`, lock in `uv.lock`)
- **Dati:** Parquet in `data/raw/` (non committati, ~70 MB)
- **ML:** scikit-learn (GradientBoostingClassifier)
- **Analisi:** numpy, pandas, scipy
- **API dati:** Cerbero MCP su `cerbero-mcp.tielogic.xyz` (Deribit, Bybit, Hyperliquid), ccxt/Binance come fallback
- **Config:** pyyaml per `strategies.yml`
- **Linguaggio:** Python 3.11+**Package manager:** uv (`pyproject.toml`, `uv.lock`)
- **Dati:** Parquet in `data/raw/` (gitignored). Solo BTC/ETH (5m/15m/1h).
- **Analisi/ML:** numpy, pandas, scipy, scikit-learn
- **Fonte dati storici:** Deribit mainnet via `ccxt` (pubblico, tokenless)
## Struttura
## Struttura (post-reset)
```
src/data/ → download e caricamento dati
downloader.py → download/caricamento parquet (gate: solo strumenti validati)
instruments.py → discovery + validazione strumenti per exchange, registry
src/data/downloader.py → load_data(asset, tf): legge i parquet certificati da data/raw/
src/strategies/base.py → Strategy (ABC), Signal, BacktestResult, YearlyStats
src/strategies/indicators.py → indicatori condivisi (ema, atr, keltner, ...)
src/strategies/trend_portfolio.py → TP01: strategia VINCENTE (PORT LF4h), causale, deployabile
src/fractal/ → indicatori frattali (patterns.py, indicators.py, similarity.py)
src/backtest/ → engine di backtesting (engine.py)
src/strategies/ → classe base Strategy ABC + indicatori condivisi
base.py Strategy, Signal, BacktestResult, YearlyStats
indicators.py → keltner_ratio, detect_squeezes, ema, atr, rv, correlation
src/live/ → paper trading live multi-strategia
multi_runner.py → orchestratore: carica YAML (strategies + pairs), fetch candele, tick worker
strategy_worker.py → worker single-leg: capital, trade log, stato persistente.
Exit guidati da strategia (TP/SL/max_bars via Signal.metadata),
fallback hold_bars/stop -2%. Usa fee_rt della strategia.
pairs_worker.py worker a 2 GAMBE per PR01 (market-neutral): long A / short B sullo
z-score del log-ratio, exit |z|<=z_exit o max_bars, fee su 2 gambe.
strategy_loader.py → import dinamico classi Strategy da scripts/strategies/
cerbero_client.py → client HTTP per Cerbero MCP (Deribit testnet): dati + ordini
(place_order market/reduce_only, get_trade_history, get_positions)
execution.py → ExecutionClient: esecuzione REALE su Deribit (shadow). notional→
amount (lineari USDC + inverse), open/close_amount reduce-only,
verifica sul trade (order_id), fee reali dai trades[]
signal_engine.py → squeeze + ML real-time (legacy ML01, ora in waste) + validazione OOS
telegram_notifier.py → notifiche Telegram per trade
src/portfolio/ → portafogli di prima classe (capitale-pool, backtest+live)
base.py → SleeveSpec, Portfolio (.backtest), load_active_portfolio
weighting.py → schemi pesi: equal/cap/inverse_vol/cluster_rp/manual
sleeves.py → builder unificato equity-per-sleeve (fonte unica, parità report)
ledger.py → PortfolioLedger: capitale/PnL/DD/persistenza+resume
runner.py → PortfolioRunner live (data Cerbero v2, sizing, ribilancio)
src/version.py → APP_VERSION (legge il file VERSION) — mostrato nei msg Telegram
src/strategies/fade_base.py → FadeStrategy + helper: atr, trend_distance, hurst_skip_mask (loss-guard),
exit close-confirm SL via param sl_confirm_atr (EXIT-16)
scripts/strategies/ → strategie con edge validato OOS: FADE (MR01/MR02/MR07),
HONEST (DIP01/TR01/ROT02), PAIRS (PR01), TSMOM + portafogli (PORT01/02/03);
FR01_hurst_calm_fade.py = record ricerca (robusto ma NON deployato)
scripts/portfolios/ → definizioni PORT01-06 (_defs.py) + report run() + hourly_report.py (Telegram)
scripts/waste/ → strategie scartate (W01-W28 + famiglia squeeze SQ/MT/ML/AD/CM/PD)
scripts/analysis/ → ricerca/validazione OOS fee-aware (strategy_research, oos_validation, ...);
regime_fetcher.py + regime_lab.py (DVOL/funding/feature regime per la ricerca);
exit_lab.py + exit_policies/ (harness ricerca exit: cache segnali, train/OOS)
scripts/bump_version.py, scripts/deploy.sh → versionamento e deploy (bump+commit+rebuild)
VERSION → versione semver (cotta nell'immagine, +1 ad ogni deploy)
strategies.yml → config multi-strategy paper trader
docs/diary/ → diario di ricerca giornaliero
docs/specs/ → specifiche di design
data/raw/ → file .parquet OHLCV (gitignored) | data/regime/ → DVOL+funding+feature (gitignored)
data/instruments_registry.json → allowlist strumenti validati (gate del downloader)
src/backtest/engine.py → engine di backtesting riusabile
src/backtest/harness.py → harness ONESTO (load BTC/ETH, backtest_signals no-leakage, OOS)
src/version.py → APP_VERSION (legge il file VERSION)
scripts/research/ → ricerca post-reset: track{A-E}_*.py (trend/ML/MR/portfolio/xsec)
scripts/live/paper_trend.py → paper trader forward-only di TP01 (no esecuzione reale)
scripts/analysis/ → SOLO i tool dati certificati:
rebuild_history.py → (ri)costruisce lo storico da Deribit mainnet (base 5m + resample)
certify_feed.py → certifica il feed (integrità, coerenza resample, spike, cross-venue)
audit_feed.py → audit per-barra vs riferimento esterno
multi_source_check.py → cross-check multi-venue (quale venue è "vero")
data/raw/ → btc/eth × {5m,15m,1h} (gitignored). UNICO dato attivo.
data/instruments_registry.json → registry strumenti (reference)
docs/diary/ → diario di ricerca (1 voce: il reset; aggiungere dopo ogni esperimento)
Old/ → ARCHIVIO: tutto il vecchio (strategie, live, ricerca, dati, diari)
VERSION → semver (2.0.0)
```
## Comandi
```bash
uv sync # installa dipendenze
uv run python -m src.data.downloader # scarica dati storici (solo strumenti validati)
uv run python -m src.data.instruments # (ri)costruisci il registry strumenti validati
uv run python scripts/strategies/MR01_bollinger_fade.py # backtest una strategia (es. fade)
uv run python scripts/strategies/PR01_pairs_reversion.py # backtest pairs market-neutral
uv run python scripts/analysis/strategy_research.py # ricerca strategie fee-aware OOS
uv run python scripts/analysis/oos_validation.py # perche' la famiglia squeeze e' scartata
uv run python scripts/analysis/report_families.py # report per anno di tutte le famiglie
uv run python scripts/analysis/validate_worker_pairs.py # replay worker 2 gambe == backtest
uv run python -m src.live.multi_runner # paper trading live multi-strategia (strategie + pairs)
uv run python scripts/portfolios/PORT06_master_shape.py # report backtest portafoglio (default)
uv run python -m src.portfolio.runner # paper trading a PORTAFOGLIO (capitale pool)
uv run python scripts/analysis/smoke_portfolio.py # smoke live data layer Cerbero v2
uv run python scripts/analysis/live_exec_smoke.py # smoke ESECUZIONE reale (ordine→verifica→fee, testnet)
uv run python scripts/analysis/live_shadow_smoke.py # smoke catena shadow nel worker (open/close reali)
uv run python scripts/analysis/regime_fetcher.py # fetch DVOL+funding (Deribit mainnet) -> data/regime/
uv run python scripts/analysis/exit_lab.py # (ri)costruisci cache segnali exit-lab + parity check
./scripts/deploy.sh [patch|minor|major] # DEPLOY: bump versione + commit + rebuild Docker
uv run python scripts/analysis/rebuild_history.py --asset BTC ETH # (ri)costruisci storico da Deribit mainnet
uv run python scripts/analysis/certify_feed.py # certifica i feed (locale + cross-venue)
uv run python scripts/analysis/certify_feed.py --local # solo check locali (veloce)
uv run python scripts/research/trackD_trendport.py # backtest strategia vincente (full report)
uv run python scripts/research/trackD_timing.py # vincitrice su 15m/1h/4h/1d + PnL/DD/trade per anno
uv run python scripts/live/paper_trend.py # avanza il paper trader TP01 (forward-only)
uv run pytest # test
```
> **Deploy.** Il sorgente è **COPY nell'immagine Docker** (non montato) → `docker compose restart`
> NON ricarica il codice: serve **`docker compose up -d --build`** (o `./scripts/deploy.sh`, che bumpa
> la versione, committa e rebuilda). Il volume `data/` persiste → i worker fanno RESUME dello stato.
> La **versione** (file `VERSION`, semver, +1 ad ogni deploy via `deploy.sh`) compare nei messaggi
> Telegram (notifiche trade + report orario) → correli ogni msg al codice che l'ha generato.
## Dati storici
Scaricati e salvati localmente in Parquet. Per rigenerarli:
```python
from src.data.downloader import download_all, load_data
download_all() # scarica BTC + ETH su 5m/15m/1h dal 2018
df = load_data("ETH", "15m") # carica un asset/timeframe
from src.data.downloader import load_data
df = load_data("BTC", "1h") # OK. load_data("SOL", ...) -> FileNotFoundError (guardrail: solo dati certi)
```
Fonte primaria: Cerbero MCP (endpoint `/mcp-deribit/tools/get_historical`).
Token observer: nel file `secrets/observer.token` del progetto CerberoSuite.
## IL DATO — fonte di verità (regola di prim'ordine)
### Strumenti & validazione (gate raccolta dati)
- **La verità è Deribit mainnet**, perché è dove (in futuro) eseguiamo. Cross-check multi-venue:
Deribit mainnet è a 0-1 bps dal consenso. **Binance NON è la verità** (è USDT, ~10 bps fuori, e
sotto depeg USDT fino al 3% off) → usare Binance/Coinbase SOLO come audit indipendente, mai come
ancora per "ripulire" i dati.
- **Aggiornare lo storico SOLO con `rebuild_history.py`** (ccxt Deribit mainnet, base 5m unica +
resample → coerenza interna garantita). **MAI** il vecchio downloader Cerbero (token testnet =
feed farlocco: è la causa della contaminazione).
- **Certificare sempre** dopo un rebuild con `certify_feed.py` (integrità OHLC, zero gap, coerenza
resample maxΔ≈0, spike = solo crash reali, accordo cross-venue per-anno vs Coinbase USD).
`src/data/instruments.py` scopre e **valida** gli strumenti per ogni exchange
implementato — **Deribit** e **Hyperliquid** (esclusi Alpaca/stocks e **Bybit**,
il cui feed testnet è farlocco). Per ogni perpetuo enumera via `get_instruments`
/`get_markets` e verifica sui **dati storici realmente raccoglibili**:
esistenza, congruenza OHLC, not-flat (scarta contratti morti), liquidità (volume
daily) e **congruenza prezzo cross-exchange** (scostamento dalla mediana del
base-coin ≤ 5% → scarta outlier come `SOL-PERPETUAL`=9.6 vs SOL reale ~82).
### Universo ricercabile certificato
- **BTC / ETH**: puliti (2-6 bps vs Coinbase USD su tutta la storia), liquidi (~0% barre flat a 1h),
storia lunga (2018/2019→oggi) → **ogni timeframe (5m/15m/1h)**. È l'unico dato in `data/raw`.
- **Alt (SOL/XRP/ADA/LTC/DOGE/BNB): FUORI.** Illiquidi (LTC 5m 82% barre flat O=H=L=C, run fino a
~3 giorni), divergenti (LTC/DOGE >1% su 10-21% delle barre 2022-23), o non certificabili
(XRP delistato da Coinbase per causa SEC; BNB non listato + storia da 2024-10). Sono archiviati in
`Old/data/raw`. Riammetterne uno richiede prima una ricertificazione che dimostri liquidità + accordo.
Output: `data/instruments_registry.json` (strumenti validi, timeframe, start-date).
**Gate:** `_download_cerbero_range` rifiuta gli strumenti non validati (override
`allow_unvalidated=True` solo per casi eccezionali). Rigenera con
`python -m src.data.instruments`.
## Metodologia obbligatoria per ogni nuova strategia
> **NB testnet.** Il token Cerbero punta a testnet; la congruenza cross-exchange
> è il filtro che distingue i feed realistici (Deribit, Hyperliquid) da quelli
> farlocchi (Bybit). Simboli Deribit: BTC/ETH = `<COIN>-PERPETUAL` (inverse);
> alt = `<COIN>_USDC-PERPETUAL` (lineari USDC). Registry attuale: Deribit 18/106,
> Hyperliquid 66/74 validi (major liquidi: BTC dal 2018, alt dal 2022).
1. **Ingresso eseguibile**: direzione e prezzo decisi con dati **fino a `close[i]`**, mai
`close[i-1]` con direzione presa da `i`; mai entry sull'estremo (high/low) di una candela.
2. **Backtest NETTO** dopo fee realistiche Deribit (**0.10% RT** taker; maker ~0%) + leva.
3. **Out-of-sample** held-out + robustezza su **griglia parametri** (entrambi gli asset, tutte le
celle positive) + **sweep fee** (0.00-0.20% RT, margine ampio).
4. **Liquidità & plausibilità** (lezione v2.0.0): incrociare ogni edge con la liquidità reale del
book (quota di barre flat) e con la plausibilità del prezzo (cross-venue). Un edge full+OOS
robusto su un book fermo o su wick fantasma NON è un edge.
5. Strategia in `scripts/strategies/` (codice univoco), test in `tests/`, diario aggiornato.
## Strategie attive
## Lezioni critiche (da NON ripetere — la storia di questo progetto)
> **LEZIONE CRITICA (2026-05-28).** L'intera famiglia squeeze-breakout (SQ01-04,
> MT01, ML01, AD01, CM01, PD01) è stata **scartata in `scripts/waste/`**: le
> accuratezze storiche 76-82% erano un **artefatto di look-ahead**. Quei backtest
> decidono la direzione con `sign(close[i]-close[i-1])` (la candela di breakout `i`)
> ma entrano a `close[i-1]` — cioè comprano *prima* della candela che usano per
> scegliere la direzione. Dal vivo il worker scopre il breakout solo a `close[i]`
> ed entra lì: l'edge sparisce (win-rate ~47%, lancio di moneta). Sotto ingresso
> onesto e fee reali **tutte perdono, anche a fee zero**. Inoltre i breakout
> *rientrano* (mean-reversion > continuation). Vedi `scripts/analysis/oos_validation.py`
> e `intrabar_test.py`.
Tutte le strategie estendono `src.strategies.base.Strategy`
(`generate_signals() → backtest()`). Le strategie mean-reversion condividono
`src.strategies.fade_base.FadeStrategy` (backtest intrabar TP/SL/max_bars).
**Strategie con edge netto validato OOS fee-aware (tutte fade/mean-reversion):**
| Codice | Nome | Meccanismo | Edge OOS netto (1h, fee 0.10% RT) | DD | Note |
|--------|------|-----------|-----------------------------------|----|------|
| **MR01** | Bollinger Fade | banda std attorno a SMA | BTC +201% / ETH +1238% | 15-72% | Fada la banda, TP alla media, SL ad ATR |
| **MR02** | Donchian Fade | estremi canale H/L | BTC +172% / ETH enorme | 30-42% | Fada la rottura del canale, TP al centro |
| **MR07** | Return Reversal | z dei rendimenti di barra | BTC +105% / ETH +195% | 25-46% | Fada il movimento estremo, exit in ATR; esposizione ~8% |
> **MR03 Keltner Fade** spostata in `scripts/waste/`: era la fade più debole
> (BTC Sharpe 1.22, il filtro trend la peggiorava) e ridondante con MR01 (stessa
> idea di banda). Rimuoverla dal portafoglio ne ha *migliorato* le metriche.
> La funzione `keltner_fade` resta in `strategy_research_v2.py` come record.
**Lezione confermata:** l'edge è sempre *mean-reversion* (i breakout rientrano).
Il trend-following (Donchian trend, RSI cross) e gli oscillatori senza filtro
(RSI revert, ADX-filtered fade) perdono netti → restano scartati.
Ogni strategia è robusta su **tutta** la sua griglia parametri (entrambi gli asset
→ tutte positive OOS) e su **tutte** le fee 0.00-0.20% RT (margine ampio).
MR01 validato col worker reale: BTC +196% / ETH +251% OOS (nov 2023→mag 2026).
Ricerca completa: `scripts/analysis/strategy_research.py` (MR01) e
`scripts/analysis/strategy_research_v2.py` (MR02/MR03/MR07).
Validazione live-path: `scripts/analysis/oos_validation.py`.
**Filtro trend (riduzione DD + aumento Acc).** Tutte le fade accettano i parametri
opzionali `trend_max` / `ema_long`: saltano i segnali quando il prezzo è troppo
esteso rispetto al trend di fondo (`|close EMA(ema_long)| / ATR(14) > trend_max`),
cioè quando si starebbe fadando un trend/crollo estremo. Con `trend_max=3.0`,
`ema_long=200` (default in `strategies.yml`): accuratezza su tutti gli sleeve
e DD giù drasticamente su ETH (MR01 71%→26%, MR02 42%→25%, MR03 66%→34%,
MR07 46%→21%), edge OOS confermato (vedi `scripts/analysis/risk_management.py`).
Unica eccezione: MR03 BTC, dove il filtro peggiora entrambe → lasciato disattivo.
Leva non robusta scartate: vol-target sizing e skip-alta-volatilità (peggiorano).
**Loss-guard Hurst (ATTIVO LIVE, 2026-06-02).** Le fade accettano `hurst_max`: saltano i segnali in
regime PERSISTENTE/trending (rolling-Hurst ≥ soglia), dove si concentrano stop-loss e perdite
(diagnosi: stop-rate 43% per hurst>0.55 vs 21% anti-persistente; i peggiori 1% trade hanno hurst
medio 0.61). Helper `src.strategies.fade_base.hurst_skip_mask` (rolling-Hurst causale **dalle sole
close** → nessun feed esterno; step=6 per velocità live). **`hurst_max=0.55` attivo sulle 6 fade in
`_defs.py`**: il test decisivo a livello PORT06 lo conferma — **FULL DD 4.10%→2.39% (quasi dimezzato),
Sharpe 6.62→6.76, OOS Sharpe 8.89→9.15**. È l'UNICO meccanismo anti-perdite che supera il gate (ADX,
vol-expansion/vratio, efficiency-ratio, time-stop, vol-target FALLISCONO: tagliano i winner insieme ai
loser; i claim esterni ADX/ATR-ratio non si replicano su queste fade crypto). NB: il filtro agisce solo
sul path LIVE (`spec.params`); il backtest canonico (`build_everything`/regression-lock) NON è filtrato
→ il live farà MEGLIO del backtest sul DD. Ricerca: `scripts/analysis/fade_lossguard_workflow.js`,
diagnosi `fade_loss_by_regime.py`, diario `docs/diary/2026-06-02-fade-lossguard.md`.
**Effetto misurato (backtest):** stop-loss fade 67% in numero (1881→621), perdite totali 68%, coda
61%→−48% (lo stop-RATE per-trade scende poco, 42→38%: il guard lavora riducendo l'ESPOSIZIONE nel
regime tossico, non rendendo sicuro ogni trade). **Monitor live:** `hourly_report.py` traccia lo
stop-rate fade PRIMA/DOPO l'attivazione (14:34 UTC del 2026-06-02) e dà il verdetto su Telegram quando
il campione DOPO ≥30 (già confermato: stop-rate live PRIMA 42% == backtest 42.1%).
**FIX EXIT-16 live — confirm su barra COMPLETATA (2026-06-05).** Il worker valutava il confirm-SL
sul prezzo della candela IN FORMAZIONE ad ogni poll → reintroduceva la wick-sensitivity che EXIT-16
elimina (audit: 2 stop su 3 del crash ETH erano wick-stop che il backtest non avrebbe preso in quel
momento). Ora `tick` valuta il confirm SOLO sul close dell'ultima barra completata (detection: la
riga -1 del df è la candela in corso finché `now < ts[-1]+bar_ms`), buf dall'ATR della stessa barra;
fill al prezzo corrente (≈ stress lag_close_exit, OK in exit-lab); TP intrabar invariato. La concausa
feed-gap NON è mitigabile lato exit (fill reali ≈ sim) e l'entry-guard post-flat è BOCCIATA (skippare
i segnali dopo barre flat PEGGIORA tutti gli sleeve ETH: la candela-gap è l'overshoot che la fade
fada). Aggiunto alert Telegram `STALE_FEED` (≥2 barre 1h flat → notifica + gap % al risveglio, solo
osservabilità). Diario `docs/diary/2026-06-05-confirm-sl-forming-bar.md`.
**EXIT-16 close-confirm SL (ATTIVO LIVE, 2026-06-04).** Le fade accettano `sl_confirm_atr` (live:
0.5 in `_defs.py`): lo SL **intrabar è disattivato** e lo stop scatta solo se il **CLOSE** della barra
sfonda `sl ∓ 0.5·ATR14`, con uscita al close (TP intrabar al livello e max_bars invariati; in modalità
confirm il TP ha priorità nel bar). Scoperta della ricerca exit-lab (34 agenti, 23 famiglie esplorate +
10 verifiche avversariali + test PORT06): **gli stop intrabar da wick sono falsi negativi** — l'overshoot
che buca lo stop e rientra è esattamente il movimento che la fade fada. Verificato: indipendente dal
loss-guard Hurst, plateau buffer 0.4-1.0, regge fee 2x/lag/slippage, coda ≈ base nei crash veri (FTX:
+2.4% vs 39% del no-SL puro). **PORT06: FULL Sharpe 6.47→7.84 DD 4.10→2.60, OOS Sharpe 8.82→10.06 DD
1.30→1.15.** La famiglia "cavalca il prezzo" (trailing/ride/partial-runner, 15 varianti) è invece tutta
SCARTATA: oltre il TP=media non c'è coda (4ª conferma). Collaterali: l'engine intrabar filla gli stop
"al livello" anche su gap-through (54% dei casi per stop tight) → bias PRO stop-stretti nelle ricerche
future; mai deployare strategie con `sl=0` (il fallback 2% del worker non si applicherebbe). Harness
riusabile `scripts/analysis/exit_lab.py` + policy in `exit_policies/`. Implementazione: `fade_base.backtest`
+ `StrategyWorker.tick` (param `sl_confirm_atr`, None = comportamento storico; il backtest canonico
`build_everything` resta NON filtrato → il live farà meglio del backtest, come per il loss-guard).
Diario `docs/diary/2026-06-04-exit-lab.md`.
**Portafoglio.** Diversificare su sotto-conti indipendenti equipesati (le 4 strategie
× BTC/ETH, pos 0.15 ciascuno) abbatte il DD aggregato: ~14% full / ~10% OOS sul
paniere di 8 sleeve, contro il 20-70% del singolo. È la vera leva anti-drawdown.
**Combinare le due famiglie (fade + honest).** Le fade (reversione intraday 1h) e le
honest (DIP/TR/ROT trend+rotazione multi-crypto) sono **quasi scorrelate**
(correlazione cross-famiglia ~0.05). Combinarle in un unico portafoglio migliora il
rischio/rendimento rispetto a ciascuna famiglia da sola: equal-weight dei 9 sleeve
→ DD 5.2% full / 4.7% OOS e Sharpe 4.23 full / 4.33 OOS (vs honest-only 12.6% DD /
2.20 Sharpe e fade-only 8.2% DD / 4.09 Sharpe), CAGR ~47% mantenuta. Studio in
`scripts/analysis/combine_portfolio.py`.
**ROT02 — riduzione DD (top_k 2→3).** La rotazione dual-momentum honest concentrava
il book su 2 soli asset (DD 40%). Diversificare su 3 (`top_k=3`) dimezza quasi il DD
(40%→26%) e *alza* pure il ritorno full (+1095%→+1303%, ret/DD da 27 a 50); il
vol-target abbassa il DD ma sacrifica ritorno, quindi si tiene top_k=3 senza VT.
Applicato a `ROT02_dual_momentum.py` e a `_rot_daily_equity` (usata dai portafogli).
**Portafogli pronti (artefatti accorpati e migliorati).** Oltre a `PORT01` (solo
honest), due script in `scripts/strategies/`:
- `PORT02_fade_master.py` — le 3 fade × BTC/ETH accorpate (6 sleeve, filtro trend),
equal-weight daily: DD ~8.2% full / 5.9% OOS, Sharpe 3.95/4.09, CAGR ~46%.
- `PORT03_all_master.py` — portafoglio MASTER (fade + honest, 9 sleeve). Due varianti:
`equal` (massimo Sharpe: DD 5.2%/4.7% OOS, Sharpe 4.23/4.33) e `5050` fra le due
famiglie (minimo DD: 5.0% full / 4.5% OOS). È la configurazione consigliata.
Come `PORT01`, sono meta-portafogli (script `run()` di report), non `Strategy` con
`generate_signals`, quindi non nel `strategy_loader`.
**Esplorazione famiglie alternative (branch `strategy_explore`, 2026-05-29).** Esplorate
9 famiglie nuove con agenti paralleli su harness onesto condiviso
(`scripts/analysis/explore_lab.py`). 7 sono rumore (rifiutate: stagionalità oraria/mensile,
cross-sectional reversal, opening-range breakout, lead-lag BTC→alt, continuation intraday —
quest'ultima riconferma la dominanza mean-reversion). Due edge reali:
- **PR01 Pairs** (`scripts/strategies/PR01_pairs_reversion.py`): spread reversion
market-neutral sul log-ratio z-score, **config UNIVERSALE** `n=50 z_in=2.0 z_exit=0.75
max_bars=72` (anti-overfit, niente tuning per-coppia). **5 coppie robuste**: ETH/BTC
(Sharpe 4.36), LTC/ETH (3.08), ADA/ETH (2.69), BTC/LTC (2.36, robusta anche 4h), ETH/SOL
(1.96, la più debole). Pattern: sempre alt-liquido vs major. Plateau confermato
(heatmap 20/20 Sharpe>1) + walk-forward (ETH/BTC 11/12 finestre+). **BNB/ETH scartata**
(overfit). Corr col mercato ~0.02-0.08. Fee su **2 gambe**: worker live implementato
(`src/live/pairs_worker.py`, sezione `pairs:` in `strategies.yml`). LOGICA validata
(`validate_worker_pairs.py`: replay == backtest ESATTO). LIVE (`live_smoke_pairs.py`,
smoke reale Cerbero): **tutte e 5 le coppie con feed live fresco**. Naming Deribit:
BTC/ETH = `<COIN>-PERPETUAL` (inverse); alt = `<COIN>_USDC-PERPETUAL` (lineari USDC,
storia dal 2022). Trappola: `LTC-PERPETUAL`/`SOL-PERPETUAL` danno vuoto/dati errati →
usare sempre `_USDC-PERPETUAL`. Resta da verificare solo liquidità/fill in esecuzione.
Verifica edge: `pairs_research.py`.
- **TSM01** (`scripts/analysis/tsmom_research.py`): TSMOM multi-orizzonte 3/6/12m + risk-off,
**gross 0.30**, distinto da ROT02 (corr 0.62), DD 15-22%, mai un anno negativo. Robusto
(36/36 config OOS+) ma diversificatore, non motore di ritorno (rende meno di ROT02).
Aggiungere i **5 pairs** al MASTER (quasi scorrelati, ~0.02-0.09) è il free-lunch più
grande (`scripts/analysis/combine_v2.py`). **Numeri sobri onesti** (l'OOS singolo 2024-25
è regime calmo → ottimistico ~50%): worst-DD su 90g rolling **~6%** (non 2.3%), Sharpe
atteso **~5** (mediana semestrale), ogni anno positivo dal 2021, regge **leva 2x +
slippage doppio** (CAGR 36%, Sharpe 5.1). Config robusta raccomandata: **MASTER-esteso
equal-weight, leva 2x, cap pairs ~30-35%** (i pairs sono ~57% del rischio; worker live a
2 gambe implementato, validato e con feed live su tutte e 5 le coppie — resta da
verificare liquidità/fill in esecuzione reale). La confluenza multi-TF è stata SCARTATA (overfit).
**Pattern del segnale per FORMA (branch `shape_patterns`, 2026-05-29).** Esplorate 5 famiglie
di *shape forecasting* con agenti paralleli su harness onesto (`scripts/analysis/shape_lab.py`:
analog kNN causale, no-look-ahead verificato). **4/5 sono RUMORE** (riconfermano la dominanza
mean-reversion): analog kNN sulla forma grezza (solo BTC-overfit), encoding candele
UP/DOWN/DOJI+body/shadow (hit-rate ~50%), DTW+template geometrici (DTW *peggiora* l'euclidea;
template overfit), PIP/pivot/zig-zag (0/48 robuste). Vedi `scripts/analysis/shape_*_research.py`.
- **SH01 Shape-ML** (`scripts/strategies/SH01_shape_ml.py`): UNICO edge. Una LogisticRegression
legge 17 feature di forma (body/shadow, rendimenti, pendenza/curvatura, pos max/min, RSI,
estensione) e predice il segno del rendimento a H barre in **walk-forward** (scaler+modello
solo sul passato, no leakage). Config **W24 H12 th0.58**. A differenza dello squeeze
**regge fee 0.20% RT**. Win-rate ~50% → l'edge è nell'**asimmetria**, non nella frequenza.
Validazione (`scripts/analysis/shape_ml_validate.py`): BTC robusto OVUNQUE (expanding +219%/
OOS +42% Sharpe 2.72 8-9 anni; rolling 2y +166%/+96%; stress 2x+slippage OK), ETH/ADA
robusti solo expanding (secondari), LTC/SOL/XRP scartati. Griglia: **5/27 celle robuste su
cresta stretta W24/H8-12** → overfit moderato, scelta la config conservativa. **Valore vero:
diversificatore** (corr +0.08 col MASTER); aggiungerlo migliora l'OOS del MASTER (Sharpe
4.33→5.10, DD 4.7%→4.2%). NON motore standalone. **LIVE (2026-06-01): gira come StrategyWorker
reale** (vedi fix wiring sotto in SCOPE LIVE). Diario: `docs/diary/2026-05-29-shape.md`.
**ARGO / GEX opzioni (analisi 2026-06-01, SCARTATO).** Valutato ARGO (lettura del gamma-exposure dei
dealer Deribit) come filtro di regime. Esito **NO-GO**: il net-GEX si calcola live (Deribit mainnet
public API, OI reale ~368k contratti, DVOL/funding storici gratis) ma **lo storico per-strike dell'OI
non è gratuito → non backtestabile OOS** (stesso muro delle opzioni W18/19/21). Niente evidenza crypto,
segno fragile, mercato dominato dai perp. Diario `docs/diary/2026-06-01-argo-gex-feasibility.md`.
**Frattali del segnale × Regime ARGO (ricerca 100 agenti, 2026-06-02, RECORD).** Cercata una strategia
che combini un segnale frattale (Hurst/Higuchi/Williams/analog) con un gate di regime (DVOL/VRP/funding).
Infrastruttura riusabile: `scripts/analysis/regime_fetcher.py` (DVOL+funding da Deribit mainnet →
**`data/regime/`**, NON `data/raw/` che è solo OHLCV) e `regime_lab.py` (feature regime+frattali causali,
cache, harness netto-OOS). Esito: **15 strategie robuste e causali, ma NESSUNA batte/migliora il PORT06**
(diversificatori sovrapposti alle fade). **Finding: il prior ARGO "VRP>0=range=fade" è SMENTITO**
l'edge è su **VRP<0 + DVOL bassa**. Il vincitore `FR01_hurst_calm_fade.py` è robusto ma DILUISCE il
PORT06 (OOS Sharpe 8.89→8.72) → **non deployato** (in `scripts/strategies/` ma NON in MODULE_MAP/yml).
Il sottoprodotto utile è stato il **loss-guard Hurst** (vedi sopra), che invece MIGLIORA il PORT06.
Diario `docs/diary/2026-06-02-fractal-argo-search.md`.
**Metodologia obbligatoria per ogni nuova strategia** (per non ripetere l'errore squeeze):
1. Ingresso eseguibile: direzione e prezzo decisi con dati **fino a `close[i]`**, mai `close[i-1]` con direzione da `i`.
2. Backtest **NETTO** dopo fee realistiche Deribit (**0.10% RT** taker, non 0.20%) + leva.
3. Validazione **out-of-sample** (held-out) + robustezza su griglia parametri + sweep fee.
4. Crea script in `scripts/strategies/`, aggiungi a `MODULE_MAP` (`strategy_loader.py`) e a `strategies.yml`.
Strategie scartate storiche in `scripts/waste/` (W01-W28 + la famiglia squeeze).
**Verso €50/giorno.** Con 4 strategie indipendenti (MR01/MR02/MR03/MR07) × 2 asset
(BTC/ETH) su €1000 ciascuna, il PnL medio storico aggregato è ben oltre €50/giorno;
ma quei numeri sono backtest a leva 3x su 8 anni e includono anni eccezionali (es.
ETH 2024). Stima onesta: il target è *plausibile* su un portafoglio diversificato di
queste fade, ma va confermato col paper trader live prima di rischiare capitale reale.
## Portafogli
- Un `Portfolio` è un oggetto di prima classe (`src/portfolio/`) con definizione (sleeve + schema pesi) e due facce sulla **STESSA** definizione: `.backtest()` (riusa il builder unico di `sleeves.py` → parità esatta con `report_families`) e live (`PortfolioRunner`: capitale pool condiviso, sizing per peso, ribilancio giornaliero, ledger aggregato in `data/portfolios/{code}/`).
- **Schemi peso:** `equal` (default), `cap` (tetto per famiglia, es. pairs 33% — config raccomandata), `inverse_vol`, `cluster_rp` (equal fra cluster naturali poi inverse-vol intra-cluster), `manual`. Definiti in `weighting.py`; la chiave cap è la famiglia (PAIRS/FADE/HONEST/SHAPE/TSM).
- **Default `portfolios.yml`:** PORT06 (master+shape), `weighting=cap pairs 0.33 + shape 0.0588`, leva 2x, ribilancio 1D. Backtest PORT06 canonico (dati al 2026-05-28, pre-cap-shape): FULL Sharpe 6.47 DD 4.10% / OOS Sharpe 8.82 DD 1.30%; **con EXIT-16 close-confirm (config live attuale): FULL 7.84 / 2.60%, OOS 10.06 / 1.15%** (i vecchi 6.07/8.19 erano pre-loss-guard/pre-refresh dati). Col cap SHAPE (2026-06-05): FULL 6.43 / 3.96%, OOS 8.58 / 1.36% — assicurazione sulla coda SH01, vedi sotto.
- **SH01 SENZA STOP-LOSS — by design, CONFERMATO da ricerca (2026-06-05).** Dopo il crash ETH (15.6% su un trade SH01 live), ricerca multi-agente con harness dedicato `scripts/analysis/sh01_exit_lab.py` (cache segnali walk-forward, engine con **fill gap-aware** `worse(livello, open)`, parity esatta con explore_lab, protocollo train≤2023-11-01/OOS): **11 famiglie di stop testate (ATR intrabar/close-confirm, %, chandelier, breakeven, giveback, loser-timestop, disaster-cap close+intrabar, swing, vol-regime), 0 sopravvissute** al gate (ETH migliorato senza degradare BTC, train E oos, plateau). Pattern: ogni stop stretto abbastanza da toccare la coda ETH rompe BTC; ogni stop largo non arriva alla coda; nei crash il fill è al gap, non al livello (lo stop "protettivo" PEGGIORA la coda OOS). Mitigazione adottata: **cap famiglia SHAPE a 0.0588 in PORT06** (≈ dimezzata; costo OOS Sharpe 0.24, FULL DD 0.14pp) — la prossima coda impatta il conto per metà. NON impostare mai `sl`/`sl_confirm_atr` su SH_BTC/SH_ETH. Direzione futura: liquidity-gate sull'entry (skip dopo feed flat). Diario `docs/diary/2026-06-05-sh01-sl-research.md`.
- **Data layer Cerbero v2:** `get_historical_v2` unificato + `get_instruments` (naming robusto) + `get_ticker_batch`. Trading su Deribit.
- **SCOPE LIVE (fase 2 completata):** il runner esegue TUTTI gli sleeve di PORT06. Worker: single `StrategyWorker` (fade MR01/02/07, DIP01, **e SH01**), `PairsWorker` (PR01 2 gambe), e i multi-asset dedicati `BasketTrendWorker` (TR01 4h), `RotationWorker` (ROT02 1d), `TsmomWorker` (TSM01 1d). Il runner fetcha 1h da Cerbero v2 e **resampla a 4h/1d** (lookback dimensionato sui daily: TSM01 usa 252g). Validazione: runner pool/ribilancio/ledger == backtest (`validate_portfolio_runner.py`, identico); worker multi-asset == reference (`validate_honest_workers.py`: TSM01 esatto, ROT02 +1303% canonico, TR01 stesso ordine — differenza di convenzione capitale-unico vs media-equity).
- **FIX SH01 wiring (2026-06-01).** SH01 gira come **`StrategyWorker` NORMALE** (NON il vecchio `MLWorkerWrapper` di `multi_runner`, che usava il `SignalEngine` **squeeze SCARTATO**: apriva senza metadata ed usciva a `hold_bars=3`, ignorando del tutto SH01_shape_ml). `SH01_shape_ml.generate_signals` fa il walk-forward (retraining) internamente ad ogni tick ed emette `metadata.max_bars=12` → exit a orizzonte via `StrategyWorker.tick`. Serve ≥4000 barre 1h (`_ML_LOOKBACK_DAYS=365`). Vedi `docs/diary/2026-06-01-sh01-wiring-squeeze-bug.md`.
- **Altri fix StrategyWorker (2026-06-01).** Exit a orizzonte puro per strategie senza TP/SL (`elif self.max_bars`, SH01 esce a H=12 non hold_bars=3); `is_win = net > 0` (win NETTO fee, non lordo); filtro `min_tp_frac` (salta micro-scalp col TP entro le fee); loss-guard `hurst_max=0.55` sulle fade (vedi sopra).
- **Exit intrabar (fase 3, risolto):** lo `StrategyWorker` ora esce sui TP/SL toccati INTRABAR (high/low della barra, al livello, SL prioritario) come il backtest — non più solo sul close. Allinea fade/DIP01 live al backtest intrabar (`tests/portfolio/test_intrabar_exit.py`). Caveat residuo onesto: nel paper trading l'high/low usato è quello della barra in corso al poll; su un fill reale conterebbe il momento del tocco.
- **ESECUZIONE REALE — shadow (v1.0.3, 2026-06-03).** I **6 fade** (MR01/MR02/MR07 × BTC/ETH) eseguono ordini **REALI su Deribit testnet** accanto al fill simulato (*shadow*: sim + reale in parallelo, il sim resta la verità che guida le decisioni). `src/live/execution.py` `ExecutionClient`: `open` (market) + `close_amount` (market **reduce-only della SOLA quota del worker** — i 3 fade BTC condividono lo strumento e le posizioni si nettano per conto, quindi NON si usa `close_position` che flatterebbe le quote altrui); **verifica l'esecuzione sul TRADE** (order_id in `get_trade_history`, non sulla size netta aggregata); **fee REALI lette dai `trades[]`**. Strumenti = **lineari USDC** (`BTC_USDC`/`ETH_USDC-PERPETUAL`, amount nel base-coin, step 0.0001/0.001): scelti perché il **payoff lineare == matematica del backtest** (l'inverse `*-PERPETUAL` introdurrebbe una base 1/prezzo) e fee/PnL sono in USDC. Lo `StrategyWorker` tiene un **ledger reale parallelo** (`real_capital`, persistito) e logga `REAL_OPEN`/`REAL_CLOSE` col confronto **slippage** (prezzo sim vs eseguito) e **fee** (assunta 0.10% vs reale). Config: `portfolios.yml``overrides.execution {enabled, sleeves:[MR01,MR02,MR07,DIP01], instruments:{BTC:BTC_USDC-PERPETUAL, ETH:ETH_USDC-PERPETUAL}}` (**DIP01 aggiunto il 2026-06-04**: stesso wiring single-leg, TP resting incluso); pairs/rotation/tsmom/shape restano **simulati** (pairs richiede un executor a 2 gambe con gestione leg-risk; shape non ha TP). **Fee reali misurate = 0.05%/lato = 0.10% RT** (== assunto del backtest, su ETH; BTC inverse era ~0.094%). **Alert Telegram:** `REAL_EXEC_LIVE` (primo ordine reale verificato per worker) + `REAL_OPEN_FAIL`. Smoke (testnet, €0): `scripts/analysis/live_exec_smoke.py` (layer: ordine→verifica→fee) e `live_shadow_smoke.py` (catena worker open/close). **Capitale live portato a 2000** (notional fade ~$35) per ridurre il rumore di arrotondamento su BTC (step lineare ~$6.7). NB: ledger reale ≠ ledger sim — i worker già in posizione sim a un restart non hanno quota reale corrispondente; lo shadow reale parte pulito dalla prossima apertura.
- **TP reale = LIMIT reduce-only AL LIVELLO (2026-06-04).** Misurati +235 bps di slippage medio sulle uscite take-profit market-on-poll (sim esce al livello intrabar, il reale chiudeva al poll post-rimbalzo: sim +11.85 vs reale +0.62 USD sui primi 7 close). Fix: a ogni `REAL_OPEN` il worker piazza un **limit reduce-only al TP** (`ExecutionClient.place_tp_limit`, prezzo quantizzato al tick, SOLA quota del worker) → `REAL_TP_RESTING`; a ogni chiusura sim `_real_close` **cancella il resting → riconcilia i fill (anche parziali) via `get_trade_history` per order_id → market reduce-only solo del residuo** → ledger su prezzo combinato. `real_tp_order_id` persistito in `status.json` (resume-safe). Lo **SL resta market-on-poll** (deliberato: i trigger Deribit generano un nuovo order_id al trigger → fill non verificabile per order_id; e sul SL il rimbalzo lavora a favore). Fill da resting = fee **maker ~0%**. Smoke: `live_shadow_smoke.py` (2 scenari, testnet). Diario `docs/diary/2026-06-04-shadow-divergence.md`.
- **Limite noto:** al ribilancio le posizioni APERTE restano sul loro notional (non travasate); fedele al backtest daily-rebalanced entro il turnover infragiornaliero.
## Multi-Strategy Paper Trader
Orchestratore che esegue N strategie in parallelo su dati live Cerbero, ognuna con €1000 USDC virtuali indipendenti.
**Config:** `strategies.yml` — due sezioni: `strategies` (single-leg: fade/honest) e
`pairs` (a 2 gambe). Attive: 6 fade (MR01/MR02/MR07 × BTC/ETH) + 5 coppie PR01.
**Due worker:** `strategy_worker.py` (single-leg) e `pairs_worker.py` (2 gambe,
long A / short B sullo z-score del log-ratio, fee su 2 gambe).
**Persistenza:** `data/paper_trades/{worker_id}/` con `trades.jsonl` (append-only) + `status.json` (resume al restart).
**Hot-add:** aggiungi riga YAML → `docker compose restart` → storico intatto.
**Exit strategia:** se un `Signal` porta `tp`/`sl`/`max_bars` in `metadata` (come le fade), il worker esce su take-profit/stop-loss/time-limit; i pairs escono su |z|≤z_exit o max_bars.
**Naming Deribit (feed live):** major = `<COIN>-PERPETUAL` (inverse); alt = `<COIN>_USDC-PERPETUAL` (lineari USDC). Vedi INSTRUMENT_MAP in `multi_runner.py`.
**Notifiche:** Telegram per ogni trade (richiede `.env` con `TELEGRAM_BOT_TOKEN` e `TELEGRAM_CHAT_ID`).
- **Feed contaminato → libreria fasulla (v2.0.0).** Print fantasma testnet + Binance/USDT hanno
prodotto edge inesistenti (+201%/+1238%/+16492% "OOS"). Tutti spariti sul feed reale. Lezione: il
dato viene prima della strategia; certificare sempre.
- **Look-ahead squeeze (storico).** L'intera famiglia squeeze-breakout aveva accuratezze 76-82% che
erano artefatto: decideva la direzione con la candela di breakout `i` ma entrava a `close[i-1]`.
Con ingresso onesto: lancio di moneta. (Dettagli nei diari in `Old/`.)
- **Entry sugli estremi di candela.** Strategie che entrano a `close` quando `close` è all'estremo
del range (≤0.1% o ≥99.9%) gonfiano i ritorni in modo irrealistico (ETH 2024: +30.848% → +2.725%
rimuovendoli). Spesso è un artefatto di dato o di entry non eseguibile.
- **Mean-reversion vs breakout.** Sui dati storici l'unica direzione che mostrava edge era la
mean-reversion (i breakout rientrano) — MA anche quegli edge erano per lo più artefatto del feed:
da riverificare da zero su dati certi.
- **Fee** = vincolo di prim'ordine. 0.10% RT baseline. Molte operazioni = morte per fee.
- **Leva**: testare 3x; 5x raddoppia il drawdown. I numeri a leva alta NON sono il caso base.
- **Data leakage** con rendimenti log: `returns[k] = log(close[k+1]/close[k])` usa `close[k+1]`. I
feature devono fermarsi a `returns[i-2]` se il prezzo corrente è `close[i-1]`. Verificare SEMPRE.
## Convenzioni
- Strategie in `scripts/strategies/` con codice univoco (MR01, ...).
- Script scartati in `scripts/waste/` (W01-W28 + famiglia squeeze).
- Diario in `docs/diary/YYYY-MM-DD.md`. Aggiornare dopo ogni esperimento significativo.
- Nessun dato sensibile nei commit (token, chiavi API). Usare `.gitignore`.
- Verificare sempre assenza di data leakage prima di fidarsi dei risultati. In particolare: `returns[i-w : i]` include `close[i]` che è un candle nel futuro — usare `returns[i-w : i-1]`.
- Strategie in `scripts/strategies/` con codice univoco; scartate documentate nel diario.
- Diario in `docs/diary/YYYY-MM-DD.md`, aggiornato dopo ogni esperimento significativo.
- **Nessun segreto nei commit** (token/chiavi). `.env` e `.env.mainnet` sono gitignored.
- Versionamento: `VERSION` (semver) + `scripts/bump_version.py`. `src/version.py` lo legge.
## Attenzione
## Archivio `Old/`
- **Data leakage:** è stata trovata e corretta nello script 05. Ogni volta che si usano rendimenti logaritmici (`np.diff(np.log(close))`), ricordare che `returns[k]` usa `close[k+1]`. I feature devono fermarsi a `returns[i-2]` se il prezzo corrente è `close[i-1]`.
- **Fee:** Deribit perp reale = taker ~0.05%/lato (**0.10% round-trip**), maker ~0%. Usare 0.10% RT come baseline (lo 0.20% storico era pessimista 2x). Includere SEMPRE nel backtest: sono vincolo di prim'ordine, molte operazioni = morte per fee. Il worker usa `strategy.fee_rt` (MR01 = 0.001).
- **Leva:** testato con 3x. Aumentare a 5x migliora i rendimenti ma raddoppia il drawdown.
- **GBM:** GradientBoostingClassifier di scikit-learn. Ensemble di alberi decisionali sequenziali. Walk-forward per evitare leakage temporale.
- **Cerbero `get_historical` (fix 2026-05-28):** `end_date` come data nuda è inclusivo dell'intera giornata fino all'ultima candela chiusa (es. `end=oggi` arriva fino ad ora, non più a mezzanotte); accettati anche timestamp con orario (`...T14:00:00`, naive=UTC); nessun cap a ~5000 righe (paginazione interna). Il client passa già `end=oggi`, ora corretto. Prima del fix il paper trader restava a zero trade perché il feed era fermo a mezzanotte.
- **Dati ETH Deribit 15m:** 14-30%/anno di candele *flat* (O=H=L=C, volume 0, run fino a ~54h) per bassa liquidità del perpetuo. Verificato (2026-05-28): escluderle NON cambia i backtest (Δacc ≤0.5pp) → edge robusto. Resta un caveat operativo (slippage/fill in trading reale, irrilevante per paper). BTC pulito eccetto picco ~8% nel 2024.
Tutto il lavoro pre-reset (preservato in git per consultazione storica): strategie
(`Old/scripts/strategies`), stack live e portafogli (`Old/src/live`, `Old/src/portfolio`,
`Old/scripts/portfolios`), ricerca/gate (`Old/scripts/analysis`), dati non certificati
(`Old/data`), 60+ diari (`Old/docs/diary`), test (`Old/tests`). Consultabile come riferimento
("come facevamo X"), ma **nessun edge lì dentro è fidato** finché non è ri-validato su dati certi.
```
View File
+212
View File
@@ -0,0 +1,212 @@
# Strategia di Grid Trading — Versione Corretta
> Documento di specifica della strategia. Descrive *cosa* deve fare il bot e *perché*,
> non l'implementazione. È il riferimento da cui partire per riscrivere il codice in
> modo sicuro e testabile.
---
## 1. Obiettivo
Estrarre profitto dalla **volatilità di un asset all'interno di un intervallo di prezzo
(range)**, comprando automaticamente quando il prezzo scende e vendendo quando risale,
secondo livelli predefiniti (la "griglia").
La griglia **non prevede** la direzione del mercato: monetizza le oscillazioni. Funziona
quando il prezzo oscilla; perde quando il prezzo prende un trend deciso. Tutta la
progettazione che segue serve a massimizzare il primo caso e a limitare i danni nel
secondo.
---
## 2. Principi corretti (cosa cambia rispetto al bot originale)
| Aspetto | Bot originale (sbagliato) | Versione corretta |
|---|---|---|
| Asset | Shitcoin illiquida (LAND) | Coppia liquida (es. ETH/USDT, BNB/USDT) |
| Passo griglia | Assoluto in USDT (`GRID_STEP=3`) | **Percentuale** sul prezzo |
| Livelli | Mobili, inseguono il prezzo | **Fissi** dentro un range definito |
| Protezione perdite | Nessuna | **Stop-loss** sotto il range + **take-profit** sopra |
| Slippage | `amountOutMin = 0` (nessuna) | Calcolato da `getAmountsOut` tolleranza |
| Break-even fee | Ignorato | Passo griglia **> costo round-trip** |
| Capitale | Tutto, senza limiti | Allocazione fissa, suddivisa per livello |
| Chiave privata | In chiaro nel `.env` | Keystore cifrato o input a runtime |
| Validazione | Nessuna | **Backtest** + **testnet** prima del capitale reale |
---
## 3. Definizione della griglia
### 3.1 Parametri di ingresso
| Parametro | Significato | Esempio |
|---|---|---|
| `PAIR` | Coppia da tradare (base/quote) | `ETH/USDT` |
| `RANGE_LOW` | Estremo inferiore del range | `2800` |
| `RANGE_HIGH` | Estremo superiore del range | `3400` |
| `GRID_LEVELS` | Numero di livelli nella griglia | `12` |
| `CAPITAL_QUOTE` | Capitale totale in quote (USDT) | `1200` |
| `STOP_LOSS` | Prezzo sotto cui il bot chiude tutto e si ferma | `2650` |
| `TAKE_PROFIT` | Prezzo sopra cui il bot chiude tutto e si ferma | `3550` |
| `SLIPPAGE_BPS` | Tolleranza slippage in basis point (50 = 0,5%) | `50` |
| `FEE_BPS` | Fee del DEX in basis point (PancakeSwap = 25) | `25` |
### 3.2 Costruzione dei livelli (griglia geometrica)
I livelli vanno spaziati in **percentuale**, non in valore assoluto. Una griglia
geometrica mantiene lo stesso rendimento percentuale per ogni gradino, indipendentemente
dal prezzo.
```
ratio = (RANGE_HIGH / RANGE_LOW) ^ (1 / GRID_LEVELS)
livello[i] = RANGE_LOW * ratio ^ i per i = 0 .. GRID_LEVELS
```
Esempio (`RANGE_LOW=2800`, `RANGE_HIGH=3400`, `GRID_LEVELS=12`):
`ratio ≈ 1,0163` → passo di circa **1,63% per gradino**.
### 3.3 Capitale per livello
```
quote_per_livello = CAPITAL_QUOTE / GRID_LEVELS
```
Ogni livello di acquisto impegna `quote_per_livello`. Il capitale è suddiviso in anticipo:
il bot **non** può comprare più di quanto allocato, e non scende mai sotto zero.
---
## 4. Vincolo di break-even (regola anti-fee)
Una griglia con passi troppo fitti perde: le fee di ogni round-trip (compra + vendi) si
mangiano il profitto. **Il passo percentuale della griglia deve superare il costo totale
di un round-trip.**
```
costo_round_trip ≈ 2 * (FEE_BPS + SLIPPAGE_BPS) / 10000 (in frazione)
passo_griglia = ratio - 1
VINCOLO: passo_griglia > costo_round_trip * margine_sicurezza (margine ≥ 1,5)
```
Esempio con `FEE_BPS=25`, `SLIPPAGE_BPS=50`:
`costo_round_trip ≈ 2 * (25+50)/10000 = 1,5%`. Con margine 1,5 → il passo deve essere
**≥ 2,25%**. Se la griglia geometrica dà 1,63%, **è troppo fitta**: vanno ridotti i
`GRID_LEVELS` o allargato il range finché il vincolo è rispettato. Il bot deve rifiutarsi
di partire se il vincolo non è soddisfatto.
---
## 5. Logica operativa
### 5.1 Inizializzazione
1. Validare i parametri: `RANGE_LOW < prezzo_attuale < RANGE_HIGH`, vincolo di break-even
rispettato, capitale disponibile sul wallet.
2. Verificare la coppia: liquidità sufficiente, contratto non honeypot (controllo
obbligatorio, non opzionale), token con decimali noti.
3. Costruire i livelli (§3.2) e marcare ognuno come `attivo`/`riempito`.
4. Allocare il capitale (§3.3).
### 5.2 Ciclo principale
A ogni tick (es. ogni nuovo blocco, o ogni N secondi):
```
prezzo = prezzo_corrente(PAIR)
# --- guardie di uscita: hanno priorità su tutto ---
SE prezzo <= STOP_LOSS:
vendi_tutta_la_posizione() # con slippage protetto
ferma il bot, log "STOP-LOSS"
SE prezzo >= TAKE_PROFIT:
vendi_tutta_la_posizione()
ferma il bot, log "TAKE-PROFIT"
# --- logica griglia ---
PER ogni livello L:
SE prezzo attraversa L verso il basso E L non è ancora riempito:
compra quote_per_livello # con amountOutMin protetto
marca L come riempito
SE prezzo attraversa L verso l'alto E il livello sotto è riempito:
vendi la quantità di quel livello # con amountOutMin protetto
marca quel livello come libero
```
I livelli sono **fissi** (calcolati una volta), non inseguono il prezzo. Questo rende il
comportamento prevedibile e backtestabile.
### 5.3 Calcolo `amountOutMin` (protezione slippage — obbligatoria)
Mai passare `0`. Prima di ogni swap:
```
atteso = router.getAmountsOut(amountIn, path)[ultimo]
amountOutMin = atteso * (10000 - SLIPPAGE_BPS) / 10000
```
Se la transazione non rientra nella tolleranza, deve **fallire** (revert), non eseguire a
qualsiasi prezzo.
---
## 6. Gestione del rischio
1. **Stop-loss obbligatorio** sotto `RANGE_LOW`. È la differenza tra "strategia" e
"gambling": senza stop-loss un trend ribassista svuota il wallet.
2. **Take-profit** sopra `RANGE_HIGH` per chiudere quando il prezzo esce dal range al
rialzo (la griglia avrebbe già venduto tutto; il take-profit evita di restare esposti).
3. **Capitale segregato**: usare un wallet dedicato, con solo il capitale destinato alla
strategia. Mai il wallet principale.
4. **Limite di gas e prezzo gas** ragionevoli, ricalcolati dinamicamente (no valori fissi
obsoleti).
5. **Kill-switch manuale**: comando per fermare il bot e liquidare in qualsiasi momento.
6. **Idempotenza/recupero**: se il bot si riavvia, deve ricostruire lo stato dei livelli
(riempiti/liberi) dal saldo on-chain, non ripartire da zero.
---
## 7. Validazione prima del capitale reale
Nessun fondo reale prima di aver superato, in ordine:
1. **Backtest** su dati storici della coppia (almeno alcuni mesi, includendo sia fasi
laterali sia un trend marcato), misurando: PnL netto **dopo** fee e slippage,
max drawdown, numero di trade, comportamento allo stop-loss.
2. **Paper trading / simulazione** in tempo reale, senza eseguire ordini veri.
3. **Testnet** (BSC testnet) con la stessa logica e router di test, per verificare
l'esecuzione on-chain end-to-end.
4. **Mainnet con capitale minimo** (es. l'equivalente di pochi euro) per la prima
settimana, poi scalare solo se i risultati combaciano col backtest.
---
## 8. Quando NON usare questa strategia
- Asset illiquido o a rischio rug-pull/honeypot.
- Mercato in trend forte e prolungato (la griglia perde: lo stop-loss limita il danno ma
non genera profitto).
- Passo griglia che non rispetta il vincolo di break-even (§4).
- Capitale che non puoi permetterti di perdere.
---
## 9. Parametri di esempio (configurazione di partenza prudente)
```
PAIR = BNB/USDT # coppia liquida su PancakeSwap
RANGE_LOW = 580
RANGE_HIGH = 720
GRID_LEVELS = 8 # passo ≈ 2,7% > break-even
CAPITAL_QUOTE = 400 # USDT, su wallet dedicato
STOP_LOSS = 545 # ~6% sotto RANGE_LOW
TAKE_PROFIT = 760 # ~5,5% sopra RANGE_HIGH
SLIPPAGE_BPS = 50 # 0,5%
FEE_BPS = 25 # PancakeSwap v2
```
Verifica break-even: passo ≈ 2,7% > `1,5 × (2×0,75%) = 2,25%`
---
*Questo documento descrive la strategia. L'implementazione (ethers v6, gestione sicura
della chiave, calcolo slippage, stato persistente, backtester) va sviluppata a parte,
con test, e validata secondo §7 prima di qualsiasi capitale reale.*
+49
View File
@@ -0,0 +1,49 @@
services:
portfolio:
build: .
container_name: pythagoras-portfolio
restart: unless-stopped
command: ["uv", "run", "python", "-m", "src.portfolio.runner"]
volumes:
- ./data:/app/data
- ./portfolios.yml:/app/portfolios.yml:ro
env_file:
- .env
- .env.mainnet # token MAINNET (soldi veri); prevale su .env (vedi micro-test plan)
environment:
- PYTHONUNBUFFERED=1
healthcheck:
test: ["CMD", "python", "-c", "import os; assert any(f.endswith('status.json') for r,d,fs in os.walk('/app/data/portfolios') for f in fs)"]
interval: 120s
timeout: 10s
retries: 3
labels:
- com.centurylinklabs.watchtower.enable=false
# Dashboard web read-only (stato live, PnL totale/per-strategia, grafico equity,
# trade attivi+chiusi). Stessa immagine del runner, monta gli stessi data/ in sola
# lettura logica (legge equity.jsonl + status/trades dei worker). Porta 8787.
# NB: nessuna auth -> non esporre su internet pubblico, solo rete interna/VPN.
dashboard:
build: .
container_name: pythagoras-dashboard
restart: unless-stopped
command: ["uv", "run", "python", "-m", "src.live.dashboard", "--port", "8787"]
ports:
- "8787:8787"
volumes:
- ./data:/app/data
- ./portfolios.yml:/app/portfolios.yml:ro
- ./docs/report:/app/docs/report:ro # scheda strategie_attive.html (modal "scheda dettagliata")
env_file:
- .env
- .env.mainnet # token MAINNET (soldi veri); prevale su .env (vedi micro-test plan)
environment:
- PYTHONUNBUFFERED=1
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8787/api/state', timeout=5)"]
interval: 120s
timeout: 10s
retries: 3
labels:
- com.centurylinklabs.watchtower.enable=false
+128
View File
@@ -0,0 +1,128 @@
# TODO — prossimi passi
> Raccolta dei follow-up aperti. Aggiornato 2026-06-11 (XS01 dispersion-gate live v1.1.20;
> real-truth ledger attivo; reset portafoglio 2026-06-10).
## Stato esecuzione per sleeve (REALE vs SIMULATO)
**REALE (shadow su Deribit testnet, REAL-TRUTH: il ledger segue i fill reali) — 15 sleeve:**
- 6 fade (MR01/MR02/MR07 × BTC/ETH) + DIP01_BTC → single-leg, v1.0.3+
- 6 pairs PR01 (ETH/BTC, LTC/ETH, ADA/ETH, BTC/LTC, ETH/SOL + ETH/BTC 15m) → 2 gambe,
v1.1.12 (2026-06-08); il 15m dal v1.1.16 (2026-06-09)
- SH01 × BTC/ETH → single-leg, exit a orizzonte (no TP/SL), v1.1.13 (2026-06-08)
**SIMULATO (paper, NON esegue reale) — 4 book multi-asset, fuori dal capitale-pool:**
- TR01 / ROT02 / TSM01 / XS01 → bloccati dal CAPITALE (rumore arrotondamento)
## Esecuzione reale — pezzi mancanti
- [x] ~~Executor a 2 gambe per i PAIRS~~ — FATTO + ATTIVO (v1.1.12, 2026-06-08).
- [x] ~~SH01 in reale~~ — FATTO (v1.1.13, 2026-06-08): l'infrastruttura no-TP esisteva già
(_place_real_tp no-op senza TP, _real_close chiude tutto market reduce-only a orizzonte).
Bastava accettare kind 'ml' in _exec_for + SH01 in execution.sleeves. Disaster-bracket
on-book = unica protezione di coda. Test SH01 open/close reale OK.
- [ ] **Multi-asset in reale** (SIMULATO: TR01/ROT02/TSM01/XS01) — **SBLOCCATO DA CAPITALE ≥ ~€20k**.
Strumenti USDC tutti esistenti (BNB/DOGE/XRP inclusi), ma a €2k il rumore di arrotondamento
per-asset è 20-30% (DOGE/XRP/SOL step grandi in $, esposizione frammentata su 5-8 asset) →
l'esecuzione reale misurerebbe il rumore dei lotti, non la strategia. Serve un
`BasketExecutionClient` (ribilancio delta multi-strumento, long-only, netting con fade+pairs
sui simboli condivisi). Beneficio limitato (~19% del portafoglio, diversificatori spesso
flat) → bassa priorità anche a capitale alto. Analisi: chat 2026-06-08.
## Capitale (decisione utente, non codice)
- [ ] **Valutare aumento capitale a €5.000-10.000** per togliere il rumore di arrotondamento.
A €2.000 il rumore BTC per trade è ~2.4% (morde); soglie: ~€4.800 (<1%), ~€9.500 (<0.5%,
"punto dolce"), ~€19.000 (trascurabile). Le commissioni invece sono % e neutre al capitale.
Col real-truth ledger il rumore di arrotondamento entra DIRETTAMENTE nell'equity → più
capitale = ledger più fedele alla strategia. Riguarda i 15 sleeve che eseguono reale.
## Follow-up dal code-review (bassa priorità, non urgenti)
- [x] ~~forming-bar su ROT02/TSM01~~ — GIÀ FIXATO (v1.1.10, 2026-06-08): `_panel` condiviso
scarta la barra in formazione via `last_bar_is_forming`. Item rimasto stantio nel TODO.
- [ ] **ancora bfill di `_daily_equity`** — QUANTIFICATO 2026-06-11 (`daily_equity_bfill_impact.py`):
**NON materiale, lasciare documentato**. PORT06 OOS invariato per costruzione (il bias vive
in testa alla serie, pre-SPLIT; ΔSharpe +0.001, ΔDD 0.000); FULL DD leggermente OTTIMISTICO
(3.46→3.67 col fix: il primo trade DIP01 2021, 3.75%, è nascosto dal bfill). Nessun verdetto
di gate a rischio (bias identico nei due bracci A-vs-B, si cancella). Unica eccezione OOS:
TSM01 (primo punto equity post-SPLIT, 0.014 Sh). Correggere SOLO in un eventuale refactor
del builder daily, OVUNQUE in un colpo (~12 file di gate replicano la convenzione) e
ri-baselinando i canonici nello stesso commit. CAVEAT per gate futuri: finestre IDX che
partono a metà storia amplificano il bug → usare lì la convenzione carry-forward corretta.
- [x] ~~convenzione TR01 worker vs reference~~ — ERA UN BUG, FIXATO (2026-06-11): `mean(rets)`
sui soli asset in posizione sovrappesava N/k a paniere parziale (con 1 long: 0.45 del
capitale invece di 0.09). Fix: `sum(rets)/len(universe)` (equal-weight 1/N canonico).
Replay post-fix: +32% vs reference +42% (residuo = convenzione capitale-unico vs
media-equity, dichiarata). Diario `2026-06-11-stability-sweep.md`.
- [x] ~~engine duplicato nei 3 gate~~ — FATTO (2026-06-11): fattorizzato in
`scripts/analysis/_port06_gate_common.py` (`build_trades_variant` versione trendmax =
superset con hurst_mask/trend_max parametrici, `equity_from_trades`, `port_metrics`,
`dd`); i 3 gate importano da lì. Nessuna drift di matematica trovata fra le copie
(solo firme/docstring). Output dei 3 gate verificato BYTE-IDENTICO prima/dopo.
`dip_trades` (DIP01) NON è una copia ma un sibling deliberato (long-only, orig_gap,
j clampato) → resta nel suo script, documentato nel modulo comune.
- [ ] **epoche hardcoded in `hourly_report.lossguard_section`** (LOSSGUARD_SINCE, TRENDSWAP_SINCE):
ogni nuova epoca-filtro richiede di editare la funzione. Derivarle da deploy history/config.
## Code-review 2026-06-11 sera (8a2b065..) — finding DEFERITI (i confermati critici sono fixati in v1.1.26)
- [ ] **RESTING reduce-only esposti al netting** (TP limit + disaster-SL): se un worker opposto
apre DOPO il piazzamento, il resting può fillare parziale o essere respinto — il fallback
netting copre solo i close sincroni. È il pezzo "position manager / sotto-conti" già noto;
lo scenario peggiore è il disaster-SL cappato proprio nel crash per cui esiste.
- [ ] **Lifecycle `orphan_legs`**: append-only — un orfano risolto a mano (o da reset_flatten)
resta nello status e MASCHERA per compensazione un drift vero futuro (falso negativo del
reconciler). Serve un comando di risoluzione (`--resolve-orphan`) e/o reset_flatten che
azzeri anche lo stato reale nei status.json. Idem: il PnL della chiusura manuale di un
orfano non viene mai bookato in real_capital (diagnostica shadow divergente).
- [ ] **TP_PHANTOM residuo**: `resting_fills` guarda solo le ultime 100 righe di trade-history
per strumento — su conto molto attivo un fill TP reale può scivolare fuori finestra →
falso phantom persistente (sim resta in posizione, reale flat). Mitigato dal quantize
conservativo (v1.1.26); fix vero = endpoint order-state in cerbero-mcp.
- [ ] **Validazione feed a monte** (altitudine): TP_PHANTOM copre solo i tocchi TP dei single-leg;
le ENTRY spike-driven, lo SL close-confirm su close spike e lo z dei pairs restano esposti
ai wick fantasma del feed testnet. Un validatore barre nel data layer coprirebbe tutti i
consumer con un meccanismo solo (su mainnet il fenomeno non esiste: priorità bassa finché
si resta su testnet, gli eventi TP_PHANTOM/NET_CLOSE ne misurano la frequenza).
- [ ] **Contratto dello schema status.json**: reconcile (`src/live/books.py`), hourly_report e i
worker condividono lo schema per convenzione implicita — books.py è ora la fonte unica per
i campi `real_*`, ma un helper `worker.real_book()` usato da _save e dai reader chiuderebbe
la classe di bug. Pulizia: `_tp_hit` helper per i 4 siti di tocco TP duplicati; port_metrics
ri-biforcato in xs01_tranche_gate/pairs30m_gate (importare da _port06_gate_common);
splits3/metriche duplicate nei games engine (estrarre in scripts/games/engine.py).
## Ricerca dispersion/correlation (2026-06-08, 165 agenti) — follow-up opzionale
- [x] ~~Gate PORT06 di `index_comp_disp` W=168~~ — FATTO (2026-06-08): PROMOSSO MARGINALE.
Decorrela bene (corr 0.06 col MASTER) ma OOS PIATTO (Sharpe 0.01). **Documentato e
rimandato** (non deployato): gate in `dispersion_edges/gate_index_comp_disp.py`,
riprendere solo se si costruisce una famiglia DISP più ampia. Diario aggiornato.
## Monitoraggio (osservare, non agire subito)
- [ ] **Churn da spike-print testnet su ETH (2026-06-11)**: il feed testnet stampa wick
anomali sulla barra 1h ETH che (a) generano segnali short MR01/MR07 (lo spike È lo
z-score estremo) e (b) "toccano" il TP intrabar della stessa barra → il sim booka
+4% fantasma a bars_held=0, il reale apre+chiude pagando solo fee/spread (~0.17€
a giro, 14 giri oggi ≈ 2.3€). Il real-truth ledger contabilizza GIUSTO (per questo
esiste) e il report orario ora conta i win dal flag reale. MITIGATO in v1.1.23:
gate `TP_PHANTOM` (il tocco TP deve essere confermato dal fill del resting sul book
reale, o dal prezzo oltre il livello) → niente più chiusure a mercato su wick
fantasma. Resta l'ENTRY spike-driven (il segnale stesso nasce dal wick): NON
filtrarlo nei segnali (= fit su artefatto testnet); osservare la frequenza dei log
TP_PHANTOM — se cresce, valutare un gate di QUALITÀ FEED nel data layer.
- [ ] **FADE in coda storica (2026-06-11)**: il rolling 120g equal-weight delle 6 fade è al
**2° percentile** della propria storia (1.0% vs p5 +0.4%); il PORT06 complessivo resta
in variazione normale (19-28° pct). NESSUN ritocco parametri (= fit sul regime corrente);
osservare se rientra. Follow-up tecnico: alert di drift per-FAMIGLIA nel `hourly_report`
(precomputare la distribuzione storica dei rolling-return di famiglia dal backtest
canonico → confrontare il rolling live; notifica sotto p5).
- [ ] **PnL live post-swap trend** (epoca TREND in `hourly_report`): verdetto stop-rate fade a n>=30.
- [ ] **SH01 post-bootstrap**: il trade-rate live deve scendere da ~25% a ~10% delle barre
(selettività della soglia ripristinata). Controllare nel report orario.
- [ ] **MR02_ETH**: ultima posizione aperta SENZA disaster-SL (aperta pre-v1.1.4). Si chiuderà
normalmente; le prossime aperture avranno il bracket.
- [ ] **parquet fresco per SH01 bootstrap**: oggi al 2026-05-28, il feed copre 365g → margine ~11
mesi. Rilanciare `download_all()` periodicamente (WARN automatico se il gap supera il lookback).
@@ -0,0 +1,71 @@
# 2026-06-06 — Improvement sweep multi-agente (review live PORT06)
> Ricerca multi-agente del 2026-06-06 (106 agenti, 196 finding grezzi, 60 verificati avversarialmente, 37 validi / 23 uccisi).
> Workflow: port06-improvement-sweep (wf_56b7d4de-a06).
## 1. Quadro d'insieme
Il sistema PORT06 gira correttamente nella struttura (17 sleeve, runner pool/ledger == backtest), ma la review ha trovato **due classi sistematiche di problemi**, non bug isolati.
**(a) Divergenze live-vs-backtest per omissione di protezione.** Il pattern documentato del progetto è "il live AGGIUNGE protezione che il backtest canonico non ha" (Hurst, EXIT-16) → il live fa meglio del backtest sul DD. Qui si trova il contrario in più punti: il filtro `trend_max=3.0` è ON nel backtest canonico ma **OFF in produzione su tutte e 6 le fade** (base.py:91 / _defs.py:35-38); il cap `SHAPE=0.0588` v1.1.2 è **inerte** (sovrascritto da portfolios.yml) → SH01 gira a **2x peso** senza stop-loss; EXIT-16 non è esteso a DIP01 (l'unico sleeve BTC che esegue davvero); la forming-bar lesson non è portata a TR01 né a PairsWorker. Diversi diari (2026-06-04-partial-tp-ladder.md:15, 2026-06-04-exit-lab.md) documentano config che il codice NON implementa.
**(b) Calibrazione/sizing che diverge dal validato.** SH01 fa fuoco sul 25% delle barre vs 2.9% validato (th0.58 inerte per train-window troppo corto); i pairs girano a `pos=0.5` mentre tutta la validazione PR01 è `pos=0.15` (DD ~3.3x); TR01 sotto-carica le fee 2x (manca il fattore leva).
Tema ricorrente onesto: **la maggior parte delle "scoperte" sono ripristini di config già validate, non nuovo alpha.** I bug di config (cap, trend_max) vanno corretti subito; ogni cambio di *strategia* deve passare il gate PORT06/OOS (precedente FR01: robusto-individualmente ≠ migliora PORT06).
## 2. P0 — Protezione capitale / gap live
| Titolo | Cosa fare | Evidenza | Effort |
|---|---|---|---|
| **Cap SHAPE 0.0588 inerte → SH01 a 2x peso** | `load_active_portfolio` deve fare **deep-merge** dei caps (partire da `p.caps` e aggiornare con `ov["caps"]`), oppure ridichiarare il dict completo in `portfolios.yml:7` come `{PAIRS:0.33, SHAPE:0.0588}`. Ri-snapshot pesi, confermare sum=1.0. SH01 non ha SL by-design e ha già subìto un 15.6% live: il cap è l'UNICA protezione coda. | `src/portfolio/base.py:91-93` setattr sovrascrive caps; `portfolios.yml:7` `{PAIRS:0.33}`; `scripts/portfolios/_defs.py:75` `{PAIRS:0.33,SHAPE:0.0588}`; live `data/portfolios/PORT06/status.json` SH_BTC=SH_ETH=0.0588 (totale 0.1176 = 2.0x intended). Backtest canonico usa il cap giusto (FULL 6.43/OOS 8.58) → è una rottura di parità. | low |
> Nota: la stessa correzione risolve anche il sotto-peso ~9% di tutti gli altri non-pairs (0.0588 live vs 0.0647 intended); il rischio materiale è solo SHAPE.
## 3. P1 — Alto valore, testabile subito
| Titolo | Cosa fare | Evidenza | Effort |
|---|---|---|---|
| **`trend_max=3.0` OFF in produzione su tutte le 6 fade** | Aggiungere `"trend_max":3.0, "ema_long":200` alla SleeveSpec FADE `_defs.py:36-37`. Ripristina la config del backtest canonico (look-ahead-free, EMA200 causale). MR02/MR07 lo onorano (non lo ignorano). Ri-validare a livello PORT06 (template `exit16_port06_impact.py`) perché Hurst+trend si sovrappongono. | `_defs.py:35-38` (no trend_max); `MR01_bollinger_fade.py:61` `get('trend_max')`→None; `combine_portfolio.py:47` canonico usa trend_max=3.0; `risk_management.py:242` lo applica a tutte e 3. Live: MR01_ETH/MR02_ETH hanno fadato il crash ETH 06-05 più volte (long@1658.9 poi long@1592.8 in caduta 1727→1517), tutti stoppati (trades.jsonl). Misurato (live-hurst-on, 3x): MR02_ETH DD 27.3%→16.2%, WR +8pp, Sharpe +1.34 (return ~dimezzato — trade-off DD/return). | low |
| **Disaster-bracket on-book durante outage feed** | Il poll-loop intero è in un try/except: in un outage le posizioni REALI restano **senza stop sul book e senza valutazione exit**. Piazzare un bracket lontano (~30%) su Deribit per i fade eseguiti [MR01,MR02,MR07,DIP01] via `cerbero_client.py:137 set_stop_loss` (esiste, non cablato). Non spara mai in operatività normale → 0 costo Sharpe; pura assicurazione. | `runner.py:246-298` (try/except unico, sleep su errore); ETH gap p99.9=996bps, max=3301bps, peggior 3h 3492bps (`data/raw/eth_1h.parquet`); REAL_OPEN 06-05 verificati; `docs/diary/2026-06-04-exit-lab.md:119-121` lo segnala, mai implementato. Caveat: nei crash il fill è al gap, non al livello → cappa ma non al trigger. | low-med |
| **SH01 th0.58 inerte: fa fuoco sul 25% delle barre vs 2.9% validato** | **Ri-validare SH01 OOS con train-window pari al regime live (~8760 barre)** invece dell'expanding full-history. La LogReg su train piccolo è over-confident (mean max-proba 0.582 vs 0.536). NON limitarsi ad alzare th (la calibrazione resta sbagliata). Se l'edge non sopravvive nel regime live, riconsiderare SH01. | misurato: small-train frac≥0.58 = 44.8% vs full 3.8%; live-window 1197 entry (49.9% long) vs 139 (71.9% long), trade-rate 25.1% vs 2.9%. `SH01_shape_ml.py:60` th=0.58; tutta la validazione (`shape_ml_validate.py`) usa training expanding. È la causa meccanica del WR live 1/9. Blast radius limitato dal cap SHAPE (vedi P0). | low (test) |
| **Pairs a pos=0.5 vs validazione pos=0.15 (DD ~3.3x)** | Introdurre un `position_size` per-famiglia (plumb `SleeveSpec`/`build_worker_for`, runner.py:43-89) e portare i pairs a **0.15-0.25**. Ri-eseguire `combine` alla pos live prima del deploy. I pairs sono la famiglia a DD-grezzo più alto (ETH/BTC 47.6%) e **senza stop** (exit solo |z|≤z_exit o max_bars). | `portfolios.yml:14` pos=0.5; `pairs_worker.py:157` pnl=cap*pos*net; `pairs_research.py:29` POS=0.15; live ADA/ETH 06-05 net 8.52% = 4.26% sleeve (sarebbe 1.28% a 0.15). Choice deliberata (commento yaml), ma diverge dal validato. | low-med |
| **TR01: fee sotto-caricate 2x (manca leva)** | Moltiplicare il termine fee per `self.leverage` (basket_trend_worker.py:69) per allinearlo al return leveraged (riga 67) e al reference `fee=FEE_RT/2*LEV` (honest_improve2.py:150). | worker fee/flip 1.5e-05 vs corretto 3.0e-05 (POS0.15/LEV2/N5) = 2.0x. tsmom/rotation_worker sono consistenti → TR01 è l'unico difettoso. Drag assoluto piccolo (low turnover) ma bias ottimistico mono-direzionale che gonfia total_capital nel rebalance. | low |
| **PairsWorker agisce sulla barra IN FORMAZIONE** | Portare la EXIT-16 forming-bar logic ai pairs: calcolare `bar_ms` da `median(diff(ts))` e valutare z sull'ultima barra COMPLETA (entry+exit), come `strategy_worker.py:445-447`. | `pairs_worker.py:185` i=len(m)-1, :195 exit, :204-207 entry su barra in formazione; backtest `pairs_research.py:69-75` solo close settled; `validate_worker_pairs.py` non esercita il path → divergenza non testata. | low |
| **TR01 valuta crossover EMA sulla barra 4h in formazione** | In `tick` scartare la barra in formazione (`k=-1 if now_ms≥ts[-1]+bar_ms else -2`) e valutare il segnale sull'ultima 4h COMPLETA; correggere ANCHE il booking del return (riga 66 legge la finestra in formazione). | `basket_trend_worker.py:60-72` legge `[-1]` (forming); reference `honest_improve2.py:152` `range(n-1)` solo completi. Evidenza: SOL flip 0→1@85.66 poi 1→0@77.72 a 59min, stessa finestra 08-12 UTC (9.3% intra-window glitch). Fix incompleto se si tocca solo il segnale. | low |
| **DIP01 senza EXIT-16 (ed è l'unico sleeve BTC che esegue reale)** | Aggiungere `"sl_confirm_atr":0.5` (e valutare `hurst_max`) ai params DIP01 `_defs.py:41-42` (il worker lo legge già, zero codice). **Ri-validare sul grid OOS proprio di DIP01 + a livello PORT06 con engine gap-aware** prima del deploy (DD standalone sale BTC 23→37%). | `_defs.py:41-42` DIP01 ha solo {min_tp_frac}; worker gira il branch intrabar wick-sensitive (`strategy_worker.py:466`) invece del close-confirm (:429). Riproducibile: BTC FULL 299→726/OOS 60→168, ETH FULL 187→898/OOS 221→255. I 3 fade BTC sono flat (0 ordini reali); DIP01_BTC è l'UNICO BTC con round-trip reale → gap non teorico. | low (fix) / med (validazione) |
| **TSM01 252d lookback: shortfall su un asset tronca l'inner-join sotto need=253, tick() esce in silenzio** | Aggiungere un WARN/Telegram quando `panel<need` (gated su "era già operativo" per evitare falsi positivi al cold-start). Coprire anche RotationWorker (stesso silent return). | `runner.py:37` 1d=440; `tsmom_worker.py:53-55` need=253, return muto; `rotation_worker._panel how='inner'` tronca al più corto; live margine 188 (OK ora) → latente, non attivo. | low |
| **Osservabilità: TR01/ROT02/TSM01 invisibili in hourly_report** | `hourly_report.collect()` filtra su chiavi (`event`, `in_position`) che i worker multi-asset non emettono → 100% delle loro righe scartate. Aggiungere `in_position` ai `_save()` dei 3 worker e/o esporre `status_summary` (long correnti + giorni-dall'ultimo-flip). Distingue "flat in chop" da "wiring rotto". | TR01 2 record (entrambi 06-03, lo SOL whipsaw), flat da allora; ROT02/TSM01 0 trade (rispettivamente cash / risk-off da 06-01, by-design). `hourly_report.py:105,111`; worker `_log`/`_save` senza `event`/`in_position`. | low |
| **MR07 reale: ripristino-fill non esercitato (short-side TP-resting mai testato)** | Smoke testnet `live_shadow_smoke.py` con `direction=-1` per esercitare il limit reduce-only BUY sotto mercato (può crossare subito → path `resting_fills`). MR07 (+ tutti i worker) ha 0 TP-resting filled finora → reconciliation completamente non testata in produzione. | MR07_ETH 5-6 REAL_OPEN tutti 06-03 PRE-fix (TP-resting 2026-06-04); +257/267/283/333bps slippage legacy (sunk, non ricorrente). Tutti i REAL_TP_RESTING verificati sono side=buy (long); il path short non è mai stato esercitato né in ledger né negli smoke. `strategy_worker.py:253` `_place_real_tp`. | low |
## 4. P2 — Filoni di ricerca
- **MR01 ETH k=2.0 vs k=2.5 deployato.** k=2.0 è uniformemente meglio OOS su ETH (no-filtro +1238% vs +487%; trend-filtrato +569% vs +196%) e neutro su BTC sotto filtro live. Non è nuovo overfit: è il config per-asset *documentato* (MR01:15-16) che il k=2.5 universale ha silenziosamente abbandonato. Da confermare a livello PORT06 con i param live (hurst/sl_confirm). `risk_management.py:40,44`.
- **Limit-order maker entry sui fade — NO-GO confermato.** Strutturalmente infattibile: il segnale fade fa fuoco quando il close ha già bucato la banda (mediana 33bps sotto); un buy-limit al livello è marketable→taker; un limit più profondo manca i winner che rimbalzano in 1-4 min. Negativo-EV, non implementare. `MR01_bollinger_fade.py:84,93`; live MR07_ETH 5 open→TP in ~1.4min.
## 5. P3 / Scartati in verifica
- **Commento errato in `_defs.py:17-18`** ("MR02/MR07 ignorano min_tp_frac") — falso (lo leggono e applicano, MR02:46,72), ma il filtro è di fatto inerte su MR02/MR07 nel campione → solo igiene doc, no PnL.
- **TSM01 inerte (0 trade da 06-03)** — corretto by-design (BTC sotto SMA100 dal 06-01 + tutti gli asset consensus negativo). Solo diagnostica; NON redistribuire il capitale (riduce la diversificazione quando il regime gira).
- **Time-limit fade NON sono un leak** — myth-busting: il bucket max_bars è net-positivo OOS (+118÷+195bps, +154%/+335% totale); il vero loser è lo SL (già gestito). NON accorciare max_bars.
- **Fill max_bars next-open vs close** — wash misurato (5/6 sleeve identici); il worker già filla ~next-open. Nessuna azione.
- **SH01 H=12 fuori scope `exit_lab.py`** — routing: usare `sh01_exit_lab.py` (gap-aware) per qualsiasi studio orizzonte SH01. Clarificazione, non bug.
- **PairsWorker bars_held conta poll-con-nuovo-ts non barre reali** — latente (0 time_limit su 9 close), bite solo dopo outage >2h con z ancora oltre soglia; rischio di introdurre exit-anticipato se bar_ms via median-diff su feed testnet flaky → usare bar_ms fisso. Igiene, bassa priorità.
## 6. Piano consigliato (2 settimane)
**Settimana 1 — protezione capitale e parità (config-only, niente cambio di strategia):**
1. **Giorno 1 — P0 cap SHAPE.** Deep-merge dei caps in `base.py:91`; ri-snapshot pesi PORT06, confermare SH totale=0.0588, sum=1.0, nessuno sleeve affamato. Verificare contro il backtest canonico (FULL 6.43/OOS 8.58). Questo dimezza l'esposizione SH01-no-SL già colpito dalla coda.
2. **Giorno 1-2 — TR01 fee 2x + forming-bar (TR01 e Pairs).** Fix puramente di parità/contabilità, nessun gate OOS necessario (avvicinano il live al backtest validato). Coprire il silent-return TSM01/Rotation con WARN.
3. **Giorno 2-3 — disaster-bracket on-book + osservabilità outage.** Cablare `set_stop_loss` lontano (~30%) sui 4 fade eseguiti; aggiungere alert su N poll falliti consecutivi con `real_in_position=True` e log del gap% al risveglio. Smoke testnet (€0). Pura assicurazione coda, 0 costo Sharpe.
4. **Giorno 3 — osservabilità report** (TR01/ROT02/TSM01 in hourly_report) + smoke MR07 short-side TP-resting.
**Settimana 2 — ripristini di config validata, ognuno dietro il gate PORT06/OOS:**
5. **Giorno 4-6 — `trend_max=3.0` sulle 6 fade.** Implementare in `_defs.py`, poi **ri-eseguire il gate PORT06** (template `exit16_port06_impact.py`) misurando trend-ON+hurst+EXIT-16 vs attuale trend-OFF: deploy SOLO se Sharpe OOS non peggiora e DD scende (sovrapposizione hurst+trend va quantificata).
6. **Giorno 6-8 — pos pairs per-famiglia.** Plumbing `SleeveSpec.position_size`; ri-eseguire `combine` a pos live (target 0.15-0.25); scegliere il punto sul trade-off DD/ritorno; deploy.
7. **Giorno 8-10 — DIP01 EXIT-16+hurst.** Validare sul grid OOS proprio di DIP01 (famiglia honest, meccanismi tarati su fade) **con engine gap-aware**, poi gate PORT06. Deploy solo se passa entrambi.
8. **Giorno 10-12 — SH01 ri-validazione train-window.** Ri-eseguire validazione con train cap ~8760 barre (regime live). Se l'edge sopravvive → tarare th/calibrazione su slice held-out; se non sopravvive → riconsiderare SH01 (NON ri-tunare il th a forza). **Mai impostare sl/sl_confirm_atr su SH.**
Filoni P2 (MR01 k ETH) e cleanup P3 in coda, opportunistici. Regola trasversale: i punti 1-4 sono fix di bug/parità e vanno per primi; 5-8 toccano il comportamento di trading e **nessuno va in produzione senza superare il gate OOS + PORT06** (precedente FR01: robusto individualmente ≠ migliora il portafoglio).
+49
View File
@@ -0,0 +1,49 @@
# 2026-06-07 — DIP01 EXIT-16: PROMOSSO (gate gap-aware, il più netto della serie)
Punto 9 della roadmap sweep: DIP01 era l'unico sleeve BTC con esecuzione REALE
round-trip e girava ancora sul branch SL intrabar wick-sensitive. EXIT-16 era
validato SOLO sulle fade: estensione a una strategia honest dietro doppio gate
(grid proprio di DIP01 + PORT06), con engine GAP-AWARE come da lezione exit-lab.
## Metodo (`scripts/analysis/dip01_exit16_impact.py`)
- Parità con la equity canonica: corr **1.00000**, ret +322% identico.
- **Engine gap-aware**: nel ramo orig lo SL filla a `worse(livello, open[j])`
l'engine canonico filla "al livello" anche su gap-through (54% dei casi per stop
tight) e regalerebbe al ramo intrabar un vantaggio fittizio.
- Grid 3×3×2 (z_in × sl_atr × max_bars) su BTC (deployato) ed ETH (robustezza);
train (pre 2024-10-12) e OOS separati. Buffer plateau {0.4, 0.5, 0.75, 1.0}.
- NB `hurst_max` NON valutato su DIP01: il gate trendmax dello stesso giorno ha
mostrato che è ridondante-dannoso post-EXIT-16.
### Trappola di metodo trovata: l'ancora bfill di `_daily_equity`
La prima parità falliva (corr 0.9956, 16pp) con **engine identici** (662 trade,
stessi return, stesso capitale finale). Causa: la convenzione canonica
`_daily_equity` (serie a punti trade-exit → `reindex(IDX).ffill().bfill()`)
**ancora il primo valore della finestra al PRIMO trade dentro IDX** (es. 942.54
dopo le perdite di gennaio 2021), non al capitale portato avanti dall'ultimo trade
del 2020 (979.23). Il mio replay hourly era più "corretto" ma per parità va
replicata la convenzione. **Finding aperto**: questo bias tocca le metriche
canoniche di TUTTI gli sleeve a punti-trade (honest/pairs/tsm) — l'ancoraggio
distorce leggermente il punto di partenza della finestra. Da valutare a parte se
correggerlo OVUNQUE in un colpo solo (cambierebbe i numeri canonici di riferimento).
## Risultati
- **Grid BTC: 36/36** (EXIT-16 ≥ orig gap-aware in train E OOS in tutte le 18
celle). OOS Sharpe ~2-4x (cella canonica 1.47→3.48), DD giù in 14/18 celle.
- **Grid ETH: 35/36** (unica eccezione la cella estrema z3.0/sl3.0/mb48, marginale).
- **Plateau buffer piatto**: OOS DD 6.4% identico da 0.4 a 1.0; scelto 0.5 (== fade).
- **PORT06**: FULL Sharpe 6.43→6.61, DD 3.96→3.58 | OOS Sharpe 8.58→8.77, DD
1.36→1.34. Migliora tutto. **PROMOSSO**.
È la 5ª conferma indipendente del principio EXIT-16: gli stop intrabar da wick
sono falsi negativi per le strategie mean-reversion — l'overshoot che buca lo
stop è il movimento che si sta comprando.
## Deploy
`_defs.py`: `"sl_confirm_atr": 0.5` nei params DIP01_BTC (il worker legge già il
param — zero codice). Live path: TP intrabar invariato, SL solo su close confermato,
disaster-bracket on-book già attivo per gli outage.
@@ -0,0 +1,46 @@
# 2026-06-07 — Gate position_size PAIRS: 0.5 → 0.20 (assicurazione, come il cap SHAPE)
Punto 8 della roadmap improvement-sweep: i pairs giravano col `position_size`
GLOBALE 0.5 a leva 2 (esposizione 1.0 della fetta) mentre TUTTA la validazione PR01
è a pos 0.15 lev 3 (esposizione 0.45) — ~2.2x il validato su una famiglia **senza
stop** (exit solo |z|≤z_exit o max_bars). Scelta deliberata (commento yml) ma mai
gateata. Evidenza live: ADA/ETH 2026-06-05 net 8.52% = 4.26% di sleeve in un trade.
## Metodo
`scripts/analysis/pairspos_port06_impact.py`: le 5 equity pairs ricostruite con
`pairs_sim` alla **leva live (2x)** su griglia pos {0.50, 0.25, 0.20, 0.15},
innestate nel PORT06 canonico (pesi cap PAIRS 0.33 + SHAPE 0.0588). Parità
verificata: `pairs_sim(0.15, lev3)` == equity canoniche (corr 1.00000, 5/5).
## Risultati
| pos | esp. | FULL Sh / DD | CAGR | OOS Sh / DD | fam DD | worst pair DD grezzo |
|---|---|---|---|---|---|---|
| 0.50 (live) | 1.00 | 6.69 / 4.77% | 130% | 9.05 / 3.40% | 14.0% | ETH/BTC 78% |
| 0.25 | 0.50 | 6.50 / 4.04% | 67% | 8.69 / 1.54% | 7.2% | 51% |
| **0.20** | **0.40** | 6.33 / 3.90% | 57% | 8.43 / **1.26%** | 5.8% | 44% |
| 0.15 | 0.30 | 6.03 / 3.78% | 47% | 7.99 / 1.33% | 4.3% | 35% |
Letture:
- Il gate meccanico (OOS Sharpe non peggiora) **boccia ogni riduzione**: ovvio —
ridurre l'esposizione di una famiglia che in-sample non è mai esplosa costa sempre
Sharpe. Ma il +130% CAGR / +2.6e9% compounded di ETH/BTC a pos 0.5 è fantasia da
compounding: dice solo che il tail event non è ancora successo in-sample.
- **0.20 è il ginocchio**: OOS DD 1.26% batte anche 0.15 (1.33% — oltre il ginocchio
si taglia ritorno senza guadagnare DD); esposizione 0.40 ≈ il validato 0.45.
- Decisione di tipo assicurativo, identica al precedente cap SHAPE (adottato pagando
0.24 OOS Sharpe).
## Decisione (utente): pos PAIRS = 0.20
Implementazione:
- `runner.pos_for_spec(sid, global_ps, family_overrides)` — override per-famiglia
(chiave = `weighting.family_of`), plumbato in `build_worker_for`.
- `portfolios.yml`: `position_size_family: {PAIRS: 0.20}` (globale 0.5 invariato
per fade/dip/shape).
- Test: `test_pos_for_spec_family_override`, `test_build_pairs_worker_position_size`.
Nota di transizione: la posizione LTC/ETH aperta al deploy (entrata a sizing 0.5)
verrà chiusa col pos nuovo 0.20 → il pnl % sleeve di quel singolo trade è bookato
alla taglia nuova (one-time, conservativo).
@@ -0,0 +1,51 @@
# 2026-06-07 — SH01 punto-10: il regime live (train 365g) NON è robusto → bootstrap full-history
Punto 10 della roadmap sweep: ri-validare SH01 col train-window del regime live
(~8760 barre, il `_ML_LOOKBACK_DAYS=365` del runner) invece dell'expanding
full-history usato da tutta la validazione storica.
## Ri-validazione (`scripts/analysis/sh01_trainwindow_validate.py`)
`ml_wf_entries(train_window=...)` rolling su storia piena ≈ il regime live.
Protocollo explore_lab (FULL / OOS 30% / sweep fee / anni positivi), W24 H12 th0.58.
| BTC train | trade-rate | FULL / OOS | Sharpe | fee 0.2%: FULL/OOS | anni+ | robusto |
|---|---|---|---|---|---|---|
| 8760 (live) | **21.7%** | +82 / +74 | 1.31 | **42** / +20 | 6/9 | ❌ |
| 17520 (2y) | 15.4% | +166 / +96 | 2.05 | +9 / +47 | 6/9 | ❌ |
| 26280 (3y) | 11.8% | +348 / +95 | 3.09 | +109 / +61 | 7/9 | ❌ |
| EXPANDING | 9.8% | +219 / +42 | 2.72 | +60 / +26 | 8/9 | ✅ |
ETH a tw=8760: **FULL 33%, Sharpe 0.02** (lancio di moneta che paga fee).
Conferme: (a) la diagnosi del sweep era esatta (trade-rate 22-26% live vs 10%
validato — soglia inerte per over-confidence su train piccolo); (b) la
progressione è MONOTONA: l'edge di SH01 *è* la memoria lunga; (c) lo sweep della
soglia nel regime corto dà risultati instabili e incoerenti fra asset → ri-tunare
th sarebbe curve-fitting sul rumore di calibrazione (come da warning del piano).
## Decisione (utente): bootstrap full-history
Ripristinare in live ESATTAMENTE il regime validato (expanding full-history):
1. **`ml_wf_entries(last_block_only=True)`** — fitta/predice SOLO l'ultimo blocco
del walk-forward. I confini dei blocchi sono deterministici (start + k·retrain)
e al worker servono solo i segnali recenti → le entries dell'ultimo blocco sono
**identiche per costruzione** al tail del WF completo (parity test esatto in
`tests/portfolio/test_sh01_last_block.py`). Costo live: **0.6 s/tick** su 73k
barre (1 fit LogReg) vs ~140 fit del WF completo.
2. **Bootstrap storia nel runner**: per gli sleeve `ml` il tick riceve
`concat(parquet_locale < feed_start, feed_live)` (`runner._with_history`).
Gap-guard: se il parquet è stantio oltre il lookback del feed → solo feed +
WARN una-tantum (meglio il regime corto dichiarato che una serie col buco).
Manutenzione: tenere fresco il parquet con `download_all()` (oggi al 2026-05-28,
il feed copre 365g → margine ~11 mesi).
3. `_defs.py`: `params={"last_block_only": True}` sugli sleeve SHAPE (solo path
live; il backtest canonico resta WF completo).
Effetto atteso live: trade-rate SH01 da ~25% a ~10% delle barre, selettività
della soglia ripristinata, WR atteso ≈ validazione (acc OOS ~56% BTC).
Il monitor naturale è il report orario (SH01 farà MENO trade).
Nota: `_ML_LOOKBACK_DAYS` resta 365 (il feed serve comunque per le barre recenti
e come fallback se il parquet è stantio).
+101
View File
@@ -0,0 +1,101 @@
# 2026-06-07 — Implementazione fix improvement-sweep (P0 + punti 2-6, settimana 1)
Implementati e deployati i fix "settimana 1" del piano dello sweep 2026-06-06
(tutti fix di parità/protezione/osservabilità, nessun cambio di strategia → nessun
gate OOS richiesto). Versioni: v1.1.3 (punti 1-4), v1.1.4 (punti 5-6).
## P0 — cap SHAPE 0.0588 ripristinato (v1.1.3)
`portfolios.yml` ridichiara il dict caps COMPLETO `{PAIRS: 0.33, SHAPE: 0.0588}`
(l'override sostituisce interamente i caps di `_defs.py` via setattr in
`base.py:load_active_portfolio` — footgun ora commentato nel yml). Verificato live
post-restart: SH_BTC+SH_ETH = 0.0588 totale (prima 0.1176 = 2x intended), non-cappati
risaliti a 0.0647, sum = 1.0 — parità col backtest canonico (FULL 6.43 / OOS 8.58).
## Punti 2-4 — parità worker multi-asset (v1.1.3)
- **TR01 fee × leva**: `POS · LEV · fee_rt/2` per flip come il reference
`honest_improve2._tr_basket_daily:150` (prima sotto-caricata 2x).
- **TR01 forming-bar**: crossover EMA E booking del return solo su barre 4h COMPLETE
(riga -1 = candela in corso finché non è trascorsa la sua durata, pattern EXIT-16).
Evidenza live che non può più ripetersi: flip SOL 0→1→0 in 59min stessa finestra 4h.
- **PairsWorker forming-bar**: entry ED exit sul close di barra COMPLETA, come il
backtest `pairs_research` (close settled).
- **TSM01/ROT02 silent-return**: WARN log + Telegram `PANEL_SHORT` quando il panel
inner-join è sotto il lookback (helper condiviso `_warn_panel_short` in
rotation_worker), gated su "era già operativo", una notifica per episodio.
Verifiche: 72 test (7 nuovi), `validate_worker_pairs` ESATTO, `validate_honest_workers`
invariato. **Nota onesta**: TR01 replay worker 44% vs reference +42% è IDENTICO
pre/post fix → divergenza di convenzione pre-esistente (capitale-unico con
`mean(rets)` solo sugli asset in posizione vs media-equity 1/N del reference).
Da rivisitare a parte: "stesso ordine di grandezza" oggi non regge più.
## Punto 5 — disaster-bracket on-book + alert outage (v1.1.4)
Durante un outage (poll-loop in except) le posizioni REALI restavano senza stop sul
book e senza valutazione exit. Ora:
- **`ExecutionClient.place_disaster_sl`**: STOP_MARKET reduce-only a ~30%
dall'ingresso (trigger sul mark), piazzato a ogni REAL_OPEN sui fade eseguiti
(MR01/MR02/MR07/DIP01) e cancellato in `_real_close` (prima del TP-reconcile).
In operatività normale non scatta mai → 0 costo Sharpe; nei crash il fill è al
gap (cappa la coda, non la elimina). Config: `overrides.execution.disaster_sl_pct`
(default 0.30, 0 = off). `real_dsl_order_id` persistito (resume-safe).
- **SCOPERTA**: il `set_stop_loss` di cerbero-mcp è un `private/edit` Deribit (solo
ordini APERTI) → inutilizzabile su market già fillati; il bracket va piazzato come
trigger order autonomo via `place_order(type="stop_market")` (già supportato).
- **Semantica cancel trigger order** (verificata su testnet): la cancel risponde con
lo stato AL MOMENTO della cancel (`untriggered` = successo; il re-cancel dà
`order_not_found`); `error` = non più in book (probabile trigger scattato → il
market a valle filla 0 e REAL_CLOSE esce verified=False).
- **Alert `FEED_OUTAGE`**: dopo 5 poll falliti consecutivi (~5 min) Telegram con
l'elenco delle posizioni reali aperte; notifica di ripresa con durata.
Smoke testnet (`live_shadow_smoke.py`, esteso): 4 scenari long+short — il path
**short-side TP-resting** (BUY limit reduce-only) non era MAI stato esercitato.
Tutti verdi: resting/cross-immediato due lati, DSL piazzato/cancellato (verificato
zero ordini orfani sul book), conto flat a fine smoke.
## Punto 6 — osservabilità multi-asset (v1.1.4)
- `in_position` aggiunto ai `_save()` di TR01/ROT02/TSM01.
- `hourly_report`: nuova sezione **MULTI-ASSET** (book corrente | ultimo flip |
freschezza status) — prima i 3 worker erano invisibili nel report (collect()
filtra su event/in_position che non emettevano): impossibile distinguere
"flat/risk-off by-design" da "wiring rotto". I multi-asset sono esclusi dalla
tabella IN CORSO (assume entry/bars single-leg).
## Code review multi-agente del giorno stesso (v1.1.7)
Review a 7 angoli su `8c4e1cd..HEAD` + check trades live. Il candidato top dei finder
("stop_market senza trigger") è REFUTATO: cerbero-mcp traduce `price→trigger_price +
trigger=mark_price` e in produzione 2 DSL erano armati + 1 ciclo completo pulito.
Quattro fix applicati:
1. **Alert `REAL_DIVERGENCE`** (|slippage sim/reale| ≥ 100bps a open/close). Scoperta
dal check trades: alle 10:37 uno **spike print testnet** (candela 10:00 H=65618 con
O/C~62400) ha fatto shortare alle 3 fade BTC un close fantasma a 65266.5 — il reale
ha fillato correttamente a ~62395 (440bps) ma il sim ha bookato +2.26 mai esistiti
(MR07 reale 0.13). Prima passava in silenzio.
2. **`FEED_OUTAGE` anche su feed degradato senza eccezione** (HTTP 200 con candles
vuote → i worker saltavano il tick in silenzio e lo streak restava 0). Helper unico
`_outage_tick` (fix anche dell'incoerenza chiavi minuti/durata_min).
3. **`src/live/bars.py`**: detection forming-bar UNIFICATA (era copiata in 4 punti,
con `_check_stale_feed` che hardcodava 1h). È l'invariante di sicurezza di EXIT-16:
ora vive in un posto solo, testato (`test_bars.py`).
4. **DSL cancel hardening**: retry su errore transitorio + alert Telegram
`REAL_DSL_CANCEL_FAIL` se lo stop resta forse ORFANO sul book (prima l'id veniva
dimenticato in silenzio → lo stop stantio poteva colpire la posizione successiva);
`order_not_found` = probabile trigger durante outage → solo log (il close a valle
esce già verified=False). Test con executor finto (`test_real_close_dsl.py`).
Finding noti NON ancora fixati (in coda): ROT02/TSM01 valutano la candela 1d in
formazione al primo poll dopo mezzanotte (stessa classe del fix TR01/Pairs, pre-esistente);
engine dei gate copiato fra i 3 script `*_port06_impact.py`; epoche hourly_report hardcoded.
## Resta in roadmap (settimana 2, OGNUNO dietro gate PORT06/OOS)
trend_max=3.0 sulle 6 fade → pos pairs 0.15-0.25 per-famiglia → DIP01 EXIT-16
(validazione gap-aware) → SH01 ri-validazione train-window ~8760 barre.
Follow-up tecnico: divergenza convenzione TR01 worker vs reference (sopra).
@@ -0,0 +1,64 @@
# 2026-06-07 — Gate trend_max sulle fade live: BOCCIATA l'aggiunta, PROMOSSO lo swap hurst→trend
Punto 7 della roadmap improvement-sweep: ripristinare `trend_max=3.0`/`ema_long=200`
sulle 6 fade live (ON nel backtest canonico, OFF in produzione). Il piano imponeva il
gate PORT06 perché hurst e trend si sovrappongono. Esito sorprendente ma coerente.
## Metodo
`scripts/analysis/trendmax_port06_impact.py` — engine riusato da `exit16_port06_impact`
(replay esatto del path canonico, parità verificata: corr 1.00000 e diff 0.00% su tutte
e 6 le fade) + maschera Hurst IDENTICA al live (`fade_base.hurst_skip_mask`, close-only,
w=100 step=6 — non la cache regime_lab). Tutte le varianti girano sul **path live**
(EXIT-16 close-confirm 0.5 attivo); PORT06 con pesi cap canonici (PAIRS 0.33,
SHAPE 0.0588), OOS da 2024-10-12.
Nota di metodo: il test PORT06 del loss-guard Hurst (`fade_lossguard_port_test`,
2026-06-02) era su entries GIÀ trend-filtrate E sull'engine PRE-EXIT-16 (SL intrabar).
La config live reale (hurst senza trend, con EXIT-16) non era MAI stata gateata.
## Risultati (PORT06, fade in path live EXIT-16)
| Variante | FULL Sh | FULL DD | FULL CAGR | OOS Sh | OOS DD | OOS CAGR |
|---|---|---|---|---|---|---|
| LIVE (hurst, no trend) | 7.23 | 2.68% | 68% | 9.35 | 1.68% | 92% |
| CANDIDATO (hurst+trend) | 7.11 | 2.06% | 59% | 9.36 | 1.62% | 83% |
| **TREND-ONLY 3.0** | 7.89 | 2.46% | 82% | **9.91** | **1.20%** | 103% |
| NESSUNO (solo EXIT-16) | **8.07** | 3.35% | **105%** | 9.72 | 1.38% | 119% |
| TREND-2.5 | 7.75 | 2.46% | 76% | 9.76 | 1.20% | 96% |
| TREND-3.5 | 7.87 | 2.46% | 84% | 9.64 | 1.20% | 103% |
## Letture
1. **CANDIDATO BOCCIATO** (gate formale): il DD scende (2.68→2.06) ma FULL Sharpe
0.12 e CAGR 9pp — over-filtering, i due filtri tagliano lo stesso regime
(n trade 50%, es. MR02_ETH 911→436).
2. **Il loss-guard Hurst è ridondante-dannoso post-EXIT-16**: NESSUNO batte LIVE su
Sharpe e ritorno ovunque. Spiegazione meccanica: hurst evitava i regimi stop-heavy
saltando gli ingressi; EXIT-16 ha eliminato i wick-stop alla radice → gli ingressi
che hurst salta sono in maggioranza tornati vincenti. Il 66% delle barre è oltre
soglia hurst → il filtro toglieva metà dell'esposizione per un beneficio che ormai
non c'è.
3. **TREND-ONLY domina LIVE su tutte e 4 le metriche** ed è ESATTAMENTE la config che
la ricerca EXIT-16 del 2026-06-04 aveva promosso (entries trend-filtrate, no hurst:
FULL 7.84/2.60, OOS 10.06/1.15 — i 7.89/2.46 e 9.91/1.20 di oggi combaciano al
netto di refresh dati e cap SHAPE). Il live non aveva mai eseguito quella config.
4. **Plateau robusto**: trend_max 2.5/3.0/3.5 quasi indistinguibili (OOS DD 1.20
identico su tutte) → non è una soglia tunata.
5. Il trend filter vs NESSUNO è un trade-off deliberato: 23pp CAGR FULL per
FULL DD 3.35→2.46 e OOS Sharpe 9.72→9.91. Coerente con la filosofia del progetto
(DD control > ritorno marginale).
## Decisione (utente, 2026-06-07)
**SWAP hurst→trend nelle 6 fade live** (`_defs.py`): `trend_max=3.0, ema_long=200`,
`hurst_max` rimosso. `hurst_skip_mask` resta in `fade_base` (param opzionale).
Monitor live aggiornato: `hourly_report` ora traccia lo stop-rate per epoca
PRE (nessun filtro) → HURST → TREND, verdetto a n≥30 nell'epoca TREND.
## Lezione
Ogni meccanismo anti-perdite va RI-gateato quando cambia l'exit engine: hurst era
genuinamente il migliore sull'engine a SL intrabar (unico su ~10 candidati), ma
EXIT-16 ne ha assorbito il lavoro. I filtri si valutano sul path live corrente,
non sui numeri storici di promozione.
@@ -0,0 +1,76 @@
# 2026-06-08 — Ricerca multi-agente: dispersion / correlation index (165 agenti)
Workflow `dispersion-correlation-research` (run wf_72edef49-6d9): 60 celle esplorate
(15 famiglie × 4 finestre di correlazione W=24/72/168/336), 165 agenti totali, ~10M
token, verifica avversariale a 2 skeptic su ogni candidato robusto + sintesi.
Infrastruttura riusabile: `scripts/analysis/dispersion_lab.py` — feature REALIZED
causali (avg pairwise correlation, dispersione cross-sectional, beta vs indice EW,
componente idiosincratica `rel_A = ret_A ret_idx`), universo 8-asset, finestra
comune dal 2022-07-22, cache su disco, no-look-ahead verificato. NB: **niente
implied** (opzioni non backtestabili — stesso muro ARGO/GEX).
## Esito: 2 edge confermati su 60, 13 famiglie su 15 sono RUMORE
Nessun look-ahead "hard" stile-squeeze (le feature hanno retto la perturbazione del
futuro ovunque). Il fallimento dominante è **overfit/concentrazione di regime**, non
leakage. Tre pattern ricorrenti di falso positivo:
1. **OOS circolare**: le feature esistono solo dal 2022-07 → lo split OOS (ultimo 30%)
cade nel bull/calm 2024-25 (il regime che il progetto documenta come ottimistico
~50%); FULL≈OOS non era robustezza ma "quasi tutti i trade in OOS".
2. **Il gate corr/disp non è quasi mai load-bearing**: in ablazione, togliere il gate
spesso MIGLIORA i numeri → è un throttle di esposizione, non alpha (e Hurst-guard +
EXIT-16 fanno già quel lavoro).
3. **Riconferma del prior**: l'edge è sempre mean-reversion del residuo, mai
momentum/continuation/breakout cross-sectional.
## I 2 edge reali (entrambi fade del residuo idiosincratico)
- **`index_comp_disp` W=168 (BTC)** — il vero "dispersion-trading realizzato": fade
della componente idiosincratica estrema verso l'indice, **gated da alta dispersione**
(`disp_168 ≥ quantile 0.7`). rel_len=12, z_thr=1.5, TP=1.0·ATR, SL=1.5·ATR, mb=24.
FULL +63% / OOS +36% (fee0.2% +23%), win 66%, Sharpe 2.7-3.0, DD 7-9%, 5/5 anni.
È l'UNICA famiglia dove il gate dispersione fa davvero qualcosa.
- **`rel_idio_fade` W=24** — fade dello z-score di `rel_A` su 24h, exit a tempo (no
TP/SL). Cella più pulita BTC z3.0 mb12: win 58.5%, Sharpe 4.40, DD 7%, OOS +98%.
Più forte ma è quasi MR07 ri-etichettata in chiave cross-sectional.
Script preservati: `scripts/analysis/dispersion_edges/{index_comp_disp,rel_idio_fade}.py`.
## Raccomandazione: bassa priorità
**Entrambi sono fade-BTC mean-reversion → sovrapposti alle fade MR già deployate.**
P(migliora PORT06): ~20-25% per index_comp_disp, ~15-20% per rel_idio_fade. Vale la
**lezione FR01** (robusto-individualmente ≠ migliora-il-portafoglio: FR01 era robusto
e causale ma diluiva PORT06 OOS Sharpe 8.89→8.72). index_comp_disp ha il profilo trade
più distinto (win 66%, hold corto, TP vicino) → è l'unico che merita eventualmente un
gate `combine_v2`/PORT06 formale, misurando corr col MASTER e ΔSharpe/ΔDD. Se non
decorrela, scartare.
**13 famiglie rumore** (record in `_disp_scratch/`, gitignored): corr_gate_fade/pairs,
disp_xs_reversal/momentum, corr_spike_btc, corr_meanrev, beta_disp, disp_regime_rot,
corr_vol_interact, leadlag_corr, corr_trend, disp_compression_breakout, corr_disp_combo
— tutte MR/fade ri-etichettate, concentrazione 2024-25, o artefatti di feed morto
(ADA/LTC 97% candele flat). disp_compression_breakout riconferma la lezione squeeze
(i breakout rientrano).
**Conclusione onesta: nessun nuovo motore di ritorno.** Il dispersion-trading realizzato
funziona solo come l'ennesima faccia della mean-reversion già sfruttata.
## Gate PORT06 del candidato n.1 (2026-06-08) — PROMOSSO MARGINALE, NON deployato
`scripts/analysis/dispersion_edges/gate_index_comp_disp.py` (config W=168: rel_len=12,
z_win=336, z_thr=1.5, disp_q=0.7, TP=1.0ATR, SL=1.5ATR, mb=24; equity daily innestata
come 18° sleeve, pesi cap):
- **Sorpresa positiva**: decorrela DAVVERO. corr daily col candidato: MR01_BTC +0.01,
MR02_BTC +0.05, MR07_BTC +0.06, DIP01_BTC +0.02, MASTER(EW) +0.06. Il timore di
ridondanza con le fade BTC era infondato (gate dispersione + TP vicino = profilo trade
distinto). Standalone 311 trade, FULL +67% / OOS +30%.
- **Ma beneficio nel rumore**: PORT06 BASE→+DISP: FULL Sharpe 6.43→6.47, FULL DD
3.96→3.73 (migliora), **OOS Sharpe 8.58→8.56 (0.01), OOS DD 1.36→1.40 (+0.04)**
l'OOS è PIATTO. Passa il gate tecnico ma il guadagno è solo nel FULL (regime storico).
- **Decisione (utente): documenta e rimanda.** NON deployato — wiring (nuova Strategy+worker,
famiglia DISP, peso cap) + resterebbe simulato (no executor), per un beneficio OOS nullo.
Gate script committato e pronto: riprendere SOLO se si costruisce una famiglia DISP più
ampia (più asset/sleeve) che insieme sposti l'OOS. Esito ~20% previsto, confermato.
@@ -0,0 +1,130 @@
# 2026-06-08 — Ricerca sostituto/miglioria a MR02/ETH (127 strategie + opzioni)
## Contesto
MR02/ETH è lo sleeve a DD più alto della famiglia fade ed è il maggior drag
dell'esecuzione reale (testnet: 19€ su 7 trade, 0 win, faded un downtrend 14%
in long poi shortato il rimbalzo — whipsaw classico). Obiettivo: trovare un
sostituto o una miglioria con ≥100 agenti su strategie differenti, metodologia
onesta (ingresso eseguibile, fee 0.10% RT, OOS, gate PORT06).
## Cosa è stato fatto
- **Workflow 1 — 109 strategie ETH distinte** (`mr02eth_search.workflow.js`):
gated-fade, mean-reversion alternative, exit, trend-following, volatilità,
cross-asset, ML/shape, microstruttura, statistico-robusto. Harness onesto
`explore_lab`. Poi **verifica avversariale** dei top-24 (audit look-ahead +
re-implementazione indipendente + robustezza). → 26 survivor, **6 confermati**.
- **Workflow 2 — 18 overlay di OPZIONI** (`mr02eth_options.workflow.js`) +
sintesi con audit crash reali. Harness nuovo `option_overlay_lab.py`: opzioni
prezzate Black-Scholes sulla **DVOL** reale di Deribit (storica/gratuita,
`data/regime/eth_dvol.parquet`), premio dedotto, ipotesi **conservative**
(skew_mult sui comprati, uscita anticipata solo a intrinseco).
- **Gate PORT06** (`mr02eth_port06_gate.py`): swap di MR02_ETH col candidato,
equity giornaliera su IDX 2021-2026, ri-misura FULL+OOS Sharpe/DD (cap
weighting). Sanity check: MR02 ricostruito col mio engine == canonico (Δ 0.00).
## Risultato (gate PORT06; baseline canonico FULL 6.43/3.96, OOS 8.58/1.36)
| variante (swap MR02/ETH) | FULL Sh | FULL DD | OOS Sh | OOS DD |
|---|---|---|---|---|
| EXIT-16 (config LIVE) | 6.73 | 3.67 | 8.80 | 1.23 |
| no-SL nudo | 6.76 | 3.68 | 8.87 | 1.23 |
| no-SL + put/call 10% OTM | 6.78 | 3.64 | 8.86 | 1.23 |
| varratio_gate (PROMOSSO vs canonico) | 6.41 | 3.73 | 8.53 | 1.36 |
| choppiness_donch | 6.37 | 3.35 | 8.47 | 1.32 |
| vrp_neg_dvol_low | 6.36 | 3.22 | 8.37 | 1.43 |
## MIGLIORIA TROVATA (gate vs LIVE EXIT-16, non vs canonico)
Riferimento onesto = config LIVE EXIT-16 (FULL 6.73/3.67 — OOS 8.80/1.23), non
l'intrabar-SL canonico che nessuno gira.
| sleeve ETH | ΔFULL Sh | ΔFULL DD | ΔOOS Sh | ΔOOS DD | esito |
|---|---|---|---|---|---|
| **no-SL + put/call 10% OTM** | **+0.05** | **0.03** | **+0.06** | 0.00 | ✅ MIGLIORIA deployabile |
| no-SL nudo | +0.04 | +0.02 | +0.07 | 0.00 | ✅ migliora ma sl=0 NON deployabile |
| EXIT16 + put10 | 0.01 | +0.03 | 0.00 | 0.00 | = pari (assicurazione gratis) |
| varratio / vrp / chop / blend | negativi | | | | peggiorano vs EXIT-16 |
**La miglioria = MR02/ETH no-SL + floor a opzioni 10% OTM** (put sui long, call
sugli short). Batte il live EXIT-16 in modo PARETO su 3/4 metriche PORT06 (4ª
piatta). Meccanismo: **rimuovere lo stop** (intrabar/close-confirm su una fade
mean-reverting sono falsi negativi — overshoot che rientra) e dare la funzione
dello stop (cap della catastrofe) a un'**opzione comprata** = protezione a
rischio-definito SENZA il costo whipsaw/falso-stop. Standalone il salto è grande
(Sharpe 8.45→12.14, OOS DD 13%→7%); a portafoglio è modesto (+0.05/+0.06 Sharpe)
perché è 1 sleeve su 17, ma è un miglioramento MISURATO e verificato (soglia
gate +0.03, ottenuto +0.05/+0.06), non rumore.
## Conclusioni (oneste)
1. **Sostituti/blend: tutti peggiorano vs il live EXIT-16.** I candidati gated
(varratio/vrp/choppiness) e i blend within-sleeve battono solo lo strawman
canonico, non il live; le loro basse esposizioni diluiscono il motore di
ritorno (Sharpe 0.08/0.44). Lo Sharpe ~20 nell'harness era artefatto di
`sqrt(N_trade)` (onesto ~4.3). → **non sostituire né blendare.**
2. **L'unica miglioria è la coppia no-SL + opzione.** Il no-SL recupera il drag
dello stop; l'opzione cappa la coda da **gap-through** che EXIT-16 NON protegge
(esce al close DOPO il buco → prende la perdita piena). Audit crash: FTX nov-22
48%→−30%, LUNA-era 2022-09 63%→−31%, 2018/19 65%→−30%. Premio ~break-even
(paga 156% / incassa 149%). È ciò che rende deployabile il no-SL.
## Azione raccomandata
- **Deployare MR02/ETH come no-SL + floor a opzioni 10% OTM** (put long / call
short, per-trade o settimanale ammortizzato). Batte il live su 3/4 metriche +
cappa esplicitamente la coda. **Prima del capitale**: validare prezzi/liquidità/
skew per-strike REALI su Deribit testnet (qui pricing BS sintetico sulla DVOL =
ATM-IV; stress skew ×1.5 gonfia il premio FULL ma l'OOS regge). Stesso muro
non-backtestabile di ARGO/GEX sul per-strike, aggirato col proxy DVOL.
- Step intermedio a costo zero: **aggiungere subito il floor opzione all'EXIT-16
live** (= "pari" sui numeri, ma assicurazione gratis sul gap-through) mentre si
valida la transizione a no-SL.
## Addendum 2026-06-09 — validazione su PREZZI REALI + integrazione cerbero-bite
- Probe live Deribit mainnet (singolo snapshot): sembrava skew ×1.6 e liquidità
pessima sul 10% OTM a 1g → l'overlay per-trade pareva bocciato.
- **cerbero-bite** (container accanto) ha però **lo storico per-strike reale** in
`option_chain_snapshots` (SQLite, dal 2026-05-01, ~110k righe/asset, bid/ask/IV/
greche/OI, cadenza ~12min). Importato in `data/options/` via `options_fetcher.py`;
loader `options_chain.py` (skew_curve/premium_levels/quote causale).
- **Correzione dai dati reali (5.5 settimane):** skew put 10% OTM = **×1.1** (liquido,
spread ~7%) — il ×1.6 del probe era un artefatto del deep-OTM a 1g illiquido. Premio
reale 10% OTM ≈ **1.0%/mese**. → la miglioria no-SL+opzione **regge** sui prezzi veri.
- **MA vincolo strutturale reale:** gli strike **10% OTM a 24h NON esistono** (n=0 in 5.5
settimane; Deribit lista solo near-ATM sui tenor corti). L'overlay **per-trade a 24h è
infattibile**; l'unica struttura eseguibile è una **put settimanale/mensile standing**
(catastrofe-cap di sleeve), da gateare a parte coi premi reali.
- **Infrastruttura sbloccata:** il muro ARGO/GEX ("opzioni non-backtestabili") è caduto
per il periodo recente — d'ora in poi le tesi opzioni si validano su prezzi VERI.
Importato anche il pannello `market_snapshots` (spot/VRP/funding/**net-GEX dealer**/
gamma-flip/liquidation, dal 2026-03-26) → `data/options/`, loader `options_chain.load_market`.
## Addendum 2 — gate del COLLAR standing (catastrophe-cap auto-finanziato) — NO-GO
Premi reali → collar = put 13% (1.0%/m) finanziata da call +10% (1.05%/m) ≈ premio netto 0.
Gate PORT06 (`eth_collar_gate.py`, BS calibrato skew_put 1.12 / skew_call 1.0, sweep OTM/hedge_frac):
- Il collar **PAGA nei crash** (ETH 40% dic-21 → +6.1%, 30% feb-25 → +6.6%): protezione reale.
- MA **net-negativo a portafoglio**: taglia il FULL DD (0.2/0.3pp) ma **costa Sharpe** (FULL e OOS)
e **ALZA l'OOS DD** (+0.18/+0.28pp) — nell'OOS (no crash profondo, qualche rally) la call corta
perde e il premio erode. Peggiora il DD che doveva ridurre.
- **Verdetto: il tail-hedge a opzioni NON conviene a PORT06** (né per-trade né collar). Strutturale:
lo sleeve ETH pesa 6.5% → la **diversificazione è già la protezione di coda** (trade 65% =
4% aggregato). La cap auto-finanziata era giusta sul *costo* (~break-even) ma la protezione non
vale il costo Sharpe/OOS-DD. Le opzioni si chiudono come **NO-GO empirico su prezzi reali**.
## Deploy della miglioria (2026-06-09): MR02/ETH stop-largo sl_atr 3.0
Nessun SOSTITUTO batte il live; la MIGLIORIA deployabile e' allargare lo stop EXIT-16.
Gate stop-width (EXIT-16 close-confirm, swap solo MR02_ETH):
| sl_atr | FULL Sh | FULL DD | OOS Sh | OOS DD | worst-trade |
|---|---|---|---|---|---|
| 2.0 (LIVE) | 6.73 | 3.67 | 8.80 | 1.23 | -48% |
| **3.0** | **6.77** | **3.30** | **8.82** | 1.23 | **-50%** |
| 4.0 | 6.78 | 3.49 | 8.82 | 1.23 | -62% |
| no-SL (sl=0) | 6.76 | 3.68 | 8.87 | 1.23 | -65% (vietato) |
`sl_atr=3.0` domina il live (FULL DD -0.37pp, Sh +0.04/+0.02) e cattura il beneficio del no-SL
(gli stop di meta'-discesa sono falsi negativi) MANTENENDO lo stop di catastrofe (worst -50%, non
-65%) -> regola "mai sl=0" rispettata; 4.0 inizia a perdere la coda. Plateau 2.5-4.0 (tutti > live
su Sharpe). Override SCOPED a MR02/ETH in `_defs.py` (`_fade_params`); gli altri 5 fade non
ri-gateati -> invariati. 93/93 test passano. Deploy: `./scripts/deploy.sh` (cambio live).
## Artefatti riusabili
- `scripts/analysis/option_overlay_lab.py` — overlay opzioni (pricing BS sintetico su DVOL).
- `scripts/analysis/options_fetcher.py` — import storico opzioni reale da cerbero-bite → data/options/.
- `scripts/analysis/options_chain.py` — loader + skew/premi reali + lookup causale (`OptionChain`).
- `scripts/analysis/mr02eth_port06_gate.py` — gate swap-sleeve PORT06.
- `scripts/analysis/mr02eth_search.workflow.js`, `mr02eth_options.workflow.js`.
@@ -0,0 +1,53 @@
# 2026-06-08 — Executor a 2 gambe per i pairs (PairsExecutionClient)
Implementato il pezzo di esecuzione reale più impegnativo rimasto: l'esecuzione
shadow a 2 gambe per la famiglia PAIRS (29% del portafoglio, finora solo simulata).
## Cosa
- **`execution.PairsExecutionClient`**: compone l'`ExecutionClient` single-leg.
- `open_pair(inst_a, inst_b, direction, notional)`: piazza le 2 gambe (long A/short B
o viceversa) a market, verifica per-gamba sul trade; **LEG-RISK**: se UNA sola gamba
filla → UNWIND (richiude la fillata reduce-only) per non restare direzionali →
verified=False.
- `close_pair(...)`: chiude entrambe reduce-only (solo `close_amount` della quota,
MAI `close_position`), riconcilia fee/prezzi.
- `register_contract`: fetch dinamico spec USDC da Deribit per strumenti non hardcodati.
- Strumenti = lineari USDC (payoff lineare = matematica del backtest a 2 gambe).
Aggiunti spec LTC/ADA/SOL_USDC (step 0.1/0.2/0.1).
- **`PairsWorker` shadow**: ledger reale parallelo (`real_capital`, `real_dir`,
`real_amount_a/b`, `real_entry_a/b`, `real_notional_a/b`, fee), persistito e resume-safe;
`_real_open_pair`/`_real_close_pair` agganciati a `_open`/`_close`; PnL reale per gamba
(dir A=+d, dir B=d). Doppio arrotondamento → piccolo sbilanciamento notional, riportato.
- **runner**: `pairs_executor` (PairsExecutionClient su stesso ExecutionClient dei fade),
`_pairs_exec_for`, gate su `execution.pairs_enabled`. Config `portfolios.yml`:
instruments estesi (LTC/ADA/SOL) + `pairs_enabled: false` (capability pronta, SPENTA).
## Validazione
- Test (executor finto): 92/92 — open/close a 2 gambe, **leg-risk unwind**, ledger reale
persiste e resume.
- **Smoke testnet end-to-end** (`live_pairs_smoke.py`, €0): open 2 gambe verificate
(long ETH 0.011 @1666 / short BTC 0.0003 @63263, fee $0.019), close 2 gambe reduce-only,
riconciliazione PnL reale 0.039 vs sim 0.036 (coerente), **conto flat dopo**.
## Incidente operativo (testnet, €0) — da ricordare
Durante la prima esecuzione, lo smoke è crashato (bug del test: prezzi sim=0 → divisione
per zero nel ramo sim) lasciando una posizione aperta. Per pulirla ho usato a mano
`ExecutionClient.close()` (= `close_position`) che **flatta l'intero strumento** → ho
chiuso anche le 3 posizioni reali SHADOW dei fade del runner (BTC/ETH_USDC condivisi).
Impatto: testnet, shadow (non guida le decisioni), €0; 3 worker fade con
`real_in_position=True` su conto flat → si auto-riconciliano al prossimo close sim
(close_amount su flat → verified=False, reset; "shadow pulito dalla prossima apertura").
I TP resting erano già stati cancellati da close_position (cancel → order_not_found).
**Lezione (CLAUDE.md la documentava già)**: MAI `close_position` su strumenti condivisi.
Fix permanente: lo smoke ora (a) usa solo `close_amount` della quota, (b) ABORTA se ci
sono posizioni di produzione aperte sugli strumenti, (c) usa prezzi sim reali.
## Stato: PRONTO ma SPENTO (`pairs_enabled: false`)
L'executor è validato (test + smoke). NON attivato in produzione: accenderlo richiede
una finestra a conto flat e un periodo di osservazione, dato il conto condiviso coi fade.
Quando si accende: i 5 pairs eseguiranno reale a 2 gambe accanto al sim.
@@ -0,0 +1,42 @@
# 2026-06-08 — Portafoglio live REALE-only: i €2000 ai soli sleeve eseguiti
Decisione utente: il portafoglio live deve mostrare il **risultato reale puro**. I €2000
si dividono SOLO tra i 14 sleeve che eseguono davvero (6 fade + DIP01 + 5 pairs + SH01);
i 3 multi-asset (TR01/ROT02/TSM01), non eseguibili in reale (bloccati dal capitale),
escono dal pool e girano **solo per statistica**.
## Implementazione (runner-level, non rompe le definizioni)
- `portfolios.yml`: `paper_sleeves: [TR01, ROT02, TSM01]`.
- `runner`: separa `live_specs` (pool/pesi/ledger) da `paper_specs`. I paper:
- capitale NOZIONALE fisso = `total_capital / N_sleeve_totali` (la fetta equal che
avrebbero avuto), NON dal pool;
- girano in `data/portfolio_paper_stats/` (binario separato);
- ticcati per statistica ma MAI in `ledger.update_equity` → non toccano l'equity reale.
- `hourly_report`: la sezione multi-asset ora legge `portfolio_paper_stats/` ed è
etichettata "PAPER — solo statistica, FUORI dal conto reale".
## Pesi del portafoglio reale-only (14 sleeve, cap)
sum=1.0; non-cappati 0.0873 (erano 0.0647 su 17), PAIRS 0.33 (cap), SHAPE 0.0588 (cap,
SH ciascuno 0.0294). Il cap SHAPE resta valido e protettivo anche su 14.
## Il costo, misurato e accettato
| | FULL Sh | FULL DD | OOS Sh | OOS DD |
|---|---|---|---|---|
| PORT06 completo (17, validato) | 6.43 | 3.96% | 8.58 | 1.36% |
| **REALE-only (14, live ora)** | 6.49 | **5.34%** | 8.54 | **1.70%** |
Perdendo i 3 diversificatori PAPER (corr 0.07), il DD del portafoglio reale sale
~3.96→5.34% FULL e 1.36→1.70% OOS, Sharpe sostanzialmente invariato. È il **prezzo di
vedere il risultato reale puro** invece di una curva mista reale+paper: scelta
consapevole dell'utente. I PAPER restano misurati per ri-integrarli quando saranno
eseguibili (SH01 è già stato integrato oggi; i multi-asset attendono capitale ~€20k).
## Note di transizione
- Il ledger `PORT06` (code invariato) ora rappresenta i 14 reali: la curva equity ha
una discontinuità di composizione da questo deploy (atteso).
- I 3 paper ripartono puliti in `portfolio_paper_stats/` con capitale nozionale uniforme
(statistica comparabile); i vecchi status in `portfolio_paper/` restano come storico.
@@ -0,0 +1,56 @@
# 2026-06-09 — Gioco "Blind Traders": 100 agenti ciechi
## Setup
100 agenti LLM (haiku) ricevono due serie anonime **X** e **Y** — in realta'
**BTC** e **ETH** 1h/15m/5m, mai etichettate — e devono proporre UNA regola che
"anticipi" i movimenti per un PnL netto positivo (fee 0.10% RT) con **>=10
trade/mese**. Non sanno cosa siano i dati. L'orchestratore (engine deterministico)
valuta ogni strategia, assegna un punteggio su **PNL + %win**, da' **90 epoche di
elaborazione** (hill-climb dei parametri) e **ogni 10 epoche blocca il 10% meno
profittevole** -> restano i **10 piu' profittevoli**.
Infrastruttura in `scripts/games/`:
- `engine.py` — dati anonimizzati, 6 famiglie segnale (zscore/breakout/ma_cross/
rsi/momentum/pairs), backtester causale fee-aware, scoring (>=10 tpm o squalifica).
- `agent_brief.py` — digest ANONIMO (stat aggregate + finestra normalizzata) + menu.
- `arena.py` — torneo a **3 finestre**: TRAIN (hill-climb), VALID (cull+rank
dell'orchestratore), TEST (OOS puro, mai ottimizzato). Anti-overfit.
- `run_game.py` — carica le 100 spec degli agenti e lancia il torneo.
## Risultato emergente
I 100 agenti ciechi, leggendo SOLO le statistiche anonime (autocorrelazione
negativa, "after_big_move_continues_pct" ~30-40% => le mosse estreme rientrano),
hanno **riscoperto da soli che il mercato e' mean-reverting**: 100/100 reversion,
67 hanno scelto il detector pairs, 30 zscore. Esattamente la lezione storica del
progetto (edge = reversione; pairs ETH/BTC il piu' robusto) — senza sapere che
fosse crypto.
## Classifica finale (top 10) — tutti PAIRS su 15m
Vincitore **agente #91** (15m, pairs market-neutral sul log-ratio X/Y):
- TEST/OOS puro: **PnL +3126%**, **win 77%**, **108.9 trade/mese**, **Sharpe 20.3**
- Full-period: PnL +8052%, win 70%, 94 tpm, Sharpe 12.2 (9604 trade)
- params: lookback 66, entry 1.67σ, exit 1.0σ, max_bars 35
- ipotesi (cieca): "Y altamente reversivo, X/Y log-ratio strong mean-reversion
(-0.43 autocorr), bassa correlazione cross-asset -> pairs market-neutral".
Tutti i 10 finalisti: pairs 15m, TEST Sharpe medio 19.9, tpm 66-109 (>>10).
## Caveat onesti
- Numeri OOS ottimistici: PnL additivo a notional fisso, **niente slippage sulle 2
gambe**, finestra OOS calma, 15m molti trade. Coerente col caveat PR01 del
progetto (Sharpe reale atteso ~4-5, non 20). Il valore del gioco e' il **metodo**
(scoperta cieca + selezione anti-overfit), non il livello assoluto di Sharpe.
- La convergenza su pairs conferma robustezza ma riduce la diversita': i 10 finalisti
sono varianti della stessa idea (ETH/BTC spread). Per un portafoglio servirebbe
diversificare (gia' fatto altrove: fade + honest + shape).
## Re-run "sobrio" con slippage (0.05%/lato)
`GAME_SLIP=0.0005` -> i pairs pagano +0.20% RT extra (4 lati). Lo slippage spinge
l'ottimizzatore verso **meno churn**: tpm dei finalisti 66-109 -> **40-47**, Sharpe
top-10 ~20 -> ~13.5. Vincitore **#43** (15m pairs): TEST PnL **+2091%**, win 77%,
**46.9 tpm**, Sharpe **15.6**. La gerarchia (pairs 15m domina) e la robustezza
reggono lo stress; lo Sharpe reale atteso resta ~4-5 (OOS calmo + PnL additivo).
Log: `data/games/game_slip.log`.
Artefatti: `data/games/tournament_result.json`, `data/games/specs/agent_*.json`,
`engine.set_slippage()` (env `GAME_SLIP`).
@@ -0,0 +1,41 @@
# 2026-06-09 — Gioco "Blind Traders" sessione 2: timing diversi (30m/2h/4h)
Seconda sessione del gioco (vedi `2026-06-09-blind-traders-game.md`), stesso protocollo
(100 agenti ciechi su BTC/ETH anonimi, scoring PNL+%win, ≥10 trade/mese, 90 epoche, cull
10% ogni 10 epoche → 10 finalisti, split a 3 anti-overfit) ma su **timeframe diversi**:
game 1 = 5m/15m/1h; **game 2 = 30m/2h/4h** (medio-lunghi). Engine con resampling aggiunto
(`engine._RESAMPLE`: 30m←15m, 2h/4h←1h). Specs in `data/games/specs2/`, risultato
`data/games/tournament_result2.json`.
## Diversita' proposte (di nuovo: riscoperta cieca della mean-reversion)
100 agenti: **74 pairs, 25 zscore, 1 breakout; 100% reversion**; tf 34/33/33. Come nel
game 1, leggendo solo le statistiche anonime (autocorrelazione negativa del log-ratio,
continuazione post-mossa ~40%) gli agenti convergono sulla reversione senza sapere che
sia crypto.
## Classifica finale — tutti 30m pairs
Vincitore **agente #36** (30m, pairs ETH/BTC):
- TEST/OOS: **PnL +1451%, win 77%, 43.4 trade/mese, Sharpe 12.3**.
- I 10 finalisti sono TUTTI 30m pairs (TEST Sharpe ~12, win 76-77%, tpm 43-49).
## Finding chiave: la regola ≥10 trade/mese e' un FILTRO sul timeframe
Quanti agenti per tf superano la soglia di attivita' + qualita':
| tf | agenti | ≥10 trade/mese | positivi OOS | miglior OOS Sharpe (pnl/win/tpm) |
|---|---|---|---|---|
| 30m | 34 | **34 (100%)** | 26 | **11.6** (1405% 76% 56) |
| 2h | 33 | 29 (88%) | 17 | 6.1 (512% 79% 17) |
| 4h | 33 | **4 (12%)** | 6 | 1.4 (103% 68% 14) |
A 4h solo 4/33 agenti riescono a fare ≥10 trade/mese (le barre sono troppo rade per
la reversione pairs); e l'edge cala col timeframe (Sharpe 11.6→6.1→1.4). Per questo i
finalisti sono tutti 30m.
## Lezione cross-game (game 1 + game 2)
Esiste una **frontiera frequenza-vs-edge**: la regola ≥10 trade/mese mette un *pavimento*
sul timeframe (i lunghi non fanno abbastanza trade), il costo/edge mette un *soffitto* (i
cortissimi sono cost-fragili). Il punto ottimo e' **il timeframe piu' corto con edge ancora
robusto**: game 1 (con 15m disponibile) → vince 15m; game 2 (senza 15m) → vince 30m. Sempre
**ETH/BTC spread reversion**. Coerente con l'analisi di robustezza del 15m
(`2026-06-09-pairs15m-robustezza.md`): piu' corto = piu' trade = piu' edge di backtest, ma
piu' fragile ai costi. Il gioco trova l'edge; la prudenza di deploy (mezza size) gestisce
la fragilita'. Artefatti: `scripts/games/`, `data/games/tournament_result2.json`.
@@ -0,0 +1,47 @@
# 2026-06-09 — Validazione edge credit-spread cerbero-bite (prezzi reali)
## Contesto
cerbero-bite (container accanto, `/opt/docker/cerbero-bite`) vende credit-spread su
ETH (bull-put primario, short delta ~0.18, DTE 18, Quarter-Kelly 13%, PT 50% / stop
2.5x credito / delta-breach 0.30 / vol-stop +10 DVOL / time-stop 7 DTE; testnet,
propose-only). Tune "Profilo B" del 2026-06-09: short delta alzato a 0.18 (da 0.10-0.15)
e `credit_to_width_ratio_min` 0.30->0.08 perche' a delta basso 0 spread erano eleggibili.
Domanda: l'edge regge su un ciclo ETH completo, o e' profittevole solo nei campioni calmi?
Validato con l'infrastruttura opzioni REALE (data/options/, importata da cerbero-bite).
Script riprendibile: `scripts/analysis/cerbero_bite_credit_spread.py`.
## Risultati
1. **Economia d'ingresso reale** (chain, 3145 spread): cw a delta 0.18 = **0.106** (p25 0.085),
eleggibilita' **65%**, short strike **~9.4% OTM** (NON 18% — quello era il vecchio delta basso),
**max-loss/credito = 8.4x**.
2. **Tail model-free** (8 anni ETH reali, cw 0.106, hold-to-expiry, niente modello opzioni):
win-rate 74%, **EV -1.0 crediti/trade, 7/9 anni NEGATIVI**, ETH <-13.4% a 17g (max-loss) il
**17.8%** delle volte. Un max-loss (-8.4cr) cancella 17 PT-winner. Gap 1g p5 = -25% (salta lo stop).
3. **Managed (skew calibrato sulle IV reali)**: win-rate **37%** (delta-breach esce sul 62% dei
trade a piccola perdita), **EV -0.02 cr/trade** (gia' a economia favorevole), worst -4.6 (2023).
2021+ EV -0.022.
## Caveat di calibrazione (TODO aperto)
Il mark mid+skew da **cw 0.228 vs 0.106 reale** -> sovrastima il credito ~2x (manca bid/ask
incrociato sulle 2 gambe + griglia strike reale). Quindi l'EV managed mostrato (-0.02) e' a
economia **2x troppo favorevole**: l'EV vero e' **<=**. Per il numero esatto: modellare bid/ask
reale + griglia (entrambi nella chain) cosi' entry cw -> 0.106. RIPRENDERE da qui.
## Verdetto
- **NON edge robusto su ciclo completo.** Il "+0.48%/mese netto" citato era **artefatto di
finestra calma** (mag-giu 2026, nessun crash): li' il body vince (EV +0.6), ma su ciclo
completo (mesi -13% al 18% di frequenza) e' breakeven-to-negativo in 2 stime indipendenti.
- **L'82% PoP e' ingannevole**: o esci presto sul delta-breach (win 37%, grind di piccole
perdite), o tieni e prendi la coda. Non c'e' la "macchina da 82% di vittorie".
- **Il tune Profilo B PEGGIORA la coda** (vendere a 9.4% OTM mette il max-loss in zona di
pullback ordinario). Strutturale: il mercato non paga per vendere lontano, vendere vicino
espone alla coda.
- **Coda CONCENTRATA con PythagorasGoal**: il gap che salta lo stop = il crash ETH, lo stesso
evento che colpisce lo sleeve fade. I due sistemi non diversificano, concentrano.
- **Azione**: cerbero-bite resta testnet/paper finche' non c'e' un campione che include un crash
vero; valutare un long deep-OTM put (0.33%/mese reale) come cap della coda del bull-put-spread.
## Stato
DA RIPRENDERE: calibrazione esatta credito (bid/ask + griglia) -> EV managed definitivo.
Tutto il resto e' chiuso e documentato.
@@ -0,0 +1,82 @@
# 2026-06-09 — Percorso live 15m per ETH/BTC pairs: COSTRUITO e VALIDATO
Seguito di `2026-06-09-pairs15m-port06-gate.md` (il gate passa, edge reale e non
artefatto flat). Qui si costruisce e VALIDA l'infrastruttura per eseguire il pairs
ETH/BTC a 15m con flat-skip, alla pari del backtest (disciplina validate_worker_pairs).
## 1. Engine canonico (regression-locked)
`scripts/analysis/pairs_research.py`: aggiunti `aligned_ohlc`, `is_flat_ohlc`,
`pairs_sim_flat(..., flat_skip, scan_buffer)`. Regola di uscita **LIVE-REALIZABLE**:
la condizione (|z|<=z_exit O bars>=max_bars) ARMA `exit_ready`; si esce al CLOSE della
PRIMA barra PULITA successiva (mai a un prezzo passato come faceva il prototipo push-back).
- **Regression-lock**: `pairs_sim_flat(flat_skip=False)` == `pairs_sim` ESATTO
(ETH/BTC 1h 1756 trade, 15m 9388 trade, ret/dd/sharpe identici al bit).
## 2. PairsWorker esteso (retrocompatibile)
`src/live/pairs_worker.py`: param `flat_skip`, stato `exit_ready` (persistito), tick
ora fa merge OHLC e rileva le candele flat (O=H=L=C in UNA gamba). Entry saltato su barra
stale; uscita con la stessa regola exit_ready dell'engine. **Default off = comportamento
1h storico invariato** (se mancano le colonne OHLC, flat=False).
## 3. Runner: fetch sub-orario (inerte finche' non c'e' uno sleeve 15m)
`src/portfolio/runner.py`: `_SUBHOURLY={5m,15m,30m}`, `_LOOKBACK_DAYS` esteso; il loop
fetcha DIRETTO da Cerbero i timeframe sub-orari per (asset,tf) (non resamplabili dal 1h) e
un router `_series_for` instrada la serie giusta a ogni worker. Zero impatto sul live
attuale: nessuno sleeve e' 15m → `subhourly_needs` vuoto → ramo morto.
## 4. VALIDAZIONE (validate_worker_pairs.py) — TUTTO OK
Replay bar-per-bar del worker == backtest:
| caso | worker | backtest | match |
|---|---|---|---|
| ETH/BTC 1h | 1756 trd, cap 2.886.616 | 1756, 2.886.616 | **OK esatto** |
| BTC/LTC 1h | 599 trd, cap 16.861 | 599, 16.861 | **OK esatto** |
| **ETH/BTC 15m-flat** | **8452 trd** | **8453 trd** (cap entro 0.15%) | **OK** |
(1 trade di differenza = posizione finale aperta non chiusa nel replay, atteso.)
## 5. Gate finale (engine == worker) — PROMOSSO
`pairs15m_gate_final.py` (corr 1h vs 15m = 0.372, 3201 ingressi flat saltati):
| variante ETH/BTC | FULL Sh | FULL DD | OOS Sh | OOS DD |
|---|---|---|---|---|
| baseline 1h | 6.43 | 3.96 | 8.58 | 1.36 |
| **SWAP 15m-flat** | 7.31 | 3.55 | **9.95** | **1.26** |
| **BLEND 1h+15m** | 7.03 | 3.66 | 9.57 | 1.24 |
Entrambi PROMOSSI (a fee backtest). Caveat slippage del gate precedente invariato → il
BLEND e' la forma raccomandata (meta' allocazione sul 1h pulito, slippage-robusto).
## Stato e attivazione (NON fatta — decisione di deploy)
Tutto il PERCORSO e' pronto e validato, ma il 15m **non e' attivo nel portafoglio live**:
attivarlo cambia il trading reale e va deciso esplicitamente. Per accenderlo:
1. `_defs.py`: aggiungere SleeveSpec pairs ETH/BTC a 15m (tf="15m",
params={n:66,z_in:1.674,z_exit:1.0,max_bars:35,flat_skip:True}) — come SWAP della 1h o
come 2a sleeve (BLEND) sotto il cap PAIRS.
2. `report_families.build_everything` / `sleeves`: l'equity del nuovo sleeve dal
`pairs_sim_flat(tf=15m, flat_skip=True)` (per parita' backtest==report).
3. Shadow smoke su testnet (come `live_smoke_pairs.py`) prima del paper reale.
4. `deploy.sh` (bump+rebuild) — il runner gia' fetcha 15m e passa flat_skip via spec.params.
Test suite: nessuna regressione (1h byte-exact). Artefatti: pairs_research.py,
pairs_worker.py, runner.py, validate_worker_pairs.py, pairs15m_gate_final.py.
## ATTIVAZIONE IN REALE (2026-06-09) — BLEND, mezza size
Deciso: BLEND (sleeve 15m ACCANTO al 1h, non swap). Implementato:
- `_defs.py`: SleeveSpec `PR_ETHBTC_15M` (tf=15m, flat_skip, params.position_size=0.10
= meta' del family PAIRS 0.20) in PAIRS -> entra in PORT04/05/06.
- `report_families.build_everything`: equity da `pairs_sim_flat(tf=15m, flat_skip=True, pos=0.075)`
(mezza size, == intento live) con sid PR_ETHBTC_15M.
- `runner.pos_for_spec`: override PER-SLEEVE (params.position_size) > famiglia > globale.
- **Mezza size perche'** a peso pieno il 15m pesava il 25.8% del rischio PORT06 (vs 9.5% del
1h): dimezzato -> 11.5% vs 10.6%, bilanciato. Disciplina come la cap SHAPE; rispetta il
caveat slippage (il 15m non domina il book).
**PORT06 col BLEND (mezza size)**: FULL Sharpe **6.43->7.20** DD **3.96->3.68**,
OOS Sharpe **8.58->9.66** DD **1.36->1.31**. Migliora tutto.
**Smoke live 15m** (`pairs15m_live_smoke.py`): Cerbero serve candele 15m FRESCHE per
ETH e BTC (ultima barra 0 min fa, flat live 2-3%), worker flat-skip ticca OK. Esecuzione
reale a 2 gambe gia' coperta da `live_pairs_smoke.py` (livello strumento, tf-indipendente).
**Regression-lock aggiornati** (miglioria attesa, non regressione): test_definitions
(17->18 sleeve), test_backtest_parity_cap (FULL 6.47->7.20, OOS 8.82->9.66). Suite verde.
Live: il runner fetcha 15m diretto, costruisce il PairsWorker(flat_skip) col pos 0.10,
e lo esegue reale a 2 gambe (pairs_enabled). Attivazione via deploy (bump+rebuild).
@@ -0,0 +1,89 @@
# 2026-06-09 — ETH/BTC pairs a 15m: gate PORT06 (dal gioco Blind Traders)
## Origine
Il gioco "Blind Traders" (100 agenti ciechi) ha eletto come vincitore una variante
ETH/BTC pairs su **15m** (config #43: n=66 z_in=1.67 z_exit=1.0 max_bars=35). Domanda:
e' un vero miglioramento o un duplicato piu' veloce della sleeve PR01 ETH/BTC gia'
deployata a 1h? Testato sul serio con l'engine di PRODUZIONE `pairs_sim` + gate PORT06.
Script: `scripts/analysis/pairs15m_port06_gate.py`.
## Risultati
- **Parita' OK** (corr 1.00000): l'harness riproduce esattamente il sleeve canonico
PR_ETHBTC → gate affidabile.
- **CORRELAZIONE 1h vs 15m = 0.349** (rendimenti giornalieri). **SMENTISCE la mia
ipotesi iniziale "duplicato ridondante"**: a 15m cattura eventi di reversione DIVERSI
→ e' un diversificatore reale, non una doppia scommessa sullo stesso spread.
- **Robustezza 15m**: griglia n×z_in → **16/16 celle Sharpe>1** (9-12), plateau non picco.
Non e' un punto overfit del gioco.
- **Standalone**: 15m fa 9388 trade (vs 1756 a 1h), Sharpe 11.7 (vs 4.36), DD 54% (vs 48%),
8/9 anni+ . (Le % FULL sono esplose dal compounding pos0.15·lev3 su 9k trade → guardare
Sharpe/DD/anni, non il livello %.)
## Gate PORT06 (pos0.15 lev3 canonico, OOS da 2024-10-12)
| variante ETH/BTC | FULL Sh | FULL DD | OOS Sh | OOS DD |
|---|---|---|---|---|
| **baseline 1h** | 6.43 | 3.96 | 8.58 | 1.36 |
| **SWAP 15m** | 7.64 | 3.49 | **10.39** | **1.26** |
| **BLEND 1h+15m** | 7.30 | 3.63 | 9.95 | 1.24 |
A fee di backtest (0.20% RT/coppia) **entrambe PROMOSSE**: Sharpe su e DD giu' ovunque.
## Stress slippage a livello PORT06 (il vero rischio: 15m = 5× i trade)
| fee_rt | RT/coppia | PORT06 FULL Sh | FULL DD | OOS Sh | OOS DD | std Sh | std oDD |
|---|---|---|---|---|---|---|---|
| baseline 1h | 0.20% | 6.43 | 3.96 | 8.58 | 1.36 | 4.36 | 16% |
| 15m | 0.20% | 7.64 | 3.49 | 10.39 | 1.26 | 11.7 | 13% |
| 15m | 0.40% | 7.04 | 4.08 | 9.78 | 1.45 | 8.5 | 27% |
| 15m | 0.60% | 6.43 | 4.67 | 9.15 | 1.66 | 5.3 | 47% |
**Degradazione graziosa ma reale**: il vantaggio di **Sharpe** sopravvive fino a slippage
pessimista (OOS 9.15 > 8.58 anche a 0.60%), ma il vantaggio di **DD si perde gia' a 0.40%**
(FULL DD 4.08 > 3.96 baseline; standalone oDD esplode 13→27→47%). La regola del progetto
("ri-gateare ogni filtro quando cambiano i costi") qui taglia: la frequenza 5× rende la
sleeve slippage-sensitive.
## Verdetto
- **NON un duplicato** (corr 0.35) e **NON overfit** (16/16 robusto) → la mia liquidazione
iniziale era SBAGLIATA, lo dico chiaro.
- **Passa il gate a fee di backtest, marginale sotto slippage**: migliora Sharpe sempre, ma
sotto slippage realistico (≥0.40% RT) peggiora leggermente il DD di portafoglio.
- **Due rischi di produzione NON ancora quantificati**: (a) qualita' dati ETH 15m (14-30%/anno
candele flat O=H=L=C → fill non eseguibili che gonfierebbero il backtest), (b) fill/liquidita'
reale a 2 gambe a 15m (5× ordini). Il worker pairs e' validato a 1h, non a 15m.
**Raccomandazione**: NON swap diretto in live. Candidato promettente → percorso forward:
preferire il **BLEND 1h+15m** (tiene il DD pulito del 1h e raccoglie il rendimento
decorrelato del 15m) **dopo** un check sull'impatto delle candele flat 15m sui pairs.
Allineato a come il progetto tratta FR01 (robusto ma non deployato finche' non domina pulito).
Resta come record di ricerca; deploy solo se il check flat-candle e' pulito.
## CHECK FLAT-CANDLE (pairs15m_flatcheck.py) — PULITO
Rischio: ETH 15m ha molte candele flat (O=H=L=C) → close stale che gonfia z-score →
reversione FINTA non eseguibile. Test:
- **Prevalenza**: ETH 15m **16.4% medio** (fino 30% nel 2022); BTC 15m solo 3.5%. Reale.
- **Fill toccati**: 12.9% degli entry e 15.2% degli exit cadono su una barra flat.
- **Test decisivo** (entry/exit SOLO su barre pulite, non-flat in entrambe le gambe):
rimuove 11.2% dei trade, **Sharpe trattenuto all'83%** (11.74→9.70; OOS Sharpe 18.4).
Se l'edge fosse un artefatto flat, filtrando crollerebbe → **NON crolla. NON e' artefatto.**
- **Gate PORT06 col 15m FLAT-FILTRATO** (corr 1h vs 15m-flat = 0.366, ancora decorrelato):
- SWAP 15m-flat: FULL 7.32/3.55, OOS **9.99/1.26** → PROMOSSO
- BLEND 1h+15m-flat: FULL 7.05/3.66, OOS **9.60/1.24** → PROMOSSO
## Conclusione (3 box su 4 puliti)
✅ NON duplicato (corr 0.35-0.37) ✅ robusto (16/16) ✅ NON artefatto flat (83% Sharpe)
⚠️ slippage-sensitive: a fee backtest passa pulito; a slippage ≥0.40% RT il vantaggio di
Sharpe regge ma il DD-edge si assottiglia. Il **BLEND** mitiga (meta' allocazione resta sul
1h pulito e slippage-robusto) → e' la forma deployabile.
## Realta' del deploy (perche' NON tocco ancora il live)
Il gate passa a livello BACKTEST. Ma il live NON puo' eseguire un sleeve 15m oggi:
- la live pairs gira SOLO a 1h (`PairsWorker`, validato da `validate_worker_pairs` a 1h);
il runner risampla a 1h/4h/1d, non gestisce un leg pairs a 15m.
- un BLEND richiede DUE sotto-sleeve ETH/BTC (1h + 15m) dentro il cap PAIRS, e il
**flat-skip va replicato nel worker live** (altrimenti il live tradera' le barre stale che
il backtest esclude → divergenza backtest-vs-live, la classe di bug che il progetto teme).
Editare `_defs.py` cambierebbe solo il backtest/report, NON il live → sarebbe ingannevole.
**Percorso deploy corretto** (da confermare): (1) estendere `PairsWorker`/runner al 15m +
flat-skip; (2) `validate_worker_pairs` a 15m (replay == backtest esatto); (3) aggiungere lo
sleeve 15m sotto il cap PAIRS; (4) shadow su testnet prima del paper. Finche' (1)-(2) non
sono fatti e validati, resta **record di ricerca PROMOSSO ma non live**.
@@ -0,0 +1,45 @@
# 2026-06-09 — ETH/BTC 15m: analisi di robustezza (il limite è il COSTO)
Domanda: lo sleeve `PR_ETHBTC_15M` (attivato in real, v1.1.16) è robusto? Risposta dai
dati: **sì su parametri, tempo e qualità-dati; fragile sui COSTI** — e quel limite è
strutturale (alta frequenza). Engine `pairs_research.pairs_sim_flat` (live-realizable).
## ✅ Robusto su parametri, tempo, dati
- **Plateau parametrico**: griglia n×z_in (40-80 × 1.5-2.5) → **16/16 celle Sharpe>1**
(range 9-12). Non un picco overfit. (`pairs15m_port06_gate.py`)
- **Consistenza temporale**: 2018-2021 +1874% (3/4 anni+, solo 2018 negativo);
2022-2026 +12958% (**5/5 anni+**); **OOS ultimo 30% Sharpe 17.6, DD 13%, +**. L'edge
è distribuito, non un singolo regime.
- **Non artefatto dei dati**: filtrando le candele flat ETH 15m (16% storico) resta
l'**83% dello Sharpe** (`pairs15m_flatcheck.py`).
- **Decorrelato dal 1h** (corr rendimenti giornalieri **0.37**): segnale diverso, non un relabel.
## ❌ Fragile sui COSTI — il vero tallone (frequenza 5× il 1h)
Sharpe in funzione del costo all-in RT/coppia (fee + slippage):
| costo RT/coppia | **15m Sharpe** | 1h Sharpe |
|---|---|---|
| 0.20% (reale, 1×) | **9.34** | 4.36 |
| 0.40% (2×) | 6.15 | 3.52 |
| 0.60% (3×) | 2.95 | 2.68 |
| 0.80% (4×) | **0.24** | 1.84 |
| 1.20% (6×) | 6.63 | 0.16 |
**Il 1h regge ~6× i costi; il 15m va negativo già a ~4×.** Tanti trade piccoli (8453 vs
1756) → margine di costo sottile. Lo **Sharpe 9.34 è un numero a costo basso**: appena lo
slippage reale a 2 gambe porta l'all-in verso 0.40-0.60%, scende a 3-6 (ancora positivo,
ma l'edge enorme del backtest è in gran parte illusione da bassi costi).
## Implicazioni (coerenti con la config deployata)
1. **La mezza size era giusta**: non si dà al 15m il peso che il backtest a costo basso
suggerirebbe. È un blend-tilt, non una scommessa.
2. **NO allo swap** (togliere il 1h, tenere solo 15m): sostituirebbe l'àncora cost-robust
(1h, regge 6×) con la sleeve cost-fragile. Confronto PORT06: swap OOS 9.95 vs blend 9.66
— +0.3 di Sharpe di backtest pagati in robustezza reale. Non vale.
3. **Giudice finale = ledger reale shadow**: misurerà lo slippage vero a 15m. Soglia di
lettura: all-in <0.40% (Sharpe ≥6) → ottimo diversificatore, valutare size piena;
verso 0.60%+ → vale appena la mezza-size che ha (già protetto).
**In una riga**: robusto come *segnale*, fragile come *esecuzione* → sta nel portafoglio a
metà size accanto al 1h, non al suo posto. Riproducibile: fee-sweep + sub-periodo +
OOS via `pairs_sim_flat`; plateau/flat-check negli script `pairs15m_*.py`.
@@ -0,0 +1,73 @@
# 2026-06-09 — Statistiche per-anno di tutte le sleeve attive in REAL
Snapshot post-attivazione BLEND ETH/BTC 15m (v1.1.16). Breakdown per-anno delle **15
sleeve che eseguono ordini reali** su Deribit testnet (escluse le 3 PAPER multi-asset
TR01/ROT02/TSM01). Engine **path-live**: EXIT-16 + filtro trend 3.0 per le fade,
walk-forward expanding per SH01, flat-skip per il pairs 15m. **PnL% = somma dei
rendimenti netti per-trade levered ×3, fee 0.10-0.20% RT incluse** — NON il contributo
al portafoglio (che applica sizing pos, cap-weighting, leva 2x, ribilancio 1D); serve a
confrontare edge grezzo e DD per sleeve.
Riproducibile: famiglie pairs via `pairs_research.pairs_sim`/`pairs_sim_flat`; fade/DIP/SH01
via le funzioni `stats_fades`/`stats_dip`/`stats_sh01` di `scripts/analysis/make_strategy_doc.py`.
## FADE (6) + DIP01 — PnL% per anno (n trade)
| anno | MR01 BTC | MR01 ETH | MR02 BTC | MR02 ETH | MR07 BTC | MR07 ETH | DIP01 BTC |
|---|---|---|---|---|---|---|---|
| 2018 | 78 (48) | 85 (42) | +69 (127) | +10 (127) | 12 (38) | +15 (25) | 78 (79) |
| 2019 | 84 (57) | +21 (46) | +18 (118) | +120 (114) | +55 (52) | 40 (35) | 104 (62) |
| 2020 | 29 (68) | 35 (55) | +100 (112) | 94 (135) | 11 (53) | +39 (43) | +67 (56) |
| 2021 | +326 (82) | +238 (60) | +332 (136) | +160 (127) | +245 (45) | +72 (24) | +315 (69) |
| 2022 | +567 (108) | +749 (76) | +728 (106) | +650 (91) | +446 (82) | +472 (58) | +407 (83) |
| 2023 | +318 (122) | +248 (73) | +235 (96) | +524 (146) | +168 (67) | +337 (58) | +228 (83) |
| 2024 | +413 (113) | +888 (126) | +868 (149) | +1952 (180) | +377 (75) | +645 (87) | +332 (81) |
| 2025 | +368 (90) | +361 (86) | +386 (136) | +931 (151) | +190 (60) | +226 (55) | +303 (85) |
| 2026* | +69 (29) | 9 (24) | +88 (37) | +25 (41) | +58 (17) | +40 (17) | +7 (29) |
| **TOT** | +1870 (717) | +2376 (588) | +2823 (1017) | +4278 (1112) | +1517 (489) | +1806 (402) | +1476 (627) |
| **maxDD%** | 32 | 23 | 19 | 31 | 12 | 23 | 37 |
## SHAPE SH01 (2) — PnL% per anno (n trade)
| anno | SH01 BTC | SH01 ETH |
|---|---|---|
| 2018 | 66 (237) | +74 (239) |
| 2019 | +88 (318) | 19 (365) |
| 2020 | +194 (240) | 293 (219) |
| 2021 | +301 (224) | +67 (146) |
| 2022 | +64 (142) | +79 (91) |
| 2023 | +17 (118) | +21 (17) |
| 2024 | +110 (144) | +108 (47) |
| 2025 | +77 (85) | +540 (108) |
| 2026* | +59 (23) | 30 (25) |
| **TOT** | +845 (1531) | +547 (1257) |
| **maxDD%** | 23 | 61 |
## PAIRS (6) — PnL% per anno (n trade) | 15m a mezza size (pos 0.075)
| anno | ETH/BTC 1h | LTC/ETH | ADA/ETH | BTC/LTC | ETH/SOL | ETH/BTC 15m |
|---|---|---|---|---|---|---|
| 2018 | 343 (177) | — | — | — | — | 492 (913) |
| 2019 | +233 (198) | — | — | — | — | +792 (963) |
| 2020 | +323 (211) | — | — | — | — | +452 (867) |
| 2021 | +549 (202) | — | — | — | — | +1122 (949) |
| 2022 | +1344 (206) | +292 (45) | +899 (157) | +328 (58) | +1243 (147) | +2136 (898) |
| 2023 | +464 (213) | +56 (94) | +341 (173) | +61 (106) | +125 (168) | +689 (976) |
| 2024 | +1661 (253) | +1024 (195) | +1078 (225) | +638 (186) | +1302 (199) | +6313 (1353) |
| 2025 | +1173 (225) | +962 (196) | +1046 (187) | +686 (178) | +1264 (202) | +3664 (1123) |
| 2026* | +61 (71) | +279 (76) | +140 (71) | +265 (71) | +270 (88) | +155 (411) |
| **TOT** | +5464 (1756) | +2614 (606) | +3504 (813) | +1978 (599) | +4204 (804) | +14832 (8453) |
| **maxDD%** | 48 | 14 | 19 | 21 | 24 | 34 |
| **Sharpe** | 4.36 | 4.22 | 4.90 | 2.72 | 4.61 | 9.34 |
\* 2026 parziale (fino al 28 mag). Coppie alt (LTC/ADA/SOL) hanno storia solo dal 2022.
## Aggregato PORT06 (BLEND attivo, 18 sleeve def.)
**FULL Sharpe 7.20 / DD 3.68% / CAGR 76% — OOS Sharpe 9.66 / DD 1.31%**. Per anno:
2021 +19.0% · 2022 +73.3% · 2023 +39.8% · 2024 +220.1% · 2025 +110.5% · 2026* +7.7%.
Nessun anno negativo dal 2021.
## Letture
- Il PnL% grezzo del 15m e' grande perche' fa ~5x i trade del 1h (8453 vs 1756), non
per-trade: il confronto equo e' Sharpe (9.34 vs 4.36) e DD (34% vs 48%). Nel portafoglio
entra a mezza size -> ~11.5% del rischio (== il 1h).
- MR02 ETH = singolo motore piu' forte (+4278%, 2024 +1952%); MR07 il piu' selettivo
(DD 12-23%); SH01 ETH il piu' rischioso (DD 61%, coda 2020 293%) -> mitigato dal cap SHAPE.
- I 3 book multi-asset (TR01/ROT02/TSM01) restano PAPER, fuori da questo elenco.
@@ -0,0 +1,49 @@
# 2026-06-09 — XS01: reversione cross-sectional (famiglia nuova, trovata + deployata PAPER)
## Origine
Dopo aver scartato (alla cieca, coi giochi) trend/breakout/seasonal/opzioni/funding come
rumore o EV, ho cercato io un meccanismo *diverso* dalla mean-reversion pairwise. Trovato:
**XS01 — reversione CROSS-SECTIONAL** su 8 asset (BTC/ETH/LTC/ADA/SOL/BNB/XRP/DOGE).
## Meccanismo
Ogni HOLD=12 ore: classifica gli 8 asset per rendimento su LB=48 ore, pesi
w = (ret media_cross-section), normalizzati a gross 1 → **long i perdenti relativi /
short i vincenti**, market-neutral. Roll non sovrapposto (entry-to-entry = hold+1 barre).
Fee 0.10% RT/book. Cattura il FATTORE reversione trasversale, distinto dai pairs (pairwise).
## Verifica (engine canonico `scripts/strategies/XS01_cross_sectional.py`)
- **No look-ahead** verificato (segnale invariato perturbando il futuro).
- **Robusto**: plateau OOS Sharpe **23.9** su lb 1272 × hold 624.
- **Scorrelato**: corr **0.006 / 0.035** da PR01 ETH/BTC, 0.028 dai fade → diversificatore.
- Per-anno (entry): 2022 +34, 2023 +6, 2024 +21, **2025 +225**, 2026 +85 (5/5 anni+).
- **Caveat**: edge concentrato sul 2025; cost-sensitive (muore ~0.35% RT/book); 8 gambe;
storia dal 2022 (no 2018-2020).
## Worker validato (== backtest esatto)
`src/live/xsec_worker.py` `CrossSectionalWorker`: book market-neutral che rolla ogni HOLD
barre, stessa formula pesi e cadenza dell'engine. `validate_xsec_worker.py`: replay
bar-per-bar == backtest **ESATTO** (worker 4993/1427 trade/49.8% == backtest 4993/1427/49.8%).
Bug risolto: il primo prototipo rollava 1 barra troppo tardi (cooldown extra) → rimosso,
guard a lb+1, entry-to-entry = hold+1.
## Gate PORT06 — PROMOSSO (con asterisco)
| | corr | FULL Sh | FULL DD | OOS Sh | OOS DD |
|---|---|---|---|---|---|
| ATTUALE (19→ senza XS01) | — | 7.20 | 3.68 | 9.66 | 1.31 |
| **+XS01** | 0.006 | **7.34** | **3.46** | **10.07** | 1.48 |
Migliora 3 metriche su 4 (OOS Sharpe **+0.41**, il salto più grande dal 15m; FULL DD giù).
Unico neo: OOS DD +0.17pp. Risk-contrib XS01 solo **2.2%** (diversificatore a bassa vol).
## Deploy (v?, 2026-06-09) — PAPER
8 gambe → niente esecuzione reale (come TR01/ROT02/TSM01) → XS01 gira **PAPER**
(`paper_sleeves`), fuori dal pool, raccoglie statistica forward. Wiring: `_defs.XSEC` in
PORT06 (19 sleeve, family XSEC via prefix "XS"), `build_everything` (equity da xsec_sim),
`runner` kind="xsec" → CrossSectionalWorker, `asset_days` ora include i paper (fix: gli alt
BNB/DOGE/XRP ora vengono fetchati anche per TR01/ROT02/TSM01). Regression-lock aggiornati
(18→19 sleeve, FULL 7.20→7.34, OOS 9.66→10.07, DD 3.68→3.46). 93 test verdi.
**Direzione futura:** se la statistica forward conferma, costruire l'esecuzione reale a
N gambe (oggi inesistente) per portarlo nel pool. Per ora: candidato validato che gira
PAPER e si osserva. Artefatti: `scripts/strategies/XS01_cross_sectional.py`,
`src/live/xsec_worker.py`, `scripts/analysis/{validate_xsec_worker,xsec_port06_gate}.py`.
@@ -0,0 +1,46 @@
# 2026-06-10 — FC01 funding-carry market-neutral: SCARTATA (il carry non paga le fee)
## Idea
Carry NEUTRALE sul funding Deribit (meccanismo mai esplorato: W12 era lo short
direzionale su funding alto, bocciato): short della gamba col funding alto /
long della gamba bassa (ETH vs BTC, dollar-neutral), incassando il differenziale
di funding con esposizione residua = solo lo spread ETH/BTC.
Dati REALI: `data/regime/{btc,eth}_funding.parquet` — funding orario effettivo
dic 2019 → giu 2026 (56.938 ore), `interest_1h` + index_price.
Harness: `scripts/analysis/funding_carry_research.py` (causale: decisione al
close t, accrual da t+1; fee 0.10% RT per gamba; TRAIN ≤2023-11-01 / OOS dopo).
## Numeri che uccidono l'idea (prima ancora del backtest)
- Funding annualizzato: BTC mean +7.2% / med +0.3%; ETH +5.2% / +0.05% —
la MEDIANA è ~zero: il funding alto è episodico, non strutturale.
- Spread ETHBTC: p10 19% / p90 +12.6% annualizzato, MA autocorr del
24h-smooth a 72h = 0.20 → poca persistenza.
- **Episodi |spread|>10% ann: durata mediana 19h** (p90 60h) → carry
incassabile per episodio ≈ 10%/8760×19h ≈ **0.02-0.05%**, contro **0.20%**
di fee (2 gambe). Strutturalmente morto: il carry è un ordine di grandezza
sotto i costi, e il price-leg dello spread ETH/BTC (vol ben maggiore del
carry) domina il PnL.
## Backtest (conferma)
- FC-A spread-carry, griglia smooth {24,72,168}h × thr {5,10,20}% ann:
TRAIN quasi tutto negativo (Sharpe 0.6…+0.03), OOS negativo ovunque
tranne una cella (s24 thr5: OOS +38% con TRAIN 45% = rumore/sign-flip).
Sweep fee: negativa anche a 0.05% RT/gamba. Annuale: 2021 93%, nessuna
stabilità.
- FC-B direzionale single-asset (confronto onesto): negativa ovunque,
riconferma W12.
## Verdetto
SCARTATA. Su Deribit BTC/ETH il funding non è un carry harvestabile: troppo
piccolo, troppo breve, e l'hedge cross-asset introduce più rischio del carry
che raccoglie. Eventuale rivisita SOLO se: (a) si aggiungono alt ad alto
funding cronico (Hyperliquid, registry già validato) dove i livelli sono
5-10x, o (b) si usa il funding come FEATURE/gate di strategie esistenti
(es. filtro sulle entry PR01) invece che come fonte di PnL.
Script: `scripts/analysis/funding_carry_research.py` (resta come record negativo).
@@ -0,0 +1,113 @@
# 2026-06-10 — Gioco "Blind Traders" sessione 3: GRID TRADERS (regola: STRATEGIA_GRIGLIA.md)
## Setup
Terza sessione del gioco dei trader ciechi, con una regola nuova: ogni agente deve
implementare la strategia descritta in `STRATEGIA_GRIGLIA.md` (grid trading a griglia
geometrica). 100 agenti (haiku, via Workflow) ricevono SOLO un digest anonimo di due
serie X/Y (in realtà BTC/ETH, mai rivelato) sul loro timing assegnato
(25×15m, 20×30m, 20×1h, 15×2h, 12×4h, 8×1d) + 5 archetipi di stile a rotazione
(prudente / aggressivo / asimmetrico-rialzista / asimmetrico-ribassista / data-driven),
e propongono la CONFIGURAZIONE della griglia: `range_down/up %`, `grid_levels`,
`sl_buf`, `tp_buf`, `max_bars`, serie.
Infrastruttura nuova in `scripts/games/`:
- **`grid_engine.py`** — backtest deterministico, causale, fee-aware della spec:
griglia geometrica `ratio=((1+ru)/(1-rd))^(1/L)` costruita sul close di deploy,
capitale 1/L per livello (§3.3), buy su attraversamento ↓ di un livello non
riempito, sell del livello su attraversamento ↑ del successivo (§5.2), SL sotto
il range e TP sopra che liquidano tutto (§6), redeploy a fine episodio
(SL/TP/max_bars). **Vincolo break-even §4 implementato alla lettera**: passo
≤ 1.5×costo RT → il motore si rifiuta di partire (`refused`, fitness 2e6);
`_normalize` dell'arena riduce i livelli al massimo legale. Fill intrabar lungo
il percorso O→L→H→C / O→H→L→C; fee 0.10% RT per round-trip + slippage opzionale.
- **`grid_arena.py`** — torneo identico alle sessioni 1-2: split 60/20/20
TRAIN/VALID/TEST, 90 epoche di hill-climb sul TRAIN, cull del 10% in VALID ogni
10 epoche → 10 superstiti; TEST = OOS puro mai ottimizzato.
- **`grid_brief.py`** — digest anonimo con statistiche per dimensionare una griglia:
escursione max/min rolling (w100/500/2000, mediana e p90) e probabilità di fuga
da un range ±5/10/20% entro 500 barre.
## Esito
`data/games/grid_result.json` (+ log `grid_tournament.log`). 100/100 spec da agenti
reali (nessun sostituto random).
- **Alla proposta, 80/100 agenti scelgono X (=BTC)**: dai soli numeri anonimi capiscono
che la griglia sopravvive meglio sulla serie meno volatile (escape ±20% in 500 barre:
BTC 34.5% vs ETH 53.2% a 1h).
- **L'evoluzione ribalta la scelta: tutti i 10 superstiti finiscono su Y (=ETH)** —
nel periodo VALID/TEST la vol più alta di ETH paga di più i round-trip, e il rischio
trend è gestito non dal range stretto ma dalla FORMA della griglia (sotto).
- **Convergenza fortissima della forma** (9/10 superstiti): griglia **asimmetrica
ribassista** — range profondo sotto (13/20%), corto sopra (+4/+8%), **livelli al
minimo (4)** → passo largo ~4.5-5.5%, SL buffer profondo (5-15%), max_bars lunghi.
Tradotto: **compra i dip di ETH in 4 tranche distanziate ~5%, rivendi ogni tranche
al rimbalzo di un passo, stop catastrofale a ~25/30%**. Il gioco ha ri-scoperto
per la terza volta la mean-reversion (qui in forma di dip-buying a tranche), e ha
imparato da solo la lezione anti-fee: meglio pochi passi larghi che griglie fitte.
**Vincitore: agente #11, ETH 15m**, griglia 17.1%/+4.6% × 4 livelli, SL buf 12.4%,
TP buf 4.8%, max 2143 barre. TEST(OOS): **PnL +891% (additivo), win 97%, 38.5
trade/mese, Sharpe 10.1**. FULL 2018-2026 (include bear 2018 e 2022): +4284%, Sharpe 9.6.
Stress slippage TEST: 0.05%/lato → 871%, 0.10%/lato → 850% (il passo ~5.4% ⋙ costi).
## Caveat onesti (perché NON è un candidato deploy così com'è)
1. **Il PnL è additivo per-trade e non misura il drawdown UNREALIZED**: l'engine
somma i round-trip realizzati; mentre la griglia tiene 4 tranche dentro un
drawdown 17%, l'equity vera è sott'acqua (fino a ~15% di episodio + SL 25/30%
quando scatta). Il win-rate 97% è il profilo classico della griglia: tante
micro-vincite, perdite rare ma grandi (stessa famiglia di rischio del
short-vol/martingala). Per un gate serio servirebbe l'equity curve mark-to-market.
2. **Long-only su ETH con VALID/TEST 2023-2026** (regime rialzista/oscillante):
l'asimmetria 17/+4.6 è anche un fit al regime. Il FULL positivo (bear inclusi)
è incoraggiante ma il grosso del PnL resta nei periodi di reversione rialzista.
3. Le candele flat ETH 15m (14-30%/anno) e i fill intrabar "al livello" condividono
i caveat noti del progetto (bias pro-stop-stretti dell'engine intrabar; qui gli
stop sono larghi, quindi l'effetto è minore).
## Lezioni
- La spec STRATEGIA_GRIGLIA.md è implementabile in modo causale e onesto; il suo
vincolo break-even (§4) è esattamente la "lezione fee" del progetto, e il torneo
l'ha confermato spingendo i livelli al minimo (passo massimo).
- La griglia evoluta è un parente povero delle fade MR già in live: stesso edge
(reversione ETH), ma incassato con inventory risk crescente invece che con
TP/SL per-trade. Non sostituisce le MR; eventuale interesse solo come variante
"a tranche" da gateare con equity mark-to-market (e confronto con MR01/MR02 a
parità di rischio) PRIMA di pensarci per il PORT06.
## GATE "si puo' inserire?" (stessa sera) — NO-GO: edge = artefatto delle wick testnet
`scripts/analysis/grid_game_gate.py`: engine **mark-to-market** dedicato (equity
per barra = capitale + inventario al close, SL gap-aware, flat-skip, fee 0.10% RT),
metriche standard del progetto + gate PORT06. Primo passaggio ingannevole: standalone
WINNER 15m FULL Sharpe 5.61 DD 15.8%, corr max coi sleeve 0.34, plateau 16/16,
e il criterio formale promuoveva il half-size (OOS Sh 10.07->10.12, DD 1.48->1.36).
MA il breakdown annuale mostrava **+4946% nel 2022** (ETH 70%): impossibile.
**Verifica avversariale decisiva**: il feed Deribit (testnet) e' pieno di **spike
print** — ETH 15m ha 1064 barre con wick ≥6% sotto i close adiacenti che rientrano
subito (fino a 19% in 15 minuti; BTC ha spike a 54% nel feb 2024). La griglia
intrabar compra su quelle wick FINTE e rivende al rimbalzo: free money in sim,
infillabile dal vivo. Stress **close-only** (fill solo su attraversamento del
close): WINNER 15m CAGR 1544%->21%, Sharpe 5.61->0.92, OOS +2738%->+32%,
trades 3803->1156; top3 1h CAGR -1.7%, OOS 18%. **~99% dell'edge vive nelle
wick.** La versione onesta (Sharpe 0.92, DD 27%) e' molto sotto ogni sleeve
deployato → **NON inseribile**, in nessuna size.
Lezioni aggiuntive:
- Il torneo ha massimizzato esattamente l'artefatto: la convergenza su ETH 15m
range profondo era la firma del **wick harvesting**, non di un edge.
- I motori intrabar dei giochi (`grid_engine`, e in misura minore i TP intrabar
di `engine.py`) vanno SEMPRE stressati con una variante close-only prima di
promuovere qualunque vincitore: aggiungere il check ai prossimi giochi.
- Gli sleeve in produzione NON sono toccati da questo artefatto nello stesso modo:
pairs entra/esce sui close, le fade hanno EXIT-16 close-confirm sullo SL, e
soprattutto il ledger REAL-TRUTH usa i fill reali (che gli spike non fillano).
Artefatti: `scripts/games/grid_{engine,arena,brief}.py`, spec agenti in
`data/games/specs_grid/`, digest `data/games/grid_digests.json`, risultato
`data/games/grid_result.json`, log `data/games/grid_tournament.log`, gate
`scripts/analysis/grid_game_gate.py` (param `close_only` per lo stress anti-wick).
@@ -0,0 +1,57 @@
# 2026-06-10 — REAL-TRUTH: il ledger segue i fill reali (sim → diagnostica)
## Richiesta
L'utente vuole che sim e reale coincidano — o meglio, che il sim non esista come
verità: «voglio vedere dati che hanno avuto movimento reale su exchange». La
ricognizione dei 77 eventi reali (dal 2026-06-03) ha mostrato che il gap
sim/reale NON era slippage (piccolo, es. 0.15 bps su un TP) ma **contabile**:
1. **Ledger separati.** Il `capital` che guida portafoglio/ribilanci/sizing era
aggiornato dal PnL SIM; il PnL reale finiva in un `real_capital` parallelo che
non guidava nulla. MR01 BTC: sim +28.93 vs reale +1.98 sugli stessi 4 trade.
2. **Prezzi sim da candele testnet** (spike print, es. 2026-06-07 sim short BTC a
65266.5 con mark reale 62395): il sim bookava PnL che il reale non vede. Con
il ledger sim come verità, l'equity del portafoglio accumulava questa fantasia.
Nota: il *notional* reale era GIÀ derivato dalla formula sim
(`capital·ps·lev` passato a `_real_open`/`_real_open_pair`) — il punto 1 della
richiesta (sizing allineato) si chiude da solo una volta che `capital` è reale.
## Implementazione (REAL-TRUTH)
- `StrategyWorker` e `PairsWorker` accettano `real_truth: bool` (default False =
shadow storico). Con flag attivo e esecuzione abilitata:
- `_real_close`/`_real_close_pair` ritornano `(real_pnl, applied)`;
`applied=True` se ci sono fill reali (o chiusura verificata).
- `_close_position`/`_close` chiamano la chiusura reale PRIMA dell'update
ledger: `capital += real_pnl` (fee reali incluse); `is_win = pnl_reale > 0`.
- Il sim resta nel log CLOSE come diagnostica: `pnl_source` ("real" |
"sim_fallback"), `sim_pnl`, `real_pnl`.
- **Fallback al sim** SOLO se il trade reale non è mai esistito/fillato
(REAL_OPEN_FAIL, fill zero) — dichiarato nel log, mai silenzioso.
- Runner: `overrides.execution.real_truth` (yml) → `build_worker_for(...,
real_truth=)`. `portfolios.yml`: **`real_truth: true`**.
- Conseguenza a catena: equity ledger → pesi → allocazioni → notional dei
prossimi ordini derivano ora dai soldi veri sul conto. Il `real_capital`
parallelo resta come ledger puro-reale di confronto.
## Test
`tests/portfolio/test_real_truth.py` (6 test): capital segue i fill reali
(single-leg e pairs), una divergenza sim-win/reale-loss viene contata come LOSS,
fallback sim dichiarato su REAL_OPEN_FAIL/leg-fail, e modalità shadow invariata
senza flag. Suite completa: 99 passed.
## Limiti onesti
- I multi-asset (TR01/ROT02/TSM01/XS01) restano sim per costruzione (paper
sleeves fuori dal pool, capitale insufficiente per i book multi-leg).
- La STORIA del ledger non è riscritta: l'equity attuale (~2154) ingloba il PnL
sim accumulato fino a oggi (reale realizzato era ≈ 15.6 dal 3/6). Da ora la
divergenza smette di accumularsi; se si vuole un azzeramento (reset equity al
conto reale) è un'operazione separata e deliberata.
- Le DECISIONI di trading (entry/exit) restano guidate dai prezzi del feed
(candele testnet): real-truth corregge la contabilità, non i segnali. Gli
spike print del feed possono ancora generare entry/exit subottimali — ma ora
il loro effetto si misura in PnL reale, non in PnL immaginario.
@@ -0,0 +1,51 @@
# 2026-06-10 — XS01 dispersion-gate: PROMOSSO e LIVE (entry solo con dispersione da fare rientrare)
## Domanda
L'edge di XS01 (reversione cross-sectional 8 asset) era concentrato (2025 domina,
2023 quasi piatto) e cost-sensitive. La reversione cross-sezionale va accesa solo
quando c'e' dispersione da far rientrare?
## Metodo (anti multiple-testing): `scripts/analysis/xs01_dispersion_gate.py`
3 feature di regime CAUSALI calcolate dallo stesso panel closes (nessun feed
esterno): `g_disp` = std cross-section del momentum lb (la grandezza che si fada),
`g_corr` = correlazione media pairwise 72h (identita' della varianza dell'indice),
`g_vol` = vol BTC 168h. Diagnostica per quintili (quintili dal TRAIN, 70/30) sul
net per-trade dell'engine canonico NON gateato, TRAIN e OOS separati: si procede
solo con relazione monotona e concorde nelle due finestre.
## Esito diagnostica
- **g_disp: monotona e concorde** — Q1 NEGATIVO (10 bps TRAIN / 8 OOS) →
Q5 +26/+280. Senza dispersione i trade sono solo fee. PROMOSSA.
- g_corr e g_vol: non monotone / segno incoerente → BOCCIATE (niente fishing).
## Gate (sweep soglie = percentili TRAIN, side dal TRAIN)
Plateau pieno p30-p70, niente picco: TRAIN Sh 1.51 → 2.0-2.3, OOS Sh 5.73 →
6.2-7.5. Scelta **p50 (disp_min = 0.0313)**, ~47% delle ore aperte:
- Standalone: trade 1427→859 (40% turnover → meta' fee), win 50→53%,
**Sharpe 2.50→3.46**, DD 16.2→15.8%. **Ogni anno migliora**: 2022 +34→+40,
2023 +6→+28, 2024 +21→+44, 2025 +225→+237, 2026 +85→+108 — risolve la
concentrazione, il punto debole della validazione originale.
- Fee stress 2x (0.20% RT/book): OOS Sh 6.76 — la cost-sensitivity e' mitigata
(il gate taglia proprio i trade che pagavano fee senza edge).
- **Gate PORT06** (swap equity sleeve): FULL Sh 7.34→7.41 DD pari,
**OOS Sh 10.07→10.37 DD 1.48→1.47** → PROMOSSO (criterio standard).
## Implementazione (solo path LIVE, come trend/hurst sulle fade)
- `src/live/xsec_worker.py`: param opzionale `disp_min` (None = off), check in
`_open_book` su `nanstd(logC[i] logC[ilb])`. Default off → la validazione
`validate_xsec_worker` (replay == backtest) resta esatta.
- `src/portfolio/runner.py`: pass-through di `disp_min` (il runner costruiva il
dict params esplicitamente e l'avrebbe perso).
- `scripts/portfolios/_defs.py`: `disp_min: 0.0313` nella spec XS01.
- Il backtest canonico (`build_everything`) resta NON filtrato → il live fara'
meglio del backtest, coerente con le altre guardie.
Unit check: gate blocca panel piatto / apre panel disperso / default off invariato.
99/99 test. Nota macro della giornata: FC01 funding-carry SCARTATA
(diario separato) — il protocollo promuove ~1 idea su molte, come deve.
@@ -0,0 +1,86 @@
# 2026-06-11 — Sweep di stabilità su tutte le strategie (anti-overfit)
Obiettivo: analisi di tutte le 19 sleeve cercando miglioramenti/correzioni/protezioni che
aumentino la STABILITÀ, con disciplina anti-overfit (ipotesi pre-registrate, griglie fissate
prima di guardare i numeri, verdetti su TRAIN E OOS con plateau, gate PORT06; nessun re-test
di idee già bocciate: ADX/vol-target/time-stop/hurst/stop SH01/multi-TF/entry-guard ecc.).
## Audit di drift (nessun fitting)
- `report_families` sui dati correnti: correlazioni cross-famiglia ancora ≈0 (pairs 0.03-0.11,
XS01 0.01, SH 0.03-0.04 vs MASTER-9) → la diversificazione NON è driftata.
- **Regression-lock trend_max** (`trendmax_port06_impact`) rieseguito su dati freschi:
parità 1.00000 su tutte e 6 le fade (engine live-path integro) e **plateau trend_max
2.5/3.0/3.5 confermato** (OOS Sh 11.14/11.24/10.98, DD 1.33 identico).
- **Percentile del rolling-return PORT06** (cap weights, storia 2021+): finestra corrente
60g al 19°, 120g al 21°, 160g al 28° percentile → periodo fiacco ma in variazione normale.
- ⚠️ **FADE in coda storica**: il rolling 120g equal-weight delle 6 fade è a **1.0% =
2° percentile** della propria storia (p5 = +0.4%). È il tratto peggiore mai attraversato
dalla famiglia. Decisione esplicita: NESSUN ritocco ai parametri (sarebbe fit sul regime
corrente, l'errore che il progetto evita da sempre); la protezione è la diversificazione
(già in atto: il portafoglio regge al 19-28° pct) + monitoraggio. Follow-up in TODO:
alert di drift per-famiglia nel hourly_report (distribuzione storica precomputata).
## Correzione: bug contabile TR01 worker (FIXATO)
`BasketTrendWorker.tick` usava `mean(rets)` sui SOLI asset in posizione → con paniere
parziale sovrappesa N/k (con 1 solo long: 0.45 del capitale invece di 0.09). Era l'origine
della divergenza replay 44% vs reference +42% annotata nel TODO. La convenzione canonica
(backtest PORT06 via `_tr_basket_daily`) è equal-weight 1/N sull'universo: fix 1 riga
(`sum(rets)/len(universe)`). Replay post-fix: **TR01 +32% vs reference +42%** (stesso segno
e ordine di grandezza = gate del validatore; il residuo è la differenza dichiarata
capitale-unico vs media-equity). ROT02 +171%==ref, TSM01 +5%==ref invariati. Solo
statistica PAPER: nessun effetto su pool/ordini. NB: il forming-bar su ROT02/TSM01 segnato
nel TODO era GIÀ fixato (v1.1.10, `_panel` scarta la barra in formazione) — TODO aggiornato.
## Protezione 1 (pre-registrata): disaster-cap z sui pairs → **NO-GO**
Ipotesi: exit immediata se |z| ≥ z_stop dopo l'ingresso taglia la coda da structural-break
senza toccare i trade normali. Griglia fissata: z_stop {3.0,3.5,4.0,5.0} × 5 coppie 1h +
{2.5,3.0,3.5,4.0} sul 15m, train <2023-11-01 / OOS, engine con regression-lock ESATTO su
`pairs_sim`/`pairs_sim_flat`. Esito (potere statistico AMPIO, centinaia di trigger):
**bocciata su tutti e 3 i criteri** — il DD peggiora quasi ovunque, il worst-trade OOS
peggiora su 4 coppie su 6 (ETH/BTC 62→−168%!), Sharpe OOS cala oltre il 10% relativo in
TUTTE le celle, e il "plateau" esiste solo per il danno (monotono: più stretto, peggio).
Meccanismi: (i) lo stop realizza la perdita al massimo overshoot — il movimento che la
strategia fada (stessa lezione EXIT-16/SH01, **5ª conferma**); (ii) l'engine non-overlap
rientra subito nello spread ancora divergente → churn di fee a 2 gambe e stop ripetuti.
I pairs restano senza stop by design; la mitigazione resta la taglia
(`position_size_family` PAIRS 0.20). Record: `scripts/analysis/pairs_zstop_research.py`.
## Protezione 2 (pre-registrata): phase-tranching XS01 → **PROMOSSA e LIVE**
Diagnosi: il roll non-sovrapposto di XS01 ha una FASE arbitraria (dipende da quando il
worker parte) e l'esito ne dipende parecchio — sulle 12 fasi possibili: Sharpe daily FULL
1.52-2.33, DD per-trade 13.8-33.1% (`xs01_tranche_research.py`). È timing-luck puro: il
backtest canonico (fase 0) è una delle estrazioni FORTUNATE sul FULL (DD 15.4 vs mediana
~21). Rimedio senza parametri fittati: **ensemble di fase** — K sub-book sfasati di hold/K
barre su capitale comune (PnL/K). Gate onesto su equity daily (`xs01_tranche_gate.py`):
- standalone OOS: Sharpe 3.79→4.27 (K=2) →4.85 (K=3), DD 7.99→7.25→5.56;
- FULL standalone: il DD sale verso la mediana di fase (~21%) — il "peggioramento" è la
RIMOZIONE della fortuna della fase 0, non un costo;
- PORT06 swap-sleeve: FULL invariato (7.33/3.46), OOS Sh 10.07→10.11 (K=2) →10.15 (K=3),
OOS DD 1.48→1.43→1.38. **Plateau: K=2 E K=3 entrambi promossi** (non best-pick).
Implementazione: `CrossSectionalWorker` param `tranches` (default 1 = storico; live K=3 in
`_defs.py`), books indipendenti con sfasamento iniziale `wait`, capitale comune, migrazione
automatica dello status legacy (il vecchio book → tranche 0), `last_bar_ts` solo-avanti
(robustezza ai panel accorciati da feed in ritardo). Solo path LIVE come `disp_min` (il
backtest canonico resta single-phase → il confronto live/backtest va letto con la mediana
di fase in mente). **Validatore esteso e PASSATO**: K=1 replay == `xsec_sim` ESATTO
(1427 trade, cap 4993==4993); K=3 replay == unione fasi 0/4/8 ESATTO (4279 trade,
4512==4512). Osservabilità: `hourly_report` aggrega i sub-book nel book medio.
## Non toccati (per evidenza, non per pigrizia)
- **Fade/DIP01**: exit-lab già esaustivo (23+11 famiglie); plateau e parità riconfermati oggi.
- **SH01**: 11 famiglie di stop già bocciate; cap famiglia + monitor trade-rate in essere.
- **ROT02/TSM01**: replay == reference; forming-bar già fixato.
- **TR01 come sleeve** (3 anni negativi su 6): rimosso? NO — sarebbe una decisione guidata
dal regime recente (recency bias); il suo ruolo è catturare i trend che le fade non
prendono. Resta, con la statistica ora contabilizzata correttamente.
## Esito test
`pytest`: 99 passed. Validatori: honest workers OK, xsec K=1/K=3 OK, trendmax lock OK.
+92
View File
@@ -0,0 +1,92 @@
# 2026-06-11 — Audit di sistema + verità contabile sul netting (v1.1.24)
Seconda tornata della giornata (dopo lo sweep strategie): analisi del SISTEMA live
(esecuzione, stato, feed, processo) con 3 indagini parallele + fix.
## Audit live (dal reset 2026-06-10T21:24Z)
- **Churn phantom-TP quantificato**: 20 round-trip fantasma sui fade ETH (2 finestre,
13:06Z e 17:32-17:58Z, feed che stampava wick ~1640 con mercato a 1675-90 per 25 min).
Somma real_pnl 2.35 USD vs sim_pnl +80 USD: il real-truth ledger ha contenuto, il gate
TP_PHANTOM (v1.1.23) copre il pattern da stasera.
- **Il conto NON quadrava coi libri**: short 0.027 ETH in più (l'hedge long ETH del pair
ETH/SOL mangiato dai close reduce-only dei fade) e BTC flat con MR02_BTC convinto di
essere short (TP resting fillato da uno spike REALE del book a 3.8%, +6.6$ non bookati,
che il worker riconcilierà alla chiusura sim) col **disaster-SL residuo sul book a
posizione flat**. Bonifica eseguita: DSL `USDC-SLMB-26521` cancellato, riallineo
+0.027 ETH reduce-only (fill 1682.5) → conto == libri (verificato).
## Root-cause strutturale
Le **quote per-worker con ordini reduce-only su un conto a NETTING si rompono quando due
worker hanno direzioni opposte sullo stesso strumento** (pairs long ETH vs fade short ETH):
- un close reduce-only può essere **cappato** (Deribit riduce l'amount in silenzio) → il
ledger bookava la chiusura PIENA perché `Fill.amount` era il richiesto, non il fillato;
- un close reduce-only nel verso "sbagliato" rispetto al netto viene **respinto** → la
gamba pairs resta orfana sul conto, ma il worker bookava il PnL al prezzo sim e azzerava
lo stato (3 volte oggi: PnL fantasma nel ledger real-truth, ETH/SOL di fatto short nudo).
## Fix (v1.1.24) — verità, non workaround
1. **`Fill.filled_amount`** (da `order.filled_amount`, fallback trades/history): tutti i
ledger usano il fillato; nota "FILL PARZIALE" nel Fill.
2. **`REAL_CLOSE_PARTIAL`** (log + Telegram): close che filla meno del residuo → residuo
orfano dichiarato, `REAL_CLOSE verified=false`.
3. **Pairs per-gamba**: PnL bookato SOLO per gambe con fill verificato; gamba respinta →
record in `orphan_legs` (persistito nello status) + alert `PAIR_LEG_ORPHAN`;
`applied=True` (real-truth) solo con ENTRAMBE le gambe → altrimenti fallback sim
DICHIARATO (meglio del numero mezzo-reale di prima).
4. **`REAL_DIVERGENCE` anche su jsonl** (prima solo Telegram: l'audit ha dovuto
ricostruire gli episodi dai REAL_CLOSE).
5. **Runner: tick isolato per-worker** — un'eccezione in un worker non salta più gli
altri né l'update equity; streak per-worker con alert `WORKER_ERROR_STREAK` a 5.
Test: 2 nuovi (partial-close, orphan-leg) + fixture aggiornate → 106 passed.
## Decisione di design APERTA (per l'utente)
Finché pairs e fade condividono strumenti in direzioni opposte, le chiusure possono
orfanizzarsi: ora è VISIBILE e contabilizzato giusto, ma non eliminato. Opzioni:
A) **Position-manager centrale per strumento** (il runner netta i delta di tutti i
worker e manda UN ordine; ricostruisce le quote contabilmente) — corretto ma
invasivo, da progettare con calma;
B) **Sotto-conti Deribit separati** per famiglia (pairs vs fade) — pulito, richiede
setup conto e client multi-token;
C) **Status quo monitorato**: alert PAIR_LEG_ORPHAN + riallineo manuale (oggi: 1 giro
in 2 minuti) — accettabile su testnet, NON per capitale vero.
Raccomandazione: C ora, A prima di passare a capitale reale.
**Secondo passo — FATTO (v1.1.25): NETTING delle chiusure market.** `close_amount`
tenta il reduce-only e riesegue il residuo cappato/respinto in market puro (= il
netting contro le quote opposte: il conto si muove del delta esatto del libro).
Un solo punto di fix (anche `close_pair` ci passa). Fill combinato per il chiamante
(prezzo pesato, fee sommate), evento `NET_CLOSE` su log+Telegram a ogni fallback,
4 test dedicati. Niente più orfani per costruzione; `orphan_legs` resta come ultima
difesa se fallisce anche il market puro. Effetto collaterale benefico: la chiusura
futura della gamba ETH di ETH_SOL#2 (che sarebbe stata respinta di nuovo) ora
eseguirà correttamente. La scelta A-vs-B-vs-C resta aperta solo per la parte
RESTING (TP/DSL su book condiviso) e per i multi-asset.
**Primo passo verso A — FATTO (sera stessa): reconciler read-only.**
`scripts/analysis/reconcile_account.py`: per ogni strumento USDC confronta
atteso (Σ quote reali dai status.json: single-leg + pairs 2 gambe + orphan_legs
registrati = drift SPIEGATO) vs conto reale (`get_positions`, size/mark → coin),
tolleranza 1.5×step, anti-race (ricontrollo a 10s prima di segnalare). In crontab
host ORARIO (:40) con alert Telegram `ACCOUNT_DRIFT`. Al primo run ha beccato un
vero positivo: BTC libro short 0.0028 vs conto flat (il TP di MR02_BTC fillato
dallo spike reale delle 12:22Z, che il worker riconcilierà alla chiusura sim) —
esattamente la classe di divergenza che prima restava invisibile per ore.
## Altri esiti della tornata
- **Dedup engine gate** (TODO chiuso): `_port06_gate_common.py`, output 3 gate
byte-identici, nessun copy-drift trovato (la paura era fondata ma non ancora avverata).
- **Bug bfill `_daily_equity` quantificato** (TODO aggiornato): NON materiale — OOS
invariato per costruzione, FULL DD 3.46→3.67 col fix (l'attuale è lievemente ottimista),
nessun verdetto di gate a rischio. Lasciato documentato.
- **Drift monitor in produzione**: `drift_monitor.py` in crontab host (07:15 UTC,
Telegram): rolling 60/120g per famiglia vs distribuzione storica propria, warn < p5.
Oggi: FADE 120g al p2 (coda storica, nessun intervento), resto normale, XSEC p84.
- **Dati cerbero-bite refreshati**: catena opzioni a ~153k righe/asset (fino a oggi
19:30), pannello regime denso (net-GEX 644/673 ultimi 7g). Sempre un solo regime:
niente validazione edge, valore forward.
+74
View File
@@ -0,0 +1,74 @@
# 2026-06-12 — ACCEL50: cosa accelera davvero verso €50/giorno
**Domanda.** Quali strategie/leve accorciano il tempo per arrivare a €50/g da ~€2k?
Script: `scripts/analysis/accel50_research.py`.
## Il quadro onesto
A €2k, anche col CAGR OOS del PORT06 (~111% a lev 2), il PnL atteso è ~€4/g: il
collo di bottiglia NON è l'edge (Sharpe OOS 10), è la **taglia**. Le vie testate,
in ordine di impatto:
## 1. LEVA — l'acceleratore dominante (nessuna ricerca nuova)
Frontiera su daily return canonici PORT06 (scala lineare, fee pro-quota):
| lev | CAGR full | DD full | CAGR OOS | DD OOS | anni a €50/g da 2k |
|-----|-----------|---------|----------|--------|--------------------|
| 2 (attuale) | 74% | 3.5% | 111% | 1.5% | 3.3 |
| 3 | 128% | 5.2% | 206% | 2.2% | 1.9 |
| 4 | 200% | 6.9% | 343% | 2.9% | 1.2 |
| 5 | 293% | 8.6% | 539% | 3.7% | 0.9 |
Anche scontando l'OOS del 50% (regime calmo, caveat noto), lev 3-4 dimezza i
tempi tenendo il DD sotto il 10%. Caveat: il modello è lineare — non cattura
margine, code grasse, slippage che cresce col notional, e gli sleeve senza stop
(PAIRS/SH01) a leva alta hanno code peggiori del modello. Proposta sobria:
**lev 2→3 subito** (DD full 5.2% = ancora metà del PORT02 storico), rivalutare
4 dopo un mese di ledger reale pulito.
## 2. FADE 15m — la candidata nuova che PASSA il probe
MR01/02/07 a 15m, parametri live 1h non ri-tunati (anti-overfit), fee 0.10% RT:
- **Tutti e 6 gli sleeve positivi**, OOS 2025-26 positivo ovunque, fee 2x OK
(Sh 1.6-2.9 — margine ampio).
- **BTC 15m domina il suo 1h**: MR01 Sh 3.37 vs 2.76 con META' del DD
(15.1% vs 31.7%); MR02 3.49 vs 3.29 (DD 10.8 vs 18.7).
- ETH 15m leggermente sotto il 1h in Sharpe ma OOS molto più grande in valore
assoluto (4x trade = compounding più veloce; MR02 +22052 vs +9560).
- Infrastruttura live 15m GIA' esistente (fetch sub-orario del BLEND pairs).
**Prossimi passi obbligati prima del deploy** (metodologia standard):
gate PORT06 con correlazione 15m↔1h (se ~1 sostituire, se bassa aggiungere),
griglia parametri al 15m, validazione worker, caveat flat ETH 15m (14-30%
storico — per le fade single-leg il fill flat è meno tossico che per i pairs,
ma va guardato con flat-aware engine).
## 3. PAIRS nuove — BOCCIATE (stale-print illusion, di nuovo)
Sweep onesto delle 19 coppie mai testate (config universale pre-registrata
n=50 z2.0/0.5 max72): 8 candidate con Sh 1.5-4.3... MA le gambe alt hanno
88-98% barre flat (ADA 98%, LTC 97%, DOGE 91%, XRP 88%, BNB 88%) e con
`flat_skip=True` muoiono quasi tutte (BTC/ADA 4.33→0.17, ETH/DOGE 3.79→0.46;
migliore superstite ETH/XRP 1.34 < le 5 deployate). Identica classe di
illusione del XEX su DOGE/SOL (stessa giornata, `xex_divergence_research.py`).
**PAXG idem**: 92% flat su Deribit → chiuso anche il ramo "oro".
NB: questo getta anche una luce nuova sulle gambe alt dei pairs GIA' deployati
(ADA/LTC/SOL hanno flat share altissime nel parquet) — il loro ledger reale
shadow è il banco di prova giusto e finora regge, ma teniamolo d'occhio.
## 4. CAPITALE — domina tutto
A config attuale: €50/g ≈ €24k di capitale. Ogni € aggiunto accorcia
linearmente; nessuna ricerca batte un deposito. In più, a ~€20k si sbloccano
in esecuzione reale i 4 book multi-asset oggi solo paper (TR01/ROT02/TSM01/XS01).
## Sintesi operativa
Ordine d'impatto: **capitale > leva (2→3) > fade 15m (da gateare) >>** tutto il
resto. Le vie "nuova strategia esotica" (pairs nuove, PAXG, XEX) sono tutte
morte oggi sotto il test di esecuzione realistica — la lezione del giorno è che
su questo testnet ogni edge va validato col **book/flat-aware engine** prima di
crederci.
@@ -0,0 +1,57 @@
# 2026-06-12 — FADE TF SWEEP: 1m / 2m / 5m / 10m / 30m (post-swap 15m)
Richiesta utente: estendere l'analisi timeframe dei fade oltre il 15m appena
deployato (v1.1.30). Script: `scripts/analysis/fade_tf_sweep.py`.
Dati: parquet locale (5m/15m/30m full-history; 10m = resample dal 5m, unit-safe);
1m/2m da Cerbero (120 giorni recenti — la storia 1m locale non esiste: esclusa
dal refresh notturno per costo, 2m/10m non sono intervalli nativi del v2).
## A. Storia completa (engine canonico, OOS da 2024-10, fee 0.10% RT)
OOS Sharpe per timeframe (e OOS Sharpe a fee 2x del peggiore):
| tf | MR01_BTC | MR02_BTC | MR07_BTC | MR01_ETH | MR02_ETH | MR07_ETH | worst f2x |
|----|---------|---------|---------|---------|---------|---------|-----------|
| 5m | 3.66 | 1.90 | 4.12 | 5.31 | 6.54 | 5.52 | **MR02_BTC 1.70** |
| 10m | 2.62 | 2.69 | 3.31 | 5.32 | 6.49 | 5.59 | MR02_BTC 0.32 |
| 15m (live) | 1.94 | 2.30 | 2.37 | 4.94 | 6.40 | 4.44 | MR02_BTC 0.60 |
| 30m | 1.35 | 2.32 | 1.56 | 3.25 | 5.23 | 2.81 | MR02_BTC 1.40 |
**La frontiera è monotona**: più il tf scende, più Sharpe sale (MR01/MR07)… e più
il margine fee si assottiglia. A fee 2x MR02_BTC muore a 5m e resta fragile a 10m.
MR02 (donchian) fa 3-6x i trade degli altri: è la strategia più esposta al churn.
## B. Finestra comune recente (2026-02-12 → 06-12, il regime CORRENTE)
- **MR02 sotto i 15m è un disastro**: 1m 64%, 2m 44%, 5m 22% (fee-death).
- **MR01 a 1m brilla** (ETH +60.6%, Sh 5.7; BTC +33.5%) ma **muore a fee 2x**
(unico sopravvissuto MR01_ETH +16.5%): margine troppo sottile per fidarsi.
- Flat share a 1m: ETH 25.6%, BTC 13.3% → rischio stale-print alto (la lezione
del giorno: pairs-alt/XEX/PAXG).
- Il regime recente è CALMO: anche il 5m vi è fiacco (+4.8/22.9/+3.7 BTC).
I tf veloci pagano nella volatilità, non nella calma — il loro vantaggio
full-history viene dai regimi mossi (2021-22, 2024).
## C. Correlazione col 15m live (daily, storia completa)
5m↔15m media **0.46**, 10m↔15m media **0.53** (range 0.28-0.81). Diversificazione
parziale: un eventuale ADD del 10m avrebbe senso ma è meno pulito del salto
1h→15m (che era a 0.26).
## Verdetto
- **1m / 2m: CHIUSI.** Fee-margin nullo a stress, microstruttura flat pesante,
validazione full-history impraticabile. Non deployare mai MR02 sotto i 15m.
- **5m: no-swap.** L'edge c'è ma MR02_BTC muore a fee 2x — viola il criterio di
robustezza fee che tutte le strategie deployate rispettano.
- **10m: in WATCHLIST.** Quasi l'edge del 5m con più margine (f2x 0.32 resta
sotto la soglia di comfort per MR02_BTC; MR01/MR07 reggono bene). Possibile
ADD selettivo (solo MR01/MR07?) da gateare su PORT06 più avanti — NON ora:
il 15m è live da poche ore, un cambio alla volta e si lascia parlare il
ledger reale.
- **15m: confermato** come ginocchio della frontiera margine-fee/rendimento.
Collaterale tecnico: bug di resample scoperto e fixato nello sweep — pandas 2.x
conserva `datetime64[ms]` da `to_datetime(unit="ms")`, quindi `.view(int64)//10**6`
divide due volte e manda i timestamp nel 1970 (equity piatta silenziosa). Usare
`(index - EPOCH) // pd.Timedelta(milliseconds=1)`.
+78
View File
@@ -0,0 +1,78 @@
# 2026-06-12 — GATE PORT06: fade 15m PROMOSSI (tutte e tre le varianti)
Seguito del probe ACCEL50: i 6 sleeve fade (MR01/02/07 × BTC/ETH) a 15m,
parametri live 1h NON ri-tunati, engine canonico `build_trades`/`fade_daily_equity`
parametrizzato sul timeframe. Script: `scripts/analysis/fade15m_port06_gate.py`.
## Risultati
**[1] Parità.** Builder locale a 1h == sleeve canonico, diff 0.00 esatto su tutti e 6.
**[2] Standalone (daily, pos 0.15 lev 3, fee 0.10% RT).** Il 15m batte il twin 1h
quasi ovunque; OOS Sharpe: MR01_ETH 4.94 (vs 1.10), MR02_ETH 6.40 (vs 4.72),
MR07_ETH 4.44 (vs 1.97), MR07_BTC 2.37 (vs 1.59). Fee 2x: regge ovunque tranne
MR02_BTC (OOS 0.60 — il piu' fee-sensitive, ma a fee reali e' il migliore BTC).
**[3] Correlazione 15m↔1h: media 0.26** (range 0.16-0.41) — più decorrelato del
pairs 15m promosso a 0.37. NON è la stessa scommessa più veloce: è un edge a
orizzonte diverso.
**[4] Gate PORT06** (cap PAIRS 0.33 / SHAPE 0.0588):
| variante | FULL CAGR | FULL DD | FULL Sh | OOS CAGR | OOS DD | OOS Sh |
|----------|-----------|---------|---------|----------|--------|--------|
| BASELINE (1h) | 74% | 3.46% | 7.34 | 111% | 1.48% | 10.07 |
| **ADD (+6 sleeve 15m)** | 85% | **2.73%** | 8.02 | 125% | **1.38%** | **10.48** |
| SWAP (15m al posto 1h) | **101%** | 2.47% | 8.13 | **163%** | 2.09% | 10.86 |
| BLEND 50/50 | 87% | 2.31% | 8.06 | 136% | 1.77% | 10.82 |
Tutte e tre PROMOSSE dal criterio standard (OOS Sharpe non peggiora E DD scende).
**[5] Anti-illusione flat (il check che ha ucciso pairs-alt e XEX/DOGE).**
ETH 15m ha 14-30%/anno di candele flat. Con flat-entry-skip l'edge SOPRAVVIVE:
MR01_ETH Sh 3.11→2.85 (OOS 4.94→4.60), MR02_ETH 4.72→4.63, MR07_ETH 3.76→3.55;
BTC invariato (≤0.01). Coerente col finding registrato 2026-05-28. La quota di
ritorno persa (~30-40% del FULL ETH) è il caveat slippage onesto: il ledger
reale shadow sarà il giudice, come per il pairs 15m.
## Raccomandazione
**ADD** è la promozione più pulita: migliora TUTTE le metriche senza buttare il
track record live dei 6 sleeve 1h (SWAP rinuncia alla config validata dal vivo;
il suo OOS DD peggiora 1.48→2.09). In subordine BLEND (miglior FULL DD 2.31%).
## Per il deploy (non fatto qui)
1. `_defs.py`: 6 SleeveSpec nuovi `MR0x_{asset}_15M` con `tf: 15m` nei params.
2. Runner: estendere il fetch sub-orario (già esistente per PR_ETHBTC_15M) ai
fade 15m; verificare lookback (EMA200 a 15m = 50h → bastano ~2200 barre).
3. Validazione worker a 15m (replay == backtest, come validate_worker_pairs).
4. Sizing: a peso pieno i 6 sleeve 15m raddoppiano la famiglia FADE; valutare
position_size ridotto come il blend pairs (il gate ADD qui è già a peso pieno
e migliora comunque, ma il caveat slippage 15m suggerisce prudenza).
5. MR02_BTC 15m è il più fee-sensitive (fee2x OOS 0.60): monitorare le fee reali.
## Addendum — DEPLOY: SWAP SECCO (stessa giornata, scelta utente)
L'utente ha scelto **SWAP** (la variante a CAGR più alto), consapevole dei due
costi discussi: OOS DD 1.48→2.09% e perdita della maturità operativa 1h (la
meccanica live — EXIT-16 confirm, TP_PHANTOM, TP resting — gira 4x più veloce
su un feed testnet con wick anomali; il gate misura l'edge, non la robustezza
operativa). Proposta alternativa staged (ADD temporaneo 1-2 settimane, poi
spegnere il 1h) declinata: "swap secco".
Implementazione (deliberatamente minima, sid INVARIATI → pesi/alloc/epoche
intatti):
- `_defs.py`: FADE specs → `tf="15m"` (DIP01 resta 1h: non era nel gate).
- `combine_portfolio.py`: `FADE_TF="15m"` nel builder canonico → le due facce
(backtest/live) restano sulla STESSA definizione; il lock di parità
(`test_backtest_parity`) confronta le facce fra loro e resta verde.
- Runner: NESSUNA modifica (il fetch sub-orario BTC/ETH 15m esisteva già per il
blend pairs; `_spec_assets_tf`/`_series_for` sono generici).
- Worker: nuove dir `*__15m` (stato 1h preservato in `*__1h`, worker flat al
momento dello switch). Esecuzione reale invariata (match per nome MR01/02/07).
- max_bars=24 barre ora = 6h; EXIT-16 confirm sulla barra 15m completata.
Monitor post-deploy: stop-rate e divergenza sim/reale dei fade 15m
nell'hourly report; fee reali su MR02_BTC; STALE_FEED (le barre flat 15m
sono più frequenti che a 1h).
@@ -0,0 +1,51 @@
# 2026-06-12 — Sweep migliorie/strategie nuove (sera, post-swap 15m)
Richiesta: cercare altre migliorie da implementare o strategie nuove. Tre
esperimenti + una miglioria di codice.
## 1. INIT_LINEAGE — eredità capitale al cambio timeframe (IMPLEMENTATO)
`StrategyWorker._inherit_lineage_capital`: al primo avvio (niente status.json)
il worker eredita `capital`/`real_capital` dal worker più recente di stessa
strategia+asset su altro tf (glob `{strategy}__{asset}__*`). MAI la posizione.
Nato dallo swap 1h→15m di oggi: i worker nuovi partivano dall'allocazione del
pool scartando il PnL del gemello (16.8 di equity fantasma, riallineata a mano
col seed). Il prossimo swap non avrà bisogno di seed manuale.
Test: `tests/portfolio/test_capital_lineage.py` (eredita / no-sibling / resume).
## 2. Gate 10m ADD selettivo (MR01/MR07) — BOCCIATO, watchlist chiusa
Baseline aggiornata al PORT06 post-swap (fade 15m): ADD di 4 sleeve 10m
(MR01/MR07 × BTC/ETH, MR02 escluso perché fee-fragile) dà FULL Sh 8.13→8.33 e
DD 2.47→2.28, ma **OOS Sharpe 10.86→10.76** → fallisce il criterio standard.
Il 15m cattura già quasi tutto l'alpha veloce (corr 10m↔15m 0.53). Chiuso.
## 3. XSEC breadth (universo 8 → 14/15) — direzione GIUSTA, venue SBAGLIATO
La breadth è la leva classica delle strategie cross-sectional. Due banchi:
- **Hyperliquid 15 coin** (dati REALI, ma profondità v2 limitata a ~207 giorni,
regime calmo recente): CORE-8 **6.2%** (Sh 1.38, coerente col
dispersion-gate live che tiene XS01 fuori in questo regime) vs FULL-15
**+9.4%** (Sh 1.58, WR 41→51%). La breadth trasforma un book perdente in
vincente — **sui prezzi veri**.
- **Deribit 14 coin** (storia piena 2022-10→2026-06, +AVAX/DOT/TRX/LINK/BCH/UNI,
parity del sim verificata ESATTA vs `xsec_sim`): FULL Sh 1.48→**1.22**, OOS
4.66→**3.41**, fee 2x da 0.33 a **1.45**. PEGGIO dell'8: le 6 gambe nuove
hanno chiusure flat al **91-99%** e il loro "momentum" è rumore stale che
corrompe il ranking cross-section (3ª conferma della lezione del giorno:
pairs-alt, XEX/DOGE, ora XSEC).
**Conclusione:** l'espansione dell'universo XS01 è promettente ma bloccata dalla
qualità dati del venue. Sblocco strategico: **routing dati Hyperliquid** nel
runner (il client v2 già supporta `exchange="hyperliquid"`) + accumulo storia
HL in avanti. Rivalutare quando HL avrà ≥12-18 mesi di storia utile.
## Direzioni aperte residue (non attaccate oggi)
- **Put settimanale standing** (catastrofe-cap): unica struttura opzioni
eseguibile, da gateare coi premi reali cerbero-bite (~1%/mese il 10% OTM).
Harness `option_overlay_lab.py` pronto.
- **Hyperliquid come venue di esecuzione** (oltre che dati): aprirebbe fades su
alt liquidi con fill realistici; lavoro infrastrutturale grosso.
- 10m fade: chiuso oggi. 1m/2m/5m: chiusi oggi. Pairs nuove/PAXG/XEX: chiusi oggi.
@@ -0,0 +1,108 @@
# 2026-06-12 — Reconcile resting + guard feed↔book + epoca report (v1.1.27)
## Contesto: la notte ha validato (di nuovo) il tema osservabilità
Due eventi nelle 24h hanno motivato la tornata:
1. **MR02_BTC (06:00):** il TP resting LIMIT ha fillato sul **book reale** a 60481
nella notte (+6.55 USD reali) mentre il **feed sim** non è mai sceso sotto 63285
(443 bps di divergenza, sim chiuso a time_limit con 1.25). Il disaster-SL è
risultato `order_not_found` alla cancel (probabile auto-cancel a posizione flat).
Caso SPECULARE del TP_PHANTOM: lì il feed stampa wick che il book non ha; qui il
book si muove e il feed non lo vede. Real-truth ha bookato il reale (corretto),
ma il sistema se n'è accorto solo ORE dopo, al close sim.
2. **Incidente venue (dalle ~09:47):** il conto Deribit testnet ha iniziato a
rifiutare ordini (`locked_by_admin`, `invalid_reduce_only_order`) e le posizioni
sul conto sono cambiate da sole (ETH LONG 0.141 con avg 1849.5 — prezzo vecchio
di settimane): **rollback/reset admin del testnet**, non un nostro bug. Difese OK:
`REAL_CLOSE_PARTIAL`, leg-fail con unwind, orfani registrati (ETH 0.026,
SOL +0.7), `sim_fallback` dichiarato. MA: il reconciler delle 11:40 è **morto in
silenzio** su un 502 — il guardiano che non suona è indistinguibile dal tutto-ok.
## Fatto
### 1. Reconcile degli ordini RESTING (cron :40, read-only)
- **cerbero-mcp 9a74052:** nuovo endpoint Deribit `get_open_orders`
(`private/get_open_orders_by_currency`; per i trigger untriggered interrogare
anche `type='trigger_all'` e merge per order_id). Pattern già presente per
Hyperliquid/IBKR. Rebuild + smoke su testnet (book vuoto == stato atteso).
- **`books.expected_resting()`:** TP/DSL attesi dai libri dei worker single-leg
in posizione reale (i pairs non hanno resting).
- **`reconcile_account.compute_resting_drift()`** — tre classi:
`FILLED_UNBOOKED` (atteso non in book + fill nel trade history col worker ancora
in posizione = il caso MR02), `MISSING` (atteso, non in book, zero fill — per il
DSL triggered il fill ha order_id NUOVO → appare qui e il drift posizioni
completa), `STALE` (in book con label di un nostro worker ma nessun libro:
fillerebbe a sorpresa). Anti-race come per le posizioni; alert `RESTING_DRIFT`.
- **`RECONCILE_FAIL`:** il main è ora guardato — su eccezione alert Telegram +
exit 2 (fix del crash silenzioso delle 11:40).
### 2. Guard FEED_BOOK_GAP nel runner
`_check_feed_book_gap`: ad ogni poll confronta il close della candela in corso col
**mark dello strumento d'ESECUZIONE** (USDC, `get_ticker_batch`, 1 chiamata);
oltre soglia (`overrides.feed_book_gap_bps`, default 150) → alert per episodio,
recovery con isteresi a soglia/2, fail-open su errori di rete. Le decisioni
restano sul feed (il sim è la verità che guida): questo dice solo QUANDO i fill
reali possono divergere dal sim. Avrebbe segnalato il caso MR02 in tempo reale.
### 3. Epoca v1.1.26 nell'hourly_report
I conteggi CHIUSI cumulativi erano inquinati dal churn TP-fantasma dell'11-06
17:3217:58 (~24 giri pre-fix). Nuova riga `epoca v1.1.26+ (TP_PHANTOM attivo)`
da `EPOCH_V1126 = 2026-06-11T21:40` (deploy del gate): oggi legge **7/1** contro
il 18/17 storico — è il numero da confrontare col backtest da qui in poi.
## Test e deploy
`pytest` 121 passed (114 + 7 nuovi: `test_reconcile_resting.py`,
`test_feed_book_gap.py`). Deploy **v1.1.27** (runner nel container; reconciler e
hourly_report girano da host → già attivi). cerbero-mcp rebuildato (endpoint).
## Aperto / lezioni
- **Drift ETH +0.167 non spiegato** dal rollback testnet: quando il conto si
sblocca serve `reset_flatten` + riallineamento ledger (decisione utente).
- I **residui dei close single-leg falliti** (`REAL_CLOSE_PARTIAL`, es. MR07
0.102) NON finiscono in `orphan_legs` → il reconciler li vede come drift NON
spiegato. Estendere la registrazione orfani anche al single-leg è il naturale
passo successivo (punto in analisi sviluppi).
- Il testnet può **riscrivere il conto sotto i piedi** (lock + rollback): nessuna
invariante "il conto cambia solo per nostri ordini" è affidabile. Il reconciler
orario è l'unico rilevatore; valutare cadenza più fitta in presenza di
posizioni reali aperte.
## Addendum (pomeriggio, v1.1.28): orfani single-leg + circuit-breaker venue-lock
I due punti promossi dall'analisi sviluppi, entrambi figli dell'incidente di oggi:
1. **Orfani anche per i close single-leg.** `REAL_CLOSE_PARTIAL` ora registra la
quota residua in `StrategyWorker.orphan_legs` (stessa semantica dei pairs,
persistita in status.json, letta da `books.real_books`) → il reconciler vede
il drift come SPIEGATO invece che anomalo (il caso MR07 0.102 ETH delle 09:47
restava invisibile ai libri). Inoltre `_close_position` ora persiste lo stato
a fine booking (prima solo al save del tick: un crash li' in mezzo perdeva
capital e orfano). NB: nessun save a meta' chiusura — con `real_in_position`
ancora true l'orfano conterebbe doppio nei libri.
2. **Circuit-breaker venue-lock in `ExecutionClient`.** Dopo `lock_trip=3` reject
'locked' consecutivi (es. `locked_by_admin` di oggi) le APERTURE sono sospese
senza toccare l'API (Fill failed → path REAL_OPEN_FAIL/sim_fallback esistente;
per i pairs entrambe le gambe rifiutate localmente → niente leg parziali da
unwindare, niente fee sprecate). Le CHIUSURE si tentano SEMPRE (path gia'
sicuro: partial/orphan/netting) e un loro reject 'locked' rinfresca il
cooldown. Riarmo: dopo `lock_cooldown_s=900` la prossima apertura fa da probe.
Alert `VENUE_LOCK` al trip + RIENTRATO al primo ordine accettato.
Test: `test_single_leg_orphan.py`, `test_venue_lock_breaker.py` (126 passed).
## Chiusura incidente (13:05): testnet sbloccato, conto riallineato
Sequenza eseguita a venue sbloccato: stop runner → `reset_flatten` (chiusi i due
artefatti del rollback: ETH long 0.141 @1664.25, SOL long 0.7 @66.651, conto FLAT)
→ svuotati gli `orphan_legs` di ETH_SOL (le quote orfane non esistono piu' sul
conto post-flatten; edit a container fermo, altrimenti il worker li riscrive
dalla memoria) → restart. **Reconciler: OK — conto allineato ai libri** (posizioni
e resting), runner healthy, log puliti. Il ledger NON e' stato toccato: i booking
dell'incidente erano gia' dichiarati (`sim_fallback`/real) e l'equity (€2020.82)
resta la verita' contabile del paper.
@@ -0,0 +1,64 @@
# 2026-06-12 — XEX: discordanze Deribit testnet vs Hyperliquid
**Domanda.** I prezzi Deribit (testnet, dove eseguiamo) e Hyperliquid (feed
realistico, proxy della realtà) divergono? La divergenza è usabile per fare trade?
**Strumenti comuni validati:** 9 base-coin (ADA, AVAX, BNB, BTC, DOGE, ETH, PAXG,
SOL, TRUMP). Analisi su BTC/ETH (inverse, liquidi) + SOL/DOGE (lineari USDC).
Script: `scripts/analysis/xex_divergence_research.py`.
## Misure (1h, 2026-03-01 → 2026-06-12, ~2470 barre)
| Coin | spread medio | std | half-life | flat Deribit | ΔbookHL live |
|------|--------------|-----|-----------|--------------|----------------|
| BTC | 0.29% | 1.25% | 7.3h | 6% | **0.97%** |
| ETH | 0.55% | 1.33% | 5.1h | 9% | **1.54…−2.16%** |
| SOL | 0.38% | 3.60% | 5.2h | 35% | 0.05% (allineato) |
| DOGE | +0.34% | 1.88% | 4.2h | 87% | +0.16% (allineato) |
Per standard mainnet questi spread sono enormi (reale <0.05%): la divergenza è
l'artefatto del feed testnet che periodicamente si stacca dalla realtà e rientra.
## Findings
1. **Lo spread è mean-reverting e il gap viene chiuso ANCHE da Deribit** (il lato
che possiamo tradare): beta del ritorno futuro Deribit sullo spread negativo e
crescente con l'orizzonte (ETH 0.36, BTC 0.23 a 24h). Non è solo HL che si
muove.
2. **Trappola smascherata — stale print arb.** Il backtest su DOGE dava Sharpe
6.7 (OOS 9.1!) e SOL 2.7, MA il book live di entrambi sta attaccato a HL
(±0.16%) mentre i print restano vecchi (DOGE 87% barre flat): l'edge era
**finzione** — i fill reali avverrebbero al prezzo vero, non al print stantio.
Stessa classe di illusione del look-ahead squeeze: il backtest compra un
prezzo a cui nessuno fa fill.
3. **Su BTC/ETH inverse la dislocazione è REALE: è il book stesso a essere
spostato** (misurato live: bid/ask 0.97% / 1.54…−2.16% sotto HL con depth
>$1M sui primi 5 livelli, spread bid/ask 1-3 bps). Lì si può davvero comprare
sotto/vendere sopra la realtà.
4. **Edge netto moderato e timing-sensitive** (fee 0.10% RT, entry |s|≥1%, exit
|s|≤0.25% o 24 barre): BTC FULL Sh 1.0 / OOS 2.05; ETH FULL 2.28 / OOS 1.37.
Con entry ritardato di 1 barra (stress staleness) BTC FULL→0, ETH OOS→0.1:
con half-life di 3-7 ore, un'ora di ritardo erode quasi tutto. Un'eventuale
implementazione live deve leggere il **book in tempo reale** (non il close 1h)
— a quel punto il lag reale è minuti, non un'ora, e l'edge eseguibile sta fra
lag0 e lag1.
## Segnale live al momento dell'analisi
ETH book Deribit **2.16%** sotto HL (z=1.7, 6.7° percentile storico), BTC
0.97%: entrambi in zona "long Deribit" per il fade dello spread. (Solo
osservazione: nessun deploy senza gate.)
## Verdetto e prossimi passi
- Discordanza tradabile trovata: **fade dello spread D/H su BTC/ETH-PERPETUAL,
segnale dal book, soglia ~1%, exit a convergenza o 24h**. Da NON estendere a
DOGE/SOL (illusione stale-print).
- È un **edge di testnet** (non trasferibile a mainnet): legittimo per il
paper/shadow corrente, ma va dichiarato per quello che è.
- Prima di qualsiasi deploy, metodologia standard: gate PORT06 (correlazione con
le fade esistenti, che già fadano in parte questi stessi spike testnet —
rischio doppio conteggio), robustezza griglia, validazione worker.
Script: `scripts/analysis/xex_divergence_research.py` (fetch v2 due exchange,
tabella convergenza, backtest lag0/lag1, book reality-check live).
+40
View File
@@ -0,0 +1,40 @@
# 2026-06-13 — Dashboard web PORT06 (stato live + PnL + grafici + trade)
Richiesta utente: frontend per visualizzare lo stato con PnL totale e per-strategia,
grafici, e liste trade (attivi in tempo reale + chiusi).
## Cosa
`src/live/dashboard.py` — server **stdlib `http.server`** (zero nuove dipendenze),
legge i file `data/` e serve:
- `GET /api/state` → JSON con tutto lo stato calcolato
- `GET /` → single-page HTML (vanilla JS, polling ogni 5s)
Contenuto della pagina:
- **KPI**: equity, PnL totale (€ e %), max DD, peak
- **Grafico equity** (Chart.js da CDN, fallback testuale se offline) dalla
`equity.jsonl` del ledger (downsample a 400 punti)
- **PnL per strategia** (barre verdi/rosse): realizzato netto fee = Σ `pnl` reali
dai CLOSE (REAL-TRUTH), n trade, win-rate, capitale; tag `paper` per i
multi-asset non eseguiti, `•aperta` se in posizione
- **Trade attivi in tempo reale**: lato, entry, **mark corrente** (Cerbero
best-effort, cache 20s), **PnL non realizzato** (€ e %, da `real_entry_notional`),
barre/max_bars, distanza al TP, età dello status (⚠ se >15min = stantio)
- **Trade chiusi** (ultimi 50): ora, strategia, motivo, PnL reale, sim, esito
## Deploy
Servizio docker-compose `dashboard` (stessa immagine del runner, monta gli stessi
`data/`, porta **8787**), `restart: unless-stopped` + healthcheck sull'API.
Accesso: `http://<host>:8787`. **Nessuna auth** → solo rete interna/VPN, non
esporre pubblicamente. Avvio: `docker compose up -d --build dashboard` (il runner
non viene toccato).
uv run python -m src.live.dashboard --port 8787 # anche standalone su host
## Note
- Il PnL per-strategia usa il PnL REALE (real_truth), coerente col report orario.
- I 6 fade 1h ritirati dallo swap restano in lista (hanno storico CLOSE): flat,
mostrano il loro PnL realizzato storico accanto ai gemelli 15m attivi.
- Unrealized € sui pairs non mostrato (posizione a 2 gambe, z-based) → "pairs (z)".
@@ -0,0 +1,49 @@
# 2026-06-13 — Report ricorrente LEDGER REALE vs BACKTEST (il gate per scalare)
## Perché
Domanda dell'utente: "come cresco il capitale a 12k". Risposta: il prerequisito
prima di mettere soldi veri è che il **ledger reale combaci col backtest**
soprattutto lo slippage del 15m appena deployato. Questo report rende quel gate
un dato osservabile, non un'opinione.
Insight chiave: per gli sleeve eseguiti (6 fade 15m, DIP01, 6 pairs, SH01) il
**sim del worker == backtest canonico PER COSTRUZIONE** (validato). Quindi
"reale vs backtest" = "reale vs sim" = la **fuga di esecuzione**: slippage +
fee reali vs assunte + effetti netting/phantom/sim_fallback.
## Cosa misura
`scripts/analysis/ledger_vs_backtest.py` (read-only: solo trades.jsonl +
status.json, nessuna rete → affidabile in cron):
- PnL realizzato sim vs reale (Σ e per-trade) → **LEAKAGE** € e per-trade (bottom line)
- slippage ingressi (REAL_OPEN) e uscite-a-mercato (REAL_CLOSE; escluse le uscite
da TP resting, fill maker al livello = no slippage)
- fee reali vs assunte (0.10% RT)
- trade `sim_fallback` (reale mai eseguito/fillato) = quota NON coperta dal reale
- ledger per-sleeve: real_capital vs capital (sim)
- **verdetto** 🟢/🟡/🔴: <10 trade = campione piccolo; leakage basso+slippage ≤15bps
= verde (si può pensare a scalare); slippage >40bps = rosso (edge erode, NON scalare)
## Clean-start (importante)
Una finestra mobile pura includerebbe l'**incidente testnet pre-fix**: a 7g il
report dà sim +82 vs reale +5 (🔴) — ma è gonfiato dai +4% FANTASMA che il sim
bookava e il reale no, prima di TP_PHANTOM (v1.1.23), netting (v1.1.25) e
ribilancio-conservativo (v1.1.31). Lo scheduler usa **`--since 2026-06-13`** →
accumula SOLO dati post-fix, e diventa statisticamente significativo coi giorni.
Finestra pulita oggi: 1 trade, leakage +0.07, slippage ingresso 12-29 bps.
## Scheduling
Cron host (come reconcile/hourly_report), **giornaliero 08:30 UTC**:
`ledger_vs_backtest.py --since 2026-06-13 --telegram` → Telegram + log in
`~/port06_ledger_vs_backtest.log`. Invio verificato.
## Come usarlo
Quando il campione supera ~10-20 trade reali e il verdetto è 🟢 stabile per
qualche giorno (leakage per-trade piccolo, slippage medio ≤15 bps), allora il
15m regge l'esecuzione e si può passare da testnet a piccolo reale → poi scalare.
Se resta 🔴/🟡, l'edge si erode sui fill e NON va scalato: prima si capisce dove
perde (slippage ingressi? uscite a mercato? sim_fallback frequenti?).
@@ -0,0 +1,41 @@
# 2026-06-13 — Orfano da swap: incidente + guard nel reconciler
## Incidente
Lo `stato trades` del mattino ha scoperto una **posizione reale orfana**: il
worker fade **MR02_BTC 1h** aveva aperto uno short reale (0.0028 BTC @ 64135.5)
ieri alle 15:01; lo **swap a 15m (v1.1.30, ~20:48) lo ha rimosso dal config
mentre era ancora in posizione** → da allora nessun runner lo gestiva. Stato:
- conto Deribit: short 0.0028 BTC (il long di apertura del gemello 15m aveva
aperto e chiuso nettando via il resto)
- il **TP limit (63387.75) era sparito** dal book (cancellato durante il netting
della chiusura 15m) → short NUDO, protetto solo dal disaster-SL a +30%
- il **reconciler NON allarmava**: lo `status.json` del worker morto dichiarava
ancora `real_in_position: true` → conto == libri. Punto cieco: il reconciler
leggeva i libri dagli status ma non sapeva quali worker fossero VIVI.
Chiusura manuale (testnet): buy 0.0028 reduce-only @63766.5 (~+$0.85 netto sullo
short), cancel disaster-SL, worker marcato flat, PnL bookato (real_capital
181.18→182.03). Conto verificato flat su BTC; SH01_ETH short intatto.
## Guard implementato
`reconcile_account.compute_stale_real_positions(max_age_min=15)`: segnala i
worker che dichiarano `real_in_position` ma il cui `status.json` è fermo da
oltre 15 min. Un worker vivo riscrive lo status a ogni poll (~60s) → la
**staleness** è il discriminante robusto e venue-agnostico (cattura
ritirati-da-swap, crashati, worker rimossi dal config). Alert Telegram
`STALE_REAL_POSITION` (con `--telegram`), incluso nell'exit code e nel verdetto.
Gira già al prossimo cron host (:40) — nessun rebuild (lo script gira dal
working tree). Test: `tests/portfolio/test_reconcile_resting.py` (stantio
flaggato / fresco no / flat-vecchio no).
## Causa radice e direzione
La feature `INIT_LINEAGE` di ieri trasferisce il *capitale* al gemello del nuovo
timeframe, ma non la *posizione*. Il guard di oggi è la **rete di sicurezza**
(rileva e allarma entro un'ora). La **prevenzione** vera — flattare/consegnare la
posizione reale del worker ritirato al boot del runner — resta da implementare
(va fatta lato runner, con cautela: piazza ordini reali all'avvio). Per ora:
swap a conto fade-flat quando possibile, e il reconciler copre il resto.
@@ -0,0 +1,56 @@
# 2026-06-13 — Equity gonfiata dal ribilancio: +4.77 fantasma (causa + fix)
## Sintomo
Report "dall'ultimo reset": equity 2020.53 → 2025.80 (+5.27), ma un solo trade
chiuso (MR02_BTC 15m, reale +0.50). Il +4.77 residuo è comparso di colpo a
00:01:52 (primo poll dopo mezzanotte UTC = ribilancio giornaliero), senza alcuna
chiusura dietro.
## Causa — doppio conteggio del capitale degli in-position al ribilancio
`rebalance_allocations` faceva:
1. `total_capital = Σ capital di tutti i worker`
2. `alloc[sid] = peso × total_capital` per OGNI sid
3. ai worker in posizione NON applicava l'alloc (la posizione tiene il suo notional)
4. ai flat: `capital = alloc[sid]`
`update_equity` (ogni poll) = Σ capital. Dopo il ribilancio la somma diventa:
Σcapital = Σ(peso_i × total) [flat] + Σ(capital_j) [in-pos]
= total Σalloc_j + Σcapital_j
= total + Σ(capital_j alloc_j) ← inflazione
Cioè i flat si dividevano l'INTERO total (che includeva anche il capitale degli
in-position), e gli in-position lo tenevano in più → doppio conteggio.
Al ribilancio delle 00:01, **MR02_BTC 15m era in posizione** col capitale seedato
**181.19** (eredità del gemello 1h, INIT_LINEAGE di ieri) mentre la sua allocazione
era ~176.42 → **+4.77** di equity dal nulla. Lo scatenante è stato il seed (capitale
ben sopra l'allocazione media) combinato con la posizione aperta al ribilancio; ma
il bug era latente da sempre (ogni worker in-pos che devia dall'alloc gonfiava/sgonfiava
l'equity al ribilancio successivo).
## Fix — conservazione (ledger.allocate `reserved`)
`allocate(weights, reserved={sid: capitale})`: i worker in posizione TRATTENGONO il
loro capitale (deployato, non spostabile); i flat si dividono `total Σreserved`
per peso **rinormalizzato sui soli flat**. Così `Σalloc == total_capital` sempre →
**l'equity è conservata dal ribilancio** (un ribilancio sposta capitale fra sleeve,
non crea valore). `runner.rebalance_allocations` calcola `reserved` dai worker
`in_position` e lo passa. Senza `reserved` (default) = comportamento storico
(corretto solo a worker tutti flat, es. allocazione iniziale).
Test: `tests/portfolio/test_ledger.py::test_alloc_conserves_total_with_reserved`.
## Correzione dello stato (rimozione del +4.77 già accumulato)
Equity riportata a **2021.03** = baseline post-seed verificata (2020.53) + unico
trade reale (MR02_BTC 15m +0.50). Capitali dei worker del pool scalati ×
(2021.03/2025.79) e peak del ledger resettato a 2021.03 (il 2025.80 non è mai
stato un massimo legittimo → niente DD fantasma). L'orfano MR02_BTC 1h chiuso a
parte (+0.85) resta sul worker ritirato, fuori dal pool.
## Esito
Equity = soldi veri: dal reset il trading ha prodotto **+0.50** (1 trade), non +5.27.
Deploy con codice fixato (rebuild) così il prossimo ribilancio conserva.
@@ -0,0 +1,149 @@
# 2026-06-14 — Timing sweep: PAIRS & HONEST su 5/10/15/30m (vs live)
## Domanda (goal utente)
Dopo lo swap fade 1h→15m (v1.1.30), i **pairs** e le **honest** beneficiano dello stesso
trattamento — girare su timeframe più veloci (5/10/15/30 min)?
Script: `scripts/analysis/timing_sweep_pairs_honest.py` (riusa i motori canonici
`pairs_sim_flat`, replica DIP intrabar verificata == `dip_market_gated(market_n=0)`, gate
PORT06 == `pairs30m_gate`/`dip01`). Niente re-tuning dei parametri al cambio TF (anti-overfit,
come lo swap fade). Tutto netto, leva 3x, OOS held-out (da 2024-10).
## Vincolo dati (hard — definisce lo scope)
Solo **BTC/ETH** hanno 5m/15m/30m in locale (10m = resample causale da 5m). **TUTTI gli alt**
(ADA/BNB/DOGE/LTC/SOL/XRP) sono **SOLO 1h**. Quindi:
- **PAIRS:** solo **ETH/BTC** è sweepabile sub-orario. Gli altri 4 pair (gambe alt: LTC/ETH,
ADA/ETH, BTC/LTC, ETH/SOL) restano 1h per sempre senza dati alt sub-orari.
- **HONEST:** solo **DIP01** (BTC, mean-reversion) ha senso + dati. **TR01** (trend EMA20/100 su
4h, basket alt) e **ROT02** (rotazione dual-momentum 1d, momentum 60g, universo alt) sono lente
(orizzonte multi-giorno/mese) E multi-asset-su-alt → sub-orario **infattibile** (dati) e
**insensato** (momentum a 60 giorni su barre da 5 min). Nessuno sweep.
## Flat-share (print stale O=H=L=C, = rischio fill) — sale al scendere del TF
| asset | 5m | 10m | 15m | 30m | 1h |
|-------|----|-----|-----|-----|-----|
| BTC | 10.0% | 4.8% | 3.5% | 1.5% | 0.9% |
| ETH | **29.0%** | 17.3% | 16.4% | 9.0% | 6.7% |
ETH 5m al 29% è un allarme operativo serio. Nota: **30m ETH (9%) < 15m (16.4%)** → il 30m ha
*meno* rischio stale-print del 15m già live.
## PAIRS ETH/BTC — il vero free-lunch
Config UNIVERSALE 1h (n=50, z_in=2.0, z_exit=0.75, max_bars=72), flat_skip live-realizable:
| tf | Sharpe FULL | OOS Sh | OOS Sh fee2x | CAGR | win% |
|----|-------------|--------|--------------|------|------|
| 5m | 9.54 | 17.19 | 11.98 | 2071% | 56.0 |
| 10m | 9.26 | 16.54 | **13.03** | 1214% | 64.1 |
| 15m (live) | 8.10 | 14.40 | 11.74 | 673% | 66.7 |
| 30m | 6.20 | 11.17 | 9.59 | 312% | 71.7 |
| 1h (live) | 3.74 | 7.11 | 6.19 | 119% | 72.4 |
(I CAGR a quattro cifre sono compounding leva su singolo sleeve, irrealistici — il portafoglio
normalizza/cappa; il segnale robusto è lo **Sharpe** e il **gate PORT06**.)
- Sharpe **monotono** al scendere del TF, e **regge le fee 2x** (a differenza delle fade, dove
MR02 muore a 5m). Pairs market-neutral: lo spread rientra più spesso ad alta frequenza, e
ETH/BTC è la coppia più liquida.
- Corr daily fra TF: 5m↔1h 0.28, 10m↔1h 0.32, 30m↔1h 0.51; 10m↔15m 0.67 → i TF veloci sono
**fasi diverse dello stesso edge**, diversificanti, non duplicati.
**Gate PORT06 (add half-size al BLEND 1h+15m, OOS da 2024-10):**
| config | FULL Sh | FULL DD% | OOS Sh | OOS DD% | verdetto |
|--------|---------|----------|--------|---------|----------|
| ATTUALE (1h+15m) | 8.13 | 2.47 | 10.86 | 2.09 | — |
| +30m | 8.24 | 2.28 | 10.87 | 1.97 | **MIGLIORA** (già `pairs30m_gate`, mai deployato) |
| +10m | 8.44 | 2.28 | 11.04 | **1.92** | **MIGLIORA** (domina tutto) |
| +5m | 8.52 | 2.30 | 11.00 | 1.95 | **MIGLIORA** (FULL Sh max) |
Tutte e tre dominano su FULL Sh, FULL DD, OOS Sh, OOS DD. **10m** è il candidato più pulito
(miglior OOS DD, flat-share moderato 17%); **30m** è il più sicuro (flat-share 9% < 15m live).
## DIP01 (BTC) — più veloce, ma lo swap NON domina
Engine canonico (n=50, z_in=2.5, sl_atr=2.5, max_bars=24), parità 1h verificata:
| tf | OOS Sh BTC | OOS Sh fee2x BTC | corr↔1h | OOS Sh ETH |
|----|------------|------------------|---------|------------|
| 5m | 4.91 | 2.41 | 0.13 | 8.43 |
| 10m | 3.47 | 1.99 | 0.17 | 6.51 |
| 15m | 2.40 | 1.40 | 0.22 | 4.49 |
| 30m | 1.34 | **0.77** | 0.41 | 3.54 |
| 1h (live) | 1.69 | 1.36 | — | 2.68 |
Standalone Sharpe sale al scendere del TF e i TF veloci sono **fortemente diversificanti**
(corr 0.130.22 con l'1h). MA: **BTC fee-fragile** (30m f2x 0.77), e il **gate-swap non domina**:
| config | FULL Sh | FULL DD% | OOS Sh | OOS DD% |
|--------|---------|----------|--------|---------|
| DIP01 1h (live) | 8.13 | 2.47 | 10.86 | 2.09 |
| DIP01 30m | 8.16 | 2.27 | 10.78 | 2.03 |
| DIP01 15m | 8.16 | 2.30 | 10.82 | 2.14 |
| DIP01 10m | 8.31 | 2.49 | 10.85 | 2.15 |
| DIP01 5m | 8.46 | 2.27 | 11.02 | 2.16 |
Tutti migliorano i FULL ma **alzano l'OOS DD** (e 30m abbassa l'OOS Sharpe) → nessuno domina
sui 4 criteri. DIP01 5m dà il miglior OOS Sharpe (11.02) ma OOS DD 2.16 vs 2.09. La bassa corr
suggerirebbe un **ADD** (DIP01 5m come secondo sleeve diversificante) più che uno swap, ma
aumenta la concentrazione BTC-rev e non è il free-lunch netto che sono i pairs.
## Caveat (perché backtest ≠ via libera a scalare)
1. **Slippage su feed flat-alto** = il vero banco di prova, già flaggato per il 15m blend
("il vero banco di prova è il ledger reale shadow"). A 5m/10m è peggio (ETH 5m 29% flat).
Il backtest usa flat_skip (timing live-realizable) ma NON modella lo slippage attorno ai
print stali. → gateare via shadow ledger PRIMA di sizing, specie il 5m.
2. **OOS = un solo regime** (2024-10→, calmo). Lo Sharpe monotono-col-TF può essere in parte
un artefatto di regime calmo (la MR veloce prospera nel range a bassa vol; lo sweep fade
trovò "i tf veloci pagano nella volatilità, non nella calma"). I pairs sono market-neutral
→ meno esposti dei fade ai crash direzionali, ma il caveat resta.
## Verdetto del backtest (gate full + OOS 2024→)
- **PAIRS ETH/BTC: il gate diceva SÌ.** Estendere il BLEND con TF più veloci migliorava PORT06
su tutte le metriche (10m domina, 30m il più sicuro, 5m FULL Sh max), fee-2x robusto.
- **DIP01: marginale.** Più veloce è diversificante ma lo swap non domina e BTC è fee-fragile.
- **TR01/ROT02: NO.** Dati alt 1h-only + orizzonte lento → fuori scope per costruzione.
## Tentativo di deploy → due muri, NESSUN deploy (2026-06-14)
L'utente ha scelto di deployare un blend pairs ETH/BTC più veloce. Il wiring è stato fatto e
validato (sleeve def, backtest face, worker replay == backtest esatto, regression-lock
aggiornato) PRIMA di scoprire due blocchi che hanno ribaltato la decisione:
1. **MURO FEED (30m e 10m).** Il feed live **Cerbero v2 (`/mcp/tools/get_historical`) serve
solo 5m/15m/1h** — il 30m e il 10m danno 400 in OGNI formato (30m/30/30min/1800), e
l'endpoint legacy è 404 (rimosso). La voce `"30m"` in `runner._SUBHOURLY` era **speculativa,
mai testata live**. → un blend 30m/10m NON è drop-in: richiede di derivarlo resamplando il
feed 15m/5m nel runner (modifica al data-path live condiviso). **Lezione: il _SUBHOURLY del
runner ≠ ciò che il feed serve davvero — smoke-testare il fetch prima di promettere un TF.**
2. **MURO REGIME (5m).** Il 5m è l'unico TF veloce nativo (drop-in), ma il gate full-history
(Sharpe 9.54) è un **relitto dei regimi volatili vecchi**. Nel regime ATTUALE è il PEGGIORE
(ETH/BTC pairs half-size, ret%/Sharpe/DD):
| tf | ultimi 30g | ultimi 90g | ultimi 180g |
|----|-----------|-----------|------------|
| 5m | 10.5% / 11.5 / 11.6% | 2.0% / 0.2 / 14.1% | +78% / 2.93 / **19.5%** |
| 15m | 4.4% / 6.2 / 5.8% | 3.1% / 0.9 / 5.8% | +56% / 3.80 / 7.9% |
| 1h | 1.0% / 1.1 / 3.0% | +3.0% / +1.3 / 3.0% | +25% / **3.95** / 3.0% |
Il 5m perde su ogni finestra recente e ha **DD 3-6× il 1h**; persino sui 180g il suo Sharpe
(2.93) è SOTTO 15m/1h. Sommato al **flat-share ETH 5m 29%** (slippage reale, non modellato
dal backtest) e al fatto che girerebbe **reale**, il guadagno modesto di backtest (FULL
Sharpe +0.39) non vale il rischio. **5m: non conviene.**
**ESITO: nessun deploy. Config live invariata (19 sleeve, pairs 1h+15m).** I file tracciati
ripristinati a HEAD; restano solo questo diario + `timing_sweep_pairs_honest.py` come record.
**Lezione di metodo:** il gate PORT06 full+OOS è necessario ma non sufficiente — va incrociato
con (a) la fattibilità del FEED live e (b) il regime RECENTE. Un edge full-history può essere
un relitto di volatilità passata; "più veloce = più Sharpe storico" si rovescia in "più veloce
= più DD nel regime calmo attuale". Se in futuro si vorrà un blend più veloce: il **30m** resta
il migliore on-paper (flat 9%), ma serve prima derivarlo dal 15m nel runner + ri-verificarlo
nel regime del momento.
@@ -0,0 +1,85 @@
# 2026-06-15 — Gate del feed CONGELATO (freeze gate)
## Problema (da "stato ordini")
Audit degli ordini live: 5 sleeve in posizione reale, **4-5 contaminati dal feed ETH
congelato**. `ETH-PERPETUAL` (inverse, il feed di DECISIONE del runner) è fermo a
**1661.95 da 36h+** (100% barre flat, 57/57 nel windows 2g, **1 solo valore distinto**),
mentre il book reale dove si ESEGUE (`ETH_USDC-PERPETUAL`, lineare) si muove ~1717-1722.
Effetto misurato sui soldi reali (log container):
- **SH01_ETH** ha aperto short a sim 1661.95 / real 1717.10 (slippage 332 bps), chiuso a
time_limit con `sim_exit 1661.95` ma `real_fill 1719.21`**2.83 USD reali vs 0.09 sim**
→ e ha **riaperto subito** lo stesso short sulla trappola.
- I 4 pairs con gamba ETH (ETH/BTC 1h+15m, ETH/SOL, LTC/ETH, ADA/ETH) sono entrati con
z-score **estremi** (3.10, 2.96, +5.58, 2.56): artefatto MECCANICO del log-ratio con
ETH pinnato a 1661.95 mentre gli alt si muovono → falsa divergenza → carico di spread long-ETH.
È un GUASTO testnet, non di strategia: in mainnet l'arbitraggio tiene inverse ≈ lineare ≈ realtà.
## Decisione utente
> "congela gli sleeve ETH-leg finché il feed non si sblocca"
## Soluzione: gate del feed congelato nel runner (auto-guarente)
`src/portfolio/runner.py`: quando il feed di decisione 1h di un asset è **congelato**, gli
sleeve CONCENTRATI (single/ml/pairs) che ne dipendono **saltano il tick** (entry E exit)
finché il feed non si sblocca. Come un outage (i worker non valutano gli exit, protezione =
disaster-SL on-book). Si rilascia DA SOLO alla prima barra completa non-flat.
### Distinguere il GUASTO dall'ILLIQUIDITÀ (il punto critico)
Un detector flat-bar ingenuo sarebbe SBAGLIATO: sul feed reale (2026-06-15) gli alt sono
flat ma VIVI. Firma misurata (run finale di close invariate / valori distinti in 48h):
| Asset | run | distinti/48h | natura |
|-------|-----|--------------|--------|
| ETH | 64 | **1** | MORTO (guasto) |
| BNB | 64 | 2 | MORTO (testnet, solo paper) |
| DOGE | 42 | 4 | MORTO (testnet, solo paper) |
| XRP | 12 | 3 | illiquido |
| ADA | 11 | 5 | illiquido ma VIVO |
| LTC | 10 | 5 | illiquido ma VIVO |
| SOL | 1 | 31 | VIVO |
→ il detector conta la **run di close INVARIATE** (prezzo che non cambia MAI), non le barre
flat. Soglia **24** (`feed_freeze_gate_bars`, configurabile, 0=off): un giorno intero di
prezzo immobile separa nettamente le due popolazioni. ETH/BNB/DOGE (run 40-64) gateati,
ADA/LTC/XRP/SOL (run ≤12) no. **PR_BTCLTC resta attivo** (BTC vivo + LTC solo illiquido NON
deve sospendere il pair: si perderebbero segnali reali sulla gamba BTC).
### NON è l'entry-guard post-flat (BOCCIATA)
Si gatea **DURANTE** il freeze (ultima barra completa ferma). La barra di RIPRESA è non-flat
→ la run si azzera → il tick riprende SU di essa. NON si salta la candela-gap di ripresa
(quella è l'overshoot che la fade fada con profitto — l'entry-guard post-flat è bocciata,
CLAUDE.md / 2026-06-05). Rilascio solo su barra **completata** non-flat (la forming non basta,
coerente con EXIT-16).
### Scope
Solo sleeve `single`/`ml`/`pairs`. I multi-asset (basket/rotation/tsmom/xsec, tutti PAPER)
NON sono gateati: diversificati su 8 asset, un feed fermo non li compromette (e ROT02 ha ETH
nell'universo ma resta attivo). Osservabilità: log `FEED_FROZEN_GATE` + alert Telegram
GATED/RIPRESO, dedup per episodio.
## Esito (smoke feed reale, soglia 24)
- CONGELATI: ETH, BNB, DOGE.
- **GATEATI: le 9 gambe ETH esatte** — MR01/MR02/MR07_ETH, SH_ETH, PR_ETHBTC, PR_ETHBTC_15M,
PR_LTCETH, PR_ADAETH, PR_ETHSOL. BTC-only, PR_BTCLTC e i multi-asset restano attivi.
Le 5 posizioni ETH già aperte NON vengono flattate (freeze = pausa, non liquidazione): un
close forzato passerebbe per lo stesso feed congelato (= il close 2.83 di SH01). Restano
gestite alla ripresa del feed; protezione di coda = disaster-SL on-book a 30%.
Test: `tests/portfolio/test_freeze_gate.py` (detector guasto-vs-illiquido, rilascio su barra
completata, scope sui veri sleeve PORT06). Suite portfolio: 140/140 verdi.
## Lezione
Tre livelli di osservabilità del feed ora distinti: STALE_FEED (≥2 barre flat, alert umano),
FEED_BOOK_GAP (feed≠book d'esecuzione, alert), **FEED_FROZEN_GATE (≥24 barre invariate, AGISCE)**.
La firma che separa guasto da illiquidità è la **run di prezzo invariato**, non la barra flat:
gli alt illiquidi stampano barre flat ma il prezzo si muove ogni ~10 barre.
@@ -0,0 +1,66 @@
# 2026-06-16 — Guard TP-invertito (churn MR02_BTC 15m)
## Sintomo
Burst di **8 trade in 15 minuti** su `MR02_donchian_fade__BTC__15m` (09:0009:15 UTC),
tutti `long`, tutti chiusi `take_profit` a `bars_held=0-1` **in perdita**, accuracy
crollata 60%→27%. TP **fisso** a 66189.25, **sotto** ogni entry (6670066834):
```
09:00 OPEN long 66780.5 tp=66189.25 sl=64218.21 ← tp E sl ENTRAMBI sotto l'entry
09:02 CLOSE take_profit exit=66189.25 held=0 sim=-2.63 real=-0.12
... ×8 ... +7× REAL_TP_FAIL: invalid_reduce_only_order
```
I trade MR02 sani prima del cluster hanno il TP dal lato giusto (long → tp>entry:
65033>63414, 64148>63775, 66079>65717). Il cluster delle 09:00 è l'unico **invertito**.
## Causa radice — segnale da wick transitorio (NON feed congelato, NON spike d'ingresso)
BTC alle 09:00 era **davvero** ~66780 (MR01 short@66919, MR07 short@66780.5 nello stesso
istante, slippage reale 0.72.5 bps → feed sano). Il difetto è a monte:
1. Una barra con **wick verso il basso** (~64218 = il livello SL) fa scattare il donchian
"rottura canale basso" → segnale **LONG**, con `tp`=centro canale (66189) e `sl`=64218
calcolati **dal contesto wickato**.
2. Il wick rientra; il worker entra `long` al prezzo **recuperato** 66780 — cioè **sopra
il proprio TP=66189**.
3. L'exit intrabar (`bar_high >= tp`) è banalmente vera all'istante → chiude "take_profit"
a 66189 = **perdita garantita a held=0**. Si ripete 8× perché il livello donchian
wickato resta nel lookback.
Differenza chiave da TP_PHANTOM: lì il wick è sull'**exit** (tocco fantasma del TP);
qui il wick produce un **tp dal lato sbagliato dell'entry** → la posizione non doveva
proprio aprirsi.
## Perché i guard esistenti non l'hanno preso
- **TP_PHANTOM** richiede un limit reduce-only resting come oracolo. Qui il limit è
**respinto** (`invalid_reduce_only_order`: una sell reduce-only a 66189 sotto il mercato
è invalida) → niente `real_tp_order_id` → fail-open → l'exit passa. E il prezzo corrente
(66780) ha **realmente** superato il livello → non è un tocco fantasma.
- **Freeze-gate (v1.1.32)**: BTC non è congelato (prezzi che si muovono) → non scatta.
## Danno reale: contenuto (il real-truth ledger ha funzionato)
- Σ **reale** del cluster ≈ **−€2.29** (solo slippage+fee: il fill reale è al prezzo vero
~66700-66730, non al TP fantasma).
- Σ **sim** sarebbe stato ≈ **−€17.9** → il real-truth ledger ha evitato che ~**€15.6** di
perdita fantasma colpisse l'equity.
- Costi collaterali: 8 round-trip di fee, 7 TP reduce-only respinti, 8 disaster-SL da
piazzare/cancellare, sim accuracy/report inquinati.
- Auto-esaurito alle 09:15 (sleeve poi flat).
## Fix — guard "TP invertito" (v1.1.33, zero parametri)
`StrategyWorker._open_position`: **prima** di aprire, se il TP è già sfondato all'ingresso
(`long & tp<=entry` oppure `short & tp>=entry`) il segnale è malformato (artefatto wick)
**NON si apre** (niente ordine reale, niente disaster-SL). Verità d'esecuzione, non un
filtro di strategia (stessa filosofia di TP_PHANTOM). Log `INVERTED_TP_SKIP` (dedup
per-barra) + alert Telegram una-tantum per processo. Agisce **solo sul path live**: il
backtest canonico entra al close del bar del segnale (entry == base di tp/sl → mai
invertito) e resta non filtrato → il live farà meglio del backtest, come loss-guard /
trend / TP_PHANTOM.
Test: `tests/portfolio/test_inverted_tp.py` (long/short invertiti skippati, controlli
positivi long/short validi aprono, no-tp apre, dedup per-barra). Suite portfolio 146/146.
## Lezione
Cerotto testnet (il feed wicka): il fix vero è **mainnet**, dove l'arbitraggio elimina i
wick-print. Ma il guard è corretto in assoluto — un segnale con TP dal lato sbagliato
dell'entry non va mai eseguito, su qualsiasi venue.
@@ -0,0 +1,57 @@
# 2026-06-16 — Fase 0 smoke mainnet VERDE (ETH→USDC, limit non market)
## Contesto
Il conto Deribit mainnet, fino al 2026-06-14 **vuoto** (€0, unico blocco al micro-test), ha
ricevuto un deposito iniziale di **~$50**: 25.00 USDC + 0.013878 ETH. Occasione per eseguire la
**Fase 0** del piano (`docs/specs/mainnet-microtest-plan.md`): validare la catena d'esecuzione su
soldi VERI con rischio ~0, abbinandola a un'operazione utile — **consolidare l'ETH stranded in
margine USDC**.
Perché consolidare: noi eseguiamo sui perp **lineari USDC** (`*_USDC-PERPETUAL`), marginati in
**USDC**. I 0.0138 ETH stavano nel wallet ETH (collateral dell'inverse, che NON usiamo per la
matematica lineare del backtest) → margine effettivo solo i $25 USDC. Convertendo, l'intero ~$50
diventa USDC utilizzabile.
## Ordine (LIMIT, non market — istruzione utente)
Check capability read-only prima: spot `ETH_USDC` esiste (bid 1780.3/ask 1780.9, `testnet:false`,
price tick 0.1). Il listing strumenti NON espone lo spot (kind=spot vuoto, endpoint v1 404) ma il
ticker basta. `place_order` supporta `order_type="limit"` + `price`.
- **LIMIT SELL `ETH_USDC` 0.0138 ETH @ floor 1776.3** (~0.1% sotto il bid) — *marketable limit con
cap di slippage*: crossa e filla al bid corrente, ma con un floor sotto cui NON può fillare.
Soddisfa "non a market" (prezzo protetto, niente slippage incontrollato) garantendo il fill.
- Step 0.0001 ok al primo colpo (niente fallback 0.013); fallback previsti 0.013/0.012.
### Esito
```
order_state = filled filled_amount = 0.0138
average_price = 1778.1 (= bid al momento dell'invio; il bid era sceso 1780.3->1778.1 tra
quote e invio, ma > floor 1776.3 -> zero slippage negativo)
fee = 0.0 (maker, costo nullo)
order_id = ETH_USDC-8488088107
```
Riconciliazione conto (before→after):
| Wallet | Prima | Dopo |
|---|---|---|
| USDC | 25.00 | **49.538** (+24.538) |
| ETH | 0.013878 | 0.000078 (dust ~$0.14, sotto il min → non convertibile) |
| BTC | 0 | 0 |
| posizioni / ordini resting | — | nessuna / nessuno |
## Cosa è dimostrato
Catena end-to-end su **mainnet** (`testnet:false`), a costo ~0: routing live + auth + `place_order`
limit + fill + verifica via `get_trade_history` + riconciliazione `get_account_summary`. La
meccanica d'esecuzione reale FUNZIONA su soldi veri. **Fase 0 ✅.**
## Limite residuo (resta il blocco vero)
~$49.54 USDC è **sotto il minimo €500** del piano: a questo margine il rumore di arrotondamento
BTC (step lineare ~$6.6) soffoca il segnale → NON si può ancora testare l'**edge**. Per la **Fase 1**
(fade-only, €1000, 2-4 settimane, verdetto ledger-vs-backtest) serve un **top-up a ~€500-1000 USDC**.
Stato piano aggiornato: token+smoke ✅, funding 🟡 parziale, Fase 1 in attesa di capitale.
## Note operative
- Operazione MANUALE one-off (non il runner): `.env.mainnet` sourcato a mano, client puntato a
mainnet solo per questa sessione. Il runner di produzione resta su **testnet** (`.env` condiviso
invariato) → nessun rischio di flip accidentale.
- La marketable-limit con floor è il pattern giusto per conversioni one-shot a prezzo controllato
(vs il TP resting/limit-al-livello degli sleeve): fill immediato + cap di slippage, fee maker.
@@ -0,0 +1,265 @@
# 2026-06-18 — Price Ladder (griglia) vs gate PORT06
**Branch:** `price_ladder_research` · **Harness:** `scripts/analysis/ladder_search.py`
(engine `grid_mtm`: mark-to-market onesto, SL gap-aware, flat-skip, fee 0.10% RT taker).
**Baseline PORT06:** FULL Sharpe 8.13 / DD 2.47% — OOS Sharpe 10.86 / DD 2.09%.
**Metodo:** caccia multi-agente su asset × TF (15m/30m/1h) × regime-gate (none / range
trend_max 1.5/2.0/2.5), poi `verify` avversariale sui finalisti (plateau, fee 2x, corr,
FULL DD di portafoglio). **Goal di progetto:** trovare un Price Ladder che *diversifichi*
(PASSI il gate) senza accendere la coda di trend.
---
## 1. Cosa era già noto
Il gioco-griglia precedente aveva trovato una **griglia ETH fortissima standalone**
(Sharpe ~5.61) ma **BOCCIATA al gate PORT06**: corr ~0.40 con le fade ETH già deployate
**ridondante**, non un diversificatore. La lezione: lo standalone Sharpe non basta, conta
quanto il ladder *aggiunge ortogonalmente* al portafoglio. Questa ricerca cerca proprio un
ladder che PASSI il gate = che abbia corr bassa con gli sleeve esistenti.
---
## 2. Risultato NUOVO — i ladder PASSANO il gate
Sì. Su **tutti gli asset liquidi** (BTC e ETH) e su quasi tutte le combinazioni TF×regime,
i ladder migliorano l'OOS Sharpe di portafoglio (10.86 → ~11.011.35) **con corr bassa**:
- **BTC** (15m/30m/1h, range o none): corr **0.150.27**, molto più ortogonale della
griglia ETH bocciata (0.40). OOS di portafoglio 11.111.35 (il guadagno OOS più grosso).
- **ETH** (15m/30m/1h): corr **0.210.57** (15m la peggiore, fino a 0.57; a 1h scende a
0.220.39). OOS di portafoglio ~10.911.1.
- **SOL/LTC**: SOL passa solo 2 celle, half-only, edge standalone quasi nullo
(full_sh ~0.2); **LTC è negativo ovunque** (full_sh 0.4…−0.8, bocciato) → solo BTC/ETH
liquidi danno un ladder vivo, come atteso.
Il gate **misura corr + OOS**, e su questi due assi i ladder BTC vincono nettamente.
### Migliori candidati (dai `verify` avversariali)
| Asset/TF/regime/struttura | OOS base→new | FULL DD standalone | FULL DD portafoglio (full / half) | corr | fee2x OOS | robust? |
|---|---|---|---|---|---|---|
| **ETH 1h range2 rd0.16 ru0.04 L4** | 10.86→**11.00** | 11.79% | **2.70** / 2.52 | 0.305 | 4.89 | ✅ sì |
| **ETH 1h range2 rd0.16 ru0.06 L6** | 10.86→11.01 | 12.09% | 2.74 / 2.54 | 0.328 (al filo) | 5.05 | ✅ sì |
| **ETH 1h range2 rd0.16 ru0.06 L4** | 10.86→10.98 | 12.64% | **2.68** / 2.51 | 0.249 | 4.44 | ✅ sì |
| **ETH 30m range2 rd0.16 ru0.04 L3** | 10.86→10.93 | 12.56% | **2.48** / 2.41 | 0.223 | 4.21 | ✅ sì |
| **ETH 1h none rd0.20 ru0.06 L6** | 10.86→11.03 | 17.89% | 2.59 / 2.46 | 0.271 | 5.43 | ✅ sì |
| **ETH 30m none rd0.16 ru0.06 L4** | 10.86→10.92 | 18.68% | **2.37** / 2.35 | 0.319 | 5.42 | ✅ sì |
| **ETH 1h none rd0.16 ru0.06 L4** | 10.86→10.96 | 23.13% | **2.40** / 2.37 | 0.224 | 4.59 | ✅ sì |
| **BTC 1h none rd0.08 ru0.06 L3** | 10.86→**11.34** | **53.69%** | 3.11 / **2.48** | 0.216 | 5.78 | ⚠ half-only |
| **BTC 30m range2.5 rd0.08 ru0.06 L3** | 10.86→11.22 | 55.33% | 3.27 / **2.50** | 0.265 | 5.19 | ⚠ half-only |
| **BTC 30m none rd0.08 ru0.06 L3** | 10.86→11.32 | 55.33% | 3.27 / **2.44** | 0.220 | 5.37 | ⚠ half-only (tail) |
| **BTC 1h range1.5 rd0.20 ru0.06 L6** | 10.86→11.13 | 54.43% | 3.19 / **2.33** | **0.161** | 5.46 | ⚠ half-only (tail) |
`rd` = spacing down, `ru` = spacing up, `L` = livelli. Tutti i finalisti hanno plateau
robusto (47 vicini di struttura restano PROMOSSO) e fee2x ~= OOS standalone.
---
## 3. IL NODO — la FULL DD (che il gate IGNORA)
**Il gate verdict NON guarda la FULL DD.** Misura solo corr + OOS Sharpe/DD. Una griglia
long-only ha una coda di trend 2021/22 (viene comprata fino in fondo mentre il prezzo crolla
in un trend persistente) → la **FULL DD standalone** è il vero rischio nascosto. Il filtro
onesto è la **FULL DD di PORTAFOGLIO** (`full_full_dd` / `half_full_dd`), con soglia di
tail-risk **~+0.8pp sopra il baseline 2.47%** (cioè restare sotto ~3.3%).
Due famiglie nette emergono:
- **BTC** — FULL DD standalone **~5467%** (la coda long-only è devastante, come temuto).
Il regime-gate range *taglia gli ingressi nei trend* ma **non immunizza** dalle posizioni
già aperte → a FULL size la FULL DD di portafoglio sale a **3.16.6%** (+0.6/+4.0pp,
**oltre la soglia tail** in quasi tutte le celle). **A HALF size invece rientra a 2.332.54%**
(~baseline) preservando l'OOS ~11.011.3. → **BTC deployabile SOLO a half-size**, e il
guadagno OOS più grosso (11.3+) viene proprio da queste celle a coda alta.
- **ETH** — FULL DD standalone **molto più bassa** del previsto: **1233%** (1h range scende
a **~12%**), NON il ~60% temuto. Lo SL-buffer + flat-skip + il regime-gate range tengono la
griglia fuori dai trend estesi. Conseguenza: la FULL DD di portafoglio resta **2.372.74%**
(+0.0/+0.27pp, **sotto soglia anche a FULL size**) — in diversi casi *scende* sotto il
baseline (es. ETH 30m none L4 → 2.37%). → **ETH è la famiglia tail-safe.** Il prezzo è una
corr più alta (0.220.33 sui finalisti 1h/30m, fino a 0.57 sul 15m da scartare).
**Lettura chiave:** il regime-gate *abbassa* la FULL DD ma su BTC **non a sufficienza** a
full size (la coda 54% è troppo grande); è l'**half-size** a fare il lavoro. Su ETH la coda
è già piccola e il portafoglio l'assorbe. Quindi il discriminante reale è **asset (ETH vs
BTC) + size**, non solo il regime-gate.
---
## 4. Vantaggio Deribit — fill MAKER
Un Price Ladder è una griglia di **ordini LIMIT ai livelli** → su Deribit i fill sono
**MAKER (~0% fee)**, non taker. L'harness assume **0.10% RT taker** ovunque → è
**CONSERVATIVA**: la stessa griglia live pagherebbe meno fee e potrebbe rendere PIÙ del
backtest. Lo confermano i `verify`: il **fee2x** (raddoppio fee) lascia l'OOS Sharpe quasi
intatto (~98% trattenuto su quasi tutti i finalisti) → nessun fee-cliff, e con i fill maker
reali c'è upside, non downside, sul lato costi. (Il rischio resta la coda di trend e lo
slippage/fill in esecuzione reale, non le fee.)
---
## 5. Verdetto operativo
**C'è un Price Ladder deployabile.** Il candidato più pulito su tutti gli assi
(PROMOSSO + FULL DD di portafoglio controllata a full size + plateau robusto + corr genuina):
> **ETH 1h, regime=range trend_max 2.0, rd=0.16 / ru=0.04 / 4 livelli, sl_buf 0.12 / tp_buf 0.05, max_bars 720.**
> OOS 10.86→11.00, FULL DD portafoglio **2.70% a full / 2.52% a half** (standalone 11.79%),
> corr 0.305, fee2x OOS 4.89, plateau 4 vicini PROMOSSO. **Deploy raccomandato a HALF size**
> (FULL DD +0.05pp, marginale; il caveat slippage live giustifica la prudenza).
**Alternative ETH equivalenti** (stessa banda di sicurezza, scegliere per corr/coda):
`ETH 1h range2 rd0.16 ru0.06 L4` (corr 0.249, la più bassa) e `ETH 30m range2 rd0.16 ru0.04 L3`
(FULL DD portafoglio 2.48, ~baseline). Tutte ETH 1h/30m **range-gated rd≥0.16** → NON scendere
sotto rd 0.16 (rd 0.12 fa risalire la corr verso 0.38 = ricomincia a somigliare alle fade ETH;
il 15m ha corr 0.430.57 = troppo ridondante, scartare).
**I BTC NON a full size.** Danno il guadagno OOS più grande (fino a 11.34) e la corr più bassa
(0.160.22 = la diversificazione migliore), MA la FULL DD standalone ~54% li rende
**tail-risky a full size** (FULL DD portafoglio 3.16.6%, oltre soglia). **Deployabili SOLO a
half-size** (FULL DD portafoglio 2.332.50), dove restano un diversificatore eccellente. Il
migliore BTC half-size è `BTC 1h range1.5 rd0.20 ru0.06 L6` (corr **0.161**, FULL DD portafoglio
half **2.33**).
**BLOCCO prima di qualsiasi deploy:** il gate PORT06 (corr + OOS) è **necessario ma non
sufficiente** — non controlla la FULL DD, e l'OOS held-out è un **regime calmo** (202425). La
coda di trend 2021/22 è reale e standalone è enorme su BTC. **Nessun deploy senza un gate
stressato sul 2022** (verificare la FULL DD di portafoglio attraverso il trend duro, non solo
l'OOS calmo) e una conferma su **ledger reale shadow** per slippage/fill maker. Il verdetto del
gate va sempre incrociato con la FULL DD di portafoglio (filtro vero) + fattibilità feed live +
regime recente — la stessa lezione del timing-sweep pairs/honest (2026-06-14).
**Sintesi:** ETH 1h/30m range-gated rd≥0.16 a half-size è il **deploy-candidate sano e
tail-safe**; BTC è il diversificatore migliore ma **solo a half-size** per la coda di trend.
Prossimo passo obbligatorio: stress-test 2022 + shadow ledger reale (i fill maker possono solo
migliorare il risultato sul lato fee).
---
## 6. ⚠️ RETTIFICA CRITICA — il verdetto §5 è ROVESCIATO (critico + verifica dati)
Il critico di completezza + una verifica diretta dei dati **ribaltano la raccomandazione di
deploy della §5**. Messo a verbale: la §5 è SUPERATA da quanto segue.
**(1) I risultati BTC poggiano su DATI CORROTTI (spike-print), verificato.** La "coda di trend
BTC ~54%" su cui ruota la §3 NON è un crollo di mercato: è **feed sporco**. Misurato su `btc_1h`:
**37 barre con range intrabar >20%**, concentrate a **feb-2024** (es. 2024-02-13: low=38.580 con
close~49.968 = wick fasullo 23% che rimbalza nella stessa barra), con min 22.706 / max 104.688 in
un trimestre dove BTC stava ~4757k. È lo **stesso spike-print testnet documentato ovunque in
CLAUDE.md** (TP_PHANTOM / feed congelato). L'engine SL-gap-aware filla su questi wick fantasma e la
griglia "compra" i dip fasulli → la coda BTC ~54% e parte dell'edge OOS sono **artefatto, non
mercato**. Conseguenza: i candidati BTC NON sono affidabili, e lo "stress 2022" proposto NON
toccherebbe la coda vera (che è a feb-2024, da dati sporchi). Serve `close_only` + pulizia feed.
**(2) Il guadagno OOS è RUMORE al peso deployabile.** +0.14 OOS Sharpe su base 10.86, con il ladder
che pesa **~5.3% full / ~2.7% half** in un portafoglio già a Sharpe ~11, su **592 giorni di un
solo regime calmo** (2024-10→2026-05, niente 2022). Nessun test di significatività
(block-bootstrap). A half-size il contributo è quasi inerte (a volte l'OOS DD *migliora*): è dentro
l'errore di campionamento, non un edge dimostrato.
**(3) La diversificazione ETH è più sottile di quanto venduto.** Il finalista ETH "raccomandato" è
**corr 0.305** (non 0.150.20: quella è BTC half-only non deployabile), già vicino allo **0.40 che
fece bocciare la griglia ETH originale**. Peggio: il `regime_mask` usa **lo stesso filtro
trend-distance** (|closeEMA200|/ATR) delle fade ETH già live → la "diversificazione" è in parte
illusoria (gate e sleeve condividono il filtro). La corr misurata è **daily in regime calmo**, NON
co-tail: una griglia long-only e una fade reversion possono co-crollare in un trend (entrambe
"comprano il calo") — la metrica che conterebbe (co-movimento nella coda) non è calcolata.
**(4) La FULL DD standalone è mascherata dal compounding.** Il finalista ETH compone a **~31x
(+3.1M%) in 5.4 anni**: una "FULL DD 11.79%" accanto a un ritorno di milioni-% è artefatto di
notional reinvestito, e il daily-resample **nasconde l'inventory-risk intrabar** che il docstring
di `grid_game_gate.py` (righe 57) avverte esplicitamente di non vedere.
**(5) Stress disponibile NON usato.** `ladder_search`/`grid_mtm` hanno `close_only=True` (filla solo
sul close, neutralizza gli spike-print): NON è stato girato sui finalisti — proprio il test che
smonterebbe la coda BTC fasulla.
### Verdetto rettificato
**NON c'è un Price Ladder deployabile dimostrato.** La ricerca ha prodotto (a) una **harness onesta
e validata** (`ladder_search.py` + regime-gate, riproduce esatto il gate noto), (b) un **risultato
negativo/guardingo** robusto: ogni candidato o è ridondante (ETH, corr→0.40), o poggia su dati
sporchi (BTC spike-print), o è rumore al peso deployabile. **Il multi-agente + critico ha evitato
un deploy ingiustificato** — esattamente la lezione anti-squeeze del progetto.
**Prima di riconsiderare QUALSIASI ladder servono, in ordine:** (1) **pulire il feed BTC** dagli
spike-print + girare `close_only` sui finalisti; (2) **test di significatività** del delta OOS
(block-bootstrap) al peso reale; (3) **co-tail / corr nei crash** (2022), non corr daily calma;
(4) isolare e spiegare l'**evento feb-2024**. Config live **invariata**.
---
## 7. RE-GATE su DATI PULITI (post clean_feed.py) — quadro onesto finale
Dopo `clean_feed.py` (254 spike-print riparati con Binance) ho ri-gateato i top candidati con
le verifiche che il critico chiedeva (`ladder_regate_clean.py`): gate PORT06 + stress
`close_only` + **DD per anno** sulla storia completa.
| Candidato | DD (gate, 2021+) | OOS Sh | corr | fee2x | close_only OOS | gate½ | **DD 2018 (tail vero)** |
|---|---:|---:|---:|---:|---:|---|---:|
| ETH 1h rd0.16 ru0.04 L4 range2 | 12.4% | 4.70 | 0.29 | 4.62 | 0.24 | PROMOSSO | 52.4% |
| ETH 1h rd0.16 ru0.06 L4 range2 | 12.6% | 4.25 | 0.23 | 4.17 | 0.20 | PROMOSSO | 49.9% |
| BTC 1h rd0.08 ru0.06 L3 none | 10.8% | 5.90 | 0.24 | 5.78 | 0.60 | PROMOSSO | 50.3% |
| **BTC 1h rd0.20 ru0.06 L6 range1.5** | 14.8% | 5.58 | 0.23 | 5.46 | 0.89 | PROMOSSO | **27.7%** |
| BTC 30m rd0.08 ru0.06 L3 none | 14.8% | 5.47 | 0.25 | 5.37 | 1.07 | PROMOSSO | 36.6% |
**Cosa cambia coi dati puliti:**
1.**L'obiezione "coda artefatto" CADE.** Il 54% del BTC era spike-print 2024: ora la DD del
gate (2021+) è **~11-15%** e tutti i candidati restano PROMOSSO a half-size, corr 0.22-0.29,
reggono fee2x. Sono candidati *veri*, non artefatti.
2. 🔴 **Emerge il tail VERO: il 2018** (44/52% standalone), che **il gate NON VEDE** perché
l'IDX del progetto parte dal 2021-01-01. Una griglia long-only sarebbe stata sventrata nel
bear 2018 (BTC 84% sull'anno). È il nuovo rischio portante — e una **lacuna metodologica
generale**: tutte le metriche "FULL" del progetto escludono il 2018. Mitigante reale: il
**regime-gate stretto** (BTC rd0.20 L6 **range1.5**) dimezza il tail 2018 a **27.7%** (vs
50% senza gate) → il gate-di-regime fa il suo lavoro contro i bear sostenuti.
3. 🟡 **L'edge dipende dai fill INTRABAR.** `close_only` (fill solo sul close) fa crollare l'OOS
(4.7→0.24 ETH, 5.9→0.6 BTC). Per ordini LIMIT i fill intrabar sono legittimi (close_only è
troppo severo come stress), ma il gap segnala forte **sensibilità all'ipotesi di fill**
il ledger shadow reale è indispensabile prima di fidarsi.
4. 🟡 Restano: corr 0.22-0.29 (moderata), guadagno OOS ai limiti del rumore al peso deployabile
(~2.7% half), OOS singolo regime calmo.
**Verdetto finale (clean data):** la pulizia ha rimosso l'obiezione FALSA (coda artefatto) e
promosso i candidati a *genuini*, ma ha anche scoperto il tail REALE (2018, gate-cieco) e
confermato due cautele (fill intrabar, OOS rumoroso). **Il candidato più sensato è ora
`BTC 1h rd0.20 ru0.06 L6 regime=range trend_max1.5`**: PROMOSSO, corr 0.23, e la **miglior coda
2018 (27.7%)** grazie al regime-gate. **Prima di QUALSIASI deploy**: (1) backtest che INCLUDA il
2018 nel gate (non solo IDX 2021+), (2) ledger shadow reale per i fill intrabar/maker, (3) half-size.
La pulizia del feed resta il guadagno trasversale più grande (irrobustisce ogni backtest del progetto).
---
## 8. I 3 PASSI PRE-DEPLOY + STUDIO SL/TP (ladder_sltp_study.py)
**[1] Valutazione 2018-INCLUSIVE** (standalone, tutta la storia; il gate IDX2021+ è cieco al 2018):
| Candidato (sl0.12/tp0.05) | full_sh | full_dd | oos_sh | DD 2018 | DD 2021 |
|---|---:|---:|---:|---:|---:|
| BTC 1h L6 **range1.5** | 3.18 | 27.7% | 4.95 | **27.7%** | 14.8% |
| BTC 1h L3 none | 2.93 | 60.2% | 5.51 | **50.3%** | 10.8% |
Il regime-gate già dimezza il tail vero (2018: 50%→−27.7%). Senza gate la griglia long-only è insostenibile (60%).
**Studio SL/TP** (sweep sl_buf × tp_buf sul candidato regime-gated, full 2018+):
- **Sweet spot: `sl_buf=0.10, tp_buf=0.03` → DD 2018 23.5%** (da 27.7%), oos_sh 5.06, full_sh 2.92. Migliora il tail SENZA intaccare l'edge.
- **Lezione (conferma il prior del progetto):** un SL **troppo stretto PEGGIORA** (sl 0.06 → 33/35%): stoppa e rideploya più in basso nel coltello che cade = la trappola del falso-negativo mean-reversion. Un SL **troppo largo** (0.20) idem (29/31%, oos più basso). Il bene sta in mezzo (0.10).
- **Il SL da solo (senza regime-gate) è ERRATICO** (tabella L3 none non-monotona: 20% → 52% → 43% al variare di pochi punti): è il **regime-gate** il controllo primario della coda; il SL moderato fine-tuna. Catastrophe-control = "non deployare nel bear" (regime) + "cappa l'episodio" (SL 0.10), NON uno stop stretto.
**[2] Fill maker vs taker** (Deribit: i livelli sono LIMIT → maker):
| fee | oos_sh | DD 2018 | gate½ |
|---|---:|---:|---|
| taker 0.10% RT (harness) | 5.06 | 23.5% | PROMOSSO (OOS 10.86→11.0, corr 0.195) |
| **maker 0%** (Deribit reale) | 5.18 | 22.7% | PROMOSSO (OOS→11.0, corr 0.194) |
Il maker è leggermente MIGLIORE → la harness taker è conservativa, nessun fee-cliff. (La parte live = shadow ledger reale resta il passo operativo finale.)
**[3] Half-size:** il gate "half" è già a metà size e PROMOSSO; la coda 2018 standalone 23.5% si dimezza sul book (~12% in un 2018-repeat a half-size) = reale ma gestibile.
### CONFIG FINALE raccomandata (con SL/TP aggiunti)
> **BTC 1h · regime=range trend_max 1.5 · rd 0.20 / ru 0.06 / 6 livelli · sl_buf 0.10 / tp_buf 0.03 · HALF-size.**
> - Tail 2018-inclusive: 23.5% standalone (~12% sul book a half) — reale ma controllato dal regime-gate + SL moderato.
> - Gate PORT06: PROMOSSO (OOS 10.86→11.0, corr 0.195); maker Deribit lo migliora; nessun fee-cliff.
> - **Unico passo residuo: shadow ledger reale** (fill intrabar/maker su Deribit) — operativo, non backtestabile.
Caveat onesti residui: l'OOS gain al peso deployabile è modesto (10.86→11.0); il tail 2018 non è zero (12% a half); l'edge dipende dai fill intrabar (per i LIMIT è legittimo, ma lo shadow lo deve confermare).
File diff suppressed because one or more lines are too long
+181
View File
@@ -0,0 +1,181 @@
# Piano operativo — Micro-test MAINNET (Deribit)
**Obiettivo.** Rispondere all'unica domanda che testnet e backtest non possono:
*quando si passa ai fill VERI, quanto edge sopravvive?* Si misura con poco denaro
reale (€5001500) per 24 settimane, leggendo il report **ledger-vs-backtest**.
Costo del verdetto: poche centinaia di € invece di scoprirlo a €5k.
**Perché serve (e perché testnet non basta).** Testnet ha feed farlocchi — il
2026-06-13 `ETH-PERPETUAL` inverse era **congelato a 1661.95 da 12h+** mentre il
prezzo reale si muoveva (gap 1.3%); BTC era vivo. Su mainnet l'arbitraggio tiene
inverse ≈ lineare ≈ realtà (entro bps): **il micro-test risolve il problema feed
per costruzione**, oltre a dare fill/slippage/fee reali. (Il 2026-06-14 `ETH-PERPETUAL`
testnet era ancora congelato a 1661.95 da 36h+ → conferma definitiva: testnet è inaffidabile.)
---
## STATO (2026-06-16)
-**Token mainnet wired e VERIFICATO.** `MAINNET_TOKEN` (da cerbero-mcp, che ha già le chiavi
Deribit LIVE) copiato in **`.env.mainnet`** (chmod 600, gitignored), SEPARATO dal `.env`
condiviso. Verifica read-only: `is_mainnet()=True`, auth OK su `get_account_summary` (nessun
ordine). Il `.env` condiviso resta testnet → il runner attuale NON flippa a mainnet a un riavvio.
-**FASE 0 SMOKE VERDE (2026-06-16).** Catena d'esecuzione validata su mainnet con soldi VERI a
costo ~0: LIMIT SELL `ETH_USDC` spot 0.0138 ETH @ floor 1776.3 (marketable, NON market) →
`order_state=filled`, average_price **1778.1** (= bid, zero slippage negativo), **fee 0.0**.
Riconciliazione conto: USDC 25.00→**49.54** (+24.54), ETH→0.000078 dust, nessun ordine resting.
Provato end-to-end: routing live + auth + place_order limit + fill + verifica trade + balance.
(Doppio scopo: consolidare l'ETH stranded in margine USDC + smoke.) Diario
`docs/diary/2026-06-16-mainnet-fase0-smoke.md`.
- 🟡 **FINANZIAMENTO PARZIALE: ~$49.54 USDC** (deposito iniziale ~$50: 25 USDC + 0.0138 ETH, ora
consolidati). **Sotto il minimo €500** del piano → basta per la meccanica (Fase 0 ✅) ma NON per
testare l'edge (a $50 di margine il rumore di arrotondamento BTC soffoca il segnale). **Serve
top-up a ~€500-1000 USDC** prima della Fase 1.
- ⏭️ Appena finanziato a ~€1000: avviare il micro-test come **servizio separato** (env_file
`.env.mainnet`, dir dati pulita, `portfolios.yml` fade-only) → Fase 1.
---
## Prerequisiti (umani, fuori dal codice)
1. **Conto Deribit MAINNET** (KYC) finanziato con **~€1000** in USDC (vedi sizing).
2. **MAINNET_TOKEN Cerbero** abilitato al trading mainnet sul conto (lato CerberoSuite).
3. Decidere il capitale: **€1000 raccomandato** (vedi sezione Sizing).
> NB sicurezza: il MAINNET_TOKEN dà accesso a soldi veri. Va in `.env` (gitignored),
> mai committato. Un bot-tag dedicato (`pythagoras-mainnet`) per isolarlo nell'audit.
---
## Come si punta a mainnet (già pronto, 1 sola modifica .env)
Il client legge il token da env (default = testnet). Il token mainnet vive in un file
**`.env.mainnet`** DEDICATO (NON nel `.env` condiviso, che resta testnet → il runner attuale
non flippa a mainnet a un riavvio):
```
# .env.mainnet (gitignored, chmod 600) — caricato SOLO dal servizio micro-test mainnet
CERBERO_TOKEN=<MAINNET_TOKEN>
CERBERO_BOT_TAG=pythagoras-mainnet
```
Il servizio micro-test usa `env_file: [.env, .env.mainnet]` (le chiavi di `.env.mainnet`
prevalgono → Telegram da `.env`, token mainnet da `.env.mainnet`).
**Nessuna modifica di codice.** `CerberoClient.is_mainnet()` lo rileva; il runner,
l'ExecutionClient e gli smoke usano lo stesso client. Per tornare a testnet: togliere
le due righe. (Verificato: default invariato testnet, override flippa il rilevamento.)
> Consiglio: **non riusare il container/data testnet**. Avviare il micro-test su una
> dir dati pulita (`data/` separata o nuovo volume) così i ledger reali mainnet non
> si mescolano allo storico testnet, e il report parte clean-start.
---
## Sizing & taglia — perché €1000 e SOLO le fade in fase 1
Notional = `alloc_sleeve × position_size × leverage`; amount = notional/prezzo,
quantizzato allo step Deribit. Rumore di arrotondamento = step/amount.
**Fades-only (6 sleeve equal, pos 0.5 × leva 3) — rumore arrotondamento:**
| Capitale | notional/fade | round% BTC | round% ETH |
|----------|---------------|-----------|-----------|
| €500 | $125 | 5.2% | 1.3% |
| **€1000** | **$250** | **2.6%** | **0.7%** |
| €1500 | $375 | 1.7% | 0.4% |
**€1000 è il punto giusto**: BTC ~2.6% di rumore (leggibile), abbastanza piccolo
da non far male. €500 funziona ma BTC sale al 5%; €1500 è il più pulito.
**Perché NIENTE pairs/SH01 in fase 1:**
- **Pairs** (pos 0.13, 2 gambe): a €1000 il notional/gamba è ~$21 → arrotondamento
**~30%** (la gamba alt anche peggio): a questa taglia misureresti l'arrotondamento,
non l'edge. Servono ~€58k per pairs puliti. Inoltre l'executor a 2 gambe ha
leg-risk: più superfici d'errore. → fuori dalla fase 1.
- **SH01**: bootstrap full-history dal parquet inverse; su mainnet va ri-derivato il
parquet da prezzi mainnet. Diversificatore, non core. → fuori dalla fase 1.
Le **6 fade single-leg (MR01/02/07 × BTC/ETH)** sono il **core edge**, l'esecuzione
più semplice (open market + TP limit resting + disaster-SL), e quelle col track
record validato col worker reale. Sono il test giusto.
---
## Fasi
### Fase 0 — Smoke mainnet (€0 di rischio reale, 1 ordine minimo)
Con il token mainnet attivo, eseguire `live_exec_smoke.py` (ordine→verifica→fee) e
`live_shadow_smoke.py` su **size minima** per confermare: auth mainnet OK, strumenti
USDC risolti, fill verificato per order_id, fee reali lette. Se uno fallisce, STOP.
### Fase 1 — Solo fade, €1000, 24 settimane
`portfolios.yml` per il micro-test:
```yaml
overrides:
total_capital: 1000
leverage: 3 # config che vorremmo deployare (misura il vero)
position_size: 0.5
paper_sleeves: [TR01, ROT02, TSM01, XS01, PR01..., SH01...] # tutto tranne le 6 fade -> paper
execution:
enabled: true
sleeves: [MR01, MR02, MR07] # solo fade (DIP01 opz.: 1h BTC, core honest)
pairs_enabled: false # niente 2-gambe in fase 1
disaster_sl_pct: 0.30 # rete di coda invariata
real_truth: true
```
> Alternativa prudente settimana-1: `leverage: 2` per dimezzare il DD mentre si
> verifica l'esecuzione, poi 3.
Avviare il runner mainnet (container separato o stessa immagine con `.env` mainnet),
+ reconciler orario + report ledger-vs-backtest giornaliero `--since <data-start>`.
### Fase 2 — Verdetto (dal report ledger-vs-backtest)
Aspettare **≥1020 trade reali** (su mainnet le fade ETH/BTC tradano regolarmente, no
feed congelato). Criterio (lo stesso del report, soglie già nel verdetto 🟢/🟡/🔴):
- 🟢 **slippage medio ≤15 bps E leakage per-trade < €0.30** in relativo → l'edge regge,
si scala.
- 🟡 leakage moderato → estendere il campione, capire dove perde.
- 🔴 slippage >40 bps o leakage che mangia l'edge → **NON scalare**; l'edge non
sopravvive all'esecuzione (hai perso ~€2050, non €5k).
### Fase 3 — Espansione (solo se 🟢)
1. Alzare il capitale (€1000→€2k→€5k) — i pairs diventano puliti sopra ~€5k.
2. Riaccendere pairs (a conto flat) e SH01 (con parquet mainnet).
3. Continuare a leggere il report ad ogni step.
---
## Rischi & safeguard (già implementati)
- **Disaster-SL on-book 30%** ad ogni apertura → protegge da outage del runner.
- **Reconciler orario** (`reconcile_account --telegram`) → drift conto-vs-libri +
guard `STALE_REAL_POSITION` (worker non gestito).
- **Netting close** (v1.1.25) → niente gambe orfane/close cappati.
- **Real-truth ledger** → equity = soldi veri sul conto.
- **Kill-switch**: `execution.enabled: false` (smette di aprire reali, gestisce le
uscite) oppure stop del container + flatten manuale (procedura già rodata).
- Perdita massima plausibile in fase 1: qualche % di €1000 = **€2080**. È il prezzo
del verdetto.
---
## Cosa NON fare
- Non partire con pairs/SH01 (rumore/complessità).
- Non scalare prima del verdetto 🟢 stabile per più giorni.
- Non mescolare i dati testnet e mainnet nello stesso ledger.
- Non committare mai il MAINNET_TOKEN.
---
## Checklist di avvio
- [x] MAINNET_TOKEN Cerbero abilitato + verificato (auth OK, is_mainnet=True) — 2026-06-14
- [x] Token in `.env.mainnet` (separato, gitignored, chmod 600) + `CERBERO_BOT_TAG=pythagoras-mainnet`
- [x] Fase 0 smoke mainnet VERDE (LIMIT SELL ETH_USDC filled, fill@bid, fee 0, balance riconciliato) — 2026-06-16
- [~] Conto Deribit mainnet finanziato: **~$49.54 USDC** (parziale, sotto il min €500) ← **TOP-UP a ~€1000 = BLOCCO Fase 1**
- [ ] Servizio micro-test separato (env_file `.env.mainnet`, dir dati pulita)
- [ ] `portfolios.yml` micro-test (fade-only, €1000, exec sleeves MR01/02/07)
- [ ] runner mainnet + reconciler + report ledger-vs-backtest schedulati
- [ ] dopo ≥1020 trade: leggere il verdetto → scala / aspetta / stop
+78
View File
@@ -0,0 +1,78 @@
# Config LIVE del trader a portafoglio. Seleziona UN portafoglio attivo
# (definito in scripts/portfolios/_defs.py) e ne fa l'override dei parametri operativi.
#
# ============ MICRO-TEST MAINNET — soldi VERI (Fase 1, 2026-06-17) ============
# Conversione da testnet a mainnet Deribit (vedi docs/specs/mainnet-microtest-plan.md).
# Capitale REALE €500 (periodo di prova; poi si scala col verdetto ledger-vs-backtest).
# Eseguono reale SOLO le 6 fade (MR01/MR02/MR07 x BTC/ETH, 15m) + DIP01 (BTC 1h);
# pairs/SH01/multi-asset -> PAPER (sola statistica, fuori dal pool/conto). Il token
# mainnet arriva da .env.mainnet (env_file del servizio). Ledger ripartito da ZERO:
# lo storico testnet e' archiviato in data/_reset_backup/pre_mainnet_20260617-205943.tgz.
active: PORT06
overrides:
# CAPITALE REALE del micro-test (era 2000 su testnet). €500 = minimo del piano
# (rumore arrotondamento BTC ~5-6%); si sale dopo il verdetto ledger-vs-backtest.
total_capital: 500
# equal-weight: nel pool REALE restano solo FADE (6) + DIP01 (1) -> 1/7 ciascuno.
# I cap PAIRS/SHAPE non servono piu' (quelle famiglie sono PAPER, fuori dal pool).
weighting: equal
# Leva 3x (scelta utente 2026-06-17 per il micro-test = config che vogliamo deployare,
# frontiera ACCEL50). NB su soldi veri al capitale minimo: DD pieno (alt prudente 2x).
leverage: 3
rebalance: 1D
poll_seconds: 60
# Gate feed CONGELATO: su mainnet l'arbitraggio tiene il feed vivo (niente freeze come
# sul testnet), lo lascio attivo come rete di sicurezza (non scattera' su BTC/ETH
# liquidi). 0 = disattivo.
feed_freeze_gate_bars: 24
# SLEEVE PAPER (fuori dal pool/pesi/ledger): i €500 si dividono SOLO tra i 7 sleeve
# REALI (6 fade + DIP01). Pairs (PR01, 2 gambe) e SH01 fuori dalla Fase 1: a €500 il
# rumore di arrotondamento li soffoca (pairs ~30%/gamba; servono ~€5-8k) e aggiungono
# superfici d'errore (leg-risk). Multi-asset (TR01/ROT02/TSM01/XS01) paper come sempre.
paper_sleeves: [TR01, ROT02, TSM01, XS01, PR01, SH01]
# Frazione di capitale-sleeve per posizione (0.5 con leva 3 = 1.5x la fetta impegnata).
position_size: 0.5
# Override per-famiglia: irrilevante per il conto reale (i pairs sono PAPER), tenuto
# solo perche' i worker pairs in sola-statistica dimensionino come da gate storico.
position_size_family: {PAIRS: 0.13}
# PAPER_EXTRA (2026-06-18): sleeve paper definiti SOLO qui (NON in _defs.py/PORT06) ->
# NON entrano nel backtest canonico/regression-lock. Shadow STAGE 1 del Price Ladder:
# GridWorker SIM-only su feed Deribit BTC 1h (NESSUN ordine reale; kind=grid non e' mai
# eseguito reale per costruzione). Config = re-gate su dati puliti (branch
# price_ladder_research): regime-gate range trend_max 1.5, rd0.20/ru0.06, 6 livelli,
# sl0.10/tp0.03. position_size 0.15 PINNATO (canonico validato; senza, erediterebbe il
# 0.5 globale del micro-test). Gira in data/portfolio_paper_stats/GRID_BTC/.
paper_extra:
- sid: GRID_BTC
kind: grid
name: GRID
asset: BTC
tf: "1h"
cluster: BTC-rev
params: {tf: "1h", range_down: 0.20, range_up: 0.06, levels: 6,
sl_buf: 0.10, tp_buf: 0.03, max_bars: 720,
regime: range, trend_max: 1.5, position_size: 0.15}
# Esecuzione REALE su Deribit MAINNET. Solo i 7 single-leg con TP/SL in metadata:
# 6 fade (MR01/MR02/MR07 x BTC/ETH 15m) + DIP01 (BTC 1h). Ordini sui LINEARI USDC
# (payoff lineare = matematica del backtest; fee/PnL in USDC).
execution:
# DISATTIVATA 2026-06-18: la verifica su dati REALI (Binance spot + perp Deribit
# MAINNET) ha mostrato che le 6 fade NON hanno edge sui prezzi veri (tutte negative
# ogni anno, Sharpe -3..-5) -> il +1000% "validato OOS" era un artefatto dei print
# fantasma del feed testnet Cerbero. Micro-test fermato e conto flattato a mano.
# Tenuta a false come blindatura: un 'docker compose up'/deploy NON deve riprendere
# a tradere reale finche' non ri-validiamo l'intera libreria su dati reali.
enabled: false
# SOLO fade + DIP01 in Fase 1 (SH01 e pairs -> paper, vedi paper_sleeves).
sleeves: [MR01, MR02, MR07, DIP01]
instruments:
BTC: BTC_USDC-PERPETUAL
ETH: ETH_USDC-PERPETUAL
# niente esecuzione a 2 gambe in Fase 1 (pairs sono paper).
pairs_enabled: false
# Disaster-bracket on-book (~-30%) a ogni apertura: assicurazione per gli outage
# del runner. In operativita' normale non scatta mai -> 0 costo. 0 = disattivo.
disaster_sl_pct: 0.30
# REAL-TRUTH: equity/ribilanci/sizing derivano dai FILL REALI (fee reali incluse),
# non dal sim. Il sim resta solo diagnostica nel log CLOSE.
real_truth: true
+142
View File
@@ -0,0 +1,142 @@
"""REGRESSION-LOCK COMUNE dei gate PORT06 live (exit16 / trendmax / dip01).
Queste funzioni erano copiate quasi-verbatim in exit16_port06_impact.py,
trendmax_port06_impact.py e dip01_exit16_impact.py. Sono il regression-lock
delle DECISIONI LIVE (EXIT-16, swap hurst->trend, DIP01 EXIT-16): la copy-drift
fra le copie avrebbe corrotto i verdetti, quindi vivono qui in un'unica copia.
NON cambiare la matematica: i gate devono restare riproducibili byte-a-byte.
Se un nuovo gate richiede un comportamento diverso, PARAMETRIZZARE (come fu
fatto per hurst_mask/trend_max), mai biforcare una copia.
Contenuto:
build_trades_variant : replay ESATTO di risk_management.build_trades sulle
fade (mode="orig" == canonico), con i rami varianti
EXIT-16 (mode="exit16"), filtro trend (trend_max) e
loss-guard Hurst (hurst_mask) parametrici.
equity_from_trades : trade -> equity giornaliera normalizzata su IDX
(stesso flusso di combine_portfolio.fade_daily_equity).
port_metrics : metriche FULL/OOS del portafoglio con la STESSA
matematica pesi di Portfolio.backtest (weight_vector
su tutti gli sleeve, ribilancio come port_returns).
dd : max drawdown % di una equity.
NB: l'engine DIP01 (dip_trades in dip01_exit16_impact.py) NON e' una copia di
build_trades_variant ma un sibling deliberatamente diverso (long-only, mode
"orig_gap" gap-aware, j clampato a n-1 a fine serie, niente filtri trend/hurst)
-> resta nel suo script.
"""
from __future__ import annotations
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 scripts.analysis.strategy_research import atr
from scripts.analysis.risk_management import FEE_RT, LEV, POS, INIT
from scripts.analysis.combine_portfolio import (
_norm, IDX, port_returns, metrics, SPLIT,
)
from src.portfolio import weighting as W
BUFFER = 0.5 # EXIT-16 close-confirm (come in produzione)
EMA_LONG = 200
def build_trades_variant(ents, df, mode, trend_max, hurst_mask=None,
buffer=BUFFER, lev=LEV, fee_rt=FEE_RT, ema_long=EMA_LONG):
"""Replica ESATTA di risk_management.build_trades, con i rami varianti.
mode="orig" : SL intrabar al livello (SL prima del TP) == canonico.
mode="exit16" : SL intrabar OFF; TP intrabar al livello (priorita' nel bar);
SL solo se il CLOSE sfonda sl0 -/+ buffer*ATR14[j], fill a close[j].
trend_max : None = filtro OFF; 3.0 = config live.
hurst_mask : bool[i]=True -> salta l'ingresso (loss-guard storico).
"""
h, l, c = df["high"].values, df["low"].values, df["close"].values
n = len(c)
a = atr(df, 14)
el = pd.Series(c).ewm(span=ema_long, adjust=False).mean().values
fee = fee_rt * lev
out = []
last = -1
for e in ents:
i, d = e["i"], e["d"]
if i <= last or i + 1 >= n:
continue
if hurst_mask is not None and hurst_mask[i]:
continue
if trend_max is not None and a[i] and abs(c[i] - el[i]) / a[i] > trend_max:
continue
entry = c[i]
tp, sl0, mb = e["tp"], e["sl"], e["max_bars"]
exit_p = c[min(i + mb, n - 1)]
j = min(i + mb, n - 1)
for k in range(1, mb + 1):
j = i + k
if j >= n:
exit_p = c[n - 1]
break
if mode == "orig":
hs = (d == 1 and l[j] <= sl0) or (d == -1 and h[j] >= sl0)
ht = (d == 1 and h[j] >= tp) or (d == -1 and l[j] <= tp)
if hs:
exit_p = sl0
break
if ht:
exit_p = tp
break
if k == mb:
exit_p = c[j]
else: # exit16
ht = (d == 1 and h[j] >= tp) or (d == -1 and l[j] <= tp)
if ht:
exit_p = tp
break
aj = a[j] if np.isfinite(a[j]) else 0.0
confirm = (d == 1 and c[j] < sl0 - buffer * aj) or \
(d == -1 and c[j] > sl0 + buffer * aj)
if confirm:
exit_p = c[j]
break
if k == mb:
exit_p = c[j]
ret = (exit_p - entry) / entry * d * lev - fee
out.append((i, j, ret))
last = j
return out
def equity_from_trades(df, trades):
"""Trade -> equity giornaliera su IDX (flusso di combine_portfolio.fade_daily_equity)."""
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
n = len(df)
eq = np.full(n, INIT, dtype=float)
cap = INIT
for i, j, ret in sorted(trades, key=lambda t: t[1]):
cap = max(cap + cap * POS * ret, 10.0)
eq[j:] = cap
s = pd.Series(eq, index=ts).resample("1D").last().reindex(IDX).ffill().bfill()
return _norm(s)
def port_metrics(members: dict[str, pd.Series], p):
"""Metriche (FULL, OOS) del portafoglio p con la STESSA matematica pesi cap
di Portfolio.backtest."""
ids = p.sleeve_ids
dr = pd.DataFrame({i: members[i].pct_change().fillna(0.0) for i in ids})
w = W.weight_vector(p.weighting, ids, dr, weights=p.weights,
caps=p.caps, clusters=p.clusters, lookback=p.vol_lookback)
drp = port_returns({i: members[i] for i in ids}, w)
return metrics(drp), metrics(drp, lo=SPLIT)
def dd(s):
"""Max drawdown % di una serie equity."""
pk = s.cummax()
return float(((pk - s) / pk).max() * 100)
+117
View File
@@ -0,0 +1,117 @@
"""ACCEL50 — Ricerca acceleratori verso l'obiettivo €50/giorno (2026-06-12).
Domanda: quali strategie/leve portano PIU' VELOCEMENTE a €50/g partendo da ~€2k?
Diario: docs/diary/2026-06-12-accel50.md. Esiti:
1. LEVA su PORT06 (acceleratore dominante, zero ricerca nuova).
La frontiera (scala lineare dei daily return canonici, fee pro-quota) mostra
che a Sharpe ~7-10 il vincolo non e' il rischio ma la taglia: lev 2->4 porta
gli anni-a-target da 3.3 a 1.2 con FULL DD 3.5->6.9%. Vedi lev_frontier().
2. FADE 15m (candidata NUOVA, validazione preliminare PASSATA).
MR01/MR02/MR07 a 15m con i parametri live 1h (trend_max=3, ema_long=200,
sl_confirm_atr=0.5, fee 0.10% RT): tutti e 6 gli sleeve positivi, OOS
2025-26 positivo ovunque (spesso > del 1h: 4x trade = compounding piu'
rapido), reggono fee 2x (Sh 1.6-2.9). BTC 15m MIGLIORA il 1h (MR01 Sh
3.37 vs 2.76 con meta' DD). Prossimo passo obbligato: gate PORT06
(correlazione col gemello 1h, parita' worker — infra 15m gia' esistente
dal BLEND pairs). Vedi fade15m_probe().
3. PAIRS NUOVE: BOCCIATE (stale-print illusion).
Lo sweep delle 19 coppie mai testate (config universale pre-registrata)
dava 8 candidate con Sharpe 1.5-4.3, MA le gambe alt hanno 88-98% di barre
flat (LTC 97%, ADA 98%, DOGE 91%, XRP 88%, BNB 88%) e con flat_skip=True
(fill solo su barre pulite) muoiono quasi tutte (BTC/ADA 4.33->0.17,
ETH/DOGE 3.79->0.46). Migliore superstite ETH/XRP a 1.34: inferiore alle
5 deployate -> niente. Stessa classe di illusione del XEX su DOGE/SOL
(vedi xex_divergence_research.py). PAXG idem: 92% flat su Deribit.
4. CAPITALE: a config attuale servono ~€24k per €50/g; ogni € aggiunto
accorcia linearmente (non e' una strategia ma domina ogni altra leva).
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
def lev_frontier() -> None:
"""Frontiera di leva su PORT06: CAGR/DD/Sharpe e anni-a-€50/g per lev 1-6.
Modello: scala lineare dei daily return del backtest canonico (strumenti
lineari, fee proporzionali al notional). NON modella margine/code grasse."""
from scripts.portfolios._defs import PORTFOLIOS
from scripts.analysis.combine_portfolio import port_returns, SPLIT
from src.portfolio.sleeves import all_sleeve_equities, sleeve_returns_df
p = PORTFOLIOS["PORT06"]
eq = all_sleeve_equities()
members = {sid: eq[sid] for sid in p.sleeve_ids}
w = p.weight_vector(sleeve_returns_df(p.sleeve_ids))
base = port_returns(members, w) # == live a lev 2 (parita' validata)
def dd(x):
c = (1 + x).cumprod()
return ((c - c.cummax()) / c.cummax()).min() * 100
def cagr(x):
c = (1 + x).cumprod()
return ((c.iloc[-1]) ** (365 / len(x)) - 1) * 100
print("lev CAGR_full% DD_full% CAGR_oos% DD_oos% K_per_50/g anni_da_2k")
for f, lev in [(0.5, 1), (1.0, 2), (1.5, 3), (2.0, 4), (2.5, 5), (3.0, 6)]:
r = base * f
roos = r.iloc[SPLIT:]
co = cagr(roos)
daily = (1 + co / 100) ** (1 / 365) - 1
k = 50 / daily if daily > 0 else float("inf")
anni = np.log(k / 2020) / np.log(1 + co / 100) if co > 0 else float("inf")
print(f"{lev:>3} {cagr(r):>11.0f} {dd(r):>9.2f} {co:>10.0f} {dd(roos):>8.2f} "
f"{k:>11,.0f} {max(anni, 0):>11.1f}")
def fade15m_probe() -> None:
"""MR01/02/07 a 15m vs 1h, parametri live, fee 0.10% e stress 2x."""
import importlib.util
import inspect
from src.strategies.base import Strategy
LIVEP = dict(trend_max=3.0, ema_long=200, sl_confirm_atr=0.5)
paths = {
"MR01": "scripts/strategies/MR01_bollinger_fade.py",
"MR02": "scripts/strategies/MR02_donchian_fade.py",
"MR07": "scripts/strategies/MR07_return_reversal.py",
}
for code, rel in paths.items():
spec = importlib.util.spec_from_file_location(code.lower(), PROJECT_ROOT / rel)
m = importlib.util.module_from_spec(spec)
spec.loader.exec_module(m)
cls = next(o for _, o in vars(m).items()
if inspect.isclass(o) and issubclass(o, Strategy) and o.__module__ == m.__name__)
s = cls()
for asset in ("BTC", "ETH"):
line = f"{code} {asset}: "
for tf in ("1h", "15m"):
r = s.backtest(asset, tf, **LIVEP)
if r is None:
line += f"{tf}: no-sig | "
continue
oos = sum(y.pnl for y in r.yearly if y.year >= 2025)
old = s.fee_rt
s.fee_rt = 0.002
r2 = s.backtest(asset, tf, **LIVEP)
s.fee_rt = old
line += (f"{tf}: Sh{r.sharpe:5.2f} DD{r.max_dd:5.1f}% n={r.trades:4d} "
f"oos25-26={oos:+8.0f} fee2x_Sh{r2.sharpe:5.2f} | ")
print(line)
if __name__ == "__main__":
print("=== 1. Frontiera di leva PORT06 ===")
lev_frontier()
print("\n=== 2. Fade 15m probe ===")
fade15m_probe()
+100
View File
@@ -0,0 +1,100 @@
"""CONFERMA su feed PURO Binance spot — la fade ha edge reale o era artefatto-print?
Il clean close-aware ha spliciato barre Binance-spot dentro la serie Deribit-perp:
il crollo del backtest potrebbe (a) rivelare la verita' (l'edge era print) o (b) essere
un artefatto dello splice (basis perp/spot ai punti di giunzione). Test decisivo:
girare lo STESSO engine fade su una serie 100% Binance spot (sorgente coerente, niente
splice). Se anche qui la fade e' negativa -> edge confermato finto.
"""
from __future__ import annotations
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
import numpy as np, pandas as pd, ccxt
from scripts.analysis.risk_management import build_trades, strats_for
EX = ccxt.binance({"enableRateLimit": True})
SYM = {"BTC": "BTC/USDT", "ETH": "ETH/USDT"}
START = "2020-06-01" # warmup per EMA200/ATR; il report usa 2021+
YEARS = [2021, 2022, 2023, 2024, 2025, 2026]
def fetch(asset, tf="15m"):
start_ms = int(pd.Timestamp(START, tz="UTC").timestamp() * 1000)
end_ms = int(pd.Timestamp("2026-05-26", tz="UTC").timestamp() * 1000)
tf_ms = 15 * 60 * 1000
rows = []
since = start_ms
while since <= end_ms:
r = EX.fetch_ohlcv(SYM[asset], tf, since=since, limit=1000)
if not r:
break
rows += r
nxt = int(r[-1][0]) + tf_ms
if nxt <= since:
break
since = nxt
df = pd.DataFrame(rows, columns=["timestamp", "open", "high", "low", "close", "volume"])
df = df.drop_duplicates("timestamp").sort_values("timestamp").reset_index(drop=True)
return df[df["timestamp"] <= end_ms]
def yearly(rows_by_year_ret, ts, trades, pos=0.15):
# per-anno compound
yr = {y: 1000.0 for y in YEARS}
# ricostruisco compound per anno separato (reset capitale ogni anno per ret% annuo)
by = {y: [] for y in YEARS}
for i, j, r in trades:
y = ts.iloc[i].year
if y in by:
by[y].append(r)
out = {}
for y in YEARS:
cap = 1000.0
for r in by[y]:
cap = max(cap + cap * pos * r, 10.0)
out[y] = (cap / 1000 - 1) * 100
return out
def full_oos(ts, trades, pos=0.15, split_date="2024-10-12"):
sd = pd.Timestamp(split_date, tz="UTC")
def comp(sub):
cap = 1000.0; rets = []
for i, j, r in sub:
cap = max(cap + cap * pos * r, 10.0); rets.append(r * pos)
return cap, rets
capF, rF = comp(trades)
oos = [(i, j, r) for i, j, r in trades if ts.iloc[i] >= sd]
capO, rO = comp(oos)
shF = float(np.mean(rF) / np.std(rF) * np.sqrt(len(rF))) if len(rF) > 1 and np.std(rF) > 0 else 0.0
shO = float(np.mean(rO) / np.std(rO) * np.sqrt(len(rO))) if len(rO) > 1 and np.std(rO) > 0 else 0.0
return (capF / 1000 - 1) * 100, shF, (capO / 1000 - 1) * 100, shO
def main():
print(f"Fetch Binance 15m (da {START})...\n")
data = {a: fetch(a) for a in ("BTC", "ETH")}
print("=" * 92)
print(" FADE su PURO Binance spot 15m | RET% per anno (pos 0.15, leva 3x, trend 3.0)")
print("=" * 92)
print(f" {'sleeve':<12s}" + "".join(f"{y:>9d}" for y in YEARS) + " | FULL% Shrp | OOS% Shrp")
print(" " + "-" * 88)
for asset in ("BTC", "ETH"):
df = data[asset].copy()
df = df[pd.to_datetime(df["timestamp"], unit="ms", utc=True).dt.year >= 2021].reset_index(drop=True) \
if False else df # tengo il warmup, filtro nei trade
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
for code in ("MR01", "MR02", "MR07"):
fn, params = strats_for(asset)[code]
trades = build_trades(fn(df, **params), df, trend_max=3.0)
trades = [(i, j, r) for i, j, r in trades if ts.iloc[i].year >= 2021]
yr = yearly(None, ts, trades)
fF, shF, fO, shO = full_oos(ts, trades)
print(f" {code+'_'+asset:<12s}" + "".join(f"{yr[y]:>+9.0f}" for y in YEARS) +
f" | {fF:>+8.0f} {shF:>5.2f} | {fO:>+6.0f} {shO:>5.2f}")
if __name__ == "__main__":
main()
@@ -0,0 +1,144 @@
"""Validazione dell'edge del credit-spread di cerbero-bite sui PREZZI REALI.
cerbero-bite (container accanto) vende credit spread su ETH (bull-put primario,
short delta ~0.18, DTE 18, PT 50% / stop 2.5x credito / delta-breach 0.30 / vol-stop
+10 DVOL / time-stop 7 DTE). Domanda: l'edge regge su un CICLO ETH completo, o e'
profittevole solo nei campioni calmi?
Tre analisi (riprendibili):
1) entry_economics() -> economia d'ingresso REALE dalla chain (data/options/eth_chain.parquet):
credit/width effettivo a delta 0.18 dai bid/ask veri, eleggibilita' sotto i gate liquidita'.
2) tail_model_free() -> esito terminale dai prezzi ETH reali (2018-2026), cw reale 0.106,
NESSUN modello opzioni (niente errore BS): win-rate, EV, frequenza max-loss.
3) managed_backtest() -> lifecycle CON management; mark con skew calibrato sulle IV reali.
ESITO (2026-06-09):
- cw reale a delta 0.18 = 0.106 (short ~9.4% OTM, NON 18%), max-loss/credito = 8.4x, eleggibilita' 65%.
- hold-to-expiry @0.106: EV -1.0 crediti/trade, 7/9 anni NEGATIVI, max-loss 17.8% delle volte.
- managed (skew): EV -0.02 cr/trade, win-rate 37% (delta-breach esce sul 62% dei trade a piccola perdita).
- VERDETTO: NON edge robusto su ciclo completo. Il "+0.48%/mese" era artefatto di finestra calma
(mag-giu 2026, no crash). Premium-selling a skew negativo: vince nei campioni calmi, restituisce
tutto (o piu') nei crash. Tune "Profilo B" (vendere a 9.4% OTM) PEGGIORA la frequenza di max-loss.
Coda CONCENTRATA col fade ETH di PythagorasGoal (stesso crash colpisce entrambi).
TODO APERTO (per nail-are l'EV managed esatto): la calibrazione non e' ancora perfetta
(mark mid+skew da cw 0.228 vs 0.106 reale -> sovrastima il credito ~2x). Manca: modellare
bid/ask reale incrociato sulle 2 gambe + griglia strike reale (entrambi nella chain) cosi'
l'entry cw scende a 0.106 e l'EV managed diventa esatto. Allora chiudere il sì/no definitivo.
uv run python scripts/analysis/cerbero_bite_credit_spread.py
"""
from __future__ import annotations
import sys, math, collections
from pathlib import Path
import numpy as np, pandas as pd
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
from scripts.analysis.options_chain import OptionChain, load_market
from scripts.analysis.explore_lab import get_df
from scripts.analysis.option_overlay_lab import bs_put, _ncdf, dvol_for
SHORT_OTM, LONG_OTM, DTE = 0.094, 0.134, 17 # da chain reale (delta 0.18, width 4%)
CW_REAL = 0.106
def entry_economics():
oc = OptionChain("ETH"); ch = oc.df
mk = load_market("ETH")[["ts_ms", "spot"]].dropna().sort_values("ts_ms")
p = ch[ch["option_type"] == "P"].copy()
p = pd.merge_asof(p.sort_values("ts_ms"), mk, on="ts_ms", direction="backward")
cand = p[(p["tenor_d"] >= 14) & (p["tenor_d"] <= 21)].dropna(subset=["delta", "bid", "ask", "strike", "spot"])
rows = []
for (ts, exp), g in cand.groupby(["timestamp", "expiry"]):
spot = g["spot"].iloc[0]
sc = g[(g["delta"] <= -0.12) & (g["delta"] >= -0.22)]
if sc.empty: continue
short = sc.iloc[(sc["delta"] + 0.18).abs().argmin()]
Ks = short["strike"]; longc = g[g["strike"] < Ks]
if longc.empty: continue
longp = longc.iloc[(longc["strike"] - (Ks - spot * 0.04)).abs().argmin()]
W = Ks - longp["strike"]
if W <= 0: continue
credit = short["bid"] - longp["ask"]
def ok(o):
sp = (o["ask"] - o["bid"]) / ((o["ask"] + o["bid"]) / 2) if (o["ask"] + o["bid"]) > 0 else 9
return (o["open_interest"] or 0) >= 100 and sp <= 0.15 and o["bid"] > 0
cw = credit / (W / spot)
rows.append(dict(cw=cw, credit=credit, elig=ok(short) and ok(longp) and cw >= 0.08 and credit > 0,
short_otm=(spot - Ks) / spot, delta=short["delta"]))
r = pd.DataFrame(rows)
print(f"[ENTRY] {len(r)} spread | eleggibili {r['elig'].mean()*100:.0f}% | cw mediano {r['cw'].median():.3f} "
f"| short OTM {r['short_otm'].median()*100:.1f}% | max-loss/credito {((1-r['cw'].median())/r['cw'].median()):.1f}x")
def tail_model_free():
df = get_df("ETH", "1h"); c = df["close"].values; n = len(c)
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True); H = DTE * 24
res = []
for i in range(200, n - H - 1, 24 * 2):
S0 = c[i]; Ks = S0 * (1 - SHORT_OTM); Kl = S0 * (1 - LONG_OTM); W = Ks - Kl
Sx = c[i + H]; intr = min(max(Ks - Sx, 0.0), W); credit = CW_REAL * W
res.append((ts.iloc[i].year, 1 - intr / credit, Sx < Kl))
R = pd.DataFrame(res, columns=["y", "pnl", "maxloss"]); P = R["pnl"].values
print(f"[TAIL model-free @cw0.106] win {(P>0).mean()*100:.0f}% | EV {P.mean():+.2f}cr | max-loss {R['maxloss'].mean()*100:.0f}% "
f"| anni neg {(R.groupby('y')['pnl'].mean()<0).sum()}/{R['y'].nunique()}")
def _skew_fit():
oc = OptionChain("ETH"); ch = oc.df
mk = load_market("ETH")[["ts_ms", "spot"]].dropna().sort_values("ts_ms")
p = ch[ch["option_type"] == "P"].copy()
p = pd.merge_asof(p.sort_values("ts_ms"), mk, on="ts_ms", direction="backward")
p = p.dropna(subset=["iv", "strike", "spot", "delta", "tenor_d"])
p = p[(p["tenor_d"] >= 7) & (p["tenor_d"] <= 35) & (p["iv"] > 0)]
p["dd"] = (p["delta"] + 0.5).abs()
atm = p.sort_values("dd").groupby("timestamp")["iv"].first()
p["atm_iv"] = p["timestamp"].map(atm); p = p.dropna(subset=["atm_iv"])
p["k"] = np.log(p["strike"] / p["spot"]); p["ratio"] = p["iv"] / p["atm_iv"]
p = p[(p["k"] > -0.35) & (p["k"] < 0.15) & (p["ratio"] > 0.5) & (p["ratio"] < 3)]
coef, *_ = np.linalg.lstsq(np.c_[p["k"], p["k"]**2], p["ratio"] - 1.0, rcond=None)
return coef # a, b
def managed_backtest():
a, b = _skew_fit()
def ivol(S, K, atm):
k = math.log(K / S); return max(atm * (1 + a * k + b * k * k), 0.05)
def put_delta(S, K, T, sig):
if T <= 0 or sig <= 0: return -1.0 if S < K else 0.0
return _ncdf((math.log(S / K) + 0.5 * sig * sig * T) / (sig * math.sqrt(T))) - 1.0
def mark(S, Ks, Kl, T, atm):
return bs_put(S, Ks, T, ivol(S, Ks, atm)) - bs_put(S, Kl, T, ivol(S, Kl, atm))
df = get_df("ETH", "1h"); c = df["close"].values; n = len(c)
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True); dvol = dvol_for(df, "ETH")
H = DTE * 24; STEP = 6; cw = []; tr = []
for i in range(200, n - H - 1, 24 * 2):
S0 = c[i]; atm0 = dvol[i] if not np.isnan(dvol[i]) else 0.6
Ks = S0 * (1 - SHORT_OTM); Kl = S0 * (1 - LONG_OTM); W = Ks - Kl
credit = mark(S0, Ks, Kl, DTE / 365.0, atm0)
if credit <= 0: continue
cw.append(credit / W); pnl = why = None
for k in range(STEP, H + 1, STEP):
j = i + k; Trem = max((H - k) / (24 * 365.0), 1e-6); Sj = c[j]
atmj = dvol[j] if not np.isnan(dvol[j]) else atm0; mk = mark(Sj, Ks, Kl, Trem, atmj)
if mk <= 0.5 * credit: pnl, why = 1 - mk / credit, "PT"; break
if mk >= 2.5 * credit: pnl, why = 1 - mk / credit, "stop"; break
if put_delta(Sj, Ks, Trem, ivol(Sj, Ks, atmj)) <= -0.30: pnl, why = 1 - mk / credit, "delta"; break
if atmj - atm0 >= 0.10: pnl, why = 1 - mk / credit, "vol"; break
if k >= (DTE - 7) * 24: pnl, why = 1 - mk / credit, "time"; break
if pnl is None:
Sx = c[i + H]; intr = min(max(Ks - Sx, 0), W); pnl, why = 1 - intr / credit, "expiry"
tr.append((ts.iloc[i].year, pnl, why))
P = np.array([t[1] for t in tr])
print(f"[MANAGED skew] cw@entry {np.median(cw):.3f} (vs 0.106 reale: sovrastima ~2x, EV vero <=) | "
f"win {(P>0).mean()*100:.0f}% | EV {P.mean():+.3f}cr | worst {P.min():.1f} | "
f"uscite {dict(collections.Counter(t[2] for t in tr))}")
R = pd.DataFrame({"y": [t[0] for t in tr], "p": P})
print(f" 2021+: EV {R[R.y>=2021]['p'].mean():+.3f}cr/trade")
if __name__ == "__main__":
entry_economics()
tail_model_free()
managed_backtest()
+77
View File
@@ -0,0 +1,77 @@
"""PROBE CERBERO MCP — quali exchange/fonti serve davvero? (cerca IBKR & alt)
Non si fida del commento nel codice: interroga il server v2 (/mcp/tools/get_historical
con `exchange=...`) su una matrice di nomi exchange + naming strumento, e riporta chi
risponde con candele vere. Cerca in particolare IBKR e Alpaca (spot US reale).
uv run python scripts/analysis/cerbero_probe.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 requests
from src.live.cerbero_client import CerberoClient, is_mainnet
C = CerberoClient()
START, END, INTERVAL = "2026-05-20", "2026-05-27", "1h"
# exchange -> naming strumento BTC da provare (varianti)
EXCHANGES = {
"deribit": ["BTC-PERPETUAL"],
"hyperliquid": ["BTC"],
"bybit": ["BTCUSDT", "BTC-PERPETUAL", "BTCUSD"],
"alpaca": ["BTC/USD", "BTCUSD", "BTC/USDT", "BTC"],
"ibkr": ["BTC", "BTC.USD", "BTCUSD"],
"interactivebrokers": ["BTC", "BTCUSD"],
"binance": ["BTCUSDT", "BTC/USDT", "BTC-PERPETUAL"],
"coinbase": ["BTC-USD", "BTC/USD", "BTCUSD"],
"kraken": ["XBTUSD", "BTC/USD", "BTCUSD"],
"okx": ["BTC-USDT", "BTC-USD-SWAP"],
}
def try_v2(exchange: str, instrument: str) -> str:
try:
candles = C.get_historical_v2(instrument, START, END, INTERVAL, exchange=exchange)
if candles:
c0, c1 = candles[0], candles[-1]
return f"OK {len(candles):>4} candele close {c1.get('close')}"
return "vuoto (0 candele)"
except requests.HTTPError as e:
code = e.response.status_code if e.response is not None else "?"
return f"HTTP {code}"
except Exception as e:
return f"{type(e).__name__}: {str(e)[:50]}"
def list_tools() -> None:
"""Tenta di enumerare i tool/endpoint del server (best-effort)."""
for path in ("/mcp/tools", "/mcp/tools/list", "/tools", "/mcp"):
try:
r = requests.post(f"{C.base_url}{path}", headers=C._headers(), json={}, timeout=10)
print(f" POST {path:<18} -> HTTP {r.status_code} {str(r.text)[:200]}")
except Exception as e:
print(f" POST {path:<18} -> {type(e).__name__}: {str(e)[:60]}")
def main():
print("=" * 80)
print(f" PROBE CERBERO MCP @ {C.base_url} (mainnet={is_mainnet()})")
print("=" * 80)
print("\n[1] Enumerazione endpoint/tool (best-effort):")
list_tools()
print(f"\n[2] get_historical_v2 BTC {INTERVAL} {START}->{END} per exchange:")
print(f" {'exchange':<20s}{'instrument':<16s}esito")
print(" " + "-" * 70)
for ex, syms in EXCHANGES.items():
for sym in syms:
res = try_v2(ex, sym)
print(f" {ex:<20s}{sym:<16s}{res}")
if res.startswith("OK"):
break # trovato il naming giusto, basta
if __name__ == "__main__":
main()
+84
View File
@@ -0,0 +1,84 @@
"""RI-ESECUZIONE FADE sul feed PULITO (data/raw ricostruito da Deribit mainnet, 2026-06-19).
Dopo il rebuild (scripts/analysis/rebuild_history.py) i parquet canonici in data/raw
sono storia Deribit mainnet reale (ccxt pubblico), certificata vs Coinbase USD. Qui giro
le 6 fade (MR01/MR02/MR07 x BTC/ETH) con l'ENGINE CANONICO (risk_management.build_trades,
strats_for) sul feed pulito, su ENTRAMBI i timeframe:
- 1h = config dei claim storici "validati OOS" (CLAUDE.md: MR01 BTC +201% / ETH +1238%)
- 15m = config LIVE attuale (swap 1h->15m, v1.1.30)
Stessi parametri del live: pos 0.15, leva 3x, trend_max 3.0, fee 0.10% RT. OOS = ultimo 30%
per indice (convenzione OOS_FRAC del progetto). Read-only, nessuna scrittura.
uv run python scripts/analysis/clean_fade_rerun.py
"""
from __future__ import annotations
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
import numpy as np
import pandas as pd
from src.data.downloader import load_data
from scripts.analysis.risk_management import strats_for, build_trades, POS, LEV, OOS_FRAC
TFS = ["1h", "15m"]
YEARS = list(range(2018, 2027))
def metrics(ts, trades, split_idx, pos=POS):
"""trades = [(i, j, r_netto)]. Ritorna (per-anno%, FULL%, FULL Sharpe, OOS%, OOS Sharpe)."""
by = {y: 0.0 for y in YEARS}
capF = capO = 1000.0
rF, rO = [], []
for i, j, r in trades:
y = ts.iloc[i].year
if y in by:
by[y] += r * pos * 1000.0 # contributo lineare per la riga annuale
capF = max(capF + capF * pos * r, 10.0)
rF.append(r * pos)
if i >= split_idx:
capO = max(capO + capO * pos * r, 10.0)
rO.append(r * pos)
yr = {y: by[y] / 1000.0 * 100 for y in YEARS}
shF = float(np.mean(rF) / np.std(rF) * np.sqrt(len(rF))) if len(rF) > 1 and np.std(rF) > 0 else 0.0
shO = float(np.mean(rO) / np.std(rO) * np.sqrt(len(rO))) if len(rO) > 1 and np.std(rO) > 0 else 0.0
return yr, (capF / 1000 - 1) * 100, shF, (capO / 1000 - 1) * 100, shO
def main():
years_present = set()
results = {}
for tf in TFS:
for asset in ("BTC", "ETH"):
df = load_data(asset, tf)
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
years_present |= set(ts.dt.year.unique().tolist())
split_idx = int(len(df) * (1 - OOS_FRAC))
cov = f"{ts.iloc[0].date()} -> {ts.iloc[-1].date()} ({len(df)} barre, OOS da {ts.iloc[split_idx].date()})"
for code in ("MR01", "MR02", "MR07"):
fn, params = strats_for(asset)[code]
trades = build_trades(fn(df, **params), df, trend_max=3.0)
results[(tf, asset, code)] = (metrics(ts, trades, split_idx), len(trades), cov)
years = [y for y in YEARS if y in years_present]
for tf in TFS:
print("\n" + "=" * (62 + 9 * len(years)))
print(f" FADE su FEED PULITO (Deribit mainnet) — {tf} | pos {POS}, leva {LEV:.0f}x, trend 3.0, fee 0.10% RT")
# mostra la copertura una volta per asset
for asset in ("BTC", "ETH"):
print(f" {asset}: {results[(tf, asset, 'MR01')][2]}")
print("=" * (62 + 9 * len(years)))
print(f" {'sleeve':<11s}" + "".join(f"{y:>9d}" for y in years) +
f"{'Trd':>7s}{'FULL%':>9s}{'Shrp':>7s}{'OOS%':>8s}{'Shrp':>7s}")
print(" " + "-" * (60 + 9 * len(years)))
for asset in ("BTC", "ETH"):
for code in ("MR01", "MR02", "MR07"):
(yr, fF, shF, fO, shO), ntr, _ = results[(tf, asset, code)]
print(f" {code+'_'+asset:<11s}" + "".join(f"{yr[y]:>+9.0f}" for y in years) +
f"{ntr:>7d}{fF:>+9.0f}{shF:>7.2f}{fO:>+8.0f}{shO:>7.2f}")
print()
if __name__ == "__main__":
main()
+274
View File
@@ -0,0 +1,274 @@
"""CLEAN FEED — ripara gli spike-print del feed Deribit/Cerbero coi dati reali di Binance.
Motivo (2026-06-18): la ricerca Price Ladder ha rivelato che data/raw/btc_1h.parquet (e gli
altri TF/asset) contengono barre con WICK FASULLI (es. BTC 2024-02-13: low 38.580 con
close ~49.968, BTC reale ~50k) — lo stesso spike-print testnet documentato in CLAUDE.md
(TP_PHANTOM / feed congelato). Sono pochi (decine per file) ma avvelenano i backtest
(stop/entry su prezzi mai avvenuti) e gonfiano le code (la "FULL DD BTC ~54%" del ladder era
in gran parte questo).
Metodo (conservativo, fonte di verita' = Binance spot via ccxt, gia' cablato nel progetto):
1. DETECT: barra sospetta = high/low che sfora >15% il cluster di close locale [i-1,i,i+1]
(close sano + wick fasullo). Soglia larga: tanto e' Binance ad arbitrare.
2. ARBITRA: per ogni sospetta, scarica la barra Binance reale (BTC/USDT, ETH/USDT) allo
stesso tf/timestamp. Sostituisce O/H/L/C SOLO se Binance dissente materialmente (>2% su
high o low) -> un wick VERO confermato da Binance resta intatto. Volume/timestamp invariati.
3. BACKUP (data/_feed_backup/) + scrittura atomica + VALIDAZIONE (re-scan = 0 sospette,
n righe invariato). Log dettagliato di ogni barra riparata (old OHLC -> new).
uv run python scripts/analysis/clean_feed.py [ASSET_TF ...] # default: tutti BTC/ETH x TF
uv run python scripts/analysis/clean_feed.py BTC_1h # un solo file
"""
from __future__ import annotations
import shutil
import sys
import time
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.data.downloader import _parquet_path, DATA_DIR
BACKUP = PROJECT_ROOT / "data" / "_feed_backup"
SYMBOL = {"BTC": "BTC/USDT", "ETH": "ETH/USDT"}
WICK_THR = 0.15 # detect: wick oltre 15% il cluster di close locale
REPLACE_THR = 0.02 # arbitra: sostituisci solo se Binance dissente >2% su high/low
CLOSE_THR = 0.01 # close-aware: sostituisci la barra se il CLOSE Deribit dista >1% da Binance
TF_MS = {"5m": 5, "8m": 8, "13m": 13, "15m": 15, "19m": 19, "30m": 30, "1h": 60}
_EX = None
def _binance():
global _EX
if _EX is None:
import ccxt
_EX = ccxt.binance({"enableRateLimit": True})
return _EX
def suspect_mask(df: pd.DataFrame) -> np.ndarray:
c = df["close"].to_numpy(float); h = df["high"].to_numpy(float); l = df["low"].to_numpy(float)
cp = np.roll(c, 1); cp[0] = c[0]; cn = np.roll(c, -1); cn[-1] = c[-1]
locmax = np.maximum.reduce([c, cp, cn]); locmin = np.minimum.reduce([c, cp, cn])
return (h > locmax * (1 + WICK_THR)) | (l < locmin * (1 - WICK_THR))
def _binance_bar(symbol: str, tf: str, ts_ms: int):
"""OHLC reale Binance alla barra ts_ms (None se assente)."""
try:
rows = _binance().fetch_ohlcv(symbol, tf, since=ts_ms - 1, limit=3)
except Exception as e:
print(f" ! binance err: {type(e).__name__}: {str(e)[:80]}")
return None
for r in rows:
if int(r[0]) == ts_ms:
return float(r[1]), float(r[2]), float(r[3]), float(r[4])
return None
def clean_file(asset: str, tf: str) -> dict:
path = _parquet_path(asset, tf)
if not path.exists():
return {"file": f"{asset}_{tf}", "skip": "no-file"}
df = pd.read_parquet(path)
mask = suspect_mask(df)
idx = np.where(mask)[0]
n0 = len(df)
if len(idx) == 0:
return {"file": f"{asset}_{tf}", "suspect": 0, "repaired": 0, "kept_real": 0,
"missing_binance": 0, "rows_before": n0, "rows_after": n0,
"still_suspect": 0, "log": []}
repaired, kept, missing = 0, 0, 0
log = []
for i in idx:
ts = int(df.iloc[i]["timestamp"])
b = _binance_bar(SYMBOL[asset], tf, ts)
oh, ol = float(df.iloc[i]["high"]), float(df.iloc[i]["low"])
if b is None:
missing += 1
continue
bo, bh, bl, bc = b
if abs(oh - bh) / bh > REPLACE_THR or abs(ol - bl) / max(bl, 1e-9) > REPLACE_THR:
df.iat[i, df.columns.get_loc("open")] = bo
df.iat[i, df.columns.get_loc("high")] = bh
df.iat[i, df.columns.get_loc("low")] = bl
df.iat[i, df.columns.get_loc("close")] = bc
repaired += 1
ts_s = pd.to_datetime(ts, unit="ms", utc=True).strftime("%Y-%m-%d %H:%M")
log.append(f" {ts_s} H {oh:,.0f}->{bh:,.0f} L {ol:,.0f}->{bl:,.0f}")
else:
kept += 1 # Binance conferma il wick: barra reale, intatta
if repaired:
BACKUP.mkdir(parents=True, exist_ok=True)
shutil.copy2(path, BACKUP / f"{asset.lower()}_{tf}.parquet.bak")
tmp = path.with_suffix(".parquet.tmp")
df.to_parquet(tmp, index=False)
tmp.replace(path)
# validazione
df2 = pd.read_parquet(path)
still = int(suspect_mask(df2).sum())
return {"file": f"{asset}_{tf}", "suspect": len(idx), "repaired": repaired,
"kept_real": kept, "missing_binance": missing, "rows_before": n0,
"rows_after": len(df2), "still_suspect": still, "log": log}
def _binance_series(asset: str, tf: str, start_ms: int, end_ms: int) -> dict:
"""OHLC reale Binance per l'intero range -> dict ts_ms -> (o,h,l,c). Bulk paginato."""
ex = _binance()
tf_ms = TF_MS[tf] * 60 * 1000
out: dict[int, tuple] = {}
since = start_ms
while since <= end_ms:
try:
rows = ex.fetch_ohlcv(SYMBOL[asset], tf, since=since, limit=1000)
except Exception as e:
print(f" ! binance err: {type(e).__name__}: {str(e)[:80]}")
break
if not rows:
break
for r in rows:
out[int(r[0])] = (float(r[1]), float(r[2]), float(r[3]), float(r[4]))
nxt = int(rows[-1][0]) + tf_ms
if nxt <= since:
break
since = nxt
if len(rows) < 1000 and since > end_ms:
break
return out
def clean_file_close(asset: str, tf: str, thr: float = CLOSE_THR, backup_dir: Path | None = None) -> dict:
"""CLOSE-AWARE: sostituisce O/H/L/C con Binance per ogni barra il cui CLOSE Deribit
dista > thr da Binance (1% default). Cattura i print 'silenziosi' che il wick-check
>15% non vede (close fantasma su barra di range piccolo). Fonte di verita' = Binance
spot (il feed storico e' perp testnet -> inaffidabile; lo spot ~ mainnet via arbitraggio)."""
if tf not in TF_MS:
return {"file": f"{asset}_{tf}", "skip": "tf-non-binance"}
path = _parquet_path(asset, tf)
if not path.exists():
return {"file": f"{asset}_{tf}", "skip": "no-file"}
df = pd.read_parquet(path)
n0 = len(df)
tms = df["timestamp"].to_numpy("int64")
c = df["close"].to_numpy(float)
bz = _binance_series(asset, tf, int(tms[0]), int(tms[-1]))
col = {k: df.columns.get_loc(k) for k in ("open", "high", "low", "close")}
fixed, by_year, missing = 0, {}, 0
log = []
for i in range(n0):
b = bz.get(int(tms[i]))
if b is None:
missing += 1
continue
bo, bh, bl, bc = b
if bc <= 0:
continue
orig = float(c[i]) # cattura PRIMA della scrittura (to_numpy puo' essere una view)
if abs(orig - bc) / bc > thr:
df.iat[i, col["open"]] = bo
df.iat[i, col["high"]] = bh
df.iat[i, col["low"]] = bl
df.iat[i, col["close"]] = bc
fixed += 1
y = pd.to_datetime(int(tms[i]), unit="ms", utc=True).year
by_year[y] = by_year.get(y, 0) + 1
if len(log) < 10:
ts_s = pd.to_datetime(int(tms[i]), unit="ms", utc=True).strftime("%Y-%m-%d %H:%M")
log.append(f" {ts_s} C {orig:,.2f}->{bc:,.2f} ({abs(orig-bc)/bc*100:.1f}%)")
if fixed:
bdir = backup_dir or BACKUP
bdir.mkdir(parents=True, exist_ok=True)
shutil.copy2(path, bdir / f"{asset.lower()}_{tf}.parquet.bak")
tmp = path.with_suffix(".parquet.tmp")
df.to_parquet(tmp, index=False)
tmp.replace(path)
# validazione: ri-scan, 0 barre residue oltre soglia (fra quelle coperte da Binance)
df2 = pd.read_parquet(path)
c2 = df2["close"].to_numpy(float)
still = sum(1 for i in range(len(df2))
if (b := bz.get(int(tms[i]))) and b[3] > 0 and abs(c2[i] - b[3]) / b[3] > thr)
return {"file": f"{asset}_{tf}", "covered": n0 - missing, "fixed": fixed,
"missing_binance": missing, "rows_before": n0, "rows_after": len(df2),
"still_over_thr": still, "by_year": by_year, "log": log}
def main():
args = [a for a in sys.argv[1:] if not a.startswith("--")]
close_mode = "--close" in sys.argv
dry = "--dry" in sys.argv
if close_mode:
targets = args or [f"{a}_{tf}" for a in ("BTC", "ETH") for tf in ("5m", "15m", "1h")]
stamp = time.strftime("%Y%m%d-%H%M%S")
bdir = BACKUP / f"pre_close_clean_{stamp}"
print(f"CLEAN FEED (close-aware vs Binance, thr={CLOSE_THR*100:.0f}%) — "
f"{'DRY-RUN (nessuna scrittura)' if dry else f'backup in {bdir}'}\n")
grand = 0
for t in targets:
asset, tf = t.split("_", 1)
if dry:
# dry: conta soltanto, niente scrittura
r = _dry_close(asset, tf)
else:
r = clean_file_close(asset, tf, backup_dir=bdir)
if r.get("skip"):
print(f" {t:<9} SKIP ({r['skip']})"); continue
grand += r.get("fixed", 0)
yr = " ".join(f"{y}:{n}" for y, n in sorted(r.get("by_year", {}).items()))
print(f" {r['file']:<9} coperte={r.get('covered',0):>7} riparate={r.get('fixed',0):>4} "
f"no-binance={r.get('missing_binance',0):>5} | righe {r['rows_before']}=={r.get('rows_after',r['rows_before'])} "
f"residue>thr={r.get('still_over_thr','-')}")
if yr:
print(f" per anno: {yr}")
for line in r.get("log", []):
print(line)
print(f"\n TOTALE barre riparate (close-aware): {grand}")
return
targets = args or [f"{a}_{tf}" for a in ("BTC", "ETH") for tf in ("5m", "15m", "30m", "1h")]
print(f"CLEAN FEED — backup in {BACKUP}\n")
grand = 0
for t in targets:
asset, tf = t.split("_", 1)
r = clean_file(asset, tf)
if r.get("skip"):
print(f" {t:<9} SKIP ({r['skip']})"); continue
grand += r.get("repaired", 0)
print(f" {r['file']:<9} sospette={r['suspect']:>3} riparate={r['repaired']:>3} "
f"reali-tenute={r.get('kept_real',0):>3} no-binance={r.get('missing_binance',0):>2} "
f"| righe {r['rows_before']}=={r['rows_after']} residue-sospette={r['still_suspect']}")
for line in r.get("log", [])[:8]:
print(line)
if len(r.get("log", [])) > 8:
print(f" ... (+{len(r['log'])-8} altre)")
print(f"\n TOTALE barre riparate: {grand}")
def _dry_close(asset: str, tf: str, thr: float = CLOSE_THR) -> dict:
"""Conta soltanto quante barre verrebbero riparate (nessuna scrittura)."""
if tf not in TF_MS:
return {"file": f"{asset}_{tf}", "skip": "tf-non-binance"}
path = _parquet_path(asset, tf)
if not path.exists():
return {"file": f"{asset}_{tf}", "skip": "no-file"}
df = pd.read_parquet(path)
tms = df["timestamp"].to_numpy("int64"); c = df["close"].to_numpy(float)
bz = _binance_series(asset, tf, int(tms[0]), int(tms[-1]))
fixed, by_year, missing = 0, {}, 0
for i in range(len(df)):
b = bz.get(int(tms[i]))
if b is None:
missing += 1; continue
if b[3] > 0 and abs(c[i] - b[3]) / b[3] > thr:
fixed += 1
y = pd.to_datetime(int(tms[i]), unit="ms", utc=True).year
by_year[y] = by_year.get(y, 0) + 1
return {"file": f"{asset}_{tf}", "covered": len(df) - missing, "fixed": fixed,
"missing_binance": missing, "rows_before": len(df), "by_year": by_year, "log": []}
if __name__ == "__main__":
main()
@@ -40,9 +40,17 @@ ANN = 365.0 # giorni/anno per annualizzare
# ---------------- equity giornaliere ----------------
def fade_daily_equity(asset: str, fn, params) -> pd.Series:
"""Equity giornaliera di uno sleeve fade: trade 1h (filtro trend 3.0) -> equity -> daily."""
df = load_data(asset, "1h")
# SWAP fade 1h -> 15m (2026-06-12, scelta utente). Gate fade15m_port06_gate.py: parametri
# 1h NON ri-tunati trasferiti a 15m, corr 15m-1h media 0.26, SWAP promosso (FULL CAGR
# 74->101% DD 3.46->2.47%, OOS Sh 10.07->10.86; OOS DD 1.48->2.09 accettato), edge ETH
# regge il flat-entry-skip. Il canonico segue il deployato per tenere la parita' delle
# due facce. Diario docs/diary/2026-06-12-fade15m-gate.md.
FADE_TF = "15m"
def fade_daily_equity(asset: str, fn, params, tf: str = FADE_TF) -> pd.Series:
"""Equity giornaliera di uno sleeve fade: trade (filtro trend 3.0) -> equity -> daily."""
df = load_data(asset, tf)
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
trades = build_trades(fn(df, **params), df, trend_max=3.0)
n = len(df); eq = np.full(n, INIT, dtype=float); cap = INIT
@@ -55,7 +63,7 @@ def fade_daily_equity(asset: str, fn, params) -> pd.Series:
def build_all_sleeves() -> dict[str, pd.Series]:
sleeves: dict[str, pd.Series] = {}
# --- FADE: 8 sleeve ---
# --- FADE: 6 sleeve (15m dal 2026-06-12, vedi FADE_TF) ---
for asset in ["BTC", "ETH"]:
for nm, (fn, params) in strats_for(asset).items():
sleeves[f"{nm}_{asset}"] = fade_daily_equity(asset, fn, params)
@@ -0,0 +1,209 @@
"""ANALISI DI IMPATTO (sola lettura, da docs/TODO.md): bug bfill di `_daily_equity`.
IL BUG (scripts/analysis/honest_improve2.py:30):
daily = s.resample("1D").last().reindex(idx).ffill().bfill()
La serie `s` e' a PUNTI-TRADE (un valore di capitale per ogni exit). Il `reindex(idx)`
taglia PRIMA di forward-fillare: i giorni di IDX precedenti al primo trade DENTRO la
finestra restano NaN (il ffill non ha un valore precedente in-finestra da propagare) e
il `.bfill()` finale li riempie col capitale DOPO il primo trade in-finestra. Effetti:
1. l'ancora a idx[0] e' il capitale post-primo-trade-in-finestra, NON il capitale
portato avanti dall'ultimo trade PRIMA della finestra;
2. il rendimento del primo trade in-finestra viene CANCELLATO dalla serie daily
(la testa e' piatta al valore post-trade -> pct_change = 0 anche il giorno del trade).
CORREZIONE (qui, solo per confronto): ffill PRIMA del reindex (carry-forward su tutta la
storia trade) + testa pre-primo-trade-assoluto = capitale iniziale 1000. MAI valori dal futuro.
Sleeve canonici interessati (serie a punti-trade -> testa di IDX scoperta):
DIP01_BTC, PR_ETHBTC, PR_ETHBTC_15M, PR_LTCETH, PR_ADAETH, PR_BTCLTC, PR_ETHSOL,
TSM01, XS01 (questi due quasi-densi: punti daily/12h -> impatto atteso ~0).
TR01_basket / ROT02_rot passano da _daily_equity ma con punti PER-BARRA (densi dal
2018) -> verificati comunque qui via monkeypatch runtime (nessun file canonico toccato).
I fade (combine_portfolio.py:52) e SH01 (shape_ml_validate.py:124) usano lo stesso
pattern reindex+bfill ma su equity PER-BARRA con dati che iniziano prima di IDX[0]
-> il bfill e' un no-op (verificato: nessun NaN in testa).
NB: le metriche OOS canoniche affettano la STESSA serie daily a SPLIT (metrics(dr,
lo=SPLIT)); la distorsione sta solo in testa (2021) -> l'OOS e' invariato per
costruzione se il primo trade in-finestra precede lo SPLIT. Questo script lo misura.
Uso: uv run python scripts/analysis/daily_equity_bfill_impact.py
"""
from __future__ import annotations
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))
import scripts.analysis.honest_improve2 as hi2
from scripts.analysis.honest_improve2 import _norm, dip_market_gated
from scripts.analysis.combine_portfolio import IDX, SPLIT, OOS_DATE, metrics, port_returns
from scripts.analysis.pairs_research import pairs_sim, pairs_sim_flat
from scripts.analysis.tsmom_research import tsmom_sim
from scripts.strategies.PR01_pairs_reversion import PAIRS as PAIR_DEFS
from scripts.strategies.XS01_cross_sectional import xsec_sim
from scripts.portfolios._defs import PORTFOLIOS
from src.portfolio import weighting as W
INIT = 1000.0
# ---------------- le due convenzioni ----------------
def daily_equity_buggy(ts_list, cap_list, idx):
"""Replica ESATTA di honest_improve2._daily_equity (per parity-check)."""
s = pd.Series(cap_list, index=pd.to_datetime(ts_list, utc=True))
s = s[~s.index.duplicated(keep="last")].sort_index()
return s.resample("1D").last().reindex(idx).ffill().bfill()
def daily_equity_fixed(ts_list, cap_list, idx, init=INIT):
"""CORRETTA: ancora = capitale portato avanti dall'ultimo trade PRIMA della
finestra (ffill prima del reindex); pre-primo-trade assoluto = capitale iniziale."""
s = pd.Series(cap_list, index=pd.to_datetime(ts_list, utc=True))
s = s[~s.index.duplicated(keep="last")].sort_index()
daily = s.resample("1D").last().ffill() # carry-forward su TUTTA la storia
daily = daily.reindex(idx).ffill() # coda oltre l'ultimo trade
return daily.fillna(init) # testa pre-primo-trade: capitale iniziale
def head_info(ts_list, cap_list, idx):
"""(primo giorno con trade dentro IDX, rendimento di testa perso dal bfill %)."""
s = pd.Series(cap_list, index=pd.to_datetime(ts_list, utc=True))
s = s[~s.index.duplicated(keep="last")].sort_index()
raw = s.resample("1D").last().reindex(idx) # senza fill: NaN = nessun trade quel giorno
first = raw.first_valid_index()
if first is None:
return None, 0.0
fixed = daily_equity_fixed(ts_list, cap_list, idx)
lost = (fixed.loc[first] / fixed.iloc[0] - 1) * 100 # ritorno idx[0]->primo trade-day
return first.date(), float(lost)
def m2(eq: pd.Series):
dr = eq.pct_change().fillna(0.0)
return metrics(dr), metrics(dr, lo=SPLIT)
def fmt_pair(label, b, f):
d_sh = f["sharpe"] - b["sharpe"]
d_dd = f["dd"] - b["dd"]
d_rt = f["ret"] - b["ret"]
return (f" {label:<22s}"
f"Sh {b['sharpe']:6.2f}->{f['sharpe']:6.2f} ({d_sh:+.3f}) "
f"DD {b['dd']:6.2f}->{f['dd']:6.2f} ({d_dd:+.3f}pp) "
f"ret {b['ret']:+9.1f}->{f['ret']:+9.1f} ({d_rt:+8.2f}pp)")
def main():
print("=" * 110)
print(" IMPATTO bug bfill _daily_equity (honest_improve2.py:30) — attuale vs corretto")
print(f" IDX {IDX[0].date()} -> {IDX[-1].date()} | OOS da {OOS_DATE} (slice a SPLIT={SPLIT} sui rendimenti daily)")
print("=" * 110)
# ---------------- [1] baseline canonica (bfill cosi' com'e') ----------------
print("\n[1] build_everything() canonico (2-3 min)...")
from scripts.analysis.report_families import build_everything
S, pairs, tsm, shape = build_everything()
base = {**S, **pairs, **tsm, **shape}
# ---------------- [2] ri-simula gli sleeve a punti-trade ----------------
print("[2] ri-simulazione sleeve a punti-trade (parity-check + versione corretta)...")
raw: dict[str, tuple] = {}
d = dip_market_gated("BTC", market_n=0, return_equity=True)
raw["DIP01_BTC"] = (d["eq_ts"], d["eq_v"])
for a, b_, p in PAIR_DEFS:
r = pairs_sim(a, b_, **p)
raw[f"PR_{a}{b_}"] = (r["eq_ts"], r["eq_v"])
r15 = pairs_sim_flat("ETH", "BTC", tf="15m", n=66, z_in=1.674, z_exit=1.0,
max_bars=35, flat_skip=True, pos=0.075)
raw["PR_ETHBTC_15M"] = (r15["eq_ts"], r15["eq_v"])
t = tsmom_sim()
raw["TSM01"] = (t["eq_ts"], t["eq_v"])
x = xsec_sim()
raw["XS01"] = (x["eq_ts"], x["eq_v"])
fixed: dict[str, pd.Series] = {}
print(f"\n {'sleeve':<16s}{'parity(max|diff|)':>18s}{'1o trade in IDX':>17s}{'ret testa perso%':>18s}")
for k, (ts, v) in raw.items():
bug = _norm(daily_equity_buggy(ts, v, IDX))
par = float((bug - base[k]).abs().max())
fixed[k] = _norm(daily_equity_fixed(ts, v, IDX))
first, lost = head_info(ts, v, IDX)
flag = "" if par < 1e-9 else " <-- PARITY FAIL"
print(f" {k:<16s}{par:>18.2e}{str(first):>17s}{lost:>+18.3f}{flag}")
# TR01/ROT02: passano da _daily_equity ma con punti per-barra (densi) ->
# ricalcolo con monkeypatch RUNTIME della funzione (nessun file toccato).
orig_de = hi2._daily_equity
try:
hi2._daily_equity = daily_equity_fixed
tr_f = _norm(hi2._tr_basket_daily(["BNB", "BTC", "DOGE", "SOL", "XRP"], IDX))
rot_f = _norm(hi2._rot_daily_equity(IDX))
finally:
hi2._daily_equity = orig_de
for k, sf in (("TR01_basket", tr_f), ("ROT02_rot", rot_f)):
diff = float((sf - base[k]).abs().max())
print(f" {k:<16s}{'(denso)':>18s}{'':>17s}{diff:>18.2e} (diff fixed-vs-base: atteso ~0)")
fixed[k] = sf
# ---------------- [3] metriche per sleeve: attuale vs corretto ----------------
print("\n" + "=" * 110)
print(" (3) SLEEVE a punti-trade — FULL e OOS, attuale(bfill) -> corretto(carry-forward)")
print("=" * 110)
rows_oos_delta = {}
for k in fixed:
bf, bo = m2(base[k])
ff, fo = m2(fixed[k])
print(fmt_pair(f"{k} FULL", bf, ff))
print(fmt_pair(f"{k} OOS ", bo, fo))
rows_oos_delta[k] = (ff["sharpe"] - bf["sharpe"], ff["dd"] - bf["dd"],
fo["sharpe"] - bo["sharpe"], fo["dd"] - bo["dd"])
# ---------------- [4] PORT06: attuale vs corretto ----------------
print("\n" + "=" * 110)
print(" (4) PORT06 (cap PAIRS 0.33 + SHAPE 0.0588) — attuale vs corretto")
print("=" * 110)
p = PORTFOLIOS["PORT06"]
def port_m(members):
ids = p.sleeve_ids
dr = pd.DataFrame({i: members[i].pct_change().fillna(0.0) for i in ids})
w = W.weight_vector(p.weighting, ids, dr, weights=p.weights,
caps=p.caps, clusters=p.clusters, lookback=p.vol_lookback)
drp = port_returns({i: members[i] for i in ids}, w)
return metrics(drp), metrics(drp, lo=SPLIT)
members_fix = {**base, **fixed}
bf, bo = port_m(base)
ff, fo = port_m(members_fix)
print(fmt_pair("PORT06 FULL", bf, ff))
print(fmt_pair("PORT06 OOS ", bo, fo))
# ---------------- [5] verdetto ----------------
print("\n" + "=" * 110)
print(" (5) VERDETTO (soglie materialita': >0.1 Sharpe o >0.5pp DD su PORT06 OOS)")
print("=" * 110)
d_sh_oos = abs(fo["sharpe"] - bo["sharpe"])
d_dd_oos = abs(fo["dd"] - bo["dd"])
d_sh_full = abs(ff["sharpe"] - bf["sharpe"])
d_dd_full = abs(ff["dd"] - bf["dd"])
materiale = d_sh_oos > 0.1 or d_dd_oos > 0.5
print(f" PORT06 OOS : dSharpe {fo['sharpe']-bo['sharpe']:+.4f} dDD {fo['dd']-bo['dd']:+.4f}pp"
f" -> {'MATERIALE' if materiale else 'NON materiale'}")
print(f" PORT06 FULL: dSharpe {ff['sharpe']-bf['sharpe']:+.4f} dDD {ff['dd']-bf['dd']:+.4f}pp")
worst = sorted(rows_oos_delta.items(), key=lambda kv: -abs(kv[1][0]) - abs(kv[1][1]) / 10)
print(" Sleeve piu' toccati (dSharpe FULL, dDD FULL, dSharpe OOS, dDD OOS):")
for k, (ds, dd_, dso, ddo) in worst[:5]:
print(f" {k:<16s} FULL {ds:+.3f} / {dd_:+.3f}pp OOS {dso:+.3f} / {ddo:+.3f}pp")
print("\n Nota strutturale: l'OOS canonico e' uno slice a SPLIT della stessa serie daily;")
print(" la distorsione bfill vive solo in testa (prima del primo trade in IDX) -> se il")
print(" primo trade in-finestra precede lo SPLIT, l'OOS e' INVARIATO per costruzione.")
if __name__ == "__main__":
main()
@@ -0,0 +1,146 @@
"""PIANO STORICO DERIBIT — quanta storia copre davvero il venue dove ESEGUIAMO?
Obiettivo: scegliere la fonte migliore per ricostruire lo storico di backtest, dato
che si esegue su Deribit. Principio (gia' misurato in multi_source_check): l'ancora
giusta e' il VENUE DI ESECUZIONE, non Binance/USDT. Qui rispondo con i numeri a:
1. COPERTURA: da quando esiste OHLCV su Deribit MAINNET (ccxt pubblico, no token) per
gli strumenti che tradiamo — inverse (BTC/ETH-PERPETUAL) e lineari USDC.
2. TIMEFRAME nativi disponibili su Deribit.
3. FEDELTA' inverse-vs-lineare (stesso indice? -> posso usare l'inverse, storia lunga,
come price-series e i lineari recenti sono ridondanti per il PREZZO).
4. GAP pre-Deribit: quanto indietro vanno le strategie e cosa manca -> da gap-fillare
con Coinbase USD (spot, NON USDT).
Tutto via ccxt pubblico Deribit (= api.deribit.com mainnet, reale). Non modifica nulla.
uv run python scripts/analysis/deribit_history_plan.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
import ccxt
DERIBIT = ccxt.deribit({"enableRateLimit": True})
COINBASE = ccxt.coinbase({"enableRateLimit": True})
def earliest(symbol: str, tf: str = "1d") -> tuple[str | None, int, str | None]:
"""Trova la prima candela disponibile (probe since 2016) + n candele totali stimate."""
since = int(pd.Timestamp("2016-01-01", tz="UTC").timestamp() * 1000)
try:
rows = DERIBIT.fetch_ohlcv(symbol, tf, since=since, limit=5000)
except Exception as e:
return None, 0, f"{type(e).__name__}: {str(e)[:60]}"
if not rows:
return None, 0, "no-data"
first = pd.to_datetime(int(rows[0][0]), unit="ms", utc=True)
last = pd.to_datetime(int(rows[-1][0]), unit="ms", utc=True)
return f"{first.date()} -> {last.date()}", len(rows), None
def list_perps() -> dict:
"""Risolve i simboli ccxt reali dei perp Deribit per BTC/ETH (inverse + lineari)."""
DERIBIT.load_markets()
found = {}
for sym, m in DERIBIT.markets.items():
if not m.get("swap"):
continue
base = m.get("base")
if base not in ("BTC", "ETH"):
continue
settle = m.get("settle")
kind = "inverse" if m.get("inverse") else "linear"
found[f"{base}-{kind}({settle})"] = sym
return found
def fetch_series(ex, symbol, tf, start, end, limit=1000):
start_ms = int(pd.Timestamp(start, tz="UTC").timestamp() * 1000)
end_ms = int(pd.Timestamp(end, tz="UTC").timestamp() * 1000)
tf_ms = ex.parse_timeframe(tf) * 1000
out, since = {}, start_ms
while since <= end_ms:
try:
rows = ex.fetch_ohlcv(symbol, tf, since=since, limit=limit)
except Exception:
break
if not rows:
break
for r in rows:
if start_ms <= int(r[0]) <= end_ms and r[4]:
out[int(r[0])] = float(r[4])
nxt = int(rows[-1][0]) + tf_ms
if nxt <= since:
break
since = nxt
if len(rows) < limit and since > end_ms:
break
return pd.Series(out)
def dev_bps(a: pd.Series, b: pd.Series) -> tuple[int, float, float, float]:
df = pd.concat([a.rename("a"), b.rename("b")], axis=1, join="inner")
if len(df) == 0:
return 0, 0, 0, 0
d = (df["a"] - df["b"]).abs() / df["b"] * 1e4
return len(df), float(d.median()), float(d.quantile(.95)), float(d.max())
def main():
print("=" * 84)
print(" PIANO STORICO DERIBIT MAINNET (ccxt pubblico, reale)")
print("=" * 84)
print("\n[1] Simboli perp Deribit BTC/ETH risolti:")
perps = list_perps()
for k, v in perps.items():
print(f" {k:<22s} -> {v}")
print("\n[2] COPERTURA storica (1d, probe da 2016):")
print(f" {'strumento':<22s}{'range disponibile':<28s}{'giorni':>8s}")
cov = {}
for k, sym in perps.items():
rng, n, err = earliest(sym, "1d")
cov[k] = (sym, rng, n)
print(f" {k:<22s}{(rng or err or '-'):<28s}{n:>8d}")
print("\n[3] TIMEFRAME nativi Deribit (test su BTC inverse, oggi):")
bsym = next((s for k, s in perps.items() if k.startswith("BTC-inverse")), "BTC/USD:BTC")
tfs = []
for tf in ("1m", "3m", "5m", "10m", "15m", "30m", "1h", "2h", "3h", "4h", "6h", "12h", "1d"):
try:
r = DERIBIT.fetch_ohlcv(bsym, tf, limit=3)
tfs.append(tf if r else f"{tf}:vuoto")
except Exception:
tfs.append(f"{tf}:NO")
print(f" ok: {[t for t in tfs if ':' not in t]}")
print(f" ko: {[t for t in tfs if ':' in t]}")
print("\n[4] FEDELTA' inverse-vs-lineare USDC (close 1h, ultimi ~40g):")
for base in ("BTC", "ETH"):
inv = next((s for k, s in perps.items() if k.startswith(f"{base}-inverse")), None)
lin = next((s for k, s in perps.items() if k.startswith(f"{base}-linear")), None)
if not inv or not lin:
print(f" {base}: manca inverse o lineare"); continue
a = fetch_series(DERIBIT, inv, "1h", "2026-04-15", "2026-05-27")
b = fetch_series(DERIBIT, lin, "1h", "2026-04-15", "2026-05-27")
n, med, p95, mx = dev_bps(a, b)
print(f" {base}: barre={n} inverse-vs-lineare med {med:.1f} bps p95 {p95:.1f} max {mx:.1f}")
print("\n[5] GAP pre-Deribit: Deribit inverse vs Coinbase USD su finestra PROFONDA (2020-06, 1d):")
for base in ("BTC", "ETH"):
inv = next((s for k, s in perps.items() if k.startswith(f"{base}-inverse")), None)
a = fetch_series(DERIBIT, inv, "1d", "2020-06-01", "2020-09-01")
b = fetch_series(COINBASE, f"{base}/USD", "1d", "2020-06-01", "2020-09-01", limit=300)
n, med, p95, mx = dev_bps(a, b)
cov_first = cov.get(f"{base}-inverse(BTC)" if base == "BTC" else f"{base}-inverse(ETH)", (None, "?", 0))[1]
print(f" {base}: Deribit-vs-Coinbase barre={n} med {med:.1f} bps p95 {p95:.1f} max {mx:.1f}")
if __name__ == "__main__":
main()
+227
View File
@@ -0,0 +1,227 @@
"""GATE DIP01 + PORT06: estendere EXIT-16 (close-confirm SL) a DIP01 (sweep punto 9).
DIP01 e' l'unico sleeve BTC con esecuzione REALE round-trip, e gira ancora col
branch SL intrabar wick-sensitive. EXIT-16 e' stato validato SULLE FADE: estenderlo
a una strategia honest richiede la validazione sul grid proprio di DIP01, con
engine GAP-AWARE (lezione exit-lab: l'engine canonico filla gli stop "al livello"
anche su gap-through -> bias PRO stop intrabar stretti; il confronto onesto filla
lo SL a worse(livello, open)).
Protocollo:
[1] parita': replay engine 'orig' (fill al livello) == equity canonica DIP01_BTC
[2] grid 3x3x2 (z_in x sl_atr x max_bars) su BTC (deployato) ed ETH (robustezza):
orig GAP-AWARE vs EXIT-16(buf 0.5), ret/DD/Sharpe train (pre-OOS) e OOS
[3] plateau buffer {0.4, 0.5, 0.75, 1.0} sulla cella canonica
[4] gate PORT06: DIP01_BTC exit16 innestato nel canonico, pesi cap
-> PROMOSSO se OOS Sharpe non peggiora E FULL/DD non degradano materialmente.
NB hurst_max NON valutato: il gate trendmax (2026-06-07) ha mostrato che il
loss-guard Hurst e' ridondante-dannoso POST-EXIT-16 (stesso regime target).
uv run python scripts/analysis/dip01_exit16_impact.py
"""
from __future__ import annotations
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.data.downloader import load_data
from scripts.analysis.strategy_research import atr
from scripts.analysis.combine_portfolio import _norm, IDX, metrics, SPLIT, OOS_DATE
from scripts.analysis._port06_gate_common import port_metrics
from scripts.portfolios._defs import PORTFOLIOS
FEE_RT, LEV, POS, INIT = 0.001, 3.0, 0.15, 1000.0
BUFFER = 0.5
GRID_Z = (2.0, 2.5, 3.0)
GRID_SL = (2.0, 2.5, 3.0)
GRID_MB = (24, 48)
CANON = dict(n=50, z_in=2.5, sl_atr=2.5, max_bars=24)
def dip_entries(df, n=50, z_in=2.5, sl_atr=2.5, max_bars=24):
"""Entries DIP01 == honest_improve2.dip_market_gated (market_n=0): crossing
di z sotto -z_in. Ritorna [{i, tp, sl, mb}] (long-only)."""
c = df["close"].values
ma = pd.Series(c).rolling(n).mean().values
sd = pd.Series(c).rolling(n).std().values
a = atr(df, 14)
z = (c - ma) / np.where(sd == 0, np.nan, sd)
out = []
for i in range(n + 14, len(c)):
if np.isnan(z[i]) or np.isnan(a[i]):
continue
if z[i] <= -z_in and z[i - 1] > -z_in:
out.append({"i": i, "tp": ma[i], "sl": c[i] - sl_atr * a[i],
"mb": max_bars})
return out
def dip_trades(ents, df, mode, buffer=BUFFER):
"""Engine exit DIP01 (long-only), non-overlap come il canonico.
mode="orig" : SL intrabar fill AL LIVELLO (== canonico, per la parita')
mode="orig_gap" : SL intrabar fill a worse(livello, open[j]) gap-aware
mode="exit16" : SL intrabar OFF; TP intrabar al livello (priorita' nel bar);
stop solo se close[j] < sl - buffer*ATR14[j], fill a close[j]
"""
h, l, c, o = df["high"].values, df["low"].values, df["close"].values, df["open"].values
n = len(c)
a = atr(df, 14)
fee = FEE_RT * LEV
out = []
last = -1
for e in ents:
i = e["i"]
if i <= last or i + 1 >= n:
continue
tp, sl, mb = e["tp"], e["sl"], e["mb"]
exit_p = c[min(i + mb, n - 1)]
j = min(i + mb, n - 1)
for k in range(1, mb + 1):
j = i + k
if j >= n:
j = n - 1
exit_p = c[j]
break
if mode in ("orig", "orig_gap"):
if l[j] <= sl:
exit_p = sl if mode == "orig" else min(sl, o[j])
break
if h[j] >= tp:
exit_p = tp
break
if k == mb:
exit_p = c[j]
else: # exit16
if h[j] >= tp:
exit_p = tp
break
aj = a[j] if np.isfinite(a[j]) else 0.0
if c[j] < sl - buffer * aj:
exit_p = c[j]
break
if k == mb:
exit_p = c[j]
ret = (exit_p - c[i]) / c[i] * LEV - fee
out.append((i, j, ret))
last = j
return out
def daily_equity(df, trades):
"""Equity giornaliera con la convenzione CANONICA honest (_daily_equity su punti
trade-exit). NB: la serie a punti-trade reindexata su IDX ancora il primo valore
al PRIMO trade dentro IDX (bfill), non al capitale portato avanti da prima
convenzione discutibile ma e' quella di build_everything: per la parita' (e il
confronto col PORT06 canonico) va replicata esattamente."""
from scripts.analysis.honest_improve2 import _daily_equity
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
cap = INIT
eq_ts, eq_v = [], []
for i, j, ret in sorted(trades, key=lambda t: t[1]):
cap = max(cap + cap * POS * ret, 10.0)
eq_ts.append(ts.iloc[j])
eq_v.append(cap)
return _norm(_daily_equity(eq_ts, eq_v, IDX))
def cell_metrics(eq):
dr = eq.pct_change().fillna(0.0)
return metrics(dr), metrics(dr, lo=SPLIT)
def main():
p = PORTFOLIOS["PORT06"]
print("=" * 104)
print(" GATE DIP01 EXIT-16 (close-confirm 0.5 ATR) — grid gap-aware + PORT06")
print(f" OOS da {OOS_DATE} | fee {FEE_RT*100:.2f}%RT x lev{LEV:.0f} | pos {POS}")
print("=" * 104)
print("\n[1] build_everything() canonico (cache)...")
from src.portfolio.sleeves import all_sleeve_equities
eq_base = dict(all_sleeve_equities())
dfs = {a: load_data(a, "1h") for a in ("BTC", "ETH")}
# --- parita' ---
ents = dip_entries(dfs["BTC"], **CANON)
rep = daily_equity(dfs["BTC"], dip_trades(ents, dfs["BTC"], "orig"))
base = eq_base["DIP01_BTC"]
corr = base.pct_change().fillna(0).corr(rep.pct_change().fillna(0))
rb = (base.iloc[-1] / base.iloc[0] - 1) * 100
rr = (rep.iloc[-1] / rep.iloc[0] - 1) * 100
print(f"\n[1] PARITA' orig vs canonico: corr={corr:.5f} ret {rb:+.0f}% vs {rr:+.0f}%")
if not (corr > 0.999 and abs(rr - rb) <= max(1.0, abs(rb) * 0.01)):
print(" >>> PARITA' FALLITA: STOP.")
return
# --- [2] grid gap-aware ---
for asset in ("BTC", "ETH"):
df = dfs[asset]
print(f"\n[2] GRID {asset} — orig GAP-AWARE vs EXIT-16 (train | OOS: ret% e Sharpe)")
print(f" {'cella':<16s}{'tr retO':>9s}{'tr retE':>9s} {'oos retO':>9s}{'oos retE':>9s}"
f" {'oos ShO':>8s}{'oos ShE':>8s} {'ddO':>6s}{'ddE':>6s} esito")
wins_tr = wins_oos = cells = 0
for z in GRID_Z:
for slm in GRID_SL:
for mb in GRID_MB:
ents = dip_entries(df, n=50, z_in=z, sl_atr=slm, max_bars=mb)
eo = daily_equity(df, dip_trades(ents, df, "orig_gap"))
ee = daily_equity(df, dip_trades(ents, df, "exit16"))
fo, oo = cell_metrics(eo)
fe, oe = cell_metrics(ee)
tr_o = fo["ret"] - oo["ret"]; tr_e = fe["ret"] - oe["ret"] # ~train (full-oos, approssimato su ret composti: usare segni)
# train ret esatto: equity al SPLIT
tr_o = (eo.iloc[SPLIT] / eo.iloc[0] - 1) * 100
tr_e = (ee.iloc[SPLIT] / ee.iloc[0] - 1) * 100
cells += 1
w_tr = tr_e >= tr_o
w_oos = oe["ret"] >= oo["ret"]
wins_tr += w_tr
wins_oos += w_oos
tag = ("OK" if (w_tr and w_oos) else "tr-" if w_oos else "oos-" if w_tr else "KO")
print(f" z{z} sl{slm} mb{mb:<3d}{tr_o:>9.0f}{tr_e:>9.0f} "
f"{oo['ret']:>9.0f}{oe['ret']:>9.0f} {oo['sharpe']:>8.2f}{oe['sharpe']:>8.2f}"
f" {fo['dd']:>6.1f}{fe['dd']:>6.1f} {tag}")
print(f" -> EXIT-16 >= orig-gap: train {wins_tr}/{cells}, OOS {wins_oos}/{cells}")
# --- [3] plateau buffer (BTC, cella canonica) ---
print("\n[3] Plateau buffer EXIT-16 (BTC, cella canonica):")
ents = dip_entries(dfs["BTC"], **CANON)
for buf in (0.4, 0.5, 0.75, 1.0):
ee = daily_equity(dfs["BTC"], dip_trades(ents, dfs["BTC"], "exit16", buffer=buf))
fe, oe = cell_metrics(ee)
print(f" buf {buf:<5}FULL ret {fe['ret']:>+7.0f}% DD {fe['dd']:>5.1f} Sh {fe['sharpe']:>5.2f}"
f" | OOS ret {oe['ret']:>+6.0f}% DD {oe['dd']:>5.1f} Sh {oe['sharpe']:>5.2f}")
# --- [4] gate PORT06 ---
ee = daily_equity(dfs["BTC"], dip_trades(ents, dfs["BTC"], "exit16"))
members_b = dict(eq_base)
members_e = dict(eq_base)
members_e["DIP01_BTC"] = ee
f_b, o_b = port_metrics(members_b, p)
f_e, o_e = port_metrics(members_e, p)
print("\n" + "=" * 104)
print(f" [4] PORT06 (pesi cap {p.caps}) — DIP01_BTC orig vs EXIT-16")
print("=" * 104)
print(f" {'variante':<10s}{'FULL Sh':>9s}{'FULL DD%':>10s}{'CAGR':>6s} | {'OOS Sh':>7s}{'OOS DD%':>8s}{'CAGR':>6s}")
for nm, (f, o) in (("BASE", (f_b, o_b)), ("EXIT-16", (f_e, o_e))):
print(f" {nm:<10s}{f['sharpe']:>9.2f}{f['dd']:>10.2f}{f['cagr']:>5.0f}% | "
f"{o['sharpe']:>7.2f}{o['dd']:>8.2f}{o['cagr']:>5.0f}%")
oos_ok = o_e["sharpe"] >= o_b["sharpe"] - 0.02 and o_e["dd"] <= o_b["dd"] + 0.20
full_ok = f_e["sharpe"] >= f_b["sharpe"] - 0.02 and f_e["dd"] <= f_b["dd"] + 0.20
promoted = oos_ok and full_ok
print(f"\n GATE: OOS {'OK' if oos_ok else 'KO'} | FULL {'OK' if full_ok else 'KO'}")
print(" VERDETTO: " + (">>> PROMOSSO <<<" if promoted else ">>> BOCCIATO <<<"))
if __name__ == "__main__":
main()
@@ -0,0 +1,156 @@
"""GATE PORT06 del candidato index_comp_disp W=168 (ricerca dispersion 2026-06-08).
Edge confermato avversarialmente: fade della componente idiosincratica di BTC verso
l'indice EW, gated da alta dispersione. Config: rel_len=12, z_win=336, z_thr=1.5,
disp_168 >= quantile rolling 0.7 (win 720), TP=1.0*ATR14, SL=1.5*ATR14, max_bars=24.
Domanda del gate (lezione FR01: robusto != migliora-il-portafoglio):
1) correlazione daily col MASTER e con le fade BTC esistenti (e' un diversificatore?)
2) PORT06 BASE (17 sleeve) vs +DISP (18 sleeve) con pesi cap: DeltaSharpe/DeltaDD FULL e OOS.
PROMOSSO solo se decorrela E migliora (o non degrada) l'OOS.
uv run python scripts/analysis/dispersion_edges/gate_index_comp_disp.py
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pandas as pd
PROJECT_ROOT = Path(__file__).resolve().parents[3]
sys.path.insert(0, str(PROJECT_ROOT))
from scripts.analysis.dispersion_lab import features, align_to
from scripts.analysis.explore_lab import get_df, atr
from scripts.analysis.combine_portfolio import _norm, IDX, port_returns, metrics, SPLIT, OOS_DATE
from scripts.analysis.honest_improve2 import _daily_equity
from scripts.portfolios._defs import PORTFOLIOS
from src.portfolio import weighting as W
FEE_RT, LEV, POS, INIT = 0.001, 3.0, 0.15, 1000.0
CFG = dict(rel_len=12, z_win=336, z_thr=1.5, disp_q=0.7, disp_q_win=720,
tp_atr=1.0, sl_atr=1.5, max_bars=24)
def _last_rank(x):
if x.shape[0] < 2:
return np.nan
return float((x[:-1] < x[-1]).mean())
def build_trades(asset="BTC"):
"""Entries CAUSALI + exit intrabar (TP/SL/max_bars) -> [(i, j, ret_netto)]."""
df = get_df(asset, "1h")
F = features()
fa = align_to(F, df)
c, h, l = df["close"].values, df["high"].values, df["low"].values
n = len(c)
a14 = atr(df, 14)
rel = fa[f"rel_{asset}"].values.astype(float)
disp = fa["disp_168"].values.astype(float)
# somma rolling rel su rel_len, z-score causale (mean/std rolling z_win shift 1)
rs = pd.Series(rel).rolling(CFG["rel_len"]).sum()
rmean = rs.rolling(CFG["z_win"]).mean().shift(1)
rstd = rs.rolling(CFG["z_win"]).std().shift(1)
z = ((rs - rmean) / rstd.replace(0, np.nan)).values
dpct = pd.Series(disp).rolling(CFG["disp_q_win"]).apply(_last_rank, raw=True).values
fee = FEE_RT * LEV
out = []
last = -1
for i in range(n - 1):
if i <= last or not np.isfinite(z[i]) or not np.isfinite(dpct[i]):
continue
if dpct[i] < CFG["disp_q"] or abs(z[i]) < CFG["z_thr"]:
continue
ai = a14[i]
if not np.isfinite(ai) or ai <= 0:
continue
d = -1 if z[i] > 0 else 1
tp = c[i] + d * CFG["tp_atr"] * ai
sl = c[i] - d * CFG["sl_atr"] * ai
mb = CFG["max_bars"]
j = min(i + mb, n - 1)
exit_p = c[j]
for k in range(1, mb + 1):
j = i + k
if j >= n:
j = n - 1; exit_p = c[j]; break
if d == 1:
if l[j] <= sl: exit_p = sl; break
if h[j] >= tp: exit_p = tp; break
else:
if h[j] >= sl: exit_p = sl; break
if l[j] <= tp: exit_p = tp; break
if k == mb: exit_p = c[j]
out.append((i, j, (exit_p - c[i]) / c[i] * d * LEV - fee))
last = j
return df, out
def daily_equity(df, trades):
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
cap = INIT; eq_ts, eq_v = [], []
for i, j, ret in sorted(trades, key=lambda t: t[1]):
cap = max(cap + cap * POS * ret, 10.0)
eq_ts.append(ts.iloc[j]); eq_v.append(cap)
return _norm(_daily_equity(eq_ts, eq_v, IDX))
def pmetrics(members, p, extra=None):
ids = list(p.sleeve_ids) + ([extra] if extra else [])
dr = pd.DataFrame({i: members[i].pct_change().fillna(0.0) for i in ids})
if extra:
caps = dict(p.caps); caps["DISP"] = caps.get("DISP", None)
w = W.weight_vector(p.weighting, ids, dr, weights=p.weights,
caps=p.caps, clusters={**{i:(p.clusters or {}).get(i,i) for i in p.sleeve_ids},
**({extra:"disp"} if extra else {})},
lookback=p.vol_lookback)
drp = port_returns({i: members[i] for i in ids}, w)
return metrics(drp), metrics(drp, lo=SPLIT)
def main():
p = PORTFOLIOS["PORT06"]
print("=" * 100)
print(" GATE PORT06 — candidato index_comp_disp W=168 (BTC) | famiglia DISP nuova")
print(f" config {CFG} | OOS da {OOS_DATE}")
print("=" * 100)
from src.portfolio.sleeves import all_sleeve_equities
eq_base = dict(all_sleeve_equities())
df, trades = build_trades("BTC")
disp_eq = daily_equity(df, trades)
fr = (disp_eq.iloc[-1] / disp_eq.iloc[0] - 1) * 100
o = disp_eq.iloc[SPLIT:]; ofr = (o.iloc[-1] / o.iloc[0] - 1) * 100
print(f"\n[1] candidato standalone: {len(trades)} trade | FULL {fr:+.0f}% | OOS {ofr:+.0f}%")
# correlazione daily col MASTER e con le fade BTC
dr_cand = disp_eq.pct_change().fillna(0.0)
print("\n[2] correlazione daily col candidato (decorrela?):")
for sid in ["MR01_BTC", "MR02_BTC", "MR07_BTC", "DIP01_BTC"]:
corr = dr_cand.corr(eq_base[sid].pct_change().fillna(0.0))
print(f" {sid:<12} corr {corr:+.3f}")
master_dr = pd.DataFrame({i: eq_base[i].pct_change().fillna(0.0) for i in p.sleeve_ids}).mean(axis=1)
print(f" {'MASTER(EW)':<12} corr {dr_cand.corr(master_dr):+.3f}")
# PORT06 base vs +DISP
f_b, o_b = pmetrics(eq_base, p)
members = dict(eq_base); members["DISP_BTC"] = disp_eq
f_e, o_e = pmetrics(members, p, extra="DISP_BTC")
print("\n[3] PORT06 BASE (17) vs +DISP (18):")
print(f" {'':<10}{'FULL Sh':>9}{'FULL DD%':>10}{'OOS Sh':>9}{'OOS DD%':>9}")
print(f" {'BASE':<10}{f_b['sharpe']:>9.2f}{f_b['dd']:>10.2f}{o_b['sharpe']:>9.2f}{o_b['dd']:>9.2f}")
print(f" {'+DISP':<10}{f_e['sharpe']:>9.2f}{f_e['dd']:>10.2f}{o_e['sharpe']:>9.2f}{o_e['dd']:>9.2f}")
print(f" {'DELTA':<10}{f_e['sharpe']-f_b['sharpe']:>+9.2f}{f_e['dd']-f_b['dd']:>+10.2f}"
f"{o_e['sharpe']-o_b['sharpe']:>+9.2f}{o_e['dd']-o_b['dd']:>+9.2f}")
promoted = (o_e['sharpe'] >= o_b['sharpe'] - 0.02 and o_e['dd'] <= o_b['dd'] + 0.20
and f_e['sharpe'] >= f_b['sharpe'] - 0.02)
print("\n VERDETTO: " + (">>> PROMOSSO <<<" if promoted else ">>> BOCCIATO (diluisce, come FR01) <<<"))
if __name__ == "__main__":
main()
@@ -0,0 +1,191 @@
"""FAMIGLIA index_comp_disp (W=24) — dispersion trading REALIZZATO.
Idea: l'indice EW vs le componenti. Quando la dispersione cross-sectional rolling
(disp_24) e' ALTA, le componenti idiosincratiche estreme (rel_A = ret_A - ret_idx)
tendono a RIENTRARE verso l'indice -> fade della componente idiosincratica estrema:
- se rel_A e' molto positivo (A ha sovraperformato l'indice oltre soglia) -> SHORT A
- se rel_A e' molto negativo (A ha sottoperformato l'indice oltre soglia) -> LONG A
condizionato a disp_24 sopra una soglia (regime di alta dispersione).
CAUSALE: la decisione a close[i] usa SOLO feature note a i:
- rel_A[i] = log-ret di A meno log-ret indice nella barra [i-1->i] (nota a close[i])
- disp_24[i] = media rolling 24 della disp cross-sectional fino a i (nota a close[i])
Ingresso eseguibile a close[i]. Niente uso di i+1 nella decisione.
Per rendere le soglie comparabili fra asset/tempo, rel_A si normalizza con la sua
deviazione standard rolling CAUSALE (rolling 168h, shiftata di 1 per non includere i).
disp_24 si normalizza col suo quantile rolling causale (percentile rolling).
Exit: time-stop max_bars (con TP/SL ATR opzionali). Il fade verso l'indice e' un
ritorno alla media -> orizzonte breve.
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pandas as pd
PROJECT_ROOT = Path(__file__).resolve().parents[3]
sys.path.insert(0, str(PROJECT_ROOT))
from scripts.analysis.dispersion_lab import features, align_to, UNIVERSE # noqa: E402
from scripts.analysis.explore_lab import get_df, evaluate, robust, atr # noqa: E402
W = 24 # finestra famiglia
def _last_rank(x: np.ndarray) -> float:
"""Frazione dei valori (esclusi l'ultimo) strettamente < dell'ultimo. Causale:
l'ultimo elemento e' la barra i, confrontata coi 719 valori precedenti."""
if x.shape[0] < 2:
return np.nan
return float((x[:-1] < x[-1]).mean())
def _causal_signals(asset: str, df: pd.DataFrame, fa: pd.DataFrame):
"""Precalcola (una volta per asset) rel_z, disp_pctl, atr — feature CAUSALI."""
a14 = atr(df, 14)
rel = fa[f"rel_{asset}"].values.astype(float)
disp = fa["disp_24"].values.astype(float)
# z-score CAUSALE di rel: media/std rolling 168h SHIFTATA di 1 (solo barre <= i-1)
rel_s = pd.Series(rel)
rmean = rel_s.rolling(168).mean().shift(1)
rstd = rel_s.rolling(168).std().shift(1)
rel_z = ((rel_s - rmean) / rstd.replace(0, np.nan)).values
# percentile rolling CAUSALE di disp_24 (rank di disp[i] vs i 720 valori fino a i).
# vettoriale via rank rolling: pos dell'ultimo elemento / (win-1).
win = 720
dr = pd.Series(disp).rolling(win).apply(_last_rank, raw=True)
disp_pctl = dr.values
return rel_z, disp_pctl, a14
def build_entries(asset: str, df: pd.DataFrame, fa: pd.DataFrame,
rel_z_thr: float, disp_pctl_thr: float,
max_bars: int, tp_atr: float | None, sl_atr: float | None,
precomp=None) -> list[dict]:
"""Costruisce entries CAUSALI per il fade della componente idiosincratica."""
n = len(df)
c = df["close"].values
if precomp is None:
rel_z, disp_pctl, a14 = _causal_signals(asset, df, fa)
else:
rel_z, disp_pctl, a14 = precomp
entries: list[dict] = []
for i in range(n - 1):
z = rel_z[i]
dp = disp_pctl[i]
if not np.isfinite(z) or not np.isfinite(dp):
continue
if dp < disp_pctl_thr: # solo regime di alta dispersione
continue
if abs(z) < rel_z_thr: # solo componente idio estrema
continue
d = -1 if z > 0 else +1 # FADE: rel alto -> short A; rel basso -> long A
a = a14[i]
if not np.isfinite(a) or a <= 0:
tp = sl = None
else:
tp = c[i] + d * tp_atr * a if tp_atr else None
sl = c[i] - d * sl_atr * a if sl_atr else None
entries.append({"i": i, "d": d, "max_bars": max_bars, "tp": tp, "sl": sl})
return entries
def check_no_lookahead(asset: str, df: pd.DataFrame, fa: pd.DataFrame) -> bool:
"""Perturba i PREZZI dopo un indice T e verifica che le entries con i<=T non cambino.
Qui ricostruiamo la entry-rule su una copia di df/fa col futuro alterato e confrontiamo
le entries (i, d) con i<=T."""
n = len(df)
T = int(n * 0.6)
base = build_entries(asset, df, fa, rel_z_thr=2.0, disp_pctl_thr=0.6,
max_bars=12, tp_atr=None, sl_atr=None)
# perturba i prezzi dopo T: alza del 50% close/high/low/open
df2 = df.copy()
for col in ("open", "high", "low", "close"):
df2.loc[df2.index > T, col] = df2.loc[df2.index > T, col] * 1.5
# perturba anche le feature riferite a barre > T (rel_<asset>, disp_24)
fa2 = fa.copy()
for col in (f"rel_{asset}", "disp_24"):
fa2.loc[fa2.index > T, col] = fa2.loc[fa2.index > T, col] * 1.5
pert = build_entries(asset, df2, fa2, rel_z_thr=2.0, disp_pctl_thr=0.6,
max_bars=12, tp_atr=None, sl_atr=None)
base_le = {(e["i"], e["d"]) for e in base if e["i"] <= T}
pert_le = {(e["i"], e["d"]) for e in pert if e["i"] <= T}
ok = base_le == pert_le
print(f"[no-look-ahead] entries con i<=T={T} invarianti al futuro: "
f"{'OK' if ok else 'VIOLATO'} (base={len(base_le)} pert={len(pert_le)})")
if not ok:
diff = (base_le ^ pert_le)
print(f" differenze: {sorted(diff)[:10]}")
return ok
def main():
F = features()
print(f"feature caricate: {F.shape[0]} barre")
# asset single-asset da esplorare (i piu' liquidi + qualche alt)
assets = ["BTC", "ETH", "SOL", "ADA", "BNB", "DOGE", "LTC", "XRP"]
# griglia piccola di soglie
rel_z_grid = [1.5, 2.0, 2.5]
disp_pctl_grid = [0.5, 0.7]
mb_grid = [6, 12, 24]
# exit: prima senza tp/sl (puro time-stop), poi con un TP/SL ATR moderato
exit_grid = [
(None, None),
(1.5, 2.0),
]
best = None
# no-look-ahead check una volta (su ETH)
df_eth = get_df("ETH", "1h")
fa_eth = align_to(F, df_eth)
la_ok = check_no_lookahead("ETH", df_eth, fa_eth)
print()
for asset in assets:
df = get_df(asset, "1h")
fa = align_to(F, df)
precomp = _causal_signals(asset, df, fa) # una volta per asset (costoso)
for rz in rel_z_grid:
for dp in disp_pctl_grid:
for mb in mb_grid:
for (tp_a, sl_a) in exit_grid:
ents = build_entries(asset, df, fa, rz, dp, mb, tp_a, sl_a,
precomp=precomp)
if len(ents) < 30:
continue
tag = f"{asset} rz{rz} dp{dp} mb{mb} tp{tp_a} sl{sl_a}"
res = evaluate(tag, ents, df)
rb = robust(res)
# criterio "migliore": OOS ret, poi sharpe; preferisci robuste
score = (rb, res["oos"]["ret"], res["full"]["sharpe"])
if best is None or score > best[0]:
best = (score, tag, res, rb,
dict(asset=asset, rz=rz, dp=dp, mb=mb, tp=tp_a, sl=sl_a))
print("\n=== MIGLIORE ===")
if best is None:
print("nessuna cella con abbastanza trade")
return
score, tag, res, rb, cfg = best
print(f"config: {tag}")
print(f"robust={rb} lookahead_ok={la_ok}")
print(f"FULL ret={res['full']['ret']:+.0f}% OOS ret={res['oos']['ret']:+.0f}% "
f"DD={res['full']['dd']:.0f}% Sharpe={res['full']['sharpe']:.2f}")
print(f"fee0.2% OOS={res['sweep_oos'][0.002]:+.0f}% anniPos={res['pos_yrs']}/{res['n_yrs']}")
if __name__ == "__main__":
main()
@@ -0,0 +1,152 @@
"""rel_idio_fade (W=24): fade della componente idiosincratica rel_A vs indice.
Idea: rel_A = ret(A) - ret(indice EW) e' il rendimento idiosincratico (residuo di
mercato). Quando l'asset diverge troppo dall'indice (z-score di rel_A su finestra
W=24 elevato), si fada il residuo verso l'indice: se A ha sovraperformato troppo
(z alto) -> SHORT A; se ha sottoperformato (z basso) -> LONG A. Mean-reversion del
residuo.
ENTRY CAUSALE: la decisione a close[i] usa SOLO rel_A fino a i incluso. Lo z-score
e' costruito con media/std rolling su [i-W+1 .. i] (causale). Ingresso eseguibile a
close[i]; exit a tempo (max_bars), opzionale TP/SL ad ATR.
Esegui: cd /opt/docker/PythagorasGoal && PYTHONPATH=. uv run python \
scripts/analysis/_disp_scratch/rel_idio_fade_24.py
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pandas as pd
PROJECT_ROOT = Path(__file__).resolve().parents[3]
sys.path.insert(0, str(PROJECT_ROOT))
from scripts.analysis.dispersion_lab import features, align_to, UNIVERSE # noqa: E402
from scripts.analysis.explore_lab import get_df, evaluate, robust, atr # noqa: E402
W = 24 # finestra correlazione/dispersione (richiesta dalla famiglia)
ASSETS = ["BTC", "ETH", "ADA", "BNB", "DOGE", "LTC", "SOL", "XRP"]
Z_GRID = [1.5, 2.0, 2.5, 3.0]
MB_GRID = [12, 24, 48]
TP_ATR = None # exit a tempo puro per il primo screening
SL_ATR = None
def build_entries(asset: str, df: pd.DataFrame, fa: pd.DataFrame,
z_thr: float, max_bars: int,
tp_atr=None, sl_atr=None) -> list[dict]:
"""Entries CAUSALI per il fade del residuo idiosincratico.
z[i] = (rel[i] - mean(rel[i-W+1..i])) / std(rel[i-W+1..i]) -> usa solo dati <= i.
rel[i] e' gia' causale (deriva da log-ret fino a close[i]). Quando |z[i]|>=thr:
z>0 (A ha sovraperformato l'indice) -> SHORT (d=-1), fade verso l'indice
z<0 (A ha sottoperformato) -> LONG (d=+1)
"""
rel = fa[f"rel_{asset}"].values.astype(float)
s = pd.Series(rel)
mu = s.rolling(W).mean().values
sd = s.rolling(W).std(ddof=0).values
z = (rel - mu) / np.where(sd > 0, sd, np.nan)
a = atr(df, 14)
c = df["close"].values
n = len(c)
entries: list[dict] = []
for i in range(W, n - 1):
zi = z[i]
if not np.isfinite(zi) or abs(zi) < z_thr:
continue
d = -1 if zi > 0 else 1 # fade del residuo
e = {"i": i, "d": d, "max_bars": max_bars}
if tp_atr is not None and np.isfinite(a[i]):
e["tp"] = c[i] + d * tp_atr * a[i] # TP nella direzione del fade
if sl_atr is not None and np.isfinite(a[i]):
e["sl"] = c[i] - d * sl_atr * a[i]
entries.append(e)
return entries
def check_no_lookahead(asset: str, df: pd.DataFrame, fa: pd.DataFrame,
z_thr: float, max_bars: int) -> bool:
"""Perturba i prezzi DOPO un indice T e verifica che le entries con i<=T non
cambino (la entry-rule usa solo dati <= close[i])."""
rel = fa[f"rel_{asset}"].values.astype(float)
n = len(rel)
T = int(n * 0.6)
def z_of(relv):
s = pd.Series(relv)
mu = s.rolling(W).mean().values
sd = s.rolling(W).std(ddof=0).values
return (relv - mu) / np.where(sd > 0, sd, np.nan)
z0 = z_of(rel)
rel2 = rel.copy()
rel2[T + 1:] = rel2[T + 1:] + 0.05 # shock del futuro
z2 = z_of(rel2)
def ents_from(z):
out = []
for i in range(W, n - 1):
if i > T:
break
zi = z[i]
if np.isfinite(zi) and abs(zi) >= z_thr:
out.append((i, -1 if zi > 0 else 1))
return out
ok = ents_from(z0) == ents_from(z2)
print(f" [no-look-ahead {asset}] entries i<=T={T} invarianti al futuro: "
f"{'OK' if ok else 'VIOLATO'}")
return ok
def main():
F = features()
print(f"feature caricate: {F.shape[0]} barre, {F.shape[1]} colonne")
best = None
look_ok_all = True
for asset in ASSETS:
df = get_df(asset, "1h")
fa = align_to(F, df)
# un check no-look-ahead per asset (config centrale)
look_ok_all &= check_no_lookahead(asset, df, fa, z_thr=2.0, max_bars=24)
print(f"--- {asset} ---")
for z_thr in Z_GRID:
for mb in MB_GRID:
ents = build_entries(asset, df, fa, z_thr, mb, TP_ATR, SL_ATR)
if len(ents) < 30:
continue
name = f"{asset} z{z_thr} mb{mb}"
res = evaluate(name, ents, df)
rb = robust(res)
score = res["oos"]["ret"] + res["full"]["ret"]
cand = {
"asset": asset, "z": z_thr, "mb": mb,
"full": res["full"]["ret"], "oos": res["oos"]["ret"],
"fee02_oos": res["sweep_oos"][0.002],
"dd": res["full"]["dd"], "sharpe": res["full"]["sharpe"],
"pos_yrs": res["pos_yrs"], "n_yrs": res["n_yrs"],
"robust": rb, "score": score, "trades": res["full"]["trades"],
}
# preferisci robuste; a parita' di robustezza, score piu' alto
if best is None or (cand["robust"], cand["score"]) > (best["robust"], best["score"]):
best = cand
print("\n=== CELLA MIGLIORE ===")
if best:
print(f" asset={best['asset']} z={best['z']} mb={best['mb']} trades={best['trades']}")
print(f" FULL={best['full']:+.0f}% OOS={best['oos']:+.0f}% "
f"fee0.2%OOS={best['fee02_oos']:+.0f}% DD={best['dd']:.0f}% "
f"Sharpe={best['sharpe']:.2f} anniPos={best['pos_yrs']}/{best['n_yrs']} "
f"robust={best['robust']}")
print(f" no-look-ahead tutti gli asset: {'OK' if look_ok_all else 'VIOLATO'}")
return best
if __name__ == "__main__":
main()
+165
View File
@@ -0,0 +1,165 @@
"""Harness CONDIVISO per la ricerca dispersion/correlation index (crypto).
Feature CAUSALI (dalle sole close, nessun feed opzioni la dispersion IMPLICITA
non e' backtestabile, muro ARGO/GEX documentato). Calcolate sull'universo comune
e allineabili a ogni singolo asset. Tutte note a close[i] (nessun look-ahead):
- avg_corr[W] : correlazione media a coppie dei log-rendimenti, rolling W (causale)
- disp[W] : dispersione cross-sectional (std cross-asset del rendimento di barra),
media rolling W
- idx_ret : rendimento dell'"indice" equal-weight (proxy mercato)
- beta_<A>[W] : beta rolling dell'asset A vs indice
- rel_<A> : rendimento di A meno rendimento indice (componente idiosincratica)
Uso dagli agenti di ricerca:
from scripts.analysis.dispersion_lab import features, align_to, UNIVERSE, COMMON_START
from scripts.analysis.explore_lab import get_df, evaluate, robust
F = features() # DataFrame indicizzato per timestamp(ms)
df = get_df("ETH", "1h")
fa = align_to(F, df) # feature riallineate alle barre di df (ffill causale)
# ... costruisci entries causali (entry decisa con dati <= close[i]) ...
res = evaluate("nome", entries, df); robust(res)
Check no-look-ahead: `python -m scripts.analysis.dispersion_lab` (perturba il futuro
e verifica che le feature fino a T non cambino).
"""
from __future__ import annotations
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 scripts.analysis.explore_lab import get_df # noqa: E402
UNIVERSE = ["ADA", "BNB", "BTC", "DOGE", "ETH", "LTC", "SOL", "XRP"]
COMMON_START = "2022-07-22" # ultimo asset a entrare (LTC) -> universo completo
WINDOWS = [24, 72, 168, 336] # 1g, 3g, 1sett, 2sett in barre 1h
_CACHE: dict | None = None
def _panel():
"""{asset: close-series} allineato sui timestamp comuni dell'universo (1h)."""
frames = {}
for a in UNIVERSE:
d = get_df(a, "1h")
frames[a] = pd.Series(d["close"].values, index=d["timestamp"].values, name=a)
P = pd.concat(frames, axis=1).dropna()
P = P[P.index >= int(pd.Timestamp(COMMON_START, tz="UTC").timestamp() * 1000)]
return P
def _avg_pairwise_corr(R: np.ndarray, win: int) -> np.ndarray:
"""Media delle correlazioni a coppie dei log-rendimenti su finestra rolling.
CAUSALE: la riga i usa R[i-win+1 .. i]. Vettoriale via media/var rolling delle
scommatorie (corr di Pearson per coppia, poi media off-diagonale)."""
n, m = R.shape
out = np.full(n, np.nan)
# somme rolling per asset
df = pd.DataFrame(R)
s = df.rolling(win).sum().values # Σx
ss = (df * df).rolling(win).sum().values # Σx²
for i in range(win - 1, n):
w = R[i - win + 1:i + 1] # (win, m)
mean = s[i] / win
var = ss[i] / win - mean * mean
sd = np.sqrt(np.clip(var, 1e-18, None))
# matrice di covarianza della finestra
cov = (w.T @ w) / win - np.outer(mean, mean)
corr = cov / np.outer(sd, sd)
iu = np.triu_indices(m, k=1)
vals = corr[iu]
vals = vals[np.isfinite(vals)]
if vals.size:
out[i] = float(np.mean(vals))
return out
_CACHE_FILE = PROJECT_ROOT / "data" / "regime" / "dispersion_features.parquet"
def features(use_disk: bool = True) -> pd.DataFrame:
"""DataFrame indicizzato per timestamp(ms) con le feature causali. Cache di
processo + cache su disco (i molti agenti di ricerca la caricano invece di
ricalcolarla; la corr rolling e' costosa)."""
global _CACHE
if _CACHE is not None:
return _CACHE
if use_disk and _CACHE_FILE.exists():
_CACHE = pd.read_parquet(_CACHE_FILE)
return _CACHE
P = _panel()
logp = np.log(P.values)
R = np.vstack([np.zeros((1, P.shape[1])), np.diff(logp, axis=0)]) # log-ret per barra
R[0] = 0.0
idx_ret = R.mean(axis=1) # indice EW
out = pd.DataFrame(index=P.index)
out["idx_ret"] = idx_ret
# dispersione cross-sectional (std cross-asset del rendimento di barra) + medie rolling
xs = R.std(axis=1)
out["disp_bar"] = xs
for w in WINDOWS:
out[f"avg_corr_{w}"] = _avg_pairwise_corr(R, w)
out[f"disp_{w}"] = pd.Series(xs, index=P.index).rolling(w).mean().values
# componente idiosincratica e beta rolling vs indice (per ogni asset)
ir = pd.Series(idx_ret, index=P.index)
for k, a in enumerate(UNIVERSE):
ra = pd.Series(R[:, k], index=P.index)
out[f"rel_{a}"] = (ra - ir).values
for w in (72, 168):
cov = ra.rolling(w).cov(ir)
var = ir.rolling(w).var()
out[f"beta_{a}_{w}"] = (cov / var.replace(0, np.nan)).values
if use_disk:
_CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
out.to_parquet(_CACHE_FILE)
_CACHE = out
return out
def align_to(F: pd.DataFrame, df: pd.DataFrame) -> pd.DataFrame:
"""Riallinea le feature (indicizzate per ts comuni) alle barre di `df` (un asset),
con ffill CAUSALE (riempie in avanti: la feature a i usa l'ultima nota <= ts[i])."""
f = F.reindex(F.index.union(df["timestamp"].values)).sort_index().ffill()
return f.reindex(df["timestamp"].values).reset_index(drop=True)
def _check_no_lookahead() -> bool:
"""Perturba il FUTURO dei prezzi e verifica che le feature fino a T non cambino."""
global _CACHE
_CACHE = None
F0 = features().copy()
P = _panel()
T = int(len(P) * 0.6)
# perturbo le close DOPO T per tutti gli asset
P2 = P.copy()
P2.iloc[T + 1:] = P2.iloc[T + 1:] * 1.5
# ricostruisco le feature da P2 inline (stessa logica)
_CACHE = None
saved = globals()["_panel"]
globals()["_panel"] = lambda: P2
_CACHE = None
F1 = features()
globals()["_panel"] = saved
_CACHE = None
cols = [c for c in F0.columns if c.startswith(("avg_corr", "disp", "beta"))]
a = F0[cols].iloc[:T - max(WINDOWS)].values
b = F1[cols].iloc[:T - max(WINDOWS)].values
ok = np.allclose(np.nan_to_num(a), np.nan_to_num(b), atol=1e-9)
print(f"[no-look-ahead] feature fino a T={T} invarianti al futuro: {'OK' if ok else 'VIOLATO'}")
return ok
if __name__ == "__main__":
F = features()
P = _panel()
print(f"universo {UNIVERSE}")
print(f"finestra comune: {pd.to_datetime(P.index[0], unit='ms', utc=True).date()} "
f"-> {pd.to_datetime(P.index[-1], unit='ms', utc=True).date()} ({len(P)} barre)")
print(f"feature: {list(F.columns)}")
print(F[[f'avg_corr_{w}' for w in WINDOWS]].describe().round(3).to_string())
_check_no_lookahead()
+106
View File
@@ -0,0 +1,106 @@
"""Drift monitor per-famiglia — il rolling-return corrente di ogni famiglia vs la
DISTRIBUZIONE STORICA dei propri rolling-return (stessa finestra, storia 2021+).
Non è un filtro di trading: è OSSERVABILITÀ (la protezione giusta contro il drift è
accorgersene presto, non ritoccare i parametri lezione 2026-06-11: le FADE al 2°
percentile sul 120g sono state trovate a mano; questo script lo rende ripetibile).
Percentile basso = la famiglia sta attraversando uno dei suoi tratti peggiori:
- sotto P_WARN (5%): segnalato coerente con la coda storica, OSSERVARE;
- il PORT06 complessivo sotto P_WARN è più serio (la diversificazione non copre).
Equity dal builder canonico (all_sleeve_equities parità coi gate).
uv run python scripts/analysis/drift_monitor.py # stampa
uv run python scripts/analysis/drift_monitor.py --telegram # + invio Telegram
"""
from __future__ import annotations
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 scripts.analysis.combine_portfolio import port_returns
from scripts.portfolios._defs import PORTFOLIOS
from src.portfolio.sleeves import all_sleeve_equities
from src.portfolio import weighting as W
WINDOWS = (60, 120) # giorni
P_WARN = 5.0 # percentile sotto cui segnalare
def family_returns():
"""Rendimenti daily per famiglia (equal-weight intra-famiglia) + PORT06 (pesi cap)."""
p = PORTFOLIOS["PORT06"]
eq = dict(all_sleeve_equities())
ids = list(p.sleeve_ids)
fams: dict[str, list] = {}
for i in ids:
fams.setdefault(W.family_of(i), []).append(i)
out = {}
for f, members in sorted(fams.items()):
out[f] = port_returns({i: eq[i] for i in members},
{i: 1 / len(members) for i in members})
dr = pd.DataFrame({i: eq[i].pct_change().fillna(0.0) for i in ids})
w = W.weight_vector("cap", ids, dr, caps=p.caps, clusters=p.clusters)
out["PORT06"] = port_returns({i: eq[i] for i in ids}, w)
return out
def drift_rows():
rows = []
for name, r in family_returns().items():
for win in WINDOWS:
# vettoriale (log1p+rolling sum) invece di apply(np.prod): identico
# numericamente, ~100x piu' veloce del callback Python per-finestra
roll = np.expm1(np.log1p(r).rolling(win).sum())
roll = roll.dropna()
if len(roll) < 100:
continue
cur = float(roll.iloc[-1])
pct = float((roll < cur).mean() * 100)
rows.append(dict(name=name, win=win, cur=cur * 100, pct=pct,
p5=float(roll.quantile(0.05) * 100),
med=float(roll.median() * 100)))
return rows
def build_report(rows) -> tuple[str, bool]:
warn = [r for r in rows if r["pct"] < P_WARN]
L = ["📉 <b>Drift monitor</b> — rolling-return vs storia propria (2021+)"]
L.append("<pre>" + f"{'famiglia':<9}{'win':>5}{'corr%':>8}{'pct':>6}{'p5%':>8}{'med%':>7}")
for r in rows:
flag = " ⚠️" if r["pct"] < P_WARN else ""
L.append(f"{r['name']:<9}{r['win']:>4}g{r['cur']:>+8.1f}{r['pct']:>5.0f}%"
f"{r['p5']:>+8.1f}{r['med']:>+7.1f}{flag}")
L.append("</pre>")
if warn:
names = ", ".join(f"{r['name']} {r['win']}g (p{r['pct']:.0f})" for r in warn)
L.append(f"⚠️ sotto il p{P_WARN:.0f} storico: {names} — coda storica della famiglia: "
"OSSERVARE, non ritoccare i parametri (drift ≠ rottura; "
"vedi docs/diary/2026-06-11-stability-sweep.md)")
else:
L.append(f"✅ tutte le famiglie sopra il p{P_WARN:.0f} storico")
return "\n".join(L), bool(warn)
def main():
rows = drift_rows()
report, warned = build_report(rows)
import re
print(re.sub(r"</?(b|pre)>", "", report))
if "--telegram" in sys.argv:
from src.live.telegram_notifier import send_telegram
ok = send_telegram(report)
print(f"[telegram] inviato: {ok}")
return warned
if __name__ == "__main__":
# exit code 1 su warn: utilizzabile da cron/script come canale d'allarme
# (coerente con reconcile_account; prima il bool era calcolato e buttato via)
sys.exit(1 if main() else 0)
+126
View File
@@ -0,0 +1,126 @@
"""Gate del CATASTROPHE-CAP auto-finanziato (collar standing) sullo sleeve ETH no-SL.
Tesi: lo sleeve ETH no-SL ha la coda da crash (un long-fade puo' perdere -50/-65% in un
gap). Un COLLAR standing rollato mensilmente put lunga ~13% OTM finanziata da call corta
~10% OTM cappa quella coda a premio netto ~zero (validato sui premi REALI di cerbero-bite:
put -13%1.0%/m IV55, call +10%1.05%/m IV49). Pricing BS calibrato sul reale: skew_put 1.12,
skew_call 1.0. Caveat: il collar aggiunge delta SHORT-ETH con dead-zone -p/+c -> cappa anche
l'upside; nei mesi tranquilli (ETH dentro la banda) costa ~zero. Il gate dice se aiuta netto.
uv run python scripts/analysis/eth_collar_gate.py
"""
from __future__ import annotations
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 scripts.analysis.explore_lab import get_df
from scripts.analysis.combine_portfolio import IDX, SPLIT, OOS_DATE, metrics, port_returns, _norm
from scripts.analysis.option_overlay_lab import dvol_for, bs_put, bs_call
from scripts.analysis.mr02eth_port06_gate import (
gen_donchian_base, build_trades, build_trades_exit16, daily_equity, port_metrics, CAPS)
from src.portfolio.sleeves import all_sleeve_equities
HY = 24 * 365.0
def collar_daily_returns(df, dvol, p_otm=0.13, c_otm=0.10, skew_put=1.12, skew_call=1.0,
roll_h=24 * 30) -> pd.Series:
"""Collar standing rollato ogni roll_h ore. Ritorna la SERIE di rendimenti GIORNALIERI
(frazione del notional collar): MTM = d(intrinseco) - theta (premio netto amortizzato)."""
c = df["close"].values; ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
n = len(c)
val = np.zeros(n) # valore collar (frac notional) marcato a intrinseco - premio residuo
T = roll_h / HY
k = 0
while k < n - 1:
S0 = c[k]; sig = dvol[k] if not np.isnan(dvol[k]) else 0.6
Kp = S0 * (1 - p_otm); Kc = S0 * (1 + c_otm)
prem = bs_put(S0, Kp, T, sig * skew_put) / S0 - bs_call(S0, Kc, T, sig * skew_call) / S0
end = min(k + roll_h, n - 1)
span = end - k
for j in range(k, end + 1):
intr = (max(Kp - c[j], 0.0) - max(c[j] - Kc, 0.0)) / S0
frac_elapsed = (j - k) / span if span else 1.0
val[j] = intr - prem * (1 - frac_elapsed) # premio pagato up-front, amortizzato a 0 a scadenza
k = end
s = pd.Series(val, index=ts)
daily = s.resample("1D").last().reindex(IDX).ffill().bfill()
return daily.diff().fillna(0.0) # rendimento giornaliero (frac notional)
def combine(fade_eq: pd.Series, collar_dr: pd.Series, hedge_frac: float) -> pd.Series:
"""sleeve = fade no-SL + hedge_frac * collar. Combina i rendimenti giornalieri."""
fr = fade_eq.pct_change().fillna(0.0)
return _norm((1 + fr + hedge_frac * collar_dr).cumprod())
def crash_audit(df, dvol, p_otm, c_otm, hedge_frac):
"""P&L del collar nei mesi di crollo ETH peggiori (frac notional)."""
cr = collar_daily_returns(df, dvol, p_otm=p_otm, c_otm=c_otm)
# ETH daily ret mensile
cdf = pd.Series(df["close"].values, index=pd.to_datetime(df["timestamp"], unit="ms", utc=True)).resample("1D").last().reindex(IDX).ffill()
mret = cdf.resample("30D").last().pct_change()
collar_m = (1 + cr).resample("30D").apply(lambda x: x.prod()) - 1
worst = mret.nsmallest(5)
print(f" {'mese (fine)':>12}{'ETH 30g%':>10}{'collar P&L%':>13}")
for t, r in worst.items():
cm = collar_m.reindex([t], method="nearest").iloc[0] * hedge_frac * 100
print(f" {str(t.date()):>12}{r*100:>9.0f}%{cm:>12.1f}%")
def main():
print("=" * 96)
print(f" GATE collar standing (catastrophe-cap) sullo sleeve ETH no-SL | OOS da {OOS_DATE}")
print("=" * 96)
df = get_df("ETH", "1h"); dvol = dvol_for(df, "ETH")
eq = dict(all_sleeve_equities())
ids = [k for k in eq if k in {"MR01_BTC","MR02_BTC","MR07_BTC","MR01_ETH","MR02_ETH","MR07_ETH",
"DIP01_BTC","TR01_basket","ROT02_rot","PR_ETHBTC","PR_LTCETH","PR_ADAETH","PR_BTCLTC","PR_ETHSOL","TSM01","SH_BTC","SH_ETH"}]
base_ents = gen_donchian_base(df, n=20, sl_atr=2.0, trend_max=3.0)
nosl_ents = gen_donchian_base(df, n=20, sl_atr=2.0, trend_max=3.0, use_sl=False)
def pm(ce):
m = dict(eq); m["MR02_ETH"] = ce; return port_metrics(m, ids)
f_l, o_l = pm(daily_equity(build_trades_exit16(base_ents, df, sl_confirm=0.5), df))
fade_nosl = daily_equity(build_trades(nosl_ents, df), df)
f0, o0 = pm(fade_nosl)
print(f"\n {'sleeve ETH':<30s}{'FULL Sh':>8s}{'FULL DD':>8s} |{'OOS Sh':>8s}{'OOS DD':>8s}")
print(" " + "-" * 78)
print(f" {'LIVE EXIT-16 (rif)':<30s}{f_l['sharpe']:>8.2f}{f_l['dd']:>8.2f} |{o_l['sharpe']:>8.2f}{o_l['dd']:>8.2f}")
print(f" {'no-SL nudo':<30s}{f0['sharpe']:>8.2f}{f0['dd']:>8.2f} |{o0['sharpe']:>8.2f}{o0['dd']:>8.2f}")
configs = [
("put13/call10 hf0.45", 0.13, 0.10, 0.45),
("put13/call10 hf0.30", 0.13, 0.10, 0.30),
("put15/call12 hf0.45", 0.15, 0.12, 0.45),
("put20/call15 hf0.45", 0.20, 0.15, 0.45),
("put13/call08 hf0.45", 0.13, 0.08, 0.45),
]
rows = []
for name, p, cc, hf in configs:
cr = collar_daily_returns(df, dvol, p_otm=p, c_otm=cc)
ce = combine(fade_nosl, cr, hf)
f_c, o_c = pm(ce)
rows.append((name, p, cc, hf, f_c, o_c))
print(f" {'no-SL + '+name:<30s}{f_c['sharpe']:>8.2f}{f_c['dd']:>8.2f} |{o_c['sharpe']:>8.2f}{o_c['dd']:>8.2f}")
print("\n " + "=" * 90)
print(f" vs LIVE EXIT-16 (FULL {f_l['sharpe']:.2f}/{f_l['dd']:.2f} OOS {o_l['sharpe']:.2f}/{o_l['dd']:.2f}) e vs no-SL")
print(" " + "-" * 90)
for name, p, cc, hf, f_c, o_c in rows:
print(f" {name:<22s} Δ vsEXIT16 FULL {f_c['sharpe']-f_l['sharpe']:+.2f}/{f_c['dd']-f_l['dd']:+.2f} "
f"OOS {o_c['sharpe']-o_l['sharpe']:+.2f}/{o_c['dd']-o_l['dd']:+.2f} | "
f"Δ vsNoSL DD {f_c['dd']-f0['dd']:+.2f}")
print("\n --- audit crash: P&L collar (hf-scaled) nei 5 mesi ETH peggiori (put13/call10 hf0.45) ---")
crash_audit(df, dvol, 0.13, 0.10, 0.45)
if __name__ == "__main__":
main()
@@ -24,106 +24,23 @@ from __future__ import annotations
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.data.downloader import load_data
from scripts.analysis.strategy_research import atr
from scripts.analysis.risk_management import strats_for, FEE_RT, LEV, POS, INIT
from scripts.analysis.combine_portfolio import (
fade_daily_equity, _norm, IDX, port_returns, metrics, SPLIT, OOS_DATE,
from scripts.analysis.risk_management import strats_for
from scripts.analysis.combine_portfolio import OOS_DATE
from scripts.analysis._port06_gate_common import (
build_trades_variant, equity_from_trades, port_metrics, dd as _dd,
)
from scripts.portfolios._defs import PORTFOLIOS
from src.portfolio import weighting as W
BUFFER = 0.5 # EXIT-16 promossa: close-confirm con buffer 0.5 ATR
# ---------------------------------------------------------------- engine replay
def build_trades_variant(ents, df, mode, buffer=BUFFER,
lev=LEV, fee_rt=FEE_RT, trend_max=3.0, ema_long=200):
"""Replica ESATTA di risk_management.build_trades, cambiando SOLO il ramo SL.
mode="orig" : SL intrabar al livello (SL prima del TP) == canonico.
mode="exit16" : SL intrabar DISATTIVATO; close-confirm sul close[j]:
long esci a close[j] se close[j] < sl0 - buffer*atr14[j]
short esci a close[j] se close[j] > sl0 + buffer*atr14[j]
TP intrabar al livello e max_bars al close INVARIATI.
"""
h, l, c = df["high"].values, df["low"].values, df["close"].values
n = len(c)
a = atr(df, 14)
el = pd.Series(c).ewm(span=ema_long, adjust=False).mean().values
fee = fee_rt * lev
out = []
last = -1
for e in ents:
i, d = e["i"], e["d"]
if i <= last or i + 1 >= n:
continue
if trend_max is not None and a[i] and abs(c[i] - el[i]) / a[i] > trend_max:
continue
entry = c[i]
tp, sl0, mb = e["tp"], e["sl"], e["max_bars"]
exit_p = c[min(i + mb, n - 1)]
j = min(i + mb, n - 1)
for k in range(1, mb + 1):
j = i + k
if j >= n:
exit_p = c[n - 1]
break
if mode == "orig":
hs = (d == 1 and l[j] <= sl0) or (d == -1 and h[j] >= sl0)
ht = (d == 1 and h[j] >= tp) or (d == -1 and l[j] <= tp)
if hs:
exit_p = sl0
break
if ht:
exit_p = tp
break
if k == mb:
exit_p = c[j]
else: # exit16: no SL intrabar; TP intrabar; poi close-confirm SL al close[j]
ht = (d == 1 and h[j] >= tp) or (d == -1 and l[j] <= tp)
if ht:
exit_p = tp
break
aj = a[j] if np.isfinite(a[j]) else 0.0
confirm = (d == 1 and c[j] < sl0 - buffer * aj) or \
(d == -1 and c[j] > sl0 + buffer * aj)
if confirm:
exit_p = c[j]
break
if k == mb:
exit_p = c[j]
ret = (exit_p - entry) / entry * d * lev - fee
out.append((i, j, ret))
last = j
return out
def fade_equity_variant(asset, fn, params, mode):
"""Stesso flusso di combine_portfolio.fade_daily_equity ma con build_trades_variant."""
df = load_data(asset, "1h")
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
trades = build_trades_variant(fn(df, **params), df, mode=mode, trend_max=3.0)
n = len(df)
eq = np.full(n, INIT, dtype=float)
cap = INIT
for i, j, ret in sorted(trades, key=lambda t: t[1]):
cap = max(cap + cap * POS * ret, 10.0)
eq[j:] = cap
s = pd.Series(eq, index=ts).resample("1D").last().reindex(IDX).ffill().bfill()
return _norm(s)
# ---------------------------------------------------------------- pesi PORT06
def port_metrics(members: dict[str, pd.Series], weights: dict[str, float]):
dr = port_returns(members, weights)
return metrics(dr), metrics(dr, lo=SPLIT)
return equity_from_trades(df, trades)
def main():
@@ -174,17 +91,9 @@ def main():
for sid in fade_ids:
members_e16[sid] = eq_e16[sid] # sostituisco SOLO le 6 colonne fade
ids = p.sleeve_ids
# pesi cap canonici (gli stessi che usa Portfolio.backtest)
dr_base = pd.DataFrame({i: members_base[i].pct_change().fillna(0.0) for i in ids})
w_base = W.weight_vector(p.weighting, ids, dr_base, weights=p.weights,
caps=p.caps, clusters=p.clusters, lookback=p.vol_lookback)
dr_e16 = pd.DataFrame({i: members_e16[i].pct_change().fillna(0.0) for i in ids})
w_e16 = W.weight_vector(p.weighting, ids, dr_e16, weights=p.weights,
caps=p.caps, clusters=p.clusters, lookback=p.vol_lookback)
f_b, o_b = port_metrics({i: members_base[i] for i in ids}, w_base)
f_e, o_e = port_metrics({i: members_e16[i] for i in ids}, w_e16)
# pesi cap canonici (gli stessi che usa Portfolio.backtest) dentro port_metrics
f_b, o_b = port_metrics(members_base, p)
f_e, o_e = port_metrics(members_e16, p)
print("\n" + "=" * 96)
print(f" [3] PORT06 — pesi={p.weighting} caps={p.caps} | OOS da {OOS_DATE} | leva3x interna fade, pos0.15")
@@ -207,8 +116,6 @@ def main():
f"{'orig DD%':>10s}{'e16 DD%':>10s}")
for sid in fade_ids:
ro = eq_orig[sid]; re = eq_e16[sid]
def _dd(s):
pk = s.cummax(); return float(((pk - s) / pk).max() * 100)
rro = (ro.iloc[-1] / ro.iloc[0] - 1) * 100
rre = (re.iloc[-1] / re.iloc[0] - 1) * 100
print(f" {sid:<10s}{rro:>12.1f}{rre:>14.1f}{rre-rro:>+10.1f}"

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