fix(live): MT01 usa trend 1h live da Cerbero, non dal parquet statico
Il paper trader restava a zero trade: il feed Cerbero era fermo a mezzanotte (bug end_date lato cerbero-mcp, poi risolto) e MT01 leggeva il trend 1h da un parquet statico, di fatto congelandolo (gap ~15h sul bar corrente). Ora il runner fa fetch 1h live per le strategie MTF e lo passa a generate_signals via il parametro df_1h (fallback al parquet se assente). Aggiornati CLAUDE.md, README e diario 2026-05-28. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -57,6 +57,8 @@ class SqueezeMTFMomentum(Strategy):
|
||||
kcr = keltner_ratio(c, h, l, 14)
|
||||
events = detect_squeezes(c, h, l, kcr, sq_thr)
|
||||
|
||||
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
|
||||
|
||||
@@ -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}")
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user