Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d39c75b103 | |||
| f42fec9fac | |||
| 56bad4741e | |||
| b79c87e4af | |||
| 0e47956f7a | |||
| fa2d74be77 | |||
| 041db2191c | |||
| 185ac0d49b | |||
| 0ab3b5698a | |||
| 7639e5012b | |||
| 2694a4a00c | |||
| a7b3c3c203 |
@@ -9,29 +9,46 @@ Progetto di ricerca: riconoscimento pattern frattali per trading algoritmico su
|
||||
- **Linguaggio:** Python 3.11+
|
||||
- **Package manager:** uv (dipendenze in `pyproject.toml`, lock in `uv.lock`)
|
||||
- **Dati:** Parquet in `data/raw/` (non committati, ~70 MB)
|
||||
- **ML:** scikit-learn (GradientBoosting), PyTorch (LSTM)
|
||||
- **ML:** scikit-learn (GradientBoostingClassifier)
|
||||
- **Analisi:** numpy, pandas, scipy
|
||||
- **API dati:** Cerbero MCP su `cerbero-mcp.tielogic.xyz` (Deribit, Bybit, Hyperliquid), ccxt/Binance come fallback
|
||||
- **Config:** pyyaml per `strategies.yml`
|
||||
|
||||
## Struttura
|
||||
|
||||
```
|
||||
src/data/ → download e caricamento dati (downloader.py)
|
||||
src/fractal/ → indicatori frattali (patterns.py, indicators.py, similarity.py)
|
||||
src/backtest/ → engine di backtesting (engine.py)
|
||||
scripts/ → analisi e strategie numerate 01–13
|
||||
docs/diary/ → diario di ricerca giornaliero
|
||||
data/raw/ → file .parquet OHLCV (gitignored)
|
||||
data/processed/ → modelli salvati (gitignored)
|
||||
src/data/ → download e caricamento dati (downloader.py)
|
||||
src/fractal/ → indicatori frattali (patterns.py, indicators.py, similarity.py)
|
||||
src/backtest/ → engine di backtesting (engine.py)
|
||||
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
|
||||
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/waste/ → strategie scartate (W01-W22 + REF originali)
|
||||
scripts/analysis/ → script di confronto e report
|
||||
strategies.yml → config multi-strategy paper trader
|
||||
docs/diary/ → diario di ricerca giornaliero
|
||||
docs/specs/ → specifiche di design
|
||||
data/raw/ → file .parquet OHLCV (gitignored)
|
||||
```
|
||||
|
||||
## Comandi
|
||||
|
||||
```bash
|
||||
uv sync # installa dipendenze
|
||||
uv run python -m src.data.downloader # scarica dati storici
|
||||
uv run python scripts/13_squeeze_ml_hybrid.py # strategia vincente
|
||||
uv run pytest # test
|
||||
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/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
|
||||
uv run pytest # test
|
||||
```
|
||||
|
||||
## Dati storici
|
||||
@@ -47,22 +64,37 @@ df = load_data("ETH", "15m") # carica un asset/timeframe
|
||||
Fonte primaria: Cerbero MCP (endpoint `/mcp-deribit/tools/get_historical`).
|
||||
Token observer: nel file `secrets/observer.token` del progetto CerberoSuite.
|
||||
|
||||
## Strategia vincente
|
||||
## Strategie attive
|
||||
|
||||
**Squeeze + ML ibrida** (script 13):
|
||||
Tutte le strategie estendono `src.strategies.base.Strategy` con interfaccia comune:
|
||||
`generate_signals() → backtest() → report()`.
|
||||
|
||||
1. Rileva squeeze di volatilità (Bollinger dentro Keltner)
|
||||
2. Al rilascio dello squeeze, estrai feature strutturali dalla finestra
|
||||
3. GradientBoosting predice direzione con walk-forward training
|
||||
4. Trade solo se modello ha confidenza ≥ 70%
|
||||
| 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 |
|
||||
|
||||
Configurazione migliore: ETH 15m, BBw=14, squeeze threshold=0.8, breakout=3 barre, leva 3x, position 15%.
|
||||
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
|
||||
|
||||
Risultato backtestato: 76.9% accuracy, 118% annuo, 4.2% drawdown, €13.78/giorno da €1.000.
|
||||
## 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).
|
||||
**Hot-add:** aggiungi riga YAML → `docker compose restart` → storico intatto.
|
||||
**Notifiche:** Telegram per ogni trade (richiede `.env` con `TELEGRAM_BOT_TOKEN` e `TELEGRAM_CHAT_ID`).
|
||||
|
||||
## Convenzioni
|
||||
|
||||
- Script numerati progressivamente (`01_`, `02_`, …). Ogni script è autocontenuto.
|
||||
- Strategie in `scripts/strategies/` con codice univoco (SQ01, ML01, ...).
|
||||
- Script scartati in `scripts/waste/` con prefisso W01-W22.
|
||||
- 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]`.
|
||||
@@ -72,3 +104,4 @@ Risultato backtestato: 76.9% accuracy, 118% annuo, 4.2% drawdown, €13.78/giorn
|
||||
- **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.
|
||||
- **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.
|
||||
|
||||
+3
-1
@@ -8,7 +8,9 @@ COPY pyproject.toml uv.lock ./
|
||||
RUN uv sync --frozen --no-dev
|
||||
|
||||
COPY src/ src/
|
||||
COPY scripts/strategies/ scripts/strategies/
|
||||
COPY strategies.yml strategies.yml
|
||||
|
||||
VOLUME /app/data
|
||||
|
||||
CMD ["uv", "run", "python", "-m", "src.live.paper_trader"]
|
||||
CMD ["uv", "run", "python", "-m", "src.live.multi_runner"]
|
||||
|
||||
@@ -8,17 +8,18 @@ Partendo da un capitale iniziale di €1.000, raggiungere un profitto medio di
|
||||
|
||||
## Risultati
|
||||
|
||||
Tredici strategie testate su dati storici 2018–2026 (BTC e ETH, timeframe 5m / 15m / 1h). Le migliori cinque:
|
||||
Oltre 30 strategie testate su dati storici 2018–2026 (BTC e ETH, timeframe 15m / 1h). Le migliori:
|
||||
|
||||
| # | Strategia | Accuracy | ROI annuo | Max DD | €/giorno |
|
||||
|---|-----------|----------|-----------|--------|----------|
|
||||
| 1 | ETH 15m Squeeze + ML ibrida | 76.9% | 118% | 4.2% | €13.78 |
|
||||
| 2 | ETH 1h Squeeze + Vol | 83.9% | 22% | 2.0% | €0.71 |
|
||||
| 3 | BTC 15m Squeeze + ML ibrida | 78.8% | 69% | 7.0% | €5.51 |
|
||||
| 4 | ETH 1h Squeeze (BBw=30) | 82.8% | 47% | 3.2% | €1.77 |
|
||||
| 5 | ETH Walk-Forward ML | 57.7% | 38% | 47% | €3.12 |
|
||||
| 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 |
|
||||
|
||||
La strategia vincente (#1) opera su ETH a 15 minuti con ~1 trade al giorno, leva 3x e drawdown contenuto al 4.2%.
|
||||
La strategia più robusta (SQ02 BTC 15m) mantiene accuracy ≥73% ogni anno dal 2018 con Sharpe 5.01.
|
||||
|
||||
## Come funziona
|
||||
|
||||
@@ -28,14 +29,14 @@ Il meccanismo centrale sfrutta i cicli naturali di compressione ed espansione de
|
||||
|
||||
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. **Conferma ML** — un modello GradientBoosting, addestrato su feature strutturali e frattali della finestra precedente, conferma la direzione e filtra i segnali deboli.
|
||||
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.
|
||||
|
||||
### Feature frattali
|
||||
### Feature ML (44 dimensioni)
|
||||
|
||||
- Rapporti body/shadow normalizzati su finestre multiple (12, 24, 48 candele)
|
||||
- Momentum, volatilità, skewness, kurtosis dei rendimenti logaritmici
|
||||
- Autocorrelazione lag-1
|
||||
- Profilo volumetrico e spike detection
|
||||
- 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
|
||||
|
||||
@@ -44,44 +45,121 @@ Il meccanismo centrale sfrutta i cicli naturali di compressione ed espansione de
|
||||
```
|
||||
PythagorasGoal/
|
||||
├── src/
|
||||
│ ├── data/ # Download e gestione dati storici (Cerbero MCP + Binance)
|
||||
│ ├── fractal/ # Indicatori frattali: Hurst, Higuchi FD, self-similarity
|
||||
│ ├── backtest/ # Motore di backtesting con fee e metriche
|
||||
│ ├── strategies/ # (predisposto per strategie modulari)
|
||||
│ ├── nn/ # (predisposto per reti neurali)
|
||||
│ └── utils/
|
||||
├── scripts/ # Script di analisi e test (01–13)
|
||||
│ ├── data/ # Download e gestione dati (Cerbero MCP + Binance)
|
||||
│ ├── fractal/ # Indicatori frattali: Hurst, Higuchi FD, self-similarity
|
||||
│ ├── backtest/ # Motore di backtesting con fee e metriche
|
||||
│ ├── strategies/ # Classe base Strategy ABC + indicatori condivisi
|
||||
│ │ ├── base.py # Strategy, Signal, BacktestResult, YearlyStats
|
||||
│ │ └── indicators.py # keltner_ratio, detect_squeezes, ema, atr, rv, corr
|
||||
│ └── live/ # Paper trading live su Deribit testnet
|
||||
│ ├── multi_runner.py # Orchestratore multi-strategia
|
||||
│ ├── 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)
|
||||
│ └── telegram_notifier.py
|
||||
├── scripts/
|
||||
│ ├── strategies/ # Strategie attive (SQ01-SQ04, ML01)
|
||||
│ ├── waste/ # Strategie scartate (W01-W22)
|
||||
│ └── analysis/ # Script di confronto e report
|
||||
├── strategies.yml # Config multi-strategy paper trader
|
||||
├── data/
|
||||
│ ├── raw/ # Parquet OHLCV (non committati, ~70 MB)
|
||||
│ └── processed/ # Modelli salvati
|
||||
│ └── raw/ # Parquet OHLCV (gitignored, ~70 MB)
|
||||
├── docs/
|
||||
│ └── diary/ # Diario di ricerca giornaliero
|
||||
├── tests/
|
||||
├── pyproject.toml
|
||||
└── README.md
|
||||
│ ├── diary/ # Diario di ricerca giornaliero
|
||||
│ └── specs/ # Specifiche di design
|
||||
├── Dockerfile
|
||||
├── docker-compose.yml
|
||||
└── pyproject.toml
|
||||
```
|
||||
|
||||
## Strategie attive
|
||||
|
||||
Tutte le strategie estendono `src.strategies.base.Strategy` con interfaccia comune: `generate_signals() → backtest() → report()`.
|
||||
|
||||
| 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 |
|
||||
|
||||
Per eseguire il backtest di una strategia:
|
||||
|
||||
```bash
|
||||
uv run python scripts/strategies/SQ02_squeeze_antifake_vol.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.
|
||||
|
||||
### Avvio
|
||||
|
||||
```bash
|
||||
# Locale
|
||||
uv run python -m src.live.multi_runner
|
||||
|
||||
# Docker
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### Configurazione
|
||||
|
||||
Le strategie attive sono definite in `strategies.yml`:
|
||||
|
||||
```yaml
|
||||
defaults:
|
||||
capital: 1000
|
||||
position_size: 0.15
|
||||
leverage: 3
|
||||
|
||||
strategies:
|
||||
- name: SQ02_antifake_vol
|
||||
asset: BTC
|
||||
tf: 15m
|
||||
enabled: true
|
||||
```
|
||||
|
||||
Per aggiungere una strategia: nuova riga in `strategies.yml`, poi `docker compose restart`. Lo storico delle strategie esistenti rimane intatto.
|
||||
|
||||
### Persistenza
|
||||
|
||||
Ogni strategia ha la sua directory in `data/paper_trades/`:
|
||||
|
||||
```
|
||||
data/paper_trades/
|
||||
SQ02_antifake_vol__BTC__15m/
|
||||
trades.jsonl # Storico trade append-only
|
||||
status.json # Stato corrente (resume al restart)
|
||||
```
|
||||
|
||||
Notifiche Telegram per ogni trade (richiede `TELEGRAM_BOT_TOKEN` e `TELEGRAM_CHAT_ID` in `.env`).
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
# Clona il repository
|
||||
# Clona e installa
|
||||
git clone <repo-url> && cd PythagorasGoal
|
||||
|
||||
# Installa dipendenze (richiede uv)
|
||||
uv sync
|
||||
|
||||
# Scarica dati storici (~70 MB, richiede connessione)
|
||||
# Scarica dati storici (~70 MB)
|
||||
uv run python -m src.data.downloader
|
||||
|
||||
# Esegui la strategia ibrida vincente
|
||||
uv run python scripts/13_squeeze_ml_hybrid.py
|
||||
# Backtest strategia migliore
|
||||
uv run python scripts/strategies/SQ02_squeeze_antifake_vol.py
|
||||
|
||||
# Paper trading live
|
||||
uv run python -m src.live.multi_runner
|
||||
```
|
||||
|
||||
### Requisiti
|
||||
|
||||
- Python ≥ 3.11
|
||||
- [uv](https://docs.astral.sh/uv/) come package manager
|
||||
- Accesso a Cerbero MCP (`cerbero-mcp.tielogic.xyz`) per i dati Deribit, oppure Binance via ccxt come fallback
|
||||
- Accesso a Cerbero MCP (`cerbero-mcp.tielogic.xyz`) per dati Deribit live
|
||||
- Docker (opzionale, per deploy su VPS)
|
||||
|
||||
## Dati
|
||||
|
||||
@@ -90,25 +168,7 @@ uv run python scripts/13_squeeze_ml_hybrid.py
|
||||
| BTC | 5m / 15m / 1h | 883K / 294K / 74K | 2018-01 → oggi |
|
||||
| ETH | 5m / 15m / 1h | 882K / 294K / 74K | 2018-01 → oggi |
|
||||
|
||||
Fonte primaria: Deribit perpetual via Cerbero MCP. Fallback per il periodo antecedente: Binance spot via ccxt. Formato: Apache Parquet.
|
||||
|
||||
## Strategie testate
|
||||
|
||||
| Script | Approccio | Esito |
|
||||
|--------|-----------|-------|
|
||||
| 01 | Pattern candlestick discreti (U/D/0) | Nessun edge |
|
||||
| 02 | DTW pattern matching | Troppo lento, edge minimo |
|
||||
| 03 | Proiezione FFT (ispirata al paper) | Random (49.8%) |
|
||||
| 04 | GBM su feature frattali (Hurst, FD) | 63.6% a soglia 0.65 |
|
||||
| 05 | GBM multi-window (corretto data leakage) | 58.9% |
|
||||
| 06 | GBM su feature strutturali normalizzate | 58.6%, +57.5% return |
|
||||
| 07 | LSTM su sequenze candele | 58.4%, comparabile a GBM |
|
||||
| 08 | Ensemble multi-timeframe (1h + 15m) | 59.2% (consensus 2/3) |
|
||||
| 09 | Walk-forward ML | 57.7%, Sharpe 7.4, €3.12/day |
|
||||
| 10 | Ensemble 5 modelli alta precisione | In corso |
|
||||
| 11 | **Volatility Squeeze Breakout** | **83.9%**, approccio strutturale |
|
||||
| 12 | Report finale e simulazione crescita | — |
|
||||
| 13 | **Squeeze + ML ibrida** | **76.9%**, 118% ann, €13.78/day |
|
||||
Fonte primaria: Deribit perpetual via Cerbero MCP. Fallback: Binance spot via ccxt. Formato: Apache Parquet.
|
||||
|
||||
## Riferimenti
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"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"}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"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"}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"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"}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"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"}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"in_position": false,
|
||||
"direction": null,
|
||||
"entry_price": 0,
|
||||
"entry_time": null,
|
||||
"bars_held": 0,
|
||||
"last_update": "2026-05-27T07:40:09.196718+00:00"
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
{"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}
|
||||
@@ -0,0 +1,3 @@
|
||||
{"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}
|
||||
@@ -0,0 +1,6 @@
|
||||
{"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}
|
||||
+5
-2
@@ -1,14 +1,17 @@
|
||||
services:
|
||||
paper-trader:
|
||||
build: .
|
||||
container_name: pythagoras-paper
|
||||
container_name: pythagoras-multi
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
- ./strategies.yml:/app/strategies.yml:ro
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- PYTHONUNBUFFERED=1
|
||||
healthcheck:
|
||||
test: ["CMD", "python", "-c", "import json; s=json.load(open('/app/data/paper_trades/status.json')); assert s['last_update']"]
|
||||
test: ["CMD", "python", "-c", "import os; assert any(f.endswith('status.json') for r,d,fs in os.walk('/app/data/paper_trades') for f in fs)"]
|
||||
interval: 120s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
# Multi-Strategy Paper Trader — Design Spec
|
||||
|
||||
## Obiettivo
|
||||
|
||||
Eseguire N strategie di trading in parallelo su Deribit testnet (paper trading locale), ognuna con capitale virtuale indipendente di €1000 USDC. Lo storico trade di ogni strategia persiste tra restart. Nuove strategie aggiungibili in corso d'opera via config YAML senza perdere lo storico delle esistenti.
|
||||
|
||||
## Architettura
|
||||
|
||||
Un singolo container Docker esegue un orchestratore (`MultiStrategyRunner`) che gestisce N `StrategyWorker`. Ogni worker è indipendente: proprio capital, propri trade, proprio stato.
|
||||
|
||||
```
|
||||
Docker Container
|
||||
├── MultiStrategyRunner (orchestratore, loop principale)
|
||||
│ ├── StrategyWorker[SQ02_BTC_15m] → paper trade → JSONL
|
||||
│ ├── StrategyWorker[ML01_ETH_15m] → paper trade → JSONL
|
||||
│ └── ...altri worker da YAML
|
||||
├── CerberoClient (condiviso, fetch prezzi)
|
||||
└── TelegramNotifier (condiviso)
|
||||
```
|
||||
|
||||
## Componenti
|
||||
|
||||
### 1. `strategies.yml` — Configurazione
|
||||
|
||||
```yaml
|
||||
defaults:
|
||||
capital: 1000
|
||||
position_size: 0.15
|
||||
leverage: 3
|
||||
hold_bars: 3
|
||||
poll_seconds: 60
|
||||
retrain_hours: 24
|
||||
|
||||
strategies:
|
||||
- name: SQ02_antifake_vol
|
||||
asset: BTC
|
||||
tf: 15m
|
||||
enabled: true
|
||||
|
||||
- name: SQ02_antifake_vol
|
||||
asset: ETH
|
||||
tf: 15m
|
||||
enabled: true
|
||||
|
||||
- name: ML01_squeeze_gbm
|
||||
asset: ETH
|
||||
tf: 15m
|
||||
enabled: true
|
||||
position_size: 0.20
|
||||
params:
|
||||
ml_threshold: 0.70
|
||||
bb_window: 14
|
||||
sq_threshold: 0.8
|
||||
```
|
||||
|
||||
Ogni entry eredita `defaults`. Override per-strategia possibile su tutti i campi. Il campo `params` passa kwargs a `generate_signals()` o al backtest ML.
|
||||
|
||||
### 2. `StrategyWorker` — Worker per singola strategia
|
||||
|
||||
Responsabilità:
|
||||
- Importa la classe Strategy corrispondente da `scripts/strategies/`
|
||||
- Mantiene stato: capital, posizione aperta, equity
|
||||
- Al startup: ricarica `status.json` se esiste (resume), altrimenti inizia da zero
|
||||
- Ad ogni tick: riceve DataFrame candele, genera segnali, paper-trade
|
||||
- Logga ogni evento in `trades.jsonl` (append-only)
|
||||
- Aggiorna `status.json` ad ogni tick
|
||||
|
||||
Stato persistente (`status.json`):
|
||||
```json
|
||||
{
|
||||
"capital": 1023.45,
|
||||
"in_position": true,
|
||||
"direction": "long",
|
||||
"entry_price": 2534.20,
|
||||
"entry_time": "2026-05-27T14:30:00Z",
|
||||
"bars_held": 1,
|
||||
"total_trades": 15,
|
||||
"total_wins": 12,
|
||||
"started_at": "2026-05-27T10:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
Trade log (`trades.jsonl`), append-only:
|
||||
```json
|
||||
{"ts": "2026-05-27T14:30:00Z", "event": "OPEN", "direction": "long", "price": 2534.20, "size": 0.18, "capital": 1023.45}
|
||||
{"ts": "2026-05-27T15:15:00Z", "event": "CLOSE", "reason": "hold_limit", "entry": 2534.20, "exit": 2560.10, "pnl": 3.45, "fee": 0.92, "net_pnl": 2.53, "capital": 1025.98}
|
||||
```
|
||||
|
||||
### 3. `MultiStrategyRunner` — Orchestratore
|
||||
|
||||
Loop principale:
|
||||
1. Carica `strategies.yml`
|
||||
2. Per ogni entry, crea `StrategyWorker` (o riprende se già esiste)
|
||||
3. Ogni 60s:
|
||||
a. Fetch candele live da Cerbero (una volta per asset/tf unico)
|
||||
b. Passa DataFrame a ogni worker
|
||||
c. Ogni worker valuta segnali e gestisce posizione
|
||||
d. Worker ML: retrain ogni 24h
|
||||
4. Notifica Telegram per ogni trade
|
||||
|
||||
Ottimizzazione: fetch candele raggruppato per (asset, tf). Se 3 strategie usano BTC 15m, fetch una volta sola.
|
||||
|
||||
### 4. Persistenza
|
||||
|
||||
```
|
||||
data/paper_trades/
|
||||
SQ02_antifake_vol__BTC__15m/
|
||||
trades.jsonl
|
||||
status.json
|
||||
SQ02_antifake_vol__ETH__15m/
|
||||
trades.jsonl
|
||||
status.json
|
||||
ML01_squeeze_gbm__ETH__15m/
|
||||
trades.jsonl
|
||||
status.json
|
||||
```
|
||||
|
||||
Directory naming: `{strategy_name}__{asset}__{tf}` con double underscore separatore.
|
||||
|
||||
Volume Docker: `./data:/app/data` — persiste tra restart.
|
||||
|
||||
### 5. Aggiunta strategia in corso
|
||||
|
||||
1. Aggiungi entry in `strategies.yml`
|
||||
2. `docker compose restart`
|
||||
3. Runner carica YAML, trova nuova entry senza `status.json` → parte da €1000
|
||||
4. Strategie esistenti riprendono da `status.json` → storico intatto
|
||||
|
||||
### 6. Docker
|
||||
|
||||
`Dockerfile` — invariato, aggiunge `strategies.yml` alla COPY.
|
||||
|
||||
`docker-compose.yml`:
|
||||
```yaml
|
||||
services:
|
||||
paper-trader:
|
||||
build: .
|
||||
container_name: pythagoras-multi
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
- ./strategies.yml:/app/strategies.yml:ro
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- PYTHONUNBUFFERED=1
|
||||
```
|
||||
|
||||
`CMD` cambia a: `uv run python -m src.live.multi_runner`
|
||||
|
||||
### 7. Strategia-specifica: ML01
|
||||
|
||||
ML01 richiede training del modello GBM. Il worker ML01:
|
||||
- Al primo avvio: train su storico (365 giorni via Cerbero)
|
||||
- Ogni `retrain_hours`: retrain
|
||||
- Usa `SignalEngine` esistente per check_signal()
|
||||
- Le strategie SQ* non hanno training — solo regole deterministiche
|
||||
|
||||
### 8. File da creare/modificare
|
||||
|
||||
Nuovi:
|
||||
- `src/live/multi_runner.py` — orchestratore
|
||||
- `src/live/strategy_worker.py` — worker per singola strategia
|
||||
- `strategies.yml` — config
|
||||
- `src/live/strategy_loader.py` — import dinamico classi Strategy
|
||||
|
||||
Modifiche:
|
||||
- `docker-compose.yml` — nuovo CMD, volume strategies.yml
|
||||
- `Dockerfile` — COPY strategies.yml
|
||||
|
||||
Invariati:
|
||||
- `src/live/cerbero_client.py`
|
||||
- `src/live/telegram_notifier.py`
|
||||
- `src/live/signal_engine.py` (usato da ML01 worker)
|
||||
@@ -14,6 +14,7 @@ dependencies = [
|
||||
"torch>=2.0",
|
||||
"matplotlib>=3.7",
|
||||
"tqdm>=4.65",
|
||||
"pyyaml>=6.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
"""Confronto migliori strategie S1 e S2 — andamento per anno."""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from src.data.downloader import load_data
|
||||
from src.fractal.patterns import encode_candles
|
||||
|
||||
FEE_PERP = 0.002 # 0.1% taker roundtrip perpetual
|
||||
FEE_OPT = 0.0052 # options roundtrip
|
||||
INITIAL = 1000
|
||||
LEVERAGE = 3
|
||||
|
||||
|
||||
def keltner_ratio(close, high, low, window=14):
|
||||
n = len(close)
|
||||
r = np.full(n, np.nan)
|
||||
for i in range(window, n):
|
||||
wc, wh, wl = close[i-window:i], high[i-window:i], low[i-window:i]
|
||||
ma = np.mean(wc)
|
||||
bb_std = np.std(wc)
|
||||
tr = np.maximum(wh-wl, np.maximum(np.abs(wh-np.roll(wc,1)), np.abs(wl-np.roll(wc,1))))
|
||||
atr = np.mean(tr[1:])
|
||||
kc = (ma+1.5*atr)-(ma-1.5*atr)
|
||||
bb = (ma+2*bb_std)-(ma-2*bb_std)
|
||||
if kc > 0:
|
||||
r[i] = bb/kc
|
||||
return r
|
||||
|
||||
|
||||
def rv_ann(close, window):
|
||||
lr = np.diff(np.log(np.where(close==0, 1e-10, close)))
|
||||
r = np.full(len(close), np.nan)
|
||||
for i in range(window, len(lr)):
|
||||
r[i+1] = np.std(lr[i-window:i]) * np.sqrt(24*365)
|
||||
return r
|
||||
|
||||
|
||||
def rsi(close, period=14):
|
||||
delta = np.diff(close)
|
||||
gain = np.where(delta>0, delta, 0)
|
||||
loss = np.where(delta<0, -delta, 0)
|
||||
result = np.full(len(close), 50.0)
|
||||
if len(gain) < period:
|
||||
return result
|
||||
ag = np.mean(gain[:period])
|
||||
al = np.mean(loss[:period])
|
||||
for i in range(period, len(delta)):
|
||||
ag = (ag*(period-1)+gain[i])/period
|
||||
al = (al*(period-1)+loss[i])/period
|
||||
result[i+1] = 100 if al == 0 else 100-100/(1+ag/al)
|
||||
return result
|
||||
|
||||
|
||||
def ema(arr, period):
|
||||
r = np.full(len(arr), np.nan)
|
||||
k = 2/(period+1)
|
||||
r[period-1] = np.mean(arr[:period])
|
||||
for i in range(period, len(arr)):
|
||||
r[i] = arr[i]*k + r[i-1]*(1-k)
|
||||
return r
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# S1 BEST: Squeeze Breakout ETH 1h (BBw=14, sq=0.8, brk=3)
|
||||
# =====================================================================
|
||||
def run_s1_squeeze(asset, tf):
|
||||
df = load_data(asset, tf)
|
||||
c, h, l, v = df["close"].values, df["high"].values, df["low"].values, df["volume"].values
|
||||
n = len(c)
|
||||
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||
kcr = keltner_ratio(c, h, l, 14)
|
||||
|
||||
yearly = {}
|
||||
in_sq = False
|
||||
sq_start = 0
|
||||
|
||||
for i in range(15, n):
|
||||
if np.isnan(kcr[i]):
|
||||
continue
|
||||
is_sq = kcr[i] < 0.8
|
||||
if is_sq and not in_sq:
|
||||
in_sq = True
|
||||
sq_start = i
|
||||
elif not is_sq and in_sq:
|
||||
in_sq = False
|
||||
if i - sq_start < 5 or i + 3 >= n:
|
||||
continue
|
||||
first_ret = (c[i] - c[i-1]) / c[i-1]
|
||||
if abs(first_ret) < 0.001:
|
||||
continue
|
||||
direction = 1 if first_ret > 0 else -1
|
||||
actual = (c[i+2] - c[i-1]) / c[i-1]
|
||||
trade_ret = actual * direction
|
||||
net = trade_ret * LEVERAGE - FEE_PERP * LEVERAGE
|
||||
|
||||
year = ts.iloc[i].year
|
||||
if year not in yearly:
|
||||
yearly[year] = {"pnls": [], "wins": 0, "total": 0}
|
||||
yearly[year]["pnls"].append(net)
|
||||
yearly[year]["total"] += 1
|
||||
if trade_ret > 0:
|
||||
yearly[year]["wins"] += 1
|
||||
|
||||
return yearly
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# S1 BEST ALT: Squeeze+ML hybrid ETH 15m
|
||||
# =====================================================================
|
||||
# Troppo complesso da ricalcolare (serve ML training). Uso i dati S1 squeeze puro.
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# S2 BEST: VRP ETH 48h (con IV stimata, unico disponibile su 8 anni)
|
||||
# =====================================================================
|
||||
def run_s2_vrp(asset, dte=48):
|
||||
df = load_data(asset, "1h")
|
||||
c = df["close"].values
|
||||
n = len(c)
|
||||
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||
rv_24 = rv_ann(c, 24)
|
||||
rv_168 = rv_ann(c, 168)
|
||||
|
||||
yearly = {}
|
||||
for i in range(170, n - dte):
|
||||
if ts.iloc[i].hour != 8:
|
||||
continue
|
||||
rv_s, rv_l = rv_24[i], rv_168[i]
|
||||
if np.isnan(rv_s) or np.isnan(rv_l) or rv_s < 0.05 or rv_l < 0.05:
|
||||
continue
|
||||
regime = rv_s / rv_l
|
||||
iv_pf = 0.9 if regime > 2 else (1.0 if regime > 1.5 else (1.1 if regime > 1 else 1.2))
|
||||
iv = rv_l * iv_pf
|
||||
prem = iv * np.sqrt(dte/(24*365)) * 0.8
|
||||
spot = c[i]
|
||||
move = abs(c[min(i+dte, n-1)] - spot) / spot
|
||||
pos = 0.10
|
||||
raw = (prem - move) * pos if move <= prem else max(-(move-prem)*pos, -pos*0.05)
|
||||
net = raw - FEE_OPT * pos
|
||||
|
||||
year = ts.iloc[i].year
|
||||
if year not in yearly:
|
||||
yearly[year] = {"pnls": [], "wins": 0, "total": 0}
|
||||
yearly[year]["pnls"].append(net)
|
||||
yearly[year]["total"] += 1
|
||||
if raw > 0:
|
||||
yearly[year]["wins"] += 1
|
||||
return yearly
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# S2 BEST PERPETUAL: Multi-TF 15m+1h BTC
|
||||
# =====================================================================
|
||||
def run_s2_multitf(asset):
|
||||
df_1h = load_data(asset, "1h")
|
||||
df_15m = load_data(asset, "15m")
|
||||
c1h = df_1h["close"].values
|
||||
ts1h = pd.to_datetime(df_1h["timestamp"], unit="ms", utc=True)
|
||||
c15 = df_15m["close"].values
|
||||
ts15 = df_15m["timestamp"].values
|
||||
n15 = len(c15)
|
||||
|
||||
ema_50 = ema(c1h, 50)
|
||||
rsi_15m = rsi(c15, 14)
|
||||
|
||||
yearly = {}
|
||||
daily_done = set()
|
||||
|
||||
for i in range(100, n15 - 12):
|
||||
ts_dt = pd.Timestamp(ts15[i], unit="ms", tz="UTC")
|
||||
day = ts_dt.strftime("%Y-%m-%d")
|
||||
if day in daily_done:
|
||||
continue
|
||||
if rsi_15m[i] > 35 and rsi_15m[i] < 65:
|
||||
continue
|
||||
h_idx = np.searchsorted(ts1h.values.astype("int64"), ts15[i]) - 1
|
||||
if h_idx < 50 or h_idx >= len(c1h) or np.isnan(ema_50[h_idx]):
|
||||
continue
|
||||
|
||||
direction = None
|
||||
if rsi_15m[i] < 30 and c1h[h_idx] > ema_50[h_idx]:
|
||||
direction = "long"
|
||||
elif rsi_15m[i] > 70 and c1h[h_idx] < ema_50[h_idx]:
|
||||
direction = "short"
|
||||
if direction is None:
|
||||
continue
|
||||
|
||||
entry = c15[i]
|
||||
exit_price = c15[min(i+12, n15-1)]
|
||||
trade_ret = (exit_price-entry)/entry if direction == "long" else (entry-exit_price)/entry
|
||||
net = trade_ret * LEVERAGE - FEE_PERP * LEVERAGE
|
||||
|
||||
year = ts_dt.year
|
||||
if year not in yearly:
|
||||
yearly[year] = {"pnls": [], "wins": 0, "total": 0}
|
||||
yearly[year]["pnls"].append(net)
|
||||
yearly[year]["total"] += 1
|
||||
if trade_ret > 0:
|
||||
yearly[year]["wins"] += 1
|
||||
daily_done.add(day)
|
||||
return yearly
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# REPORT
|
||||
# =====================================================================
|
||||
strategies = {
|
||||
"S1: Squeeze BTC 1h": run_s1_squeeze("BTC", "1h"),
|
||||
"S1: Squeeze ETH 1h": run_s1_squeeze("ETH", "1h"),
|
||||
"S1: Squeeze ETH 15m": run_s1_squeeze("ETH", "15m"),
|
||||
"S2: VRP ETH 48h (IV est)": run_s2_vrp("ETH", 48),
|
||||
"S2: VRP BTC 48h (IV est)": run_s2_vrp("BTC", 48),
|
||||
"S2: MultiTF BTC 15m+1h": run_s2_multitf("BTC"),
|
||||
"S2: MultiTF ETH 15m+1h": run_s2_multitf("ETH"),
|
||||
}
|
||||
|
||||
all_years = sorted(set(y for v in strategies.values() for y in v))
|
||||
|
||||
print("=" * 120)
|
||||
print(" MIGLIORI STRATEGIE — ANDAMENTO PER ANNO")
|
||||
print(" Fee reali. PnL su €1000 flat (no compounding). Dati OHLCV reali 2018-2026.")
|
||||
print(" ⚠ VRP usa IV STIMATA (non reale) — fidarsi solo dei dati perpetual per backtest lungo")
|
||||
print("=" * 120)
|
||||
|
||||
# Header
|
||||
hdr = f" {'Anno':>6s}"
|
||||
for name in strategies:
|
||||
short = name.split(": ")[1][:18]
|
||||
hdr += f" | {short:>18s}"
|
||||
print(hdr)
|
||||
print(f" {'-' * (len(hdr) - 2)}")
|
||||
|
||||
# Per anno: accuracy / PnL totale
|
||||
for year in all_years:
|
||||
row_acc = f" {year:>6d}"
|
||||
row_pnl = f" {'':>6s}"
|
||||
for name, yearly in strategies.items():
|
||||
if year in yearly:
|
||||
d = yearly[year]
|
||||
acc = d["wins"]/d["total"]*100 if d["total"] > 0 else 0
|
||||
pnl = sum(d["pnls"]) * INITIAL
|
||||
tag = "▓" if acc >= 75 else "▒" if acc >= 65 else "░" if acc >= 55 else " "
|
||||
row_acc += f" | {acc:>5.1f}% {tag} {d['total']:>3d}t"
|
||||
row_pnl += f" | €{pnl:>+8.0f} "
|
||||
else:
|
||||
row_acc += f" | {'—':>18s}"
|
||||
row_pnl += f" | {'':>18s}"
|
||||
print(row_acc)
|
||||
print(row_pnl)
|
||||
|
||||
# Totali
|
||||
print(f" {'-' * (len(hdr) - 2)}")
|
||||
row_tot = f" {'TOT':>6s}"
|
||||
for name, yearly in strategies.items():
|
||||
all_pnls = [p for d in yearly.values() for p in d["pnls"]]
|
||||
all_wins = sum(d["wins"] for d in yearly.values())
|
||||
all_total = sum(d["total"] for d in yearly.values())
|
||||
acc = all_wins/all_total*100 if all_total > 0 else 0
|
||||
pnl = sum(all_pnls) * INITIAL
|
||||
row_tot += f" | {acc:>5.1f}% {all_total:>4d}t"
|
||||
print(row_tot)
|
||||
|
||||
row_pnl_tot = f" {'€TOT':>6s}"
|
||||
for name, yearly in strategies.items():
|
||||
all_pnls = [p for d in yearly.values() for p in d["pnls"]]
|
||||
pnl = sum(all_pnls) * INITIAL
|
||||
row_pnl_tot += f" | €{pnl:>+8.0f} "
|
||||
print(row_pnl_tot)
|
||||
|
||||
# Compounding
|
||||
print(f"\n {'':>6s}", end="")
|
||||
for name in strategies:
|
||||
short = name.split(": ")[1][:18]
|
||||
print(f" | {short:>18s}", end="")
|
||||
print()
|
||||
|
||||
row_comp = f" {'COMP':>6s}"
|
||||
for name, yearly in strategies.items():
|
||||
cap = float(INITIAL)
|
||||
for year in sorted(yearly):
|
||||
for pnl in yearly[year]["pnls"]:
|
||||
cap += cap * pnl
|
||||
cap = max(cap, 10)
|
||||
row_comp += f" | €{cap:>12,.0f} "
|
||||
print(row_comp)
|
||||
|
||||
# Drawdown
|
||||
row_dd = f" {'MAXDD':>6s}"
|
||||
for name, yearly in strategies.items():
|
||||
cap = float(INITIAL)
|
||||
peak = cap
|
||||
mdd = 0
|
||||
for year in sorted(yearly):
|
||||
for pnl in yearly[year]["pnls"]:
|
||||
cap += cap * pnl
|
||||
cap = max(cap, 10)
|
||||
if cap > peak: peak = cap
|
||||
dd = (peak - cap) / peak
|
||||
mdd = max(mdd, dd)
|
||||
row_dd += f" | {mdd*100:>12.1f}% "
|
||||
print(row_dd)
|
||||
|
||||
# Legenda
|
||||
print(f"\n Legenda: ▓ ≥75% acc ▒ ≥65% acc ░ ≥55% acc")
|
||||
print(f" ⚠ S2 VRP: IV stimata (rv_long × 1.0-1.2), NON dati reali opzioni")
|
||||
print(f" S1 Squeeze e S2 MultiTF: dati OHLCV reali al 100%")
|
||||
@@ -0,0 +1,559 @@
|
||||
"""Analisi finale — S1 (squeeze puro) vs Script 13 (squeeze+ML GBM).
|
||||
Metriche: PnL, num trades, DD max, tempo medio a mercato, descrizione.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from sklearn.ensemble import GradientBoostingClassifier
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
from src.data.downloader import load_data
|
||||
from src.fractal.patterns import encode_candles
|
||||
|
||||
FEE_PERP = 0.002
|
||||
FEE_ML = 0.001
|
||||
INITIAL = 1000
|
||||
LEVERAGE = 3
|
||||
|
||||
TF_MINUTES = {"1m": 1, "5m": 5, "15m": 15, "1h": 60, "4h": 240, "1d": 1440}
|
||||
|
||||
|
||||
# ── helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
def keltner_ratio(close, high, low, window=14):
|
||||
n = len(close)
|
||||
r = np.full(n, np.nan)
|
||||
for i in range(window, n):
|
||||
wc, wh, wl = close[i-window:i], high[i-window:i], low[i-window:i]
|
||||
ma = np.mean(wc)
|
||||
bb_std = np.std(wc)
|
||||
tr = np.maximum(wh-wl, np.maximum(np.abs(wh-np.roll(wc,1)), np.abs(wl-np.roll(wc,1))))
|
||||
atr = np.mean(tr[1:])
|
||||
kc = (ma+1.5*atr)-(ma-1.5*atr)
|
||||
bb = (ma+2*bb_std)-(ma-2*bb_std)
|
||||
if kc > 0:
|
||||
r[i] = bb/kc
|
||||
return r
|
||||
|
||||
|
||||
def detect_squeezes(close, high, low, kcr, sq_thr=0.8, min_dur=5):
|
||||
events = []
|
||||
in_sq = False
|
||||
sq_start = 0
|
||||
for i in range(1, len(close)):
|
||||
if np.isnan(kcr[i]):
|
||||
continue
|
||||
is_sq = kcr[i] < sq_thr
|
||||
if is_sq and not in_sq:
|
||||
in_sq = True
|
||||
sq_start = i
|
||||
elif not is_sq and in_sq:
|
||||
in_sq = False
|
||||
dur = i - sq_start
|
||||
if dur < min_dur:
|
||||
continue
|
||||
events.append({"idx": i, "dur": dur, "sq_start": sq_start,
|
||||
"avg_vol_squeeze": np.mean(close[sq_start:i]),
|
||||
"kcr_at_release": kcr[i]})
|
||||
return events
|
||||
|
||||
|
||||
def _build_result(yearly, capital, max_dd, all_t, all_w, time_pct, avg_dur_h):
|
||||
acc = all_w / all_t * 100
|
||||
tot_pnl = sum(p for d in yearly.values() for p in d["pnls"])
|
||||
years_active = len(yearly)
|
||||
all_pnls = [p for d in yearly.values() for p in d["pnls"]]
|
||||
sharpe = np.mean(all_pnls) / np.std(all_pnls) * np.sqrt(252) if len(all_pnls) > 1 and np.std(all_pnls) > 0 else 0
|
||||
|
||||
year_details = {}
|
||||
for y in sorted(yearly):
|
||||
d = yearly[y]
|
||||
ya = d["w"] / d["t"] * 100 if d["t"] > 0 else 0
|
||||
yp = sum(d["pnls"])
|
||||
year_details[y] = {"trades": d["t"], "acc": ya, "pnl": yp}
|
||||
|
||||
valid_years = {y: d for y, d in year_details.items() if d["trades"] >= 10}
|
||||
if valid_years:
|
||||
worst_y = min(valid_years, key=lambda y: valid_years[y]["acc"])
|
||||
worst_acc = valid_years[worst_y]["acc"]
|
||||
elif year_details:
|
||||
worst_y = min(year_details, key=lambda y: year_details[y]["acc"])
|
||||
worst_acc = year_details[worst_y]["acc"]
|
||||
else:
|
||||
worst_y = "N/A"
|
||||
worst_acc = 0
|
||||
|
||||
daily_pnl = tot_pnl / (years_active * 365) if years_active > 0 else 0
|
||||
|
||||
return {
|
||||
"trades": all_t, "acc": acc, "pnl": tot_pnl, "capital": capital,
|
||||
"max_dd": max_dd * 100, "sharpe": sharpe, "daily_pnl": daily_pnl,
|
||||
"time_in_market_pct": time_pct, "avg_dur_h": avg_dur_h,
|
||||
"years_active": years_active, "worst_year": str(worst_y),
|
||||
"worst_acc": worst_acc, "year_details": year_details,
|
||||
}
|
||||
|
||||
|
||||
# ── S1: Squeeze breakout puro ────────────────────────────────────────
|
||||
|
||||
def run_s1_squeeze(asset, tf, hold=3):
|
||||
df = load_data(asset, tf)
|
||||
c, h, l, v = df["close"].values, df["high"].values, df["low"].values, df["volume"].values
|
||||
n = len(c)
|
||||
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||
kcr = keltner_ratio(c, h, l, 14)
|
||||
events = detect_squeezes(c, h, l, kcr)
|
||||
|
||||
yearly = {}
|
||||
capital = float(INITIAL)
|
||||
peak = capital
|
||||
max_dd = 0
|
||||
total_bars = 0
|
||||
|
||||
for ev in events:
|
||||
i = ev["idx"]
|
||||
if i + hold + 1 >= n:
|
||||
continue
|
||||
first_ret = (c[i] - c[i-1]) / c[i-1] if c[i-1] > 0 else 0
|
||||
if abs(first_ret) < 0.001:
|
||||
continue
|
||||
direction = 1 if first_ret > 0 else -1
|
||||
entry = c[i-1]
|
||||
exit_price = c[min(i + hold - 1, n - 1)]
|
||||
actual = (exit_price - entry) / entry * direction
|
||||
net = actual * LEVERAGE - FEE_PERP * LEVERAGE
|
||||
|
||||
capital += capital * 0.15 * net
|
||||
capital = max(capital, 10)
|
||||
if capital > peak: peak = capital
|
||||
dd = (peak - capital) / peak
|
||||
max_dd = max(max_dd, dd)
|
||||
total_bars += hold
|
||||
|
||||
year = ts.iloc[i].year
|
||||
if year not in yearly:
|
||||
yearly[year] = {"w": 0, "t": 0, "pnls": []}
|
||||
yearly[year]["t"] += 1
|
||||
if actual > 0: yearly[year]["w"] += 1
|
||||
yearly[year]["pnls"].append(net * INITIAL)
|
||||
|
||||
all_t = sum(d["t"] for d in yearly.values())
|
||||
all_w = sum(d["w"] for d in yearly.values())
|
||||
if all_t == 0: return None
|
||||
return _build_result(yearly, capital, max_dd, all_t, all_w,
|
||||
total_bars / n * 100, hold * TF_MINUTES.get(tf, 60) / 60)
|
||||
|
||||
|
||||
def run_s1_antifake_vol(asset, tf, hold=3):
|
||||
df = load_data(asset, tf)
|
||||
c, h, l, v = df["close"].values, df["high"].values, df["low"].values, df["volume"].values
|
||||
n = len(c)
|
||||
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||
kcr = keltner_ratio(c, h, l, 14)
|
||||
events = detect_squeezes(c, h, l, kcr)
|
||||
|
||||
yearly = {}
|
||||
capital = float(INITIAL)
|
||||
peak = capital
|
||||
max_dd = 0
|
||||
total_bars = 0
|
||||
|
||||
for ev in events:
|
||||
i = ev["idx"]
|
||||
if i + hold + 1 >= n:
|
||||
continue
|
||||
first_ret = (c[i] - c[i-1]) / c[i-1] if c[i-1] > 0 else 0
|
||||
if abs(first_ret) < 0.001:
|
||||
continue
|
||||
br = h[i] - l[i]
|
||||
if br > 0:
|
||||
if c[i] > c[i-1]:
|
||||
if (h[i] - c[i]) / br > 0.6:
|
||||
continue
|
||||
else:
|
||||
if (c[i] - l[i]) / br > 0.6:
|
||||
continue
|
||||
avg_v = np.mean(v[ev["sq_start"]:i])
|
||||
if avg_v > 0 and v[i] <= avg_v * 1.3:
|
||||
continue
|
||||
|
||||
direction = 1 if first_ret > 0 else -1
|
||||
entry = c[i-1]
|
||||
exit_price = c[min(i + hold - 1, n - 1)]
|
||||
actual = (exit_price - entry) / entry * direction
|
||||
net = actual * LEVERAGE - FEE_PERP * LEVERAGE
|
||||
capital += capital * 0.15 * net
|
||||
capital = max(capital, 10)
|
||||
if capital > peak: peak = capital
|
||||
dd = (peak - capital) / peak
|
||||
max_dd = max(max_dd, dd)
|
||||
total_bars += hold
|
||||
|
||||
year = ts.iloc[i].year
|
||||
if year not in yearly:
|
||||
yearly[year] = {"w": 0, "t": 0, "pnls": []}
|
||||
yearly[year]["t"] += 1
|
||||
if actual > 0: yearly[year]["w"] += 1
|
||||
yearly[year]["pnls"].append(net * INITIAL)
|
||||
|
||||
all_t = sum(d["t"] for d in yearly.values())
|
||||
all_w = sum(d["w"] for d in yearly.values())
|
||||
if all_t == 0: return None
|
||||
return _build_result(yearly, capital, max_dd, all_t, all_w,
|
||||
total_bars / n * 100, hold * TF_MINUTES.get(tf, 60) / 60)
|
||||
|
||||
|
||||
# ── Script 13: Squeeze + ML ibrida (GBM walk-forward) ────────────────
|
||||
|
||||
def build_features_at(df, i, squeeze_info):
|
||||
if i < 100:
|
||||
return None
|
||||
o = df["open"].values
|
||||
h = df["high"].values
|
||||
l = df["low"].values
|
||||
c = df["close"].values
|
||||
v = df["volume"].values
|
||||
feats = []
|
||||
for w in [12, 24, 48]:
|
||||
win_c = c[i-w:i]
|
||||
win_o = o[i-w:i]
|
||||
win_h = h[i-w:i]
|
||||
win_l = l[i-w:i]
|
||||
win_v = v[i-w:i]
|
||||
mn, mx = win_l.min(), max(win_h.max(), win_c.max())
|
||||
rng = mx - mn if mx - mn > 0 else 1e-10
|
||||
total = win_h - win_l
|
||||
total = np.where(total == 0, 1e-10, total)
|
||||
body = np.abs(win_c - win_o) / total
|
||||
direction = np.sign(win_c - win_o)
|
||||
log_c = np.log(np.where(win_c == 0, 1e-10, win_c))
|
||||
rets = np.diff(log_c)
|
||||
v_mean = np.mean(win_v)
|
||||
feats.extend([
|
||||
np.mean(rets) if len(rets) > 0 else 0,
|
||||
np.std(rets) if len(rets) > 0 else 0,
|
||||
np.sum(rets) if len(rets) > 0 else 0,
|
||||
float(pd.Series(rets).skew()) if len(rets) > 2 else 0,
|
||||
float(pd.Series(rets).kurtosis()) if len(rets) > 3 else 0,
|
||||
np.mean(body), np.std(body),
|
||||
np.mean(direction), np.mean(direction[-min(3, w):]),
|
||||
(win_c[-1] - mn) / rng,
|
||||
win_v[-1] / v_mean if v_mean > 0 else 1,
|
||||
np.corrcoef(rets[:-1], rets[1:])[0, 1] if len(rets) > 1 and np.std(rets) > 0 else 0,
|
||||
])
|
||||
sq = squeeze_info
|
||||
feats.extend([
|
||||
sq["dur"], sq["dur"] / 24, sq["kcr_at_release"],
|
||||
v[i-1] / sq["avg_vol_squeeze"] if sq["avg_vol_squeeze"] > 0 else 1,
|
||||
np.mean(v[i:min(i+3, len(v))]) / sq["avg_vol_squeeze"] if sq["avg_vol_squeeze"] > 0 else 1,
|
||||
])
|
||||
h48 = np.max(h[max(0, i-48):i])
|
||||
l48 = np.min(l[max(0, i-48):i])
|
||||
r48 = h48 - l48
|
||||
feats.append((c[i-1] - l48) / r48 if r48 > 0 else 0.5)
|
||||
tr = np.maximum(h[i-14:i] - l[i-14:i],
|
||||
np.maximum(np.abs(h[i-14:i] - np.roll(c[i-14:i], 1)),
|
||||
np.abs(l[i-14:i] - np.roll(c[i-14:i], 1))))
|
||||
atr = np.mean(tr[1:])
|
||||
feats.append(atr / c[i-1] if c[i-1] > 0 else 0)
|
||||
first_ret = (c[i] - c[i-1]) / c[i-1] if c[i-1] > 0 else 0
|
||||
feats.append(first_ret)
|
||||
return np.nan_to_num(np.array(feats), nan=0, posinf=1e6, neginf=-1e6)
|
||||
|
||||
|
||||
def run_s13_hybrid(asset, tf, bb_w, sq_thr, brk_bars, leverage, pos_pct, ml_thr):
|
||||
df = load_data(asset, tf)
|
||||
close = df["close"].values
|
||||
high = df["high"].values
|
||||
low = df["low"].values
|
||||
volume = df["volume"].values
|
||||
n = len(df)
|
||||
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||
|
||||
kcr = keltner_ratio(close, high, low, bb_w)
|
||||
events = detect_squeezes(close, high, low, kcr, sq_thr)
|
||||
|
||||
X_all, y_all, ev_all = [], [], []
|
||||
for ev in events:
|
||||
i = ev["idx"]
|
||||
if i + brk_bars >= n or i < 100:
|
||||
continue
|
||||
feats = build_features_at(df, i, ev)
|
||||
if feats is None:
|
||||
continue
|
||||
actual_ret = (close[i + brk_bars - 1] - close[i - 1]) / close[i - 1]
|
||||
X_all.append(feats)
|
||||
y_all.append(1 if actual_ret > 0 else 0)
|
||||
ev_all.append(ev)
|
||||
|
||||
if len(X_all) < 50:
|
||||
return None
|
||||
|
||||
X = np.array(X_all)
|
||||
y = np.array(y_all)
|
||||
|
||||
TRAIN_SIZE = max(int(len(X) * 0.5), 50)
|
||||
STEP_SIZE = max(int(len(X) * 0.1), 10)
|
||||
|
||||
yearly = {}
|
||||
capital = float(INITIAL)
|
||||
peak = capital
|
||||
max_dd = 0
|
||||
total_bars = 0
|
||||
all_t = 0
|
||||
all_w = 0
|
||||
|
||||
start = 0
|
||||
while start + TRAIN_SIZE + STEP_SIZE <= len(X):
|
||||
train_end = start + TRAIN_SIZE
|
||||
test_end = min(train_end + STEP_SIZE, len(X))
|
||||
X_tr, y_tr = X[start:train_end], y[start:train_end]
|
||||
X_te = X[train_end:test_end]
|
||||
|
||||
if len(np.unique(y_tr)) < 2:
|
||||
start += STEP_SIZE
|
||||
continue
|
||||
|
||||
scaler = StandardScaler()
|
||||
X_tr_s = scaler.fit_transform(X_tr)
|
||||
X_te_s = scaler.transform(X_te)
|
||||
|
||||
model = GradientBoostingClassifier(
|
||||
n_estimators=150, max_depth=4, min_samples_leaf=10,
|
||||
learning_rate=0.05, subsample=0.8, random_state=42,
|
||||
)
|
||||
model.fit(X_tr_s, y_tr)
|
||||
|
||||
up_idx = list(model.classes_).index(1) if 1 in model.classes_ else -1
|
||||
if up_idx < 0:
|
||||
start += STEP_SIZE
|
||||
continue
|
||||
|
||||
for j in range(len(X_te)):
|
||||
proba = model.predict_proba(X_te_s[j:j+1])[0]
|
||||
p_up = proba[up_idx]
|
||||
|
||||
ev = ev_all[train_end + j]
|
||||
i = ev["idx"]
|
||||
actual_ret = (close[i + brk_bars - 1] - close[i - 1]) / close[i - 1]
|
||||
|
||||
direction = None
|
||||
if p_up >= ml_thr:
|
||||
direction = 1
|
||||
elif p_up <= (1 - ml_thr):
|
||||
direction = -1
|
||||
if direction is None:
|
||||
continue
|
||||
|
||||
is_correct = (direction == 1 and actual_ret > 0) or (direction == -1 and actual_ret < 0)
|
||||
trade_ret = actual_ret * direction
|
||||
net = trade_ret * leverage - FEE_ML * 2 * leverage
|
||||
capital += capital * pos_pct * net
|
||||
capital = max(capital, 10)
|
||||
if capital > peak: peak = capital
|
||||
dd = (peak - capital) / peak
|
||||
max_dd = max(max_dd, dd)
|
||||
total_bars += brk_bars
|
||||
|
||||
all_t += 1
|
||||
if is_correct: all_w += 1
|
||||
|
||||
year = ts.iloc[i].year
|
||||
if year not in yearly:
|
||||
yearly[year] = {"w": 0, "t": 0, "pnls": []}
|
||||
yearly[year]["t"] += 1
|
||||
if is_correct: yearly[year]["w"] += 1
|
||||
yearly[year]["pnls"].append(net * INITIAL)
|
||||
|
||||
start += STEP_SIZE
|
||||
|
||||
if all_t == 0:
|
||||
return None
|
||||
return _build_result(yearly, capital, max_dd, all_t, all_w,
|
||||
total_bars / n * 100, brk_bars * TF_MINUTES.get(tf, 60) / 60)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# ESECUZIONE
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
print("Calcolo in corso...\n")
|
||||
|
||||
strategies = []
|
||||
|
||||
def add(name, desc, cat, result):
|
||||
if result and result["trades"] >= 20:
|
||||
strategies.append({"name": name, "desc": desc, "cat": cat, **result})
|
||||
|
||||
# ── S1: Squeeze puro ────────────────────────────────────────────
|
||||
add("S1 Squeeze BTC 15m", "Squeeze breakout puro, BBw=14, hold 3×15m, leva 3x",
|
||||
"S1", run_s1_squeeze("BTC", "15m"))
|
||||
add("S1 Squeeze ETH 15m", "Squeeze breakout puro, BBw=14, hold 3×15m, leva 3x",
|
||||
"S1", run_s1_squeeze("ETH", "15m"))
|
||||
add("S1 Squeeze BTC 1h", "Squeeze breakout puro, BBw=14, hold 3×1h, leva 3x",
|
||||
"S1", run_s1_squeeze("BTC", "1h"))
|
||||
add("S1 Squeeze ETH 1h", "Squeeze breakout puro, BBw=14, hold 3×1h, leva 3x",
|
||||
"S1", run_s1_squeeze("ETH", "1h"))
|
||||
add("S1 AF+Vol BTC 15m", "Squeeze + antifakeout + volume confirm >1.3× media",
|
||||
"S1", run_s1_antifake_vol("BTC", "15m"))
|
||||
add("S1 AF+Vol ETH 15m", "Squeeze + antifakeout + volume confirm >1.3× media",
|
||||
"S1", run_s1_antifake_vol("ETH", "15m"))
|
||||
add("S1 AF+Vol BTC 1h", "Squeeze + antifakeout + volume confirm >1.3× media",
|
||||
"S1", run_s1_antifake_vol("BTC", "1h"))
|
||||
add("S1 AF+Vol ETH 1h", "Squeeze + antifakeout + volume confirm >1.3× media",
|
||||
"S1", run_s1_antifake_vol("ETH", "1h"))
|
||||
|
||||
# ── Script 13: Squeeze + ML (GBM walk-forward) ─────────────────
|
||||
print(" Training ML models...")
|
||||
add("S13 ETH 15m bb14 ml70", "Squeeze+GBM walk-forward, BBw=14 sq=0.8 ml≥0.70, 3x leva 15% pos",
|
||||
"S13", run_s13_hybrid("ETH", "15m", 14, 0.8, 3, 3, 0.15, 0.70))
|
||||
add("S13 ETH 15m bb14 ml65", "Squeeze+GBM walk-forward, BBw=14 sq=0.8 ml≥0.65, 3x leva 15% pos",
|
||||
"S13", run_s13_hybrid("ETH", "15m", 14, 0.8, 3, 3, 0.15, 0.65))
|
||||
add("S13 ETH 15m bb20 ml70", "Squeeze+GBM walk-forward, BBw=20 sq=0.9 ml≥0.70, 3x leva 15% pos",
|
||||
"S13", run_s13_hybrid("ETH", "15m", 20, 0.9, 3, 3, 0.15, 0.70))
|
||||
add("S13 BTC 15m bb14 ml70", "Squeeze+GBM walk-forward, BBw=14 sq=0.9 ml≥0.70, 3x leva 15% pos",
|
||||
"S13", run_s13_hybrid("BTC", "15m", 14, 0.9, 3, 3, 0.15, 0.70))
|
||||
add("S13 BTC 15m bb14 ml65", "Squeeze+GBM walk-forward, BBw=14 sq=0.9 ml≥0.65, 3x leva 15% pos",
|
||||
"S13", run_s13_hybrid("BTC", "15m", 14, 0.9, 3, 3, 0.15, 0.65))
|
||||
add("S13 BTC 1h bb14 ml70", "Squeeze+GBM walk-forward, BBw=14 sq=0.8 ml≥0.70, 3x leva 20% pos",
|
||||
"S13", run_s13_hybrid("BTC", "1h", 14, 0.8, 3, 3, 0.20, 0.70))
|
||||
add("S13 BTC 1h bb14 ml65", "Squeeze+GBM walk-forward, BBw=14 sq=0.8 ml≥0.65, 3x leva 20% pos",
|
||||
"S13", run_s13_hybrid("BTC", "1h", 14, 0.8, 3, 3, 0.20, 0.65))
|
||||
add("S13 ETH 1h bb14 ml70", "Squeeze+GBM walk-forward, BBw=14 sq=0.8 ml≥0.70, 3x leva 20% pos",
|
||||
"S13", run_s13_hybrid("ETH", "1h", 14, 0.8, 3, 3, 0.20, 0.70))
|
||||
|
||||
strategies.sort(key=lambda x: x["acc"], reverse=True)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# TABELLA 1: Classifica
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
W = 150
|
||||
print("=" * W)
|
||||
print(" S1 (SQUEEZE PURO) vs S13 (SQUEEZE + GBM) — CLASSIFICA FINALE")
|
||||
print(f" Fee: 0.2% RT. Dati OHLCV reali 2018-2026. Position 15%. Leva 3x.")
|
||||
print("=" * W)
|
||||
hdr = (f" {'#':>2s} {'Cat':>3s} {'Nome':<26s} {'Trades':>6s} {'Acc':>6s} "
|
||||
f"{'PnL€':>9s} {'DD%':>6s} {'€/day':>7s} {'Sharpe':>7s} "
|
||||
f"{'Mkt%':>5s} {'Dur':>6s} {'Worst':>12s} {'Anni':>4s}")
|
||||
print(hdr)
|
||||
print(f" {'─'*(W-4)}")
|
||||
|
||||
for idx, s in enumerate(strategies, 1):
|
||||
worst = f"{s['worst_year']}({s['worst_acc']:.0f}%)"
|
||||
dur_str = f"{s['avg_dur_h']:.0f}h" if s['avg_dur_h'] >= 1 else f"{s['avg_dur_h']*60:.0f}m"
|
||||
tag = " ★★" if s["acc"] >= 78 else " ★" if s["acc"] >= 76 else ""
|
||||
print(f" {idx:>2d} {s['cat']:>3s} {s['name']:<26s} {s['trades']:>6d} {s['acc']:>5.1f}% "
|
||||
f"€{s['pnl']:>+8.0f} {s['max_dd']:>5.1f}% {s['daily_pnl']:>+6.2f} {s['sharpe']:>7.2f} "
|
||||
f"{s['time_in_market_pct']:>4.1f}% {dur_str:>6s} {worst:>12s} {s['years_active']:>4d}{tag}")
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# TABELLA 2: Descrizione
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
print(f"\n\n{'=' * W}")
|
||||
print(" DESCRIZIONE")
|
||||
print(f"{'=' * W}")
|
||||
print(f" {'#':>2s} {'Nome':<26s} {'Descrizione'}")
|
||||
print(f" {'─'*(W-4)}")
|
||||
for idx, s in enumerate(strategies, 1):
|
||||
print(f" {idx:>2d} {s['name']:<26s} {s['desc']}")
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# TABELLA 3: Breakdown per anno
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
top_n = min(12, len(strategies))
|
||||
top = strategies[:top_n]
|
||||
all_years = sorted(set(y for s in top for y in s["year_details"]))
|
||||
|
||||
print(f"\n\n{'=' * W}")
|
||||
print(f" BREAKDOWN PER ANNO — TOP {top_n} (accuracy% / trades)")
|
||||
print(f"{'=' * W}")
|
||||
|
||||
header = f" {'Nome':<26s}"
|
||||
for y in all_years:
|
||||
header += f" {y:>10d}"
|
||||
print(header)
|
||||
print(f" {'─'*(W-4)}")
|
||||
|
||||
for s in top:
|
||||
line = f" {s['name']:<26s}"
|
||||
for y in all_years:
|
||||
if y in s["year_details"]:
|
||||
d = s["year_details"][y]
|
||||
line += f" {d['acc']:>4.0f}%/{d['trades']:<4d}"
|
||||
else:
|
||||
line += f" {'—':>10s}"
|
||||
print(line)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# TABELLA 4: Robustezza
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
print(f"\n\n{'=' * W}")
|
||||
print(f" ANALISI ROBUSTEZZA")
|
||||
print(f"{'=' * W}")
|
||||
print(f" {'#':>2s} {'Nome':<26s} {'MinAcc':>7s} {'MaxAcc':>7s} {'Spread':>7s} "
|
||||
f"{'AnniOK':>7s} {'€/trade':>8s} {'Verdict':<12s}")
|
||||
print(f" {'─'*90}")
|
||||
|
||||
for idx, s in enumerate(strategies, 1):
|
||||
yd = s["year_details"]
|
||||
valid = {y: d for y, d in yd.items() if d["trades"] >= 10}
|
||||
accs = [d["acc"] for d in (valid if valid else yd).values()]
|
||||
if not accs:
|
||||
continue
|
||||
min_a, max_a = min(accs), max(accs)
|
||||
spread = max_a - min_a
|
||||
years_ok = sum(1 for a in accs if a >= 70)
|
||||
avg_pnl = s["pnl"] / s["trades"] if s["trades"] > 0 else 0
|
||||
n_valid = len(valid if valid else yd)
|
||||
|
||||
if n_valid < 4:
|
||||
verdict = "⚠ CORTO"
|
||||
elif min_a < 60:
|
||||
verdict = "⚠ FRAGILE"
|
||||
elif min_a >= 72 and s["acc"] >= 77:
|
||||
verdict = "✅ SOLIDO"
|
||||
elif min_a >= 65 and s["acc"] >= 74:
|
||||
verdict = "~ BUONO"
|
||||
else:
|
||||
verdict = "~ OK"
|
||||
|
||||
print(f" {idx:>2d} {s['name']:<26s} {min_a:>6.1f}% {max_a:>6.1f}% {spread:>6.1f}% "
|
||||
f"{years_ok:>3d}/{n_valid:<3d} €{avg_pnl:>+7.1f} {verdict:<12s}")
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# VERDETTO
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
print(f"\n\n{'=' * W}")
|
||||
print(f" VERDETTO FINALE")
|
||||
print(f"{'=' * W}")
|
||||
|
||||
solidi = [s for s in strategies if s["trades"] >= 200 and s["years_active"] >= 5 and s["worst_acc"] >= 65]
|
||||
solidi_s1 = [s for s in solidi if s["cat"] == "S1"]
|
||||
solidi_ml = [s for s in solidi if s["cat"] == "S13"]
|
||||
solidi_s1.sort(key=lambda x: x["acc"], reverse=True)
|
||||
solidi_ml.sort(key=lambda x: x["daily_pnl"], reverse=True)
|
||||
|
||||
if solidi_s1:
|
||||
b = solidi_s1[0]
|
||||
print(f"\n MIGLIORE S1 (regole pure, facile da deployare):")
|
||||
print(f" {b['name']} — {b['acc']:.1f}% acc, {b['trades']} trades, DD {b['max_dd']:.1f}%, €{b['daily_pnl']:+.2f}/day, Sharpe {b['sharpe']:.2f}")
|
||||
|
||||
if solidi_ml:
|
||||
m = solidi_ml[0]
|
||||
print(f"\n MIGLIORE S13 (squeeze+GBM, più complesso):")
|
||||
print(f" {m['name']} — {m['acc']:.1f}% acc, {m['trades']} trades, DD {m['max_dd']:.1f}%, €{m['daily_pnl']:+.2f}/day, Sharpe {m['sharpe']:.2f}")
|
||||
|
||||
max_pnl = max(strategies, key=lambda x: x["pnl"])
|
||||
print(f"\n MAX PnL: {max_pnl['name']} — €{max_pnl['pnl']:+,.0f}")
|
||||
@@ -0,0 +1,205 @@
|
||||
"""AD01 — Adaptive Squeeze Threshold.
|
||||
|
||||
Problema SQ02: sq_threshold fisso (0.8) non si adatta al regime di volatilità.
|
||||
Soluzione: threshold adattivo basato su volatilità recente.
|
||||
|
||||
Logica:
|
||||
- Calcola volatilità rolling (std dei rendimenti su finestra 100 barre)
|
||||
- Confronta con percentile storico (rolling 500 barre)
|
||||
- Alta vol (>70° percentile) → soglia BASSA (0.65) — squeeze più "lenti"
|
||||
- Bassa vol (<30° percentile) → soglia ALTA (0.90) — squeeze "stretti"
|
||||
- Vol media → soglia standard (0.80)
|
||||
|
||||
Razionale: in mercati calmi, il BB si stringe molto → sq_threshold alto cattura
|
||||
segnali migliori. In mercati volatili, bastano squeeze minori per essere significativi.
|
||||
|
||||
Anti-overfitting: solo 3 parametri (low_thr, mid_thr, high_thr), logica deterministica.
|
||||
Eredita antifakeout + volume da SQ02.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from src.strategies.base import Strategy, Signal, BacktestResult, YearlyStats, TF_MINUTES
|
||||
from src.strategies.indicators import keltner_ratio, ema
|
||||
from src.data.downloader import load_data
|
||||
|
||||
|
||||
def _adaptive_sq_threshold(close: np.ndarray,
|
||||
vol_window: int = 100,
|
||||
regime_window: int = 500,
|
||||
low_thr: float = 0.65,
|
||||
mid_thr: float = 0.80,
|
||||
high_thr: float = 0.90) -> np.ndarray:
|
||||
"""Calcola sq_threshold adattivo per ogni barra."""
|
||||
n = len(close)
|
||||
lr = np.diff(np.log(np.where(close <= 0, 1e-10, close)))
|
||||
vol = np.full(n, np.nan)
|
||||
for i in range(vol_window, n):
|
||||
vol[i] = np.std(lr[i - vol_window:i])
|
||||
|
||||
# Percentile rolling della volatilità
|
||||
thresh = np.full(n, mid_thr)
|
||||
for i in range(regime_window, n):
|
||||
if np.isnan(vol[i]):
|
||||
continue
|
||||
hist = vol[i - regime_window:i]
|
||||
hist = hist[~np.isnan(hist)]
|
||||
if len(hist) < 10:
|
||||
continue
|
||||
p30 = np.percentile(hist, 30)
|
||||
p70 = np.percentile(hist, 70)
|
||||
if vol[i] < p30:
|
||||
thresh[i] = high_thr # vol bassa → soglia alta
|
||||
elif vol[i] > p70:
|
||||
thresh[i] = low_thr # vol alta → soglia bassa
|
||||
else:
|
||||
thresh[i] = mid_thr
|
||||
return thresh
|
||||
|
||||
|
||||
def _detect_adaptive_squeezes(close, high, low, kcr, adaptive_thr,
|
||||
min_dur: int = 5) -> list[dict]:
|
||||
"""Squeeze con threshold adattivo per ogni barra."""
|
||||
events = []
|
||||
in_sq = False
|
||||
sq_start = 0
|
||||
for i in range(1, len(close)):
|
||||
if np.isnan(kcr[i]) or np.isnan(adaptive_thr[i]):
|
||||
continue
|
||||
thr = adaptive_thr[i]
|
||||
is_sq = kcr[i] < thr
|
||||
if is_sq and not in_sq:
|
||||
in_sq = True
|
||||
sq_start = i
|
||||
elif not is_sq and in_sq:
|
||||
in_sq = False
|
||||
dur = i - sq_start
|
||||
if dur < min_dur:
|
||||
continue
|
||||
events.append({
|
||||
"idx": i, "dur": dur, "sq_start": sq_start,
|
||||
"kcr_at_release": kcr[i],
|
||||
"thr_used": adaptive_thr[i],
|
||||
})
|
||||
return events
|
||||
|
||||
|
||||
class AdaptiveSqueeze(Strategy):
|
||||
name = "AD01_adaptive_squeeze"
|
||||
description = "Squeeze con threshold adattivo a regime volatilità"
|
||||
default_assets = ["BTC", "ETH"]
|
||||
default_timeframes = ["15m", "1h"]
|
||||
fee_rt = 0.002
|
||||
leverage = 3.0
|
||||
position_size = 0.15
|
||||
initial_capital = 1000.0
|
||||
|
||||
def generate_signals(self, df: pd.DataFrame, ts: pd.DatetimeIndex,
|
||||
**params) -> list[Signal]:
|
||||
c = df["close"].values
|
||||
h = df["high"].values
|
||||
l = df["low"].values
|
||||
v = df["volume"].values
|
||||
n = len(c)
|
||||
|
||||
bb_w = params.get("bb_window", 14)
|
||||
low_thr = params.get("low_thr", 0.65)
|
||||
mid_thr = params.get("mid_thr", 0.80)
|
||||
high_thr = params.get("high_thr", 0.90)
|
||||
retrace_limit = params.get("retrace_limit", 0.6)
|
||||
vol_mult = params.get("vol_multiplier", 1.3)
|
||||
use_vol = params.get("use_vol", True)
|
||||
vol_window = params.get("vol_window", 100)
|
||||
regime_window = params.get("regime_window", 500)
|
||||
|
||||
kcr = keltner_ratio(c, h, l, bb_w)
|
||||
adaptive_thr = _adaptive_sq_threshold(
|
||||
c, vol_window, regime_window, low_thr, mid_thr, high_thr
|
||||
)
|
||||
events = _detect_adaptive_squeezes(c, h, l, kcr, adaptive_thr)
|
||||
|
||||
signals = []
|
||||
for ev in events:
|
||||
i = ev["idx"]
|
||||
if i < 1 or i >= n:
|
||||
continue
|
||||
|
||||
first_ret = (c[i] - c[i - 1]) / c[i - 1] if c[i - 1] > 0 else 0
|
||||
if abs(first_ret) < 0.001:
|
||||
continue
|
||||
direction = 1 if first_ret > 0 else -1
|
||||
|
||||
# Anti-fakeout
|
||||
br = h[i] - l[i]
|
||||
if br > 0:
|
||||
if direction == 1 and (h[i] - c[i]) / br > retrace_limit:
|
||||
continue
|
||||
elif direction == -1 and (c[i] - l[i]) / br > retrace_limit:
|
||||
continue
|
||||
|
||||
# Volume confirm
|
||||
if use_vol:
|
||||
sq_start = ev["sq_start"]
|
||||
avg_sq_v = np.mean(v[sq_start:i])
|
||||
if avg_sq_v > 0 and v[i] <= avg_sq_v * vol_mult:
|
||||
continue
|
||||
|
||||
signals.append(Signal(
|
||||
idx=i,
|
||||
direction=direction,
|
||||
entry_price=c[i - 1],
|
||||
metadata={
|
||||
"dur": ev["dur"],
|
||||
"thr_used": ev.get("thr_used", mid_thr),
|
||||
},
|
||||
))
|
||||
return signals
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
strategy = AdaptiveSqueeze()
|
||||
|
||||
configs = [
|
||||
# low_thr, mid_thr, high_thr, use_vol
|
||||
{"low_thr": 0.65, "mid_thr": 0.80, "high_thr": 0.90, "use_vol": True},
|
||||
{"low_thr": 0.65, "mid_thr": 0.80, "high_thr": 0.90, "use_vol": False},
|
||||
{"low_thr": 0.60, "mid_thr": 0.78, "high_thr": 0.92, "use_vol": True},
|
||||
{"low_thr": 0.70, "mid_thr": 0.82, "high_thr": 0.90, "use_vol": True},
|
||||
{"low_thr": 0.65, "mid_thr": 0.80, "high_thr": 0.95, "use_vol": True},
|
||||
{"low_thr": 0.65, "mid_thr": 0.80, "high_thr": 0.90,
|
||||
"use_vol": True, "vol_multiplier": 1.2},
|
||||
]
|
||||
|
||||
all_results = []
|
||||
for cfg in configs:
|
||||
for asset in ["BTC", "ETH"]:
|
||||
for tf in ["15m", "1h"]:
|
||||
for hold in [3, 6]:
|
||||
r = strategy.backtest(asset, tf, hold=hold, **cfg)
|
||||
if r and r.trades >= 20:
|
||||
lbl = (f"AD01 lt={cfg['low_thr']} ht={cfg['high_thr']} "
|
||||
f"v={cfg['use_vol']} h={hold}")
|
||||
r.strategy_name = lbl
|
||||
all_results.append(r)
|
||||
|
||||
all_results.sort(key=lambda r: r.accuracy, reverse=True)
|
||||
|
||||
print(f"\n{'=' * 130}")
|
||||
print(" AD01 ADAPTIVE SQUEEZE THRESHOLD — TOP 20")
|
||||
print(f"{'=' * 130}")
|
||||
print(f" {'Nome':<50s} {'A/T':>7s} {'Trades':>6s} {'Acc':>6s} "
|
||||
f"{'PnL€':>10s} {'DD%':>6s} {'€/day':>7s} "
|
||||
f"{'Mkt%':>5s} {'Dur':>5s} {'Anni':>4s}")
|
||||
print(f" {'─' * 120}")
|
||||
for r in all_results[:20]:
|
||||
r.print_summary()
|
||||
|
||||
if all_results:
|
||||
all_results[0].print_yearly()
|
||||
|
||||
print(f"\n BENCHMARK SQ02: 79.7% acc, 1250t, DD 6.5%, €5.23/day, 9 anni")
|
||||
print(f" BENCHMARK MT01: 82.7% acc, 503t, DD 5.9%")
|
||||
@@ -0,0 +1,183 @@
|
||||
"""CM01 — Cross-Market Momentum Filter.
|
||||
|
||||
Squeeze su asset primario, entra SOLO se l'altro asset (BTC↔ETH)
|
||||
mostra momentum short-term nella STESSA direzione.
|
||||
|
||||
Differenza da MT01: MT01 usa EMA slope su 1h (trend lento).
|
||||
CM01 usa rendimento grezzo degli ultimi 3-6 bar sull'asset cross
|
||||
(momentum veloce, stesso timeframe).
|
||||
|
||||
Razionale: BTC e ETH sono altamente correlati ma non perfettamente.
|
||||
Se BTC fa squeeze breakout UP e anche ETH sta salendo (momentum 3-6 bar),
|
||||
la probabilità di continuazione è maggiore perché c'è consenso di mercato.
|
||||
|
||||
Anti-overfitting: 1 parametro chiave (cross_bars 3-6), logica deterministica.
|
||||
Eredita antifakeout + volume da SQ02.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from src.strategies.base import Strategy, Signal, BacktestResult, YearlyStats
|
||||
from src.strategies.indicators import keltner_ratio, detect_squeezes
|
||||
from src.data.downloader import load_data
|
||||
|
||||
|
||||
class CrossMarketMomentum(Strategy):
|
||||
name = "CM01_cross_momentum"
|
||||
description = "Squeeze + cross-asset short-term momentum filter"
|
||||
default_assets = ["BTC", "ETH"]
|
||||
default_timeframes = ["15m", "1h"]
|
||||
fee_rt = 0.002
|
||||
leverage = 3.0
|
||||
position_size = 0.15
|
||||
initial_capital = 1000.0
|
||||
|
||||
# Map asset → cross asset
|
||||
_CROSS = {"BTC": "ETH", "ETH": "BTC"}
|
||||
|
||||
def generate_signals(self, df: pd.DataFrame, ts: pd.DatetimeIndex,
|
||||
**params) -> list[Signal]:
|
||||
"""Genera segnali con cross-market momentum."""
|
||||
c = df["close"].values
|
||||
h = df["high"].values
|
||||
l = df["low"].values
|
||||
v = df["volume"].values
|
||||
n = len(c)
|
||||
ts_ms = df["timestamp"].values
|
||||
|
||||
asset = params.get("asset", "BTC")
|
||||
tf = params.get("tf", "15m")
|
||||
bb_w = params.get("bb_window", 14)
|
||||
sq_thr = params.get("sq_threshold", 0.8)
|
||||
retrace_limit = params.get("retrace_limit", 0.6)
|
||||
vol_mult = params.get("vol_multiplier", 1.3)
|
||||
use_vol = params.get("use_vol", True)
|
||||
cross_bars = params.get("cross_bars", 4) # barre momentum cross
|
||||
mom_min = params.get("mom_min", 0.0) # momentum minimo (0 = solo direzione)
|
||||
|
||||
# Carica cross asset
|
||||
cross_asset = self._CROSS.get(asset)
|
||||
if cross_asset is None:
|
||||
return []
|
||||
|
||||
try:
|
||||
df_cross = load_data(cross_asset, tf)
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
c_cross = df_cross["close"].values
|
||||
ts_cross_ms = df_cross["timestamp"].values
|
||||
n_cross = len(c_cross)
|
||||
|
||||
# Momentum cross: rendimento log su cross_bars barre
|
||||
cross_mom = np.full(n_cross, np.nan)
|
||||
for i in range(cross_bars, n_cross):
|
||||
if c_cross[i - cross_bars] > 0:
|
||||
cross_mom[i] = np.log(c_cross[i] / c_cross[i - cross_bars])
|
||||
|
||||
kcr = keltner_ratio(c, h, l, bb_w)
|
||||
events = detect_squeezes(c, h, l, kcr, sq_thr)
|
||||
|
||||
signals = []
|
||||
for ev in events:
|
||||
i = ev["idx"]
|
||||
if i < 1 or i >= n:
|
||||
continue
|
||||
|
||||
first_ret = (c[i] - c[i - 1]) / c[i - 1] if c[i - 1] > 0 else 0
|
||||
if abs(first_ret) < 0.001:
|
||||
continue
|
||||
direction = 1 if first_ret > 0 else -1
|
||||
|
||||
# Anti-fakeout
|
||||
br = h[i] - l[i]
|
||||
if br > 0:
|
||||
if direction == 1 and (h[i] - c[i]) / br > retrace_limit:
|
||||
continue
|
||||
elif direction == -1 and (c[i] - l[i]) / br > retrace_limit:
|
||||
continue
|
||||
|
||||
# Volume confirm
|
||||
if use_vol:
|
||||
sq_start = ev["sq_start"]
|
||||
avg_sq_v = np.mean(v[sq_start:i])
|
||||
if avg_sq_v > 0 and v[i] <= avg_sq_v * vol_mult:
|
||||
continue
|
||||
|
||||
# Cross-market momentum: trova indice cross corrispondente
|
||||
i_cross = np.searchsorted(ts_cross_ms, ts_ms[i]) - 1
|
||||
if i_cross < cross_bars or i_cross >= n_cross:
|
||||
continue
|
||||
mom = cross_mom[i_cross]
|
||||
if np.isnan(mom):
|
||||
continue
|
||||
|
||||
# Filtra per direzione concordante
|
||||
if direction == 1 and mom <= mom_min:
|
||||
continue
|
||||
if direction == -1 and mom >= -mom_min:
|
||||
continue
|
||||
|
||||
signals.append(Signal(
|
||||
idx=i,
|
||||
direction=direction,
|
||||
entry_price=c[i - 1],
|
||||
metadata={
|
||||
"dur": ev["dur"],
|
||||
"cross_mom": float(mom),
|
||||
},
|
||||
))
|
||||
return signals
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
strategy = CrossMarketMomentum()
|
||||
|
||||
configs = [
|
||||
# cross_bars, mom_min, use_vol
|
||||
{"cross_bars": 3, "mom_min": 0.0, "use_vol": True},
|
||||
{"cross_bars": 4, "mom_min": 0.0, "use_vol": True},
|
||||
{"cross_bars": 6, "mom_min": 0.0, "use_vol": True},
|
||||
{"cross_bars": 4, "mom_min": 0.001, "use_vol": True},
|
||||
{"cross_bars": 4, "mom_min": 0.002, "use_vol": True},
|
||||
{"cross_bars": 4, "mom_min": 0.0, "use_vol": False},
|
||||
{"cross_bars": 3, "mom_min": 0.001, "use_vol": False},
|
||||
{"cross_bars": 6, "mom_min": 0.001, "use_vol": True},
|
||||
]
|
||||
|
||||
all_results = []
|
||||
for cfg in configs:
|
||||
for asset in ["BTC", "ETH"]:
|
||||
for tf in ["15m", "1h"]:
|
||||
for hold in [3, 6]:
|
||||
r = strategy.backtest(asset, tf, hold=hold,
|
||||
cross_bars=cfg["cross_bars"],
|
||||
mom_min=cfg["mom_min"],
|
||||
use_vol=cfg["use_vol"])
|
||||
if r and r.trades >= 20:
|
||||
lbl = (f"CM01 cb={cfg['cross_bars']} "
|
||||
f"mm={cfg['mom_min']} v={cfg['use_vol']} h={hold}")
|
||||
r.strategy_name = lbl
|
||||
all_results.append(r)
|
||||
|
||||
all_results.sort(key=lambda r: r.accuracy, reverse=True)
|
||||
|
||||
print(f"\n{'=' * 130}")
|
||||
print(" CM01 CROSS-MARKET MOMENTUM — TOP 20")
|
||||
print(f"{'=' * 130}")
|
||||
print(f" {'Nome':<50s} {'A/T':>7s} {'Trades':>6s} {'Acc':>6s} "
|
||||
f"{'PnL€':>10s} {'DD%':>6s} {'€/day':>7s} "
|
||||
f"{'Mkt%':>5s} {'Dur':>5s} {'Anni':>4s}")
|
||||
print(f" {'─' * 120}")
|
||||
for r in all_results[:20]:
|
||||
r.print_summary()
|
||||
|
||||
if all_results:
|
||||
all_results[0].print_yearly()
|
||||
|
||||
print(f"\n BENCHMARK SQ02: 79.7% acc, 1250t, DD 6.5%, €5.23/day, 9 anni")
|
||||
print(f" BENCHMARK MT01: 82.7% acc, 503t, DD 5.9%")
|
||||
@@ -0,0 +1,266 @@
|
||||
"""ML01 — Squeeze + GBM (Gradient Boosting Machine) Walk-Forward.
|
||||
|
||||
Strategia ibrida: squeeze breakout come pre-filtro (QUANDO tradare),
|
||||
GradientBoosting su features strutturali come conferma (QUALE direzione).
|
||||
|
||||
Pipeline:
|
||||
1. Rileva squeeze release (Bollinger esce da Keltner)
|
||||
2. Estrai 44 features dalla finestra (structural multi-window + squeeze
|
||||
metadata + price position + ATR + momentum breakout)
|
||||
3. GBM walk-forward: train su 50% rolling, step 10%, predice direzione
|
||||
4. Trade solo se ML ha confidenza ≥ ml_threshold
|
||||
|
||||
IN:
|
||||
- OHLCV DataFrame
|
||||
- Parametri: bb_window (14), sq_threshold (0.8), brk_bars (3),
|
||||
ml_threshold (0.70), leverage (3), position_pct (0.15)
|
||||
|
||||
OUT:
|
||||
- BacktestResult con metriche walk-forward (no data leakage)
|
||||
- Solo periodo di test (seconda metà dati)
|
||||
|
||||
Risultati tipici:
|
||||
ETH 15m bb14 ml=0.70: 76.9% acc, 1213 trades, DD 4.2%, €13.78/day
|
||||
BTC 15m bb14 ml=0.70: 78.8% acc, 1964 trades, DD 7.0%, €5.51/day
|
||||
BTC 1h bb14 ml=0.70: 77.3% acc, 617 trades, DD 6.7%, €3.85/day
|
||||
|
||||
Note:
|
||||
- GBM = GradientBoostingClassifier di scikit-learn
|
||||
- Walk-forward: nessun look-ahead, train sempre prima di test
|
||||
- Il baseline squeeze puro ha accuracy più alta (~79.5%) ma DD peggiore
|
||||
- Il valore del ML è filtrare breakout deboli → DD ridotto
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from sklearn.ensemble import GradientBoostingClassifier
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
|
||||
from src.strategies.base import Strategy, Signal, BacktestResult, YearlyStats, TF_MINUTES
|
||||
from src.strategies.indicators import keltner_ratio, detect_squeezes
|
||||
from src.data.downloader import load_data
|
||||
|
||||
|
||||
def _build_features(df: pd.DataFrame, i: int, squeeze_info: dict) -> np.ndarray | None:
|
||||
"""44 features per il punto di squeeze release."""
|
||||
if i < 100:
|
||||
return None
|
||||
o, h, l, c, v = (df["open"].values, df["high"].values, df["low"].values,
|
||||
df["close"].values, df["volume"].values)
|
||||
feats = []
|
||||
for w in [12, 24, 48]:
|
||||
wc, wo = c[i-w:i], o[i-w:i]
|
||||
wh, wl, wv = h[i-w:i], l[i-w:i], v[i-w:i]
|
||||
mn, mx = wl.min(), max(wh.max(), wc.max())
|
||||
rng = mx - mn if mx - mn > 0 else 1e-10
|
||||
total = np.where(wh - wl == 0, 1e-10, wh - wl)
|
||||
body = np.abs(wc - wo) / total
|
||||
direction = np.sign(wc - wo)
|
||||
log_c = np.log(np.where(wc == 0, 1e-10, wc))
|
||||
rets = np.diff(log_c)
|
||||
v_mean = np.mean(wv)
|
||||
feats.extend([
|
||||
np.mean(rets) if len(rets) > 0 else 0,
|
||||
np.std(rets) if len(rets) > 0 else 0,
|
||||
np.sum(rets) if len(rets) > 0 else 0,
|
||||
float(pd.Series(rets).skew()) if len(rets) > 2 else 0,
|
||||
float(pd.Series(rets).kurtosis()) if len(rets) > 3 else 0,
|
||||
np.mean(body), np.std(body),
|
||||
np.mean(direction), np.mean(direction[-min(3, w):]),
|
||||
(wc[-1] - mn) / rng,
|
||||
wv[-1] / v_mean if v_mean > 0 else 1,
|
||||
np.corrcoef(rets[:-1], rets[1:])[0, 1] if len(rets) > 1 and np.std(rets) > 0 else 0,
|
||||
])
|
||||
sq = squeeze_info
|
||||
feats.extend([
|
||||
sq["dur"], sq["dur"] / 24, sq["kcr_at_release"],
|
||||
v[i-1] / sq.get("avg_vol", 1) if sq.get("avg_vol", 0) > 0 else 1,
|
||||
np.mean(v[i:min(i+3, len(v))]) / sq.get("avg_vol", 1) if sq.get("avg_vol", 0) > 0 else 1,
|
||||
])
|
||||
h48, l48 = np.max(h[max(0, i-48):i]), np.min(l[max(0, i-48):i])
|
||||
r48 = h48 - l48
|
||||
feats.append((c[i-1] - l48) / r48 if r48 > 0 else 0.5)
|
||||
tr = np.maximum(h[i-14:i] - l[i-14:i],
|
||||
np.maximum(np.abs(h[i-14:i] - np.roll(c[i-14:i], 1)),
|
||||
np.abs(l[i-14:i] - np.roll(c[i-14:i], 1))))
|
||||
feats.append(np.mean(tr[1:]) / c[i-1] if c[i-1] > 0 else 0)
|
||||
feats.append((c[i] - c[i-1]) / c[i-1] if c[i-1] > 0 else 0)
|
||||
return np.nan_to_num(np.array(feats), nan=0, posinf=1e6, neginf=-1e6)
|
||||
|
||||
|
||||
class SqueezeGBM(Strategy):
|
||||
name = "ML01_squeeze_gbm"
|
||||
description = "Squeeze + GBM walk-forward — ML filtra breakout deboli"
|
||||
default_assets = ["BTC", "ETH"]
|
||||
default_timeframes = ["15m", "1h"]
|
||||
fee_ml = 0.001
|
||||
|
||||
def generate_signals(self, df, ts, **params):
|
||||
raise NotImplementedError("ML01 usa backtest custom con walk-forward")
|
||||
|
||||
def backtest(self, asset: str, tf: str, hold: int = 3, **params) -> BacktestResult | None:
|
||||
bb_w = params.get("bb_window", 14)
|
||||
sq_thr = params.get("sq_threshold", 0.8 if tf == "1h" else 0.9)
|
||||
brk = params.get("brk_bars", hold)
|
||||
ml_thr = params.get("ml_threshold", 0.70)
|
||||
lev = params.get("leverage", self.leverage)
|
||||
pos = params.get("position_pct", self.position_size)
|
||||
|
||||
df = load_data(asset, tf)
|
||||
close = df["close"].values
|
||||
high = df["high"].values
|
||||
low = df["low"].values
|
||||
volume = df["volume"].values
|
||||
n = len(df)
|
||||
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||
|
||||
kcr = keltner_ratio(close, high, low, bb_w)
|
||||
raw_events = detect_squeezes(close, high, low, kcr, sq_thr)
|
||||
|
||||
# Aggiungi avg_vol a ogni evento
|
||||
events = []
|
||||
for ev in raw_events:
|
||||
ev["avg_vol"] = float(np.mean(volume[ev["sq_start"]:ev["idx"]]))
|
||||
events.append(ev)
|
||||
|
||||
X_all, y_all, ev_all = [], [], []
|
||||
for ev in events:
|
||||
i = ev["idx"]
|
||||
if i + brk >= n or i < 100:
|
||||
continue
|
||||
feats = _build_features(df, i, ev)
|
||||
if feats is None:
|
||||
continue
|
||||
actual_ret = (close[i + brk - 1] - close[i - 1]) / close[i - 1]
|
||||
X_all.append(feats)
|
||||
y_all.append(1 if actual_ret > 0 else 0)
|
||||
ev_all.append(ev)
|
||||
|
||||
if len(X_all) < 50:
|
||||
return None
|
||||
|
||||
X, y = np.array(X_all), np.array(y_all)
|
||||
TRAIN_SIZE = max(int(len(X) * 0.5), 50)
|
||||
STEP_SIZE = max(int(len(X) * 0.1), 10)
|
||||
|
||||
yearly: dict[int, dict] = {}
|
||||
capital = float(self.initial_capital)
|
||||
peak = capital
|
||||
max_dd = 0.0
|
||||
total_bars = 0
|
||||
all_t = all_w = 0
|
||||
|
||||
start = 0
|
||||
while start + TRAIN_SIZE + STEP_SIZE <= len(X):
|
||||
train_end = start + TRAIN_SIZE
|
||||
test_end = min(train_end + STEP_SIZE, len(X))
|
||||
X_tr, y_tr = X[start:train_end], y[start:train_end]
|
||||
X_te = X[train_end:test_end]
|
||||
|
||||
if len(np.unique(y_tr)) < 2:
|
||||
start += STEP_SIZE
|
||||
continue
|
||||
|
||||
scaler = StandardScaler()
|
||||
X_tr_s = scaler.fit_transform(X_tr)
|
||||
X_te_s = scaler.transform(X_te)
|
||||
|
||||
model = GradientBoostingClassifier(
|
||||
n_estimators=150, max_depth=4, min_samples_leaf=10,
|
||||
learning_rate=0.05, subsample=0.8, random_state=42,
|
||||
)
|
||||
model.fit(X_tr_s, y_tr)
|
||||
up_idx = list(model.classes_).index(1) if 1 in model.classes_ else -1
|
||||
if up_idx < 0:
|
||||
start += STEP_SIZE
|
||||
continue
|
||||
|
||||
for j in range(len(X_te)):
|
||||
proba = model.predict_proba(X_te_s[j:j+1])[0]
|
||||
p_up = proba[up_idx]
|
||||
ev = ev_all[train_end + j]
|
||||
i = ev["idx"]
|
||||
actual_ret = (close[i + brk - 1] - close[i - 1]) / close[i - 1]
|
||||
|
||||
if p_up >= ml_thr:
|
||||
direction = 1
|
||||
elif p_up <= (1 - ml_thr):
|
||||
direction = -1
|
||||
else:
|
||||
continue
|
||||
|
||||
is_correct = (direction == 1 and actual_ret > 0) or (direction == -1 and actual_ret < 0)
|
||||
trade_ret = actual_ret * direction
|
||||
net = trade_ret * lev - self.fee_ml * 2 * lev
|
||||
capital += capital * pos * net
|
||||
capital = max(capital, 10)
|
||||
if capital > peak:
|
||||
peak = capital
|
||||
dd = (peak - capital) / peak
|
||||
max_dd = max(max_dd, dd)
|
||||
total_bars += brk
|
||||
|
||||
all_t += 1
|
||||
if is_correct:
|
||||
all_w += 1
|
||||
|
||||
year = ts.iloc[i].year
|
||||
if year not in yearly:
|
||||
yearly[year] = {"w": 0, "t": 0, "pnl": 0.0}
|
||||
yearly[year]["t"] += 1
|
||||
if is_correct:
|
||||
yearly[year]["w"] += 1
|
||||
yearly[year]["pnl"] += net * self.initial_capital
|
||||
|
||||
start += STEP_SIZE
|
||||
|
||||
if all_t == 0:
|
||||
return None
|
||||
|
||||
yearly_stats = [
|
||||
YearlyStats(year=y, trades=d["t"], wins=d["w"], pnl=d["pnl"])
|
||||
for y, d in sorted(yearly.items())
|
||||
]
|
||||
|
||||
return BacktestResult(
|
||||
strategy_name=self.name,
|
||||
asset=asset,
|
||||
timeframe=tf,
|
||||
params={"bb_w": bb_w, "sq_thr": sq_thr, "ml_thr": ml_thr,
|
||||
"brk": brk, "lev": lev, "pos": pos},
|
||||
trades=all_t,
|
||||
wins=all_w,
|
||||
pnl=sum(d["pnl"] for d in yearly.values()),
|
||||
capital=capital,
|
||||
initial_capital=self.initial_capital,
|
||||
max_dd=max_dd * 100,
|
||||
time_in_market_pct=total_bars / n * 100,
|
||||
avg_trade_duration_h=brk * TF_MINUTES.get(tf, 60) / 60,
|
||||
years_active=len(yearly),
|
||||
yearly=yearly_stats,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
strategy = SqueezeGBM()
|
||||
print("Training ML models...\n")
|
||||
results = []
|
||||
for asset in ["ETH", "BTC"]:
|
||||
for tf in ["15m", "1h"]:
|
||||
for ml_thr in [0.65, 0.70]:
|
||||
r = strategy.backtest(asset, tf, ml_threshold=ml_thr)
|
||||
if r and r.trades >= 20:
|
||||
r.strategy_name = f"ML01 {asset} {tf} ml={ml_thr}"
|
||||
results.append(r)
|
||||
results.sort(key=lambda r: r.accuracy, reverse=True)
|
||||
|
||||
print(f"{'=' * 120}")
|
||||
print(f" ML01 SQUEEZE+GBM — RISULTATI")
|
||||
print(f"{'=' * 120}")
|
||||
for r in results:
|
||||
r.print_summary()
|
||||
if results:
|
||||
results[0].print_yearly()
|
||||
@@ -0,0 +1,259 @@
|
||||
"""MT01 — Squeeze + Multi-Timeframe Momentum.
|
||||
|
||||
Problema SQ02: entra al breakout 15m ma non sa se il trend 1h è allineato.
|
||||
Soluzione: squeeze su 15m + conferma momentum su 1h.
|
||||
|
||||
Anti-overfitting: usa solo 2 indicatori (squeeze + EMA slope),
|
||||
nessun parametro complesso.
|
||||
|
||||
IN:
|
||||
- OHLCV 15m + 1h per lo stesso asset
|
||||
- Parametri: sq_threshold, ema_period_1h, min_slope
|
||||
|
||||
OUT:
|
||||
- Signal al breakout 15m confermato da trend 1h
|
||||
- BacktestResult
|
||||
|
||||
Logica:
|
||||
1. Squeeze release su 15m (come SQ01)
|
||||
2. Antifakeout filter (come SQ02)
|
||||
3. Check 1h: EMA slope positiva per long, negativa per short
|
||||
4. Check 1h: prezzo sopra/sotto EMA per conferma trend
|
||||
5. Entra solo se 15m e 1h concordano
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from src.strategies.base import Strategy, Signal, BacktestResult, YearlyStats, TF_MINUTES
|
||||
from src.strategies.indicators import keltner_ratio, detect_squeezes, ema
|
||||
from src.data.downloader import load_data
|
||||
|
||||
|
||||
class SqueezeMTFMomentum(Strategy):
|
||||
name = "MT01_squeeze_mtf"
|
||||
description = "Squeeze 15m + momentum trend 1h — multi-timeframe"
|
||||
default_assets = ["BTC", "ETH"]
|
||||
default_timeframes = ["15m"]
|
||||
fee_rt = 0.002
|
||||
|
||||
def generate_signals(self, df, ts, **params):
|
||||
"""Genera segnali squeeze 15m confermati da trend 1h."""
|
||||
c = df["close"].values
|
||||
h = df["high"].values
|
||||
l = df["low"].values
|
||||
v = df["volume"].values
|
||||
n = len(c)
|
||||
|
||||
asset = params.get("asset", "BTC")
|
||||
sq_thr = params.get("sq_threshold", 0.8)
|
||||
ema_period = params.get("ema_period", 50)
|
||||
min_slope_val = params.get("min_slope", 0.001)
|
||||
use_antifake = params.get("antifake", True)
|
||||
use_vol = params.get("vol_filter", False)
|
||||
|
||||
kcr = keltner_ratio(c, h, l, 14)
|
||||
events = detect_squeezes(c, h, l, kcr, sq_thr)
|
||||
|
||||
df_1h = load_data(asset, "1h")
|
||||
c1h = df_1h["close"].values
|
||||
ts1h_ms = df_1h["timestamp"].values
|
||||
n1h = len(c1h)
|
||||
ema_1h = ema(c1h, ema_period)
|
||||
ema_slope_arr = np.full(n1h, np.nan)
|
||||
for i in range(5, n1h):
|
||||
if not np.isnan(ema_1h[i]) and not np.isnan(ema_1h[i-5]) and ema_1h[i-5] > 0:
|
||||
ema_slope_arr[i] = (ema_1h[i] - ema_1h[i-5]) / ema_1h[i-5]
|
||||
|
||||
ts_ms = df["timestamp"].values
|
||||
signals = []
|
||||
|
||||
for ev in events:
|
||||
i = ev["idx"]
|
||||
if i < 1 or i >= n:
|
||||
continue
|
||||
first_ret = (c[i] - c[i-1]) / c[i-1] if c[i-1] > 0 else 0
|
||||
if abs(first_ret) < 0.001:
|
||||
continue
|
||||
if use_antifake:
|
||||
br = h[i] - l[i]
|
||||
if br > 0:
|
||||
if c[i] > c[i-1] and (h[i] - c[i]) / br > 0.6:
|
||||
continue
|
||||
elif c[i] <= c[i-1] and (c[i] - l[i]) / br > 0.6:
|
||||
continue
|
||||
if use_vol:
|
||||
avg_v = np.mean(v[ev["sq_start"]:i])
|
||||
if avg_v > 0 and v[i] <= avg_v * 1.3:
|
||||
continue
|
||||
|
||||
direction = 1 if first_ret > 0 else -1
|
||||
i1h = np.searchsorted(ts1h_ms, ts_ms[i]) - 1
|
||||
if i1h < ema_period or i1h >= n1h:
|
||||
continue
|
||||
if np.isnan(ema_1h[i1h]) or np.isnan(ema_slope_arr[i1h]):
|
||||
continue
|
||||
if direction == 1:
|
||||
if c1h[i1h] < ema_1h[i1h] or ema_slope_arr[i1h] < min_slope_val:
|
||||
continue
|
||||
else:
|
||||
if c1h[i1h] > ema_1h[i1h] or ema_slope_arr[i1h] > -min_slope_val:
|
||||
continue
|
||||
|
||||
signals.append(Signal(idx=i, direction=direction, entry_price=c[i-1]))
|
||||
|
||||
return signals
|
||||
|
||||
def backtest(self, asset, tf="15m", hold=3, **params):
|
||||
sq_thr = params.get("sq_threshold", 0.8)
|
||||
ema_period = params.get("ema_period", 50)
|
||||
min_slope = params.get("min_slope", 0.001)
|
||||
use_antifake = params.get("antifake", True)
|
||||
use_vol = params.get("vol_filter", False)
|
||||
|
||||
# Carica 15m e 1h
|
||||
df_15m = load_data(asset, "15m")
|
||||
df_1h = load_data(asset, "1h")
|
||||
|
||||
c15 = df_15m["close"].values
|
||||
h15 = df_15m["high"].values
|
||||
l15 = df_15m["low"].values
|
||||
v15 = df_15m["volume"].values
|
||||
n15 = len(c15)
|
||||
ts15 = pd.to_datetime(df_15m["timestamp"], unit="ms", utc=True)
|
||||
ts15_ms = df_15m["timestamp"].values
|
||||
|
||||
c1h = df_1h["close"].values
|
||||
ts1h_ms = df_1h["timestamp"].values
|
||||
n1h = len(c1h)
|
||||
|
||||
kcr = keltner_ratio(c15, h15, l15, 14)
|
||||
events = detect_squeezes(c15, h15, l15, kcr, sq_thr)
|
||||
|
||||
# EMA su 1h
|
||||
ema_1h = ema(c1h, ema_period)
|
||||
|
||||
# EMA slope (variazione percentuale su 5 barre)
|
||||
ema_slope = np.full(n1h, np.nan)
|
||||
for i in range(5, n1h):
|
||||
if not np.isnan(ema_1h[i]) and not np.isnan(ema_1h[i - 5]) and ema_1h[i - 5] > 0:
|
||||
ema_slope[i] = (ema_1h[i] - ema_1h[i - 5]) / ema_1h[i - 5]
|
||||
|
||||
yearly = {}
|
||||
capital = float(self.initial_capital)
|
||||
peak = capital
|
||||
max_dd = 0.0
|
||||
total_bars = 0
|
||||
|
||||
for ev in events:
|
||||
i = ev["idx"]
|
||||
if i + hold + 1 >= n15 or i < 1:
|
||||
continue
|
||||
|
||||
first_ret = (c15[i] - c15[i - 1]) / c15[i - 1] if c15[i - 1] > 0 else 0
|
||||
if abs(first_ret) < 0.001:
|
||||
continue
|
||||
|
||||
# Antifake
|
||||
if use_antifake:
|
||||
br = h15[i] - l15[i]
|
||||
if br > 0:
|
||||
if c15[i] > c15[i - 1] and (h15[i] - c15[i]) / br > 0.6:
|
||||
continue
|
||||
elif c15[i] <= c15[i - 1] and (c15[i] - l15[i]) / br > 0.6:
|
||||
continue
|
||||
|
||||
# Volume filter
|
||||
if use_vol:
|
||||
avg_v = np.mean(v15[ev["sq_start"]:i])
|
||||
if avg_v > 0 and v15[i] <= avg_v * 1.3:
|
||||
continue
|
||||
|
||||
direction = 1 if first_ret > 0 else -1
|
||||
|
||||
# Trova indice 1h corrispondente
|
||||
i1h = np.searchsorted(ts1h_ms, ts15_ms[i]) - 1
|
||||
if i1h < ema_period or i1h >= n1h or np.isnan(ema_1h[i1h]) or np.isnan(ema_slope[i1h]):
|
||||
continue
|
||||
|
||||
# Conferma trend 1h
|
||||
if direction == 1:
|
||||
if c1h[i1h] < ema_1h[i1h]:
|
||||
continue
|
||||
if ema_slope[i1h] < min_slope:
|
||||
continue
|
||||
else:
|
||||
if c1h[i1h] > ema_1h[i1h]:
|
||||
continue
|
||||
if ema_slope[i1h] > -min_slope:
|
||||
continue
|
||||
|
||||
entry = c15[i - 1]
|
||||
exit_price = c15[min(i + hold - 1, n15 - 1)]
|
||||
actual = (exit_price - entry) / entry * direction
|
||||
net = actual * self.leverage - self.fee_rt * self.leverage
|
||||
|
||||
capital += capital * self.position_size * net
|
||||
capital = max(capital, 10)
|
||||
if capital > peak: peak = capital
|
||||
dd = (peak - capital) / peak
|
||||
max_dd = max(max_dd, dd)
|
||||
total_bars += hold
|
||||
|
||||
year = ts15.iloc[i].year
|
||||
if year not in yearly:
|
||||
yearly[year] = {"w": 0, "t": 0, "pnl": 0.0}
|
||||
yearly[year]["t"] += 1
|
||||
if actual > 0: yearly[year]["w"] += 1
|
||||
yearly[year]["pnl"] += net * self.initial_capital
|
||||
|
||||
all_t = sum(d["t"] for d in yearly.values())
|
||||
all_w = sum(d["w"] for d in yearly.values())
|
||||
if all_t == 0:
|
||||
return None
|
||||
|
||||
yearly_stats = [YearlyStats(y, d["t"], d["w"], d["pnl"]) for y, d in sorted(yearly.items())]
|
||||
return BacktestResult(
|
||||
strategy_name=self.name, asset=asset, timeframe="15m", params=params,
|
||||
trades=all_t, wins=all_w, pnl=sum(d["pnl"] for d in yearly.values()),
|
||||
capital=capital, initial_capital=self.initial_capital,
|
||||
max_dd=max_dd * 100, time_in_market_pct=total_bars / n15 * 100,
|
||||
avg_trade_duration_h=hold * 15 / 60, years_active=len(yearly), yearly=yearly_stats,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
strategy = SqueezeMTFMomentum()
|
||||
|
||||
configs = [
|
||||
("ema50 sl0.1%", {"ema_period": 50, "min_slope": 0.001}),
|
||||
("ema50 sl0.05%", {"ema_period": 50, "min_slope": 0.0005}),
|
||||
("ema50 sl0.2%", {"ema_period": 50, "min_slope": 0.002}),
|
||||
("ema20 sl0.1%", {"ema_period": 20, "min_slope": 0.001}),
|
||||
("ema50 sl0.1%+vol", {"ema_period": 50, "min_slope": 0.001, "vol_filter": True}),
|
||||
("ema20 sl0.1%+vol", {"ema_period": 20, "min_slope": 0.001, "vol_filter": True}),
|
||||
("ema50 noAF", {"ema_period": 50, "min_slope": 0.001, "antifake": False}),
|
||||
("ema100 sl0.05%", {"ema_period": 100, "min_slope": 0.0005}),
|
||||
]
|
||||
|
||||
all_results = []
|
||||
for label, params in configs:
|
||||
for asset in ["BTC", "ETH"]:
|
||||
for hold in [3, 6]:
|
||||
r = strategy.backtest(asset, "15m", hold=hold, **params)
|
||||
if r and r.trades >= 30:
|
||||
r.strategy_name = f"MT01 {label} h={hold}"
|
||||
all_results.append(r)
|
||||
|
||||
all_results.sort(key=lambda r: r.accuracy, reverse=True)
|
||||
print(f"\n{'=' * 130}")
|
||||
print(f" MT01 SQUEEZE + MTF MOMENTUM — TOP 20")
|
||||
print(f"{'=' * 130}")
|
||||
for r in all_results[:20]:
|
||||
r.print_summary()
|
||||
if all_results:
|
||||
all_results[0].print_yearly()
|
||||
|
||||
print(f"\n BENCHMARK SQ02: 79.7% acc, 1250t, DD 6.5%, 9 anni, €5.23/day")
|
||||
@@ -0,0 +1,158 @@
|
||||
"""PD01 — Price-Volume Divergence Squeeze.
|
||||
|
||||
Estende SQ02 con volume TREND come filtro:
|
||||
- Breakout UP con volume CRESCENTE (ultimi 3 bar vs media squeeze) → ENTRA
|
||||
- Breakout UP con volume CALANTE → SALTA (divergenza bearish)
|
||||
- Viceversa per short
|
||||
|
||||
Logica anti-fakeout:
|
||||
1. Squeeze rilascio (come SQ02)
|
||||
2. Anti-fakeout candela (come SQ02)
|
||||
3. Volume al breakout > media squeeze (come SQ02)
|
||||
4. NUOVO: volume trending UP nelle ultime 3 barre prima del breakout
|
||||
|
||||
Parametri semplici, nessun overfitting.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from src.strategies.base import Strategy, Signal
|
||||
from src.strategies.indicators import keltner_ratio, detect_squeezes
|
||||
|
||||
|
||||
class PriceVolumeDivergence(Strategy):
|
||||
name = "PD01_price_vol_div"
|
||||
description = "Squeeze + antifakeout + volume trend confirmation"
|
||||
default_assets = ["BTC", "ETH"]
|
||||
default_timeframes = ["15m", "1h"]
|
||||
fee_rt = 0.002
|
||||
leverage = 3.0
|
||||
position_size = 0.15
|
||||
initial_capital = 1000.0
|
||||
|
||||
def generate_signals(self, df: pd.DataFrame, ts: pd.DatetimeIndex,
|
||||
**params) -> list[Signal]:
|
||||
c = df["close"].values
|
||||
h = df["high"].values
|
||||
l = df["low"].values
|
||||
v = df["volume"].values
|
||||
n = len(c)
|
||||
|
||||
bb_w = params.get("bb_window", 14)
|
||||
sq_thr = params.get("sq_threshold", 0.8)
|
||||
retrace_limit = params.get("retrace_limit", 0.6)
|
||||
vol_mult = params.get("vol_multiplier", 1.3)
|
||||
vol_trend_bars = params.get("vol_trend_bars", 3) # barre per trend volume
|
||||
|
||||
kcr = keltner_ratio(c, h, l, bb_w)
|
||||
events = detect_squeezes(c, h, l, kcr, sq_thr)
|
||||
|
||||
signals = []
|
||||
for ev in events:
|
||||
i = ev["idx"]
|
||||
if i < vol_trend_bars + 1 or i >= n:
|
||||
continue
|
||||
|
||||
# Direzione breakout
|
||||
first_ret = (c[i] - c[i - 1]) / c[i - 1] if c[i - 1] > 0 else 0
|
||||
if abs(first_ret) < 0.001:
|
||||
continue
|
||||
direction = 1 if first_ret > 0 else -1
|
||||
|
||||
# Anti-fakeout
|
||||
br = h[i] - l[i]
|
||||
if br > 0:
|
||||
if direction == 1 and (h[i] - c[i]) / br > retrace_limit:
|
||||
continue
|
||||
elif direction == -1 and (c[i] - l[i]) / br > retrace_limit:
|
||||
continue
|
||||
|
||||
# Volume al breakout > media squeeze
|
||||
sq_start = ev["sq_start"]
|
||||
avg_sq_v = np.mean(v[sq_start:i])
|
||||
if avg_sq_v <= 0 or v[i] <= avg_sq_v * vol_mult:
|
||||
continue
|
||||
|
||||
# Volume TREND: slope delle ultime vol_trend_bars barre
|
||||
# Usa regressione lineare semplice (rank correlation del volume)
|
||||
recent_v = v[i - vol_trend_bars:i + 1] # include breakout bar
|
||||
if len(recent_v) < vol_trend_bars:
|
||||
continue
|
||||
# slope: media seconda metà vs prima metà
|
||||
mid = len(recent_v) // 2
|
||||
v_early = np.mean(recent_v[:mid])
|
||||
v_late = np.mean(recent_v[mid:])
|
||||
vol_trending_up = v_late > v_early
|
||||
vol_trending_down = v_early > v_late
|
||||
|
||||
# Concordanza: long richiede volume trending up, short trending down
|
||||
if direction == 1 and not vol_trending_up:
|
||||
continue
|
||||
if direction == -1 and not vol_trending_down:
|
||||
continue
|
||||
|
||||
signals.append(Signal(
|
||||
idx=i,
|
||||
direction=direction,
|
||||
entry_price=c[i - 1],
|
||||
metadata={
|
||||
"dur": ev["dur"],
|
||||
"vol_ratio": v[i] / avg_sq_v if avg_sq_v > 0 else 0,
|
||||
"vol_trend": v_late / v_early if v_early > 0 else 1,
|
||||
},
|
||||
))
|
||||
return signals
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
strategy = PriceVolumeDivergence()
|
||||
|
||||
configs = [
|
||||
{"bb_window": 14, "sq_threshold": 0.8, "retrace_limit": 0.6,
|
||||
"vol_multiplier": 1.3, "vol_trend_bars": 3},
|
||||
{"bb_window": 14, "sq_threshold": 0.8, "retrace_limit": 0.6,
|
||||
"vol_multiplier": 1.2, "vol_trend_bars": 3},
|
||||
{"bb_window": 14, "sq_threshold": 0.8, "retrace_limit": 0.6,
|
||||
"vol_multiplier": 1.3, "vol_trend_bars": 5},
|
||||
{"bb_window": 14, "sq_threshold": 0.8, "retrace_limit": 0.5,
|
||||
"vol_multiplier": 1.3, "vol_trend_bars": 3},
|
||||
{"bb_window": 14, "sq_threshold": 0.75, "retrace_limit": 0.6,
|
||||
"vol_multiplier": 1.3, "vol_trend_bars": 3},
|
||||
{"bb_window": 20, "sq_threshold": 0.8, "retrace_limit": 0.6,
|
||||
"vol_multiplier": 1.3, "vol_trend_bars": 3},
|
||||
]
|
||||
|
||||
all_results = []
|
||||
for cfg in configs:
|
||||
for asset in ["BTC", "ETH"]:
|
||||
for tf in ["15m", "1h"]:
|
||||
for hold in [3, 6]:
|
||||
r = strategy.backtest(asset, tf, hold=hold, **cfg)
|
||||
if r and r.trades >= 20:
|
||||
lbl = (f"PD01 vtb={cfg['vol_trend_bars']} "
|
||||
f"vm={cfg['vol_multiplier']} "
|
||||
f"sq={cfg['sq_threshold']} h={hold}")
|
||||
r.strategy_name = lbl
|
||||
all_results.append(r)
|
||||
|
||||
all_results.sort(key=lambda r: r.accuracy, reverse=True)
|
||||
|
||||
print(f"\n{'=' * 130}")
|
||||
print(" PD01 PRICE-VOLUME DIVERGENCE — TOP 20")
|
||||
print(f"{'=' * 130}")
|
||||
print(f" {'Nome':<50s} {'A/T':>7s} {'Trades':>6s} {'Acc':>6s} "
|
||||
f"{'PnL€':>10s} {'DD%':>6s} {'€/day':>7s} "
|
||||
f"{'Mkt%':>5s} {'Dur':>5s} {'Anni':>4s}")
|
||||
print(f" {'─' * 120}")
|
||||
for r in all_results[:20]:
|
||||
r.print_summary()
|
||||
|
||||
if all_results:
|
||||
all_results[0].print_yearly()
|
||||
|
||||
print(f"\n BENCHMARK SQ02: 79.7% acc, 1250t, DD 6.5%, €5.23/day, 9 anni")
|
||||
print(f" BENCHMARK MT01: 82.7% acc, 503t, DD 5.9%")
|
||||
@@ -0,0 +1,68 @@
|
||||
"""SQ01 — Squeeze Breakout Base.
|
||||
|
||||
Strategia strutturale: rileva compressione di volatilità (Bollinger dentro
|
||||
Keltner Channel) e segue la direzione del breakout al rilascio.
|
||||
|
||||
IN:
|
||||
- OHLCV DataFrame (da load_data)
|
||||
- Parametri: bb_window (14), sq_threshold (0.8), min_squeeze_dur (5)
|
||||
|
||||
OUT:
|
||||
- Lista di Signal con direzione breakout (+1/-1)
|
||||
- BacktestResult con equity, yearly breakdown, metriche
|
||||
|
||||
Risultati tipici:
|
||||
BTC 15m: 76.7% acc, 4062 trades, DD 6.7%, €9.32/day
|
||||
ETH 15m: 76.4% acc, 2948 trades, DD 6.2%, €10.31/day
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from src.strategies.base import Strategy, Signal
|
||||
from src.strategies.indicators import keltner_ratio, detect_squeezes
|
||||
|
||||
|
||||
class SqueezeBase(Strategy):
|
||||
name = "SQ01_squeeze_base"
|
||||
description = "Squeeze breakout puro — segui direzione al rilascio"
|
||||
default_assets = ["BTC", "ETH"]
|
||||
default_timeframes = ["15m", "1h"]
|
||||
|
||||
def generate_signals(self, df: pd.DataFrame, ts: pd.DatetimeIndex,
|
||||
**params) -> list[Signal]:
|
||||
c = df["close"].values
|
||||
h = df["high"].values
|
||||
l = df["low"].values
|
||||
n = len(c)
|
||||
|
||||
bb_w = params.get("bb_window", 14)
|
||||
sq_thr = params.get("sq_threshold", 0.8)
|
||||
min_dur = params.get("min_dur", 5)
|
||||
|
||||
kcr = keltner_ratio(c, h, l, bb_w)
|
||||
events = detect_squeezes(c, h, l, kcr, sq_thr, min_dur)
|
||||
|
||||
signals = []
|
||||
for ev in events:
|
||||
i = ev["idx"]
|
||||
if i < 1 or i >= n:
|
||||
continue
|
||||
first_ret = (c[i] - c[i - 1]) / c[i - 1] if c[i - 1] > 0 else 0
|
||||
if abs(first_ret) < 0.001:
|
||||
continue
|
||||
signals.append(Signal(
|
||||
idx=i,
|
||||
direction=1 if first_ret > 0 else -1,
|
||||
entry_price=c[i - 1],
|
||||
metadata={"dur": ev["dur"], "kcr": ev["kcr_at_release"]},
|
||||
))
|
||||
return signals
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
strategy = SqueezeBase()
|
||||
strategy.report()
|
||||
@@ -0,0 +1,87 @@
|
||||
"""SQ02 — Squeeze Breakout + Anti-Fakeout + Volume Confirmation.
|
||||
|
||||
Migliora SQ01 con due filtri:
|
||||
1. Anti-fakeout: scarta breakout dove la candela ritraccia >60% del range
|
||||
2. Volume confirm: volume al breakout deve essere >1.3× la media durante squeeze
|
||||
|
||||
IN:
|
||||
- OHLCV DataFrame
|
||||
- Parametri: bb_window (14), sq_threshold (0.8), retrace_limit (0.6),
|
||||
vol_multiplier (1.3)
|
||||
|
||||
OUT:
|
||||
- Lista di Signal filtrati
|
||||
- BacktestResult
|
||||
|
||||
Risultati tipici:
|
||||
BTC 15m: 79.7% acc, 1250 trades, DD 6.5%, €5.23/day — SOLIDO 9/9 anni
|
||||
ETH 15m: 78.6% acc, 942 trades, DD 3.4%, €4.33/day
|
||||
BTC 1h: 78.0% acc, 473 trades, DD 3.5%, Sharpe 6.57
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from src.strategies.base import Strategy, Signal
|
||||
from src.strategies.indicators import keltner_ratio, detect_squeezes
|
||||
|
||||
|
||||
class SqueezeAntifakeVol(Strategy):
|
||||
name = "SQ02_antifake_vol"
|
||||
description = "Squeeze + antifakeout + volume confirmation"
|
||||
default_assets = ["BTC", "ETH"]
|
||||
default_timeframes = ["15m", "1h"]
|
||||
|
||||
def generate_signals(self, df: pd.DataFrame, ts: pd.DatetimeIndex,
|
||||
**params) -> list[Signal]:
|
||||
c = df["close"].values
|
||||
h = df["high"].values
|
||||
l = df["low"].values
|
||||
v = df["volume"].values
|
||||
n = len(c)
|
||||
|
||||
bb_w = params.get("bb_window", 14)
|
||||
sq_thr = params.get("sq_threshold", 0.8)
|
||||
retrace_limit = params.get("retrace_limit", 0.6)
|
||||
vol_mult = params.get("vol_multiplier", 1.3)
|
||||
|
||||
kcr = keltner_ratio(c, h, l, bb_w)
|
||||
events = detect_squeezes(c, h, l, kcr, sq_thr)
|
||||
|
||||
signals = []
|
||||
for ev in events:
|
||||
i = ev["idx"]
|
||||
if i < 1 or i >= n:
|
||||
continue
|
||||
first_ret = (c[i] - c[i - 1]) / c[i - 1] if c[i - 1] > 0 else 0
|
||||
if abs(first_ret) < 0.001:
|
||||
continue
|
||||
|
||||
br = h[i] - l[i]
|
||||
if br > 0:
|
||||
if c[i] > c[i - 1]:
|
||||
if (h[i] - c[i]) / br > retrace_limit:
|
||||
continue
|
||||
else:
|
||||
if (c[i] - l[i]) / br > retrace_limit:
|
||||
continue
|
||||
|
||||
avg_v = np.mean(v[ev["sq_start"]:i])
|
||||
if avg_v > 0 and v[i] <= avg_v * vol_mult:
|
||||
continue
|
||||
|
||||
signals.append(Signal(
|
||||
idx=i,
|
||||
direction=1 if first_ret > 0 else -1,
|
||||
entry_price=c[i - 1],
|
||||
metadata={"dur": ev["dur"], "vol_ratio": v[i] / avg_v if avg_v > 0 else 0},
|
||||
))
|
||||
return signals
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
strategy = SqueezeAntifakeVol()
|
||||
strategy.report()
|
||||
@@ -0,0 +1,175 @@
|
||||
"""SQ03 — Squeeze con filtri selezionabili.
|
||||
|
||||
Ogni filtro è opzionale e attivabile via parametro. Di default attiva solo
|
||||
antifake + long_squeeze (i due filtri con miglior rapporto accuracy/trade).
|
||||
Esegue tutte le combinazioni utili e classifica.
|
||||
|
||||
Filtri disponibili:
|
||||
- antifake: scarta breakout con retrace >60% (guadagna ~+1% acc)
|
||||
- long_sq: solo squeeze durata ≥10 barre (+1% acc, dimezza trade)
|
||||
- timing: solo ore 4-16 UTC (+0.5% acc)
|
||||
- cross: asset secondario in squeeze nelle ultime 10 barre (+0.5%)
|
||||
- vol: volume al breakout >1.3× media squeeze (+1% acc)
|
||||
|
||||
IN:
|
||||
- OHLCV DataFrame (primario + secondario per cross-check)
|
||||
- Parametri: filters (lista), bb_window, sq_threshold
|
||||
|
||||
OUT:
|
||||
- BacktestResult per ogni preset di filtri
|
||||
|
||||
Risultati tipici (BTC 15m):
|
||||
antifake+long: 77.3% acc, 2179 trades
|
||||
antifake+vol: 79.7% acc, 1250 trades — SOLIDO
|
||||
ALL_FILTERS: 79.2% acc, 696 trades (restrittivo)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from src.strategies.base import Strategy, Signal, BacktestResult, YearlyStats, TF_MINUTES
|
||||
from src.strategies.indicators import keltner_ratio, detect_squeezes
|
||||
from src.data.downloader import load_data
|
||||
|
||||
|
||||
PRESETS = {
|
||||
"antifake": ["antifake"],
|
||||
"long_sq": ["long_sq"],
|
||||
"antifake+long": ["antifake", "long_sq"],
|
||||
"antifake+vol": ["antifake", "vol"],
|
||||
"antifake+timing": ["antifake", "timing"],
|
||||
"long+timing": ["long_sq", "timing"],
|
||||
"antifake+long+time": ["antifake", "long_sq", "timing"],
|
||||
"antifake+cross": ["antifake", "cross"],
|
||||
"ALL_FILTERS": ["antifake", "long_sq", "timing", "cross"],
|
||||
}
|
||||
|
||||
|
||||
class SqueezeFiltered(Strategy):
|
||||
name = "SQ03_filtered"
|
||||
description = "Squeeze + filtri selezionabili (antifake, long, timing, cross, vol)"
|
||||
default_assets = ["BTC", "ETH"]
|
||||
default_timeframes = ["15m", "1h"]
|
||||
|
||||
def generate_signals(self, df: pd.DataFrame, ts: pd.DatetimeIndex,
|
||||
**params) -> list[Signal]:
|
||||
c = df["close"].values
|
||||
h = df["high"].values
|
||||
l = df["low"].values
|
||||
v = df["volume"].values
|
||||
n = len(c)
|
||||
|
||||
bb_w = params.get("bb_window", 14)
|
||||
sq_thr = params.get("sq_threshold", 0.8)
|
||||
filters = params.get("filters", ["antifake", "long_sq"])
|
||||
asset = params.get("asset", "BTC")
|
||||
tf = params.get("tf", "15m")
|
||||
|
||||
kcr = keltner_ratio(c, h, l, bb_w)
|
||||
events = detect_squeezes(c, h, l, kcr, sq_thr)
|
||||
|
||||
kcr2 = None
|
||||
ts2 = None
|
||||
if "cross" in filters:
|
||||
secondary = "ETH" if asset == "BTC" else "BTC"
|
||||
df2 = load_data(secondary, tf)
|
||||
kcr2 = keltner_ratio(df2["close"].values, df2["high"].values,
|
||||
df2["low"].values, bb_w)
|
||||
ts2 = df2["timestamp"].values
|
||||
|
||||
signals = []
|
||||
for ev in events:
|
||||
i = ev["idx"]
|
||||
if i < 1 or i >= n:
|
||||
continue
|
||||
first_ret = (c[i] - c[i - 1]) / c[i - 1] if c[i - 1] > 0 else 0
|
||||
if abs(first_ret) < 0.001:
|
||||
continue
|
||||
|
||||
skip = False
|
||||
|
||||
if "antifake" in filters:
|
||||
br = h[i] - l[i]
|
||||
if br > 0:
|
||||
if c[i] > c[i - 1] and (h[i] - c[i]) / br > 0.6:
|
||||
skip = True
|
||||
elif c[i] <= c[i - 1] and (c[i] - l[i]) / br > 0.6:
|
||||
skip = True
|
||||
|
||||
if not skip and "long_sq" in filters:
|
||||
if ev["dur"] < 10:
|
||||
skip = True
|
||||
|
||||
if not skip and "timing" in filters:
|
||||
hour = ts.iloc[i].hour
|
||||
if hour < 4 or hour > 16:
|
||||
skip = True
|
||||
|
||||
if not skip and "vol" in filters:
|
||||
avg_v = np.mean(v[ev["sq_start"]:i])
|
||||
if avg_v > 0 and v[i] <= avg_v * 1.3:
|
||||
skip = True
|
||||
|
||||
if not skip and "cross" in filters and kcr2 is not None and ts2 is not None:
|
||||
i2 = np.searchsorted(ts2, ts.values[i].astype("int64") // 10**6)
|
||||
i2 = min(i2, len(kcr2) - 1)
|
||||
cross_ok = any(
|
||||
not np.isnan(kcr2[j]) and kcr2[j] < 0.85
|
||||
for j in range(max(0, i2 - 10), i2 + 1)
|
||||
)
|
||||
if not cross_ok:
|
||||
skip = True
|
||||
|
||||
if skip:
|
||||
continue
|
||||
|
||||
signals.append(Signal(
|
||||
idx=i,
|
||||
direction=1 if first_ret > 0 else -1,
|
||||
entry_price=c[i - 1],
|
||||
metadata={"dur": ev["dur"], "filters": filters},
|
||||
))
|
||||
return signals
|
||||
|
||||
def report_all_presets(self, assets=None, timeframes=None, hold=3):
|
||||
"""Esegue tutti i preset di filtri × asset × tf."""
|
||||
assets = assets or self.default_assets
|
||||
timeframes = timeframes or self.default_timeframes
|
||||
all_results = []
|
||||
|
||||
for preset_name, filter_list in PRESETS.items():
|
||||
for asset in assets:
|
||||
for tf in timeframes:
|
||||
r = self.backtest(asset, tf, hold, filters=filter_list)
|
||||
if r and r.trades >= 20:
|
||||
r.strategy_name = f"SQ03 {preset_name}"
|
||||
all_results.append(r)
|
||||
|
||||
all_results.sort(key=lambda r: r.accuracy, reverse=True)
|
||||
|
||||
print(f"\n{'=' * 120}")
|
||||
print(f" SQ03 SQUEEZE FILTRATO — TUTTI I PRESET ({len(all_results)} config)")
|
||||
print(f" Fee: {self.fee_rt*100:.1f}% RT | Leva: {self.leverage:.0f}x | Pos: {self.position_size*100:.0f}%")
|
||||
print(f"{'=' * 120}")
|
||||
print(f" {'Nome':<30s} {'A/T':>7s} {'Trades':>6s} {'Acc':>6s} "
|
||||
f"{'PnL€':>10s} {'DD%':>6s} {'€/day':>7s} "
|
||||
f"{'Mkt%':>5s} {'Dur':>5s} {'Worst':>12s} {'Anni':>4s}")
|
||||
print(f" {'─' * 110}")
|
||||
|
||||
for r in all_results:
|
||||
r.print_summary()
|
||||
|
||||
if all_results:
|
||||
print(f"\n MIGLIORE: ", end="")
|
||||
best = all_results[0]
|
||||
best.print_yearly()
|
||||
|
||||
return all_results
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
strategy = SqueezeFiltered()
|
||||
strategy.report_all_presets()
|
||||
@@ -0,0 +1,204 @@
|
||||
"""SQ04 — Ultimate Squeeze — combinazione incrementale di tutti i filtri.
|
||||
|
||||
Testa combinazioni di filtri (antifake, long_sq, timing, cross-asset,
|
||||
correlation, volume, trend alignment, volatility regime) e classifica
|
||||
per accuracy.
|
||||
|
||||
IN:
|
||||
- OHLCV DataFrame (primario + secondario)
|
||||
- Parametri: bb_window, sq_threshold, lista filtri da attivare
|
||||
|
||||
OUT:
|
||||
- BacktestResult per ogni combinazione di filtri
|
||||
- Classifica globale
|
||||
|
||||
Risultati tipici:
|
||||
BTC 15m antifake+corr: 81.6% acc (ma concentrato 2018)
|
||||
BTC 15m antifake+vol: 79.7% acc, 1250 trades — robusto
|
||||
ETH 1h antifake+corr: 80.7% acc (solo 2018)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from src.strategies.base import Strategy, Signal
|
||||
from src.strategies.indicators import (
|
||||
keltner_ratio, detect_squeezes, ema, rv_annualized, rolling_correlation,
|
||||
)
|
||||
from src.data.downloader import load_data
|
||||
|
||||
|
||||
class SqueezeUltimate(Strategy):
|
||||
name = "SQ04_ultimate"
|
||||
description = "Ultimate squeeze — tutti i filtri combinabili"
|
||||
default_assets = ["BTC", "ETH"]
|
||||
default_timeframes = ["15m", "1h"]
|
||||
|
||||
FILTER_PRESETS = {
|
||||
"antifake+vol": ["antifake", "vol_confirm"],
|
||||
"antifake+corr": ["antifake", "corr_high"],
|
||||
"af+long+corr+trend": ["antifake", "long_sq", "corr_high", "trend_align"],
|
||||
"ALL": ["antifake", "long_sq", "cross", "timing", "corr_high",
|
||||
"vol_confirm", "trend_align", "low_rv"],
|
||||
}
|
||||
|
||||
def generate_signals(self, df: pd.DataFrame, ts: pd.DatetimeIndex,
|
||||
**params) -> list[Signal]:
|
||||
c = df["close"].values
|
||||
h = df["high"].values
|
||||
l = df["low"].values
|
||||
v = df["volume"].values
|
||||
n = len(c)
|
||||
|
||||
asset = params.get("asset", "BTC")
|
||||
tf = params.get("tf", "15m")
|
||||
filters = params.get("filters", ["antifake", "vol_confirm"])
|
||||
|
||||
kcr = keltner_ratio(c, h, l, 14)
|
||||
events = detect_squeezes(c, h, l, kcr)
|
||||
|
||||
secondary = "ETH" if asset == "BTC" else "BTC"
|
||||
df2 = load_data(secondary, tf)
|
||||
c2 = df2["close"].values
|
||||
kcr2 = keltner_ratio(c2, df2["high"].values, df2["low"].values, 14)
|
||||
ts2 = df2["timestamp"].values
|
||||
|
||||
ema_50 = ema(c, 50)
|
||||
rv_48 = rv_annualized(c, 48)
|
||||
corr = rolling_correlation(c, c2)
|
||||
|
||||
signals = []
|
||||
for ev in events:
|
||||
i = ev["idx"]
|
||||
if i < 1 or i >= n:
|
||||
continue
|
||||
first_ret = (c[i] - c[i - 1]) / c[i - 1] if c[i - 1] > 0 else 0
|
||||
if abs(first_ret) < 0.001:
|
||||
continue
|
||||
|
||||
skip = False
|
||||
for f in filters:
|
||||
if f == "antifake":
|
||||
br = h[i] - l[i]
|
||||
if br > 0:
|
||||
if c[i] > c[i-1] and (h[i] - c[i]) / br > 0.6:
|
||||
skip = True
|
||||
elif c[i] <= c[i-1] and (c[i] - l[i]) / br > 0.6:
|
||||
skip = True
|
||||
elif f == "long_sq":
|
||||
if ev["dur"] < 10:
|
||||
skip = True
|
||||
elif f == "timing":
|
||||
if ts.iloc[i].hour < 4 or ts.iloc[i].hour > 16:
|
||||
skip = True
|
||||
elif f == "cross":
|
||||
i2 = np.searchsorted(ts2, ts.values[i].astype("int64") // 10**6)
|
||||
i2 = min(i2, len(kcr2) - 1)
|
||||
if not any(not np.isnan(kcr2[j]) and kcr2[j] < 0.85
|
||||
for j in range(max(0, i2 - 10), i2 + 1)):
|
||||
skip = True
|
||||
elif f == "corr_high":
|
||||
if np.isnan(corr[i]) or abs(corr[i]) < 0.6:
|
||||
skip = True
|
||||
elif f == "vol_confirm":
|
||||
avg_v = np.mean(v[ev["sq_start"]:i])
|
||||
if avg_v > 0 and v[i] <= avg_v * 1.3:
|
||||
skip = True
|
||||
elif f == "trend_align":
|
||||
if not np.isnan(ema_50[i]):
|
||||
if first_ret > 0 and c[i] < ema_50[i]:
|
||||
skip = True
|
||||
elif first_ret < 0 and c[i] > ema_50[i]:
|
||||
skip = True
|
||||
elif f == "low_rv":
|
||||
if not np.isnan(rv_48[i]) and rv_48[i] >= 1.5:
|
||||
skip = True
|
||||
if skip:
|
||||
break
|
||||
|
||||
if skip:
|
||||
continue
|
||||
|
||||
signals.append(Signal(
|
||||
idx=i,
|
||||
direction=1 if first_ret > 0 else -1,
|
||||
entry_price=c[i - 1],
|
||||
metadata={"dur": ev["dur"], "filters": filters},
|
||||
))
|
||||
return signals
|
||||
|
||||
def backtest(self, asset: str, tf: str, hold: int = 3, **params):
|
||||
params.setdefault("asset", asset)
|
||||
params.setdefault("tf", tf)
|
||||
df = load_data(asset, tf)
|
||||
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||
signals = self.generate_signals(df, ts, **params)
|
||||
# Usa il backtest della base ma passando i segnali già generati
|
||||
from src.strategies.base import BacktestResult, YearlyStats, TF_MINUTES
|
||||
c = df["close"].values
|
||||
n = len(c)
|
||||
yearly: dict[int, dict] = {}
|
||||
capital = float(self.initial_capital)
|
||||
peak = capital
|
||||
max_dd = 0.0
|
||||
total_bars = 0
|
||||
for sig in signals:
|
||||
i = sig.idx
|
||||
if i + hold >= n or i < 1:
|
||||
continue
|
||||
entry = sig.entry_price
|
||||
exit_price = c[min(i + hold - 1, n - 1)]
|
||||
actual = (exit_price - entry) / entry * sig.direction
|
||||
net = actual * self.leverage - self.fee_rt * self.leverage
|
||||
capital += capital * self.position_size * net
|
||||
capital = max(capital, 10)
|
||||
if capital > peak: peak = capital
|
||||
dd = (peak - capital) / peak
|
||||
max_dd = max(max_dd, dd)
|
||||
total_bars += hold
|
||||
year = ts.iloc[i].year
|
||||
if year not in yearly:
|
||||
yearly[year] = {"w": 0, "t": 0, "pnl": 0.0}
|
||||
yearly[year]["t"] += 1
|
||||
if actual > 0: yearly[year]["w"] += 1
|
||||
yearly[year]["pnl"] += net * self.initial_capital
|
||||
all_t = sum(d["t"] for d in yearly.values())
|
||||
all_w = sum(d["w"] for d in yearly.values())
|
||||
if all_t == 0: return None
|
||||
yearly_stats = [YearlyStats(y, d["t"], d["w"], d["pnl"]) for y, d in sorted(yearly.items())]
|
||||
return BacktestResult(
|
||||
strategy_name=self.name, asset=asset, timeframe=tf, params=params,
|
||||
trades=all_t, wins=all_w, pnl=sum(d["pnl"] for d in yearly.values()),
|
||||
capital=capital, initial_capital=self.initial_capital,
|
||||
max_dd=max_dd * 100, time_in_market_pct=total_bars / n * 100,
|
||||
avg_trade_duration_h=hold * TF_MINUTES.get(tf, 60) / 60,
|
||||
years_active=len(yearly), yearly=yearly_stats,
|
||||
)
|
||||
|
||||
def report_all_presets(self):
|
||||
"""Esegue tutte le combinazioni preset × asset × tf."""
|
||||
all_results = []
|
||||
for preset_name, filter_list in self.FILTER_PRESETS.items():
|
||||
for asset in self.default_assets:
|
||||
for tf in self.default_timeframes:
|
||||
r = self.backtest(asset, tf, filters=filter_list)
|
||||
if r and r.trades >= 20:
|
||||
r.strategy_name = f"SQ04 {preset_name}"
|
||||
all_results.append(r)
|
||||
|
||||
all_results.sort(key=lambda r: r.accuracy, reverse=True)
|
||||
|
||||
print(f"\n{'=' * 120}")
|
||||
print(f" SQ04 ULTIMATE — TUTTI I PRESET")
|
||||
print(f"{'=' * 120}")
|
||||
for r in all_results:
|
||||
r.print_summary()
|
||||
return all_results
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
strategy = SqueezeUltimate()
|
||||
strategy.report_all_presets()
|
||||
@@ -0,0 +1,317 @@
|
||||
"""S3-01: Squeeze Migliorato — test per-anno, dati reali.
|
||||
Miglioramenti rispetto al squeeze base:
|
||||
1. Cross-asset: squeeze su BTC + ETH contemporaneo = segnale più forte
|
||||
2. Timing orario: accuracy per fascia oraria
|
||||
3. Squeeze duration weighted: squeeze lunghi → breakout più forti
|
||||
4. Dual-timeframe: squeeze su 1h confermato da 15m
|
||||
5. Anti-fakeout: skip se candela post-breakout ritraccia >50%
|
||||
6. Dynamic exit: trailing stop basato su ATR
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from src.data.downloader import load_data
|
||||
|
||||
FEE_RT = 0.002
|
||||
INITIAL = 1000
|
||||
LEVERAGE = 3
|
||||
|
||||
|
||||
def keltner_ratio(close, high, low, window=14):
|
||||
n = len(close)
|
||||
r = np.full(n, np.nan)
|
||||
for i in range(window, n):
|
||||
wc, wh, wl = close[i-window:i], high[i-window:i], low[i-window:i]
|
||||
ma = np.mean(wc)
|
||||
bb_std = np.std(wc)
|
||||
tr = np.maximum(wh-wl, np.maximum(np.abs(wh-np.roll(wc,1)), np.abs(wl-np.roll(wc,1))))
|
||||
atr = np.mean(tr[1:])
|
||||
kc = (ma+1.5*atr)-(ma-1.5*atr)
|
||||
bb = (ma+2*bb_std)-(ma-2*bb_std)
|
||||
if kc > 0:
|
||||
r[i] = bb/kc
|
||||
return r
|
||||
|
||||
|
||||
def atr_calc(high, low, close, period=14):
|
||||
tr = np.maximum(high-low, np.maximum(np.abs(high-np.roll(close,1)), np.abs(low-np.roll(close,1))))
|
||||
tr[0] = high[0]-low[0]
|
||||
r = np.full(len(close), np.nan)
|
||||
r[period-1] = np.mean(tr[:period])
|
||||
k = 2/(period+1)
|
||||
for i in range(period, len(close)):
|
||||
r[i] = tr[i]*k + r[i-1]*(1-k)
|
||||
return r
|
||||
|
||||
|
||||
def detect_squeezes(close, high, low, volume, kcr, sq_thr=0.8, min_dur=5):
|
||||
"""Ritorna lista di squeeze events con metadata."""
|
||||
events = []
|
||||
in_sq = False
|
||||
sq_start = 0
|
||||
n = len(close)
|
||||
|
||||
for i in range(1, n):
|
||||
if np.isnan(kcr[i]):
|
||||
continue
|
||||
is_sq = kcr[i] < sq_thr
|
||||
if is_sq and not in_sq:
|
||||
in_sq = True
|
||||
sq_start = i
|
||||
elif not is_sq and in_sq:
|
||||
in_sq = False
|
||||
dur = i - sq_start
|
||||
if dur < min_dur:
|
||||
continue
|
||||
avg_vol = np.mean(volume[sq_start:i])
|
||||
# Range durante squeeze
|
||||
sq_range = (np.max(high[sq_start:i]) - np.min(low[sq_start:i])) / close[sq_start] if close[sq_start] > 0 else 0
|
||||
events.append({
|
||||
"release_idx": i,
|
||||
"duration": dur,
|
||||
"avg_vol": avg_vol,
|
||||
"squeeze_range": sq_range,
|
||||
"kcr_at_release": kcr[i],
|
||||
})
|
||||
return events
|
||||
|
||||
|
||||
def run_improved_squeeze(primary_asset, tf="1h"):
|
||||
# Carica asset primario
|
||||
df = load_data(primary_asset, tf)
|
||||
c, h, l, v = df["close"].values, df["high"].values, df["low"].values, df["volume"].values
|
||||
n = len(df)
|
||||
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||
ts_ms = df["timestamp"].values
|
||||
|
||||
kcr = keltner_ratio(c, h, l, 14)
|
||||
atr_14 = atr_calc(h, l, c, 14)
|
||||
events = detect_squeezes(c, h, l, v, kcr)
|
||||
|
||||
# Carica asset secondario per cross-check
|
||||
secondary = "BTC" if primary_asset == "ETH" else "ETH"
|
||||
df2 = load_data(secondary, tf)
|
||||
c2, h2, l2 = df2["close"].values, df2["high"].values, df2["low"].values
|
||||
ts2_ms = df2["timestamp"].values
|
||||
kcr2 = keltner_ratio(c2, h2, l2, 14)
|
||||
|
||||
# Mappa ts2 → indici per allineare
|
||||
def find_idx2(ts_val):
|
||||
idx = np.searchsorted(ts2_ms, ts_val)
|
||||
return min(idx, len(c2)-1)
|
||||
|
||||
# Carica 15m per dual-TF
|
||||
if tf == "1h":
|
||||
df_15m = load_data(primary_asset, "15m")
|
||||
c15 = df_15m["close"].values
|
||||
h15 = df_15m["high"].values
|
||||
l15 = df_15m["low"].values
|
||||
ts15 = df_15m["timestamp"].values
|
||||
kcr_15m = keltner_ratio(c15, h15, l15, 14)
|
||||
else:
|
||||
kcr_15m = None
|
||||
ts15 = None
|
||||
|
||||
# ================================================================
|
||||
# CONFIGURAZIONI
|
||||
# ================================================================
|
||||
configs = [
|
||||
# (name, use_cross, use_timing, use_duration, use_dual_tf, use_antifake, use_trailing, hold, stop_atr)
|
||||
("BASE", False, False, False, False, False, False, 3, 0),
|
||||
("cross_asset", True, False, False, False, False, False, 3, 0),
|
||||
("timing_filter", False, True, False, False, False, False, 3, 0),
|
||||
("long_squeeze", False, False, True, False, False, False, 3, 0),
|
||||
("dual_tf", False, False, False, True, False, False, 3, 0),
|
||||
("anti_fakeout", False, False, False, False, True, False, 3, 0),
|
||||
("trailing_stop", False, False, False, False, False, True, 6, 1.5),
|
||||
("cross+timing", True, True, False, False, False, False, 3, 0),
|
||||
("cross+long+timing", True, True, True, False, False, False, 3, 0),
|
||||
("cross+dual_tf", True, False, False, True, False, False, 3, 0),
|
||||
("ALL_FILTERS", True, True, True, True, True, False, 3, 0),
|
||||
("ALL+trailing", True, True, True, True, True, True, 6, 1.5),
|
||||
("cross+antifake", True, False, False, False, True, False, 3, 0),
|
||||
("timing+antifake", False, True, False, False, True, False, 3, 0),
|
||||
("cross+timing+antifk", True, True, False, False, True, False, 3, 0),
|
||||
("cross+timing+trail", True, True, False, False, False, True, 6, 1.5),
|
||||
]
|
||||
|
||||
print(f"\n{'#'*75}")
|
||||
print(f" {primary_asset} {tf} — SQUEEZE MIGLIORATO")
|
||||
print(f"{'#'*75}")
|
||||
|
||||
results = []
|
||||
|
||||
for name, f_cross, f_timing, f_dur, f_dual, f_antifake, f_trail, hold, stop_atr_m in configs:
|
||||
yearly = {}
|
||||
capital = float(INITIAL)
|
||||
peak = capital
|
||||
max_dd = 0
|
||||
|
||||
for ev in events:
|
||||
i = ev["release_idx"]
|
||||
if i + hold + 2 >= n:
|
||||
continue
|
||||
|
||||
# --- FILTRI ---
|
||||
skip = False
|
||||
|
||||
# Cross-asset: secondary deve anche essere in squeeze recente o breakout
|
||||
if f_cross:
|
||||
i2 = find_idx2(ts_ms[i])
|
||||
if i2 >= 5:
|
||||
sec_in_squeeze = any(not np.isnan(kcr2[j]) and kcr2[j] < 0.85 for j in range(max(0,i2-10), i2+1))
|
||||
if not sec_in_squeeze:
|
||||
skip = True
|
||||
|
||||
# Timing: solo certe ore (testato: 6-14 UTC migliori)
|
||||
if f_timing:
|
||||
hour = ts.iloc[i].hour
|
||||
if hour < 4 or hour > 16:
|
||||
skip = True
|
||||
|
||||
# Duration: solo squeeze > 10 barre
|
||||
if f_dur:
|
||||
if ev["duration"] < 10:
|
||||
skip = True
|
||||
|
||||
# Dual-TF: squeeze anche su 15m
|
||||
if f_dual and kcr_15m is not None and ts15 is not None:
|
||||
i15 = np.searchsorted(ts15, ts_ms[i])
|
||||
if i15 >= 5:
|
||||
sq_15m = any(not np.isnan(kcr_15m[j]) and kcr_15m[j] < 0.85 for j in range(max(0,i15-20), i15+1))
|
||||
if not sq_15m:
|
||||
skip = True
|
||||
|
||||
# Anti-fakeout: prima candela post-breakout non deve ritracciare >50%
|
||||
if f_antifake and i + 1 < n:
|
||||
breakout_bar_range = h[i] - l[i]
|
||||
if breakout_bar_range > 0:
|
||||
if c[i] > c[i-1]: # breakout up
|
||||
retrace = (h[i] - c[i]) / breakout_bar_range
|
||||
else: # breakout down
|
||||
retrace = (c[i] - l[i]) / breakout_bar_range
|
||||
if retrace > 0.6:
|
||||
skip = True
|
||||
|
||||
if skip:
|
||||
continue
|
||||
|
||||
# --- DIREZIONE ---
|
||||
first_ret = (c[i] - c[i-1]) / c[i-1]
|
||||
if abs(first_ret) < 0.001:
|
||||
continue
|
||||
direction = 1 if first_ret > 0 else -1
|
||||
|
||||
# --- EXIT ---
|
||||
entry = c[i-1]
|
||||
if f_trail and not np.isnan(atr_14[i]):
|
||||
# Trailing stop
|
||||
trail_dist = atr_14[i] * stop_atr_m
|
||||
best_price = entry
|
||||
exit_price = c[min(i+hold, n-1)]
|
||||
for j in range(i, min(i+hold+1, n)):
|
||||
if direction == 1:
|
||||
best_price = max(best_price, h[j])
|
||||
if l[j] <= best_price - trail_dist:
|
||||
exit_price = best_price - trail_dist
|
||||
break
|
||||
else:
|
||||
best_price = min(best_price, l[j])
|
||||
if h[j] >= best_price + trail_dist:
|
||||
exit_price = best_price + trail_dist
|
||||
break
|
||||
exit_price = c[j]
|
||||
else:
|
||||
exit_price = c[min(i+hold-1, n-1)]
|
||||
|
||||
actual = (exit_price - entry) / entry * direction
|
||||
net = actual * LEVERAGE - FEE_RT * LEVERAGE
|
||||
|
||||
capital += capital * 0.15 * net
|
||||
capital = max(capital, 10)
|
||||
if capital > peak: peak = capital
|
||||
dd = (peak - capital) / peak
|
||||
max_dd = max(max_dd, dd)
|
||||
|
||||
year = ts.iloc[i].year
|
||||
if year not in yearly:
|
||||
yearly[year] = {"wins": 0, "total": 0, "pnls": []}
|
||||
yearly[year]["total"] += 1
|
||||
if actual > 0:
|
||||
yearly[year]["wins"] += 1
|
||||
yearly[year]["pnls"].append(net * INITIAL)
|
||||
|
||||
all_t = sum(d["total"] for d in yearly.values())
|
||||
all_w = sum(d["wins"] for d in yearly.values())
|
||||
if all_t < 30:
|
||||
continue
|
||||
|
||||
acc = all_w / all_t * 100
|
||||
all_pnls = [p for d in yearly.values() for p in d["pnls"]]
|
||||
tot_pnl = sum(all_pnls)
|
||||
|
||||
# Worst year
|
||||
worst_y_acc = 100
|
||||
worst_y = ""
|
||||
for y, d in yearly.items():
|
||||
ya = d["wins"]/d["total"]*100 if d["total"] > 0 else 0
|
||||
if ya < worst_y_acc:
|
||||
worst_y_acc = ya
|
||||
worst_y = str(y)
|
||||
|
||||
results.append({
|
||||
"name": name, "trades": all_t, "acc": acc, "pnl": tot_pnl,
|
||||
"max_dd": max_dd*100, "capital": capital,
|
||||
"worst": f"{worst_y}({worst_y_acc:.0f}%)",
|
||||
"yearly": yearly,
|
||||
})
|
||||
|
||||
# Sort by accuracy
|
||||
results.sort(key=lambda x: x["acc"], reverse=True)
|
||||
|
||||
print(f"\n {'Name':.<26s} {'Trades':>7s} {'Acc':>6s} {'PnL€':>9s} {'DD%':>6s} {'Capital':>10s} {'Worst':>12s}")
|
||||
print(f" {'-'*80}")
|
||||
for r in results:
|
||||
tag = "✅✅" if r["acc"] >= 80 else "✅" if r["acc"] >= 76 else ""
|
||||
print(f" {r['name']:.<26s} {r['trades']:>7d} {r['acc']:>5.1f}% €{r['pnl']:>+8.0f} {r['max_dd']:>5.1f}% €{r['capital']:>9,.0f} {r['worst']:>12s} {tag}")
|
||||
|
||||
# Dettaglio per anno del migliore
|
||||
if results:
|
||||
best = results[0]
|
||||
print(f"\n MIGLIORE: {best['name']} → {best['acc']:.1f}% acc")
|
||||
print(f" {'Anno':>6s} {'Trades':>7s} {'Acc':>6s} {'PnL€':>9s}")
|
||||
for y in sorted(best["yearly"]):
|
||||
d = best["yearly"][y]
|
||||
ya = d["wins"]/d["total"]*100 if d["total"] > 0 else 0
|
||||
yp = sum(d["pnls"])
|
||||
tag = " ← CRASH" if y in [2020,2021,2022] else ""
|
||||
print(f" {y:>6d} {d['total']:>7d} {ya:>5.1f}% €{yp:>+8.0f}{tag}")
|
||||
|
||||
return results
|
||||
|
||||
|
||||
# Run su entrambi gli asset e timeframe
|
||||
all_results = {}
|
||||
for asset in ["ETH", "BTC"]:
|
||||
for tf in ["1h", "15m"]:
|
||||
key = f"{asset}_{tf}"
|
||||
all_results[key] = run_improved_squeeze(asset, tf)
|
||||
|
||||
# Classifica globale
|
||||
print(f"\n\n{'='*75}")
|
||||
print(f" CLASSIFICA GLOBALE — TOP 15")
|
||||
print(f"{'='*75}")
|
||||
|
||||
global_list = []
|
||||
for key, results in all_results.items():
|
||||
for r in results:
|
||||
global_list.append({**r, "asset_tf": key})
|
||||
|
||||
global_list.sort(key=lambda x: x["acc"], reverse=True)
|
||||
print(f"\n {'Asset_TF':.<12s} {'Name':.<26s} {'Trades':>6s} {'Acc':>6s} {'PnL€':>9s} {'DD%':>5s} {'Worst':>12s}")
|
||||
for r in global_list[:15]:
|
||||
tag = "✅✅" if r["acc"] >= 80 else "✅" if r["acc"] >= 76 else ""
|
||||
print(f" {r['asset_tf']:.<12s} {r['name']:.<26s} {r['trades']:>6d} {r['acc']:>5.1f}% €{r['pnl']:>+8.0f} {r['max_dd']:>4.1f}% {r['worst']:>12s} {tag}")
|
||||
@@ -0,0 +1,290 @@
|
||||
"""S3-02: Lead-lag multi-asset squeeze.
|
||||
Quando BTC fa squeeze breakout, ETH/SOL spesso seguono.
|
||||
Usa il breakout di BTC per anticipare entrata su ETH (e viceversa).
|
||||
Testa anche correlazione inter-asset per conferma segnale.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from src.data.downloader import load_data
|
||||
|
||||
FEE_RT = 0.002
|
||||
INITIAL = 1000
|
||||
LEVERAGE = 3
|
||||
|
||||
|
||||
def keltner_ratio(close, high, low, window=14):
|
||||
n = len(close)
|
||||
r = np.full(n, np.nan)
|
||||
for i in range(window, n):
|
||||
wc, wh, wl = close[i-window:i], high[i-window:i], low[i-window:i]
|
||||
ma = np.mean(wc)
|
||||
bb_std = np.std(wc)
|
||||
tr = np.maximum(wh-wl, np.maximum(np.abs(wh-np.roll(wc,1)), np.abs(wl-np.roll(wc,1))))
|
||||
atr = np.mean(tr[1:])
|
||||
kc = (ma+1.5*atr)-(ma-1.5*atr)
|
||||
bb = (ma+2*bb_std)-(ma-2*bb_std)
|
||||
if kc > 0: r[i] = bb/kc
|
||||
return r
|
||||
|
||||
|
||||
def load_aligned(assets, tf):
|
||||
"""Carica e allinea dati multi-asset per timestamp."""
|
||||
dfs = {}
|
||||
for asset in assets:
|
||||
try:
|
||||
if asset == "SOL":
|
||||
df = pd.read_parquet(f"data/raw/sol_{tf}.parquet")
|
||||
df["datetime"] = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||
else:
|
||||
df = load_data(asset, tf)
|
||||
dfs[asset] = df
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if len(dfs) < 2:
|
||||
return None
|
||||
|
||||
# Allinea per timestamp
|
||||
common_ts = set(dfs[list(dfs.keys())[0]]["timestamp"].values)
|
||||
for df in dfs.values():
|
||||
common_ts &= set(df["timestamp"].values)
|
||||
common_ts = sorted(common_ts)
|
||||
|
||||
aligned = {}
|
||||
for asset, df in dfs.items():
|
||||
mask = df["timestamp"].isin(common_ts)
|
||||
aligned[asset] = df[mask].sort_values("timestamp").reset_index(drop=True)
|
||||
|
||||
return aligned
|
||||
|
||||
|
||||
def detect_breakouts(close, high, low, volume, kcr, sq_thr=0.8, min_dur=5):
|
||||
"""Detect squeeze breakout events."""
|
||||
events = []
|
||||
in_sq = False
|
||||
sq_start = 0
|
||||
for i in range(1, len(close)):
|
||||
if np.isnan(kcr[i]):
|
||||
continue
|
||||
is_sq = kcr[i] < sq_thr
|
||||
if is_sq and not in_sq:
|
||||
in_sq = True
|
||||
sq_start = i
|
||||
elif not is_sq and in_sq:
|
||||
in_sq = False
|
||||
if i - sq_start < min_dur:
|
||||
continue
|
||||
first_ret = (close[i] - close[i-1]) / close[i-1] if close[i-1] > 0 else 0
|
||||
if abs(first_ret) < 0.001:
|
||||
continue
|
||||
events.append({
|
||||
"idx": i,
|
||||
"duration": i - sq_start,
|
||||
"direction": 1 if first_ret > 0 else -1,
|
||||
"first_ret": first_ret,
|
||||
})
|
||||
return events
|
||||
|
||||
|
||||
print("=" * 75)
|
||||
print(" S3-02: LEAD-LAG MULTI-ASSET SQUEEZE")
|
||||
print("=" * 75)
|
||||
|
||||
for tf in ["1h", "15m"]:
|
||||
aligned = load_aligned(["BTC", "ETH", "SOL"], tf)
|
||||
if aligned is None:
|
||||
continue
|
||||
|
||||
n = len(aligned["BTC"])
|
||||
ts = pd.to_datetime(aligned["BTC"]["timestamp"], unit="ms", utc=True)
|
||||
|
||||
print(f"\n Timeframe: {tf}, Candles allineate: {n}")
|
||||
|
||||
# Calcola squeeze per ogni asset
|
||||
asset_data = {}
|
||||
for asset in aligned:
|
||||
df = aligned[asset]
|
||||
c, h, l, v = df["close"].values, df["high"].values, df["low"].values, df["volume"].values
|
||||
kcr = keltner_ratio(c, h, l, 14)
|
||||
events = detect_breakouts(c, h, l, v, kcr)
|
||||
asset_data[asset] = {"close": c, "high": h, "low": l, "vol": v, "kcr": kcr, "events": events}
|
||||
print(f" {asset}: {len(events)} squeeze breakouts")
|
||||
|
||||
# ================================================================
|
||||
# STRATEGIA A: Leader-follower
|
||||
# Quando BTC fa breakout, entra su ETH/SOL nella stessa direzione
|
||||
# ================================================================
|
||||
print(f"\n --- LEADER-FOLLOWER ({tf}) ---")
|
||||
|
||||
for leader, follower in [("BTC", "ETH"), ("BTC", "SOL"), ("ETH", "BTC"), ("ETH", "SOL")]:
|
||||
if leader not in asset_data or follower not in asset_data:
|
||||
continue
|
||||
|
||||
leader_events = asset_data[leader]["events"]
|
||||
fc = asset_data[follower]["close"]
|
||||
|
||||
for hold in [3, 6]:
|
||||
for delay in [0, 1, 2]:
|
||||
yearly = {}
|
||||
|
||||
for ev in leader_events:
|
||||
i = ev["idx"] + delay
|
||||
if i + hold >= n:
|
||||
continue
|
||||
|
||||
# Anti-fakeout su follower
|
||||
entry = fc[i]
|
||||
exit_price = fc[min(i + hold, n - 1)]
|
||||
direction = ev["direction"]
|
||||
actual = (exit_price - entry) / entry * direction
|
||||
net = actual * LEVERAGE - FEE_RT * LEVERAGE
|
||||
|
||||
year = ts.iloc[min(i, n-1)].year
|
||||
if year not in yearly:
|
||||
yearly[year] = {"w": 0, "t": 0, "pnls": []}
|
||||
yearly[year]["t"] += 1
|
||||
if actual > 0:
|
||||
yearly[year]["w"] += 1
|
||||
yearly[year]["pnls"].append(net * INITIAL)
|
||||
|
||||
all_t = sum(d["t"] for d in yearly.values())
|
||||
all_w = sum(d["w"] for d in yearly.values())
|
||||
if all_t < 30:
|
||||
continue
|
||||
acc = all_w / all_t * 100
|
||||
pnl = sum(p for d in yearly.values() for p in d["pnls"])
|
||||
worst_y = min(yearly.items(), key=lambda x: x[1]["w"]/x[1]["t"] if x[1]["t"]>0 else 0)
|
||||
worst_acc = worst_y[1]["w"]/worst_y[1]["t"]*100 if worst_y[1]["t"]>0 else 0
|
||||
tag = "✅" if acc >= 76 else ""
|
||||
print(f" {leader}→{follower} d={delay} h={hold}: trades={all_t:5d} acc={acc:.1f}% pnl=€{pnl:+.0f} worst={worst_y[0]}({worst_acc:.0f}%) {tag}")
|
||||
|
||||
# ================================================================
|
||||
# STRATEGIA B: Consensus multi-asset
|
||||
# Trade solo quando 2+ asset hanno squeeze breakout nello stesso momento
|
||||
# ================================================================
|
||||
print(f"\n --- CONSENSUS MULTI-ASSET ({tf}) ---")
|
||||
|
||||
# Build event map: timestamp → list of (asset, direction)
|
||||
event_map = {}
|
||||
for asset, data in asset_data.items():
|
||||
for ev in data["events"]:
|
||||
idx = ev["idx"]
|
||||
if idx not in event_map:
|
||||
event_map[idx] = []
|
||||
event_map[idx].append((asset, ev["direction"]))
|
||||
|
||||
for target in ["BTC", "ETH", "SOL"]:
|
||||
if target not in asset_data:
|
||||
continue
|
||||
tc = asset_data[target]["close"]
|
||||
|
||||
for min_consensus in [2, 3]:
|
||||
for window_bars in [1, 3, 5]:
|
||||
yearly = {}
|
||||
daily_done = set()
|
||||
|
||||
for idx in sorted(event_map.keys()):
|
||||
if idx + 6 >= n:
|
||||
continue
|
||||
|
||||
day = ts.iloc[idx].strftime("%Y-%m-%d")
|
||||
if day in daily_done:
|
||||
continue
|
||||
|
||||
# Count consensus within window
|
||||
nearby_events = []
|
||||
for j in range(max(0, idx - window_bars), idx + window_bars + 1):
|
||||
if j in event_map:
|
||||
nearby_events.extend(event_map[j])
|
||||
|
||||
# Unique assets
|
||||
unique_assets = set(a for a, d in nearby_events)
|
||||
if len(unique_assets) < min_consensus:
|
||||
continue
|
||||
|
||||
# Majority direction
|
||||
dirs = [d for a, d in nearby_events]
|
||||
majority = 1 if sum(dirs) > 0 else -1
|
||||
|
||||
entry = tc[idx]
|
||||
exit_price = tc[min(idx + 3, n - 1)]
|
||||
actual = (exit_price - entry) / entry * majority
|
||||
net = actual * LEVERAGE - FEE_RT * LEVERAGE
|
||||
|
||||
year = ts.iloc[idx].year
|
||||
if year not in yearly:
|
||||
yearly[year] = {"w": 0, "t": 0, "pnls": []}
|
||||
yearly[year]["t"] += 1
|
||||
if actual > 0:
|
||||
yearly[year]["w"] += 1
|
||||
yearly[year]["pnls"].append(net * INITIAL)
|
||||
daily_done.add(day)
|
||||
|
||||
all_t = sum(d["t"] for d in yearly.values())
|
||||
all_w = sum(d["w"] for d in yearly.values())
|
||||
if all_t < 20:
|
||||
continue
|
||||
acc = all_w / all_t * 100
|
||||
pnl = sum(p for d in yearly.values() for p in d["pnls"])
|
||||
tag = "✅" if acc >= 76 else ""
|
||||
print(f" {target} consensus>={min_consensus} w={window_bars}: trades={all_t:4d} acc={acc:.1f}% pnl=€{pnl:+.0f} {tag}")
|
||||
|
||||
# ================================================================
|
||||
# STRATEGIA C: Correlation-weighted squeeze
|
||||
# Peso il segnale squeeze in base alla correlazione rolling con BTC
|
||||
# ================================================================
|
||||
print(f"\n --- CORRELATION-WEIGHTED ({tf}) ---")
|
||||
|
||||
for target in ["ETH", "SOL"]:
|
||||
if target not in asset_data:
|
||||
continue
|
||||
tc = asset_data[target]["close"]
|
||||
btc_c = asset_data["BTC"]["close"]
|
||||
|
||||
# Rolling correlation
|
||||
corr_window = 48 # 48 bars
|
||||
rolling_corr = np.full(n, np.nan)
|
||||
ret_t = np.diff(np.log(np.where(tc == 0, 1e-10, tc)))
|
||||
ret_b = np.diff(np.log(np.where(btc_c == 0, 1e-10, btc_c)))
|
||||
for i in range(corr_window, len(ret_t)):
|
||||
c_val = np.corrcoef(ret_t[i-corr_window:i], ret_b[i-corr_window:i])[0, 1]
|
||||
rolling_corr[i + 1] = c_val if np.isfinite(c_val) else 0
|
||||
|
||||
events = asset_data[target]["events"]
|
||||
|
||||
for corr_thr in [0.5, 0.6, 0.7, 0.8]:
|
||||
yearly = {}
|
||||
for ev in events:
|
||||
i = ev["idx"]
|
||||
if i + 3 >= n or np.isnan(rolling_corr[i]):
|
||||
continue
|
||||
|
||||
# Solo quando correlazione con BTC è alta
|
||||
if abs(rolling_corr[i]) < corr_thr:
|
||||
continue
|
||||
|
||||
entry = tc[i - 1]
|
||||
exit_price = tc[min(i + 2, n - 1)]
|
||||
actual = (exit_price - entry) / entry * ev["direction"]
|
||||
net = actual * LEVERAGE - FEE_RT * LEVERAGE
|
||||
|
||||
year = ts.iloc[i].year
|
||||
if year not in yearly:
|
||||
yearly[year] = {"w": 0, "t": 0, "pnls": []}
|
||||
yearly[year]["t"] += 1
|
||||
if actual > 0:
|
||||
yearly[year]["w"] += 1
|
||||
yearly[year]["pnls"].append(net * INITIAL)
|
||||
|
||||
all_t = sum(d["t"] for d in yearly.values())
|
||||
all_w = sum(d["w"] for d in yearly.values())
|
||||
if all_t < 20:
|
||||
continue
|
||||
acc = all_w / all_t * 100
|
||||
pnl = sum(p for d in yearly.values() for p in d["pnls"])
|
||||
tag = "✅" if acc >= 76 else ""
|
||||
print(f" {target} corr>={corr_thr}: trades={all_t:4d} acc={acc:.1f}% pnl=€{pnl:+.0f} {tag}")
|
||||
@@ -0,0 +1,256 @@
|
||||
"""S3-03: Ultimate Squeeze — combina TUTTI i filtri migliori.
|
||||
Filtri che funzionano (testati singolarmente):
|
||||
- Anti-fakeout (+1% acc)
|
||||
- Long squeeze duration (+1% acc)
|
||||
- Cross-asset squeeze simultaneo (+0.5%)
|
||||
- Timing 4-16 UTC (+0.5%)
|
||||
- Correlation ETH-BTC alta per ETH trades (+1%)
|
||||
- Volume confirmation al breakout
|
||||
|
||||
Nuovi filtri da testare:
|
||||
- Volume delta: up_volume - down_volume al breakout
|
||||
- Momentum confirmation: breakout nella direzione del trend 1h
|
||||
- Volatility regime: skip in regime estremo (RV > 100%)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from src.data.downloader import load_data
|
||||
|
||||
FEE_RT = 0.002
|
||||
INITIAL = 1000
|
||||
LEVERAGE = 3
|
||||
|
||||
|
||||
def keltner_ratio(close, high, low, window=14):
|
||||
n = len(close)
|
||||
r = np.full(n, np.nan)
|
||||
for i in range(window, n):
|
||||
wc, wh, wl = close[i-window:i], high[i-window:i], low[i-window:i]
|
||||
ma = np.mean(wc)
|
||||
bb_std = np.std(wc)
|
||||
tr = np.maximum(wh-wl, np.maximum(np.abs(wh-np.roll(wc,1)), np.abs(wl-np.roll(wc,1))))
|
||||
atr = np.mean(tr[1:])
|
||||
kc = (ma+1.5*atr)-(ma-1.5*atr)
|
||||
bb = (ma+2*bb_std)-(ma-2*bb_std)
|
||||
if kc > 0: r[i] = bb/kc
|
||||
return r
|
||||
|
||||
|
||||
def ema(arr, period):
|
||||
r = np.full(len(arr), np.nan)
|
||||
k = 2/(period+1)
|
||||
r[period-1] = np.mean(arr[:period])
|
||||
for i in range(period, len(arr)):
|
||||
r[i] = arr[i]*k + r[i-1]*(1-k)
|
||||
return r
|
||||
|
||||
|
||||
def rv_ann(close, window):
|
||||
lr = np.diff(np.log(np.where(close == 0, 1e-10, close)))
|
||||
r = np.full(len(close), np.nan)
|
||||
for i in range(window, len(lr)):
|
||||
r[i+1] = np.std(lr[i-window:i]) * np.sqrt(24*365)
|
||||
return r
|
||||
|
||||
|
||||
def run_ultimate(primary, tf="15m"):
|
||||
secondary = "ETH" if primary == "BTC" else "BTC"
|
||||
|
||||
df = load_data(primary, tf)
|
||||
c, h, l, v = df["close"].values, df["high"].values, df["low"].values, df["volume"].values
|
||||
n = len(df)
|
||||
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||
|
||||
df2 = load_data(secondary, tf)
|
||||
c2, ts2 = df2["close"].values, df2["timestamp"].values
|
||||
|
||||
kcr = keltner_ratio(c, h, l, 14)
|
||||
kcr2 = keltner_ratio(c2, df2["high"].values, df2["low"].values, 14)
|
||||
|
||||
ema_50 = ema(c, 50)
|
||||
rv_48 = rv_ann(c, 48)
|
||||
|
||||
# Rolling correlation
|
||||
ret1 = np.diff(np.log(np.where(c == 0, 1e-10, c)))
|
||||
ret2 = np.diff(np.log(np.where(c2[:len(c)] == 0, 1e-10, c2[:len(c)])))
|
||||
min_len = min(len(ret1), len(ret2))
|
||||
ret1 = ret1[:min_len]
|
||||
ret2 = ret2[:min_len]
|
||||
corr = np.full(n, np.nan)
|
||||
for i in range(48, min_len):
|
||||
cv = np.corrcoef(ret1[i-48:i], ret2[i-48:i])[0,1]
|
||||
corr[i+1] = cv if np.isfinite(cv) else 0
|
||||
|
||||
# Detect squeezes
|
||||
events = []
|
||||
in_sq = False
|
||||
sq_start = 0
|
||||
for i in range(15, n):
|
||||
if np.isnan(kcr[i]): continue
|
||||
is_sq = kcr[i] < 0.8
|
||||
if is_sq and not in_sq:
|
||||
in_sq = True
|
||||
sq_start = i
|
||||
elif not is_sq and in_sq:
|
||||
in_sq = False
|
||||
dur = i - sq_start
|
||||
if dur < 5 or i + 6 >= n:
|
||||
continue
|
||||
events.append({"idx": i, "dur": dur, "sq_start": sq_start})
|
||||
|
||||
print(f"\n{'#'*70}")
|
||||
print(f" {primary} {tf} — ULTIMATE SQUEEZE ({len(events)} squeeze events)")
|
||||
print(f"{'#'*70}")
|
||||
|
||||
filters_map = {
|
||||
"antifake": lambda ev, i: not _antifake(c, h, l, i),
|
||||
"long_sq": lambda ev, i: ev["dur"] >= 10,
|
||||
"timing": lambda ev, i: 4 <= ts.iloc[i].hour <= 16,
|
||||
"cross": lambda ev, i: _cross_squeeze(kcr2, i, ts, ts2),
|
||||
"corr_high": lambda ev, i: not np.isnan(corr[i]) and abs(corr[i]) >= 0.6,
|
||||
"vol_confirm": lambda ev, i: _vol_confirm(v, i, ev["sq_start"]),
|
||||
"trend_align": lambda ev, i: _trend_align(c, ema_50, i),
|
||||
"low_rv": lambda ev, i: not np.isnan(rv_48[i]) and rv_48[i] < 1.5,
|
||||
}
|
||||
|
||||
def _antifake(c, h, l, i):
|
||||
if i + 1 >= len(c): return False
|
||||
br = h[i] - l[i]
|
||||
if br <= 0: return False
|
||||
if c[i] > c[i-1]:
|
||||
return (h[i] - c[i]) / br > 0.6
|
||||
return (c[i] - l[i]) / br > 0.6
|
||||
|
||||
def _cross_squeeze(kcr2, i, ts1, ts2_arr):
|
||||
i2 = np.searchsorted(ts2_arr, ts.values[i].astype("int64") // 10**6)
|
||||
i2 = min(i2, len(kcr2)-1)
|
||||
return any(not np.isnan(kcr2[j]) and kcr2[j] < 0.85 for j in range(max(0,i2-10), i2+1))
|
||||
|
||||
def _vol_confirm(v, i, sq_start):
|
||||
avg = np.mean(v[sq_start:i])
|
||||
return avg > 0 and v[i] > avg * 1.3
|
||||
|
||||
def _trend_align(c, ema_val, i):
|
||||
if np.isnan(ema_val[i]): return True
|
||||
first_ret = (c[i] - c[i-1]) / c[i-1] if c[i-1] > 0 else 0
|
||||
if first_ret > 0:
|
||||
return c[i] > ema_val[i]
|
||||
return c[i] < ema_val[i]
|
||||
|
||||
# Test combinazioni incrementali
|
||||
combos = [
|
||||
("BASE", []),
|
||||
("antifake", ["antifake"]),
|
||||
("long_sq", ["long_sq"]),
|
||||
("antifake+long", ["antifake", "long_sq"]),
|
||||
("antifake+timing", ["antifake", "timing"]),
|
||||
("antifake+cross", ["antifake", "cross"]),
|
||||
("antifake+corr", ["antifake", "corr_high"]),
|
||||
("antifake+vol", ["antifake", "vol_confirm"]),
|
||||
("antifake+trend", ["antifake", "trend_align"]),
|
||||
("af+long+timing", ["antifake", "long_sq", "timing"]),
|
||||
("af+long+cross", ["antifake", "long_sq", "cross"]),
|
||||
("af+long+corr", ["antifake", "long_sq", "corr_high"]),
|
||||
("af+long+trend", ["antifake", "long_sq", "trend_align"]),
|
||||
("af+long+cross+time", ["antifake", "long_sq", "cross", "timing"]),
|
||||
("af+long+corr+time", ["antifake", "long_sq", "corr_high", "timing"]),
|
||||
("af+long+corr+trend", ["antifake", "long_sq", "corr_high", "trend_align"]),
|
||||
("ALL_NO_VOL", ["antifake", "long_sq", "cross", "timing", "corr_high", "trend_align", "low_rv"]),
|
||||
("ALL", ["antifake", "long_sq", "cross", "timing", "corr_high", "vol_confirm", "trend_align", "low_rv"]),
|
||||
("BEST_5", ["antifake", "long_sq", "corr_high", "trend_align", "low_rv"]),
|
||||
]
|
||||
|
||||
results = []
|
||||
for combo_name, filter_names in combos:
|
||||
yearly = {}
|
||||
capital = float(INITIAL)
|
||||
peak = capital
|
||||
max_dd = 0
|
||||
|
||||
for ev in events:
|
||||
i = ev["idx"]
|
||||
first_ret = (c[i] - c[i-1]) / c[i-1] if c[i-1] > 0 else 0
|
||||
if abs(first_ret) < 0.001:
|
||||
continue
|
||||
|
||||
skip = False
|
||||
for fn in filter_names:
|
||||
if fn in filters_map and not filters_map[fn](ev, i):
|
||||
skip = True
|
||||
break
|
||||
if skip:
|
||||
continue
|
||||
|
||||
direction = 1 if first_ret > 0 else -1
|
||||
entry = c[i-1]
|
||||
exit_price = c[min(i+2, n-1)]
|
||||
actual = (exit_price - entry) / entry * direction
|
||||
net = actual * LEVERAGE - FEE_RT * LEVERAGE
|
||||
|
||||
capital += capital * 0.15 * net
|
||||
capital = max(capital, 10)
|
||||
if capital > peak: peak = capital
|
||||
dd = (peak - capital) / peak
|
||||
max_dd = max(max_dd, dd)
|
||||
|
||||
year = ts.iloc[i].year
|
||||
if year not in yearly:
|
||||
yearly[year] = {"w": 0, "t": 0, "pnls": []}
|
||||
yearly[year]["t"] += 1
|
||||
if actual > 0: yearly[year]["w"] += 1
|
||||
yearly[year]["pnls"].append(net * INITIAL)
|
||||
|
||||
all_t = sum(d["t"] for d in yearly.values())
|
||||
all_w = sum(d["w"] for d in yearly.values())
|
||||
if all_t < 20: continue
|
||||
|
||||
acc = all_w / all_t * 100
|
||||
pnl = sum(p for d in yearly.values() for p in d["pnls"])
|
||||
worst = min(yearly.items(), key=lambda x: x[1]["w"]/x[1]["t"] if x[1]["t"]>0 else 0)
|
||||
wa = worst[1]["w"]/worst[1]["t"]*100 if worst[1]["t"]>0 else 0
|
||||
|
||||
results.append({
|
||||
"name": combo_name, "trades": all_t, "acc": acc, "pnl": pnl,
|
||||
"dd": max_dd*100, "capital": capital, "worst": f"{worst[0]}({wa:.0f}%)",
|
||||
"yearly": yearly,
|
||||
})
|
||||
|
||||
results.sort(key=lambda x: x["acc"], reverse=True)
|
||||
|
||||
print(f"\n {'Name':.<28s} {'Trades':>6s} {'Acc':>6s} {'PnL€':>9s} {'DD%':>5s} {'Worst':>12s}")
|
||||
print(f" {'-'*70}")
|
||||
for r in results[:20]:
|
||||
tag = "✅✅" if r["acc"] >= 80 else "✅" if r["acc"] >= 78 else ""
|
||||
print(f" {r['name']:.<28s} {r['trades']:>6d} {r['acc']:>5.1f}% €{r['pnl']:>+8.0f} {r['dd']:>4.1f}% {r['worst']:>12s} {tag}")
|
||||
|
||||
# Dettaglio migliore
|
||||
if results:
|
||||
best = results[0]
|
||||
print(f"\n MIGLIORE: {best['name']} → {best['acc']:.1f}% acc, DD {best['dd']:.1f}%")
|
||||
for y in sorted(best["yearly"]):
|
||||
d = best["yearly"][y]
|
||||
ya = d["w"]/d["t"]*100 if d["t"]>0 else 0
|
||||
tag = " ← CRASH" if y in [2020,2021,2022] else ""
|
||||
print(f" {y}: {d['t']:4d}t {ya:5.1f}% €{sum(d['pnls']):+.0f}{tag}")
|
||||
|
||||
return results
|
||||
|
||||
|
||||
all_r = []
|
||||
for asset in ["BTC", "ETH"]:
|
||||
for tf in ["15m", "1h"]:
|
||||
r = run_ultimate(asset, tf)
|
||||
for x in r:
|
||||
all_r.append({**x, "key": f"{asset}_{tf}"})
|
||||
|
||||
all_r.sort(key=lambda x: x["acc"], reverse=True)
|
||||
print(f"\n\n{'='*70}")
|
||||
print(f" TOP 10 GLOBALE")
|
||||
print(f"{'='*70}")
|
||||
for r in all_r[:10]:
|
||||
tag = "✅✅" if r["acc"] >= 80 else "✅" if r["acc"] >= 78 else ""
|
||||
print(f" {r['key']:.<10s} {r['name']:.<28s} {r['trades']:>5d} {r['acc']:>5.1f}% €{r['pnl']:>+8.0f} DD {r['dd']:.1f}% {r['worst']:>12s} {tag}")
|
||||
@@ -0,0 +1,131 @@
|
||||
"""IB01 — Inside Bar Breakout.
|
||||
|
||||
Pattern di compressione a singola candela: quando una barra ha high < prev high
|
||||
E low > prev low, il prezzo si sta comprimendo. Al breakout del range della
|
||||
inside bar, segui la direzione.
|
||||
|
||||
17% delle candele 15m sono inside bars → frequenza altissima.
|
||||
|
||||
IN:
|
||||
- OHLCV DataFrame
|
||||
- Parametri: min_consecutive (N inside bars consecutivi),
|
||||
volume_filter, breakout_confirm
|
||||
|
||||
OUT:
|
||||
- Signal al breakout del range dell'inside bar
|
||||
- BacktestResult
|
||||
|
||||
Logica:
|
||||
1. Identifica N inside bars consecutivi (compressione)
|
||||
2. Quando il prezzo rompe il range → entra nella direzione del breakout
|
||||
3. Filtro: volume al breakout > media
|
||||
4. Hold fisso
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from src.strategies.base import Strategy, Signal
|
||||
|
||||
|
||||
class InsideBarBreakout(Strategy):
|
||||
name = "IB01_inside_bar"
|
||||
description = "Inside bar breakout — compressione a singola candela"
|
||||
default_assets = ["BTC", "ETH"]
|
||||
default_timeframes = ["15m", "1h"]
|
||||
fee_rt = 0.002
|
||||
|
||||
def generate_signals(self, df, ts, **params):
|
||||
c = df["close"].values
|
||||
h = df["high"].values
|
||||
l = df["low"].values
|
||||
v = df["volume"].values
|
||||
n = len(c)
|
||||
|
||||
min_consec = params.get("min_consecutive", 2)
|
||||
use_vol = params.get("vol_filter", False)
|
||||
min_range_pct = params.get("min_range_pct", 0.002)
|
||||
|
||||
# Volume media
|
||||
vol_ma = np.full(n, np.nan)
|
||||
for i in range(20, n):
|
||||
vol_ma[i] = np.mean(v[i - 20:i])
|
||||
|
||||
signals = []
|
||||
consec = 0
|
||||
mother_high = 0.0
|
||||
mother_low = 0.0
|
||||
|
||||
for i in range(1, n - 1):
|
||||
is_inside = h[i] <= h[i - 1] and l[i] >= l[i - 1]
|
||||
|
||||
if is_inside:
|
||||
if consec == 0:
|
||||
mother_high = h[i - 1]
|
||||
mother_low = l[i - 1]
|
||||
consec += 1
|
||||
else:
|
||||
if consec >= min_consec:
|
||||
range_pct = (mother_high - mother_low) / mother_low if mother_low > 0 else 0
|
||||
if range_pct < min_range_pct:
|
||||
consec = 0
|
||||
continue
|
||||
|
||||
# Breakout detection sulla barra corrente
|
||||
if c[i] > mother_high:
|
||||
direction = 1
|
||||
elif c[i] < mother_low:
|
||||
direction = -1
|
||||
else:
|
||||
consec = 0
|
||||
continue
|
||||
|
||||
# Volume filter
|
||||
if use_vol and not np.isnan(vol_ma[i]):
|
||||
if v[i] < vol_ma[i] * 1.2:
|
||||
consec = 0
|
||||
continue
|
||||
|
||||
signals.append(Signal(
|
||||
idx=i, direction=direction, entry_price=c[i],
|
||||
metadata={"consec": consec, "range_pct": round(range_pct * 100, 3)},
|
||||
))
|
||||
|
||||
consec = 0
|
||||
|
||||
return signals
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
strategy = InsideBarBreakout()
|
||||
|
||||
configs = [
|
||||
("2ib", {"min_consecutive": 2}),
|
||||
("3ib", {"min_consecutive": 3}),
|
||||
("4ib", {"min_consecutive": 4}),
|
||||
("2ib+vol", {"min_consecutive": 2, "vol_filter": True}),
|
||||
("3ib+vol", {"min_consecutive": 3, "vol_filter": True}),
|
||||
("2ib r>0.3%", {"min_consecutive": 2, "min_range_pct": 0.003}),
|
||||
("3ib r>0.3%", {"min_consecutive": 3, "min_range_pct": 0.003}),
|
||||
]
|
||||
|
||||
all_results = []
|
||||
for label, params in configs:
|
||||
for asset in ["BTC", "ETH"]:
|
||||
for tf in ["15m", "1h"]:
|
||||
for hold in [3, 6]:
|
||||
r = strategy.backtest(asset, tf, hold=hold, **params)
|
||||
if r and r.trades >= 30:
|
||||
r.strategy_name = f"IB01 {label} h={hold}"
|
||||
all_results.append(r)
|
||||
|
||||
all_results.sort(key=lambda r: r.accuracy, reverse=True)
|
||||
print(f"\n{'=' * 120}")
|
||||
print(f" IB01 INSIDE BAR BREAKOUT — TOP 20")
|
||||
print(f"{'=' * 120}")
|
||||
for r in all_results[:20]:
|
||||
r.print_summary()
|
||||
if all_results:
|
||||
all_results[0].print_yearly()
|
||||
@@ -0,0 +1,133 @@
|
||||
"""DC01 — Donchian Channel Breakout con filtri.
|
||||
|
||||
Trend-following classico: quando il prezzo rompe il massimo/minimo degli
|
||||
ultimi N periodi, entra nella direzione del breakout.
|
||||
|
||||
Completamente diverso dallo squeeze (che usa Bollinger/Keltner).
|
||||
Donchian cattura breakout di RANGE, non di VOLATILITÀ.
|
||||
|
||||
IN:
|
||||
- OHLCV DataFrame
|
||||
- Parametri: channel_period, volume_filter, atr_stop, trend_filter
|
||||
|
||||
OUT:
|
||||
- Signal al breakout del canale Donchian
|
||||
- BacktestResult
|
||||
|
||||
Logica:
|
||||
1. Donchian upper = max(high, N periodi), lower = min(low, N periodi)
|
||||
2. Close > upper → LONG (breakout rialzista)
|
||||
3. Close < lower → SHORT (breakout ribassista)
|
||||
4. Filtri: volume, trend EMA, ATR minimo
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from src.strategies.base import Strategy, Signal
|
||||
|
||||
|
||||
class DonchianBreakout(Strategy):
|
||||
name = "DC01_donchian"
|
||||
description = "Donchian Channel breakout — trend-following su range"
|
||||
default_assets = ["BTC", "ETH"]
|
||||
default_timeframes = ["15m", "1h"]
|
||||
fee_rt = 0.002
|
||||
|
||||
def generate_signals(self, df, ts, **params):
|
||||
c = df["close"].values
|
||||
h = df["high"].values
|
||||
l = df["low"].values
|
||||
v = df["volume"].values
|
||||
n = len(c)
|
||||
|
||||
period = params.get("channel_period", 48)
|
||||
use_vol = params.get("vol_filter", False)
|
||||
use_trend = params.get("trend_filter", False)
|
||||
cooldown = params.get("cooldown", 6)
|
||||
|
||||
# EMA per trend filter
|
||||
ema_50 = np.full(n, np.nan)
|
||||
k = 2 / 51
|
||||
ema_50[49] = np.mean(c[:50])
|
||||
for i in range(50, n):
|
||||
ema_50[i] = c[i] * k + ema_50[i - 1] * (1 - k)
|
||||
|
||||
# Volume media
|
||||
vol_ma = np.full(n, np.nan)
|
||||
for i in range(20, n):
|
||||
vol_ma[i] = np.mean(v[i - 20:i])
|
||||
|
||||
signals = []
|
||||
last_signal_idx = -cooldown
|
||||
|
||||
for i in range(period + 1, n):
|
||||
if i - last_signal_idx < cooldown:
|
||||
continue
|
||||
|
||||
upper = np.max(h[i - period:i])
|
||||
lower = np.min(l[i - period:i])
|
||||
|
||||
direction = 0
|
||||
if c[i] > upper:
|
||||
direction = 1
|
||||
elif c[i] < lower:
|
||||
direction = -1
|
||||
|
||||
if direction == 0:
|
||||
continue
|
||||
|
||||
# Trend filter: breakout must align with EMA trend
|
||||
if use_trend and not np.isnan(ema_50[i]):
|
||||
if direction == 1 and c[i] < ema_50[i]:
|
||||
continue
|
||||
if direction == -1 and c[i] > ema_50[i]:
|
||||
continue
|
||||
|
||||
# Volume filter
|
||||
if use_vol and not np.isnan(vol_ma[i]):
|
||||
if v[i] < vol_ma[i] * 1.3:
|
||||
continue
|
||||
|
||||
signals.append(Signal(
|
||||
idx=i, direction=direction, entry_price=c[i],
|
||||
metadata={"upper": float(upper), "lower": float(lower)},
|
||||
))
|
||||
last_signal_idx = i
|
||||
|
||||
return signals
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
strategy = DonchianBreakout()
|
||||
|
||||
configs = [
|
||||
("p=24", {"channel_period": 24}),
|
||||
("p=48", {"channel_period": 48}),
|
||||
("p=96", {"channel_period": 96}),
|
||||
("p=48+trend", {"channel_period": 48, "trend_filter": True}),
|
||||
("p=48+vol", {"channel_period": 48, "vol_filter": True}),
|
||||
("p=48+t+v", {"channel_period": 48, "trend_filter": True, "vol_filter": True}),
|
||||
("p=96+t+v", {"channel_period": 96, "trend_filter": True, "vol_filter": True}),
|
||||
]
|
||||
|
||||
all_results = []
|
||||
for label, params in configs:
|
||||
for asset in ["BTC", "ETH"]:
|
||||
for tf in ["15m", "1h"]:
|
||||
for hold in [3, 6, 12]:
|
||||
r = strategy.backtest(asset, tf, hold=hold, **params)
|
||||
if r and r.trades >= 30:
|
||||
r.strategy_name = f"DC01 {label} h={hold}"
|
||||
all_results.append(r)
|
||||
|
||||
all_results.sort(key=lambda r: r.accuracy, reverse=True)
|
||||
print(f"\n{'=' * 120}")
|
||||
print(f" DC01 DONCHIAN BREAKOUT — TOP 20")
|
||||
print(f"{'=' * 120}")
|
||||
for r in all_results[:20]:
|
||||
r.print_summary()
|
||||
if all_results:
|
||||
all_results[0].print_yearly()
|
||||
@@ -0,0 +1,163 @@
|
||||
"""SB01 — Squeeze Breakout con Retest.
|
||||
|
||||
Il problema di SQ01/SQ02: entri al breakout, ma molti breakout sono fakeout.
|
||||
Soluzione: aspetta il RETEST. Dopo il breakout, il prezzo spesso torna a
|
||||
testare il livello di breakout prima di continuare.
|
||||
|
||||
Più selettivo di SQ02 → meno trade ma più accurati.
|
||||
Anti-overfitting: meccanismo strutturale (retest è fenomeno di mercato reale).
|
||||
|
||||
IN:
|
||||
- OHLCV DataFrame
|
||||
- Parametri: bb_window, sq_threshold, retest_window (quante barre aspettare
|
||||
il retest), retest_tolerance (quanto può tornare indietro)
|
||||
|
||||
OUT:
|
||||
- Signal al retest confermato (non al breakout iniziale)
|
||||
- BacktestResult
|
||||
|
||||
Logica:
|
||||
1. Rileva squeeze release (come SQ01)
|
||||
2. NON entrare subito — segna direzione e livello di breakout
|
||||
3. Nelle N barre successive, aspetta che il prezzo torni verso il livello
|
||||
4. Se il prezzo torna nel range di tolleranza e poi rimbalza → ENTRA
|
||||
5. Se il prezzo non torna → skip (momentum troppo forte, entry persa)
|
||||
6. Se il prezzo sfonda il livello → fakeout confermato, skip
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from src.strategies.base import Strategy, Signal
|
||||
from src.strategies.indicators import keltner_ratio, detect_squeezes
|
||||
|
||||
|
||||
class SqueezeBreakoutRetest(Strategy):
|
||||
name = "SB01_squeeze_retest"
|
||||
description = "Squeeze breakout con retest — entra solo dopo pullback confermato"
|
||||
default_assets = ["BTC", "ETH"]
|
||||
default_timeframes = ["15m", "1h"]
|
||||
fee_rt = 0.002
|
||||
|
||||
def generate_signals(self, df, ts, **params):
|
||||
c = df["close"].values
|
||||
h = df["high"].values
|
||||
l = df["low"].values
|
||||
v = df["volume"].values
|
||||
n = len(c)
|
||||
|
||||
bb_w = params.get("bb_window", 14)
|
||||
sq_thr = params.get("sq_threshold", 0.8)
|
||||
retest_window = params.get("retest_window", 8)
|
||||
retest_tol = params.get("retest_tolerance", 0.5)
|
||||
use_vol = params.get("vol_filter", False)
|
||||
|
||||
kcr = keltner_ratio(c, h, l, bb_w)
|
||||
events = detect_squeezes(c, h, l, kcr, sq_thr)
|
||||
|
||||
vol_ma = np.full(n, np.nan)
|
||||
for i in range(20, n):
|
||||
vol_ma[i] = np.mean(v[i - 20:i])
|
||||
|
||||
signals = []
|
||||
|
||||
for ev in events:
|
||||
brk_idx = ev["idx"]
|
||||
if brk_idx + retest_window + 3 >= n or brk_idx < 1:
|
||||
continue
|
||||
|
||||
# Direzione breakout
|
||||
first_ret = (c[brk_idx] - c[brk_idx - 1]) / c[brk_idx - 1]
|
||||
if abs(first_ret) < 0.001:
|
||||
continue
|
||||
|
||||
direction = 1 if first_ret > 0 else -1
|
||||
breakout_level = c[brk_idx - 1]
|
||||
breakout_move = abs(first_ret)
|
||||
|
||||
# Aspetta retest nelle prossime N barre
|
||||
retest_found = False
|
||||
retest_idx = -1
|
||||
|
||||
for j in range(brk_idx + 1, min(brk_idx + retest_window + 1, n)):
|
||||
if direction == 1:
|
||||
# Long: il prezzo deve tornare GIÙ verso breakout_level
|
||||
pullback = (h[brk_idx] - l[j]) / (h[brk_idx] - breakout_level) if h[brk_idx] > breakout_level else 0
|
||||
if pullback >= retest_tol:
|
||||
# Tornato abbastanza — ora deve rimbalzare
|
||||
if c[j] > breakout_level:
|
||||
retest_found = True
|
||||
retest_idx = j
|
||||
break
|
||||
elif c[j] < breakout_level * 0.998:
|
||||
# Sfondato sotto → fakeout
|
||||
break
|
||||
else:
|
||||
# Short: il prezzo deve tornare SU verso breakout_level
|
||||
pullback = (h[j] - l[brk_idx]) / (breakout_level - l[brk_idx]) if breakout_level > l[brk_idx] else 0
|
||||
if pullback >= retest_tol:
|
||||
if c[j] < breakout_level:
|
||||
retest_found = True
|
||||
retest_idx = j
|
||||
break
|
||||
elif c[j] > breakout_level * 1.002:
|
||||
break
|
||||
|
||||
if not retest_found or retest_idx < 0:
|
||||
continue
|
||||
|
||||
# Volume filter al retest
|
||||
if use_vol and not np.isnan(vol_ma[retest_idx]):
|
||||
if v[retest_idx] < vol_ma[retest_idx] * 0.8:
|
||||
continue
|
||||
|
||||
signals.append(Signal(
|
||||
idx=retest_idx, direction=direction,
|
||||
entry_price=c[retest_idx],
|
||||
metadata={
|
||||
"breakout_idx": brk_idx,
|
||||
"retest_bars": retest_idx - brk_idx,
|
||||
"breakout_move": round(breakout_move * 100, 3),
|
||||
},
|
||||
))
|
||||
|
||||
return signals
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
strategy = SqueezeBreakoutRetest()
|
||||
|
||||
configs = [
|
||||
("rt8 tol50%", {"retest_window": 8, "retest_tolerance": 0.5}),
|
||||
("rt6 tol50%", {"retest_window": 6, "retest_tolerance": 0.5}),
|
||||
("rt10 tol50%", {"retest_window": 10, "retest_tolerance": 0.5}),
|
||||
("rt8 tol30%", {"retest_window": 8, "retest_tolerance": 0.3}),
|
||||
("rt8 tol70%", {"retest_window": 8, "retest_tolerance": 0.7}),
|
||||
("rt8 tol50%+vol", {"retest_window": 8, "retest_tolerance": 0.5, "vol_filter": True}),
|
||||
("rt6 tol30%", {"retest_window": 6, "retest_tolerance": 0.3}),
|
||||
("rt12 tol50%", {"retest_window": 12, "retest_tolerance": 0.5}),
|
||||
]
|
||||
|
||||
all_results = []
|
||||
for label, params in configs:
|
||||
for asset in ["BTC", "ETH"]:
|
||||
for tf in ["15m", "1h"]:
|
||||
for hold in [3, 6]:
|
||||
r = strategy.backtest(asset, tf, hold=hold, **params)
|
||||
if r and r.trades >= 30:
|
||||
r.strategy_name = f"SB01 {label} h={hold}"
|
||||
all_results.append(r)
|
||||
|
||||
all_results.sort(key=lambda r: r.accuracy, reverse=True)
|
||||
print(f"\n{'=' * 130}")
|
||||
print(f" SB01 SQUEEZE BREAKOUT RETEST — TOP 25")
|
||||
print(f"{'=' * 130}")
|
||||
for r in all_results[:25]:
|
||||
r.print_summary()
|
||||
if all_results:
|
||||
all_results[0].print_yearly()
|
||||
|
||||
# Confronto con benchmark
|
||||
print(f"\n BENCHMARK SQ02: 79.7% acc, 1250 trades, DD 6.5%, 9/9 anni")
|
||||
@@ -0,0 +1,148 @@
|
||||
"""MR01 — Mean Reversion da estremi RSI.
|
||||
|
||||
Approccio opposto allo squeeze: quando il prezzo va troppo lontano troppo veloce,
|
||||
scommetti che torni indietro. Autocorrelazione lag-1 negativa (-0.21 BTC, -0.35 ETH)
|
||||
conferma che il mercato a 15m è mean-reverting.
|
||||
|
||||
IN:
|
||||
- OHLCV DataFrame
|
||||
- Parametri: rsi_period, rsi_oversold, rsi_overbought, hold_bars,
|
||||
volume_filter (volume > N× media), atr_filter (move > N×ATR)
|
||||
|
||||
OUT:
|
||||
- Signal: long quando RSI < oversold, short quando RSI > overbought
|
||||
- BacktestResult con metriche
|
||||
|
||||
Logica:
|
||||
1. RSI scende sotto soglia oversold → LONG (prezzo tornerà su)
|
||||
2. RSI sale sopra soglia overbought → SHORT (prezzo tornerà giù)
|
||||
3. Filtro opzionale: volume spike conferma l'eccesso
|
||||
4. Filtro opzionale: move recente > N×ATR (eccesso di prezzo)
|
||||
5. Hold fisso, poi chiudi
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from src.strategies.base import Strategy, Signal
|
||||
|
||||
|
||||
def rsi(close, period=14):
|
||||
delta = np.diff(close)
|
||||
gain = np.where(delta > 0, delta, 0)
|
||||
loss = np.where(delta < 0, -delta, 0)
|
||||
result = np.full(len(close), 50.0)
|
||||
if len(gain) < period:
|
||||
return result
|
||||
ag = np.mean(gain[:period])
|
||||
al = np.mean(loss[:period])
|
||||
for i in range(period, len(delta)):
|
||||
ag = (ag * (period - 1) + gain[i]) / period
|
||||
al = (al * (period - 1) + loss[i]) / period
|
||||
result[i + 1] = 100 if al == 0 else 100 - 100 / (1 + ag / al)
|
||||
return result
|
||||
|
||||
|
||||
class MeanReversionRSI(Strategy):
|
||||
name = "MR01_mean_reversion_rsi"
|
||||
description = "Mean reversion da estremi RSI — fade eccessi direzionali"
|
||||
default_assets = ["BTC", "ETH"]
|
||||
default_timeframes = ["15m", "1h"]
|
||||
fee_rt = 0.002
|
||||
|
||||
def generate_signals(self, df, ts, **params):
|
||||
c = df["close"].values
|
||||
h = df["high"].values
|
||||
l = df["low"].values
|
||||
v = df["volume"].values
|
||||
n = len(c)
|
||||
|
||||
rsi_period = params.get("rsi_period", 14)
|
||||
oversold = params.get("rsi_oversold", 25)
|
||||
overbought = params.get("rsi_overbought", 75)
|
||||
use_vol_filter = params.get("vol_filter", False)
|
||||
use_atr_filter = params.get("atr_filter", False)
|
||||
cooldown = params.get("cooldown", 4)
|
||||
|
||||
rsi_vals = rsi(c, rsi_period)
|
||||
|
||||
# Volume media rolling
|
||||
vol_ma = np.full(n, np.nan)
|
||||
for i in range(20, n):
|
||||
vol_ma[i] = np.mean(v[i - 20:i])
|
||||
|
||||
# ATR
|
||||
tr = np.maximum(h[1:] - l[1:],
|
||||
np.maximum(np.abs(h[1:] - c[:-1]), np.abs(l[1:] - c[:-1])))
|
||||
atr_vals = np.full(n, np.nan)
|
||||
for i in range(15, len(tr)):
|
||||
atr_vals[i + 1] = np.mean(tr[i - 14:i])
|
||||
|
||||
signals = []
|
||||
last_signal_idx = -cooldown
|
||||
|
||||
for i in range(20, n):
|
||||
if i - last_signal_idx < cooldown:
|
||||
continue
|
||||
|
||||
direction = 0
|
||||
if rsi_vals[i] < oversold:
|
||||
direction = 1 # oversold → long
|
||||
elif rsi_vals[i] > overbought:
|
||||
direction = -1 # overbought → short
|
||||
|
||||
if direction == 0:
|
||||
continue
|
||||
|
||||
# Volume filter
|
||||
if use_vol_filter and not np.isnan(vol_ma[i]):
|
||||
if v[i] < vol_ma[i] * 1.5:
|
||||
continue
|
||||
|
||||
# ATR filter: il move recente deve essere > 1.5× ATR
|
||||
if use_atr_filter and not np.isnan(atr_vals[i]):
|
||||
recent_move = abs(c[i] - c[max(0, i - 3)]) / c[max(0, i - 3)]
|
||||
if recent_move < atr_vals[i] / c[i] * 1.5:
|
||||
continue
|
||||
|
||||
signals.append(Signal(
|
||||
idx=i, direction=direction, entry_price=c[i],
|
||||
metadata={"rsi": float(rsi_vals[i])},
|
||||
))
|
||||
last_signal_idx = i
|
||||
|
||||
return signals
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
strategy = MeanReversionRSI()
|
||||
|
||||
configs = [
|
||||
("RSI25/75", {}),
|
||||
("RSI20/80", {"rsi_oversold": 20, "rsi_overbought": 80}),
|
||||
("RSI25/75+vol", {"vol_filter": True}),
|
||||
("RSI20/80+vol", {"rsi_oversold": 20, "rsi_overbought": 80, "vol_filter": True}),
|
||||
("RSI25/75+atr", {"atr_filter": True}),
|
||||
("RSI20/80+vol+atr", {"rsi_oversold": 20, "rsi_overbought": 80, "vol_filter": True, "atr_filter": True}),
|
||||
]
|
||||
|
||||
all_results = []
|
||||
for label, params in configs:
|
||||
for asset in ["BTC", "ETH"]:
|
||||
for tf in ["15m", "1h"]:
|
||||
for hold in [3, 6]:
|
||||
r = strategy.backtest(asset, tf, hold=hold, **params)
|
||||
if r and r.trades >= 30:
|
||||
r.strategy_name = f"MR01 {label} h={hold}"
|
||||
all_results.append(r)
|
||||
|
||||
all_results.sort(key=lambda r: r.accuracy, reverse=True)
|
||||
print(f"\n{'=' * 120}")
|
||||
print(f" MR01 MEAN REVERSION RSI — TOP 20")
|
||||
print(f"{'=' * 120}")
|
||||
for r in all_results[:20]:
|
||||
r.print_summary()
|
||||
if all_results:
|
||||
all_results[0].print_yearly()
|
||||
@@ -0,0 +1,133 @@
|
||||
"""VO01 — Volume Spike Reversal.
|
||||
|
||||
Quando il volume esplode (>3× media) con un forte move direzionale,
|
||||
il mercato è in eccesso → fade il move (mean reversion).
|
||||
|
||||
Diverso dallo squeeze: non cerca compressione, cerca ECCESSO.
|
||||
Il volume spike indica panico/euforia → reversal probabile.
|
||||
|
||||
IN:
|
||||
- OHLCV DataFrame
|
||||
- Parametri: vol_mult (3), move_threshold (0.005), hold
|
||||
|
||||
OUT:
|
||||
- Signal: fade la direzione del volume spike
|
||||
- BacktestResult
|
||||
|
||||
Logica:
|
||||
1. Volume > vol_mult × media 20 periodi
|
||||
2. Move nella candela > move_threshold (0.5%)
|
||||
3. Direzione: opposta al move (mean reversion)
|
||||
4. Filtro: non entrare se già in trend forte (EMA slope)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from src.strategies.base import Strategy, Signal
|
||||
|
||||
|
||||
class VolumeSpikeReversal(Strategy):
|
||||
name = "VO01_vol_spike_reversal"
|
||||
description = "Volume spike reversal — fade eccessi di volume/prezzo"
|
||||
default_assets = ["BTC", "ETH"]
|
||||
default_timeframes = ["15m", "1h"]
|
||||
fee_rt = 0.002
|
||||
|
||||
def generate_signals(self, df, ts, **params):
|
||||
c = df["close"].values
|
||||
o = df["open"].values
|
||||
h = df["high"].values
|
||||
l = df["low"].values
|
||||
v = df["volume"].values
|
||||
n = len(c)
|
||||
|
||||
vol_mult = params.get("vol_mult", 3.0)
|
||||
move_thr = params.get("move_threshold", 0.005)
|
||||
use_trend_filter = params.get("trend_filter", False)
|
||||
cooldown = params.get("cooldown", 4)
|
||||
|
||||
# Volume media rolling
|
||||
vol_ma = np.full(n, np.nan)
|
||||
for i in range(20, n):
|
||||
vol_ma[i] = np.mean(v[i - 20:i])
|
||||
|
||||
# EMA per trend filter
|
||||
ema_20 = np.full(n, np.nan)
|
||||
k = 2 / 21
|
||||
ema_20[19] = np.mean(c[:20])
|
||||
for i in range(20, n):
|
||||
ema_20[i] = c[i] * k + ema_20[i - 1] * (1 - k)
|
||||
|
||||
signals = []
|
||||
last_idx = -cooldown
|
||||
|
||||
for i in range(21, n):
|
||||
if i - last_idx < cooldown:
|
||||
continue
|
||||
if np.isnan(vol_ma[i]):
|
||||
continue
|
||||
|
||||
# Volume spike
|
||||
if v[i] < vol_ma[i] * vol_mult:
|
||||
continue
|
||||
|
||||
# Price move
|
||||
move = (c[i] - o[i]) / o[i] if o[i] > 0 else 0
|
||||
if abs(move) < move_thr:
|
||||
continue
|
||||
|
||||
# Fade: opposto al move
|
||||
direction = -1 if move > 0 else 1
|
||||
|
||||
# Trend filter: non fare mean reversion contro trend forte
|
||||
if use_trend_filter and not np.isnan(ema_20[i]):
|
||||
ema_slope = (ema_20[i] - ema_20[max(0, i - 5)]) / ema_20[max(0, i - 5)]
|
||||
if direction == -1 and ema_slope > 0.005:
|
||||
continue
|
||||
if direction == 1 and ema_slope < -0.005:
|
||||
continue
|
||||
|
||||
signals.append(Signal(
|
||||
idx=i, direction=direction, entry_price=c[i],
|
||||
metadata={"vol_ratio": float(v[i] / vol_ma[i]), "move_pct": round(move * 100, 3)},
|
||||
))
|
||||
last_idx = i
|
||||
|
||||
return signals
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
strategy = VolumeSpikeReversal()
|
||||
|
||||
configs = [
|
||||
("v3x m0.5%", {"vol_mult": 3.0, "move_threshold": 0.005}),
|
||||
("v3x m1%", {"vol_mult": 3.0, "move_threshold": 0.01}),
|
||||
("v4x m0.5%", {"vol_mult": 4.0, "move_threshold": 0.005}),
|
||||
("v4x m1%", {"vol_mult": 4.0, "move_threshold": 0.01}),
|
||||
("v3x m0.5%+tf", {"vol_mult": 3.0, "move_threshold": 0.005, "trend_filter": True}),
|
||||
("v3x m1%+tf", {"vol_mult": 3.0, "move_threshold": 0.01, "trend_filter": True}),
|
||||
("v5x m1%", {"vol_mult": 5.0, "move_threshold": 0.01}),
|
||||
("v5x m1%+tf", {"vol_mult": 5.0, "move_threshold": 0.01, "trend_filter": True}),
|
||||
]
|
||||
|
||||
all_results = []
|
||||
for label, params in configs:
|
||||
for asset in ["BTC", "ETH"]:
|
||||
for tf in ["15m", "1h"]:
|
||||
for hold in [3, 6]:
|
||||
r = strategy.backtest(asset, tf, hold=hold, **params)
|
||||
if r and r.trades >= 30:
|
||||
r.strategy_name = f"VO01 {label} h={hold}"
|
||||
all_results.append(r)
|
||||
|
||||
all_results.sort(key=lambda r: r.accuracy, reverse=True)
|
||||
print(f"\n{'=' * 120}")
|
||||
print(f" VO01 VOLUME SPIKE REVERSAL — TOP 20")
|
||||
print(f"{'=' * 120}")
|
||||
for r in all_results[:20]:
|
||||
r.print_summary()
|
||||
if all_results:
|
||||
all_results[0].print_yearly()
|
||||
@@ -0,0 +1,169 @@
|
||||
"""HY01 — Squeeze + Mean Reversion Ibrida.
|
||||
|
||||
Insight: durante lo squeeze (bassa volatilità), il prezzo mean-reverte
|
||||
DENTRO il range compresso. Autocorrelazione negativa a 15m conferma.
|
||||
Invece di aspettare il BREAKOUT, tradi la MEAN REVERSION dentro lo squeeze.
|
||||
|
||||
Completamente diverso da SQ01-SQ04 che aspettano il RILASCIO.
|
||||
|
||||
IN:
|
||||
- OHLCV DataFrame
|
||||
- Parametri: bb_window, sq_threshold, rsi_period, rsi_levels,
|
||||
vol_filter, bb_touch (prezzo tocca banda Bollinger)
|
||||
|
||||
OUT:
|
||||
- Signal: long quando RSI oversold DURANTE squeeze, short quando overbought
|
||||
- BacktestResult
|
||||
|
||||
Logica:
|
||||
1. Verifica che siamo IN squeeze (BB dentro KC)
|
||||
2. Prezzo tocca banda inferiore BB → LONG (tornerà alla media)
|
||||
3. Prezzo tocca banda superiore BB → SHORT (tornerà alla media)
|
||||
4. Conferma RSI: deve essere estremo nella direzione
|
||||
5. Hold corto (2-3 barre) — target: ritorno alla media
|
||||
6. Stop: se prezzo rompe lo squeeze → chiudi subito
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from src.strategies.base import Strategy, Signal
|
||||
from src.strategies.indicators import keltner_ratio
|
||||
|
||||
|
||||
def rsi(close, period=14):
|
||||
delta = np.diff(close)
|
||||
gain = np.where(delta > 0, delta, 0)
|
||||
loss = np.where(delta < 0, -delta, 0)
|
||||
result = np.full(len(close), 50.0)
|
||||
if len(gain) < period:
|
||||
return result
|
||||
ag = np.mean(gain[:period])
|
||||
al = np.mean(loss[:period])
|
||||
for i in range(period, len(delta)):
|
||||
ag = (ag * (period - 1) + gain[i]) / period
|
||||
al = (al * (period - 1) + loss[i]) / period
|
||||
result[i + 1] = 100 if al == 0 else 100 - 100 / (1 + ag / al)
|
||||
return result
|
||||
|
||||
|
||||
def bollinger(close, window=14):
|
||||
n = len(close)
|
||||
upper = np.full(n, np.nan)
|
||||
lower = np.full(n, np.nan)
|
||||
mid = np.full(n, np.nan)
|
||||
for i in range(window, n):
|
||||
wc = close[i - window:i]
|
||||
m = np.mean(wc)
|
||||
s = np.std(wc)
|
||||
mid[i] = m
|
||||
upper[i] = m + 2 * s
|
||||
lower[i] = m - 2 * s
|
||||
return upper, mid, lower
|
||||
|
||||
|
||||
class SqueezeMeanReversion(Strategy):
|
||||
name = "HY01_squeeze_mr"
|
||||
description = "Mean reversion DENTRO lo squeeze — fade estremi in range compresso"
|
||||
default_assets = ["BTC", "ETH"]
|
||||
default_timeframes = ["15m", "1h"]
|
||||
fee_rt = 0.002
|
||||
|
||||
def generate_signals(self, df, ts, **params):
|
||||
c = df["close"].values
|
||||
h = df["high"].values
|
||||
l = df["low"].values
|
||||
v = df["volume"].values
|
||||
n = len(c)
|
||||
|
||||
bb_w = params.get("bb_window", 14)
|
||||
sq_thr = params.get("sq_threshold", 0.8)
|
||||
rsi_period = params.get("rsi_period", 14)
|
||||
rsi_low = params.get("rsi_oversold", 30)
|
||||
rsi_high = params.get("rsi_overbought", 70)
|
||||
use_bb_touch = params.get("bb_touch", True)
|
||||
cooldown = params.get("cooldown", 3)
|
||||
|
||||
kcr = keltner_ratio(c, h, l, bb_w)
|
||||
rsi_vals = rsi(c, rsi_period)
|
||||
bb_upper, bb_mid, bb_lower = bollinger(c, bb_w)
|
||||
|
||||
signals = []
|
||||
last_idx = -cooldown
|
||||
|
||||
for i in range(bb_w + 1, n):
|
||||
if i - last_idx < cooldown:
|
||||
continue
|
||||
if np.isnan(kcr[i]) or np.isnan(bb_lower[i]):
|
||||
continue
|
||||
|
||||
# Must be IN squeeze
|
||||
if kcr[i] >= sq_thr:
|
||||
continue
|
||||
|
||||
direction = 0
|
||||
|
||||
if use_bb_touch:
|
||||
# Prezzo tocca/rompe BB lower → long (mean reversion up)
|
||||
if c[i] <= bb_lower[i] and rsi_vals[i] < rsi_low:
|
||||
direction = 1
|
||||
# Prezzo tocca/rompe BB upper → short (mean reversion down)
|
||||
elif c[i] >= bb_upper[i] and rsi_vals[i] > rsi_high:
|
||||
direction = -1
|
||||
else:
|
||||
# Solo RSI
|
||||
if rsi_vals[i] < rsi_low:
|
||||
direction = 1
|
||||
elif rsi_vals[i] > rsi_high:
|
||||
direction = -1
|
||||
|
||||
if direction == 0:
|
||||
continue
|
||||
|
||||
signals.append(Signal(
|
||||
idx=i, direction=direction, entry_price=c[i],
|
||||
metadata={
|
||||
"rsi": float(rsi_vals[i]),
|
||||
"kcr": float(kcr[i]),
|
||||
"bb_pos": "lower" if direction == 1 else "upper",
|
||||
},
|
||||
))
|
||||
last_idx = i
|
||||
|
||||
return signals
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
strategy = SqueezeMeanReversion()
|
||||
|
||||
configs = [
|
||||
("bb+rsi30/70", {"bb_touch": True, "rsi_oversold": 30, "rsi_overbought": 70}),
|
||||
("bb+rsi25/75", {"bb_touch": True, "rsi_oversold": 25, "rsi_overbought": 75}),
|
||||
("bb+rsi35/65", {"bb_touch": True, "rsi_oversold": 35, "rsi_overbought": 65}),
|
||||
("rsi30/70 only", {"bb_touch": False, "rsi_oversold": 30, "rsi_overbought": 70}),
|
||||
("rsi25/75 only", {"bb_touch": False, "rsi_oversold": 25, "rsi_overbought": 75}),
|
||||
("sq<0.7 bb+rsi30", {"bb_touch": True, "sq_threshold": 0.7, "rsi_oversold": 30, "rsi_overbought": 70}),
|
||||
("sq<0.9 bb+rsi30", {"bb_touch": True, "sq_threshold": 0.9, "rsi_oversold": 30, "rsi_overbought": 70}),
|
||||
("sq<0.9 rsi35/65", {"bb_touch": False, "sq_threshold": 0.9, "rsi_oversold": 35, "rsi_overbought": 65}),
|
||||
]
|
||||
|
||||
all_results = []
|
||||
for label, params in configs:
|
||||
for asset in ["BTC", "ETH"]:
|
||||
for tf in ["15m", "1h"]:
|
||||
for hold in [2, 3, 4]:
|
||||
r = strategy.backtest(asset, tf, hold=hold, **params)
|
||||
if r and r.trades >= 30:
|
||||
r.strategy_name = f"HY01 {label} h={hold}"
|
||||
all_results.append(r)
|
||||
|
||||
all_results.sort(key=lambda r: r.accuracy, reverse=True)
|
||||
print(f"\n{'=' * 130}")
|
||||
print(f" HY01 SQUEEZE MEAN REVERSION — TOP 25")
|
||||
print(f"{'=' * 130}")
|
||||
for r in all_results[:25]:
|
||||
r.print_summary()
|
||||
if all_results:
|
||||
all_results[0].print_yearly()
|
||||
@@ -0,0 +1,271 @@
|
||||
"""Multi-Strategy Paper Trader — orchestratore per N strategie in parallelo."""
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
import yaml
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from src.live.cerbero_client import CerberoClient
|
||||
from src.live.strategy_loader import load_strategy
|
||||
from src.live.strategy_worker import StrategyWorker
|
||||
from src.live.signal_engine import SignalEngine
|
||||
from src.live.telegram_notifier import send_telegram
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
DATA_DIR = PROJECT_ROOT / "data" / "paper_trades"
|
||||
|
||||
RESOLUTION_MAP = {"15m": "15", "1h": "60", "5m": "5"}
|
||||
INSTRUMENT_MAP = {
|
||||
"BTC": "BTC-PERPETUAL",
|
||||
"ETH": "ETH-PERPETUAL",
|
||||
}
|
||||
|
||||
|
||||
class MLWorkerWrapper:
|
||||
"""Wrapper speciale per ML01 che usa SignalEngine con training."""
|
||||
|
||||
def __init__(self, worker: StrategyWorker, config: dict):
|
||||
self.worker = worker
|
||||
self.engine = SignalEngine(
|
||||
bb_w=config.get("params", {}).get("bb_window", 14),
|
||||
sq_thr=config.get("params", {}).get("sq_threshold", 0.8),
|
||||
ml_thr=config.get("params", {}).get("ml_threshold", 0.70),
|
||||
)
|
||||
self.trained = False
|
||||
self.last_train: datetime | None = None
|
||||
self.retrain_hours = config.get("retrain_hours", 24)
|
||||
|
||||
def needs_training(self) -> bool:
|
||||
if not self.trained:
|
||||
return True
|
||||
if self.last_train is None:
|
||||
return True
|
||||
elapsed = (datetime.now(timezone.utc) - self.last_train).total_seconds()
|
||||
return elapsed > self.retrain_hours * 3600
|
||||
|
||||
def train(self, df: pd.DataFrame, hold: int = 3):
|
||||
result = self.engine.train(df, lookahead=hold)
|
||||
if "error" not in result:
|
||||
self.trained = True
|
||||
self.last_train = datetime.now(timezone.utc)
|
||||
print(f" [{self.worker.worker_id}] TRAIN OK: {result}")
|
||||
else:
|
||||
print(f" [{self.worker.worker_id}] TRAIN FAIL: {result}")
|
||||
|
||||
def tick(self, df: pd.DataFrame):
|
||||
if not self.trained:
|
||||
return
|
||||
|
||||
worker = self.worker
|
||||
c = df["close"].values
|
||||
current_price = float(c[-1])
|
||||
current_ts = int(df["timestamp"].iloc[-1])
|
||||
|
||||
if worker.in_position:
|
||||
if current_ts > worker.last_bar_ts:
|
||||
worker.bars_held += 1
|
||||
worker.last_bar_ts = current_ts
|
||||
if worker.bars_held >= worker.hold_bars:
|
||||
worker._close_position(current_price, "hold_limit")
|
||||
else:
|
||||
pnl_pct = (current_price - worker.entry_price) / worker.entry_price * worker.direction
|
||||
if pnl_pct <= -0.02:
|
||||
worker._close_position(current_price, "stop_loss")
|
||||
worker._save_state()
|
||||
return
|
||||
|
||||
signal = self.engine.check_signal(df)
|
||||
if signal:
|
||||
from src.strategies.base import Signal
|
||||
direction = 1 if signal["direction"] == "buy" else -1
|
||||
sig = Signal(idx=len(df)-1, direction=direction, entry_price=current_price)
|
||||
worker._open_position(sig, current_price)
|
||||
worker.last_bar_ts = current_ts
|
||||
|
||||
worker._save_state()
|
||||
|
||||
|
||||
def load_config(path: Path) -> dict:
|
||||
with open(path) as f:
|
||||
return yaml.safe_load(f)
|
||||
|
||||
|
||||
def build_workers(config: dict) -> tuple[list[StrategyWorker], list[MLWorkerWrapper]]:
|
||||
"""Crea worker da config YAML."""
|
||||
defaults = config.get("defaults", {})
|
||||
regular_workers: list[StrategyWorker] = []
|
||||
ml_workers: list[MLWorkerWrapper] = []
|
||||
|
||||
for entry in config.get("strategies", []):
|
||||
if not entry.get("enabled", True):
|
||||
continue
|
||||
|
||||
name = entry["name"]
|
||||
asset = entry["asset"]
|
||||
tf = entry["tf"]
|
||||
capital = entry.get("capital", defaults.get("capital", 1000))
|
||||
pos_size = entry.get("position_size", defaults.get("position_size", 0.15))
|
||||
leverage = entry.get("leverage", defaults.get("leverage", 3))
|
||||
hold = entry.get("hold_bars", defaults.get("hold_bars", 3))
|
||||
params = entry.get("params", {})
|
||||
|
||||
strategy = load_strategy(name)
|
||||
|
||||
worker = StrategyWorker(
|
||||
strategy=strategy, asset=asset, tf=tf,
|
||||
capital=capital, position_size=pos_size,
|
||||
leverage=leverage, hold_bars=hold,
|
||||
params=params, data_dir=DATA_DIR,
|
||||
)
|
||||
|
||||
if name == "ML01_squeeze_gbm":
|
||||
ml_wrapper = MLWorkerWrapper(worker, {**defaults, **entry})
|
||||
ml_workers.append(ml_wrapper)
|
||||
else:
|
||||
regular_workers.append(worker)
|
||||
|
||||
return regular_workers, ml_workers
|
||||
|
||||
|
||||
def run():
|
||||
config_path = PROJECT_ROOT / "strategies.yml"
|
||||
if not config_path.exists():
|
||||
print(f"ERRORE: {config_path} non trovato")
|
||||
return
|
||||
|
||||
config = load_config(config_path)
|
||||
defaults = config.get("defaults", {})
|
||||
poll_seconds = defaults.get("poll_seconds", 60)
|
||||
lookback_days = 60
|
||||
train_lookback_days = 365
|
||||
|
||||
regular_workers, ml_workers = build_workers(config)
|
||||
all_worker_count = len(regular_workers) + len(ml_workers)
|
||||
|
||||
if all_worker_count == 0:
|
||||
print("Nessuna strategia abilitata in strategies.yml")
|
||||
return
|
||||
|
||||
client = CerberoClient()
|
||||
|
||||
print("=" * 70)
|
||||
print(f" MULTI-STRATEGY PAPER TRADER")
|
||||
print(f" Strategie attive: {all_worker_count}")
|
||||
print(f" Poll: ogni {poll_seconds}s")
|
||||
print(f" Data dir: {DATA_DIR}")
|
||||
print("=" * 70)
|
||||
|
||||
for w in regular_workers:
|
||||
print(f" • {w.status_summary}")
|
||||
for mw in ml_workers:
|
||||
print(f" • {mw.worker.status_summary} [ML]")
|
||||
|
||||
send_telegram(f"🚀 Multi-Strategy avviato: {all_worker_count} strategie")
|
||||
|
||||
# Raccogli asset/tf unici per fetch raggruppato
|
||||
def _get_data_keys() -> set[tuple[str, str]]:
|
||||
keys = set()
|
||||
for w in regular_workers:
|
||||
keys.add((w.asset, w.tf))
|
||||
for mw in ml_workers:
|
||||
keys.add((mw.worker.asset, mw.worker.tf))
|
||||
return keys
|
||||
|
||||
# Training iniziale ML
|
||||
for mw in ml_workers:
|
||||
asset = mw.worker.asset
|
||||
instrument = INSTRUMENT_MAP.get(asset, f"{asset}-PERPETUAL")
|
||||
resolution = RESOLUTION_MAP.get(mw.worker.tf, "15")
|
||||
end = datetime.now(timezone.utc)
|
||||
start = end - timedelta(days=train_lookback_days)
|
||||
candles = client.get_historical(instrument, start.strftime("%Y-%m-%d"),
|
||||
end.strftime("%Y-%m-%d"), resolution)
|
||||
if candles:
|
||||
df_train = pd.DataFrame(candles)
|
||||
df_train["timestamp"] = df_train["timestamp"].astype("int64")
|
||||
df_train = df_train.sort_values("timestamp").reset_index(drop=True)
|
||||
mw.train(df_train, hold=mw.worker.hold_bars)
|
||||
|
||||
while True:
|
||||
try:
|
||||
data_keys = _get_data_keys()
|
||||
candle_cache: dict[tuple[str, str], pd.DataFrame] = {}
|
||||
|
||||
for asset, tf in data_keys:
|
||||
instrument = INSTRUMENT_MAP.get(asset, f"{asset}-PERPETUAL")
|
||||
resolution = RESOLUTION_MAP.get(tf, "15")
|
||||
end = datetime.now(timezone.utc)
|
||||
start = end - timedelta(days=lookback_days)
|
||||
|
||||
candles = client.get_historical(
|
||||
instrument, start.strftime("%Y-%m-%d"),
|
||||
end.strftime("%Y-%m-%d"), resolution,
|
||||
)
|
||||
if candles:
|
||||
df = pd.DataFrame(candles)
|
||||
df["timestamp"] = df["timestamp"].astype("int64")
|
||||
df = df.sort_values("timestamp").reset_index(drop=True)
|
||||
candle_cache[(asset, tf)] = df
|
||||
|
||||
# Tick regular workers
|
||||
for w in regular_workers:
|
||||
key = (w.asset, w.tf)
|
||||
if key in candle_cache:
|
||||
try:
|
||||
w.tick(candle_cache[key])
|
||||
except Exception as e:
|
||||
print(f" [{w.worker_id}] ERRORE: {e}")
|
||||
|
||||
# Tick ML workers
|
||||
for mw in ml_workers:
|
||||
key = (mw.worker.asset, mw.worker.tf)
|
||||
if key not in candle_cache:
|
||||
continue
|
||||
|
||||
if mw.needs_training():
|
||||
mw.train(candle_cache[key], hold=mw.worker.hold_bars)
|
||||
|
||||
try:
|
||||
mw.tick(candle_cache[key])
|
||||
except Exception as e:
|
||||
print(f" [{mw.worker.worker_id}] ERRORE: {e}")
|
||||
|
||||
# Status periodico
|
||||
now = datetime.now(timezone.utc)
|
||||
if now.minute == 0 and now.second < poll_seconds:
|
||||
lines = [f"📊 Status {now.strftime('%H:%M')} UTC"]
|
||||
for w in regular_workers:
|
||||
lines.append(f" {w.status_summary}")
|
||||
for mw in ml_workers:
|
||||
lines.append(f" {mw.worker.status_summary} [ML]")
|
||||
send_telegram("\n".join(lines))
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\nShutdown...")
|
||||
for w in regular_workers:
|
||||
if w.in_position:
|
||||
df = candle_cache.get((w.asset, w.tf))
|
||||
if df is not None and not df.empty:
|
||||
w._close_position(float(df["close"].iloc[-1]), "shutdown")
|
||||
w._save_state()
|
||||
for mw in ml_workers:
|
||||
if mw.worker.in_position:
|
||||
df = candle_cache.get((mw.worker.asset, mw.worker.tf))
|
||||
if df is not None and not df.empty:
|
||||
mw.worker._close_position(float(df["close"].iloc[-1]), "shutdown")
|
||||
mw.worker._save_state()
|
||||
send_telegram("🛑 Multi-Strategy arrestato")
|
||||
break
|
||||
except Exception as e:
|
||||
print(f" ERRORE GLOBALE: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
time.sleep(poll_seconds)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
@@ -10,6 +10,7 @@ import pandas as pd
|
||||
|
||||
from src.live.cerbero_client import CerberoClient
|
||||
from src.live.signal_engine import SignalEngine
|
||||
from src.live.telegram_notifier import notify_event
|
||||
|
||||
LOG_DIR = Path(__file__).resolve().parents[2] / "data" / "paper_trades"
|
||||
INSTRUMENT = "ETH_USDC-PERPETUAL"
|
||||
@@ -52,6 +53,7 @@ class PaperTrader:
|
||||
with open(self.log_path, "a") as f:
|
||||
f.write(json.dumps(entry) + "\n")
|
||||
print(f" [{entry['timestamp'][:19]}] {event}: {json.dumps(data or {})}")
|
||||
notify_event(event, data)
|
||||
|
||||
def save_status(self):
|
||||
status = {
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Import dinamico delle classi Strategy da scripts/strategies/."""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from src.strategies.base import Strategy
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
STRATEGIES_DIR = PROJECT_ROOT / "scripts" / "strategies"
|
||||
|
||||
_REGISTRY: dict[str, type[Strategy]] = {}
|
||||
|
||||
MODULE_MAP = {
|
||||
"SQ01_squeeze_base": ("SQ01_squeeze_base", "SqueezeBase"),
|
||||
"SQ02_antifake_vol": ("SQ02_squeeze_antifake_vol", "SqueezeAntifakeVol"),
|
||||
"SQ03_filtered": ("SQ03_squeeze_all_filters", "SqueezeFiltered"),
|
||||
"SQ04_ultimate": ("SQ04_squeeze_ultimate", "SqueezeUltimate"),
|
||||
"ML01_squeeze_gbm": ("ML01_squeeze_gbm", "SqueezeGBM"),
|
||||
"MT01_squeeze_mtf": ("MT01_squeeze_mtf_momentum", "SqueezeMTFMomentum"),
|
||||
}
|
||||
|
||||
|
||||
def load_strategy(name: str) -> Strategy:
|
||||
"""Carica e istanzia una Strategy per nome."""
|
||||
if name in _REGISTRY:
|
||||
return _REGISTRY[name]()
|
||||
|
||||
if name not in MODULE_MAP:
|
||||
raise ValueError(f"Strategia sconosciuta: {name}. Disponibili: {list(MODULE_MAP)}")
|
||||
|
||||
module_file, class_name = MODULE_MAP[name]
|
||||
module_path = STRATEGIES_DIR / f"{module_file}.py"
|
||||
|
||||
if not module_path.exists():
|
||||
raise FileNotFoundError(f"File strategia non trovato: {module_path}")
|
||||
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
spec = importlib.util.spec_from_file_location(f"strategies.{module_file}", module_path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
cls = getattr(module, class_name)
|
||||
_REGISTRY[name] = cls
|
||||
return cls()
|
||||
|
||||
|
||||
def list_available() -> list[str]:
|
||||
return list(MODULE_MAP.keys())
|
||||
@@ -0,0 +1,226 @@
|
||||
"""Worker per singola strategia — paper trading con stato persistente."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from src.strategies.base import Strategy, Signal
|
||||
from src.live.telegram_notifier import notify_event
|
||||
|
||||
FEE_RT = 0.002
|
||||
|
||||
|
||||
class StrategyWorker:
|
||||
"""Gestisce paper trading per una singola strategia/asset/tf."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
strategy: Strategy,
|
||||
asset: str,
|
||||
tf: str,
|
||||
capital: float = 1000.0,
|
||||
position_size: float = 0.15,
|
||||
leverage: float = 3.0,
|
||||
hold_bars: int = 3,
|
||||
params: dict | None = None,
|
||||
data_dir: Path = Path("data/paper_trades"),
|
||||
):
|
||||
self.strategy = strategy
|
||||
self.asset = asset
|
||||
self.tf = tf
|
||||
self.initial_capital = capital
|
||||
self.position_size = position_size
|
||||
self.leverage = leverage
|
||||
self.hold_bars = hold_bars
|
||||
self.params = params or {}
|
||||
|
||||
self.worker_id = f"{strategy.name}__{asset}__{tf}"
|
||||
self.work_dir = data_dir / self.worker_id
|
||||
self.work_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.trades_path = self.work_dir / "trades.jsonl"
|
||||
self.status_path = self.work_dir / "status.json"
|
||||
|
||||
self.capital = capital
|
||||
self.in_position = False
|
||||
self.direction: int = 0
|
||||
self.entry_price: float = 0
|
||||
self.entry_time: str = ""
|
||||
self.bars_held: int = 0
|
||||
self.total_trades: int = 0
|
||||
self.total_wins: int = 0
|
||||
self.started_at = datetime.now(timezone.utc).isoformat()
|
||||
self.last_bar_ts: int = 0
|
||||
|
||||
self._load_state()
|
||||
self._save_state()
|
||||
|
||||
def _load_state(self):
|
||||
"""Riprende stato da status.json se esiste."""
|
||||
if not self.status_path.exists():
|
||||
self._log("INIT", {"capital": self.capital, "strategy": self.strategy.name,
|
||||
"asset": self.asset, "tf": self.tf})
|
||||
return
|
||||
|
||||
with open(self.status_path) as f:
|
||||
state = json.load(f)
|
||||
|
||||
self.capital = state.get("capital", self.initial_capital)
|
||||
self.in_position = state.get("in_position", False)
|
||||
self.direction = state.get("direction", 0)
|
||||
self.entry_price = state.get("entry_price", 0)
|
||||
self.entry_time = state.get("entry_time", "")
|
||||
self.bars_held = state.get("bars_held", 0)
|
||||
self.total_trades = state.get("total_trades", 0)
|
||||
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._log("RESUME", {"capital": round(self.capital, 2),
|
||||
"total_trades": self.total_trades,
|
||||
"in_position": self.in_position})
|
||||
|
||||
def _save_state(self):
|
||||
state = {
|
||||
"capital": round(self.capital, 2),
|
||||
"in_position": self.in_position,
|
||||
"direction": self.direction,
|
||||
"entry_price": self.entry_price,
|
||||
"entry_time": self.entry_time,
|
||||
"bars_held": self.bars_held,
|
||||
"total_trades": self.total_trades,
|
||||
"total_wins": self.total_wins,
|
||||
"started_at": self.started_at,
|
||||
"last_bar_ts": self.last_bar_ts,
|
||||
"last_update": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
with open(self.status_path, "w") as f:
|
||||
json.dump(state, f, indent=2)
|
||||
|
||||
def _log(self, event: str, data: dict | None = None):
|
||||
entry = {
|
||||
"ts": datetime.now(timezone.utc).isoformat(),
|
||||
"worker": self.worker_id,
|
||||
"event": event,
|
||||
**(data or {}),
|
||||
}
|
||||
with open(self.trades_path, "a") as f:
|
||||
f.write(json.dumps(entry) + "\n")
|
||||
print(f" [{self.worker_id}] {event}: {json.dumps(data or {}, default=str)}")
|
||||
|
||||
def _notify(self, event: str, data: dict | None = None):
|
||||
enriched = {"worker": self.worker_id, **(data or {})}
|
||||
notify_event(event, enriched)
|
||||
|
||||
def _open_position(self, signal: Signal, current_price: float):
|
||||
notional = self.capital * self.position_size * self.leverage
|
||||
size = notional / current_price if current_price > 0 else 0
|
||||
|
||||
self.in_position = True
|
||||
self.direction = signal.direction
|
||||
self.entry_price = current_price
|
||||
self.entry_time = datetime.now(timezone.utc).isoformat()
|
||||
self.bars_held = 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),
|
||||
}
|
||||
self._log("OPEN", trade_data)
|
||||
self._notify("OPENED", trade_data)
|
||||
|
||||
def _close_position(self, current_price: float, reason: str):
|
||||
if not self.in_position:
|
||||
return
|
||||
|
||||
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
|
||||
pnl = self.capital * self.position_size * net
|
||||
|
||||
is_win = trade_return > 0
|
||||
self.capital += pnl
|
||||
self.capital = max(self.capital, 0)
|
||||
self.total_trades += 1
|
||||
if is_win:
|
||||
self.total_wins += 1
|
||||
|
||||
accuracy = self.total_wins / self.total_trades * 100 if self.total_trades > 0 else 0
|
||||
|
||||
trade_data = {
|
||||
"reason": reason,
|
||||
"direction": "long" if self.direction == 1 else "short",
|
||||
"entry": round(self.entry_price, 2),
|
||||
"exit": round(current_price, 2),
|
||||
"pnl": round(pnl, 2),
|
||||
"net_return": round(net * 100, 3),
|
||||
"capital": round(self.capital, 2),
|
||||
"bars_held": self.bars_held,
|
||||
"win": is_win,
|
||||
"total_trades": self.total_trades,
|
||||
"accuracy": round(accuracy, 1),
|
||||
}
|
||||
self._log("CLOSE", trade_data)
|
||||
self._notify("CLOSED", trade_data)
|
||||
|
||||
self.in_position = False
|
||||
self.direction = 0
|
||||
self.entry_price = 0
|
||||
self.entry_time = ""
|
||||
self.bars_held = 0
|
||||
|
||||
def tick(self, df: pd.DataFrame):
|
||||
"""Chiamato ad ogni poll con DataFrame OHLCV aggiornato."""
|
||||
if df.empty or len(df) < 100:
|
||||
return
|
||||
|
||||
c = df["close"].values
|
||||
current_price = float(c[-1])
|
||||
current_ts = int(df["timestamp"].iloc[-1])
|
||||
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||
|
||||
if self.in_position:
|
||||
if current_ts > self.last_bar_ts:
|
||||
self.bars_held += 1
|
||||
self.last_bar_ts = current_ts
|
||||
|
||||
if 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
|
||||
if pnl_pct <= -0.02:
|
||||
self._close_position(current_price, "stop_loss")
|
||||
|
||||
self._save_state()
|
||||
return
|
||||
|
||||
# Genera segnali
|
||||
signals = self.strategy.generate_signals(
|
||||
df, ts, asset=self.asset, tf=self.tf, **self.params
|
||||
)
|
||||
|
||||
if not signals:
|
||||
self._save_state()
|
||||
return
|
||||
|
||||
last_signal = signals[-1]
|
||||
last_idx = len(df) - 1
|
||||
|
||||
if last_signal.idx >= last_idx - 1:
|
||||
self._open_position(last_signal, current_price)
|
||||
self.last_bar_ts = current_ts
|
||||
|
||||
self._save_state()
|
||||
|
||||
@property
|
||||
def status_summary(self) -> str:
|
||||
acc = self.total_wins / self.total_trades * 100 if self.total_trades > 0 else 0
|
||||
pos = "LONG" if self.direction == 1 else "SHORT" if self.direction == -1 else "FLAT"
|
||||
return (f"{self.worker_id}: €{self.capital:.0f} | {self.total_trades}t "
|
||||
f"{acc:.0f}% | {pos}")
|
||||
@@ -0,0 +1,39 @@
|
||||
"""Notifiche Telegram per il paper trader."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import json
|
||||
|
||||
BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "")
|
||||
CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "")
|
||||
|
||||
NOTIFY_EVENTS = {
|
||||
"SIGNAL", "OPENED", "CLOSED", "OPEN_FAILED", "CLOSE_FAILED",
|
||||
"ERROR", "STARTUP", "SHUTDOWN", "TRAINING_FAILED",
|
||||
}
|
||||
|
||||
|
||||
def send_telegram(text: str) -> bool:
|
||||
if not BOT_TOKEN or not CHAT_ID:
|
||||
return False
|
||||
try:
|
||||
url = f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage"
|
||||
data = urllib.parse.urlencode({"chat_id": CHAT_ID, "text": text, "parse_mode": "HTML"}).encode()
|
||||
urllib.request.urlopen(url, data, timeout=10)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def notify_event(event: str, data: dict | None = None):
|
||||
if event not in NOTIFY_EVENTS:
|
||||
return
|
||||
lines = [f"📊 <b>{event}</b>"]
|
||||
if data:
|
||||
for k, v in data.items():
|
||||
if k in ("signal",):
|
||||
continue
|
||||
lines.append(f" {k}: {v}")
|
||||
send_telegram("\n".join(lines))
|
||||
@@ -0,0 +1,11 @@
|
||||
"""Strategie di trading — classe base e indicatori condivisi."""
|
||||
from src.strategies.base import Strategy, Signal, BacktestResult, YearlyStats
|
||||
from src.strategies.indicators import (
|
||||
keltner_ratio, detect_squeezes, ema, atr, rv_annualized, rolling_correlation,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"Strategy", "Signal", "BacktestResult", "YearlyStats",
|
||||
"keltner_ratio", "detect_squeezes", "ema", "atr",
|
||||
"rv_annualized", "rolling_correlation",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
"""Classe base astratta per tutte le strategie di trading."""
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from src.data.downloader import load_data
|
||||
|
||||
|
||||
@dataclass
|
||||
class Signal:
|
||||
"""Segnale di trading generato da una strategia."""
|
||||
idx: int
|
||||
direction: int # +1 long, -1 short
|
||||
entry_price: float
|
||||
metadata: dict = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class YearlyStats:
|
||||
year: int
|
||||
trades: int
|
||||
wins: int
|
||||
pnl: float
|
||||
|
||||
@property
|
||||
def accuracy(self) -> float:
|
||||
return self.wins / self.trades * 100 if self.trades > 0 else 0.0
|
||||
|
||||
|
||||
@dataclass
|
||||
class BacktestResult:
|
||||
"""Risultato completo di un backtest."""
|
||||
strategy_name: str
|
||||
asset: str
|
||||
timeframe: str
|
||||
params: dict
|
||||
|
||||
trades: int
|
||||
wins: int
|
||||
pnl: float
|
||||
capital: float
|
||||
initial_capital: float
|
||||
max_dd: float
|
||||
time_in_market_pct: float
|
||||
avg_trade_duration_h: float
|
||||
years_active: int
|
||||
yearly: list[YearlyStats]
|
||||
|
||||
@property
|
||||
def accuracy(self) -> float:
|
||||
return self.wins / self.trades * 100 if self.trades > 0 else 0.0
|
||||
|
||||
@property
|
||||
def sharpe(self) -> float:
|
||||
pnls = []
|
||||
for ys in self.yearly:
|
||||
pnls.append(ys.pnl)
|
||||
if len(pnls) < 2 or np.std(pnls) == 0:
|
||||
return 0.0
|
||||
return float(np.mean(pnls) / np.std(pnls) * np.sqrt(len(pnls)))
|
||||
|
||||
@property
|
||||
def daily_pnl(self) -> float:
|
||||
return self.pnl / (self.years_active * 365) if self.years_active > 0 else 0.0
|
||||
|
||||
@property
|
||||
def worst_year(self) -> YearlyStats | None:
|
||||
valid = [y for y in self.yearly if y.trades >= 10]
|
||||
if not valid:
|
||||
valid = self.yearly
|
||||
return min(valid, key=lambda y: y.accuracy) if valid else None
|
||||
|
||||
def print_summary(self):
|
||||
worst = self.worst_year
|
||||
worst_str = f"{worst.year}({worst.accuracy:.0f}%)" if worst else "N/A"
|
||||
dur = f"{self.avg_trade_duration_h:.0f}h" if self.avg_trade_duration_h >= 1 else f"{self.avg_trade_duration_h * 60:.0f}m"
|
||||
print(f" {self.strategy_name:<30s} {self.asset:>3s} {self.timeframe:>3s} "
|
||||
f"{self.trades:>5d}t {self.accuracy:>5.1f}% "
|
||||
f"€{self.pnl:>+9.0f} DD {self.max_dd:>4.1f}% "
|
||||
f"€/d {self.daily_pnl:>+6.2f} "
|
||||
f"Mkt {self.time_in_market_pct:>4.1f}% {dur:>5s} "
|
||||
f"worst={worst_str} {self.years_active}y")
|
||||
|
||||
def print_yearly(self):
|
||||
print(f"\n {self.strategy_name} [{self.asset} {self.timeframe}] — per anno:")
|
||||
print(f" {'Anno':>6s} {'Trades':>7s} {'Acc':>6s} {'PnL€':>9s}")
|
||||
for ys in sorted(self.yearly, key=lambda y: y.year):
|
||||
print(f" {ys.year:>6d} {ys.trades:>7d} {ys.accuracy:>5.1f}% €{ys.pnl:>+8.0f}")
|
||||
|
||||
|
||||
TF_MINUTES = {"1m": 1, "5m": 5, "15m": 15, "1h": 60, "4h": 240, "1d": 1440}
|
||||
|
||||
|
||||
class Strategy(ABC):
|
||||
"""Classe base per tutte le strategie.
|
||||
|
||||
Sottoclassi devono implementare:
|
||||
- name, description, default_assets, default_timeframes
|
||||
- generate_signals(df, timestamps, **params) -> list[Signal]
|
||||
"""
|
||||
|
||||
name: str = "unnamed"
|
||||
description: str = ""
|
||||
default_assets: list[str] = ["BTC", "ETH"]
|
||||
default_timeframes: list[str] = ["15m", "1h"]
|
||||
|
||||
# Parametri di backtest
|
||||
fee_rt: float = 0.002
|
||||
leverage: float = 3.0
|
||||
position_size: float = 0.15
|
||||
initial_capital: float = 1000.0
|
||||
|
||||
@abstractmethod
|
||||
def generate_signals(self, df: pd.DataFrame, ts: pd.DatetimeIndex,
|
||||
**params) -> list[Signal]:
|
||||
"""Genera segnali di trading dal dataframe OHLCV.
|
||||
|
||||
Args:
|
||||
df: DataFrame con colonne open, high, low, close, volume, timestamp
|
||||
ts: DatetimeIndex UTC dei timestamp
|
||||
**params: parametri specifici della strategia
|
||||
|
||||
Returns:
|
||||
Lista di Signal con idx, direction, entry_price
|
||||
"""
|
||||
...
|
||||
|
||||
def backtest(self, asset: str, tf: str, hold: int = 3,
|
||||
**params) -> BacktestResult | None:
|
||||
"""Esegue backtest su un asset/timeframe."""
|
||||
df = load_data(asset, tf)
|
||||
c = df["close"].values
|
||||
n = len(c)
|
||||
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||
|
||||
sig_params = {**params, "asset": asset, "tf": tf}
|
||||
signals = self.generate_signals(df, ts, **sig_params)
|
||||
if not signals:
|
||||
return None
|
||||
|
||||
yearly: dict[int, dict] = {}
|
||||
capital = float(self.initial_capital)
|
||||
peak = capital
|
||||
max_dd = 0.0
|
||||
total_bars = 0
|
||||
|
||||
for sig in signals:
|
||||
i = sig.idx
|
||||
if i + hold >= n or i < 1:
|
||||
continue
|
||||
|
||||
entry = sig.entry_price
|
||||
exit_price = c[min(i + hold - 1, n - 1)]
|
||||
actual = (exit_price - entry) / entry * sig.direction
|
||||
net = actual * self.leverage - self.fee_rt * self.leverage
|
||||
|
||||
capital += capital * self.position_size * net
|
||||
capital = max(capital, 10)
|
||||
if capital > peak:
|
||||
peak = capital
|
||||
dd = (peak - capital) / peak
|
||||
max_dd = max(max_dd, dd)
|
||||
total_bars += hold
|
||||
|
||||
year = ts.iloc[i].year
|
||||
if year not in yearly:
|
||||
yearly[year] = {"w": 0, "t": 0, "pnl": 0.0}
|
||||
yearly[year]["t"] += 1
|
||||
if actual > 0:
|
||||
yearly[year]["w"] += 1
|
||||
yearly[year]["pnl"] += net * self.initial_capital
|
||||
|
||||
all_t = sum(d["t"] for d in yearly.values())
|
||||
all_w = sum(d["w"] for d in yearly.values())
|
||||
if all_t == 0:
|
||||
return None
|
||||
|
||||
yearly_stats = [
|
||||
YearlyStats(year=y, trades=d["t"], wins=d["w"], pnl=d["pnl"])
|
||||
for y, d in sorted(yearly.items())
|
||||
]
|
||||
|
||||
return BacktestResult(
|
||||
strategy_name=self.name,
|
||||
asset=asset,
|
||||
timeframe=tf,
|
||||
params=params,
|
||||
trades=all_t,
|
||||
wins=all_w,
|
||||
pnl=sum(d["pnl"] for d in yearly.values()),
|
||||
capital=capital,
|
||||
initial_capital=self.initial_capital,
|
||||
max_dd=max_dd * 100,
|
||||
time_in_market_pct=total_bars / n * 100,
|
||||
avg_trade_duration_h=hold * TF_MINUTES.get(tf, 60) / 60,
|
||||
years_active=len(yearly),
|
||||
yearly=yearly_stats,
|
||||
)
|
||||
|
||||
def run_all(self, assets: list[str] | None = None,
|
||||
timeframes: list[str] | None = None,
|
||||
hold: int = 3, **params) -> list[BacktestResult]:
|
||||
"""Esegue backtest su tutte le combinazioni asset/timeframe."""
|
||||
assets = assets or self.default_assets
|
||||
timeframes = timeframes or self.default_timeframes
|
||||
results = []
|
||||
for asset in assets:
|
||||
for tf in timeframes:
|
||||
r = self.backtest(asset, tf, hold=hold, **params)
|
||||
if r and r.trades >= 20:
|
||||
results.append(r)
|
||||
results.sort(key=lambda r: r.accuracy, reverse=True)
|
||||
return results
|
||||
|
||||
def report(self, results: list[BacktestResult] | None = None,
|
||||
assets: list[str] | None = None,
|
||||
timeframes: list[str] | None = None,
|
||||
hold: int = 3, **params):
|
||||
"""Esegue e stampa report completo."""
|
||||
if results is None:
|
||||
results = self.run_all(assets, timeframes, hold, **params)
|
||||
|
||||
print(f"\n{'=' * 120}")
|
||||
print(f" {self.name} — {self.description}")
|
||||
print(f" Fee: {self.fee_rt*100:.1f}% RT | Leva: {self.leverage:.0f}x | Pos: {self.position_size*100:.0f}%")
|
||||
print(f"{'=' * 120}")
|
||||
print(f" {'Nome':<30s} {'A/T':>7s} {'Trades':>6s} {'Acc':>6s} "
|
||||
f"{'PnL€':>10s} {'DD%':>6s} {'€/day':>7s} "
|
||||
f"{'Mkt%':>5s} {'Dur':>5s} {'Worst':>12s} {'Anni':>4s}")
|
||||
print(f" {'─' * 110}")
|
||||
|
||||
for r in results:
|
||||
r.print_summary()
|
||||
|
||||
if results:
|
||||
best = results[0]
|
||||
best.print_yearly()
|
||||
|
||||
return results
|
||||
@@ -0,0 +1,102 @@
|
||||
"""Indicatori tecnici condivisi tra tutte le strategie."""
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
def keltner_ratio(close: np.ndarray, high: np.ndarray, low: np.ndarray,
|
||||
window: int = 14) -> np.ndarray:
|
||||
"""Rapporto Bollinger / Keltner. Sotto 1 = squeeze (BB dentro KC)."""
|
||||
n = len(close)
|
||||
r = np.full(n, np.nan)
|
||||
for i in range(window, n):
|
||||
wc = close[i - window:i]
|
||||
wh = high[i - window:i]
|
||||
wl = low[i - window:i]
|
||||
ma = np.mean(wc)
|
||||
bb_std = np.std(wc)
|
||||
tr = np.maximum(
|
||||
wh - wl,
|
||||
np.maximum(np.abs(wh - np.roll(wc, 1)), np.abs(wl - np.roll(wc, 1))),
|
||||
)
|
||||
atr = np.mean(tr[1:])
|
||||
kc = (ma + 1.5 * atr) - (ma - 1.5 * atr)
|
||||
bb = (ma + 2 * bb_std) - (ma - 2 * bb_std)
|
||||
if kc > 0:
|
||||
r[i] = bb / kc
|
||||
return r
|
||||
|
||||
|
||||
def detect_squeezes(close: np.ndarray, high: np.ndarray, low: np.ndarray,
|
||||
kcr: np.ndarray, sq_thr: float = 0.8,
|
||||
min_dur: int = 5) -> list[dict]:
|
||||
"""Rileva squeeze events: periodi dove BB sta dentro KC."""
|
||||
events: list[dict] = []
|
||||
in_sq = False
|
||||
sq_start = 0
|
||||
for i in range(1, len(close)):
|
||||
if np.isnan(kcr[i]):
|
||||
continue
|
||||
is_sq = kcr[i] < sq_thr
|
||||
if is_sq and not in_sq:
|
||||
in_sq = True
|
||||
sq_start = i
|
||||
elif not is_sq and in_sq:
|
||||
in_sq = False
|
||||
dur = i - sq_start
|
||||
if dur < min_dur:
|
||||
continue
|
||||
events.append({
|
||||
"idx": i, "dur": dur, "sq_start": sq_start,
|
||||
"kcr_at_release": kcr[i],
|
||||
})
|
||||
return events
|
||||
|
||||
|
||||
def ema(arr: np.ndarray, period: int) -> np.ndarray:
|
||||
"""Exponential Moving Average."""
|
||||
r = np.full(len(arr), np.nan)
|
||||
k = 2 / (period + 1)
|
||||
r[period - 1] = np.mean(arr[:period])
|
||||
for i in range(period, len(arr)):
|
||||
r[i] = arr[i] * k + r[i - 1] * (1 - k)
|
||||
return r
|
||||
|
||||
|
||||
def atr(high: np.ndarray, low: np.ndarray, close: np.ndarray,
|
||||
period: int = 14) -> np.ndarray:
|
||||
"""Average True Range (EMA-smoothed)."""
|
||||
tr = np.maximum(
|
||||
high - low,
|
||||
np.maximum(np.abs(high - np.roll(close, 1)), np.abs(low - np.roll(close, 1))),
|
||||
)
|
||||
tr[0] = high[0] - low[0]
|
||||
r = np.full(len(close), np.nan)
|
||||
r[period - 1] = np.mean(tr[:period])
|
||||
k = 2 / (period + 1)
|
||||
for i in range(period, len(close)):
|
||||
r[i] = tr[i] * k + r[i - 1] * (1 - k)
|
||||
return r
|
||||
|
||||
|
||||
def rv_annualized(close: np.ndarray, window: int) -> np.ndarray:
|
||||
"""Realized volatility annualizzata (hourly data assumed)."""
|
||||
lr = np.diff(np.log(np.where(close == 0, 1e-10, close)))
|
||||
r = np.full(len(close), np.nan)
|
||||
for i in range(window, len(lr)):
|
||||
r[i + 1] = np.std(lr[i - window:i]) * np.sqrt(24 * 365)
|
||||
return r
|
||||
|
||||
|
||||
def rolling_correlation(close_a: np.ndarray, close_b: np.ndarray,
|
||||
window: int = 48) -> np.ndarray:
|
||||
"""Correlazione rolling tra rendimenti logaritmici di due asset."""
|
||||
n = max(len(close_a), len(close_b))
|
||||
ret_a = np.diff(np.log(np.where(close_a == 0, 1e-10, close_a)))
|
||||
ret_b = np.diff(np.log(np.where(close_b[:len(close_a)] == 0, 1e-10, close_b[:len(close_a)])))
|
||||
min_len = min(len(ret_a), len(ret_b))
|
||||
corr = np.full(n, np.nan)
|
||||
for i in range(window, min_len):
|
||||
cv = np.corrcoef(ret_a[i - window:i], ret_b[i - window:i])[0, 1]
|
||||
corr[i + 1] = cv if np.isfinite(cv) else 0
|
||||
return corr
|
||||
@@ -0,0 +1,51 @@
|
||||
defaults:
|
||||
capital: 1000
|
||||
position_size: 0.15
|
||||
leverage: 3
|
||||
hold_bars: 3
|
||||
poll_seconds: 60
|
||||
retrain_hours: 24
|
||||
|
||||
strategies:
|
||||
- name: SQ02_antifake_vol
|
||||
asset: BTC
|
||||
tf: 15m
|
||||
enabled: true
|
||||
|
||||
- name: SQ02_antifake_vol
|
||||
asset: ETH
|
||||
tf: 15m
|
||||
enabled: true
|
||||
|
||||
- name: SQ01_squeeze_base
|
||||
asset: BTC
|
||||
tf: 15m
|
||||
enabled: true
|
||||
|
||||
- name: ML01_squeeze_gbm
|
||||
asset: ETH
|
||||
tf: 15m
|
||||
enabled: true
|
||||
position_size: 0.15
|
||||
params:
|
||||
ml_threshold: 0.70
|
||||
bb_window: 14
|
||||
sq_threshold: 0.8
|
||||
|
||||
- name: MT01_squeeze_mtf
|
||||
asset: BTC
|
||||
tf: 15m
|
||||
enabled: true
|
||||
params:
|
||||
ema_period: 20
|
||||
min_slope: 0.001
|
||||
vol_filter: true
|
||||
|
||||
- name: MT01_squeeze_mtf
|
||||
asset: ETH
|
||||
tf: 15m
|
||||
enabled: true
|
||||
params:
|
||||
ema_period: 20
|
||||
min_slope: 0.001
|
||||
vol_filter: true
|
||||
@@ -542,30 +542,30 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "cuda-bindings"
|
||||
version = "13.2.0"
|
||||
version = "13.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cuda-pathfinder", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/a9/3a8241c6e19483ac1f1dcf5c10238205dcb8a6e9d0d4d4709240dff28ff4/cuda_bindings-13.2.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:721104c603f059780d287969be3d194a18d0cc3b713ed9049065a1107706759d", size = 5730273, upload-time = "2026-03-11T00:12:37.18Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/94/2748597f47bb1600cd466b20cab4159f1530a3a33fe7f70fee199b3abb9e/cuda_bindings-13.2.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1eba9504ac70667dd48313395fe05157518fd6371b532790e96fbb31bbb5a5e1", size = 6313924, upload-time = "2026-03-11T00:12:39.462Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/c8/b2589d68acf7e3d63e2be330b84bc25712e97ed799affbca7edd7eae25d6/cuda_bindings-13.2.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e865447abfb83d6a98ad5130ed3c70b1fc295ae3eeee39fd07b4ddb0671b6788", size = 5722404, upload-time = "2026-03-11T00:12:44.041Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/92/f899f7bbb5617bb65ec52a6eac1e9a1447a86b916c4194f8a5001b8cde0c/cuda_bindings-13.2.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46d8776a55d6d5da9dd6e9858fba2efcda2abe6743871dee47dd06eb8cb6d955", size = 6320619, upload-time = "2026-03-11T00:12:45.939Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/93/eef988860a3ca985f82c4f3174fc0cdd94e07331ba9a92e8e064c260337f/cuda_bindings-13.2.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6629ca2df6f795b784752409bcaedbd22a7a651b74b56a165ebc0c9dcbd504d0", size = 5614610, upload-time = "2026-03-11T00:12:50.337Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/23/6db3aba46864aee357ab2415135b3fe3da7e9f1fa0221fa2a86a5968099c/cuda_bindings-13.2.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7dca0da053d3b4cc4869eff49c61c03f3c5dbaa0bcd712317a358d5b8f3f385d", size = 6149914, upload-time = "2026-03-11T00:12:52.374Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/87/87a014f045b77c6de5c8527b0757fe644417b184e5367db977236a141602/cuda_bindings-13.2.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6464b30f46692d6c7f65d4a0e0450d81dd29de3afc1bb515653973d01c2cd6e", size = 5685673, upload-time = "2026-03-11T00:12:56.371Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/5e/c0fe77a73aaefd3fff25ffaccaac69c5a63eafdf8b9a4c476626ef0ac703/cuda_bindings-13.2.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4af9f3e1be603fa12d5ad6cfca7844c9d230befa9792b5abdf7dd79979c3626", size = 6191386, upload-time = "2026-03-11T00:12:58.965Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/58/ed2c3b39c8dd5f96aa7a4abef0d47a73932c7a988e30f5fa428f00ed0da1/cuda_bindings-13.2.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df850a1ff8ce1b3385257b08e47b70e959932f5f432d0a4e46a355962b4e4771", size = 5507469, upload-time = "2026-03-11T00:13:04.063Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/01/0c941b112ceeb21439b05895eace78ca1aa2eaaf695c8521a068fd9b4c00/cuda_bindings-13.2.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8a16384c6494e5485f39314b0b4afb04bee48d49edb16d5d8593fd35bbd231b", size = 6059693, upload-time = "2026-03-11T00:13:06.003Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/52/50673d25e46d199556f827514bf646a49471d50538c5e577201245b348a9/cuda_bindings-13.3.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:244a167d81a9d07d3209d02c8e02e848c2b7c38f4d01e8e4d1f9620b173ae006", size = 6051409, upload-time = "2026-05-27T03:59:01.648Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/ee/e8f4bdfb808c3689539b7c035d63b6dac9f236b2d6f807f18c7f5f3ef879/cuda_bindings-13.3.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:820ab45be3f39a088f39c4a04eb8b852d0b339bff8c518da5c258882b8a4e21b", size = 6671833, upload-time = "2026-05-27T03:59:03.761Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/e0/4b3fdba08ff177e9451f376a4ba2df18d76f9158e6a16cdc062bd83db9fa/cuda_bindings-13.3.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f72f67f5790e7fa51e3f893e007e49567573ccdf4ed1ca988fbbbd36cd77847c", size = 6020531, upload-time = "2026-05-27T03:59:07.942Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/40/a2ea4d8f032bfd6c220d50b6f92cd61f33d48f31959da39ed1b178cfee54/cuda_bindings-13.3.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a99c3b8d584f266c616bd0f30c7cd83e33553e3ef2abad41ff5a74fbc033a69a", size = 6653764, upload-time = "2026-05-27T03:59:09.981Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/a0/156efe7816699c2de1ea2395031db7d010b7af23c243563a3ee6f0ecc1de/cuda_bindings-13.3.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7698fcc4577aa96372866f4d0c9a6cf686cd5c90eab94581c29d37fe6600542", size = 5914803, upload-time = "2026-05-27T03:59:14.011Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/91/510aae64d53227b5b36db6bfaea41514b66d92cd65ddc43aa49566f18313/cuda_bindings-13.3.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:abd908f651160d12c45c5714a38ee102a1173a55433c0d1509ec0e8293beb4a6", size = 6472506, upload-time = "2026-05-27T03:59:16.551Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/53/2ef49e5b3734a5531b2ba5d726cba724d9cbb262404e586ed61070604826/cuda_bindings-13.3.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a801fa30e75d25b74252123aefc746b6c4275624d2b8640632dd1dfeeaa1f88", size = 6008814, upload-time = "2026-05-27T03:59:20.921Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/cb/3a9fcf0651e0a49b4d0f1955837ce079245b27086c22fb2f253039bdf324/cuda_bindings-13.3.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94d40ef7b4bdd9dce0244a1baa132e0e538f1eb2c0d162fb3648a15e48515365", size = 6531477, upload-time = "2026-05-27T03:59:23.391Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/0f/6987c5ee98f117317a85650ddc79480a3fa59a573ae1c923d0722b56ae71/cuda_bindings-13.3.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e5911bea15810b749a8077f8c45423ed785d51618b8e8664dea1fc8f5a2a76c8", size = 5807073, upload-time = "2026-05-27T03:59:28.218Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/ab/46ceee07dc19f18a5d1c28d592750ed9dbdc803077eb083576a442c9938c/cuda_bindings-13.3.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2870fed7707a37f8af0c02364b05f355ebe8921604e8c68eb56cf66867e0798", size = 6354325, upload-time = "2026-05-27T03:59:30.715Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cuda-pathfinder"
|
||||
version = "1.5.4"
|
||||
version = "1.5.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/11/d0/c177e29701cf1d3008d7d2b16b5fc626592ce13bd535f8795c5f57187e0e/cuda_pathfinder-1.5.4-py3-none-any.whl", hash = "sha256:9563d3175ce1828531acf4b94e1c1c7d67208c347ca002493e2654878b26f4b7", size = 51657, upload-time = "2026-04-27T22:42:07.712Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/c8/26f2e4aae92f11522a96043892ba39a90eac610d5242523aa863212bc1c7/cuda_pathfinder-1.5.5-py3-none-any.whl", hash = "sha256:0228c023f95d1480f143ef5c8922d27a2ab052087a942e81dc289c9eb8f91689", size = 51671, upload-time = "2026-05-27T01:21:25.413Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2057,6 +2057,7 @@ dependencies = [
|
||||
{ name = "numpy" },
|
||||
{ name = "pandas" },
|
||||
{ name = "pyarrow" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "requests" },
|
||||
{ name = "scikit-learn" },
|
||||
{ name = "scipy" },
|
||||
@@ -2081,6 +2082,7 @@ requires-dist = [
|
||||
{ name = "pyarrow", specifier = ">=15.0" },
|
||||
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" },
|
||||
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.24" },
|
||||
{ name = "pyyaml", specifier = ">=6.0" },
|
||||
{ name = "requests", specifier = ">=2.31" },
|
||||
{ name = "scikit-learn", specifier = ">=1.3" },
|
||||
{ name = "scipy", specifier = ">=1.11" },
|
||||
@@ -2101,6 +2103,61 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyyaml"
|
||||
version = "6.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.34.2"
|
||||
|
||||
Reference in New Issue
Block a user