diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..97a581a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,91 @@ +# CLAUDE.md + +Guida per Claude Code che lavora su **Cerbero MCP** — server MCP multi-exchange +(FastAPI) distribuito come singola immagine Docker, con routing testnet/mainnet +per-request basato sul token bearer. + +## Comandi + +Gestione progetto con `uv` (Python ≥ 3.11): + +```bash +uv sync # installa dipendenze (incl. dev) +uv run cerbero-mcp # avvia il server (porta da .env, default 9000) +uv run pytest # suite completa (~323 test attesi) +uv run pytest tests/unit -v # solo unit +uv run pytest tests/unit/exchanges/cross/test_unified.py::test_get_historical_single_exchange # singolo test +uv run ruff check src tests # lint (E,F,I,W,UP,B,SIM; line-length 100) +uv run mypy src/cerbero_mcp # type check (non-strict) +``` + +Tutti e quattro (pytest, ruff, mypy) devono essere verdi prima di committare. +`testpaths = ["tests"]` → `old/` non viene mai raccolto dai test. + +## Architettura + +Flusso runtime: + +``` +Bot → Authorization: Bearer _TOKEN (+ header X-Bot-Tag) + → auth middleware → request.state.environment ∈ {testnet, mainnet} + → Router (/mcp/... , /mcp-{exchange}/... , /mcp-cross/...) + → ClientRegistry.get(exchange, env) → client cached lazy per (exchange, env) + → tool function (logica pura) → exchange API +``` + +File chiave: +- `src/cerbero_mcp/__main__.py` — `_make_app()` registra tutti i router. +- `src/cerbero_mcp/server.py` — `build_app()`, Swagger, exception handler, + e `_TimestampInjectorMiddleware` che inietta `data_timestamp` (ISO UTC) nel + top level di **ogni** risposta JSON sotto `/tools/*` (+ header `X-Data-Timestamp`). +- `src/cerbero_mcp/auth.py` — bearer → env, e `X-Bot-Tag` obbligatorio (path + whitelisted: `/health`, `/health/ready`, `/apidocs`, `/openapi.json`). +- `src/cerbero_mcp/client_registry.py` — cache `{(exchange, env): client}`. +- `src/cerbero_mcp/exchanges/__init__.py` — `build_client()`: factory per exchange. +- `src/cerbero_mcp/settings.py` — Pydantic Settings da `.env`. +- `src/cerbero_mcp/routers/` — un file per exchange + `unified.py` (`/mcp`) + + `cross.py` (`/mcp-cross`). +- `src/cerbero_mcp/exchanges//{client.py,tools.py}` — client HTTP + schemi + Pydantic / wrapper tool. `cross/` contiene aggregatore consensus, interfaccia + unificata, normalizzatori e symbol-map. + +## Exchange attivi + +Trading: **deribit, hyperliquid, ibkr**. Data-provider read-only: **macro, +sentiment**. **Bybit e Alpaca sono ritirati** dalla superficie API in V2: +codice archiviato in `old/` (fuori dal package, non importato, non testato). +Nota: nel modulo `sentiment` "bybit" resta come **fonte dati pubblica** +(funding/OI), non come client di trading. + +## Interfacce dati + +- **`/mcp` (comune, raccomandata)** — `get_instruments` con schema uniforme + (ogni elemento porta `exchange`, `fees`, `history_start`, `type`, `tick_size`, + `native`); `get_historical` con `exchange` selezionabile (singolo venue). + Supporta deribit + hyperliquid (IBKR non ancora integrato). +- **`/mcp-cross`** — `get_historical` a consenso multi-exchange (mediana OHLC, + `sources`, `div_pct`). +- **`/mcp-{exchange}`** — router per-exchange legacy, ancora attivi. + +Convenzione: ogni `get_historical` ritorna la chiave uniforme +`candles: [{timestamp(ms), open, high, low, close, volume}]` (validatore in +`common/candles.py`). `fees`/`history_start` sono popolati **live dove +l'upstream li espone** (oggi: Deribit), altrimenti `null`. + +## Convenzioni + +- Schemi richiesta = modelli Pydantic in `/tools.py`; il router fa solo + dependency-injection di env + client e chiama il wrapper del tool. +- Errori applicativi: sollevare `fastapi.HTTPException`; gli handler in + `server.py` li avvolgono in un envelope con `request_id`. +- Firme/crittografia: il signing L1 Hyperliquid usa `eth_utils.to_hex` come + l'SDK ufficiale → `r`/`s` hex possono essere < 66 char (byte alto a zero, + ~1/256); non assumere lunghezza fissa. +- Branch di produzione: **`V2.0.0`** (non `main`). Deploy via + `scripts/deploy-vps.sh`. + +## Documentazione + +- `README.md` — overview, setup, deploy, migrazione V1→V2, IBKR OAuth. +- `docs/API_REFERENCE.md` — catalogo completo endpoint/tool. Aggiornarlo + quando si aggiungono/rimuovono tool o si cambiano gli schemi. diff --git a/README.md b/README.md index 4e836ce..743d609 100644 --- a/README.md +++ b/README.md @@ -9,20 +9,24 @@ sul token bearer fornito dal client. - **Una singola immagine Docker** (`cerbero-mcp`) ospita tutti i router exchange in un unico processo FastAPI -- **Cinque exchange** (Deribit, Bybit, Hyperliquid, Alpaca, IBKR) e **due - data provider** read-only (Macro, Sentiment) +- **Tre exchange** (Deribit, Hyperliquid, IBKR) e **due data provider** + read-only (Macro, Sentiment). Bybit e Alpaca sono stati ritirati dalla + superficie API in V2 (codice archiviato in `old/`) +- **Interfaccia comune `/mcp`** trasversale agli exchange: `get_instruments` + con schema uniforme (ogni elemento porta `exchange`, `fees`, + `history_start`) e `get_historical` per-exchange selezionabile - **Switch testnet/mainnet per-request** tramite header `Authorization: Bearer `: lo stesso container serve entrambi gli ambienti senza riavvii - **Configurazione interamente in `.env`**: nessun file JSON di credenziali separato; le URL upstream (live/testnet) di ciascun exchange sono override-abili tramite variabili dedicate (`DERIBIT_URL_*`, - `BYBIT_URL_*`, `HYPERLIQUID_URL_*`, `ALPACA_URL_*`) + `HYPERLIQUID_URL_*`, `IBKR_URL_*`) - **Documentazione interattiva** OpenAPI/Swagger esposta a `/apidocs` -- **Endpoint cross-exchange unificato** (`/mcp-cross/tools/get_historical`): - fan-out a tutti gli exchange che supportano (symbol, asset_class) e - consensus per-bar (mediana OHLC + `div_pct` + `sources`) -- **Qualità verificata**: 399 test (unit + integration + smoke), mypy +- **Endpoint cross-exchange a consenso** (`/mcp-cross/tools/get_historical`): + fan-out agli exchange che supportano (symbol, asset_class) e consensus + per-bar (mediana OHLC + `div_pct` + `sources`) +- **Qualità verificata**: 323 test (unit + integration + smoke), mypy pulito, ruff pulito ## Avvio rapido (sviluppo, senza Docker) @@ -87,10 +91,10 @@ non è richiesto sugli endpoint pubblici (`/health`, `/apidocs`, | `GET /health/ready` | Readiness check con ping client exchange (no auth) | | `GET /apidocs` | Swagger UI (no auth) | | `GET /openapi.json` | Schema OpenAPI 3.1 (no auth) | +| `POST /mcp/tools/get_instruments` | Lista strumenti uniforme cross-exchange (deribit, hyperliquid) | +| `POST /mcp/tools/get_historical` | Storico di un singolo exchange selezionabile | | `POST /mcp-deribit/tools/{tool}` | Tool exchange Deribit | -| `POST /mcp-bybit/tools/{tool}` | Tool exchange Bybit | | `POST /mcp-hyperliquid/tools/{tool}` | Tool exchange Hyperliquid | -| `POST /mcp-alpaca/tools/{tool}` | Tool exchange Alpaca | | `POST /mcp-ibkr/tools/{tool}` | Tool exchange Interactive Brokers | | `POST /mcp-macro/tools/{tool}` | Tool macro/market data | | `POST /mcp-sentiment/tools/{tool}` | Tool sentiment/news | @@ -145,7 +149,7 @@ Parametri di query (tutti opzionali): - `from`, `to`: ISO 8601 datetime (es. `2026-05-01` o `2026-05-01T12:34:56Z`) - `actor`: `testnet` | `mainnet` -- `exchange`: nome dell'exchange (`deribit`, `bybit`, `hyperliquid`, `alpaca`, `ibkr`) +- `exchange`: nome dell'exchange (`deribit`, `hyperliquid`, `ibkr`) - `action`: nome del tool (es. `place_order`) - `bot_tag`: identificatore del bot - `limit`: massimo record restituiti, default `1000`, massimo `10000` @@ -178,19 +182,10 @@ get_dealer_gamma_profile, get_vanna_charm, get_oi_weighted_skew, get_smile_asymmetry, get_atm_vs_wings_vol, get_orderbook_imbalance, place_combo_order. -### Bybit -Ticker, orderbook, OHLCV, funding rate, open interest, basis spot/perp, -indicatori tecnici, place_batch_order, get_orderbook_imbalance, -get_basis_term_structure. - ### Hyperliquid Account summary, positions, orderbook, historical, indicators, funding rate, basis spot/perp, place_order, set_stop_loss, set_take_profit. -### Alpaca -Account, positions, bars, snapshot, option chain, place_order, -amend_order, cancel_order, close_position. - ### IBKR (Interactive Brokers) Account, positions, activities, ticker, bars, snapshot, option chain, search_contracts, clock, streaming (tick + depth via WebSocket @@ -208,15 +203,25 @@ News (CryptoPanic/CoinDesk), social (LunarCrush), funding multi-exchange, OI history, get_funding_arb_spread, get_liquidation_heatmap, get_cointegration_pairs. -### Cross (storico unificato) +### Unified (`/mcp`) — interfaccia comune +`get_instruments` ritorna una lista strumenti **uniforme**: ogni elemento +porta in sé `exchange`, `fees` (maker/taker, popolati live da Deribit, `null` +dove l'exchange non li espone per-strumento), `history_start` (data inizio +storico, live da Deribit), oltre a `type`, `tick_size` e un blocco `native` +lossless. Con `exchange` opzionale filtra una singola sorgente; se omesso fa +fan-out su tutti gli exchange integrati. `get_historical` è generalizzato a +`{exchange, instrument, interval, start_date, end_date}` e ritorna le candele +del **singolo exchange scelto**. Exchange supportati: `deribit`, `hyperliquid` +(IBKR non ancora integrato — usa `/mcp-ibkr`). + +### Cross (storico a consenso) `get_historical` aggrega le candele dello stesso simbolo da tutti gli exchange che lo supportano e ritorna una serie consensus: la chiusura è la mediana, `sources` è il numero di exchange che hanno contribuito al bar e `div_pct = (max-min)/median` segnala il disaccordo tra fonti — un -quality gate per i bot. Crypto: BTC/ETH/SOL via Bybit + Hyperliquid + -Deribit. Stocks: AAPL/SPY/QQQ/TSLA/NVDA via Alpaca. In caso di fallimento -parziale ritorna i dati disponibili più `failed_sources`; se *tutti* gli -upstream falliscono → HTTP 502 retryable. +quality gate per i bot. Crypto: BTC/ETH via Hyperliquid + Deribit, SOL via +Hyperliquid. In caso di fallimento parziale ritorna i dati disponibili più +`failed_sources`; se *tutti* gli upstream falliscono → HTTP 502 retryable. ## Deploy su VPS con Traefik @@ -302,7 +307,7 @@ PORT=9000 TESTNET_TOKEN="$TESTNET_TOKEN" bash tests/smoke/run.sh ```bash uv sync -uv run pytest # tutta la suite (399 test attesi) +uv run pytest # tutta la suite (323 test attesi) uv run pytest tests/unit -v # solo unit uv run pytest tests/integration -v uv run ruff check src/ tests/ @@ -320,9 +325,11 @@ src/cerbero_mcp/ ├── auth.py # middleware bearer → request.state.environment ├── server.py # build_app() + Swagger + middleware + handlers ├── client_registry.py # cache lazy {(exchange, env): client} -├── routers/ # un file per exchange (deribit, bybit, ...) -├── exchanges/ # logica per-exchange: client + tools +├── routers/ # un file per exchange + unified.py (/mcp) + cross.py +├── exchanges/ # logica per-exchange: client + tools (+ cross/) └── common/ # indicators, options, microstructure, stats, ... + +old/ # codice ritirato (bybit, alpaca) — fuori dal package ``` ## Migrazione da V1 (1.x → 2.0.0) @@ -336,12 +343,13 @@ Per chi è in produzione su V1: | V1 (file JSON) | V2 (variabile `.env`) | |---|---| | `secrets/deribit.json` `client_id` / `client_secret` | `DERIBIT_CLIENT_ID` / `DERIBIT_CLIENT_SECRET` | - | `secrets/bybit.json` `api_key` / `api_secret` | `BYBIT_API_KEY` / `BYBIT_API_SECRET` | | `secrets/hyperliquid.json` `wallet_address` / `private_key` | `HYPERLIQUID_WALLET_ADDRESS` / `HYPERLIQUID_PRIVATE_KEY` | - | `secrets/alpaca.json` `api_key_id` / `secret_key` | `ALPACA_API_KEY_ID` / `ALPACA_SECRET_KEY` | | `secrets/macro.json` `fred_api_key` / `finnhub_api_key` | `FRED_API_KEY` / `FINNHUB_API_KEY` | | `secrets/sentiment.json` `cryptopanic_key` / `lunarcrush_key` | `CRYPTOPANIC_KEY` / `LUNARCRUSH_KEY` | + > Bybit e Alpaca non sono più esposti in V2 (vedi `old/`); le rispettive + > variabili `BYBIT_*` / `ALPACA_*` non sono più lette. + 4. Aggiornare i client bot: - i path API restano identici (`/mcp-{exchange}/tools/{tool}`) - sostituire `core.token` / `observer.token` con `TESTNET_TOKEN` o @@ -378,10 +386,8 @@ Bot → Authorization: Bearer _TOKEN ### Override URL upstream L'override delle URL upstream da `.env` è completo per Deribit e -Hyperliquid. Per Bybit funziona tramite l'attributo `endpoint` interno di -pybit (workaround documentato nel client). Per Alpaca l'override è -applicato al solo trading endpoint: gli endpoint dati -(`data.alpaca.markets`) restano quelli predefiniti dell'SDK. +Hyperliquid (`DERIBIT_URL_*`, `HYPERLIQUID_URL_*`). IBKR usa +`IBKR_URL_*` / `IBKR_WS_URL_*`. ## IBKR Setup