3 Commits

Author SHA1 Message Date
Adriano Dal Pastro 8fd2c16cac 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>
2026-05-28 15:30:26 +00:00
Adriano 31be1b43aa docs: aggiorna README e CLAUDE.md con strategie MT01/PD01/CM01/AD01
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 09:50:58 +02:00
Adriano bdcef09057 chore: untrack paper_trades runtime data + report per anno/mercato
- data/paper_trades/ rimosso dal tracking (dati runtime, gitignored)
- scripts/analysis/yearly_market_report.py: accuracy/trades/PnL per anno×mercato

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 09:46:24 +02:00
20 changed files with 363 additions and 105 deletions
+1
View File
@@ -16,3 +16,4 @@ data/processed/
*.pt
*.pth
notebooks/.ipynb_checkpoints/
data/paper_trades/
+27 -10
View File
@@ -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
@@ -44,7 +44,8 @@ data/raw/ → file .parquet OHLCV (gitignored)
```bash
uv sync # installa dipendenze
uv run python -m src.data.downloader # scarica dati storici
uv run python scripts/strategies/SQ02_squeeze_antifake_vol.py # miglior strategia robusta
uv run python scripts/strategies/MT01_squeeze_mtf_momentum.py # miglior strategia (82.7% acc)
uv run python scripts/analysis/yearly_market_report.py # confronto per anno×mercato
uv run python scripts/strategies/ML01_squeeze_gbm.py # squeeze + ML (GBM)
uv run python -m src.live.multi_runner # paper trading live multi-strategia
docker compose up -d # deploy Docker
@@ -69,19 +70,32 @@ Token observer: nel file `secrets/observer.token` del progetto CerberoSuite.
Tutte le strategie estendono `src.strategies.base.Strategy` con interfaccia comune:
`generate_signals() → backtest() → report()`.
| Codice | Nome | Tipo | Accuracy | Note |
|--------|------|------|----------|------|
| SQ01 | Squeeze Base | Regole | 76.7% | Squeeze breakout puro, baseline |
| SQ02 | Antifake+Vol | Regole | **79.7%** | **Miglior robusto** — 9 anni, Sharpe 5.01 |
| SQ03 | Filtered | Regole | 79.2% | Filtri selezionabili (9 preset) |
| SQ04 | Ultimate | Regole | 81.6% | Max accuracy ma concentrato 2018 |
| ML01 | Squeeze+GBM | ML | 78.8% | Walk-forward, €8-12/day, DD basso |
Accuracy/DD su BTC 15m (9 anni 2018-2026, fee 0.2% RT, leva 3x, pos 15%):
| Codice | Nome | Tipo | Acc | DD | Trades | Note |
|--------|------|------|-----|----|--------|------|
| SQ01 | Squeeze Base | Regole | 76.7% | 6.7% | 4062 | Squeeze breakout puro, baseline |
| SQ02 | Antifake+Vol | Regole | 79.7% | 6.5% | 1250 | Squeeze + antifake + volume |
| SQ03 | Filtered | Regole | 79.2% | — | — | Filtri selezionabili (9 preset) |
| SQ04 | Ultimate | Regole | 81.6% | 4.3% | 376 | Concentrato 2018, poco robusto |
| ML01 | Squeeze+GBM | ML | 78.8% | 7.0% | 1964 | Walk-forward, €8-12/day |
| **MT01** | Squeeze+MTF | Regole | **82.7%** | 5.9% | 503 | **Max accuracy** — squeeze 15m + EMA trend 1h |
| **PD01** | Price-Vol Divergence | Regole | 80.6% | **2.7%** | 578 | Volume TREND al breakout, DD bassissimo |
| **CM01** | Cross-Market Momentum | Regole | 79.5% | 2.2%* | 611 | Squeeze + momentum cross BTC↔ETH (*DD su ETH) |
| **AD01** | Adaptive Squeeze | Regole | 79.9% | 9.9% | 1364 | Soglia squeeze adattiva per regime vol, max PnL |
Le strategie MT01/PD01/CM01/AD01 (branch strategy4) battono SQ02 e hanno DD inferiore
(eccetto AD01 su BTC). PD01 ed CM01 su ETH raggiungono DD 2.2-2.3%.
Report dettagliato per anno×mercato: `scripts/analysis/yearly_market_report.py`.
Per aggiungere una strategia:
1. Crea script in `scripts/strategies/` che estende `Strategy`
2. Aggiungi mapping in `src/live/strategy_loader.py``MODULE_MAP`
3. Aggiungi entry in `strategies.yml` per paper trading
Strategie scartate in `scripts/waste/` (W23-W28): inside bar, donchian, retest,
mean reversion RSI, volume spike, squeeze+MR — tutte sotto 65% acc o DD >14%.
## Multi-Strategy Paper Trader
Orchestratore che esegue N strategie in parallelo su dati live Cerbero, ognuna con €1000 USDC virtuali indipendenti.
@@ -89,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
@@ -105,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.
+35 -15
View File
@@ -8,18 +8,19 @@ Partendo da un capitale iniziale di €1.000, raggiungere un profitto medio di
## Risultati
Oltre 30 strategie testate su dati storici 20182026 (BTC e ETH, timeframe 15m / 1h). Le migliori:
Oltre 35 strategie testate su dati storici 20182026 (BTC e ETH, timeframe 15m / 1h, fee 0.2% round-trip, leva 3x, posizione 15%). Le migliori:
| Codice | Strategia | Accuracy | Trades | Max DD | €/giorno | Robustezza |
|--------|-----------|----------|--------|--------|----------|------------|
| SQ02 | Antifake+Vol BTC 15m | **79.7%** | 1250 | 6.5% | €5.23 | ✅ 9/9 anni |
| ML01 | Squeeze+GBM BTC 15m | 79.1% | 1929 | 5.5% | €8.45 | ✅ 5/5 anni |
| SQ02 | Antifake+Vol ETH 15m | 78.6% | 942 | 3.4% | €4.33 | 8/9 anni |
| SQ02 | Antifake+Vol BTC 1h | 78.0% | 473 | 3.5% | €3.85 | ✅ 9/9 anni |
| SQ01 | Squeeze Base ETH 15m | 76.4% | 2948 | 6.2% | €10.31 | 9/9 anni |
| ML01 | Squeeze+GBM ETH 15m | 76.7% | 1210 | 4.2% | €11.12 | 5/5 anni |
| Codice | Strategia | Mercato | Accuracy | Trades | Max DD | Robustezza |
|--------|-----------|---------|----------|--------|--------|------------|
| MT01 | Squeeze + MTF Momentum | BTC 15m | **82.7%** | 503 | 5.9% | ✅ 9/9 anni |
| MT01 | Squeeze + MTF Momentum | ETH 15m | 81.2% | 404 | 2.9% | ✅ 9/9 anni |
| PD01 | Price-Volume Divergence | ETH 15m | 80.9% | 465 | **2.3%** | ✅ 9/9 anni |
| PD01 | Price-Volume Divergence | BTC 15m | 80.6% | 578 | 2.7% | ✅ 9/9 anni |
| CM01 | Cross-Market Momentum | ETH 15m | 80.6% | 433 | **2.2%** | ✅ 9/9 anni |
| AD01 | Adaptive Squeeze | BTC 15m | 79.9% | 1364 | 9.9% | ✅ 9/9 anni |
| SQ02 | Antifake + Volume | BTC 15m | 79.7% | 1250 | 6.5% | ✅ 9/9 anni |
La strategia più robusta (SQ02 BTC 15m) mantiene accuracy ≥73% ogni anno dal 2018 con Sharpe 5.01.
La famiglia squeeze (compressione → espansione di volatilità) è il nucleo comune. Le migliori varianti aggiungono un filtro indipendente: conferma del trend su timeframe superiore (MT01), trend del volume al breakout (PD01), momentum dell'asset correlato (CM01), o soglia di squeeze adattiva al regime di volatilità (AD01). Tutte mantengono accuracy ≥67% in ogni singolo anno dal 2018, con i drawdown più contenuti (2-3%) su ETH.
## Come funziona
@@ -32,6 +33,15 @@ Il meccanismo centrale sfrutta i cicli naturali di compressione ed espansione de
3. **Filtri** — anti-fakeout (scarta breakout con retrace >60%) + volume confirmation (breakout >1.3× media).
4. **ML opzionale** — un modello GradientBoosting (GBM), addestrato walk-forward su feature strutturali, conferma la direzione e filtra i segnali deboli.
### Filtri di conferma indipendenti
Le varianti più recenti aggiungono un singolo filtro di conferma al breakout, ciascuno motivato da una dinamica di mercato reale (non da ottimizzazione dei parametri):
- **MT01 — Multi-timeframe momentum.** Entra solo se la EMA su timeframe orario conferma il trend nella direzione del breakout. Filtra i breakout controtendenza.
- **PD01 — Price-volume divergence.** Richiede che il volume sia in *crescita* al momento del breakout, non solo elevato: un breakout su volume calante è debole e viene scartato.
- **CM01 — Cross-market momentum.** Entra solo se l'asset correlato (BTC ↔ ETH) mostra momentum nella stessa direzione nelle ultime barre.
- **AD01 — Adaptive squeeze.** La soglia di squeeze si adatta al regime di volatilità: più selettiva nei mercati agitati, più permissiva in quelli calmi.
### Feature ML (44 dimensioni)
- Rapporti body/shadow normalizzati su finestre multiple (12, 24, 48 candele)
@@ -59,8 +69,8 @@ PythagorasGoal/
│ ├── signal_engine.py # Squeeze + ML real-time (per ML01)
│ └── telegram_notifier.py
├── scripts/
│ ├── strategies/ # Strategie attive (SQ01-SQ04, ML01)
│ ├── waste/ # Strategie scartate (W01-W22)
│ ├── strategies/ # Strategie attive (SQ01-SQ04, ML01, MT01, PD01, CM01, AD01)
│ ├── waste/ # Strategie scartate (W01-W28)
│ └── analysis/ # Script di confronto e report
├── strategies.yml # Config multi-strategy paper trader
├── data/
@@ -84,16 +94,26 @@ Tutte le strategie estendono `src.strategies.base.Strategy` con interfaccia comu
| SQ03 | `SQ03_squeeze_all_filters.py` | Regole | Squeeze + filtri selezionabili (9 preset) |
| SQ04 | `SQ04_squeeze_ultimate.py` | Regole | Combo incrementali con correlazione/trend |
| ML01 | `ML01_squeeze_gbm.py` | ML | Squeeze + GBM walk-forward |
| MT01 | `MT01_squeeze_mtf_momentum.py` | Regole | Squeeze 15m + conferma trend EMA 1h (max accuracy) |
| PD01 | `PD01_price_volume_divergence.py` | Regole | Squeeze + trend del volume al breakout (DD minimo) |
| CM01 | `CM01_cross_market_momentum.py` | Regole | Squeeze + momentum dell'asset correlato (BTC ↔ ETH) |
| AD01 | `AD01_adaptive_squeeze.py` | Regole | Squeeze con soglia adattiva al regime di volatilità |
Per eseguire il backtest di una strategia:
```bash
uv run python scripts/strategies/SQ02_squeeze_antifake_vol.py
uv run python scripts/strategies/MT01_squeeze_mtf_momentum.py
```
Per il confronto completo per anno e mercato:
```bash
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
@@ -148,7 +168,7 @@ uv sync
uv run python -m src.data.downloader
# Backtest strategia migliore
uv run python scripts/strategies/SQ02_squeeze_antifake_vol.py
uv run python scripts/strategies/MT01_squeeze_mtf_momentum.py
# Paper trading live
uv run python -m src.live.multi_runner
@@ -1,13 +0,0 @@
{
"capital": 1000,
"in_position": false,
"direction": 0,
"entry_price": 0,
"entry_time": "",
"bars_held": 0,
"total_trades": 0,
"total_wins": 0,
"started_at": "2026-05-27T21:16:02.087963+00:00",
"last_bar_ts": 0,
"last_update": "2026-05-27T21:16:04.705726+00:00"
}
@@ -1 +0,0 @@
{"ts": "2026-05-27T21:16:02.087975+00:00", "worker": "ML01_squeeze_gbm__ETH__15m", "event": "INIT", "capital": 1000, "strategy": "ML01_squeeze_gbm", "asset": "ETH", "tf": "15m"}
@@ -1,13 +0,0 @@
{
"capital": 1000,
"in_position": false,
"direction": 0,
"entry_price": 0,
"entry_time": "",
"bars_held": 0,
"total_trades": 0,
"total_wins": 0,
"started_at": "2026-05-27T21:16:02.087646+00:00",
"last_bar_ts": 0,
"last_update": "2026-05-27T21:16:04.584685+00:00"
}
@@ -1 +0,0 @@
{"ts": "2026-05-27T21:16:02.087660+00:00", "worker": "SQ01_squeeze_base__BTC__15m", "event": "INIT", "capital": 1000, "strategy": "SQ01_squeeze_base", "asset": "BTC", "tf": "15m"}
@@ -1,13 +0,0 @@
{
"capital": 1000,
"in_position": false,
"direction": 0,
"entry_price": 0,
"entry_time": "",
"bars_held": 0,
"total_trades": 0,
"total_wins": 0,
"started_at": "2026-05-27T21:16:02.087214+00:00",
"last_bar_ts": 0,
"last_update": "2026-05-27T21:16:04.339917+00:00"
}
@@ -1 +0,0 @@
{"ts": "2026-05-27T21:16:02.087241+00:00", "worker": "SQ02_antifake_vol__BTC__15m", "event": "INIT", "capital": 1000, "strategy": "SQ02_antifake_vol", "asset": "BTC", "tf": "15m"}
@@ -1,13 +0,0 @@
{
"capital": 1000,
"in_position": false,
"direction": 0,
"entry_price": 0,
"entry_time": "",
"bars_held": 0,
"total_trades": 0,
"total_wins": 0,
"started_at": "2026-05-27T21:16:02.087438+00:00",
"last_bar_ts": 0,
"last_update": "2026-05-27T21:16:04.463602+00:00"
}
@@ -1 +0,0 @@
{"ts": "2026-05-27T21:16:02.087448+00:00", "worker": "SQ02_antifake_vol__ETH__15m", "event": "INIT", "capital": 1000, "strategy": "SQ02_antifake_vol", "asset": "ETH", "tf": "15m"}
-8
View File
@@ -1,8 +0,0 @@
{
"in_position": false,
"direction": null,
"entry_price": 0,
"entry_time": null,
"bars_held": 0,
"last_update": "2026-05-27T07:40:09.196718+00:00"
}
@@ -1,2 +0,0 @@
{"timestamp": "2026-05-27T07:35:10.715321+00:00", "event": "TRAINING", "lookback_days": 365}
{"timestamp": "2026-05-27T07:35:11.967644+00:00", "event": "TRAINING_DONE", "samples": 90, "up_ratio": 48.888888888888886, "train_accuracy": 100.0}
@@ -1,3 +0,0 @@
{"timestamp": "2026-05-27T07:36:03.120802+00:00", "event": "STARTUP", "equity": 101459.276155, "testnet": true}
{"timestamp": "2026-05-27T07:36:03.121518+00:00", "event": "TRAINING", "lookback_days": 365}
{"timestamp": "2026-05-27T07:36:04.249123+00:00", "event": "TRAINING_DONE", "samples": 90, "up_ratio": 48.888888888888886, "train_accuracy": 100.0}
@@ -1,6 +0,0 @@
{"timestamp": "2026-05-27T08:04:41.544464+00:00", "event": "TRAINING", "lookback_days": 365, "instrument": "ETH-PERPETUAL"}
{"timestamp": "2026-05-27T08:04:42.704464+00:00", "event": "TRAINING_DONE", "samples": 90, "up_ratio": 48.888888888888886, "train_accuracy": 100.0}
{"timestamp": "2026-05-27T08:04:42.918237+00:00", "event": "OPENING", "side": "buy", "amount": 0.216, "price": 2083.75, "virtual_capital": 1000.0, "notional": 450.0, "signal": {"direction": "buy", "probability": 0.75, "squeeze_duration": 10}}
{"timestamp": "2026-05-27T08:04:43.143718+00:00", "event": "OPENED", "order_result": {"order": {"label": "pythagoras-squeeze", "price": 2292.25, "order_id": "USDC-209283595178", "user_id": 81070, "amount": 0.216, "instrument_name": "ETH_USDC-PERPETUAL", "direction": "buy", "time_in_force": "good_til_cancelled", "web": false, "api": true, "creation_timestamp": 1779869083116, "mmp": false, "replaced": false, "post_only": false, "reduce_only": false, "filled_amount": 0.216, "last_update_timestamp": 1779869083116, "average_price": 2083.9, "contracts": 216.0, "order_state": "filled", "order_type": "market", "is_liquidation": false, "risk_reducing": false}, "trades": [{"label": "pythagoras-squeeze", "timestamp": 1779869083116, "state": "filled", "price": 2083.9, "order_id": "USDC-209283595178", "user_id": 81070, "amount": 0.216, "instrument_name": "ETH_USDC-PERPETUAL", "direction": "buy", "index_price": 2083.37, "trade_seq": 6674514, "api": true, "mark_price": 2083.86, "matching_id": null, "tick_direction": 0, "profit_loss": 0.0, "mmp": false, "post_only": false, "reduce_only": false, "self_trade": false, "contracts": 216.0, "trade_id": "USDC-32731729", "fee_currency": "USDC", "order_type": "market", "fee": 0.2250612, "liquidity": "T", "risk_reducing": false}], "data_timestamp": "2026-05-27T08:04:43.126155+00:00"}}
{"timestamp": "2026-05-27T08:04:46.361078+00:00", "event": "CLOSING", "reason": "test", "entry_price": 2083.75, "exit_price": 2083.95, "size": 0.216, "trade_pnl": 0.04, "fee": 0.9, "net_pnl": -0.86, "pnl_pct": -0.086, "bars_held": 0, "capital_before": 1000.0}
{"timestamp": "2026-05-27T08:04:46.574322+00:00", "event": "CLOSED", "result": {"order_id": "USDC-209283608601", "state": "filled", "data_timestamp": "2026-05-27T08:04:46.555823+00:00"}, "net_pnl": -0.86, "pnl_pct": -0.086, "virtual_capital": 999.14}
+96
View File
@@ -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.
+169
View File
@@ -0,0 +1,169 @@
"""Report accuracy per ANNO × MERCATO delle strategie migliori.
Esegue ogni strategia vincente su BTC e ETH e produce tabella
accuracy/trades per ogni anno. Permette di vedere robustezza temporale
e differenze tra mercati.
"""
from __future__ import annotations
import sys
sys.path.insert(0, ".")
import importlib.util
from pathlib import Path
STRATEGIES_DIR = Path("scripts/strategies")
def load_class(module_file, class_name):
path = STRATEGIES_DIR / f"{module_file}.py"
spec = importlib.util.spec_from_file_location(module_file, path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return getattr(mod, class_name)
# (label, module, class, params, hold)
STRATEGIES = [
("SQ02 antifake+vol", "SQ02_squeeze_antifake_vol", "SqueezeAntifakeVol", {}, 3),
("MT01 ema20+vol", "MT01_squeeze_mtf_momentum", "SqueezeMTFMomentum",
{"ema_period": 20, "min_slope": 0.001, "vol_filter": True}, 3),
("PD01 vtb3 vm1.3", "PD01_price_volume_divergence", "PriceVolumeDivergence",
{}, 3),
("CM01 cb6+vol", "CM01_cross_market_momentum", "CrossMarketMomentum",
{"cross_bars": 6, "mom_min": 0.001, "use_vol": True}, 3),
("AD01 lt.65 ht.95", "AD01_adaptive_squeeze", "AdaptiveSqueeze",
{"low_thr": 0.65, "high_thr": 0.95, "use_vol": True}, 3),
]
ASSETS = ["BTC", "ETH"]
TF = "15m"
ALL_YEARS = list(range(2018, 2027))
def run():
results = {} # (label, asset) -> BacktestResult
for label, module, cls_name, params, hold in STRATEGIES:
try:
cls = load_class(module, cls_name)
except Exception as e:
print(f"SKIP {label}: {e}")
continue
strat = cls()
for asset in ASSETS:
try:
r = strat.backtest(asset, TF, hold=hold, **params)
if r:
results[(label, asset)] = r
except Exception as e:
print(f" errore {label} {asset}: {e}")
# ── Tabella ACCURACY per anno × mercato ──────────────────────────
print(f"\n{'=' * 140}")
print(f" ACCURACY PER ANNO × MERCATO — {TF} (fee 0.2% RT, leva 3x, pos 15%)")
print(f"{'=' * 140}")
header = f" {'Strategia':<22s} {'Mkt':>3s}"
for y in ALL_YEARS:
header += f" {y:>7d}"
header += f"{'TOT':>6s} {'DD%':>5s} {'Worst':>10s}"
print(header)
print(f" {'' * 136}")
for label, module, cls_name, params, hold in STRATEGIES:
for asset in ASSETS:
r = results.get((label, asset))
if not r:
continue
yd = {ys.year: ys for ys in r.yearly}
line = f" {label:<22s} {asset:>3s}"
for y in ALL_YEARS:
if y in yd:
line += f" {yd[y].accuracy:>5.0f}%↑" if yd[y].accuracy >= 80 else f" {yd[y].accuracy:>5.0f}% "
else:
line += f" {'':>7s}"
worst = r.worst_year
worst_str = f"{worst.year}({worst.accuracy:.0f}%)" if worst else "N/A"
line += f"{r.accuracy:>5.1f}% {r.max_dd:>4.1f}% {worst_str:>10s}"
print(line)
print(f" {'·' * 136}")
# ── Tabella TRADES per anno × mercato ────────────────────────────
print(f"\n{'=' * 140}")
print(f" NUMERO TRADES PER ANNO × MERCATO")
print(f"{'=' * 140}")
header = f" {'Strategia':<22s} {'Mkt':>3s}"
for y in ALL_YEARS:
header += f" {y:>7d}"
header += f"{'TOT':>6s} {'€/day':>6s}"
print(header)
print(f" {'' * 130}")
for label, module, cls_name, params, hold in STRATEGIES:
for asset in ASSETS:
r = results.get((label, asset))
if not r:
continue
yd = {ys.year: ys for ys in r.yearly}
line = f" {label:<22s} {asset:>3s}"
for y in ALL_YEARS:
if y in yd:
line += f" {yd[y].trades:>7d}"
else:
line += f" {'':>7s}"
line += f"{r.trades:>6d} {r.daily_pnl:>+6.2f}"
print(line)
print(f" {'·' * 130}")
# ── Tabella PnL per anno × mercato ──────────────────────────────
print(f"\n{'=' * 140}")
print(f" PnL € PER ANNO × MERCATO (su €1000, no compounding tra anni)")
print(f"{'=' * 140}")
header = f" {'Strategia':<22s} {'Mkt':>3s}"
for y in ALL_YEARS:
header += f" {y:>7d}"
header += f"{'TOT€':>8s}"
print(header)
print(f" {'' * 132}")
for label, module, cls_name, params, hold in STRATEGIES:
for asset in ASSETS:
r = results.get((label, asset))
if not r:
continue
yd = {ys.year: ys for ys in r.yearly}
line = f" {label:<22s} {asset:>3s}"
for y in ALL_YEARS:
if y in yd:
line += f" {yd[y].pnl:>+7.0f}"
else:
line += f" {'':>7s}"
line += f"{r.pnl:>+8.0f}"
print(line)
print(f" {'·' * 132}")
# ── Sintesi: media per anno (tutte le strategie) ────────────────
print(f"\n{'=' * 140}")
print(f" SINTESI — Accuracy media per anno (tutte le strategie, BTC+ETH)")
print(f"{'=' * 140}")
year_acc = {y: [] for y in ALL_YEARS}
for (label, asset), r in results.items():
for ys in r.yearly:
if ys.trades >= 10:
year_acc[ys.year].append(ys.accuracy)
line_y = f" {'Anno':<22s} "
line_a = f" {'Acc media':<22s} "
for y in ALL_YEARS:
accs = year_acc[y]
avg = sum(accs) / len(accs) if accs else 0
line_y += f" {y:>7d}"
line_a += f" {avg:>6.1f}%"
print(line_y)
print(line_a)
if __name__ == "__main__":
run()
@@ -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)
+21 -1
View File
@@ -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}")
+11 -3
View File
@@ -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: