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>
This commit is contained in:
@@ -0,0 +1,282 @@
|
||||
# 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`.
|
||||
Reference in New Issue
Block a user