13 Commits

Author SHA1 Message Date
Adriano 8292e5e6b8 docs: verify cerbero-bite MCP is MAINNET (real option chain) -> VRP source unlocked
- 3-level check: spot matches certified feed (<0.3%); environment_info=mainnet/testnet=false;
  chain-level decisive: ETH_USDC-26JUN26-1650-P identical bid/ask/IV/delta on ccxt mainnet
  vs Cerbero MCP (25.6/26.6/54.54%/-0.315)
- contamination was the OLD testnet token on get_historical, NOT Cerbero MCP itself
- Cerbero MCP gives real per-strike bid/ask/IV/greeks/OI + regime features ccxt lacks
  (dealer gamma, gamma-flip, OI delta, liquidation, funding) -> usable for VRP validation
- no deep chain history -> multi-year VRP still model-priced, but model-vs-real calibration
  now robust/repeatable. Token used only for verification, never logged/committed.
2026-06-19 22:11:14 +02:00
Adriano 922947d2aa research: verify options sleeve on REAL Deribit quotes (spread+skew haircut)
- options_real_quote_check.py: fetches real weekly BTC put chain, measures premium
  haircut (bid vs BS@DVOL-ATM), re-runs CSP sleeve with real haircut
- KEY FINDING (reverses a prior critique): backtest UNDER-prices the OTM put by using
  ATM DVOL; real skew (+28% gross) exceeds the ~4% bid/ask spread -> real bid premium
  = 1.29x modeled. Sleeve premium is conservative at current (calm) quotes.
- Real risk SHIFTS to the tail + roll-liquidity in stress (skew = market pricing fat
  tail), not premium magnitude. Breakpoint: sleeve dies below ~70% premium capture.
- updated eval diary with the verification
2026-06-19 21:48:12 +02:00
Adriano 69be9eb75f docs: critical eval of external crypto_backtest strategy (trend + options VRP)
- spot sleeve independently confirms our TP01 (12h vol-targeted trend); our multi-horizon
  blend is slightly better (Sharpe 1.32 vs their 0.77 window / 1.07 full)
- options sleeve (75% weight) = the most promising lead to break the ~1.3 Sharpe ceiling
  (adds VRP, a different return source) BUT priced on modeled IV (DVOL+BS, no real bid/ask,
  ATM vol on OTM puts ignoring skew, tail under-modeled, leverage no funding, window bias)
  -> headline blend Sharpe 1.21 must be discounted until validated on real option quotes
- next steps: real Deribit quotes/skew, crash-week stress, testnet paper trading
2026-06-19 21:43:08 +02:00
Adriano 58fc10de77 research tracks H+I: volume/vol/range + alt-momentum/reversal (both NEGATIVE for alpha)
- trackH volume_vol: no uncorrelated additive edge; profitable signals are trend-in-disguise
  (corr 0.6-0.75); MR/declining-volume fade dead even at fee 0; OBV-up filter is a defensive
  DD overlay only (13.3->10.1% DD but -CAGR), not new alpha
- trackI momentum/reversal: no formulation beats 1-3-6m sign-blend OOS on both assets;
  z-score continuous momentum = same edge (corr 0.96), lower DD 8.4% but lower CAGR;
  long-horizon reversal not bankable (negative/flat standalone). ~1.3 Sharpe ceiling holds.
- TP01 (12h sign-blend) remains the deployable winner
2026-06-19 21:22:49 +02:00
Adriano eac2aa1d00 audit+fix: anti-look-ahead audit, migrate deployable config to >=12h
- trackD_lookahead_audit.py: relabel test (left==right, no labeling leak) + execution-lag
  stress -> our trend pipeline is CLEAN (4h Sharpe 1.36 robust to +1 bar lag, label-invariant)
- ADOPT conservative conclusion: deploy at 12h (sub-12h: costs/overfit dominate, slight Sharpe
  bump unreliable). 12h: Sharpe 1.32, DD 13.3%, CAGR 16.2% ~ identical and robust
- trend_portfolio: DEPLOY_TF=12h, resample_tf(rule); paper trader + tests on 12h
- calendar research (NEGATIVE, both): trackF seasonality (spurious), trackG prior-levels
  (breakouts continue, fade dead; only long-drift survivor, redundant with TP01)
- gitignore data/paper_trend runtime state
2026-06-19 21:13:57 +02:00
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
418 changed files with 8200 additions and 779 deletions
+6
View File
@@ -39,3 +39,9 @@ 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
data/paper_trend/
+131 -634
View File
@@ -1,667 +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);
options_fetcher.py + options_chain.py (storico opzioni REALE da cerbero-bite);
option_overlay_lab.py (overlay opzioni prezzato; mr02eth_port06_gate.py = gate swap-sleeve);
pairs15m_*.py (gate/flatcheck/smoke ETH/BTC 15m -> sleeve PR_ETHBTC_15M)
scripts/games/ → gioco "Blind Traders": 100 agenti ciechi su BTC/ETH anonimi (engine.py,
arena.py epoche+cull, agent_brief.py digest, run_game.py). Origine del BLEND 15m.
Varianti: options_* (strutture in opzioni, BS+skew+DVOL; opt_calibrate da
cerbero-bite), session_* (pattern orari), grid_* (griglie da STRATEGIA_GRIGLIA.md;
gate scripts/analysis/grid_game_gate.py). arena: GAME_NO_LIVE=1 vieta le
strategie gia' deployate (pairs, fade zscore/breakout/momentum)
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/options/ → storico catena opzioni per-strike (bid/ask/IV/greche/OI) importato da cerbero-bite (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
uv run python -m src.live.dashboard --port 8787 # dashboard web (servizio compose 'dashboard', porta 8787)
uv run python scripts/analysis/ledger_vs_backtest.py --since 2026-06-13 # report fuga di esecuzione (reale vs sim)
uv run python scripts/analysis/trades_status.py [--since ISO] # report "stato trades": POOL reale vs PAPER-STATS, realizzato + unrealized
./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
```
> **Dashboard web (`src/live/dashboard.py`, servizio compose `dashboard`, porta 8787).**
> Stdlib http.server, legge `data/`: equity (restyling), PnL totale e per-strategia,
> **grafico equity per famiglia**, trade attivi in tempo reale (entry/mark/PnL **REALI** —
> il feed sim-decisione testnet è dislocato, vedi sotto) + chiusi, modal per-strategia
> (curva reale vs sim + scheda `strategie_attive.html`), **area PAPER distinta** (equity
> propria dei 4 multi-asset), strategie **ritirate** marcate (staleness >30min) + versione
> sistema/strategia. Nessuna auth → solo rete interna. `docker compose up -d --build dashboard`.
> **Cron host (monitoraggio, già schedulati):** `hourly_report.py` (orario), `drift_monitor.py`
> (07:15), `reconcile_account.py --telegram` (:40), **`ledger_vs_backtest.py --since 2026-06-13
> --telegram` (08:30)** = il GATE per scalare il capitale (per gli sleeve eseguiti sim==backtest
> per costruzione → reale-vs-sim = fuga di esecuzione: slippage+fee+netting; verdetto 🟢/🟡/🔴).
> **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).
**SWAP filtri fade: hurst→trend (2026-06-07).** Il gate PORT06 sul path live
(`scripts/analysis/trendmax_port06_impact.py`, parità 1.00000 col canonico) ha mostrato che
**post-EXIT-16 il loss-guard Hurst è ridondante-dannoso**: EXIT-16 ha eliminato i wick-stop che
hurst evitava → gli ingressi saltati (66% delle barre!) sono in maggioranza tornati vincenti.
Su PORT06: LIVE hurst-only FULL Sh 7.23 / OOS 9.35-DD 1.68 vs **TREND-ONLY 7.89 / 9.91-DD 1.20**
(domina su tutte le metriche; hurst+trend insieme over-filtra: 7.11, metà dei trade; plateau
trend_max 2.5/3.0/3.5 robusto). TREND-ONLY è la config che la ricerca EXIT-16 aveva davvero
promosso (entries trend-filtrate, no hurst) e che il live non aveva mai eseguito. **Live ora:
`trend_max=3.0, ema_long=200` nelle 6 fade di `_defs.py`, `hurst_max` rimosso** (la maschera resta
in `fade_base`). Monitor: `hourly_report` traccia lo stop-rate per epoca PRE→HURST→TREND.
Lezione: ri-gateare ogni filtro quando cambia l'exit engine. Diario
`docs/diary/2026-06-07-trendmax-gate.md`. Il paragrafo sotto resta come record storico:
**Loss-guard Hurst (storico: live dal 2026-06-02 al 2026-06-07, poi sostituito dal filtro trend).** 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; esteso a DIP01 il 2026-06-07).** Le fade E
DIP01 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`.
**Storico opzioni REALE da cerbero-bite (2026-06-09, il muro ARGO è caduto per il periodo recente).**
Il container **cerbero-bite** (accanto, `/opt/docker/cerbero-bite`) accumula in continuo lo **storico
per-strike** della catena opzioni Deribit (BTC+ETH) nella tabella `option_chain_snapshots` del suo
SQLite (`bite-data:/app/data/state.sqlite`, root-only): bid/ask/mid/**IV per-strike**/greche/OI/volume,
**dal 2026-05-01**, cadenza ~12 min (~110k righe/asset). È esattamente il dato che ARGO/W18-21
credevano non-gratuito. `scripts/analysis/options_fetcher.py` lo importa via `docker exec`
`data/options/{eth,btc}_chain.parquet`; `scripts/analysis/options_chain.py` (`OptionChain`) espone
loader + `skew_curve()` / `premium_levels()` (aggregati ROBUSTI) + `quote()` causale best-effort.
**Caveat granularità:** cerbero-bite snapshotta una **fetta rotante** (~1 scadenza per ciclo) → ottimo
per skew/premi **aggregati**, limitato per il pricing **per-trade** preciso (usare la finestra di
staleness). **Pannello regime `market_snapshots`** (anch'esso importato → `data/options/market_snapshots.parquet`,
loader `options_chain.load_market(asset)`; merge causale su prezzo via `options_chain.attach_market(df, asset)`):
feature REALI pre-calcolate — **spot, dvol, realized_vol_30d, iv_minus_rv (VRP), funding perp/cross,
`dealer_net_gamma` (net-GEX dealer!), `gamma_flip_level`, oi_delta_pct_4h, liquidation_long/short_risk**.
È il segnale ARGO/GEX che il progetto dichiarò non-backtestabile: ora accumula, reale e allineato (lo
`spot` elimina il proxy ATM). **Copertura reale:** spot/dvol dal 2026-03-26 (sparse/daily fino a fine
aprile), **net-GEX denso orario solo da ~2026-05-01** → ~5-6 settimane, **un singolo regime calmo**:
analisi esplorativa OK ma NIENTE edge validabile ora (i prior GEX/VRP non si replicano su 9 sett.;
`liquidation_risk` costante 'low'). Valore FORWARD: rivalutare quando il pannello attraversa
gamma-flip/liquidazioni/crash con held-out. **NB look-ahead:** usare `attach_market` (merge_asof
causale, NaN prima della copertura), MAI `astype(int64)` su un timestamp datetime (darebbe ns →
match all'ultimo snapshot = leak). **Numeri reali misurati (ETH):** skew put 10% OTM = **×1.1** (liquido, spread ~7%, NON ×1.6
come sembrava da un singolo snapshot a 1g, illiquido); premio reale put 10% OTM ≈ **1.0%/mese** (0.65%/sett),
catastrofe 15%+ OTM ≈ 0.33%/mese. **Vincolo strutturale:** gli strike **10% OTM a 24h NON esistono**
(Deribit lista solo near-ATM sui tenor corti) → un overlay **per-trade a 24h è infattibile**; l'unica
struttura eseguibile è una **put settimanale/mensile standing** (catastrofe-cap di sleeve), da gateare
coi premi reali. Vedi `docs/diary/2026-06-08-mr02eth-replace-search.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/XSEC).
- **Default `portfolios.yml`:** PORT06 (master+shape), `weighting=cap pairs 0.33 + shape 0.0588`, **leva 3x** (era 2x fino al 2026-06-12, vedi sotto), 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: 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. Col BLEND ETH/BTC 15m (2026-06-09, v1.1.16): FULL 7.20 / 3.68%, OOS 9.66 / 1.31% — 18 sleeve. Con XS01 (2026-06-09): OOS Sharpe 10.07, FULL DD 3.46 — 19 sleeve (pool live real-only 15; i 4 book multi-asset TR01/ROT02/TSM01/XS01 girano in statistica). **Con SWAP fade 1h→15m (config live attuale, v1.1.30, 2026-06-12, vedi sotto): FULL Sharpe 8.13 DD 2.47% / OOS Sharpe 10.86 DD 2.09%** (regression-lock `test_backtest_parity_cap`).
- **LEVA 3x (v1.1.29, 2026-06-12, scelta utente).** Era 2x ("sobrio") dal primo deploy live. Frontiera ACCEL50 (`scripts/analysis/accel50_research.py`): a Sharpe OOS ~10 il collo di bottiglia è la TAGLIA, non il rischio — lev 2→3 porta gli anni-a-€50/g da 3.3 a 1.9 con FULL DD 3.5→5.2% (modello lineare). PAIRS `position_size_family` 0.20→0.13 per CONSERVARE l'esposizione validata ~0.40 (la leva accelera le famiglie con stop, non i pairs no-stop). Diario `docs/diary/2026-06-12-accel50.md`.
- **SWAP fade 1h→15m (v1.1.30, 2026-06-12, scelta utente "swap secco").** Le 6 fade (MR01/02/07 × BTC/ETH) girano a **15m** invece di 1h: stessi `sid` (pesi/alloc/ledger intatti), `tf="15m"` in `_defs.FADE` + `combine_portfolio.FADE_TF` (le due facce backtest/live sulla stessa def). Gate `scripts/analysis/fade15m_port06_gate.py`: parametri 1h NON ri-tunati (anti-overfit), corr daily 15m↔1h media **0.26** (diversificazione vera, non lo stesso edge più veloce), edge ETH regge il flat-entry-skip (non stale-print). SWAP promosso: FULL 7.34→8.13 / OOS 10.07→10.86 (OOS DD 1.48→2.09 accettato). DIP01 resta 1h (non era nel gate). `max_bars=24` ora = 6h; EXIT-16 confirm sulla barra 15m completata. Caveat: MR02_BTC 15m è il più fee-sensitive (fee2x OOS Sh 0.60) → monitorare le fee reali. **TF sweep** (`fade_tf_sweep.py`): 1m/2m chiusi (fee-death + flat 13-26%), 5m no-swap (MR02_BTC muore a fee2x), 10m in watchlist (ADD bocciato: OOS 10.86→10.76). Diari `docs/diary/2026-06-12-fade15m-gate.md` / `-fade-tf-sweep.md`.
- **XS01 — Cross-Sectional Reversion (ATTIVO LIVE 2026-06-09; dispersion-gate v1.1.20, 2026-06-10).**
Famiglia nuova XSEC, distinta da pairs (pairwise) e fade (single-asset): ogni `hold=12` ore
classifica 8 crypto (UNIVERSE8) per rendimento a `lb=48` ore e va long i perdenti relativi /
short i vincenti (peso ∝ (ret media cross-section)), market-neutral gross 1, fee 0.20%
RT/book (turnover 2). Scorrelato ~0 da pairs e fade. FULL Sharpe ~3.3, plateau lb 12-72 ×
hold 6-24; cost-sensitive (muore a ~0.35% RT/book). Gate PORT06: OOS Sharpe 9.66→10.07,
FULL DD 3.68→3.46. **Dispersion-gate** (`disp_min=0.0313` = p50 TRAIN): entra solo se la
std cross-section del momentum ≥ soglia — diagnostica monotona TRAIN+OOS, plateau p30-p70,
standalone Sharpe 2.50→3.46 (regge fee 2x), PORT06 OOS 10.07→10.37 a DD pari. Come
trend/hurst sulle fade, il gate agisce SOLO sul path live (backtest canonico non filtrato
→ il live farà meglio del backtest). 8 gambe → PAPER (niente esecuzione reale a €2k).
Worker `CrossSectionalWorker` validato (`validate_xsec_worker`: replay == backtest esatto).
**PHASE-TRANCHING (2026-06-11, `tranches=3`)**: la fase del roll non-sovrapposto è arbitraria
e da sola muove Sharpe daily FULL 1.52-2.33 / DD 13.8-33% (timing-luck) → live gira con 3
sub-book sfasati di hold/3 su capitale comune (PnL/K), ensemble di fase SENZA parametri
fittati. Gate `xs01_tranche_gate.py`: plateau K=2 E K=3 promossi (PORT06 OOS Sh 10.07→10.15,
DD 1.48→1.38, FULL pari); validatore esteso (K=1 == xsec_sim esatto; K=3 == unione fasi
0/4/8 esatto). Solo path live (backtest canonico single-phase). Strategia
`scripts/strategies/XS01_cross_sectional.py`, gate `scripts/analysis/xsec_port06_gate.py` /
`xs01_dispersion_gate.py`. NB scartata nella stessa tornata: FC01 funding-carry (nessun
edge su 6.5 anni). Diari `docs/diary/2026-06-09/10-*`.
- **BLEND ETH/BTC 15m flat-skip (ATTIVO LIVE v1.1.16, 2026-06-09).** Sleeve `PR_ETHBTC_15M` accanto al 1h: pairs ETH/BTC a **15m** (n=66, z_in=1.674, z_exit=1.0, max_bars=35) a **mezza size** (`params.position_size=0.10`). **Origine: gioco "Blind Traders"** (100 agenti ciechi su BTC/ETH anonimizzati → riscoprono la mean-reversion, vincitore = spread ETH/BTC 15m; `scripts/games/`). Gate PORT06 sul serio: **non duplicato (corr 1h↔15m = 0.37)**, robusto (16/16 celle Sharpe>1), e l'edge **NON è artefatto delle candele flat ETH 15m** (16% storico; filtrandole resta l'83% dello Sharpe). Engine `pairs_research.pairs_sim_flat` con uscita **LIVE-REALIZABLE** (`flat_skip`: niente entry/exit su barre O=H=L=C, esce alla 1a barra pulita), **regression-lock** a `pairs_sim` con flat_skip=False. **Worker validato** (`validate_worker_pairs` a 15m: replay == backtest, 8452 vs 8453 trade; 1h byte-exact). Runner: fetch sub-orario diretto da Cerbero + `pos_for_spec` override **per-sleeve** (params.position_size > famiglia > globale). Mezza size perché a peso pieno il 15m pesava il 25.8% del rischio PORT06 (→ 11.5%, bilanciato col 1h) — blend-tilt prudente sul **caveat slippage** (a slippage realistico il vantaggio di Sharpe regge, quello di DD si assottiglia: il vero banco di prova è il ledger reale shadow). Diari `docs/diary/2026-06-09-pairs15m-*.md`, gate `scripts/analysis/pairs15m_port06_gate.py` / `pairs15m_gate_final.py` / `pairs15m_flatcheck.py`, smoke `pairs15m_live_smoke.py`.
- **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 +32% vs +42% — residuo di convenzione capitale-unico vs media-equity; il bug `mean(rets)` sui soli asset in posizione, che dava 44%, è FIXATO il 2026-06-11: sovrappesava N/k a paniere parziale).
- **SH01 bootstrap full-history (punto-10, 2026-06-07).** La ri-validazione col train-window del
regime live (`sh01_trainwindow_validate.py`) ha mostrato che SH01 a train 365g **NON è robusto**
(BTC fee-2x FULL 42%, ETH Sharpe 0.02, trade-rate 22-26% vs 10% validato: LogReg over-confident,
soglia inerte — la diagnosi sweep era esatta). L'edge è MONOTONO nella memoria: solo l'expanding
full-history passa il gate. Fix live: il runner passa agli sleeve `ml` la storia FULL
(`_with_history`: parquet locale + feed, gap-guard con WARN) e `ml_wf_entries(last_block_only=True)`
fitta SOLO l'ultimo blocco del walk-forward → segnali **identici per costruzione** al WF completo
(parity test esatto), 0.6 s/tick su 73k barre. Manutenzione: tenere fresco il parquet
(`download_all()`). MAI ri-tunare la soglia nel regime corto (instabile/incoerente fra asset).
Diario `docs/diary/2026-06-07-sh01-trainwindow.md`.
- **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 — pairs 2 gambe + SH01 (2026-06-08).** Estesa oltre i fade: i **5 pairs PR01**
eseguono reale a 2 gambe (`PairsExecutionClient`: open/close long A/short B, leg-risk unwind, MAI
close_position; `pairs_enabled: true` acceso a conto flat, v1.1.12); **SH01** (BTC/ETH) esegue
single-leg con exit a orizzonte H=12 — niente TP/SL, `_place_real_tp` no-op e `_real_close` chiude
tutto market reduce-only, disaster-bracket on-book come unica protezione di coda (v1.1.13). Motivo:
SH01 è il diversificatore più decorrelato (senza i 5 sleeve PAPER il DD del portafoglio sale
3.96→5.35%). Copertura reale ora 15 sleeve su 19 (fade+DIP+**6 pairs incl. ETH/BTC 15m**+SH01);
restano simulati TR01/ROT02/TSM01/XS01 (book multi-asset, bloccati dal capitale: rumore
arrotondamento 20-30% a €2k, serve ~€20k). Diari `docs/diary/2026-06-08-pairs-executor.md`.
- **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.
- **Disaster-bracket on-book + alert outage (v1.1.4, 2026-06-07).** A ogni `REAL_OPEN` dei fade
eseguiti il worker piazza uno **STOP_MARKET reduce-only a ~30%** dall'ingresso (trigger sul mark,
`ExecutionClient.place_disaster_sl`, cancellato in `_real_close`): assicurazione per gli outage
(poll-loop fermo = exit non valutati), in operatività normale non scatta mai → 0 costo Sharpe.
Config `overrides.execution.disaster_sl_pct` (0.30; 0=off). NB: il `set_stop_loss` di cerbero-mcp
è un `private/edit` (solo ordini APERTI) → inutilizzabile su market fillati; la cancel di un
trigger order risponde `untriggered` (= successo, verificato testnet). Alert Telegram `FEED_OUTAGE`
dopo 5 poll falliti consecutivi (con elenco posizioni reali aperte) + notifica di ripresa. I fix
di parità della stessa tornata (v1.1.3): TR01 fee×leva + forming-bar TR01/Pairs + WARN
`PANEL_SHORT` su TSM01/ROT02; `hourly_report` ora mostra i multi-asset (sezione MULTI-ASSET).
Diario `docs/diary/2026-06-07-sweep-fixes.md`.
- **VERITÀ CONTABILE su netting di conto (v1.1.24, 2026-06-11, da audit live).** Il modello
"quote per-worker con reduce-only" si rompe quando worker dello stesso strumento hanno
direzioni OPPOSTE (pairs long ETH vs fade short ETH): Deribit cappa/respinge i reduce-only
in silenzio. L'audit ha trovato: close fillato 0.078 ma bookato 0.105 (`Fill.amount` era il
RICHIESTO), 3 gambe pairs mai eseguite col PnL sim sommato al ledger reale (gamba orfana sul
conto, ETH/SOL di fatto short nudo), e il conto short 0.027 ETH più dei libri (riallineato a
mano). Fix: (1) `Fill.filled_amount` (fonte `order.filled_amount`) usato da TUTTI i ledger;
(2) `REAL_CLOSE_PARTIAL` (log+alert) quando il close filla meno del residuo; (3) pairs:
PnL bookato SOLO per le gambe verificate, gamba respinta → `orphan_legs` persistito +
alert `PAIR_LEG_ORPHAN`, `applied` solo con ENTRAMBE le gambe (altrimenti sim_fallback
dichiarato); (4) `REAL_DIVERGENCE` anche su jsonl (prima solo Telegram); (5) runner:
tick isolato per-worker (un'eccezione non salta gli altri; alert `WORKER_ERROR_STREAK` a 5).
**RISOLTO in v1.1.25 col NETTING delle chiusure**: `close_amount` tenta il reduce-only
(sicurezza storica: un bug di stato filla 0) e riesegue il RESIDUO cappato/respinto in
**market puro** — che muove il conto esattamente del delta del libro, cioè netta contro le
quote opposte → niente più gambe orfane/close cappati per costruzione (copre anche i pairs
via `close_pair`). Il chiamante riceve UN Fill combinato (prezzo medio pesato, fee sommate,
`notes` con 'netting'); evento `NET_CLOSE` (log+Telegram) a ogni fallback. La sicurezza
persa sul residuo è coperta dal **reconciler orario** (`reconcile_account.py`, cron host
:40, alert `ACCOUNT_DRIFT`): conto vs Σ libri+orfani, tolleranza 1.5×step, anti-race.
`orphan_legs`/`REAL_CLOSE_PARTIAL` restano come ultima difesa (se fallisce anche il market
puro). Test `tests/portfolio/test_netting_close.py`.
- **TP_PHANTOM — il tocco TP va confermato dal book reale (v1.1.23, 2026-06-11).** Il feed
testnet stampa wick anomali che (a) generano segnali fade su ETH e (b) "toccano" il TP
intrabar della stessa barra: il sim bookava +4% fantasma a bars_held=0 e `_real_close`
chiudeva A MERCATO una posizione il cui resting TP non aveva mai fillato (fee/spread a
giro, 14 giri l'11-06, report Telegram 26/0 vs reale 11/15 — fix conteggio in v1.1.22).
Gate in `StrategyWorker._tp_phantom` (zero parametri, verita' d'esecuzione, NON un filtro
di strategia): tocco sim + **resting LIMIT a zero fill** + prezzo corrente che non ha
raggiunto il livello → exit SOPPRESSA (il limit sul book reale e' l'oracolo: se il prezzo
avesse scambiato li', avrebbe fillato); SL close-confirm e max_bars restano attivi.
Fill (anche parziale) o prezzo oltre il livello o worker non eseguito → comportamento
storico. Fail-open su errori di rete. Log `TP_PHANTOM` dedup per barra + alert Telegram.
Test `tests/portfolio/test_tp_phantom.py`.
- **INVERTED_TP_SKIP — l'entry con TP dal lato sbagliato va soppressa (v1.1.33, 2026-06-16).**
Un wick transitorio sul feed puo' far calcolare alla strategia un `tp` dal lato SBAGLIATO
dell'entry (es. donchian: segnale su barra wickata in basso → tp=centro canale, entry al
prezzo RECUPERATO sopra il proprio tp). L'exit intrabar `bar_high>=tp` (long)/`bar_low<=tp`
(short) scatta a `bars_held=0` in PERDITA, con churn di fee e TP reduce-only respinti
(16-06: 8 giri MR02_BTC 15m, sim 17.9 / reale 2.3 grazie al real-truth; TP_PHANTOM NON
lo prende — niente resting oracle, il prezzo HA superato il livello). Gate in
`StrategyWorker._open_position` (zero parametri, verita' d'esecuzione): se `tp<=entry`
(long) / `tp>=entry` (short) → **entry soppressa** (niente ordine reale/disaster-SL), log
`INVERTED_TP_SKIP` dedup per-barra + alert Telegram. Solo path live (il backtest entra al
close del bar del segnale → mai invertito, resta non filtrato → live meglio del backtest).
Cerotto testnet (il fix vero e' mainnet). Test `tests/portfolio/test_inverted_tp.py`,
diario `docs/diary/2026-06-16-inverted-tp-guard.md`.
- **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`.
- **position_size per-famiglia (2026-06-07).** `portfolios.yml` accetta `position_size_family`
(chiave = `weighting.family_of`); plumbing `runner.pos_for_spec`. **PAIRS a 0.20** (esposizione
0.40 ≈ il validato 0.45): la famiglia è senza stop e col globale 0.5×lev2 girava a ~2.2x il
validato (ETH/BTC DD grezzo 78% a quella taglia). PORT06 OOS DD 3.40→1.26% al costo di OOS
Sharpe 9.05→8.43 — assicurazione come il cap SHAPE. Gate `pairspos_port06_impact.py`,
diario `docs/diary/2026-06-07-pairspos-gate.md`. NB (2026-06-11): il **disaster-cap sullo
z** (exit se |z|≥z_stop, griglia pre-registrata 3.0-5.0 su tutte le coppie) è **BOCCIATO**
— coda e Sharpe OOS peggiorano ovunque (lo stop realizza la perdita al massimo overshoot e
l'engine rientra nello spread divergente: churn di fee), **5ª conferma** che gli stop su
mean-reversion sono falsi negativi. Record `scripts/analysis/pairs_zstop_research.py`,
diario `docs/diary/2026-06-11-stability-sweep.md`.
- **REAL-TRUTH ledger (2026-06-10, scelta utente).** Con `overrides.execution.real_truth: true`
(ATTIVO) il `capital` dei worker eseguiti si aggiorna col **PnL dei FILL REALI** (fee reali
incluse) invece del PnL sim: `_real_close`/`_real_close_pair` ritornano `(real_pnl, applied)` e
`_close_position`/`_close` applicano il reale al ledger; il sim resta SOLO diagnostica nel log
CLOSE (`pnl_source`/`sim_pnl`/`real_pnl`). Fallback al sim dichiarato (`pnl_source=sim_fallback`)
solo se il trade reale non è mai esistito/fillato (REAL_OPEN_FAIL/leg-fail). Equity → pesi →
allocazioni → notional derivano così dai soldi veri sul conto (il notional reale era GIÀ la
formula sim `capital·ps·lev`; il gap storico sim/reale era contabile: ledger separati + spike
print delle candele testnet che il sim bookava e il reale no). Le DECISIONI (entry/exit) restano
guidate dal feed; i multi-asset (TR01/ROT02/TSM01/XS01) restano sim per costruzione. Test:
`tests/portfolio/test_real_truth.py`. Diario `docs/diary/2026-06-10-real-truth-ledger.md`.
- **RIBILANCIO CONSERVA L'EQUITY (fix v1.1.31, 2026-06-13).** Il ribilancio giornaliero
gonfiava l'equity quando un worker era in posizione: i flat si dividevano l'INTERO `total`
(che includeva il capitale degli in-position) e gli in-position lo trattenevano in più →
doppio conteggio di Σ(capitalalloc) sugli in-pos. Caso reale: MR02_BTC 15m seedato a 181.19
(eredità INIT_LINEAGE) e in posizione al ribilancio delle 00:01 → **+4.77 di equity dal nulla**
(scoperto perché l'equity saliva senza un trade dietro). Fix: `ledger.allocate(weights, reserved={sid:cap})`
— gli in-position TRATTENGONO il loro capitale (deployato, non spostabile), i flat si dividono
`total Σreserved` per peso RINORMALIZZATO → `Σalloc == total` sempre, equity conservata per
costruzione. `runner.rebalance_allocations` passa i `reserved` dai worker `in_position`. Parità
runner intatta (5.8e-08). Test `test_ledger.py`/`test_runner_rebalance.py`. Diario
`docs/diary/2026-06-13-rebalance-conservation.md`.
- **INIT_LINEAGE — eredità capitale al cambio timeframe (2026-06-12).** Un worker al primo avvio
(niente status.json) eredita `capital`/`real_capital` dal worker più recente di STESSA
strategia+asset su altro tf (glob `{strategy}__{asset}__*`), MAI la posizione. Nato dallo swap
fade 1h→15m: i worker nuovi partivano dall'allocazione del pool scartando il PnL del gemello
(16.8 di equity, riallineata a mano col seed la prima volta). `StrategyWorker._inherit_lineage_capital`,
evento `INIT_LINEAGE`. Test `tests/portfolio/test_capital_lineage.py`.
- **STALE_REAL_POSITION — guard anti-orfano nel reconciler (v1.1.31, 2026-06-13).** Lo swap
1h→15m aveva ritirato MR02_BTC 1h dal config mentre teneva uno short REALE → posizione nuda non
gestita per 13h, e il reconciler NON la vedeva (lo status fermo del worker morto contava ancora
come "libro" → conto == libri). `reconcile_account.compute_stale_real_positions(max_age_min=15)`:
un worker con `real_in_position` e status.json fermo da >15 min = non gestito → alert
`STALE_REAL_POSITION`. Discriminante = STALENESS (un worker vivo riscrive ogni poll): cattura
ritirati-da-swap, crashati, rimossi dal config. Orfano chiuso a mano (testnet, +0.85). Resta da
fare la PREVENZIONE lato runner (flattare/consegnare la posizione del ritirato al boot). Diario
`docs/diary/2026-06-13-orphan-swap-guard.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.
- **Report "stato trades" — separare SEMPRE pool reale e paper-stats.** La directory è la
fonte di verità: `data/portfolio_paper/` = sleeve nel POOL (capitale reale, muovono
equity/conto); `data/portfolio_paper_stats/` = TR01/ROT02/TSM01/**XS01** in SOLA statistica
(PnL a precisione frazionaria, MAI nel ledger). **NON** fare glob su `portfolio_paper*`
(matcha entrambi → gli utili sim dei paper-stats inquinano il realizzato: errore reale del
2026-06-17, +8.15 XS01 attribuiti per sbaglio all'equity). Verità aggregata = ledger PORT06
(`equity`, `total_capital`; unrealized = equitytotal_capital). Usare
`scripts/analysis/trades_status.py` (riconcilia: realizzato POOL + Δunrealized ≈ Δequity).
- **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.
- **FEED TESTNET INAFFIDABILE — `ETH-PERPETUAL` inverse CONGELATO (2026-06-13).** Il feed di
DECISIONE che il runner usa per BTC/ETH (inverse perp, `INSTRUMENT_MAP`) può congelarsi sul
testnet: il 13-06 `ETH-PERPETUAL` era fermo a **1661.95 per 12h+** (1 solo valore) mentre il
reale (`ETH_USDC-PERPETUAL`, lineare, dove si ESEGUE) si muoveva (gap 1.3%); BTC era vivo.
Effetto: tutte le decisioni ETH (SH01_ETH, 3 fade ETH 15m, gambe ETH dei pairs) girano su un
prezzo morto → **spiega lo 0-trade delle fade ETH 15m** (feed piatto = niente rottura banda).
È un GUASTO testnet, non di strategia: su **mainnet** l'arbitraggio tiene inverse ≈ lineare ≈
realtà. Fix testnet possibile (reroute decisione ETH a USDC lineare in `INSTRUMENT_MAP`, con
caveat bootstrap SH01) NON applicato — il fix vero è andare a mainnet. Nota: la dashboard mostra
entry/mark/PnL **REALI** (non il sim-feed dislocato); il sim resta solo nel modal diagnostico.
**Update 2026-06-14: ANCORA congelato a 1661.95 (36h+).** Confermato anche un vincolo strutturale
del feed: **Cerbero v2 (`get_historical_v2`) serve SOLO 5m/15m/1h** — 30m/10m danno HTTP 400 in
ogni formato (l'endpoint legacy `get_historical` è 404, rimosso). La voce `"30m"` in
`runner._SUBHOURLY` era **speculativa, mai testata live** → qualsiasi TF sub-orario oltre 5m/15m
va DERIVATO resamplando nel runner (come 4h/1d dal 1h), non fetchato diretto.
- **GATE FEED CONGELATO — sleeve ETH-leg sospesi (v1.1.32, 2026-06-15, scelta utente "congela
gli sleeve ETH-leg finché il feed non si sblocca").** Audit "stato ordini": il feed ETH congelato
stava generando **perdite REALI** (SH01_ETH 2.83 reali vs 0.09 sim su un close, poi riapertura
della stessa trappola; 4 pairs con gamba ETH entrati su z-score spuri 3/5/+5.6 = artefatto del
log-ratio con ETH pinnato). Fix: `runner._frozen_assets` + `_feed_gated_sids` — 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é non si sblocca (come un outage; disaster-SL on-book =
protezione di coda). **Auto-guarente**: si rilascia alla prima barra COMPLETA non-flat (NON è
l'entry-guard post-flat bocciata: quella salta la candela-gap di ripresa, questo no). **Detector
guasto-vs-illiquido**: conta la run di close **INVARIATE** (prezzo mai cambiato), NON le barre flat
— sul feed reale ETH/BNB/DOGE hanno run 40-64 con 1-4 valori distinti/48h (MORTI) mentre
SOL/LTC/ADA sono flat ma VIVI (run ≤12, 5-31 valori/48h: si muovono ogni ~10 barre). Soglia
`feed_freeze_gate_bars=24` (1 giorno di prezzo immobile, configurabile, 0=off): gatea le **9 gambe
ETH esatte** (3 fade ETH 15m + SH_ETH + 5 pairs con gamba ETH) lasciando attivi BTC-only,
**PR_BTCLTC** (BTC vivo + LTC solo illiquido) e i multi-asset PAPER. Le posizioni ETH già aperte
NON vengono flattate (freeze=pausa: un close forzato passerebbe per lo stesso feed morto). Alert
Telegram `FEED_FROZEN_GATE` GATED/RIPRESO. Test `tests/portfolio/test_freeze_gate.py`, diario
`docs/diary/2026-06-15-frozen-feed-gate.md`. **Resta un cerotto testnet: il fix vero è mainnet.**
- **TIMING SWEEP pairs & honest (2026-06-14, NESSUN deploy).** Domanda utente: pairs/honest
beneficiano di TF più veloci come le fade (swap 1h→15m)? Esito: **no, niente deployato.** Vincolo
dati (alt 1h-only) → sweepabili solo **pairs ETH/BTC** e **DIP01 (BTC)**; TR01/ROT02 fuori scope
(multi-asset alt + lente). Il gate full+OOS migliorava PORT06 (+10m/30m/5m su tutte le metriche),
MA due muri: (a) **30m/10m feed-blocked** (v2 serve solo 5/15m/1h, vedi sopra); (b) **5m** è
nativo ma nel regime ATTUALE è il PEGGIORE (10.5% 30g, DD 19.5%/180g vs 3% del 1h, Sharpe sotto
15m/1h) + flat-share ETH 29% (slippage reale) → guadagno backtest modesto (FULL Sh +0.39) non vale
il rischio su soldi reali. **Lezione: il gate PORT06 full+OOS è necessario ma NON sufficiente**
incrociare con (1) la fattibilità del FEED live e (2) 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"). Artefatti: `scripts/analysis/timing_sweep_pairs_honest.py`,
diario `docs/diary/2026-06-14-timing-sweep-pairs-honest.md`. (Config live invariata: 19 sleeve.)
- **MICRO-TEST MAINNET = il gate per scalare il capitale (piano pronto, 2026-06-13).** Testnet
valida solo la MECCANICA (feed/fill farlocchi); l'edge sopravvive ai fill veri? si risponde solo
con poco denaro reale su mainnet. **Switch già eseguibile via `.env`**: `CerberoClient` legge il
token da env (`CERBERO_TOKEN`, default testnet invariato; `is_mainnet()` helper) → puntare a
mainnet = solo `.env`, niente codice. Piano completo (fasi smoke→fade-only €1000 2-4 sett→verdetto
ledger-vs-backtest→espansione; sizing: fade €1000 = arrotondamento 2.6% BTC, pairs esclusi al 30%):
`docs/specs/mainnet-microtest-plan.md`. **Stato 2026-06-14:** token mainnet (`MAINNET_TOKEN` di
cerbero-mcp, che ha già le chiavi Deribit LIVE) **wired e VERIFICATO**`is_mainnet()=True`, auth
OK su `get_account_summary` read-only. Tenuto in **`.env.mainnet`** dedicato (chmod 600, gitignored),
FUORI dal `.env` condiviso → il runner testnet NON flippa a mainnet a un riavvio accidentale (il
micro-test girerà come **servizio separato** con dir dati pulita + portfolios fade-only, per non
mescolare i ledger). NB architettura token: il `MAINNET_TOKEN` è la chiave d'accesso *all'MCP* +
selettore d'ambiente (non una chiave Deribit) → il nostro client DEVE presentarlo per essere
instradato al live. **UNICO BLOCCO: conto Deribit mainnet VUOTO (€0 USDC/BTC/ETH) → serve deposito
~€1000 USDC** prima di Fase 0 smoke. Minimo assoluto €500 (rumore BTC ~5%), raccomandato €1000 (2.6%).
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
View File
+7 -1
View File
@@ -56,7 +56,13 @@ overrides:
# 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:
enabled: true
# 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:
+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()
+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()
@@ -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()

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