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