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
|
||||
|
||||
|
||||
@@ -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 IV−RV."},
|
||||
{"name": "place_order", "description": "Invia ordine (CORE only, testnet)."},
|
||||
|
||||
Reference in New Issue
Block a user