# 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`.