feat: 15 nuovi indicatori quant (common + deribit + bybit + macro + sentiment)

Common (mcp_common):
- indicators.py: vol_cone, hurst_exponent, half_life_mean_reversion,
  garch11_forecast, autocorrelation, rolling_sharpe, var_cvar
- options.py (nuovo): oi_weighted_skew, smile_asymmetry, atm_vs_wings_vol,
  dealer_gamma_profile, vanna_charm_aggregate
- microstructure.py (nuovo): orderbook_imbalance (ratio + microprice + slope)
- stats.py (nuovo): cointegration_test Engle-Granger + ADF helper

Deribit (+6 tool MCP):
- get_dealer_gamma_profile (net dealer gamma + flip level)
- get_vanna_charm (vanna/charm aggregati pesati OI)
- get_oi_weighted_skew, get_smile_asymmetry, get_atm_vs_wings_vol
- get_orderbook_imbalance

Bybit (+2 tool MCP):
- get_orderbook_imbalance, get_basis_term_structure (futures dated curve)

Macro (+2 tool MCP):
- get_yield_curve_slope (2y10y/5y30y + butterfly + regime)
- get_breakeven_inflation (FRED T5YIE/T10YIE/T5YIFR)

Sentiment (+3 tool MCP):
- get_funding_arb_spread (opportunità arb compatte annualizzate)
- get_liquidation_heatmap (heuristic da OI delta + funding extreme,
  no feed paid Coinglass)
- get_cointegration_pairs (Engle-Granger su coppie crypto Binance hourly)

Tutto in TDD pure-Python (no numpy/scipy in mcp_common). README
aggiornato con elenco completo. 442 test totali verdi.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
AdrianoDev
2026-04-27 23:58:07 +02:00
parent 867180f4bf
commit a13e3fe045
21 changed files with 1922 additions and 1 deletions
@@ -6,6 +6,8 @@ from typing import Any
import httpx
from mcp_common import indicators as ind
from mcp_common import microstructure as micro
from mcp_common import options as opt
BASE_LIVE = "https://www.deribit.com/api/v2"
BASE_TESTNET = "https://test.deribit.com/api/v2"
@@ -262,6 +264,18 @@ class DeribitClient:
"data_timestamp": _dt.datetime.now(_dt.UTC).isoformat(),
}
async def get_orderbook_imbalance(self, instrument_name: str, depth: int = 10) -> dict:
"""Microstructure: bid/ask imbalance + microprice + slope su top-N livelli."""
ob = await self.get_orderbook(instrument_name, depth=max(depth, 10))
result = micro.orderbook_imbalance(ob.get("bids") or [], ob.get("asks") or [], depth=depth)
return {
"instrument_name": instrument_name,
"depth": depth,
**result,
"timestamp": ob.get("timestamp"),
"testnet": self.testnet,
}
async def get_positions(self, currency: str = "USDC") -> list:
raw = await self._request("private/get_positions", {"currency": currency})
result = raw.get("result") or []
@@ -525,6 +539,159 @@ class DeribitClient:
"testnet": self.testnet,
}
async def _fetch_chain_legs(
self,
currency: str,
expiry_from: str | None = None,
expiry_to: str | None = None,
top_n_strikes: int = 50,
) -> tuple[float, list[dict[str, Any]]]:
"""Fetch chain options + ticker per top-N strikes per OI; restituisce
(spot, legs[]) con campi normalizzati per le funzioni in mcp_common.options.
"""
import asyncio
currency = currency.upper()
try:
idx_tk = await self.get_ticker(f"{currency}-PERPETUAL")
spot = float(idx_tk.get("mark_price") or 0)
except Exception:
spot = 0.0
chain = await self.get_instruments(
currency=currency,
kind="option",
expiry_from=expiry_from,
expiry_to=expiry_to,
limit=2000,
)
items = chain.get("instruments", [])
items.sort(key=lambda x: -(x.get("open_interest") or 0))
top = items[:top_n_strikes]
async def _ticker(name: str) -> dict:
try:
return await self.get_ticker(name)
except Exception:
return {}
tickers = await asyncio.gather(*[_ticker(i["name"]) for i in top])
legs: list[dict[str, Any]] = []
for meta, tk in zip(top, tickers, strict=True):
greeks = tk.get("greeks") or {}
legs.append({
"strike": meta.get("strike"),
"option_type": meta.get("option_type"),
"oi": meta.get("open_interest") or 0,
"iv": tk.get("mark_iv"),
"delta": greeks.get("delta"),
"gamma": greeks.get("gamma"),
"vanna": greeks.get("vanna"),
"charm": greeks.get("charm"),
"vega": greeks.get("vega"),
})
return spot, legs
async def get_dealer_gamma_profile(
self,
currency: str,
expiry_from: str | None = None,
expiry_to: str | None = None,
top_n_strikes: int = 50,
) -> dict:
"""Net dealer gamma per strike (assume dealer short calls/long puts).
Identifica il gamma flip level: sopra → mercato pinning, sotto → squeeze.
"""
import datetime as _dt
spot, legs = await self._fetch_chain_legs(currency, expiry_from, expiry_to, top_n_strikes)
result = opt.dealer_gamma_profile(legs, spot)
return {
"currency": currency.upper(),
"spot_price": spot,
**result,
"strikes_analyzed": len(result["by_strike"]),
"data_timestamp": _dt.datetime.now(_dt.UTC).isoformat(),
"testnet": self.testnet,
}
async def get_vanna_charm(
self,
currency: str,
expiry_from: str | None = None,
expiry_to: str | None = None,
top_n_strikes: int = 50,
) -> dict:
"""Vanna (∂delta/∂IV) e Charm (∂delta/∂t) aggregati pesati per OI.
Vanna positiva: dealer compra spot quando IV sale.
Charm negativa: time decay erode delta hedging.
"""
import datetime as _dt
spot, legs = await self._fetch_chain_legs(currency, expiry_from, expiry_to, top_n_strikes)
result = opt.vanna_charm_aggregate(legs, spot)
return {
"currency": currency.upper(),
**result,
"data_timestamp": _dt.datetime.now(_dt.UTC).isoformat(),
"testnet": self.testnet,
}
async def get_oi_weighted_skew(
self,
currency: str,
expiry_from: str | None = None,
expiry_to: str | None = None,
top_n_strikes: int = 100,
) -> dict:
"""Skew aggregato pesato OI: IV media puts - calls. Positivo = paura.
"""
import datetime as _dt
_, legs = await self._fetch_chain_legs(currency, expiry_from, expiry_to, top_n_strikes)
result = opt.oi_weighted_skew(legs)
return {
"currency": currency.upper(),
**result,
"data_timestamp": _dt.datetime.now(_dt.UTC).isoformat(),
"testnet": self.testnet,
}
async def get_smile_asymmetry(
self,
currency: str,
expiry_from: str | None = None,
expiry_to: str | None = None,
top_n_strikes: int = 100,
) -> dict:
"""Asymmetry IV otm-puts vs otm-calls. Positivo = put-side richer."""
import datetime as _dt
spot, legs = await self._fetch_chain_legs(currency, expiry_from, expiry_to, top_n_strikes)
result = opt.smile_asymmetry(legs, spot)
return {
"currency": currency.upper(),
"spot_price": spot,
**result,
"data_timestamp": _dt.datetime.now(_dt.UTC).isoformat(),
"testnet": self.testnet,
}
async def get_atm_vs_wings_vol(
self,
currency: str,
expiry_from: str | None = None,
expiry_to: str | None = None,
top_n_strikes: int = 100,
) -> dict:
"""IV ATM vs IV alle ali 25-delta. wing_richness > 0 → smile (kurtosis)."""
import datetime as _dt
spot, legs = await self._fetch_chain_legs(currency, expiry_from, expiry_to, top_n_strikes)
result = opt.atm_vs_wings_vol(legs, spot)
return {
"currency": currency.upper(),
"spot_price": spot,
**result,
"data_timestamp": _dt.datetime.now(_dt.UTC).isoformat(),
"testnet": self.testnet,
}
async def get_pc_ratio(self, currency: str) -> dict:
import datetime as _dt
@@ -62,6 +62,11 @@ class GetOrderbookReq(BaseModel):
depth: int = 10
class OrderbookImbalanceReq(BaseModel):
instrument_name: str
depth: int = 10
class GetPositionsReq(BaseModel):
currency: str = "USDC"
@@ -110,6 +115,15 @@ class GetGexReq(BaseModel):
top_n_strikes: int = 50
class OptionFlowReq(BaseModel):
"""Body comune per indicatori option-flow (dealer gamma, vanna/charm,
OI-weighted skew, smile asymmetry, ATM vs wings)."""
currency: str
expiry_from: str | None = None
expiry_to: str | None = None
top_n_strikes: int = 100
class GetPcRatioReq(BaseModel):
currency: str
@@ -336,6 +350,13 @@ def create_app(
_check(principal, core=True, observer=True)
return await client.get_orderbook(body.instrument_name, body.depth)
@app.post("/tools/get_orderbook_imbalance", tags=["reads"])
async def t_get_ob_imbalance(
body: OrderbookImbalanceReq, principal: Principal = Depends(require_principal)
):
_check(principal, core=True, observer=True)
return await client.get_orderbook_imbalance(body.instrument_name, body.depth)
@app.post("/tools/get_positions", tags=["reads"])
async def t_get_positions(
body: GetPositionsReq, principal: Principal = Depends(require_principal)
@@ -384,6 +405,51 @@ def create_app(
body.currency, body.expiry_from, body.expiry_to, body.top_n_strikes
)
@app.post("/tools/get_dealer_gamma_profile", tags=["reads"])
async def t_get_dealer_gamma_profile(
body: OptionFlowReq, principal: Principal = Depends(require_principal)
):
_check(principal, core=True, observer=True)
return await client.get_dealer_gamma_profile(
body.currency, body.expiry_from, body.expiry_to, body.top_n_strikes
)
@app.post("/tools/get_vanna_charm", tags=["reads"])
async def t_get_vanna_charm(
body: OptionFlowReq, principal: Principal = Depends(require_principal)
):
_check(principal, core=True, observer=True)
return await client.get_vanna_charm(
body.currency, body.expiry_from, body.expiry_to, body.top_n_strikes
)
@app.post("/tools/get_oi_weighted_skew", tags=["reads"])
async def t_get_oi_weighted_skew(
body: OptionFlowReq, principal: Principal = Depends(require_principal)
):
_check(principal, core=True, observer=True)
return await client.get_oi_weighted_skew(
body.currency, body.expiry_from, body.expiry_to, body.top_n_strikes
)
@app.post("/tools/get_smile_asymmetry", tags=["reads"])
async def t_get_smile_asymmetry(
body: OptionFlowReq, principal: Principal = Depends(require_principal)
):
_check(principal, core=True, observer=True)
return await client.get_smile_asymmetry(
body.currency, body.expiry_from, body.expiry_to, body.top_n_strikes
)
@app.post("/tools/get_atm_vs_wings_vol", tags=["reads"])
async def t_get_atm_vs_wings_vol(
body: OptionFlowReq, principal: Principal = Depends(require_principal)
):
_check(principal, core=True, observer=True)
return await client.get_atm_vs_wings_vol(
body.currency, body.expiry_from, body.expiry_to, body.top_n_strikes
)
@app.post("/tools/get_pc_ratio", tags=["reads"])
async def t_get_pc_ratio(
body: GetPcRatioReq, principal: Principal = Depends(require_principal)
@@ -563,6 +629,7 @@ def create_app(
{"name": "get_ticker_batch", "description": "Ticker per N instruments in parallelo (max 20)."},
{"name": "get_instruments", "description": "Lista instruments per currency."},
{"name": "get_orderbook", "description": "Orderbook L1/L2 per instrument."},
{"name": "get_orderbook_imbalance", "description": "Microstructure: imbalance ratio + microprice + slope."},
{"name": "get_positions", "description": "Posizioni aperte."},
{"name": "get_account_summary", "description": "Summary account (equity, balance)."},
{"name": "get_trade_history", "description": "Storia trade recenti."},
@@ -577,6 +644,11 @@ def create_app(
{"name": "get_skew_25d", "description": "Skew 25-delta put/call IV + risk reversal + butterfly per expiry."},
{"name": "get_pc_ratio", "description": "Put/Call ratio aggregato su OI e volume 24h."},
{"name": "get_gex", "description": "Gamma exposure per strike + zero gamma level (top N strikes per OI)."},
{"name": "get_dealer_gamma_profile", "description": "Net dealer gamma per strike (short calls/long puts) + gamma flip level."},
{"name": "get_vanna_charm", "description": "Vanna (∂delta/∂IV) e Charm (∂delta/∂t) aggregati pesati OI."},
{"name": "get_oi_weighted_skew", "description": "Skew aggregato pesato per OI: IV puts - IV calls. Positivo = paura."},
{"name": "get_smile_asymmetry", "description": "Asymmetry IV otm-puts vs otm-calls + ATM IV reference."},
{"name": "get_atm_vs_wings_vol", "description": "IV ATM vs IV ali 25-delta. wing_richness > 0 = smile/kurtosis."},
{"name": "get_technical_indicators", "description": "Indicatori tecnici (RSI, MACD, ATR, ADX)."},
{"name": "get_realized_vol", "description": "Volatilità realizzata annualizzata (log-return std) BTC/ETH + spread IVRV."},
{"name": "place_order", "description": "Invia ordine (CORE only, testnet)."},