Phase 4 hardening: dealer-gamma + liquidation-heatmap entry filters

Integra due nuovi filtri dal pacchetto quant indicators rilasciato in
Cerbero_mcp (commit a13e3fe). 335 test pass, mypy strict pulito,
ruff clean.

Filtri (§2.8 — nuovo):
- dealer-gamma: blocca entry quando total_net_dealer_gamma <
  dealer_gamma_min (default 0). Long-gamma regime favorisce credit
  spread (vol-suppressing dealer flow); short-gamma flow lo amplifica
  ed è da evitare.
- liquidation-heatmap: blocca entry quando il segnale euristico di
  cerbero-sentiment riporta long o short squeeze risk = "high"
  (cluster di liquidations imminenti entro 24h).

Entrambi sono best-effort: se il tool MCP fallisce o restituisce
dati anomali l'entry_cycle popola EntryContext con None e
validate_entry salta il gate per non bloccare entry su problemi
infrastrutturali.

Wrapper:
- DeribitClient.dealer_gamma_profile_eth → DealerGammaSnapshot.
- SentimentClient.liquidation_heatmap → LiquidationHeatmap con
  property has_high_squeeze_risk.

Schema:
- EntryConfig.dealer_gamma_min, dealer_gamma_filter_enabled,
  liquidation_filter_enabled.
- EntryContext.dealer_net_gamma, liquidation_squeeze_risk_high
  opzionali.
- strategy.yaml: nuovi campi documentati con commento + hash
  ricalcolato (4c2be4c5...).

Documentazione:
- docs/04-mcp-integration.md riscritto al modello attuale (HTTP
  REST, no mcp SDK, no memory/brain-bridge, place_combo_order
  documentato, environment_info al boot).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-28 07:26:33 +02:00
parent b5b96f959c
commit f4faef6fd1
11 changed files with 489 additions and 190 deletions
+102 -187
View File
@@ -1,243 +1,158 @@
# 04 — MCP Integration
Tutti i server MCP sono già configurati a livello di `CerberoSuite` (vedi
`Cerbero_Office/.mcp.json`). Cerbero Bite vi si connette come **client
MCP** usando l'SDK ufficiale `mcp` per Python.
Cerbero Bite consuma sei servizi MCP HTTP della suite (`Cerbero_mcp`).
Non utilizza l'SDK Python `mcp`: ogni server espone gli endpoint REST
`POST <base_url>/tools/<tool_name>` con autenticazione Bearer, e Cerbero
Bite vi si collega tramite `httpx.AsyncClient` long-lived
(`clients/_base.py`).
## Configurazione di connessione
Cerbero Bite legge `~/.config/cerbero-suite/mcp.json` (o, in dev, da
`.mcp.json` locale puntato via env var `CERBERO_BITE_MCP_CONFIG`). I
server vengono risolti **per nome** dichiarato nel file di config.
Le URL sono risolte da `cerbero_bite.config.mcp_endpoints.load_endpoints`,
con default che corrispondono al DNS della rete Docker
`cerbero-suite` (`http://mcp-deribit:9011`, `http://mcp-macro:9013`,
ecc.). Ogni servizio può essere sovrascritto da una variabile
d'ambiente dedicata, utile in sviluppo:
| Servizio | Variabile d'ambiente | Default Docker DNS |
|---|---|---|
| Deribit | `CERBERO_BITE_MCP_DERIBIT_URL` | `http://mcp-deribit:9011` |
| Hyperliquid | `CERBERO_BITE_MCP_HYPERLIQUID_URL` | `http://mcp-hyperliquid:9012` |
| Macro | `CERBERO_BITE_MCP_MACRO_URL` | `http://mcp-macro:9013` |
| Sentiment | `CERBERO_BITE_MCP_SENTIMENT_URL` | `http://mcp-sentiment:9014` |
| Telegram | `CERBERO_BITE_MCP_TELEGRAM_URL` | `http://mcp-telegram:9017` |
| Portfolio | `CERBERO_BITE_MCP_PORTFOLIO_URL` | `http://mcp-portfolio:9018` |
Il bearer token per le chiamate è il token con capability `core` letto
da `secrets/core.token` (path configurabile via
`CERBERO_BITE_CORE_TOKEN_FILE`, default `/run/secrets/core_token` nel
container). Non è loggato.
```python
# clients/_base.py — abstract
class McpClient:
name: str # "cerbero-deribit", ecc.
# clients/_base.py — sintesi
class HttpToolClient:
service: str # "deribit", "macro", ...
base_url: str # "http://mcp-deribit:9011"
token: str # bearer
timeout_s: float = 8.0
retry_max: int = 3
retry_base_delay: float = 1.0 # esponenziale
retry_max: int = 3 # esponenziale 1s/5s/30s
client: httpx.AsyncClient | None # condiviso dal RuntimeContext
async def call(self, tool: str, **params) -> dict: ...
async def call(self, tool: str, body: dict | None = None) -> Any: ...
```
Ogni wrapper concreto eredita `McpClient` ed espone metodi tipizzati.
La logica di retry e timeout è centralizzata.
Ogni wrapper concreto compone un `HttpToolClient` e ritorna i record
Pydantic consumati direttamente dagli algoritmi `core/`.
## Server MCP usati
### `cerbero-deribit`
Tool consumati:
Sorgente di tutti i dati di mercato sulle opzioni e canale di
esecuzione: Cerbero Bite invia gli ordini combo direttamente al broker
attraverso questo MCP, senza intermediazioni.
| Tool | Uso | Frequenza |
|---|---|---|
| `get_index_price(asset="ETH")` | Spot ETH per calcolo strike | Ogni ciclo entry + monitor |
| `get_dvol()` | Volatilità implicita aggregata ETH | Ogni ciclo entry + monitor |
| `get_options_chain(asset, expiry_window)` | Lista strumenti per dato DTE | Solo entry |
| `get_instrument(instrument_name)` | Mid, bid, ask, greche su singolo strumento | Entry + monitor |
| `get_orderbook(instrument_name, depth=5)` | Profondità per liquidity_gate e slippage | Solo entry |
| `get_combo_mark(legs)` | Mark price del combo (debito di chiusura) | Solo monitor |
| `get_account_summary(currency="USDC")` | Equity Deribit, margin libero | Periodico |
| `environment_info` | Verifica al boot: testnet/mainnet, base_url, max_leverage | Boot + ogni ciclo health |
| `get_ticker(instrument_name)` | Spot proxy via `ETH-PERPETUAL.mark_price`, mid/bid/ask + greche per le leg | Ogni ciclo entry + monitor |
| `get_ticker_batch(instrument_names)` | Quotes in batch per la chain candidata (max 20) | Solo entry |
| `get_dvol(currency="ETH", start_date, end_date)` | Latest DVOL per filtro §2.3 e bias §3.1 | Ogni ciclo entry + monitor |
| `get_instruments(currency, kind="option", expiry_from, expiry_to, min_open_interest)` | Lista strike per il DTE window | Solo entry |
| `get_orderbook(instrument_name, depth=3)` | `book_depth_top3` per liquidity gate | Solo entry |
| `get_historical(instrument, start_date, end_date, resolution)` | Spot 30g fa per bias direzionale + bootstrap return_4h | Entry + monitor (fallback) |
| `get_technical_indicators(instrument, indicators=["adx"], ...)` | ADX(14) per il filtro Iron Condor §3.1 | Solo entry |
| `get_account_summary(currency="USDC")` | Equity Deribit, margin libero (informativo) | Boot + monitor |
| `get_positions(currency="USDC")` | Riconciliazione stato dopo crash | Boot |
| `place_combo_order(legs, side, amount, type, price, label)` | **Esecuzione**: combo atomico via `private/create_combo` + `private/buy/sell` sul combo creato | Entry + monitor (close) |
| `cancel_order(order_id)` | Repricing e annullamenti | Solo monitor |
Wrapper:
Note operative:
```python
# clients/deribit.py
class DeribitClient(McpClient):
name = "cerbero-deribit"
async def index_price(self, asset: str) -> Decimal: ...
async def dvol(self) -> Decimal: ...
async def options_chain(self, asset: str, dte_min: int, dte_max: int) -> list[InstrumentSnapshot]: ...
async def instrument(self, name: str) -> InstrumentSnapshot: ...
async def orderbook(self, name: str, depth: int = 5) -> OrderbookSnapshot: ...
async def combo_mark(self, legs: list[OptionLeg]) -> Decimal: ...
async def account_summary(self) -> AccountSummary: ...
```
**Note:**
- Tutti i prezzi sono ricevuti come float dal MCP, convertiti in
`Decimal` con `quantize` a 6 cifre nel wrapper.
- Greche convertite con la stessa quantizzazione.
- Se `mark_iv = 7%` o `300%` o `bid = 0` su orderbook ATM su tutti gli
strumenti → wrapper solleva `DeribitDataAnomalyError` (probabile
testnet o feed rotto). Il decision orchestrator cattura, alert,
skippa il ciclo.
- Tutti i prezzi e le greche sono restituiti come `float` dal server e
convertiti in `Decimal` ad alta precisione nel wrapper, mai usati
come `float` nel motore decisionale.
- Se la chain risponde con `mark_iv` palesemente fuori range
(es. 7% o 300%) o tutti i `bid == 0` la chiamata viene segnalata come
`McpDataAnomalyError`; l'orchestrator emette un alert e salta il
ciclo.
- L'invio di `place_combo_order` è atomico: la creazione del combo e
l'ordine eseguito sul combo viaggiano in sequenza ma all'interno di
un'unica chiamata MCP, senza esposizione a leg risk.
### `cerbero-hyperliquid`
| Tool | Uso |
|---|---|
| `get_perp_funding_rate(asset="ETH")` | Filtro entry §2.6 |
| `get_perp_summary(asset="ETH")` | Volume 24h, conferma liquidità correlata |
| `get_account_summary()` | Solo per coerenza, non usato in decision loop |
```python
class HyperliquidClient(McpClient):
async def funding_rate_annualized(self, asset: str) -> Decimal: ...
async def perp_summary(self, asset: str) -> PerpSummary: ...
```
| `get_funding_rate(instrument="ETH")` | Funding rate ETH-PERP (annualizzato × 8760) per il filtro entry §2.6 |
### `cerbero-sentiment`
| Tool | Uso |
|---|---|
| `get_funding_cross_exchange(asset="ETH")` | Bias direzionale §3.1 (mediana 4 maggiori) |
| `get_cross_exchange_funding(assets=["ETH"])` | Mediana funding annualizzato (Binance/Bybit/OKX 1095, Hyperliquid 8760) per bias direzionale §3.1 |
Le news qualitative **non sono usate** nel decision loop (no LLM).
Vengono eventualmente lette da Adriano in occasione del report
settimanale.
```python
class SentimentClient(McpClient):
async def funding_cross_median(self, asset: str) -> Decimal: ...
```
Le news qualitative non vengono consumate dal decision loop:
Cerbero Bite è deterministico e non interpreta testi liberi.
### `cerbero-macro`
| Tool | Uso |
|---|---|
| `get_calendar(days_ahead=18)` | Filtro eventi macro pre-entry |
Eventi rilevanti (filtra per `severity = high` e `country in {US, EU}`):
FOMC, FED minutes, CPI, NFP, ECB, GDP, Powell speech, Lagarde speech.
```python
class MacroClient(McpClient):
async def upcoming_events(self, days_ahead: int) -> list[MacroEvent]: ...
async def first_high_severity_within(self, days: int) -> int | None:
"""Days until first high-severity event, None if none in window."""
```
| `get_macro_calendar(days, country_filter, importance_min)` | Filtro entry §2.5: zero eventi `high` in `country_filter` (default `["US","EU"]`) entro la finestra DTE |
### `cerbero-portfolio`
| Tool | Uso |
|---|---|
| `get_holdings()` | Capitale corrente complessivo |
| `get_holdings_by_asset()` | Filtro entry §2.7 (ETH < 30% portfolio) |
| `get_correlation()` | Sanity check, non bloccante |
```python
class PortfolioClient(McpClient):
async def total_equity_usd(self) -> Decimal: ...
async def asset_pct(self, asset: str) -> Decimal: ...
```
### `cerbero-memory`
| Tool | Uso |
|---|---|
| `push_user_instruction(payload, source="cerbero-bite")` | Invio istruzione apertura/chiusura a Cerbero core |
| `get_pending(source="cerbero-bite")` | Verifica ack di Cerbero core |
```python
class MemoryClient(McpClient):
async def push_instruction(self, instruction: CerberoInstruction) -> str:
"""Returns instruction_id."""
async def is_acknowledged(self, instruction_id: str) -> bool: ...
```
**Payload `CerberoInstruction`** (schema condiviso con Cerbero core,
documentato in `Cerbero/prompt.base v4`):
```json
{
"source": "cerbero-bite",
"kind": "open_combo" | "close_combo",
"exchange": "deribit",
"asset": "ETH",
"proposal_id": "uuid-...",
"legs": [
{"instrument": "ETH-13MAY26-1900-P", "side": "SELL", "size": 2,
"limit_price_eth": "0.0048"},
{"instrument": "ETH-13MAY26-1810-P", "side": "BUY", "size": 2,
"limit_price_eth": "0.0021"}
],
"limit_combo_eth": "0.0027",
"tif": "GTC",
"expires_at": "2026-04-27T16:00:00Z",
"max_slippage_eth": "0.0005",
"reason": "weekly_open" | "profit_take" | "stop_loss" | "vol_stop" |
"time_stop" | "delta_breach" | "adverse_move",
"milestone": "advisory_only" | "approved_by_user"
}
```
Cerbero core deduplica per `proposal_id` (idempotenza in caso di retry).
| `get_total_portfolio_value(currency="EUR")` | Capitale di base per il sizing engine, dopo conversione in USD |
| `get_holdings()` | Aggregazione manuale di `current_value_eur` per i ticker che contengono `"ETH"`, usata dal filtro §2.7 (`eth_holdings_pct_max`) |
### `cerbero-telegram`
| Tool | Uso |
|---|---|
| `send_message(text, parse_mode="MarkdownV2")` | Report pre/post trade, alert |
| `send_with_buttons(text, buttons)` | Conferma ad Adriano (yes/no) |
Le conferme devono ritornare entro 60 minuti (entry) o 30 minuti (exit).
Implementazione: l'engine si mette in `await` su una coda interna
alimentata dal callback Telegram via webhook locale.
```python
class TelegramClient(McpClient):
async def send(self, text: str, parse_mode: str = "MarkdownV2") -> int: ...
async def request_confirmation(self, text: str, timeout_s: int) -> bool: ...
```
### `cerbero-brain-bridge`
Cerbero Bite usa Telegram in modalità **notify-only**: nessuna conferma
manuale, nessun callback. L'engine apre e chiude le posizioni
automaticamente quando le regole sono soddisfatte; Telegram viene
informato post-fact.
| Tool | Uso |
|---|---|
| `kb_search(query)` | Lookup pre-trade su pattern simili (consultivo) |
| `kb_read(path)` | Lettura nota wiki specifica |
| `kb_write(path, content)` | Salvataggio learning post-trade |
**Importante:** il brain-bridge non partecipa al decision loop. Le
chiamate `kb_search` sono **consultive** e i risultati allegati al
report di Adriano per contesto, mai consumati come input ai filtri
deterministici.
```python
class BrainBridgeClient(McpClient):
async def search(self, query: str, limit: int = 5) -> list[KbHit]: ...
async def write_note(self, path: str, content: str) -> None: ...
```
### `cerbero-scheduler`
**Non usato** dal decision loop. Cerbero Bite ha il proprio scheduler
APScheduler interno. Il MCP scheduler resta a disposizione del core
Cerbero per altre routine.
| `notify(message, priority, tag)` | Alert MEDIUM o messaggi informativi |
| `notify_position_opened(instrument, side, size, strategy, greeks, expected_pnl)` | Notifica di entry placed |
| `notify_position_closed(instrument, realized_pnl, reason)` | Notifica di exit filled |
| `notify_alert(source, message, priority)` | Alert HIGH (kill switch) |
| `notify_system_error(message, component, priority)` | Alert CRITICAL |
## Errori e degradation
| Server down | Comportamento |
| Server fuori uso | Comportamento |
|---|---|
| `cerbero-deribit` | Skip ciclo entry; per monitor → alert e marca posizione come `unknown_state` (non chiude alla cieca) |
| `cerbero-hyperliquid` | Skip filtro funding §2.6 con warning; entry può proseguire se altre condizioni soddisfatte |
| `cerbero-sentiment` | Bias §3.1 cade in `no_entry` per default (no funding cross → niente direzione) |
| `cerbero-macro` | **Hard fail**: senza calendar non si apre. È un filtro irrinunciabile |
| `cerbero-portfolio` | Skip filtro §2.7 con warning; sizing usa ultimo capitale noto da SQLite con warning |
| `cerbero-memory` | Hard fail per esecuzione: senza push_user_instruction non si può aprire/chiudere |
| `cerbero-telegram` | Skip ciclo: senza canale di conferma niente proposta |
| `cerbero-brain-bridge` | Skip lookup, log warning. Mai bloccante |
| `cerbero-deribit` | **Hard fail**: senza dati di mercato e canale di esecuzione il ciclo viene saltato; in monitor le posizioni esistenti restano nello stato corrente, alert HIGH e kill switch |
| `cerbero-hyperliquid` | Skip del filtro funding §2.6 con warning; il ciclo prosegue se le altre condizioni sono soddisfatte |
| `cerbero-sentiment` | Bias §3.1 cade su `no_entry` per default (senza funding cross il bias non può fissare la direzione) |
| `cerbero-macro` | Hard fail per il filtro §2.5; senza calendar non si apre |
| `cerbero-portfolio` | Skip dei filtri §2.7 con warning; il sizing usa l'ultimo capitale noto da SQLite |
| `cerbero-telegram` | Skip notifiche post-fact; il ciclo decisionale non viene bloccato (l'engine non aspetta risposte) |
Ogni "hard fail" → alert sonoro su Telegram via canale di backup
(BotPapà), kill switch armato fino al ripristino.
I trigger HIGH e CRITICAL armano il kill switch e propagano un alert
in audit chain.
## Verifica ambiente al boot
All'avvio l'orchestrator (`runtime/orchestrator.boot`) chiama
`cerbero-deribit.environment_info` e confronta il campo `environment`
con `strategy.execution.environment`. Un `mismatch` (per esempio engine
configurato per `testnet` ma server agganciato a `mainnet`) produce un
alert CRITICAL e arma il kill switch prima che qualsiasi ciclo
trading parta. La stessa verifica viene ripetuta dal probe periodico
(ogni 5 minuti) di `runtime/health_check.HealthCheck`.
## Versioning
Cerbero Bite verifica all'avvio la versione di ciascun MCP via
`get_version()` (tool standard). Schema di versioning attesa:
```python
EXPECTED_MCP_VERSIONS = {
"cerbero-deribit": "^2.0.0",
"cerbero-hyperliquid": "^1.5.0",
"cerbero-memory": "^4.0.0",
"cerbero-portfolio": "^1.2.0",
...
}
```
Mismatch → kill switch e alert manuale. Mai partire con MCP a versione
incompatibile.
I server MCP non espongono attualmente un endpoint `get_version()`
formale; il check di compatibilità si limita a `environment_info` per
Deribit e a un round-trip lightweight sui tool read-only degli altri
servizi nel job di health check. Quando i server pubblicheranno il
versionamento esplicito, l'orchestrator confronterà al boot le
versioni con la tabella `EXPECTED_MCP_VERSIONS` e armerà il kill
switch su mismatch.