docs(V2): update README + add CLAUDE.md for /mcp interface and Bybit/Alpaca retirement
- README: three active exchanges (Deribit/Hyperliquid/IBKR); document the unified /mcp interface (get_instruments uniform schema + per-exchange get_historical); drop Bybit/Alpaca from endpoints, tool list, audit filters, migration table and URL-override notes; update test count (323) and source layout (routers/unified.py, old/). - CLAUDE.md: new project guide — commands (uv/pytest/ruff/mypy), runtime architecture, active exchanges, data interfaces, conventions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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 <TESTNET|MAINNET>_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/<ex>/{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 `<ex>/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.
|
||||||
@@ -9,20 +9,24 @@ sul token bearer fornito dal client.
|
|||||||
|
|
||||||
- **Una singola immagine Docker** (`cerbero-mcp`) ospita tutti i router
|
- **Una singola immagine Docker** (`cerbero-mcp`) ospita tutti i router
|
||||||
exchange in un unico processo FastAPI
|
exchange in un unico processo FastAPI
|
||||||
- **Cinque exchange** (Deribit, Bybit, Hyperliquid, Alpaca, IBKR) e **due
|
- **Tre exchange** (Deribit, Hyperliquid, IBKR) e **due data provider**
|
||||||
data provider** read-only (Macro, Sentiment)
|
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
|
- **Switch testnet/mainnet per-request** tramite header
|
||||||
`Authorization: Bearer <TOKEN>`: lo stesso container serve entrambi gli
|
`Authorization: Bearer <TOKEN>`: lo stesso container serve entrambi gli
|
||||||
ambienti senza riavvii
|
ambienti senza riavvii
|
||||||
- **Configurazione interamente in `.env`**: nessun file JSON di credenziali
|
- **Configurazione interamente in `.env`**: nessun file JSON di credenziali
|
||||||
separato; le URL upstream (live/testnet) di ciascun exchange sono
|
separato; le URL upstream (live/testnet) di ciascun exchange sono
|
||||||
override-abili tramite variabili dedicate (`DERIBIT_URL_*`,
|
override-abili tramite variabili dedicate (`DERIBIT_URL_*`,
|
||||||
`BYBIT_URL_*`, `HYPERLIQUID_URL_*`, `ALPACA_URL_*`)
|
`HYPERLIQUID_URL_*`, `IBKR_URL_*`)
|
||||||
- **Documentazione interattiva** OpenAPI/Swagger esposta a `/apidocs`
|
- **Documentazione interattiva** OpenAPI/Swagger esposta a `/apidocs`
|
||||||
- **Endpoint cross-exchange unificato** (`/mcp-cross/tools/get_historical`):
|
- **Endpoint cross-exchange a consenso** (`/mcp-cross/tools/get_historical`):
|
||||||
fan-out a tutti gli exchange che supportano (symbol, asset_class) e
|
fan-out agli exchange che supportano (symbol, asset_class) e consensus
|
||||||
consensus per-bar (mediana OHLC + `div_pct` + `sources`)
|
per-bar (mediana OHLC + `div_pct` + `sources`)
|
||||||
- **Qualità verificata**: 399 test (unit + integration + smoke), mypy
|
- **Qualità verificata**: 323 test (unit + integration + smoke), mypy
|
||||||
pulito, ruff pulito
|
pulito, ruff pulito
|
||||||
|
|
||||||
## Avvio rapido (sviluppo, senza Docker)
|
## 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 /health/ready` | Readiness check con ping client exchange (no auth) |
|
||||||
| `GET /apidocs` | Swagger UI (no auth) |
|
| `GET /apidocs` | Swagger UI (no auth) |
|
||||||
| `GET /openapi.json` | Schema OpenAPI 3.1 (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-deribit/tools/{tool}` | Tool exchange Deribit |
|
||||||
| `POST /mcp-bybit/tools/{tool}` | Tool exchange Bybit |
|
|
||||||
| `POST /mcp-hyperliquid/tools/{tool}` | Tool exchange Hyperliquid |
|
| `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-ibkr/tools/{tool}` | Tool exchange Interactive Brokers |
|
||||||
| `POST /mcp-macro/tools/{tool}` | Tool macro/market data |
|
| `POST /mcp-macro/tools/{tool}` | Tool macro/market data |
|
||||||
| `POST /mcp-sentiment/tools/{tool}` | Tool sentiment/news |
|
| `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`)
|
- `from`, `to`: ISO 8601 datetime (es. `2026-05-01` o `2026-05-01T12:34:56Z`)
|
||||||
- `actor`: `testnet` | `mainnet`
|
- `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`)
|
- `action`: nome del tool (es. `place_order`)
|
||||||
- `bot_tag`: identificatore del bot
|
- `bot_tag`: identificatore del bot
|
||||||
- `limit`: massimo record restituiti, default `1000`, massimo `10000`
|
- `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,
|
get_smile_asymmetry, get_atm_vs_wings_vol, get_orderbook_imbalance,
|
||||||
place_combo_order.
|
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
|
### Hyperliquid
|
||||||
Account summary, positions, orderbook, historical, indicators, funding
|
Account summary, positions, orderbook, historical, indicators, funding
|
||||||
rate, basis spot/perp, place_order, set_stop_loss, set_take_profit.
|
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)
|
### IBKR (Interactive Brokers)
|
||||||
Account, positions, activities, ticker, bars, snapshot, option chain,
|
Account, positions, activities, ticker, bars, snapshot, option chain,
|
||||||
search_contracts, clock, streaming (tick + depth via WebSocket
|
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,
|
OI history, get_funding_arb_spread, get_liquidation_heatmap,
|
||||||
get_cointegration_pairs.
|
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
|
`get_historical` aggrega le candele dello stesso simbolo da tutti gli
|
||||||
exchange che lo supportano e ritorna una serie consensus: la chiusura è
|
exchange che lo supportano e ritorna una serie consensus: la chiusura è
|
||||||
la mediana, `sources` è il numero di exchange che hanno contribuito al
|
la mediana, `sources` è il numero di exchange che hanno contribuito al
|
||||||
bar e `div_pct = (max-min)/median` segnala il disaccordo tra fonti — un
|
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 +
|
quality gate per i bot. Crypto: BTC/ETH via Hyperliquid + Deribit, SOL via
|
||||||
Deribit. Stocks: AAPL/SPY/QQQ/TSLA/NVDA via Alpaca. In caso di fallimento
|
Hyperliquid. In caso di fallimento parziale ritorna i dati disponibili più
|
||||||
parziale ritorna i dati disponibili più `failed_sources`; se *tutti* gli
|
`failed_sources`; se *tutti* gli upstream falliscono → HTTP 502 retryable.
|
||||||
upstream falliscono → HTTP 502 retryable.
|
|
||||||
|
|
||||||
## Deploy su VPS con Traefik
|
## Deploy su VPS con Traefik
|
||||||
|
|
||||||
@@ -302,7 +307,7 @@ PORT=9000 TESTNET_TOKEN="$TESTNET_TOKEN" bash tests/smoke/run.sh
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv sync
|
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/unit -v # solo unit
|
||||||
uv run pytest tests/integration -v
|
uv run pytest tests/integration -v
|
||||||
uv run ruff check src/ tests/
|
uv run ruff check src/ tests/
|
||||||
@@ -320,9 +325,11 @@ src/cerbero_mcp/
|
|||||||
├── auth.py # middleware bearer → request.state.environment
|
├── auth.py # middleware bearer → request.state.environment
|
||||||
├── server.py # build_app() + Swagger + middleware + handlers
|
├── server.py # build_app() + Swagger + middleware + handlers
|
||||||
├── client_registry.py # cache lazy {(exchange, env): client}
|
├── client_registry.py # cache lazy {(exchange, env): client}
|
||||||
├── routers/ # un file per exchange (deribit, bybit, ...)
|
├── routers/ # un file per exchange + unified.py (/mcp) + cross.py
|
||||||
├── exchanges/ # logica per-exchange: client + tools
|
├── exchanges/ # logica per-exchange: client + tools (+ cross/)
|
||||||
└── common/ # indicators, options, microstructure, stats, ...
|
└── common/ # indicators, options, microstructure, stats, ...
|
||||||
|
|
||||||
|
old/ # codice ritirato (bybit, alpaca) — fuori dal package
|
||||||
```
|
```
|
||||||
|
|
||||||
## Migrazione da V1 (1.x → 2.0.0)
|
## Migrazione da V1 (1.x → 2.0.0)
|
||||||
@@ -336,12 +343,13 @@ Per chi è in produzione su V1:
|
|||||||
| V1 (file JSON) | V2 (variabile `.env`) |
|
| V1 (file JSON) | V2 (variabile `.env`) |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `secrets/deribit.json` `client_id` / `client_secret` | `DERIBIT_CLIENT_ID` / `DERIBIT_CLIENT_SECRET` |
|
| `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/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/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` |
|
| `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:
|
4. Aggiornare i client bot:
|
||||||
- i path API restano identici (`/mcp-{exchange}/tools/{tool}`)
|
- i path API restano identici (`/mcp-{exchange}/tools/{tool}`)
|
||||||
- sostituire `core.token` / `observer.token` con `TESTNET_TOKEN` o
|
- sostituire `core.token` / `observer.token` con `TESTNET_TOKEN` o
|
||||||
@@ -378,10 +386,8 @@ Bot → Authorization: Bearer <TESTNET|MAINNET>_TOKEN
|
|||||||
### Override URL upstream
|
### Override URL upstream
|
||||||
|
|
||||||
L'override delle URL upstream da `.env` è completo per Deribit e
|
L'override delle URL upstream da `.env` è completo per Deribit e
|
||||||
Hyperliquid. Per Bybit funziona tramite l'attributo `endpoint` interno di
|
Hyperliquid (`DERIBIT_URL_*`, `HYPERLIQUID_URL_*`). IBKR usa
|
||||||
pybit (workaround documentato nel client). Per Alpaca l'override è
|
`IBKR_URL_*` / `IBKR_WS_URL_*`.
|
||||||
applicato al solo trading endpoint: gli endpoint dati
|
|
||||||
(`data.alpaca.markets`) restano quelli predefiniti dell'SDK.
|
|
||||||
|
|
||||||
## IBKR Setup
|
## IBKR Setup
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user