Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b01443efe | |||
| 5ac9ebed5b | |||
| 1561005d41 | |||
| b9e5176a5b |
@@ -292,3 +292,40 @@ curl -X POST http://localhost:9000/mcp-bybit/tools/get_ticker \
|
||||
|
||||
Per lo schema completo dei body di richiesta e risposta:
|
||||
<http://localhost:9000/apidocs>.
|
||||
|
||||
---
|
||||
|
||||
## 15. Discovery strumenti — schemi `get_instruments` / `get_markets` / `get_historical`
|
||||
|
||||
Schemi dei body verificati sull'OpenAPI live (usati da `src/data/instruments.py`).
|
||||
|
||||
### Lista strumenti
|
||||
| Exchange | Tool | Body | Risposta (campi utili) |
|
||||
|---|---|---|---|
|
||||
| Deribit | `get_instruments` | `{currency:"any", kind:"future", offset:int, limit:100}` (paginato, `has_more`) | `instruments[].name` (es. `BTC-PERPETUAL`, `SOL_USDC-PERPETUAL`), `expiry`, `tick_size` |
|
||||
| Bybit | `get_instruments` | `{category:"linear", symbol?}` | `instruments[]`: `symbol`, `status`, `base_coin`, `quote_coin` |
|
||||
| Hyperliquid | `get_markets` | `{}` | lista `{asset, mark_price, funding_rate, open_interest, volume_24h, max_leverage}` |
|
||||
|
||||
### Storico OHLCV (`get_historical`, chiave `candles` uniforme `{timestamp(ms),open,high,low,close,volume}`)
|
||||
| Exchange | Body |
|
||||
|---|---|
|
||||
| Deribit | `{instrument, start_date:"YYYY-MM-DD", end_date, resolution}` — resolution `1/5/15/60/1D` |
|
||||
| Bybit | `{symbol, category:"linear", interval:"1/5/15/60/D", start:int_ms, end:int_ms, limit}` |
|
||||
| Hyperliquid | `{asset|instrument, start_date, end_date, resolution:"1m/5m/15m/1h/1d", limit}` |
|
||||
|
||||
### Simboli Deribit
|
||||
- BTC/ETH → perpetui **inverse**: `BTC-PERPETUAL`, `ETH-PERPETUAL`
|
||||
- Altcoin → perpetui **lineari USDC**: `<COIN>_USDC-PERPETUAL` (es. `SOL_USDC-PERPETUAL`)
|
||||
- Trappola: `LTC-PERPETUAL`/`ADA-PERPETUAL` non esistono; `SOL-PERPETUAL` esiste ma è un contratto sbagliato (prezzo ~9.6 vs SOL reale ~82).
|
||||
|
||||
### Validazione (lato progetto)
|
||||
`src/data/instruments.py` valida ogni strumento sui dati storici realmente
|
||||
raccoglibili — esistenza, congruenza OHLC, not-flat, liquidità (volume daily) e
|
||||
**congruenza prezzo cross-exchange** (scostamento dalla mediana del base-coin ≤5%).
|
||||
Solo gli exchange con feed affidabile sono inclusi: **Deribit** e **Hyperliquid**
|
||||
(esclusi Alpaca/stocks e **Bybit**, il cui feed testnet è farlocco). Output in
|
||||
`data/instruments_registry.json`; il downloader scarica **solo** strumenti validati.
|
||||
|
||||
> **Testnet.** Il token osservatore punta a testnet (`"testnet": true` nei ticker):
|
||||
> i prezzi possono divergere dal mainnet. La congruenza cross-exchange via mediana
|
||||
> è il filtro che scarta i feed incongrui prima di usarli per backtest/trading.
|
||||
|
||||
@@ -17,7 +17,9 @@ Progetto di ricerca: riconoscimento pattern frattali per trading algoritmico su
|
||||
## Struttura
|
||||
|
||||
```
|
||||
src/data/ → download e caricamento dati (downloader.py)
|
||||
src/data/ → download e caricamento dati
|
||||
downloader.py → download/caricamento parquet (gate: solo strumenti validati)
|
||||
instruments.py → discovery + validazione strumenti per exchange, registry
|
||||
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
|
||||
@@ -34,25 +36,30 @@ src/live/ → paper trading live multi-strategia
|
||||
cerbero_client.py → client HTTP per Cerbero MCP (Deribit testnet)
|
||||
signal_engine.py → squeeze + ML real-time (legacy ML01, ora in waste) + validazione OOS
|
||||
telegram_notifier.py → notifiche Telegram per trade
|
||||
scripts/strategies/ → strategie con edge validato OOS (solo MR01_bollinger_fade)
|
||||
scripts/strategies/ → strategie con edge validato OOS: FADE (MR01/MR02/MR07),
|
||||
HONEST (DIP01/TR01/ROT02), PAIRS (PR01), TSMOM + portafogli (PORT01/02/03)
|
||||
scripts/waste/ → strategie scartate (W01-W28 + famiglia squeeze SQ/MT/ML/AD/CM/PD)
|
||||
scripts/analysis/ → ricerca/validazione OOS fee-aware (strategy_research, oos_validation, ...)
|
||||
strategies.yml → config multi-strategy paper trader
|
||||
docs/diary/ → diario di ricerca giornaliero
|
||||
docs/specs/ → specifiche di design
|
||||
data/raw/ → file .parquet OHLCV (gitignored)
|
||||
data/instruments_registry.json → allowlist strumenti validati (gate del downloader)
|
||||
```
|
||||
|
||||
## Comandi
|
||||
|
||||
```bash
|
||||
uv sync # installa dipendenze
|
||||
uv run python -m src.data.downloader # scarica dati storici
|
||||
uv run python scripts/strategies/MR01_bollinger_fade.py # strategia attiva (mean-reversion)
|
||||
uv run python -m src.data.downloader # scarica dati storici (solo strumenti validati)
|
||||
uv run python -m src.data.instruments # (ri)costruisci il registry strumenti validati
|
||||
uv run python scripts/strategies/MR01_bollinger_fade.py # backtest una strategia (es. fade)
|
||||
uv run python scripts/strategies/PR01_pairs_reversion.py # backtest pairs market-neutral
|
||||
uv run python scripts/analysis/strategy_research.py # ricerca strategie fee-aware OOS
|
||||
uv run python scripts/analysis/oos_validation.py # perche' la famiglia squeeze e' scartata
|
||||
uv run python scripts/analysis/validate_worker_mr01.py # replay worker reale su MR01
|
||||
uv run python -m src.live.multi_runner # paper trading live multi-strategia
|
||||
uv run python scripts/analysis/report_families.py # report per anno di tutte le famiglie
|
||||
uv run python scripts/analysis/validate_worker_pairs.py # replay worker 2 gambe == backtest
|
||||
uv run python -m src.live.multi_runner # paper trading live multi-strategia (strategie + pairs)
|
||||
docker compose up -d # deploy Docker
|
||||
uv run pytest # test
|
||||
```
|
||||
@@ -70,6 +77,27 @@ 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.
|
||||
|
||||
### Strumenti & validazione (gate raccolta dati)
|
||||
|
||||
`src/data/instruments.py` scopre e **valida** gli strumenti per ogni exchange
|
||||
implementato — **Deribit** e **Hyperliquid** (esclusi Alpaca/stocks e **Bybit**,
|
||||
il cui feed testnet è farlocco). Per ogni perpetuo enumera via `get_instruments`
|
||||
/`get_markets` e verifica sui **dati storici realmente raccoglibili**:
|
||||
esistenza, congruenza OHLC, not-flat (scarta contratti morti), liquidità (volume
|
||||
daily) e **congruenza prezzo cross-exchange** (scostamento dalla mediana del
|
||||
base-coin ≤ 5% → scarta outlier come `SOL-PERPETUAL`=9.6 vs SOL reale ~82).
|
||||
|
||||
Output: `data/instruments_registry.json` (strumenti validi, timeframe, start-date).
|
||||
**Gate:** `_download_cerbero_range` rifiuta gli strumenti non validati (override
|
||||
`allow_unvalidated=True` solo per casi eccezionali). Rigenera con
|
||||
`python -m src.data.instruments`.
|
||||
|
||||
> **NB testnet.** Il token Cerbero punta a testnet; la congruenza cross-exchange
|
||||
> è il filtro che distingue i feed realistici (Deribit, Hyperliquid) da quelli
|
||||
> farlocchi (Bybit). Simboli Deribit: BTC/ETH = `<COIN>-PERPETUAL` (inverse);
|
||||
> alt = `<COIN>_USDC-PERPETUAL` (lineari USDC). Registry attuale: Deribit 18/106,
|
||||
> Hyperliquid 66/74 validi (major liquidi: BTC dal 2018, alt dal 2022).
|
||||
|
||||
## Strategie attive
|
||||
|
||||
> **LEZIONE CRITICA (2026-05-28).** L'intera famiglia squeeze-breakout (SQ01-04,
|
||||
@@ -198,10 +226,14 @@ queste fade, ma va confermato col paper trader live prima di rischiare capitale
|
||||
|
||||
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. Attualmente solo MR01 (BTC/ETH 1h).
|
||||
**Persistenza:** `data/paper_trades/{strategy}___{asset}__{tf}/` con `trades.jsonl` (append-only) + `status.json` (resume al restart, include tp/sl/max_bars).
|
||||
**Config:** `strategies.yml` — due sezioni: `strategies` (single-leg: fade/honest) e
|
||||
`pairs` (a 2 gambe). Attive: 6 fade (MR01/MR02/MR07 × BTC/ETH) + 5 coppie PR01.
|
||||
**Due worker:** `strategy_worker.py` (single-leg) e `pairs_worker.py` (2 gambe,
|
||||
long A / short B sullo z-score del log-ratio, fee su 2 gambe).
|
||||
**Persistenza:** `data/paper_trades/{worker_id}/` con `trades.jsonl` (append-only) + `status.json` (resume al restart).
|
||||
**Hot-add:** aggiungi riga YAML → `docker compose restart` → storico intatto.
|
||||
**Exit strategia:** se un `Signal` porta `tp`/`sl`/`max_bars` in `metadata` (come MR01), il worker esce su take-profit/stop-loss/time-limit a quei livelli; altrimenti usa il fallback `hold_bars`/stop -2%.
|
||||
**Exit strategia:** se un `Signal` porta `tp`/`sl`/`max_bars` in `metadata` (come le fade), il worker esce su take-profit/stop-loss/time-limit; i pairs escono su |z|≤z_exit o max_bars.
|
||||
**Naming Deribit (feed live):** major = `<COIN>-PERPETUAL` (inverse); alt = `<COIN>_USDC-PERPETUAL` (lineari USDC). Vedi INSTRUMENT_MAP in `multi_runner.py`.
|
||||
**Notifiche:** Telegram per ogni trade (richiede `.env` con `TELEGRAM_BOT_TOKEN` e `TELEGRAM_CHAT_ID`).
|
||||
|
||||
## Convenzioni
|
||||
|
||||
@@ -4,7 +4,7 @@ Sistema di riconoscimento pattern frattali e predizione per il trading di cripto
|
||||
|
||||
## Obiettivo
|
||||
|
||||
Partendo da un capitale iniziale di €1.000, raggiungere un profitto medio di €50 al giorno entro 6–8 mesi, tramite strategie algoritmiche che combinano analisi frattale, squeeze di volatilità e machine learning.
|
||||
Partendo da un capitale iniziale di €1.000, raggiungere un profitto medio di €50 al giorno entro 6–8 mesi, tramite un portafoglio di strategie algoritmiche poco correlate fra loro — mean-reversion, trend/rotazione e spread market-neutral — validate out-of-sample e fee-aware.
|
||||
|
||||
## Risultati
|
||||
|
||||
@@ -15,18 +15,40 @@ Partendo da un capitale iniziale di €1.000, raggiungere un profitto medio di
|
||||
> ingresso onesto (`close[i]`) e fee reali, l'edge sparisce e tutte perdono, anche
|
||||
> a fee zero. Dettagli e prove: `scripts/analysis/oos_validation.py`.
|
||||
|
||||
Dopo una validazione **out-of-sample, fee-aware** di tutte le famiglie, l'unica con
|
||||
edge netto reale è il **mean-reversion** (i breakout *rientrano*, non continuano):
|
||||
Dopo una validazione **out-of-sample, fee-aware** di molte famiglie di strategie,
|
||||
emergono quattro famiglie con edge netto reale, tutte radicate nella stessa lezione
|
||||
(in cripto la **mean-reversion** funziona, la continuazione no) o nella diversificazione:
|
||||
|
||||
| Codice | Strategia | Mercato | Edge OOS netto | Max DD | Robustezza |
|
||||
|--------|-----------|---------|----------------|--------|------------|
|
||||
| **MR01** | Bollinger Fade (mean-reversion) | BTC 1h | **+196 / +201%** | 15% | ✅ |
|
||||
| **MR01** | Bollinger Fade (mean-reversion) | ETH 1h | **+251%** | ~25% | ⚠️ DD alto |
|
||||
| Famiglia | Meccanismo | Strategie | Profilo (netto OOS) |
|
||||
|----------|-----------|-----------|---------------------|
|
||||
| **FADE** | mean-reversion intraday 1h (long/short, BTC/ETH) | MR01 Bollinger, MR02 Donchian, MR07 Return-reversal | Acc 52-55%, DD 18-34% |
|
||||
| **HONEST** | long-only multi-regime multi-crypto | DIP01 dip-buy, TR01 EMA-trend, ROT02 dual-momentum | CAGR 31-56%, DD 15-27% |
|
||||
| **PAIRS** | spread reversion *market-neutral* (2 gambe) | PR01 ETH/BTC, LTC/ETH, ADA/ETH, BTC/LTC, ETH/SOL | Sharpe 2.0-4.4, corr col mercato ~0.05 |
|
||||
| **TSMOM** | time-series momentum multi-orizzonte | TSM01 (3/6/12m + risk-off) | diversificatore, DD 15-22% |
|
||||
|
||||
Netto dopo **fee realistiche Deribit 0.10% RT** (taker), leva 3x, pos 15%, su finestra
|
||||
held-out (nov 2023→mag 2026). MR01 è positivo su **tutta** la griglia parametri
|
||||
(`n∈{14,20,30,50}` × `k∈{2.0,2.5,3.0}`) e per **ogni** livello di fee 0.00-0.20% RT —
|
||||
margine di sicurezza ampio, niente parametro fortunato. Ri-validato col worker live reale.
|
||||
Tutti i numeri sono **netti** dopo fee realistiche (Deribit 0.10% RT single-leg, 0.20%
|
||||
RT/coppia sui pairs), leva 3x, su finestra held-out. Le strategie sono robuste su griglia
|
||||
parametri, sweep fee 0.00-0.20% RT e — per i pairs — validate con **walk-forward** e
|
||||
config universale (niente cherry-picking).
|
||||
|
||||
### Portafoglio combinato (la vera leva anti-drawdown)
|
||||
|
||||
Le famiglie sono **quasi scorrelate fra loro** (~0.05). Combinandole in un unico
|
||||
portafoglio equipesato il drawdown crolla sotto quello di ogni singola sleeve:
|
||||
|
||||
| Portafoglio | CAGR | Max DD | Sharpe |
|
||||
|-------------|------|--------|--------|
|
||||
| FADE (6 sleeve) | ~46% | 8% | 3.9 |
|
||||
| HONEST (3 sleeve) | ~46% | 13% | 2.2 |
|
||||
| **MASTER** (FADE + HONEST, 9) | ~47% | **5%** | 4.2 |
|
||||
| **MASTER + PAIRS + TSM01** (15) | ~67% | ~5% | ~6 |
|
||||
|
||||
> 🔎 **Numeri sobri (anti-overfit).** L'OOS singolo cade nel regime favorevole 2024-25:
|
||||
> i valori di Sharpe/DD sopra sono ottimistici di circa il 50%. Da pianificare per le
|
||||
> decisioni: **Sharpe atteso ~5**, **worst-drawdown su 90 giorni ~6%**, profilo che regge
|
||||
> a leva 2x con slippage raddoppiato. Configurazione raccomandata: equal-weight, leva 2x,
|
||||
> con un cap sull'allocazione ai pairs (~30-35%, poiché concentrano ~57% del rischio).
|
||||
> Tutto resta da confermare nel paper trading live.
|
||||
|
||||
## Come funziona
|
||||
|
||||
@@ -42,6 +64,21 @@ di prezzo **rientrano verso la media** più di quanto proseguano:
|
||||
|
||||
Nessun look-ahead: direzione e livelli sono calcolati con dati fino a `close[i]`.
|
||||
|
||||
### Le altre famiglie
|
||||
|
||||
- **FADE** (oltre MR01): MR02 fada la rottura del canale Donchian verso il centro;
|
||||
MR07 fada il movimento di barra estremo misurato in deviazioni standard dei
|
||||
rendimenti. Stessa logica di reversione, indicatori indipendenti.
|
||||
- **HONEST** (long-only, multi-crypto): DIP01 compra i dip estremi e rivende al
|
||||
recupero; TR01 segue il trend con incrocio di EMA su un paniere; ROT02 ruota ogni
|
||||
giorno sui tre asset col momentum più forte, andando in cash quando BTC è sotto la
|
||||
sua media (risk-off). Coprono i regimi di trend e rotazione, complementari alle fade.
|
||||
- **PAIRS** (market-neutral): scommette sul rientro verso la media del log-ratio fra
|
||||
due cripto (z-score). Long su una, short sull'altra: l'esposizione netta al mercato è
|
||||
quasi nulla (correlazione ~0.02), il che la rende un diversificatore eccellente.
|
||||
- **TSMOM**: tiene gli asset con momentum positivo persistente su più orizzonti
|
||||
(3/6/12 mesi), con overlay risk-off. Rende meno ma è poco correlato, utile in ensemble.
|
||||
|
||||
### Perché lo squeeze breakout è stato abbandonato
|
||||
|
||||
L'ipotesi originale era opposta — *continuazione* dopo la compressione di volatilità
|
||||
@@ -71,16 +108,17 @@ PythagorasGoal/
|
||||
│ │ ├── 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
|
||||
│ ├── multi_runner.py # Orchestratore multi-strategia (strategie + pairs)
|
||||
│ ├── strategy_worker.py # Worker single-leg con stato persistente
|
||||
│ ├── pairs_worker.py # Worker a 2 gambe per i pairs (market-neutral)
|
||||
│ ├── strategy_loader.py # Import dinamico classi Strategy
|
||||
│ ├── cerbero_client.py # Client HTTP per Cerbero MCP
|
||||
│ ├── signal_engine.py # Squeeze + ML real-time (legacy) + validazione OOS
|
||||
│ └── telegram_notifier.py
|
||||
├── scripts/
|
||||
│ ├── strategies/ # Strategie con edge validato OOS (solo MR01_bollinger_fade)
|
||||
│ ├── waste/ # Strategie scartate (W01-W28 + famiglia squeeze SQ/MT/ML/AD/CM/PD)
|
||||
│ └── analysis/ # Ricerca/validazione OOS fee-aware (strategy_research, oos_validation, ...)
|
||||
│ ├── strategies/ # Strategie con edge validato OOS (FADE, HONEST, PAIRS, TSMOM + portafogli)
|
||||
│ ├── waste/ # Strategie scartate (squeeze SQ/MT/ML/AD/CM/PD, MR03, ROT01, W01-W28)
|
||||
│ └── analysis/ # Ricerca/validazione OOS fee-aware, gestione rischio, report
|
||||
├── strategies.yml # Config multi-strategy paper trader
|
||||
├── data/
|
||||
│ └── raw/ # Parquet OHLCV (gitignored, ~70 MB)
|
||||
@@ -94,32 +132,65 @@ PythagorasGoal/
|
||||
|
||||
## Strategie attive
|
||||
|
||||
Tutte le strategie estendono `src.strategies.base.Strategy` (`generate_signals() → backtest()`).
|
||||
Le strategie single-asset estendono `src.strategies.base.Strategy`
|
||||
(`generate_signals() → backtest()`); i pairs hanno un worker dedicato a 2 gambe.
|
||||
|
||||
| Codice | Script | Tipo | Descrizione |
|
||||
|--------|--------|------|-------------|
|
||||
| **MR01** | `MR01_bollinger_fade.py` | Mean-reversion | Fada la banda di Bollinger, TP alla media, SL ad ATR. Unica con edge netto validato OOS. |
|
||||
| Codice | Script | Famiglia | Descrizione |
|
||||
|--------|--------|----------|-------------|
|
||||
| **MR01** | `MR01_bollinger_fade.py` | FADE | Fada la banda di Bollinger, TP alla media, SL ad ATR |
|
||||
| **MR02** | `MR02_donchian_fade.py` | FADE | Fada la rottura del canale Donchian, TP al centro |
|
||||
| **MR07** | `MR07_return_reversal.py` | FADE | Fada il movimento di barra estremo (z dei rendimenti) |
|
||||
| **DIP01** | `DIP01_dip_reversion.py` | HONEST | Dip-buy long-only su z-score estremo |
|
||||
| **TR01** | `TR01_ema_trend.py` | HONEST | EMA 20/100 trend-following su paniere cripto (4h) |
|
||||
| **ROT02** | `ROT02_dual_momentum.py` | HONEST | Rotazione cross-sectional top-3 + risk-off (1d) |
|
||||
| **PR01** | `PR01_pairs_reversion.py` | PAIRS | Spread reversion market-neutral su 5 coppie |
|
||||
| **TSM01** | `tsmom_research.py` | TSMOM | Time-series momentum multi-orizzonte + risk-off |
|
||||
|
||||
La famiglia squeeze (SQ01-04, ML01, MT01, PD01, CM01, AD01) è in `scripts/waste/`:
|
||||
edge storico = artefatto di look-ahead (vedi sezione *Come funziona*).
|
||||
Le fade applicano un **filtro trend** opzionale (`trend_max`/`ema_long`): saltano i
|
||||
segnali quando il prezzo è troppo esteso rispetto alla EMA200 — alza l'accuratezza e
|
||||
abbassa il drawdown. Portafogli pronti: `PORT01` (honest), `PORT02` (fade), `PORT03`
|
||||
(master fade+honest).
|
||||
|
||||
Per eseguire il backtest della strategia:
|
||||
**Scartate** (in `scripts/waste/`): la famiglia squeeze (SQ01-04, ML01, MT01, PD01,
|
||||
CM01, AD01 — artefatto di look-ahead), MR03 Keltner (debole/ridondante con MR01) e
|
||||
ROT01 (dominata da ROT02).
|
||||
|
||||
### Comandi utili
|
||||
|
||||
```bash
|
||||
# Backtest di una strategia
|
||||
uv run python scripts/strategies/MR01_bollinger_fade.py
|
||||
```
|
||||
uv run python scripts/strategies/PR01_pairs_reversion.py
|
||||
|
||||
Per la ricerca/validazione fee-aware out-of-sample:
|
||||
# Ricerca e validazione fee-aware out-of-sample
|
||||
uv run python scripts/analysis/strategy_research.py # screening famiglie + deep-dive fade
|
||||
uv run python scripts/analysis/strategy_research_v2.py # MR02 / MR03 / MR07
|
||||
uv run python scripts/analysis/oos_validation.py # perche' la famiglia squeeze e' scartata
|
||||
uv run python scripts/analysis/pairs_research.py # ricerca + verifica no-look-ahead dei pairs
|
||||
|
||||
```bash
|
||||
uv run python scripts/analysis/strategy_research.py # screening famiglie + deep-dive MR01
|
||||
uv run python scripts/analysis/oos_validation.py # perche' la famiglia squeeze e' scartata
|
||||
uv run python scripts/analysis/validate_worker_mr01.py # replay del worker live su MR01
|
||||
# Gestione rischio, combinazione, report
|
||||
uv run python scripts/analysis/risk_management.py # filtro trend + portafoglio fade
|
||||
uv run python scripts/analysis/combine_portfolio.py # combinare fade + honest
|
||||
uv run python scripts/analysis/combine_v2.py # master esteso con pairs + TSM01
|
||||
uv run python scripts/analysis/report_families.py # report per anno di tutte le famiglie
|
||||
|
||||
# Validazione dei worker live (replay == backtest)
|
||||
uv run python scripts/analysis/validate_worker_mr01.py # worker single-leg su MR01
|
||||
uv run python scripts/analysis/validate_worker_pairs.py # worker a 2 gambe sui pairs
|
||||
uv run python scripts/analysis/live_smoke_pairs.py # smoke test feed live reale dei pairs
|
||||
```
|
||||
|
||||
## Paper Trading Live
|
||||
|
||||
Il multi-strategy runner esegue N strategie in parallelo su dati live da Cerbero MCP, ognuna con €1000 USDC virtuali indipendenti. Se un `Signal` porta `tp`/`sl`/`max_bars` in `metadata` (come MR01), il worker chiude su take-profit alla media / stop-loss ad ATR / time-limit; altrimenti usa il fallback `hold_bars`/stop -2%.
|
||||
Il multi-strategy runner esegue N strategie in parallelo su dati live da Cerbero MCP,
|
||||
ognuna con €1000 USDC virtuali indipendenti. Gestisce due tipi di worker:
|
||||
|
||||
- **Single-leg** (`strategy_worker.py`): per le strategie direzionali. Se un `Signal`
|
||||
porta `tp`/`sl`/`max_bars` in `metadata` (come le fade), chiude su take-profit /
|
||||
stop-loss / time-limit; altrimenti usa il fallback `hold_bars`/stop -2%.
|
||||
- **Due gambe** (`pairs_worker.py`): per i pairs market-neutral. Apre long su una gamba
|
||||
e short sull'altra, esce sul rientro dello z-score o per time-limit, conta le fee su
|
||||
entrambe le gambe. Validato: il replay storico coincide *esattamente* col backtest.
|
||||
|
||||
### Avvio
|
||||
|
||||
@@ -141,19 +212,24 @@ defaults:
|
||||
position_size: 0.15
|
||||
leverage: 3
|
||||
|
||||
strategies:
|
||||
strategies: # strategie single-leg
|
||||
- name: MR01_bollinger_fade
|
||||
asset: BTC
|
||||
tf: 1h
|
||||
enabled: true
|
||||
params:
|
||||
bb_window: 50
|
||||
k: 2.5
|
||||
sl_atr: 2.0
|
||||
max_bars: 24
|
||||
params: { bb_window: 50, k: 2.5, sl_atr: 2.0, max_bars: 24, trend_max: 3.0, ema_long: 200 }
|
||||
|
||||
pairs: # strategie a 2 gambe (market-neutral)
|
||||
- name: PR01_pairs_reversion
|
||||
a: ETH
|
||||
b: BTC
|
||||
tf: 1h
|
||||
enabled: true
|
||||
params: { n: 50, z_in: 2.0, z_exit: 0.75, max_bars: 72, jump_max: 0.08 }
|
||||
```
|
||||
|
||||
Per aggiungere una strategia: nuova riga in `strategies.yml`, poi `docker compose restart`. Lo storico delle strategie esistenti rimane intatto.
|
||||
Per aggiungere una strategia: nuova riga in `strategies.yml` (sezione `strategies` o
|
||||
`pairs`), poi `docker compose restart`. Lo storico delle strategie esistenti rimane intatto.
|
||||
|
||||
### Persistenza
|
||||
|
||||
@@ -194,12 +270,41 @@ uv run python -m src.live.multi_runner
|
||||
|
||||
## Dati
|
||||
|
||||
| Asset | Timeframe | Candele | Copertura |
|
||||
|-------|-----------|---------|-----------|
|
||||
| BTC | 5m / 15m / 1h | 883K / 294K / 74K | 2018-01 → oggi |
|
||||
| ETH | 5m / 15m / 1h | 882K / 294K / 74K | 2018-01 → oggi |
|
||||
| Asset | Timeframe | Copertura |
|
||||
|-------|-----------|-----------|
|
||||
| BTC, ETH | 5m / 15m / 1h | 2018-01 → oggi |
|
||||
| SOL, LTC, ADA, XRP, BNB, DOGE | 15m / 1h | 2019-2022 → oggi (variabile per asset) |
|
||||
|
||||
Fonte primaria: Deribit perpetual via Cerbero MCP. Fallback: Binance spot via ccxt. Formato: Apache Parquet.
|
||||
Fonte primaria: perpetual Deribit via Cerbero MCP. Fallback: Binance spot via ccxt.
|
||||
Formato: Apache Parquet (in `data/raw/`, gitignored).
|
||||
|
||||
> **Nota sul naming Deribit (per il feed live).** I major sono perpetui *inverse*
|
||||
> (`BTC-PERPETUAL`, `ETH-PERPETUAL`); gli altcoin sono perpetui *lineari USDC*
|
||||
> (`SOL_USDC-PERPETUAL`, `LTC_USDC-PERPETUAL`, …) con storia dal 2022. Attenzione:
|
||||
> `LTC-PERPETUAL`/`ADA-PERPETUAL` non esistono e `SOL-PERPETUAL` restituisce dati
|
||||
> errati — per gli altcoin usare sempre la forma `_USDC-PERPETUAL`.
|
||||
|
||||
### Discovery & validazione strumenti
|
||||
|
||||
`src/data/instruments.py` scopre e **valida** gli strumenti disponibili sugli
|
||||
exchange implementati — **Deribit** e **Hyperliquid** (esclusi Alpaca/stocks e
|
||||
**Bybit**, feed testnet inaffidabile). Ogni perpetuo viene testato sui dati
|
||||
storici realmente raccoglibili: esistenza, congruenza OHLC, contratto non-morto,
|
||||
liquidità e **congruenza prezzo cross-exchange** (mediana per base-coin, tolleranza
|
||||
5%) — così feed farlocchi e contratti sbagliati (es. `SOL-PERPETUAL`=9.6) vengono
|
||||
scartati. Il risultato è `data/instruments_registry.json` (strumenti validi +
|
||||
timeframe + data d'inizio).
|
||||
|
||||
**Solo gli strumenti validati possono essere scaricati**: il downloader ha un gate
|
||||
(`_download_cerbero_range`) che rifiuta quelli non nel registry. Rigenera con:
|
||||
|
||||
```bash
|
||||
uv run python -m src.data.instruments
|
||||
```
|
||||
|
||||
Simboli Deribit: BTC/ETH = `<COIN>-PERPETUAL` (inverse); altcoin =
|
||||
`<COIN>_USDC-PERPETUAL` (lineari USDC). Registry attuale (testnet): Deribit 18/106
|
||||
validi (major liquidi, BTC dal 2018), Hyperliquid 66/74.
|
||||
|
||||
## Riferimenti
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+12
-1
@@ -69,8 +69,19 @@ def _fetch_binance(symbol: str, tf: str, since_ms: int, limit: int = 1000) -> li
|
||||
|
||||
|
||||
def _download_cerbero_range(
|
||||
instrument: str, resolution: str, tf: str, start_date: str, end_date: str
|
||||
instrument: str, resolution: str, tf: str, start_date: str, end_date: str,
|
||||
allow_unvalidated: bool = False,
|
||||
) -> pd.DataFrame:
|
||||
# Gate: si raccolgono dati SOLO per strumenti validati nel registry.
|
||||
# Esegui `python -m src.data.instruments` per (ri)costruirlo.
|
||||
if not allow_unvalidated:
|
||||
from src.data.instruments import is_validated
|
||||
if not is_validated(instrument, tf, "deribit"):
|
||||
raise ValueError(
|
||||
f"Strumento non validato: {instrument} @ {tf}. "
|
||||
f"Costruisci il registry (python -m src.data.instruments) o passa "
|
||||
f"allow_unvalidated=True per forzare."
|
||||
)
|
||||
all_candles: list[dict] = []
|
||||
max_days = MAX_DAYS_PER_REQUEST[tf]
|
||||
current = datetime.fromisoformat(start_date)
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
"""Discovery + validazione strumenti per gli exchange implementati (via Cerbero MCP).
|
||||
|
||||
Per ogni exchange (Deribit, Hyperliquid — esclusi Alpaca/stocks e Bybit, il cui
|
||||
feed testnet e' farlocco) enumera i perpetui, ne verifica i dati e produce un
|
||||
registry di strumenti VALIDATI.
|
||||
Solo gli strumenti nel registry possono essere usati per la raccolta dati
|
||||
(vedi gate in src/data/downloader.py).
|
||||
|
||||
Controlli di validazione (uno strumento e' valido solo se TUTTI passano):
|
||||
- exists : la storia daily ritorna candele
|
||||
- ohlc_sane : high>=low, high>=max(o,c), low<=min(o,c), prezzi>0
|
||||
- not_flat : non e' un contratto morto (quasi tutte le barre O==H==L==C)
|
||||
- liquid : volume_24h>0 dal ticker
|
||||
- congruent : il prezzo concorda (entro tolleranza) con la MEDIANA dello
|
||||
stesso base-coin su tutti gli exchange. Scarta i feed testnet
|
||||
farlocchi (es. Bybit BTC=300k) e i contratti sbagliati
|
||||
(es. Deribit SOL-PERPETUAL=9.6 vs SOL reale ~82).
|
||||
|
||||
NB: il token Cerbero punta a TESTNET; la congruenza cross-exchange e' il filtro
|
||||
che distingue i feed realistici (Deribit, Hyperliquid) da quelli farlocchi.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import statistics
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from src.live.cerbero_client import CerberoClient
|
||||
|
||||
REGISTRY_PATH = Path(__file__).resolve().parents[2] / "data" / "instruments_registry.json"
|
||||
|
||||
# I nostri timestep -> codice risoluzione per ciascun exchange
|
||||
TF_CODES = {
|
||||
"deribit": {"1m": "1", "5m": "5", "15m": "15", "1h": "60", "1d": "1D"},
|
||||
"hyperliquid": {"1m": "1m", "5m": "5m", "15m": "15m", "1h": "1h", "1d": "1d"},
|
||||
}
|
||||
CONGRUENCE_TOL = 0.05 # 5% di scostamento dalla mediana del base-coin
|
||||
|
||||
|
||||
def _today() -> str:
|
||||
return datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
||||
|
||||
|
||||
@dataclass
|
||||
class Quote:
|
||||
base: str
|
||||
symbol: str
|
||||
last: float | None = None
|
||||
volume_24h: float | None = None
|
||||
open_interest: float | None = None
|
||||
|
||||
|
||||
# --------------------------- adapters ---------------------------
|
||||
class ExchangeAdapter:
|
||||
name = "base"
|
||||
|
||||
def __init__(self, client: CerberoClient):
|
||||
self.c = client
|
||||
|
||||
def _post(self, tool: str, payload: dict) -> dict:
|
||||
return self.c._post(f"/mcp-{self.name}/tools/{tool}", payload)
|
||||
|
||||
def list_symbols(self) -> list[Quote]:
|
||||
"""Lista perpetui (economica). I prezzi possono essere None (vedi ticker)."""
|
||||
raise NotImplementedError
|
||||
|
||||
def ticker(self, q: Quote) -> None:
|
||||
"""Riempie last/volume/OI sul Quote (per-simbolo). No-op se gia' pieni."""
|
||||
|
||||
def candles(self, symbol: str, tf: str, start: str, end: str) -> pd.DataFrame:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class DeribitAdapter(ExchangeAdapter):
|
||||
name = "deribit"
|
||||
|
||||
def list_symbols(self) -> list[Quote]:
|
||||
perps, offset = [], 0
|
||||
while True:
|
||||
r = self._post("get_instruments", {"currency": "any", "kind": "future",
|
||||
"offset": offset, "limit": 100})
|
||||
insts = r.get("instruments", [])
|
||||
perps += [i["name"] for i in insts if i.get("name", "").endswith("-PERPETUAL")]
|
||||
if not r.get("has_more") or not insts:
|
||||
break
|
||||
offset += len(insts)
|
||||
if offset > 2000:
|
||||
break
|
||||
out = []
|
||||
for name in perps:
|
||||
base = name.split("-PERPETUAL")[0].replace("_USDC", "").replace("_USD", "")
|
||||
out.append(Quote(base, name))
|
||||
return out
|
||||
|
||||
def ticker(self, q: Quote) -> None:
|
||||
t = self._post("get_ticker", {"instrument": q.symbol})
|
||||
q.last, q.volume_24h, q.open_interest = t.get("last_price"), t.get("volume_24h"), t.get("open_interest")
|
||||
|
||||
def candles(self, symbol, tf, start, end) -> pd.DataFrame:
|
||||
r = self._post("get_historical", {"instrument": symbol, "start_date": start,
|
||||
"end_date": end, "resolution": TF_CODES["deribit"][tf]})
|
||||
return _to_df(r.get("candles", []))
|
||||
|
||||
|
||||
class HyperliquidAdapter(ExchangeAdapter):
|
||||
name = "hyperliquid"
|
||||
|
||||
def list_symbols(self) -> list[Quote]:
|
||||
r = self._post("get_markets", {})
|
||||
markets = r if isinstance(r, list) else r.get("markets", [])
|
||||
return [Quote(m["asset"], m["asset"], m.get("mark_price"),
|
||||
m.get("volume_24h"), m.get("open_interest")) for m in markets]
|
||||
|
||||
# prezzi gia' presenti da get_markets -> ticker no-op
|
||||
|
||||
def candles(self, symbol, tf, start, end) -> pd.DataFrame:
|
||||
r = self._post("get_historical", {"asset": symbol, "start_date": start, "end_date": end,
|
||||
"resolution": TF_CODES["hyperliquid"][tf], "limit": 5000})
|
||||
return _to_df(r.get("candles", []))
|
||||
|
||||
|
||||
ADAPTERS = {"deribit": DeribitAdapter, "hyperliquid": HyperliquidAdapter}
|
||||
|
||||
|
||||
def _to_df(candles: list[dict]) -> pd.DataFrame:
|
||||
if not candles:
|
||||
return pd.DataFrame()
|
||||
df = pd.DataFrame(candles)
|
||||
df["timestamp"] = df["timestamp"].astype("int64")
|
||||
return df.sort_values("timestamp").reset_index(drop=True)
|
||||
|
||||
|
||||
# --------------------------- validazione ---------------------------
|
||||
def _ohlc_sane(df: pd.DataFrame) -> bool:
|
||||
if df.empty:
|
||||
return False
|
||||
o, h, l, c = df["open"], df["high"], df["low"], df["close"]
|
||||
ok = (h >= l) & (h >= o) & (h >= c) & (l <= o) & (l <= c) & (c > 0) & (l > 0)
|
||||
return bool(ok.mean() > 0.99)
|
||||
|
||||
|
||||
def _not_flat(df: pd.DataFrame) -> bool:
|
||||
if df.empty:
|
||||
return False
|
||||
flat = (df["open"] == df["high"]) & (df["high"] == df["low"]) & (df["low"] == df["close"])
|
||||
return bool(flat.mean() < 0.90)
|
||||
|
||||
|
||||
def build_registry(exchanges: list[str] | None = None,
|
||||
tf_check: tuple[str, ...] = ("1m", "5m", "15m", "1h"),
|
||||
start_scan_from: str = "2017-01-01",
|
||||
save: bool = True) -> dict:
|
||||
exchanges = exchanges or ["deribit", "hyperliquid"] # NO alpaca, NO bybit (testnet farlocco)
|
||||
client = CerberoClient()
|
||||
adapters = {ex: ADAPTERS[ex](client) for ex in exchanges}
|
||||
|
||||
# 1) lista economica per ogni exchange
|
||||
listed: dict[str, list[Quote]] = {}
|
||||
for ex, ad in adapters.items():
|
||||
try:
|
||||
listed[ex] = ad.list_symbols()
|
||||
print(f" [{ex}] {len(listed[ex])} strumenti elencati")
|
||||
except Exception as e:
|
||||
print(f" [{ex}] discovery FALLITA: {type(e).__name__}: {e}")
|
||||
listed[ex] = []
|
||||
|
||||
# 2) universo = base-coin presenti su Deribit (il nostro venue). Bybit/HL
|
||||
# vengono validati solo sull'overlap (cross-check), non su 500+ simboli.
|
||||
deribit_bases = {q.base for q in listed.get("deribit", [])}
|
||||
selected: dict[str, list[Quote]] = {}
|
||||
for ex, qs in listed.items():
|
||||
selected[ex] = qs if ex == "deribit" else [q for q in qs if q.base in deribit_bases]
|
||||
|
||||
# 3) timeframe disponibili per exchange (testati su BTC recente)
|
||||
ref = {"deribit": "BTC-PERPETUAL", "hyperliquid": "BTC"}
|
||||
tf_by_ex: dict[str, list[str]] = {}
|
||||
for ex, ad in adapters.items():
|
||||
oks = []
|
||||
for tf in tf_check:
|
||||
try:
|
||||
if not ad.candles(ref[ex], tf, _today(), _today()).empty:
|
||||
oks.append(tf)
|
||||
except Exception:
|
||||
pass
|
||||
tf_by_ex[ex] = oks
|
||||
print(f" [{ex}] timeframe ok: {oks}")
|
||||
|
||||
# 4) UNA fetch daily per strumento: e' il dato che davvero raccoglieremmo.
|
||||
# Da qui ricaviamo esistenza, OHLC, not-flat, start-date, prezzo-per-congruenza
|
||||
# (ultima close STORICA, non il ticker) e liquidita' (volume daily recente).
|
||||
scan: dict[tuple[str, str], dict] = {}
|
||||
for ex, ad in adapters.items():
|
||||
for q in selected[ex]:
|
||||
rec = {"reasons": [], "last_close": None, "start_date": None, "vol": 0.0}
|
||||
try:
|
||||
d = ad.candles(q.symbol, "1d", start_scan_from, _today())
|
||||
if d.empty:
|
||||
rec["reasons"].append("no_history")
|
||||
else:
|
||||
if not _ohlc_sane(d):
|
||||
rec["reasons"].append("ohlc_insane")
|
||||
if not _not_flat(d):
|
||||
rec["reasons"].append("flat_dead")
|
||||
rec["last_close"] = float(d["close"].iloc[-1])
|
||||
rec["vol"] = float(d["volume"].tail(7).mean())
|
||||
rec["start_date"] = str(pd.to_datetime(d["timestamp"].iloc[0], unit="ms", utc=True).date())
|
||||
except Exception as e:
|
||||
rec["reasons"].append(f"history_err:{type(e).__name__}")
|
||||
scan[(ex, q.symbol)] = rec
|
||||
|
||||
# 5) mediana per base-coin dall'ULTIMA CLOSE STORICA (riferimento congruenza)
|
||||
by_base: dict[str, list[float]] = {}
|
||||
for (ex, sym), rec in scan.items():
|
||||
base = next(q.base for q in selected[ex] if q.symbol == sym)
|
||||
if rec["last_close"] and rec["last_close"] > 0:
|
||||
by_base.setdefault(base, []).append(rec["last_close"])
|
||||
median_px = {b: statistics.median(v) for b, v in by_base.items()}
|
||||
|
||||
# 6) finalizza validazione
|
||||
registry: dict = {"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
"congruence_tol": CONGRUENCE_TOL, "testnet": True, "exchanges": {}}
|
||||
for ex, ad in adapters.items():
|
||||
registry["exchanges"][ex] = {"timeframes": tf_by_ex[ex], "instruments": {}}
|
||||
for q in selected[ex]:
|
||||
rec = scan[(ex, q.symbol)]
|
||||
reasons = list(rec["reasons"])
|
||||
px, med, n_src = rec["last_close"], median_px.get(q.base), len(by_base.get(q.base, []))
|
||||
if not (rec["vol"] and rec["vol"] > 0):
|
||||
reasons.append("no_volume")
|
||||
if px is None or px <= 0:
|
||||
if "no_history" not in reasons:
|
||||
reasons.append("no_price")
|
||||
elif med and n_src >= 2 and abs(px - med) / med > CONGRUENCE_TOL:
|
||||
reasons.append(f"incongruent(px={px:.4g},med={med:.4g})")
|
||||
valid = len(reasons) == 0
|
||||
registry["exchanges"][ex]["instruments"][q.symbol] = {
|
||||
"base": q.base, "valid": valid, "reasons": reasons,
|
||||
"last_price": px, "start_date": rec["start_date"],
|
||||
"timeframes": tf_by_ex[ex] if valid else [],
|
||||
}
|
||||
if save:
|
||||
REGISTRY_PATH.write_text(json.dumps(registry, indent=2))
|
||||
print(f" registry salvato in {REGISTRY_PATH}")
|
||||
return registry
|
||||
|
||||
|
||||
# --------------------------- gate per il downloader ---------------------------
|
||||
def load_registry() -> dict:
|
||||
return json.loads(REGISTRY_PATH.read_text()) if REGISTRY_PATH.exists() else {}
|
||||
|
||||
|
||||
def is_validated(symbol: str, tf: str, exchange: str = "deribit") -> bool:
|
||||
"""True solo se lo strumento e' nel registry come valido per quel timeframe."""
|
||||
inst = load_registry().get("exchanges", {}).get(exchange, {}).get("instruments", {}).get(symbol)
|
||||
return bool(inst and inst.get("valid") and tf in inst.get("timeframes", []))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
reg = build_registry()
|
||||
print("\n" + "=" * 96)
|
||||
print(" REGISTRY STRUMENTI VALIDATI")
|
||||
print("=" * 96)
|
||||
for ex, exd in reg["exchanges"].items():
|
||||
insts = exd["instruments"]
|
||||
valid = {s: i for s, i in insts.items() if i["valid"]}
|
||||
print(f"\n {ex.upper()} | tf={exd['timeframes']} | validi {len(valid)}/{len(insts)}")
|
||||
for s, i in sorted(valid.items(), key=lambda kv: kv[1]["base"]):
|
||||
print(f" {s:30s} {i['base']:10s} px={i['last_price']:<12.6g} dal {i['start_date']}")
|
||||
bad = {s: i for s, i in insts.items() if not i["valid"]}
|
||||
if bad:
|
||||
shown = list(bad.items())[:6]
|
||||
print(f" -- scartati {len(bad)} (primi {len(shown)}):")
|
||||
for s, i in shown:
|
||||
print(f" {s:30s} {','.join(i['reasons'])[:64]}")
|
||||
Reference in New Issue
Block a user