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:
+102
-187
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user