Files
Cerbero-Bite/docs/12-mcp-deribit-changes.md
T
Adriano 466e63dc19 Phase 3: MCP HTTP clients + Dockerization
Wrapper async tipizzati sui sei servizi MCP HTTP che Cerbero Bite
consuma in autonomia. 277 test pass, copertura clients 93%, mypy
strict pulito, ruff clean.

Base layer:
- clients/_base.py: HttpToolClient con httpx + tenacity (retry
  esponenziale 3x, timeout 8s, mapping HTTP→eccezioni tipizzate).
- clients/_exceptions.py: McpAuthError, McpServerError, McpToolError,
  McpDataAnomalyError, McpNotFoundError, McpTimeoutError.
- config/mcp_endpoints.py: risoluzione URL via Docker DNS
  (mcp-deribit:9011, ...) con override per servizio via env var;
  caricamento bearer token da secrets/core.token o
  CERBERO_BITE_CORE_TOKEN_FILE.

Wrapper:
- clients/macro.py: next_high_severity_within() per filtro entry §2.5.
- clients/sentiment.py: funding_cross_median_annualized() con
  annualizzazione per period nativo per exchange (Binance/Bybit/OKX
  1095, Hyperliquid 8760).
- clients/hyperliquid.py: funding_rate_annualized() per filtro §2.6.
- clients/portfolio.py: total_equity_eur(), asset_pct_of_portfolio()
  per sizing engine + filtro §2.7.
- clients/telegram.py: notify-only (no callback queue, no
  conferme — Bite auto-execute).
- clients/deribit.py: environment_info, index_price_eth,
  latest_dvol, options_chain, get_tickers, orderbook_depth_top3,
  get_account_summary, get_positions, place_combo_order (combo
  atomico), cancel_order.

CLI:
- cerbero-bite ping: health-check parallelo di tutti gli MCP con
  tabella rich (OK/FAIL/SKIPPED).

Docker:
- Dockerfile multi-stage Python 3.13 + uv, user non-root.
- docker-compose.yml con rete external "cerbero-suite", secret
  core_token montato a /run/secrets/core_token, env per ogni MCP.
- secrets/README.md documenta il setup del token.

Documentazione di intervento:
- docs/12-mcp-deribit-changes.md: spec delle modifiche apportate
  al server mcp-deribit (place_combo_order + override testnet via
  DERIBIT_TESTNET).

Dipendenze:
- aggiunto pytest-httpx per i test HTTP.
- rimosso mcp>=1.0 (non usiamo l'SDK MCP, parliamo via HTTP REST).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 23:36:30 +02:00

283 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 12 — Modifiche da apportare a `mcp-deribit`
## Premessa
Cerbero Bite, a partire dalla Fase 3, opera in modo completamente
autonomo: nessuna conferma interattiva da parte di Adriano nel primo
periodo, niente bottoni Telegram, niente coda di approvazione manuale.
Quando il rule engine valuta che le condizioni di entrata e di uscita
sono soddisfatte, l'engine deve poter inviare l'ordine senza
intermediazione umana. Il primo periodo di esercizio sarà condotto su
ambiente **testnet** Deribit, in modo da raccogliere dati di slippage e
fill reali senza esporre capitale.
Per realizzare questo flusso sono necessarie due modifiche al servizio
`mcp-deribit` ospitato in `CerberoSuite/Cerbero/services/mcp-deribit`.
Entrambe le modifiche sono additive e non rompono i tool esistenti.
Lo stato corrente del servizio è il seguente:
* `place_order` esiste, ma è limitato a un singolo strumento per
invocazione (`PlaceOrderReq.instrument_name`); non è atomico per uno
spread a due gambe.
* `client.py` espone l'attributo `testnet`, letto dal file di
credenziali (`secrets/deribit.json`, campo `testnet`). Il tool
`is_testnet` riporta correttamente lo stato. Non esiste oggi un modo
di sovrascrivere quella scelta a livello di environment del
container.
## Modifica 1 — Nuovo tool `place_combo_order`
### Obiettivo
Inviare a Deribit un singolo ordine combo a due o più gambe
(legs) in modo atomico, evitando il rischio di leg risk tipico
delle credit spread (la prima gamba si riempie e la seconda no, oppure
si riempie a un prezzo che invalida il rapporto credito/larghezza).
### Comportamento attesto
L'endpoint riceve la lista di legs, costruisce o riusa lo strumento
combo corrispondente sul lato Deribit (API `public/create_combo`),
quindi invia l'ordine di acquisto netto sul combo (`private/buy` con
tipo `limit` o `market`). Il prezzo limite è espresso come prezzo
netto del combo (in ETH per le opzioni inverse Deribit), cioè la somma
algebrica firmata dei mid-price delle gambe.
L'ordine è soggetto agli stessi guard rail di `place_order`:
verifica `core` capability, `enforce_single_notional`,
`enforce_aggregate`, `enforce_leverage`. Per le opzioni il notional è
calcolato come `width × n_contracts × spot_index` (perdita massima del
combo).
### Schema della richiesta
```python
class PlaceComboOrderReq(BaseModel):
legs: list[ComboLegReq] # 2..4 gambe
side: Literal["buy", "sell"] # "sell" = incassa credito
amount: float # numero di contratti combo
type: Literal["limit", "market"] = "limit"
price: float | None = None # prezzo netto in ETH; richiesto se type=limit
label: str | None = None # propagato a Deribit per riconciliazione
time_in_force: Literal["good_til_cancelled", "good_til_day", "fill_or_kill", "immediate_or_cancel"] = "good_til_cancelled"
post_only: bool = False
reduce_only: bool = False # vero per combo di chiusura
class ComboLegReq(BaseModel):
instrument_name: str # ETH-15MAY26-2475-P
direction: Literal["buy", "sell"] # direzione della singola gamba
ratio: int = 1 # multiplo della size combo (Deribit usa direction/ratio)
```
Vincoli di validazione:
* `len(legs)` compreso fra 2 e 4 (esclusi naked e ratio sbilanciati).
* Tutte le gambe hanno la stessa scadenza (`expiry`). Il payload deve
rifiutare combo a scadenze miste.
* Se `type == "limit"`, `price` è obbligatorio.
* `direction` di ciascuna gamba e `side` del combo sono coerenti con
la convenzione Deribit (vedi
[docs Deribit `public/create_combo`](https://docs.deribit.com/#public-create_combo)).
### Schema della risposta
```python
class PlaceComboOrderResp(BaseModel):
combo_id: str # ETH-15MAY26-2475P_2350P
order_id: str # ID dell'ordine sul combo
state: str # open, filled, rejected, ecc.
average_price: float | None # in ETH
filled_amount: float
legs: list[ComboLegFill] # per gamba: instrument, direction, fill price, fees
raw: dict # response Deribit completa per audit
```
### Endpoint
```python
@app.post("/tools/place_combo_order", tags=["writes"])
async def t_place_combo_order(
body: PlaceComboOrderReq,
principal: Principal = Depends(require_principal),
):
_check(principal, core=True)
return await client.place_combo_order(...)
```
### Estensioni del client (`mcp_deribit/client.py`)
Aggiungere un metodo `place_combo_order` che incapsula due chiamate
Deribit:
1. `public/create_combo` con la lista delle gambe; ritorna
`combo_id` (Deribit gestisce idempotenza per pair identico, perciò
chiamate ripetute con gli stessi parametri restituiscono lo stesso
id).
2. `private/buy` o `private/sell` (a seconda di `side`) sul combo
appena creato, con `amount`, `price`, `type`, `time_in_force`,
`post_only`, `reduce_only`, `label`.
In aggiunta: `cancel_combo_order(order_id)` che oggi può semplicemente
delegare a `cancel_order` (i combo sono cancellabili come ordini
normali); è opportuno tracciarlo come metodo distinto per rendere il
log dell'audit più leggibile.
### Tag MCP
Aggiungere alla lista `tools` di `mount_mcp_endpoint` la voce
`place_combo_order` con descrizione *"Invia un combo order
multi-leg atomico (Bull Put, Bear Call, Iron Condor)"*.
### Test
* Mock del client che simula `create_combo` + `buy` e verifica che il
body propagato a Deribit rispetti la convenzione direction/ratio.
* Test 403 quando il principal non ha capability `core`.
* Test di rifiuto per legs a scadenze miste, `len(legs) < 2`,
`len(legs) > 4`, `price` mancante con `type=limit`.
## Modifica 2 — Override `testnet` via environment variable
### Obiettivo
Permettere di forzare l'ambiente di esecuzione (testnet o mainnet)
senza dover riscrivere `secrets/deribit.json`. Questa flessibilità è
essenziale per Cerbero Bite, che gira in un container Docker dedicato
e deve poter passare da paper trading a soft launch modificando solo
una variabile d'ambiente.
### Comportamento attesto
In `__main__.py`, dopo la lettura del file di credenziali, applicare
la seguente precedenza di risoluzione:
1. Se la variabile d'ambiente `DERIBIT_TESTNET` è valorizzata, la sua
conversione booleana (`true|false`, `1|0`, case-insensitive)
sovrascrive il campo `testnet` del file di credenziali.
2. Altrimenti vale il campo `testnet` del JSON.
3. Se nessuno dei due è presente, default `True` (precauzione: meglio
testnet che mainnet per errore).
### Variazione al main
```python
def _resolve_testnet(creds: dict) -> bool:
env = os.environ.get("DERIBIT_TESTNET")
if env is not None:
return env.strip().lower() in {"1", "true", "yes", "on"}
return bool(creds.get("testnet", True))
client = DeribitClient(
client_id=creds["client_id"],
client_secret=creds["client_secret"],
testnet=_resolve_testnet(creds),
)
```
### Estensione del tool `is_testnet`
Il tool oggi ritorna `{"testnet": bool, "base_url": str}`. Aggiungere:
* `source`: `"env"` o `"credentials"` o `"default"` per indicare
l'origine della scelta.
* `env_value`: valore grezzo letto da `DERIBIT_TESTNET` (utile per
diagnosticare typo nel docker-compose).
In questo modo Cerbero Bite, all'avvio, può chiamare `is_testnet`,
loggare il risultato e, se in disaccordo con la propria configurazione
attesa (`strategy.yaml` o equivalente), armare il kill switch prima
che parta qualsiasi ciclo di entry.
### Documentazione e compose
* In `docker-compose.yml`, aggiungere `environment: DERIBIT_TESTNET:
"true"` al servizio `mcp-deribit` con commento *"override secrets/
deribit.json testnet flag"*.
* In `services/mcp-deribit/README.md` (se esiste, altrimenti creare),
documentare la precedenza e le tre forme accettate di valore
booleano.
### Test
* Test che monta sia il file di credenziali (con `testnet:false`) sia
l'env var (`DERIBIT_TESTNET=true`) e verifica che `is_testnet`
riporti `True` con `source="env"`.
* Test del default a `True` quando entrambe le sorgenti mancano (file
con campo assente, env non settata).
## Impatto su Cerbero Bite
Le due modifiche abilitano i seguenti flussi nella Fase 3 di Cerbero
Bite:
* Il wrapper `clients/deribit.py` espone un metodo
`place_combo_order(short, long, side, n_contracts, limit_price)` che
inoltra direttamente al nuovo tool. Niente più sequenza di due
`place_order`.
* All'avvio, l'orchestrator (Fase 4) chiama
`cerbero-deribit.is_testnet` e:
* blocca il boot se l'ambiente non corrisponde a quanto previsto in
`strategy.yaml` (campo nuovo proposto: `execution.environment:
"testnet" | "mainnet"`);
* scrive l'esito in `system_state.config_version` o in un campo
dedicato `system_state.environment` per renderlo visibile alla
GUI.
* Il documento di flusso operativo (`docs/06-operational-flow.md`)
viene aggiornato per rimuovere la fase di conferma utente e
introdurre l'auto-execute condizionato alle regole già codificate
in `core/`.
## Modifiche correlate ai documenti di Cerbero Bite
Una volta integrate le modifiche al server `mcp-deribit`, occorrerà
allineare i seguenti documenti del progetto Cerbero Bite (sono
modifiche editoriali, nessuna implicazione architetturale ulteriore):
* `docs/04-mcp-integration.md`: rimuovere le sezioni
`cerbero-memory` e `cerbero-brain-bridge`; aggiungere la voce
`place_combo_order` nella tabella di `cerbero-deribit`; aggiornare
la matrice di degradation in modo che `cerbero-deribit` resti
l'unico hard-fail di esecuzione.
* `docs/06-operational-flow.md`: sostituire il flusso di conferma
Telegram con un flusso di sola notifica e auto-execute.
* `docs/02-architecture.md`: rimuovere il blocco
`cerbero-memory ⇆ Cerbero core` dal diagramma.
* `docs/05-data-model.md`: la tabella `instructions` diventa il log
degli ordini Deribit (combo `combo_id`, `order_id`, fill price,
fees) anziché il tracking di `push_user_instruction`.
* `docs/07-risk-controls.md`: kill switch trigger per mismatch
testnet/mainnet, da aggiungere alla matrice.
* `strategy.yaml`: aggiungere il blocco
`execution.environment: "testnet"` con `last_review` aggiornato.
## Out of scope di questo documento
Non sono oggetto di modifica al server `mcp-deribit`:
* La gestione di ordini combo a più di quattro gambe (i prodotti
Cerbero Bite restano due o quattro gambe per Bull Put, Bear Call,
Iron Condor).
* L'aggiunta di logica di repricing (incremento di un tick verso
ask combinato): è responsabilità di Cerbero Bite via
`cancel_combo_order` + `place_combo_order` con prezzo aggiornato.
* L'integrazione con `cerbero-memory` o `cerbero-brain-bridge`: nessun
collegamento, neanche indiretto.
## Sequenza di lavoro consigliata
1. Aprire un branch `feat/place-combo-order` su `CerberoSuite/Cerbero`.
2. Implementare il metodo `client.place_combo_order` con relativi
test unitari sul client (mock di `create_combo` + `buy`).
3. Esporre il tool `place_combo_order` in `server.py` con i guard rail
e i test FastAPI.
4. Aprire un branch separato `feat/deribit-testnet-env` per la modifica
2 (più piccola, indipendente dal resto).
5. Aggiornare `docker-compose.yml` con la variabile d'ambiente.
6. Una volta in main, su Cerbero Bite implementare il wrapper
`clients/deribit.py` con i metodi `place_combo_order` e
`cancel_combo_order`, scrivere i fake corrispondenti in
`tests/fixtures/fakes/deribit.py` e i test integration in
`tests/integration/test_deribit_combo.py`.