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