feat(live): worker con exit TP/SL/max_bars per MR01 + doc aggiornata
StrategyWorker ora supporta exit guidati dalla strategia via Signal.metadata (take-profit alla media / stop-loss ad ATR / time-limit), con fallback al vecchio hold_bars/stop -2% per strategie senza metadata. Usa fee_rt della strategia (MR01 = 0.10% RT reale Deribit, non piu' 0.20% hardcoded). Persistenza di tp/sl/max_bars in status.json per resume. Re-validato col worker reale (replay finestre mobili 1h, fee 0.10%): BTC 1h MR01: +196% OOS, ETH 1h: +251% OOS (nov 2023->mag 2026) — coerente col backtest. README + CLAUDE.md riscritti: squeeze = artefatto di look-ahead -> waste, MR01 mean-reversion unica attiva, metodologia anti-look-ahead e fee reali 0.10% RT. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,15 +24,17 @@ src/strategies/ → classe base Strategy ABC + indicatori condivisi
|
|||||||
base.py → Strategy, Signal, BacktestResult, YearlyStats
|
base.py → Strategy, Signal, BacktestResult, YearlyStats
|
||||||
indicators.py → keltner_ratio, detect_squeezes, ema, atr, rv, correlation
|
indicators.py → keltner_ratio, detect_squeezes, ema, atr, rv, correlation
|
||||||
src/live/ → paper trading live multi-strategia
|
src/live/ → paper trading live multi-strategia
|
||||||
multi_runner.py → orchestratore: carica YAML, fetch candele (15m + 1h live per MTF), tick worker
|
multi_runner.py → orchestratore: carica YAML, fetch candele, tick worker
|
||||||
strategy_worker.py → worker indipendente: capital, trade log, stato persistente
|
strategy_worker.py → worker indipendente: 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.
|
||||||
strategy_loader.py → import dinamico classi Strategy da scripts/strategies/
|
strategy_loader.py → import dinamico classi Strategy da scripts/strategies/
|
||||||
cerbero_client.py → client HTTP per Cerbero MCP (Deribit testnet)
|
cerbero_client.py → client HTTP per Cerbero MCP (Deribit testnet)
|
||||||
signal_engine.py → squeeze + ML real-time (per ML01)
|
signal_engine.py → squeeze + ML real-time (legacy ML01, ora in waste) + validazione OOS
|
||||||
telegram_notifier.py → notifiche Telegram per trade
|
telegram_notifier.py → notifiche Telegram per trade
|
||||||
scripts/strategies/ → strategie attive (SQ01-SQ04, ML01, MT01, PD01, CM01, AD01)
|
scripts/strategies/ → strategie con edge validato OOS (solo MR01_bollinger_fade)
|
||||||
scripts/waste/ → strategie scartate (W01-W22 + REF originali)
|
scripts/waste/ → strategie scartate (W01-W28 + famiglia squeeze SQ/MT/ML/AD/CM/PD)
|
||||||
scripts/analysis/ → script di confronto e report
|
scripts/analysis/ → ricerca/validazione OOS fee-aware (strategy_research, oos_validation, ...)
|
||||||
strategies.yml → config multi-strategy paper trader
|
strategies.yml → config multi-strategy paper trader
|
||||||
docs/diary/ → diario di ricerca giornaliero
|
docs/diary/ → diario di ricerca giornaliero
|
||||||
docs/specs/ → specifiche di design
|
docs/specs/ → specifiche di design
|
||||||
@@ -44,9 +46,10 @@ data/raw/ → file .parquet OHLCV (gitignored)
|
|||||||
```bash
|
```bash
|
||||||
uv sync # installa dipendenze
|
uv sync # installa dipendenze
|
||||||
uv run python -m src.data.downloader # scarica dati storici
|
uv run python -m src.data.downloader # scarica dati storici
|
||||||
uv run python scripts/strategies/MT01_squeeze_mtf_momentum.py # miglior strategia (82.7% acc)
|
uv run python scripts/strategies/MR01_bollinger_fade.py # strategia attiva (mean-reversion)
|
||||||
uv run python scripts/analysis/yearly_market_report.py # confronto per anno×mercato
|
uv run python scripts/analysis/strategy_research.py # ricerca strategie fee-aware OOS
|
||||||
uv run python scripts/strategies/ML01_squeeze_gbm.py # squeeze + ML (GBM)
|
uv run python scripts/analysis/oos_validation.py # perche' la famiglia squeeze e' scartata
|
||||||
|
uv run python scripts/analysis/validate_worker_mr01.py # replay worker reale su MR01
|
||||||
uv run python -m src.live.multi_runner # paper trading live multi-strategia
|
uv run python -m src.live.multi_runner # paper trading live multi-strategia
|
||||||
docker compose up -d # deploy Docker
|
docker compose up -d # deploy Docker
|
||||||
uv run pytest # test
|
uv run pytest # test
|
||||||
@@ -67,49 +70,51 @@ Token observer: nel file `secrets/observer.token` del progetto CerberoSuite.
|
|||||||
|
|
||||||
## Strategie attive
|
## Strategie attive
|
||||||
|
|
||||||
Tutte le strategie estendono `src.strategies.base.Strategy` con interfaccia comune:
|
> **LEZIONE CRITICA (2026-05-28).** L'intera famiglia squeeze-breakout (SQ01-04,
|
||||||
`generate_signals() → backtest() → report()`.
|
> 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`.
|
||||||
|
|
||||||
Accuracy/DD su BTC 15m (9 anni 2018-2026, fee 0.2% RT, leva 3x, pos 15%):
|
Tutte le strategie estendono `src.strategies.base.Strategy`
|
||||||
|
(`generate_signals() → backtest()`). **Unica strategia con edge netto validato:**
|
||||||
|
|
||||||
| Codice | Nome | Tipo | Acc | DD | Trades | Note |
|
| Codice | Nome | Tipo | Edge OOS netto | DD | Note |
|
||||||
|--------|------|------|-----|----|--------|------|
|
|--------|------|------|----------------|----|------|
|
||||||
| SQ01 | Squeeze Base | Regole | 76.7% | 6.7% | 4062 | Squeeze breakout puro, baseline |
|
| **MR01** | Bollinger Fade | Mean-reversion | **BTC 1h n50 k2.5: +201% / +196% (worker)** | 15% | Fada la banda, TP alla media, SL ad ATR |
|
||||||
| 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
|
MR01 è robusto su **tutta** la griglia parametri (`n∈{14,20,30,50}` × `k∈{2.0,2.5,3.0}`,
|
||||||
(eccetto AD01 su BTC). PD01 ed CM01 su ETH raggiungono DD 2.2-2.3%.
|
entrambi gli asset → tutte positive OOS) e su **tutte** le fee 0.00-0.20% RT.
|
||||||
Report dettagliato per anno×mercato: `scripts/analysis/yearly_market_report.py`.
|
Validato col worker reale: BTC +196% / ETH +251% OOS (nov 2023→mag 2026).
|
||||||
|
Ricerca completa: `scripts/analysis/strategy_research.py`.
|
||||||
|
|
||||||
Per aggiungere una strategia:
|
**Metodologia obbligatoria per ogni nuova strategia** (per non ripetere l'errore squeeze):
|
||||||
1. Crea script in `scripts/strategies/` che estende `Strategy`
|
1. Ingresso eseguibile: direzione e prezzo decisi con dati **fino a `close[i]`**, mai `close[i-1]` con direzione da `i`.
|
||||||
2. Aggiungi mapping in `src/live/strategy_loader.py` → `MODULE_MAP`
|
2. Backtest **NETTO** dopo fee realistiche Deribit (**0.10% RT** taker, non 0.20%) + leva.
|
||||||
3. Aggiungi entry in `strategies.yml` per paper trading
|
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 in `scripts/waste/` (W23-W28): inside bar, donchian, retest,
|
Strategie scartate storiche in `scripts/waste/` (W01-W28 + la famiglia squeeze).
|
||||||
mean reversion RSI, volume spike, squeeze+MR — tutte sotto 65% acc o DD >14%.
|
|
||||||
|
|
||||||
## Multi-Strategy Paper Trader
|
## Multi-Strategy Paper Trader
|
||||||
|
|
||||||
Orchestratore che esegue N strategie in parallelo su dati live Cerbero, ognuna con €1000 USDC virtuali indipendenti.
|
Orchestratore che esegue N strategie in parallelo su dati live Cerbero, ognuna con €1000 USDC virtuali indipendenti.
|
||||||
|
|
||||||
**Config:** `strategies.yml` — lista strategie con asset, tf, sizing, parametri.
|
**Config:** `strategies.yml` — lista strategie con asset, tf, sizing, parametri. Attualmente solo MR01 (BTC/ETH 1h).
|
||||||
**Persistenza:** `data/paper_trades/{strategy}___{asset}__{tf}/` con `trades.jsonl` (append-only) + `status.json` (resume al restart).
|
**Persistenza:** `data/paper_trades/{strategy}___{asset}__{tf}/` con `trades.jsonl` (append-only) + `status.json` (resume al restart, include tp/sl/max_bars).
|
||||||
**Hot-add:** aggiungi riga YAML → `docker compose restart` → storico intatto.
|
**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.
|
**Exit strategia:** se un `Signal` porta `tp`/`sl`/`max_bars` in `metadata` (come MR01), il worker esce su take-profit/stop-loss/time-limit a quei livelli; altrimenti usa il fallback `hold_bars`/stop -2%.
|
||||||
**Notifiche:** Telegram per ogni trade (richiede `.env` con `TELEGRAM_BOT_TOKEN` e `TELEGRAM_CHAT_ID`).
|
**Notifiche:** Telegram per ogni trade (richiede `.env` con `TELEGRAM_BOT_TOKEN` e `TELEGRAM_CHAT_ID`).
|
||||||
|
|
||||||
## Convenzioni
|
## Convenzioni
|
||||||
|
|
||||||
- Strategie in `scripts/strategies/` con codice univoco (SQ01, ML01, ...).
|
- Strategie in `scripts/strategies/` con codice univoco (MR01, ...).
|
||||||
- Script scartati in `scripts/waste/` con prefisso W01-W22.
|
- Script scartati in `scripts/waste/` (W01-W28 + famiglia squeeze).
|
||||||
- Diario in `docs/diary/YYYY-MM-DD.md`. Aggiornare dopo ogni esperimento significativo.
|
- Diario in `docs/diary/YYYY-MM-DD.md`. Aggiornare dopo ogni esperimento significativo.
|
||||||
- Nessun dato sensibile nei commit (token, chiavi API). Usare `.gitignore`.
|
- 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]`.
|
- 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]`.
|
||||||
@@ -117,7 +122,7 @@ Orchestratore che esegue N strategie in parallelo su dati live Cerbero, ognuna c
|
|||||||
## Attenzione
|
## Attenzione
|
||||||
|
|
||||||
- **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]`.
|
- **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:** sempre 0.1% per lato (0.2% round-trip). Includere nel backtest.
|
- **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.
|
- **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.
|
- **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.
|
- **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.
|
||||||
|
|||||||
@@ -8,47 +8,56 @@ Partendo da un capitale iniziale di €1.000, raggiungere un profitto medio di
|
|||||||
|
|
||||||
## Risultati
|
## Risultati
|
||||||
|
|
||||||
Oltre 35 strategie testate su dati storici 2018–2026 (BTC e ETH, timeframe 15m / 1h, fee 0.2% round-trip, leva 3x, posizione 15%). Le migliori:
|
> ⚠️ **Revisione 2026-05-28.** La famiglia squeeze-breakout (SQ/MT/ML/AD/CM/PD, con
|
||||||
|
> accuracy storiche dichiarate 76-82%) è stata **scartata**: quei numeri erano un
|
||||||
|
> **artefatto di look-ahead**. I backtest decidevano la direzione dalla candela di
|
||||||
|
> breakout `close[i]` ma entravano a `close[i-1]` — impossibile dal vivo. Sotto
|
||||||
|
> ingresso onesto (`close[i]`) e fee reali, l'edge sparisce e tutte perdono, anche
|
||||||
|
> a fee zero. Dettagli e prove: `scripts/analysis/oos_validation.py`.
|
||||||
|
|
||||||
| Codice | Strategia | Mercato | Accuracy | Trades | Max DD | Robustezza |
|
Dopo una validazione **out-of-sample, fee-aware** di tutte le famiglie, l'unica con
|
||||||
|--------|-----------|---------|----------|--------|--------|------------|
|
edge netto reale è il **mean-reversion** (i breakout *rientrano*, non continuano):
|
||||||
| 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 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.
|
| Codice | Strategia | Mercato | Edge OOS netto | Max DD | Robustezza |
|
||||||
|
|--------|-----------|---------|----------------|--------|------------|
|
||||||
|
| **MR01** | Bollinger Fade (mean-reversion) | BTC 1h | **+196 / +201%** | 15% | ✅ |
|
||||||
|
| **MR01** | Bollinger Fade (mean-reversion) | ETH 1h | **+251%** | ~25% | ⚠️ DD alto |
|
||||||
|
|
||||||
|
Netto dopo **fee realistiche Deribit 0.10% RT** (taker), leva 3x, pos 15%, su finestra
|
||||||
|
held-out (nov 2023→mag 2026). MR01 è positivo su **tutta** la griglia parametri
|
||||||
|
(`n∈{14,20,30,50}` × `k∈{2.0,2.5,3.0}`) e per **ogni** livello di fee 0.00-0.20% RT —
|
||||||
|
margine di sicurezza ampio, niente parametro fortunato. Ri-validato col worker live reale.
|
||||||
|
|
||||||
## Come funziona
|
## Come funziona
|
||||||
|
|
||||||
### Volatility Squeeze Breakout
|
### MR01 — Bollinger Fade (mean-reversion)
|
||||||
|
|
||||||
Il meccanismo centrale sfrutta i cicli naturali di compressione ed espansione della volatilità:
|
La strategia attiva sfrutta il fatto, emerso dai dati, che su BTC/ETH a 1h gli estremi
|
||||||
|
di prezzo **rientrano verso la media** più di quanto proseguano:
|
||||||
|
|
||||||
1. **Compressione** — le Bollinger Bands entrano dentro i Keltner Channel (il prezzo si muove sempre meno, accumulando "energia").
|
1. **Bollinger Bands** (window `n`, `k` deviazioni standard) sul close.
|
||||||
2. **Breakout** — le bande escono dal canale. Un impulso direzionale parte.
|
2. **Entry** — quando il close esce *sotto* la banda inferiore → **long** (o *sopra* la superiore → **short**). Ingresso a `close[i]`, eseguibile dal vivo.
|
||||||
3. **Filtri** — anti-fakeout (scarta breakout con retrace >60%) + volume confirmation (breakout >1.3× media).
|
3. **Take-profit** alla media mobile (il rientro atteso).
|
||||||
4. **ML opzionale** — un modello GradientBoosting (GBM), addestrato walk-forward su feature strutturali, conferma la direzione e filtra i segnali deboli.
|
4. **Stop-loss** a `sl_atr × ATR` oltre l'estremo; **time-limit** a `max_bars`.
|
||||||
|
|
||||||
### Filtri di conferma indipendenti
|
Nessun look-ahead: direzione e livelli sono calcolati con dati fino a `close[i]`.
|
||||||
|
|
||||||
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):
|
### Perché lo squeeze breakout è stato abbandonato
|
||||||
|
|
||||||
- **MT01 — Multi-timeframe momentum.** Entra solo se la EMA su timeframe orario conferma il trend nella direzione del breakout. Filtra i breakout controtendenza.
|
L'ipotesi originale era opposta — *continuazione* dopo la compressione di volatilità
|
||||||
- **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.
|
(Bollinger dentro Keltner → breakout direzionale). Su dati storici sembrava dare
|
||||||
- **CM01 — Cross-market momentum.** Entra solo se l'asset correlato (BTC ↔ ETH) mostra momentum nella stessa direzione nelle ultime barre.
|
76-82% di accuracy, ma era un **artefatto di look-ahead**: il backtest entrava a
|
||||||
- **AD01 — Adaptive squeeze.** La soglia di squeeze si adatta al regime di volatilità: più selettiva nei mercati agitati, più permissiva in quelli calmi.
|
`close[i-1]` con direzione decisa da `close[i]`. Replicando l'esecuzione reale
|
||||||
|
(ingresso a `close[i]`) l'edge collassa al ~47% (lancio di moneta) e i costi fanno
|
||||||
|
il resto. Il test sui breakout intra-barra a 5m conferma che il movimento *rientra*
|
||||||
|
subito (mean-reversion), giustificando MR01. Tutta la famiglia squeeze è in `scripts/waste/`.
|
||||||
|
|
||||||
### Feature ML (44 dimensioni)
|
### Lezione metodologica
|
||||||
|
|
||||||
- Rapporti body/shadow normalizzati su finestre multiple (12, 24, 48 candele)
|
Ogni nuova strategia deve passare: (1) **ingresso eseguibile** senza look-ahead,
|
||||||
- Momentum, volatilità, skewness, kurtosis dei rendimenti logaritmici
|
(2) backtest **netto** dopo fee realistiche (0.10% RT Deribit), (3) validazione
|
||||||
- Autocorrelazione lag-1 e profilo volumetrico
|
**out-of-sample** + robustezza su griglia parametri + sweep fee. Strumenti in
|
||||||
- Durata della fase di squeeze e rapporto di espansione Keltner
|
`scripts/analysis/` (`strategy_research.py`, `oos_validation.py`, `intrabar_test.py`).
|
||||||
- Posizione del prezzo rispetto al range recente e ATR normalizzato
|
|
||||||
|
|
||||||
## Struttura progetto
|
## Struttura progetto
|
||||||
|
|
||||||
@@ -66,12 +75,12 @@ PythagorasGoal/
|
|||||||
│ ├── strategy_worker.py # Worker indipendente con stato persistente
|
│ ├── strategy_worker.py # Worker indipendente con stato persistente
|
||||||
│ ├── strategy_loader.py # Import dinamico classi Strategy
|
│ ├── strategy_loader.py # Import dinamico classi Strategy
|
||||||
│ ├── cerbero_client.py # Client HTTP per Cerbero MCP
|
│ ├── cerbero_client.py # Client HTTP per Cerbero MCP
|
||||||
│ ├── signal_engine.py # Squeeze + ML real-time (per ML01)
|
│ ├── signal_engine.py # Squeeze + ML real-time (legacy) + validazione OOS
|
||||||
│ └── telegram_notifier.py
|
│ └── telegram_notifier.py
|
||||||
├── scripts/
|
├── scripts/
|
||||||
│ ├── strategies/ # Strategie attive (SQ01-SQ04, ML01, MT01, PD01, CM01, AD01)
|
│ ├── strategies/ # Strategie con edge validato OOS (solo MR01_bollinger_fade)
|
||||||
│ ├── waste/ # Strategie scartate (W01-W28)
|
│ ├── waste/ # Strategie scartate (W01-W28 + famiglia squeeze SQ/MT/ML/AD/CM/PD)
|
||||||
│ └── analysis/ # Script di confronto e report
|
│ └── analysis/ # Ricerca/validazione OOS fee-aware (strategy_research, oos_validation, ...)
|
||||||
├── strategies.yml # Config multi-strategy paper trader
|
├── strategies.yml # Config multi-strategy paper trader
|
||||||
├── data/
|
├── data/
|
||||||
│ └── raw/ # Parquet OHLCV (gitignored, ~70 MB)
|
│ └── raw/ # Parquet OHLCV (gitignored, ~70 MB)
|
||||||
@@ -85,35 +94,32 @@ PythagorasGoal/
|
|||||||
|
|
||||||
## Strategie attive
|
## Strategie attive
|
||||||
|
|
||||||
Tutte le strategie estendono `src.strategies.base.Strategy` con interfaccia comune: `generate_signals() → backtest() → report()`.
|
Tutte le strategie estendono `src.strategies.base.Strategy` (`generate_signals() → backtest()`).
|
||||||
|
|
||||||
| Codice | Script | Tipo | Descrizione |
|
| Codice | Script | Tipo | Descrizione |
|
||||||
|--------|--------|------|-------------|
|
|--------|--------|------|-------------|
|
||||||
| SQ01 | `SQ01_squeeze_base.py` | Regole | Squeeze breakout puro |
|
| **MR01** | `MR01_bollinger_fade.py` | Mean-reversion | Fada la banda di Bollinger, TP alla media, SL ad ATR. Unica con edge netto validato OOS. |
|
||||||
| SQ02 | `SQ02_squeeze_antifake_vol.py` | Regole | Squeeze + antifakeout + volume confirm |
|
|
||||||
| 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:
|
La famiglia squeeze (SQ01-04, ML01, MT01, PD01, CM01, AD01) è in `scripts/waste/`:
|
||||||
|
edge storico = artefatto di look-ahead (vedi sezione *Come funziona*).
|
||||||
|
|
||||||
|
Per eseguire il backtest della strategia:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv run python scripts/strategies/MT01_squeeze_mtf_momentum.py
|
uv run python scripts/strategies/MR01_bollinger_fade.py
|
||||||
```
|
```
|
||||||
|
|
||||||
Per il confronto completo per anno e mercato:
|
Per la ricerca/validazione fee-aware out-of-sample:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv run python scripts/analysis/yearly_market_report.py
|
uv run python scripts/analysis/strategy_research.py # screening famiglie + deep-dive MR01
|
||||||
|
uv run python scripts/analysis/oos_validation.py # perche' la famiglia squeeze e' scartata
|
||||||
|
uv run python scripts/analysis/validate_worker_mr01.py # replay del worker live su MR01
|
||||||
```
|
```
|
||||||
|
|
||||||
## Paper Trading Live
|
## Paper Trading Live
|
||||||
|
|
||||||
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.
|
Il multi-strategy runner esegue N strategie in parallelo su dati live da Cerbero MCP, ognuna con €1000 USDC virtuali indipendenti. Se un `Signal` porta `tp`/`sl`/`max_bars` in `metadata` (come MR01), il worker chiude su take-profit alla media / stop-loss ad ATR / time-limit; altrimenti usa il fallback `hold_bars`/stop -2%.
|
||||||
|
|
||||||
### Avvio
|
### Avvio
|
||||||
|
|
||||||
@@ -136,10 +142,15 @@ defaults:
|
|||||||
leverage: 3
|
leverage: 3
|
||||||
|
|
||||||
strategies:
|
strategies:
|
||||||
- name: SQ02_antifake_vol
|
- name: MR01_bollinger_fade
|
||||||
asset: BTC
|
asset: BTC
|
||||||
tf: 15m
|
tf: 1h
|
||||||
enabled: true
|
enabled: true
|
||||||
|
params:
|
||||||
|
bb_window: 50
|
||||||
|
k: 2.5
|
||||||
|
sl_atr: 2.0
|
||||||
|
max_bars: 24
|
||||||
```
|
```
|
||||||
|
|
||||||
Per aggiungere una strategia: nuova riga in `strategies.yml`, poi `docker compose restart`. Lo storico delle strategie esistenti rimane intatto.
|
Per aggiungere una strategia: nuova riga in `strategies.yml`, poi `docker compose restart`. Lo storico delle strategie esistenti rimane intatto.
|
||||||
@@ -150,9 +161,9 @@ Ogni strategia ha la sua directory in `data/paper_trades/`:
|
|||||||
|
|
||||||
```
|
```
|
||||||
data/paper_trades/
|
data/paper_trades/
|
||||||
SQ02_antifake_vol__BTC__15m/
|
MR01_bollinger_fade__BTC__1h/
|
||||||
trades.jsonl # Storico trade append-only
|
trades.jsonl # Storico trade append-only
|
||||||
status.json # Stato corrente (resume al restart)
|
status.json # Stato corrente (resume al restart, include tp/sl/max_bars)
|
||||||
```
|
```
|
||||||
|
|
||||||
Notifiche Telegram per ogni trade (richiede `TELEGRAM_BOT_TOKEN` e `TELEGRAM_CHAT_ID` in `.env`).
|
Notifiche Telegram per ogni trade (richiede `TELEGRAM_BOT_TOKEN` e `TELEGRAM_CHAT_ID` in `.env`).
|
||||||
@@ -167,8 +178,8 @@ uv sync
|
|||||||
# Scarica dati storici (~70 MB)
|
# Scarica dati storici (~70 MB)
|
||||||
uv run python -m src.data.downloader
|
uv run python -m src.data.downloader
|
||||||
|
|
||||||
# Backtest strategia migliore
|
# Backtest strategia attiva
|
||||||
uv run python scripts/strategies/MT01_squeeze_mtf_momentum.py
|
uv run python scripts/strategies/MR01_bollinger_fade.py
|
||||||
|
|
||||||
# Paper trading live
|
# Paper trading live
|
||||||
uv run python -m src.live.multi_runner
|
uv run python -m src.live.multi_runner
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
"""Re-validazione: il StrategyWorker REALE tradi MR01 con edge netto?
|
||||||
|
|
||||||
|
Guida il worker vero (generate_signals + nuova logica exit TP/SL/max_bars) su
|
||||||
|
finestre mobili di dati 1h storici, simulando il polling live. Conferma che
|
||||||
|
sulla finestra OOS l'edge netto (dopo fee 0.10% RT) sopravvive alla meccanica
|
||||||
|
del worker (exit su prezzo corrente, piu' conservativa del backtest high/low).
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
|
||||||
|
from src.data.downloader import load_data
|
||||||
|
from src.live.strategy_loader import load_strategy
|
||||||
|
from src.live.strategy_worker import StrategyWorker
|
||||||
|
|
||||||
|
OOS_FRAC = 0.30
|
||||||
|
WIN = 250 # barre per finestra di poll (warmup bb_window=50 + ATR)
|
||||||
|
|
||||||
|
|
||||||
|
def replay(asset: str, params: dict):
|
||||||
|
df = load_data(asset, "1h").reset_index(drop=True)
|
||||||
|
n = len(df)
|
||||||
|
split = int(n * (1 - OOS_FRAC))
|
||||||
|
strat = load_strategy("MR01_bollinger_fade")
|
||||||
|
w = StrategyWorker(strat, asset, "1h", capital=1000.0, position_size=0.15,
|
||||||
|
leverage=3.0, hold_bars=3, params=params,
|
||||||
|
data_dir=Path(f"/tmp/replay_{asset}"))
|
||||||
|
w._notify = lambda *a, **k: None
|
||||||
|
# stato pulito
|
||||||
|
for attr, val in dict(capital=1000.0, in_position=False, direction=0, entry_price=0,
|
||||||
|
bars_held=0, total_trades=0, total_wins=0, last_bar_ts=0,
|
||||||
|
tp=0.0, sl=0.0, max_bars=0).items():
|
||||||
|
setattr(w, attr, val)
|
||||||
|
|
||||||
|
start = max(split, WIN)
|
||||||
|
with contextlib.redirect_stdout(open(os.devnull, "w")):
|
||||||
|
for j in range(start, n):
|
||||||
|
w.tick(df.iloc[j - WIN + 1 : j + 1])
|
||||||
|
|
||||||
|
ret = (w.capital / 1000 - 1) * 100
|
||||||
|
acc = w.total_wins / w.total_trades * 100 if w.total_trades else 0.0
|
||||||
|
import pandas as pd
|
||||||
|
period = (f"{pd.to_datetime(df['timestamp'].iloc[start], unit='ms', utc=True).date()}"
|
||||||
|
f"->{pd.to_datetime(df['timestamp'].iloc[-1], unit='ms', utc=True).date()}")
|
||||||
|
return w.total_trades, acc, ret, w.capital, period
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("=" * 90)
|
||||||
|
print(" RE-VALIDAZIONE WORKER REALE su MR01 (OOS, fee 0.10% RT, leva 3x) — finestra poll 250b")
|
||||||
|
print("=" * 90)
|
||||||
|
params = dict(bb_window=50, k=2.5, sl_atr=2.0, max_bars=24)
|
||||||
|
print(f" {'Asset':>6s}{'Periodo OOS':>26s}{'Trade':>7s}{'Win%':>7s}{'Ret%':>9s}{'Cap€':>9s}")
|
||||||
|
print(" " + "-" * 80)
|
||||||
|
for asset in ["BTC", "ETH"]:
|
||||||
|
t, acc, ret, cap, period = replay(asset, params)
|
||||||
|
print(f" {asset:>6s}{period:>26s}{t:>7d}{acc:>7.1f}{ret:>+9.1f}{cap:>9.0f}")
|
||||||
|
print(" " + "-" * 80)
|
||||||
|
print(" Atteso: Ret% positivo (l'edge mean-reversion sopravvive alla meccanica del worker).")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -55,6 +55,13 @@ class StrategyWorker:
|
|||||||
self.started_at = datetime.now(timezone.utc).isoformat()
|
self.started_at = datetime.now(timezone.utc).isoformat()
|
||||||
self.last_bar_ts: int = 0
|
self.last_bar_ts: int = 0
|
||||||
|
|
||||||
|
# Exit guidati dalla strategia via Signal.metadata (0 = usa hold_bars/stop legacy)
|
||||||
|
self.tp: float = 0.0
|
||||||
|
self.sl: float = 0.0
|
||||||
|
self.max_bars: int = 0
|
||||||
|
# Fee dalla strategia (MR01 = 0.001 realistico Deribit), fallback al default modulo
|
||||||
|
self.fee_rt: float = float(getattr(strategy, "fee_rt", FEE_RT))
|
||||||
|
|
||||||
self._load_state()
|
self._load_state()
|
||||||
self._save_state()
|
self._save_state()
|
||||||
|
|
||||||
@@ -78,6 +85,9 @@ class StrategyWorker:
|
|||||||
self.total_wins = state.get("total_wins", 0)
|
self.total_wins = state.get("total_wins", 0)
|
||||||
self.started_at = state.get("started_at", self.started_at)
|
self.started_at = state.get("started_at", self.started_at)
|
||||||
self.last_bar_ts = state.get("last_bar_ts", 0)
|
self.last_bar_ts = state.get("last_bar_ts", 0)
|
||||||
|
self.tp = state.get("tp", 0.0)
|
||||||
|
self.sl = state.get("sl", 0.0)
|
||||||
|
self.max_bars = state.get("max_bars", 0)
|
||||||
|
|
||||||
self._log("RESUME", {"capital": round(self.capital, 2),
|
self._log("RESUME", {"capital": round(self.capital, 2),
|
||||||
"total_trades": self.total_trades,
|
"total_trades": self.total_trades,
|
||||||
@@ -95,6 +105,9 @@ class StrategyWorker:
|
|||||||
"total_wins": self.total_wins,
|
"total_wins": self.total_wins,
|
||||||
"started_at": self.started_at,
|
"started_at": self.started_at,
|
||||||
"last_bar_ts": self.last_bar_ts,
|
"last_bar_ts": self.last_bar_ts,
|
||||||
|
"tp": self.tp,
|
||||||
|
"sl": self.sl,
|
||||||
|
"max_bars": self.max_bars,
|
||||||
"last_update": datetime.now(timezone.utc).isoformat(),
|
"last_update": datetime.now(timezone.utc).isoformat(),
|
||||||
}
|
}
|
||||||
with open(self.status_path, "w") as f:
|
with open(self.status_path, "w") as f:
|
||||||
@@ -125,12 +138,19 @@ class StrategyWorker:
|
|||||||
self.entry_time = datetime.now(timezone.utc).isoformat()
|
self.entry_time = datetime.now(timezone.utc).isoformat()
|
||||||
self.bars_held = 0
|
self.bars_held = 0
|
||||||
|
|
||||||
|
meta = signal.metadata or {}
|
||||||
|
self.tp = float(meta.get("tp", 0.0) or 0.0)
|
||||||
|
self.sl = float(meta.get("sl", 0.0) or 0.0)
|
||||||
|
self.max_bars = int(meta.get("max_bars", 0) or 0)
|
||||||
|
|
||||||
trade_data = {
|
trade_data = {
|
||||||
"direction": "long" if signal.direction == 1 else "short",
|
"direction": "long" if signal.direction == 1 else "short",
|
||||||
"price": round(current_price, 2),
|
"price": round(current_price, 2),
|
||||||
"size": round(size, 6),
|
"size": round(size, 6),
|
||||||
"notional": round(notional, 2),
|
"notional": round(notional, 2),
|
||||||
"capital": round(self.capital, 2),
|
"capital": round(self.capital, 2),
|
||||||
|
"tp": round(self.tp, 2) if self.tp else None,
|
||||||
|
"sl": round(self.sl, 2) if self.sl else None,
|
||||||
}
|
}
|
||||||
self._log("OPEN", trade_data)
|
self._log("OPEN", trade_data)
|
||||||
self._notify("OPENED", trade_data)
|
self._notify("OPENED", trade_data)
|
||||||
@@ -141,7 +161,7 @@ class StrategyWorker:
|
|||||||
|
|
||||||
price_change = (current_price - self.entry_price) / self.entry_price
|
price_change = (current_price - self.entry_price) / self.entry_price
|
||||||
trade_return = price_change * self.direction
|
trade_return = price_change * self.direction
|
||||||
net = trade_return * self.leverage - FEE_RT * self.leverage
|
net = trade_return * self.leverage - self.fee_rt * self.leverage
|
||||||
pnl = self.capital * self.position_size * net
|
pnl = self.capital * self.position_size * net
|
||||||
|
|
||||||
is_win = trade_return > 0
|
is_win = trade_return > 0
|
||||||
@@ -174,6 +194,9 @@ class StrategyWorker:
|
|||||||
self.entry_price = 0
|
self.entry_price = 0
|
||||||
self.entry_time = ""
|
self.entry_time = ""
|
||||||
self.bars_held = 0
|
self.bars_held = 0
|
||||||
|
self.tp = 0.0
|
||||||
|
self.sl = 0.0
|
||||||
|
self.max_bars = 0
|
||||||
|
|
||||||
def tick(self, df: pd.DataFrame, df_1h: pd.DataFrame | None = None):
|
def tick(self, df: pd.DataFrame, df_1h: pd.DataFrame | None = None):
|
||||||
"""Chiamato ad ogni poll con DataFrame OHLCV aggiornato.
|
"""Chiamato ad ogni poll con DataFrame OHLCV aggiornato.
|
||||||
@@ -195,7 +218,23 @@ class StrategyWorker:
|
|||||||
self.bars_held += 1
|
self.bars_held += 1
|
||||||
self.last_bar_ts = current_ts
|
self.last_bar_ts = current_ts
|
||||||
|
|
||||||
if self.bars_held >= self.hold_bars:
|
if self.tp and self.sl:
|
||||||
|
# Exit guidati dalla strategia: SL (conservativo, prima), poi TP, poi time-limit
|
||||||
|
if self.direction == 1:
|
||||||
|
if current_price <= self.sl:
|
||||||
|
self._close_position(current_price, "stop_loss")
|
||||||
|
elif current_price >= self.tp:
|
||||||
|
self._close_position(current_price, "take_profit")
|
||||||
|
elif self.max_bars and self.bars_held >= self.max_bars:
|
||||||
|
self._close_position(current_price, "time_limit")
|
||||||
|
else:
|
||||||
|
if current_price >= self.sl:
|
||||||
|
self._close_position(current_price, "stop_loss")
|
||||||
|
elif current_price <= self.tp:
|
||||||
|
self._close_position(current_price, "take_profit")
|
||||||
|
elif self.max_bars and self.bars_held >= self.max_bars:
|
||||||
|
self._close_position(current_price, "time_limit")
|
||||||
|
elif self.bars_held >= self.hold_bars:
|
||||||
self._close_position(current_price, "hold_limit")
|
self._close_position(current_price, "hold_limit")
|
||||||
else:
|
else:
|
||||||
pnl_pct = (current_price - self.entry_price) / self.entry_price * self.direction
|
pnl_pct = (current_price - self.entry_price) / self.entry_price * self.direction
|
||||||
|
|||||||
Reference in New Issue
Block a user