From 48435f685853a0ee355f13f214290cb591d56bd2 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Thu, 28 May 2026 20:46:35 +0000 Subject: [PATCH] feat(live): worker con exit TP/SL/max_bars per MR01 + doc aggiornata MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- CLAUDE.md | 81 +++++++-------- README.md | 119 +++++++++++++---------- scripts/analysis/validate_worker_mr01.py | 69 +++++++++++++ src/live/strategy_worker.py | 43 +++++++- 4 files changed, 218 insertions(+), 94 deletions(-) create mode 100644 scripts/analysis/validate_worker_mr01.py diff --git a/CLAUDE.md b/CLAUDE.md index d28e5b7..9f8a4ba 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,15 +24,17 @@ 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 (15m + 1h live per MTF), tick worker - strategy_worker.py → worker indipendente: capital, trade log, stato persistente + multi_runner.py → orchestratore: carica YAML, fetch candele, tick worker + 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/ 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 -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 +scripts/strategies/ → strategie con edge validato OOS (solo MR01_bollinger_fade) +scripts/waste/ → strategie scartate (W01-W28 + famiglia squeeze SQ/MT/ML/AD/CM/PD) +scripts/analysis/ → ricerca/validazione OOS fee-aware (strategy_research, oos_validation, ...) strategies.yml → config multi-strategy paper trader docs/diary/ → diario di ricerca giornaliero docs/specs/ → specifiche di design @@ -44,9 +46,10 @@ 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/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 scripts/strategies/MR01_bollinger_fade.py # strategia attiva (mean-reversion) +uv run python scripts/analysis/strategy_research.py # ricerca strategie fee-aware OOS +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 docker compose up -d # deploy Docker uv run pytest # test @@ -67,49 +70,51 @@ Token observer: nel file `secrets/observer.token` del progetto CerberoSuite. ## Strategie attive -Tutte le strategie estendono `src.strategies.base.Strategy` con interfaccia comune: -`generate_signals() → backtest() → report()`. +> **LEZIONE CRITICA (2026-05-28).** L'intera famiglia squeeze-breakout (SQ01-04, +> 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 | -|--------|------|------|-----|----|--------|------| -| 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 | +| Codice | Nome | Tipo | Edge OOS netto | DD | Note | +|--------|------|------|----------------|----|------| +| **MR01** | Bollinger Fade | Mean-reversion | **BTC 1h n50 k2.5: +201% / +196% (worker)** | 15% | Fada la banda, TP alla media, SL ad ATR | -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`. +MR01 è robusto su **tutta** la griglia parametri (`n∈{14,20,30,50}` × `k∈{2.0,2.5,3.0}`, +entrambi gli asset → tutte positive OOS) e su **tutte** le fee 0.00-0.20% RT. +Validato col worker reale: BTC +196% / ETH +251% OOS (nov 2023→mag 2026). +Ricerca completa: `scripts/analysis/strategy_research.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 +**Metodologia obbligatoria per ogni nuova strategia** (per non ripetere l'errore squeeze): +1. Ingresso eseguibile: direzione e prezzo decisi con dati **fino a `close[i]`**, mai `close[i-1]` con direzione da `i`. +2. Backtest **NETTO** dopo fee realistiche Deribit (**0.10% RT** taker, non 0.20%) + leva. +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, -mean reversion RSI, volume spike, squeeze+MR — tutte sotto 65% acc o DD >14%. +Strategie scartate storiche in `scripts/waste/` (W01-W28 + la famiglia squeeze). ## Multi-Strategy Paper Trader 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. -**Persistenza:** `data/paper_trades/{strategy}___{asset}__{tf}/` con `trades.jsonl` (append-only) + `status.json` (resume al restart). +**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, include tp/sl/max_bars). **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`). ## Convenzioni -- Strategie in `scripts/strategies/` con codice univoco (SQ01, ML01, ...). -- Script scartati in `scripts/waste/` con prefisso W01-W22. +- Strategie in `scripts/strategies/` con codice univoco (MR01, ...). +- Script scartati in `scripts/waste/` (W01-W28 + famiglia squeeze). - Diario in `docs/diary/YYYY-MM-DD.md`. Aggiornare dopo ogni esperimento significativo. - 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]`. @@ -117,7 +122,7 @@ Orchestratore che esegue N strategie in parallelo su dati live Cerbero, ognuna c ## 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]`. -- **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. - **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. diff --git a/README.md b/README.md index 04d8b68..056e41b 100644 --- a/README.md +++ b/README.md @@ -8,47 +8,56 @@ Partendo da un capitale iniziale di €1.000, raggiungere un profitto medio di ## 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 | -|--------|-----------|---------|----------|--------|--------|------------| -| 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 | +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): -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 -### 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"). -2. **Breakout** — le bande escono dal canale. Un impulso direzionale parte. -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. +1. **Bollinger Bands** (window `n`, `k` deviazioni standard) sul close. +2. **Entry** — quando il close esce *sotto* la banda inferiore → **long** (o *sopra* la superiore → **short**). Ingresso a `close[i]`, eseguibile dal vivo. +3. **Take-profit** alla media mobile (il rientro atteso). +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. -- **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. +L'ipotesi originale era opposta — *continuazione* dopo la compressione di volatilità +(Bollinger dentro Keltner → breakout direzionale). Su dati storici sembrava dare +76-82% di accuracy, ma era un **artefatto di look-ahead**: il backtest entrava a +`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) -- Momentum, volatilità, skewness, kurtosis dei rendimenti logaritmici -- Autocorrelazione lag-1 e profilo volumetrico -- Durata della fase di squeeze e rapporto di espansione Keltner -- Posizione del prezzo rispetto al range recente e ATR normalizzato +Ogni nuova strategia deve passare: (1) **ingresso eseguibile** senza look-ahead, +(2) backtest **netto** dopo fee realistiche (0.10% RT Deribit), (3) validazione +**out-of-sample** + robustezza su griglia parametri + sweep fee. Strumenti in +`scripts/analysis/` (`strategy_research.py`, `oos_validation.py`, `intrabar_test.py`). ## Struttura progetto @@ -66,12 +75,12 @@ PythagorasGoal/ │ ├── strategy_worker.py # Worker indipendente con stato persistente │ ├── strategy_loader.py # Import dinamico classi Strategy │ ├── 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 ├── scripts/ -│ ├── strategies/ # Strategie attive (SQ01-SQ04, ML01, MT01, PD01, CM01, AD01) -│ ├── waste/ # Strategie scartate (W01-W28) -│ └── analysis/ # Script di confronto e report +│ ├── strategies/ # Strategie con edge validato OOS (solo MR01_bollinger_fade) +│ ├── waste/ # Strategie scartate (W01-W28 + famiglia squeeze SQ/MT/ML/AD/CM/PD) +│ └── analysis/ # Ricerca/validazione OOS fee-aware (strategy_research, oos_validation, ...) ├── strategies.yml # Config multi-strategy paper trader ├── data/ │ └── raw/ # Parquet OHLCV (gitignored, ~70 MB) @@ -85,35 +94,32 @@ PythagorasGoal/ ## 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 | |--------|--------|------|-------------| -| SQ01 | `SQ01_squeeze_base.py` | Regole | Squeeze breakout puro | -| 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à | +| **MR01** | `MR01_bollinger_fade.py` | Mean-reversion | Fada la banda di Bollinger, TP alla media, SL ad ATR. Unica con edge netto validato OOS. | -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 -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 -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 -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 @@ -136,10 +142,15 @@ defaults: leverage: 3 strategies: - - name: SQ02_antifake_vol + - name: MR01_bollinger_fade asset: BTC - tf: 15m + tf: 1h 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. @@ -150,9 +161,9 @@ Ogni strategia ha la sua directory in `data/paper_trades/`: ``` data/paper_trades/ - SQ02_antifake_vol__BTC__15m/ + MR01_bollinger_fade__BTC__1h/ 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`). @@ -167,8 +178,8 @@ uv sync # Scarica dati storici (~70 MB) uv run python -m src.data.downloader -# Backtest strategia migliore -uv run python scripts/strategies/MT01_squeeze_mtf_momentum.py +# Backtest strategia attiva +uv run python scripts/strategies/MR01_bollinger_fade.py # Paper trading live uv run python -m src.live.multi_runner diff --git a/scripts/analysis/validate_worker_mr01.py b/scripts/analysis/validate_worker_mr01.py new file mode 100644 index 0000000..1ce42f1 --- /dev/null +++ b/scripts/analysis/validate_worker_mr01.py @@ -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() diff --git a/src/live/strategy_worker.py b/src/live/strategy_worker.py index 07ab51e..6557824 100644 --- a/src/live/strategy_worker.py +++ b/src/live/strategy_worker.py @@ -55,6 +55,13 @@ class StrategyWorker: self.started_at = datetime.now(timezone.utc).isoformat() 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._save_state() @@ -78,6 +85,9 @@ class StrategyWorker: self.total_wins = state.get("total_wins", 0) self.started_at = state.get("started_at", self.started_at) 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), "total_trades": self.total_trades, @@ -95,6 +105,9 @@ class StrategyWorker: "total_wins": self.total_wins, "started_at": self.started_at, "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(), } with open(self.status_path, "w") as f: @@ -125,12 +138,19 @@ class StrategyWorker: self.entry_time = datetime.now(timezone.utc).isoformat() 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 = { "direction": "long" if signal.direction == 1 else "short", "price": round(current_price, 2), "size": round(size, 6), "notional": round(notional, 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._notify("OPENED", trade_data) @@ -141,7 +161,7 @@ class StrategyWorker: price_change = (current_price - self.entry_price) / self.entry_price 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 is_win = trade_return > 0 @@ -174,6 +194,9 @@ class StrategyWorker: self.entry_price = 0 self.entry_time = "" 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): """Chiamato ad ogni poll con DataFrame OHLCV aggiornato. @@ -195,7 +218,23 @@ class StrategyWorker: self.bars_held += 1 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") else: pnl_pct = (current_price - self.entry_price) / self.entry_price * self.direction