diff --git a/CLAUDE.md b/CLAUDE.md index 5a19193..d28e5b7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,13 +24,13 @@ 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, fetch candele, tick worker + multi_runner.py → orchestratore: carica YAML, fetch candele (15m + 1h live per MTF), tick worker strategy_worker.py → worker indipendente: capital, trade log, stato persistente strategy_loader.py → import dinamico classi Strategy da scripts/strategies/ cerbero_client.py → client HTTP per Cerbero MCP (Deribit testnet) signal_engine.py → squeeze + ML real-time (per ML01) telegram_notifier.py → notifiche Telegram per trade -scripts/strategies/ → strategie attive (SQ01-SQ04, ML01) +scripts/strategies/ → strategie attive (SQ01-SQ04, ML01, MT01, PD01, CM01, AD01) scripts/waste/ → strategie scartate (W01-W22 + REF originali) scripts/analysis/ → script di confronto e report strategies.yml → config multi-strategy paper trader @@ -103,6 +103,7 @@ Orchestratore che esegue N strategie in parallelo su dati live Cerbero, ognuna c **Config:** `strategies.yml` — lista strategie con asset, tf, sizing, parametri. **Persistenza:** `data/paper_trades/{strategy}___{asset}__{tf}/` con `trades.jsonl` (append-only) + `status.json` (resume al restart). **Hot-add:** aggiungi riga YAML → `docker compose restart` → storico intatto. +**MTF live:** le strategie multi-timeframe (MT01) ricevono l'1h **live da Cerbero** ad ogni poll (non dal parquet statico), passato a `generate_signals` via il parametro `df_1h`. Evita il drift del trend 1h tra un `download_all()` e l'altro. **Notifiche:** Telegram per ogni trade (richiede `.env` con `TELEGRAM_BOT_TOKEN` e `TELEGRAM_CHAT_ID`). ## Convenzioni @@ -119,3 +120,5 @@ Orchestratore che esegue N strategie in parallelo su dati live Cerbero, ognuna c - **Fee:** sempre 0.1% per lato (0.2% round-trip). Includere nel backtest. - **Leva:** testato con 3x. Aumentare a 5x migliora i rendimenti ma raddoppia il drawdown. - **GBM:** GradientBoostingClassifier di scikit-learn. Ensemble di alberi decisionali sequenziali. Walk-forward per evitare leakage temporale. +- **Cerbero `get_historical` (fix 2026-05-28):** `end_date` come data nuda è inclusivo dell'intera giornata fino all'ultima candela chiusa (es. `end=oggi` arriva fino ad ora, non più a mezzanotte); accettati anche timestamp con orario (`...T14:00:00`, naive=UTC); nessun cap a ~5000 righe (paginazione interna). Il client passa già `end=oggi`, ora corretto. Prima del fix il paper trader restava a zero trade perché il feed era fermo a mezzanotte. +- **Dati ETH Deribit 15m:** 14-30%/anno di candele *flat* (O=H=L=C, volume 0, run fino a ~54h) per bassa liquidità del perpetuo. Verificato (2026-05-28): escluderle NON cambia i backtest (Δacc ≤0.5pp) → edge robusto. Resta un caveat operativo (slippage/fill in trading reale, irrilevante per paper). BTC pulito eccetto picco ~8% nel 2024. diff --git a/README.md b/README.md index 88f580a..04d8b68 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ uv run python scripts/analysis/yearly_market_report.py ## Paper Trading Live -Il multi-strategy runner esegue N strategie in parallelo su dati live da Cerbero MCP, ognuna con €1000 USDC virtuali indipendenti. +Il multi-strategy runner esegue N strategie in parallelo su dati live da Cerbero MCP, ognuna con €1000 USDC virtuali indipendenti. Le strategie multi-timeframe (MT01) ricevono anche l'1h **live** da Cerbero ad ogni poll, così la conferma del trend non resta indietro rispetto al feed 15m. ### Avvio diff --git a/docs/diary/2026-05-28.md b/docs/diary/2026-05-28.md new file mode 100644 index 0000000..acd3ee1 --- /dev/null +++ b/docs/diary/2026-05-28.md @@ -0,0 +1,96 @@ +# 2026-05-28 — Giorno 3: Bug dati Cerbero, paper trader fermo, fix MT01 multi-timeframe + +### 12:20 — Sintomo: paper trader live a zero trade + +**Cosa:** check del container `pythagoras-multi` (multi-strategy paper trader, 6 strategie). +**Reale:** container healthy da ore, ma **0 trade** su tutte le strategie, tutte FLAT a €1000. +Primo falso indizio: `last_bar_ts: 0` in tutti gli `status.json`. Indagando il worker, +quel campo si aggiorna **solo a posizione aperta** (contatore `hold_bars`), non ad ogni +candela → non è la causa. Il loop era vivo (status.json riscritti ogni 60s). +**Lezione:** non fidarsi del nome di un campo; verificare nel codice quando viene scritto. +L'healthcheck del container controlla solo l'esistenza di `status.json`, non la freschezza +→ un loop bloccato risulterebbe comunque "healthy". + +### 12:45 — Causa radice: bug lato Cerbero MCP `get_historical` + +**Cosa:** probe dirette all'endpoint `/mcp-deribit/tools/get_historical`. +**Reale:** due bug lato server: +1. **`end_date` data-nuda tronca a mezzanotte:** `end=oggi` restituiva candele solo fino a + `oggi 00:00`. Il `df` live finiva sempre alla barra di mezzanotte e **non avanzava** durante + la giornata → nessun breakout fresco sull'ultima barra → nessun ingresso (condizione worker + `last_signal.idx >= last_idx - 1`). +2. **Cap a ~5000 righe** che ignora `start_date`: una richiesta di 365g a 15m restituiva ~52 + giorni. Ecco perché ML01 si addestrava su soli 88 samples (overfit, train_acc 100%). +**Lezione:** lo zero-trade non era nelle strategie ma nel feed dati. Sempre validare la +freschezza/copertura dei dati prima di sospettare la logica. + +### 13:30 — Fix lato Cerbero + verifica + +**Cosa:** report passato al dev di `cerbero-mcp`; fix deployato (riavvio container) + doc +aggiornata in `cerbero-mcp/docs/API_REFERENCE.md`. +**Reale dopo deploy (verificato con probe):** +- `end=oggi` (data nuda) → ultima candela = ora corrente (age ~3 min). ✅ +- 365g a 15m → **35.099 candele**, span 365.6g, nessun cap. ✅ +- Supportati anche timestamp con orario (`...T14:00:00`, naive = UTC). ✅ + +Nostro client (`src/live/cerbero_client.py`) invariato: passa già `end=oggi`, ora corretto. +**Lezione:** "trust but verify" — la doc dichiarava i fix prima che fossero deployati; solo +la probe diretta ha confermato cosa era davvero attivo sul server. + +### 14:00 — Problema residuo: MT01 usava un trend 1h STANTIO + +**Cosa:** check di tutte le strategie sul percorso di codice reale con dati freschi. +**Reale:** +- Tutte le 6 strategie girano senza crash; SQ01/SQ02 generano molti segnali. +- **MT01 leggeva il trend 1h dal parquet statico** (`load_data(asset,"1h")`), non da Cerbero. + Il parquet finiva a mezzanotte → per ogni barra 15m di oggi `searchsorted` cadeva oltre la + fine e si agganciava sempre alla candela di mezzanotte (gap 14.8h). La conferma + multi-timeframe — il cuore di MT01 — era di fatto congelata e il gap cresce ogni giorno. +- In `data/raw/` mancavano del tutto i parquet **15m** (`btc_15m`, `eth_15m`) → backtest 15m rotti. +**Lezione:** una strategia live che dipende da un file statico ha un punto cieco temporale; +il dato live e quello di backtest devono provenire da fonti coerenti. + +### 14:30 — Fix MT01: trend 1h live da Cerbero + +**Cosa:** modifica al runner perché MT01 prenda l'1h live, non dal parquet. +- `MT01.generate_signals` accetta un `df_1h` opzionale (fallback al parquet se assente). +- `StrategyWorker.tick(df, df_1h=None)` lo inoltra ai signal. +- `multi_runner` fa fetch 1h live (resolution 60) per gli asset MT01 ad ogni poll (`htf_cache`). +**Reale (verificato a codice montato, pre-rebuild):** gap del trend 1h sull'ultima barra +**0.75h** (fresco) contro **14.8h** col parquet statico. Segnali invariati sullo storico. +**Lezione:** isolare la dipendenza dal file statico rende MT01 immune al drift tra un +`download_all()` e l'altro. + +### 14:55 — Rigenerazione dati + rebuild + +**Cosa:** `download_asset` per 15m+1h (saltati 1m/5m, lenti e inutilizzati), poi +`docker compose up -d --build` (il codice `src/` è baked nell'immagine). +**Reale:** parquet rigenerati con storia completa 2018→2026 e freschi (15m fino alle 14:45, +1h fino alle 14:00). Container ripartito: 6 strategie attive, ML01 riaddestrato su **534 +samples** (anno pieno), MT01 senza errori, fetch 1h live OK. + +### 15:00 — Regressione backtest sui dati rigenerati + +**Cosa:** rilanciati i backtest per confermare che i numeri documentati si riproducano sui +dati ricreati da zero (BTC/ETH 15m, hold=3, fee 0.2% RT, leva 3x, pos 15%). +**Reale:** accuratezze e drawdown **identici**, solo +1/+3 trade dalle barre recenti in più. + +| Strategia | Ottenuto | Documentato | Esito | +|---|---|---|---| +| SQ01 BTC 15m | 76.7% / DD 6.7% / 4063t | 76.7% / 6.7% / 4062 | ✓ | +| SQ01 ETH 15m | 76.4% / 6.2% / 2951t | 76.4% / 6.2% / 2948 | ✓ | +| SQ02 BTC 15m | 79.7% / 6.5% / 1251t | 79.7% / 6.5% / 1250 | ✓ | +| SQ02 ETH 15m | 78.6% / 3.4% / 944t | 78.6% / 3.4% / 942 | ✓ | +| **MT01 BTC 15m (ema20+vol)** | **82.7% / 5.9% / 503t** | 82.7% / 5.9% / 503 | ✓ esatto | +| MT01 ETH 15m (ema20+vol) | 81.2% / 2.9% / 404t | — | ok | + +**Lezione:** l'integrità dei dati rigenerati è confermata — la pipeline di download produce +risultati riproducibili. La config live di MT01 (ema20+vol) coincide col best documentato. + +### Punti aperti + +1. **Backtest e drift dati:** MT01 live ora è immune (1h da Cerbero), ma i backtest girano + sempre sui dati fino all'ultimo `download_all()`. Per dati di backtest sempre freschi + serve uno scheduling del download (cron/job). +2. **Healthcheck:** valutare un check su mtime di `status.json` (< 180s) per rilevare uno + stallo del loop, non solo l'esistenza del file. diff --git a/scripts/strategies/MT01_squeeze_mtf_momentum.py b/scripts/strategies/MT01_squeeze_mtf_momentum.py index 65f76bc..a9ed922 100644 --- a/scripts/strategies/MT01_squeeze_mtf_momentum.py +++ b/scripts/strategies/MT01_squeeze_mtf_momentum.py @@ -57,7 +57,9 @@ class SqueezeMTFMomentum(Strategy): kcr = keltner_ratio(c, h, l, 14) events = detect_squeezes(c, h, l, kcr, sq_thr) - df_1h = load_data(asset, "1h") + df_1h = params.get("df_1h") + if df_1h is None: + df_1h = load_data(asset, "1h") c1h = df_1h["close"].values ts1h_ms = df_1h["timestamp"].values n1h = len(c1h) diff --git a/src/live/multi_runner.py b/src/live/multi_runner.py index a1ead2a..080489f 100644 --- a/src/live/multi_runner.py +++ b/src/live/multi_runner.py @@ -210,12 +210,32 @@ def run(): df = df.sort_values("timestamp").reset_index(drop=True) candle_cache[(asset, tf)] = df + # Fetch 1h live per strategie multi-timeframe (es. MT01): + # il trend va preso da Cerbero, non dal parquet statico (che resta indietro). + htf_cache: dict[str, pd.DataFrame] = {} + mtf_assets = {w.asset for w in regular_workers if w.strategy.name.startswith("MT01")} + for asset in mtf_assets: + instrument = INSTRUMENT_MAP.get(asset, f"{asset}-PERPETUAL") + end = datetime.now(timezone.utc) + start = end - timedelta(days=lookback_days) + try: + candles_1h = client.get_historical( + instrument, start.strftime("%Y-%m-%d"), + end.strftime("%Y-%m-%d"), "60", + ) + if candles_1h: + df1h = pd.DataFrame(candles_1h) + df1h["timestamp"] = df1h["timestamp"].astype("int64") + htf_cache[asset] = df1h.sort_values("timestamp").reset_index(drop=True) + except Exception as e: + print(f" [1h fetch {asset}] ERRORE: {e}") + # Tick regular workers for w in regular_workers: key = (w.asset, w.tf) if key in candle_cache: try: - w.tick(candle_cache[key]) + w.tick(candle_cache[key], df_1h=htf_cache.get(w.asset)) except Exception as e: print(f" [{w.worker_id}] ERRORE: {e}") diff --git a/src/live/strategy_worker.py b/src/live/strategy_worker.py index 76fe022..07ab51e 100644 --- a/src/live/strategy_worker.py +++ b/src/live/strategy_worker.py @@ -175,8 +175,13 @@ class StrategyWorker: self.entry_time = "" self.bars_held = 0 - def tick(self, df: pd.DataFrame): - """Chiamato ad ogni poll con DataFrame OHLCV aggiornato.""" + def tick(self, df: pd.DataFrame, df_1h: pd.DataFrame | None = None): + """Chiamato ad ogni poll con DataFrame OHLCV aggiornato. + + df_1h: serie 1h live opzionale per strategie multi-timeframe (es. MT01), + passata ai generate_signals via params. Se None la strategia ricade sul + parquet statico. + """ if df.empty or len(df) < 100: return @@ -201,8 +206,11 @@ class StrategyWorker: return # Genera segnali + extra = dict(self.params) + if df_1h is not None: + extra["df_1h"] = df_1h signals = self.strategy.generate_signals( - df, ts, asset=self.asset, tf=self.tf, **self.params + df, ts, asset=self.asset, tf=self.tf, **extra ) if not signals: