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