4 Commits

Author SHA1 Message Date
Adriano Dal Pastro 2b01443efe Merge branch 'main' of ssh://git.tielogic.xyz:222/Adriano/PythagorasGoal
# Conflicts:
#	CLAUDE.md
#	README.md
2026-05-29 08:06:19 +00:00
Adriano Dal Pastro 5ac9ebed5b docs: documenta discovery/validazione strumenti e gate del downloader
CLAUDE.md, README.md, API_REFERENCE.md aggiornati per il nuovo layer
src/data/instruments.py: validazione strumenti per exchange (Deribit +
Hyperliquid; esclusi Alpaca e Bybit testnet), congruenza prezzo cross-exchange,
registry come allowlist, gate nel downloader. Aggiunti schemi param
get_instruments/get_markets/get_historical per exchange e convenzione simboli
Deribit (inverse vs USDC lineari). Universo dati esteso con SOL/LTC/ADA 1h.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 07:51:47 +00:00
Adriano Dal Pastro 1561005d41 feat(data): discovery + validazione strumenti con gate nel downloader
src/data/instruments.py: enumera i perpetui per exchange (Deribit, Hyperliquid;
esclusi Alpaca/stocks e Bybit per feed testnet farlocco) e valida ogni strumento
sui DATI STORICI realmente raccoglibili:
  - esistenza, congruenza OHLC, not-flat (scarta contratti morti)
  - liquidita' (volume daily) e congruenza prezzo cross-exchange via mediana
    del base-coin (scarta outlier come Deribit SOL-PERPETUAL=9.6 vs SOL ~82)
Produce data/instruments_registry.json con strumenti validi, timeframe e start-date.

Gate: _download_cerbero_range rifiuta strumenti non validati (override esplicito
allow_unvalidated). La raccolta dati e' possibile solo per strumenti validati.

Registry attuale (testnet): Deribit 18/106 validi (BTC dal 2018, alt dal 2022),
Hyperliquid 66/74. I major liquidi (BTC,ETH,SOL,LTC,ADA,XRP,DOGE,AVAX,BNB,...) passano.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 07:49:57 +00:00
Adriano b9e5176a5b docs: aggiorna README e CLAUDE.md allo stato attuale (4 famiglie + pairs worker)
README riscritto: 4 famiglie (FADE/HONEST/PAIRS/TSMOM) con profili netti OOS,
portafoglio MASTER e numeri sobri anti-overfit, tabella strategie completa, comandi
analisi/validazione, struttura aggiornata (pairs_worker), dati 8 asset + nota naming
Deribit (alt = _USDC-PERPETUAL), paper trading a 2 gambe con sezione pairs in YAML.
CLAUDE.md: struttura/comandi/sezione paper trader aggiornati (niente piu' "solo MR01").

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 09:48:39 +02:00
6 changed files with 2682 additions and 52 deletions
+37
View File
@@ -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.
+41 -9
View File
@@ -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
+147 -42
View File
@@ -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 68 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 68 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:
```bash
uv run python scripts/analysis/strategy_research.py # screening famiglie + deep-dive MR01
# 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/validate_worker_mr01.py # replay del worker live su MR01
uv run python scripts/analysis/pairs_research.py # ricerca + verifica no-look-ahead dei pairs
# 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
View File
@@ -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)
+278
View File
@@ -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]}")