diff --git a/.gitignore b/.gitignore index b9f78d0..3a9f070 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,4 @@ data/_reset_backup/ # game artifacts (log/json di scripts/games e gate) data/games/ +.env.mainnet diff --git a/CLAUDE.md b/CLAUDE.md index 0a1f33d..152700e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -593,10 +593,35 @@ long A / short B sullo z-score del log-ratio, fee su 2 gambe). 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. +- **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`. + `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%). diff --git a/docs/diary/2026-06-14-timing-sweep-pairs-honest.md b/docs/diary/2026-06-14-timing-sweep-pairs-honest.md new file mode 100644 index 0000000..796805c --- /dev/null +++ b/docs/diary/2026-06-14-timing-sweep-pairs-honest.md @@ -0,0 +1,149 @@ +# 2026-06-14 — Timing sweep: PAIRS & HONEST su 5/10/15/30m (vs live) + +## Domanda (goal utente) + +Dopo lo swap fade 1h→15m (v1.1.30), i **pairs** e le **honest** beneficiano dello stesso +trattamento — girare su timeframe più veloci (5/10/15/30 min)? + +Script: `scripts/analysis/timing_sweep_pairs_honest.py` (riusa i motori canonici +`pairs_sim_flat`, replica DIP intrabar verificata == `dip_market_gated(market_n=0)`, gate +PORT06 == `pairs30m_gate`/`dip01`). Niente re-tuning dei parametri al cambio TF (anti-overfit, +come lo swap fade). Tutto netto, leva 3x, OOS held-out (da 2024-10). + +## Vincolo dati (hard — definisce lo scope) + +Solo **BTC/ETH** hanno 5m/15m/30m in locale (10m = resample causale da 5m). **TUTTI gli alt** +(ADA/BNB/DOGE/LTC/SOL/XRP) sono **SOLO 1h**. Quindi: +- **PAIRS:** solo **ETH/BTC** è sweepabile sub-orario. Gli altri 4 pair (gambe alt: LTC/ETH, + ADA/ETH, BTC/LTC, ETH/SOL) restano 1h per sempre senza dati alt sub-orari. +- **HONEST:** solo **DIP01** (BTC, mean-reversion) ha senso + dati. **TR01** (trend EMA20/100 su + 4h, basket alt) e **ROT02** (rotazione dual-momentum 1d, momentum 60g, universo alt) sono lente + (orizzonte multi-giorno/mese) E multi-asset-su-alt → sub-orario **infattibile** (dati) e + **insensato** (momentum a 60 giorni su barre da 5 min). Nessuno sweep. + +## Flat-share (print stale O=H=L=C, = rischio fill) — sale al scendere del TF + +| asset | 5m | 10m | 15m | 30m | 1h | +|-------|----|-----|-----|-----|-----| +| BTC | 10.0% | 4.8% | 3.5% | 1.5% | 0.9% | +| ETH | **29.0%** | 17.3% | 16.4% | 9.0% | 6.7% | + +ETH 5m al 29% è un allarme operativo serio. Nota: **30m ETH (9%) < 15m (16.4%)** → il 30m ha +*meno* rischio stale-print del 15m già live. + +## PAIRS ETH/BTC — il vero free-lunch + +Config UNIVERSALE 1h (n=50, z_in=2.0, z_exit=0.75, max_bars=72), flat_skip live-realizable: + +| tf | Sharpe FULL | OOS Sh | OOS Sh fee2x | CAGR | win% | +|----|-------------|--------|--------------|------|------| +| 5m | 9.54 | 17.19 | 11.98 | 2071% | 56.0 | +| 10m | 9.26 | 16.54 | **13.03** | 1214% | 64.1 | +| 15m (live) | 8.10 | 14.40 | 11.74 | 673% | 66.7 | +| 30m | 6.20 | 11.17 | 9.59 | 312% | 71.7 | +| 1h (live) | 3.74 | 7.11 | 6.19 | 119% | 72.4 | + +(I CAGR a quattro cifre sono compounding leva su singolo sleeve, irrealistici — il portafoglio +normalizza/cappa; il segnale robusto è lo **Sharpe** e il **gate PORT06**.) + +- Sharpe **monotono** al scendere del TF, e **regge le fee 2x** (a differenza delle fade, dove + MR02 muore a 5m). Pairs market-neutral: lo spread rientra più spesso ad alta frequenza, e + ETH/BTC è la coppia più liquida. +- Corr daily fra TF: 5m↔1h 0.28, 10m↔1h 0.32, 30m↔1h 0.51; 10m↔15m 0.67 → i TF veloci sono + **fasi diverse dello stesso edge**, diversificanti, non duplicati. + +**Gate PORT06 (add half-size al BLEND 1h+15m, OOS da 2024-10):** + +| config | FULL Sh | FULL DD% | OOS Sh | OOS DD% | verdetto | +|--------|---------|----------|--------|---------|----------| +| ATTUALE (1h+15m) | 8.13 | 2.47 | 10.86 | 2.09 | — | +| +30m | 8.24 | 2.28 | 10.87 | 1.97 | **MIGLIORA** (già `pairs30m_gate`, mai deployato) | +| +10m | 8.44 | 2.28 | 11.04 | **1.92** | **MIGLIORA** (domina tutto) | +| +5m | 8.52 | 2.30 | 11.00 | 1.95 | **MIGLIORA** (FULL Sh max) | + +Tutte e tre dominano su FULL Sh, FULL DD, OOS Sh, OOS DD. **10m** è il candidato più pulito +(miglior OOS DD, flat-share moderato 17%); **30m** è il più sicuro (flat-share 9% < 15m live). + +## DIP01 (BTC) — più veloce, ma lo swap NON domina + +Engine canonico (n=50, z_in=2.5, sl_atr=2.5, max_bars=24), parità 1h verificata: + +| tf | OOS Sh BTC | OOS Sh fee2x BTC | corr↔1h | OOS Sh ETH | +|----|------------|------------------|---------|------------| +| 5m | 4.91 | 2.41 | 0.13 | 8.43 | +| 10m | 3.47 | 1.99 | 0.17 | 6.51 | +| 15m | 2.40 | 1.40 | 0.22 | 4.49 | +| 30m | 1.34 | **0.77** | 0.41 | 3.54 | +| 1h (live) | 1.69 | 1.36 | — | 2.68 | + +Standalone Sharpe sale al scendere del TF e i TF veloci sono **fortemente diversificanti** +(corr 0.13–0.22 con l'1h). MA: **BTC fee-fragile** (30m f2x 0.77), e il **gate-swap non domina**: + +| config | FULL Sh | FULL DD% | OOS Sh | OOS DD% | +|--------|---------|----------|--------|---------| +| DIP01 1h (live) | 8.13 | 2.47 | 10.86 | 2.09 | +| DIP01 30m | 8.16 | 2.27 | 10.78 | 2.03 | +| DIP01 15m | 8.16 | 2.30 | 10.82 | 2.14 | +| DIP01 10m | 8.31 | 2.49 | 10.85 | 2.15 | +| DIP01 5m | 8.46 | 2.27 | 11.02 | 2.16 | + +Tutti migliorano i FULL ma **alzano l'OOS DD** (e 30m abbassa l'OOS Sharpe) → nessuno domina +sui 4 criteri. DIP01 5m dà il miglior OOS Sharpe (11.02) ma OOS DD 2.16 vs 2.09. La bassa corr +suggerirebbe un **ADD** (DIP01 5m come secondo sleeve diversificante) più che uno swap, ma +aumenta la concentrazione BTC-rev e non è il free-lunch netto che sono i pairs. + +## Caveat (perché backtest ≠ via libera a scalare) + +1. **Slippage su feed flat-alto** = il vero banco di prova, già flaggato per il 15m blend + ("il vero banco di prova è il ledger reale shadow"). A 5m/10m è peggio (ETH 5m 29% flat). + Il backtest usa flat_skip (timing live-realizable) ma NON modella lo slippage attorno ai + print stali. → gateare via shadow ledger PRIMA di sizing, specie il 5m. +2. **OOS = un solo regime** (2024-10→, calmo). Lo Sharpe monotono-col-TF può essere in parte + un artefatto di regime calmo (la MR veloce prospera nel range a bassa vol; lo sweep fade + trovò "i tf veloci pagano nella volatilità, non nella calma"). I pairs sono market-neutral + → meno esposti dei fade ai crash direzionali, ma il caveat resta. + +## Verdetto del backtest (gate full + OOS 2024→) + +- **PAIRS ETH/BTC: il gate diceva SÌ.** Estendere il BLEND con TF più veloci migliorava PORT06 + su tutte le metriche (10m domina, 30m il più sicuro, 5m FULL Sh max), fee-2x robusto. +- **DIP01: marginale.** Più veloce è diversificante ma lo swap non domina e BTC è fee-fragile. +- **TR01/ROT02: NO.** Dati alt 1h-only + orizzonte lento → fuori scope per costruzione. + +## Tentativo di deploy → due muri, NESSUN deploy (2026-06-14) + +L'utente ha scelto di deployare un blend pairs ETH/BTC più veloce. Il wiring è stato fatto e +validato (sleeve def, backtest face, worker replay == backtest esatto, regression-lock +aggiornato) PRIMA di scoprire due blocchi che hanno ribaltato la decisione: + +1. **MURO FEED (30m e 10m).** Il feed live **Cerbero v2 (`/mcp/tools/get_historical`) serve + solo 5m/15m/1h** — il 30m e il 10m danno 400 in OGNI formato (30m/30/30min/1800), e + l'endpoint legacy è 404 (rimosso). La voce `"30m"` in `runner._SUBHOURLY` era **speculativa, + mai testata live**. → un blend 30m/10m NON è drop-in: richiede di derivarlo resamplando il + feed 15m/5m nel runner (modifica al data-path live condiviso). **Lezione: il _SUBHOURLY del + runner ≠ ciò che il feed serve davvero — smoke-testare il fetch prima di promettere un TF.** + +2. **MURO REGIME (5m).** Il 5m è l'unico TF veloce nativo (drop-in), ma il gate full-history + (Sharpe 9.54) è un **relitto dei regimi volatili vecchi**. Nel regime ATTUALE è il PEGGIORE + (ETH/BTC pairs half-size, ret%/Sharpe/DD): + + | tf | ultimi 30g | ultimi 90g | ultimi 180g | + |----|-----------|-----------|------------| + | 5m | −10.5% / −11.5 / 11.6% | −2.0% / −0.2 / 14.1% | +78% / 2.93 / **19.5%** | + | 15m | −4.4% / −6.2 / 5.8% | −3.1% / −0.9 / 5.8% | +56% / 3.80 / 7.9% | + | 1h | −1.0% / −1.1 / 3.0% | +3.0% / +1.3 / 3.0% | +25% / **3.95** / 3.0% | + + Il 5m perde su ogni finestra recente e ha **DD 3-6× il 1h**; persino sui 180g il suo Sharpe + (2.93) è SOTTO 15m/1h. Sommato al **flat-share ETH 5m 29%** (slippage reale, non modellato + dal backtest) e al fatto che girerebbe **reale**, il guadagno modesto di backtest (FULL + Sharpe +0.39) non vale il rischio. **5m: non conviene.** + +**ESITO: nessun deploy. Config live invariata (19 sleeve, pairs 1h+15m).** I file tracciati +ripristinati a HEAD; restano solo questo diario + `timing_sweep_pairs_honest.py` come record. + +**Lezione di metodo:** il gate PORT06 full+OOS è necessario ma non sufficiente — va incrociato +con (a) la fattibilità del FEED live e (b) il regime RECENTE. Un edge full-history può essere +un relitto di volatilità passata; "più veloce = più Sharpe storico" si rovescia in "più veloce += più DD nel regime calmo attuale". Se in futuro si vorrà un blend più veloce: il **30m** resta +il migliore on-paper (flat 9%), ma serve prima derivarlo dal 15m nel runner + ri-verificarlo +nel regime del momento. diff --git a/docs/specs/mainnet-microtest-plan.md b/docs/specs/mainnet-microtest-plan.md index ccfaebe..fc3213f 100644 --- a/docs/specs/mainnet-microtest-plan.md +++ b/docs/specs/mainnet-microtest-plan.md @@ -9,7 +9,21 @@ Costo del verdetto: poche centinaia di € invece di scoprirlo a €5k. 2026-06-13 `ETH-PERPETUAL` inverse era **congelato a 1661.95 da 12h+** mentre il prezzo reale si muoveva (gap −1.3%); BTC era vivo. Su mainnet l'arbitraggio tiene inverse ≈ lineare ≈ realtà (entro bps): **il micro-test risolve il problema feed -per costruzione**, oltre a dare fill/slippage/fee reali. +per costruzione**, oltre a dare fill/slippage/fee reali. (Il 2026-06-14 `ETH-PERPETUAL` +testnet era ancora congelato a 1661.95 da 36h+ → conferma definitiva: testnet è inaffidabile.) + +--- + +## STATO (2026-06-14) + +- ✅ **Token mainnet wired e VERIFICATO.** `MAINNET_TOKEN` (da cerbero-mcp, che ha già le chiavi + Deribit LIVE) copiato in **`.env.mainnet`** (chmod 600, gitignored), SEPARATO dal `.env` + condiviso. Verifica read-only: `is_mainnet()=True`, auth OK su `get_account_summary` (nessun + ordine). Il `.env` condiviso resta testnet → il runner attuale NON flippa a mainnet a un riavvio. +- 🔴 **BLOCCO: conto Deribit mainnet VUOTO** (equity 0.0 USDC/BTC/ETH). Serve il deposito (~€1000 + USDC) prima di qualsiasi smoke/trade. +- ⏭️ Appena finanziato: avviare il micro-test come **servizio separato** (env_file `.env.mainnet`, + dir dati pulita, `portfolios.yml` fade-only) → Fase 0 smoke → Fase 1. --- @@ -26,13 +40,19 @@ per costruzione**, oltre a dare fill/slippage/fee reali. ## Come si punta a mainnet (già pronto, 1 sola modifica .env) -Il client legge il token da env (default = testnet). Per il micro-test, nel `.env`: +Il client legge il token da env (default = testnet). Il token mainnet vive in un file +**`.env.mainnet`** DEDICATO (NON nel `.env` condiviso, che resta testnet → il runner attuale +non flippa a mainnet a un riavvio): ``` +# .env.mainnet (gitignored, chmod 600) — caricato SOLO dal servizio micro-test mainnet CERBERO_TOKEN= CERBERO_BOT_TAG=pythagoras-mainnet ``` +Il servizio micro-test usa `env_file: [.env, .env.mainnet]` (le chiavi di `.env.mainnet` +prevalgono → Telegram da `.env`, token mainnet da `.env.mainnet`). + **Nessuna modifica di codice.** `CerberoClient.is_mainnet()` lo rileva; il runner, l'ExecutionClient e gli smoke usano lo stesso client. Per tornare a testnet: togliere le due righe. (Verificato: default invariato testnet, override flippa il rilevamento.) @@ -142,9 +162,10 @@ feed congelato). Criterio (lo stesso del report, soglie già nel verdetto 🟢/ ## Checklist di avvio -- [ ] Conto Deribit mainnet KYC + finanziato €1000 USDC -- [ ] MAINNET_TOKEN Cerbero abilitato al trading sul conto -- [ ] `.env` con `CERBERO_TOKEN` + `CERBERO_BOT_TAG=pythagoras-mainnet` (dir dati pulita) +- [x] MAINNET_TOKEN Cerbero abilitato + verificato (auth OK, is_mainnet=True) — 2026-06-14 +- [x] Token in `.env.mainnet` (separato, gitignored, chmod 600) + `CERBERO_BOT_TAG=pythagoras-mainnet` +- [ ] **Conto Deribit mainnet KYC + finanziato ~€1000 USDC** ← BLOCCO ATTUALE (conto a €0) +- [ ] Servizio micro-test separato (env_file `.env.mainnet`, dir dati pulita) - [ ] Fase 0 smoke mainnet verde (ordine→verifica→fee) - [ ] `portfolios.yml` micro-test (fade-only, €1000, exec sleeves MR01/02/07) - [ ] runner mainnet + reconciler + report ledger-vs-backtest schedulati diff --git a/scripts/analysis/timing_sweep_pairs_honest.py b/scripts/analysis/timing_sweep_pairs_honest.py new file mode 100644 index 0000000..f10e7cc --- /dev/null +++ b/scripts/analysis/timing_sweep_pairs_honest.py @@ -0,0 +1,273 @@ +"""TIMING SWEEP — famiglie PAIRS & HONEST su 5/10/15/30m (vs live), goal 2026-06-14. + +Domanda utente: i pairs e le honest beneficiano di timeframe piu' veloci, come hanno +fatto le fade (swap 1h->15m, v1.1.30)? + +VINCOLO DATI (hard): solo BTC/ETH hanno 5m/15m/30m in locale (10m = resample da 5m, qui +in data/raw/{a}_10m.parquet temporanei). TUTTI gli alt (ADA/BNB/DOGE/LTC/SOL/XRP) sono +SOLO 1h. Conseguenze: + - PAIRS: solo ETH/BTC e' sweepabile sub-orario. Gli altri 4 pair (gambe alt: + LTC/ETH, ADA/ETH, BTC/LTC, ETH/SOL) restano 1h per mancanza di dati alt sub-orari. + - HONEST: solo DIP01 (BTC, mean-reversion) ha senso + dati. TR01 (trend EMA 4h su + basket alt) e ROT02 (rotazione dual-momentum 1d su universo alt) sono lente + (orizzonte multi-giorno/mese) E multi-asset-su-alt -> sub-orario infattibile (dati) + e insensato (momentum a 60g su barre 5min). + +Tutto NETTO (fee 0.10% RT single / 0.20% RT per coppia a 2 gambe), leva 3x, OOS = held-out. +Engine CANONICI riusati (pairs_sim_flat, replica dip intrabar == dip_market_gated, gate +PORT06 == pairs30m_gate/dip01). Niente re-tuning dei parametri al cambio TF (anti-overfit, +come lo swap fade). + + uv run python scripts/analysis/timing_sweep_pairs_honest.py +""" +from __future__ import annotations + +import sys +from pathlib import Path + +import numpy as np +import pandas as pd + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(PROJECT_ROOT)) + +from src.data.downloader import load_data +from scripts.analysis.pairs_research import pairs_sim_flat +from scripts.analysis.report_families import daily_from +from scripts.analysis.combine_portfolio import port_returns, metrics, SPLIT, OOS_DATE, IDX, _norm +from scripts.portfolios._defs import PORTFOLIOS +from src.portfolio.sleeves import all_sleeve_equities +from src.portfolio import weighting as W + +TFS = ["5m", "10m", "15m", "30m", "1h"] +BARS_PER_DAY = {"5m": 288, "10m": 144, "15m": 96, "30m": 48, "1h": 24} +LEV, POS = 3.0, 0.15 +EPOCH = pd.Timestamp(0, tz="UTC") + + +def _ensure_10m(): + """10m non e' nativo (download_all fa 5m/15m/30m/1h): lo resampla da 5m se manca. + Aggregazione OHLCV causale (first/max/min/last/sum). File gitignored, rigenerabile.""" + for a in ("btc", "eth"): + dst = PROJECT_ROOT / f"data/raw/{a}_10m.parquet" + if dst.exists(): + continue + df = load_data(a.upper(), "5m").sort_values("timestamp").reset_index(drop=True) + ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True) + g = df.set_index(ts).resample("10min") + out = pd.DataFrame({"open": g["open"].first(), "high": g["high"].max(), + "low": g["low"].min(), "close": g["close"].last(), + "volume": g["volume"].sum()}).dropna() + out["timestamp"] = (out.index - EPOCH) // pd.Timedelta(milliseconds=1) + out.reset_index(drop=True).to_parquet(dst, index=False) + print(f" [bootstrap] generato {dst.name} ({len(out)} righe da 5m)") + +# config UNIVERSALE pairs (1h, CLAUDE.md) — NON ri-tunata al cambio TF (anti-overfit) +PAIR_CFG = dict(n=50, z_in=2.0, z_exit=0.75, max_bars=72) +# config DIP01 canonica (dip_market_gated default, market_n=0 = live) +DIP_CFG = dict(n=50, z_in=2.5, sl_atr=2.5, max_bars=24) + + +# ============================ helper dati ============================ +def flat_share(asset, tf): + df = load_data(asset, tf) + o, h, l, c = df["open"], df["high"], df["low"], df["close"] + return ((o == h) & (h == l) & (l == c)).mean() * 100 + + +# ============================ DIP01 engine TF-aware ============================ +def _atr(df, n=14): + h, l, c = df["high"].values, df["low"].values, df["close"].values + pc = np.roll(c, 1); pc[0] = c[0] + tr = np.maximum(h - l, np.maximum(np.abs(h - pc), np.abs(l - pc))) + return pd.Series(tr).rolling(n).mean().values + + +def dip_sim(asset, tf, n=50, z_in=2.5, sl_atr=2.5, max_bars=24, + fee_rt=0.001, oos_frac=0.0): + """Replica TF-aware di honest_improve2.dip_market_gated(market_n=0): dip-buy z-score, + TP=SMA intrabar, SL=close-sl_atr*ATR intrabar (SL prioritario), max_bars. Engine canonico.""" + df = load_data(asset, tf) + h, l, c = df["high"].values, df["low"].values, df["close"].values + N = len(c); ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True) + ma = pd.Series(c).rolling(n).mean().values + sd = pd.Series(c).rolling(n).std().values + a = _atr(df, 14) + z = (c - ma) / np.where(sd == 0, np.nan, sd) + fee = fee_rt * LEV + cap = peak = 1000.0; dd = 0.0; last_exit = -1 + eq_ts, eq_v = [], []; rets = []; trades = wins = 0; yearly = {} + split = int(N * (1 - oos_frac)) if oos_frac else 0 + for i in range(n + 14, N): + if i < split or np.isnan(z[i]) or np.isnan(a[i]): + continue + if not (z[i] <= -z_in and z[i - 1] > -z_in): + continue + if i <= last_exit or i + 1 >= N: + continue + entry = c[i]; tp, sl, mb = ma[i], c[i] - sl_atr * a[i], max_bars + exit_p = c[min(i + mb, N - 1)]; j = min(i + mb, N - 1) + for k in range(1, mb + 1): + j = i + k + if j >= N: + j = N - 1; exit_p = c[j]; break + if l[j] <= sl: + exit_p = sl; break + if h[j] >= tp: + exit_p = tp; break + if k == mb: + exit_p = c[j] + ret = (exit_p - entry) / entry * LEV - fee + cap = max(cap + cap * POS * ret, 10.0) + peak = max(peak, cap); dd = max(dd, (peak - cap) / peak) + last_exit = j; trades += 1; wins += ret > 0; rets.append(ret * POS) + yearly[ts.iloc[i].year] = yearly.get(ts.iloc[i].year, 0.0) + ret * 100 + eq_ts.append(ts.iloc[j]); eq_v.append(cap) + yrs = (ts.iloc[-1] - ts.iloc[max(split, 0)]).days / 365.25 or 1 + sh = float(np.mean(rets) / np.std(rets) * np.sqrt(trades / yrs)) if len(rets) > 1 and np.std(rets) > 0 else 0.0 + return dict(trades=trades, win=wins / trades * 100 if trades else 0, ret=(cap / 1000 - 1) * 100, + dd=dd * 100, sharpe=sh, yearly=yearly, eq_ts=eq_ts, eq_v=eq_v) + + +# ============================ gate PORT06 ============================ +def port_metrics(members, ids, clusters, caps): + dr = pd.DataFrame({i: members[i].pct_change().fillna(0.0) for i in ids}) + w = W.weight_vector("cap", ids, dr, caps=caps, clusters=clusters) + drp = port_returns({i: members[i] for i in ids}, w) + return metrics(drp), metrics(drp, lo=SPLIT), w + + +def daily_norm(eq_ts, eq_v): + return _norm(daily_from(eq_ts, eq_v)) + + +# ============================ PART 1: dati ============================ +def part1_data(): + print("=" * 96) + print(" PART 1 — REALTA' DATI: flat-share (O=H=L=C, = print stale, rischio fill) per TF") + print("=" * 96) + print(f" {'asset':<6}" + "".join(f"{tf:>8}" for tf in TFS)) + for a in ("BTC", "ETH"): + print(f" {a:<6}" + "".join(f"{flat_share(a, tf):>7.1f}%" for tf in TFS)) + print("\n ALT (ADA/BNB/DOGE/LTC/SOL/XRP): SOLO 1h disponibile -> NON sweepabili sub-orario.") + print(" => pairs con gamba alt (4/5) e honest multi-asset (TR01/ROT02) bloccati a 1h dai dati.") + + +# ============================ PART 2: pairs ETH/BTC standalone ============================ +def part2_pairs_standalone(): + print("\n" + "=" * 96) + print(" PART 2 — PAIRS ETH/BTC standalone, config UNIVERSALE n=50 z_in=2.0 z_exit=0.75 mb=72") + print(f" (flat_skip=True, live-realizable; OOS held-out; fee 0.20% RT/coppia; f2x = OOS Sh a fee 2x)") + print("=" * 96) + print(f" {'tf':<5}{'trd':>7}{'FULL%':>9}{'DD%':>7}{'Sh':>7} | {'OOS%':>9}{'oDD%':>7}{'oSh':>7} | {'f2x_oSh':>8}{'mb_h':>6}") + eqs = {} + for tf in TFS: + f = pairs_sim_flat("ETH", "BTC", tf=tf, **PAIR_CFG, flat_skip=True) + o = pairs_sim_flat("ETH", "BTC", tf=tf, **PAIR_CFG, flat_skip=True, split_frac=1 - 0.30) + o2 = pairs_sim_flat("ETH", "BTC", tf=tf, **PAIR_CFG, flat_skip=True, split_frac=1 - 0.30, fee_rt=0.002) + eqs[tf] = daily_norm(f["eq_ts"], f["eq_v"]) + mb_h = PAIR_CFG["max_bars"] / BARS_PER_DAY[tf] * 24 + print(f" {tf:<5}{f['trades']:>7}{f['ret']:>9.0f}{f['dd']:>7.1f}{f['sharpe']:>7.2f}" + f" | {o['ret']:>9.0f}{o['dd']:>7.1f}{o['sharpe']:>7.2f} | {o2['sharpe']:>8.2f}{mb_h:>6.1f}") + # correlazioni daily fra TF + print("\n CORR rendimenti daily fra TF (alta = ridondante):") + print(f" {'':<6}" + "".join(f"{tf:>7}" for tf in TFS)) + for t1 in TFS: + row = [] + for t2 in TFS: + c = eqs[t1].pct_change().fillna(0).corr(eqs[t2].pct_change().fillna(0)) + row.append(f"{c:>7.2f}") + print(f" {t1:<6}" + "".join(row)) + return eqs + + +# ============================ PART 3: pairs gate PORT06 ============================ +def part3_pairs_gate(eqs): + print("\n" + "=" * 96) + print(" PART 3 — GATE PORT06: aggiungere ETH/BTC 5m e/o 10m al BLEND live (1h+15m), mezza size") + print(f" (baseline = sleeve canonici live; OOS da {OOS_DATE})") + print("=" * 96) + p = PORTFOLIOS["PORT06"] + base = dict(all_sleeve_equities()) # include PR_ETHBTC (1h) + PR_ETHBTC_15M + ids0 = list(p.sleeve_ids); cl0 = p.clusters; caps = p.caps + f0, o0, _ = port_metrics(base, ids0, cl0, caps) + print(f" {'config':<26}{'FULL Sh':>9}{'FULL DD%':>10}{'OOS Sh':>9}{'OOS DD%':>9}") + print(f" {'ATTUALE (1h+15m)':<26}{f0['sharpe']:>9.2f}{f0['dd']:>10.2f}{o0['sharpe']:>9.2f}{o0['dd']:>9.2f}") + # half-size pairs equity (pos 0.075 come 15m live): ricalcolo eq a pos dimezzato + for tf in ("10m", "5m"): + fr = pairs_sim_flat("ETH", "BTC", tf=tf, **PAIR_CFG, flat_skip=True, pos=0.075) + cand = daily_norm(fr["eq_ts"], fr["eq_v"]) + mem = dict(base); sid = f"PR_ETHBTC_{tf.upper()}" + mem[sid] = cand + ids = ids0 + [sid]; cl = dict(cl0); cl[sid] = "ETH-rev" + f1, o1, w1 = port_metrics(mem, ids, cl, caps) + ok = (o1["sharpe"] >= o0["sharpe"] - 0.02 and o1["dd"] <= o0["dd"] + 1e-9 + and f1["sharpe"] >= f0["sharpe"] - 0.02 and f1["dd"] <= f0["dd"] + 1e-9) + verdict = "MIGLIORA (promosso)" if ok else "non domina (vedi numeri)" + print(f" {'+' + tf + ' (half size)':<26}{f1['sharpe']:>9.2f}{f1['dd']:>10.2f}{o1['sharpe']:>9.2f}{o1['dd']:>9.2f} {verdict}") + + +# ============================ PART 4: DIP01 ============================ +def part4_dip(): + print("\n" + "=" * 96) + print(" PART 4 — HONEST/DIP01 (BTC) standalone, config canonica n=50 z_in=2.5 sl_atr=2.5 mb=24") + print(" (engine == dip_market_gated market_n=0; OOS held-out; fee 0.10% RT; f2x = OOS Sh a fee 2x)") + print("=" * 96) + print(f" {'tf':<5}{'asset':<5}{'trd':>7}{'WR%':>6}{'FULL%':>9}{'DD%':>7}{'Sh':>7} | {'OOS%':>9}{'oDD%':>7}{'oSh':>7} | {'f2x_oSh':>8}{'mb_h':>6}") + eqs = {} + for asset in ("BTC", "ETH"): + for tf in TFS: + f = dip_sim(asset, tf, **DIP_CFG) + o = dip_sim(asset, tf, **DIP_CFG, oos_frac=0.30) + o2 = dip_sim(asset, tf, **DIP_CFG, oos_frac=0.30, fee_rt=0.002) + if asset == "BTC": + eqs[tf] = daily_norm(f["eq_ts"], f["eq_v"]) if f["eq_v"] else None + mb_h = DIP_CFG["max_bars"] / BARS_PER_DAY[tf] * 24 + print(f" {tf:<5}{asset:<5}{f['trades']:>7}{f['win']:>6.1f}{f['ret']:>9.0f}{f['dd']:>7.1f}{f['sharpe']:>7.2f}" + f" | {o['ret']:>9.0f}{o['dd']:>7.1f}{o['sharpe']:>7.2f} | {o2['sharpe']:>8.2f}{mb_h:>6.1f}") + print() + # corr daily BTC fra TF + print(" CORR rendimenti daily DIP01 BTC fra TF:") + print(f" {'':<6}" + "".join(f"{tf:>7}" for tf in TFS)) + for t1 in TFS: + row = [] + for t2 in TFS: + if eqs.get(t1) is None or eqs.get(t2) is None: + row.append(f"{'-':>7}"); continue + c = eqs[t1].pct_change().fillna(0).corr(eqs[t2].pct_change().fillna(0)) + row.append(f"{c:>7.2f}") + print(f" {t1:<6}" + "".join(row)) + return eqs + + +# ============================ PART 5: DIP01 gate PORT06 ============================ +def part5_dip_gate(eqs): + print("\n" + "=" * 96) + print(" PART 5 — GATE PORT06: SWAP DIP01_BTC 1h -> TF piu' veloce (sostituisce lo sleeve)") + print("=" * 96) + p = PORTFOLIOS["PORT06"] + base = dict(all_sleeve_equities()) + ids0 = list(p.sleeve_ids); cl0 = p.clusters; caps = p.caps + f0, o0, _ = port_metrics(base, ids0, cl0, caps) + print(f" {'config':<26}{'FULL Sh':>9}{'FULL DD%':>10}{'OOS Sh':>9}{'OOS DD%':>9}") + print(f" {'ATTUALE (DIP01 1h)':<26}{f0['sharpe']:>9.2f}{f0['dd']:>10.2f}{o0['sharpe']:>9.2f}{o0['dd']:>9.2f}") + for tf in ("30m", "15m", "10m", "5m"): + if eqs.get(tf) is None: + continue + mem = dict(base); mem["DIP01_BTC"] = eqs[tf] + f1, o1, _ = port_metrics(mem, ids0, cl0, caps) + ok = (o1["sharpe"] >= o0["sharpe"] - 0.02 and o1["dd"] <= o0["dd"] + 1e-9 + and f1["sharpe"] >= f0["sharpe"] - 0.02 and f1["dd"] <= f0["dd"] + 1e-9) + verdict = "MIGLIORA" if ok else "non domina" + print(f" {'DIP01 ' + tf:<26}{f1['sharpe']:>9.2f}{f1['dd']:>10.2f}{o1['sharpe']:>9.2f}{o1['dd']:>9.2f} {verdict}") + + +if __name__ == "__main__": + _ensure_10m() + part1_data() + pe = part2_pairs_standalone() + part3_pairs_gate(pe) + de = part4_dip() + part5_dip_gate(de) + print("\n NB TR01/ROT02: nessuno sweep — dati alt solo 1h + orizzonte multi-giorno/mese") + print(" (trend EMA20/100 4h, rotazione momentum 60g 1d) rendono il sub-orario infattibile e insensato.")