Files
Cerbero-mcp/src/cerbero_mcp/exchanges/deribit/tools.py
T
AdrianoDev 697d118522 chore(V2): mypy clean — fix radice V2 nuovo + suppress mirato V1 legacy
- settings.py: lambda factory + type:ignore[call-arg] per env-loaded models
- routers/*.py (6 file): cast esplicito Environment / Client per request.state
- __main__.py: cast Literal env in builder, type:ignore Settings()
- server.py: type:ignore[method-assign] su app.openapi
- deribit/tools.py: assert su validator-normalized fields, list return type
- deribit/client.py: type:ignore mirato no-any-return / has-type, rinomina types→types_list
- hyperliquid/{client,tools}.py: assert su validator-normalized fields, var-annotated
- alpaca/client.py: type:ignore mirato per SDK quirks (assignment, no-any-return, arg-type, union-attr)
- {macro,sentiment}/fetchers.py: type:ignore mirato no-any-return / operator / union-attr

Mypy: 68 → 0 errors. Test: 259 passing. Ruff: clean.
2026-04-30 20:43:03 +02:00

531 lines
15 KiB
Python

"""Tool deribit V2: pydantic schemas + async functions.
Ogni funzione prende (client: DeribitClient, params: <Req>) e restituisce
un dict (o un model Pydantic). Pure logica, no FastAPI dependency, no ACL.
L'autenticazione bearer è gestita dal middleware in cerbero_mcp.auth;
l'audit verrà cablato dal router via request.state.environment.
"""
from __future__ import annotations
import contextlib
from typing import Any
from pydantic import BaseModel, field_validator, model_validator
from cerbero_mcp.exchanges.deribit.client import DeribitClient
from cerbero_mcp.exchanges.deribit.leverage_cap import (
enforce_leverage as _enforce_leverage,
)
from cerbero_mcp.exchanges.deribit.leverage_cap import get_max_leverage
# === Schemas ===
class GetTickerReq(BaseModel):
instrument_name: str | None = None
instrument: str | None = None
model_config = {"extra": "allow"}
@model_validator(mode="after")
def _normalize(self):
sym = self.instrument_name or self.instrument
if not sym:
raise ValueError("instrument_name (or instrument) is required")
self.instrument_name = sym
return self
class GetTickerBatchReq(BaseModel):
instrument_names: list[str] | None = None
instruments: list[str] | None = None
model_config = {"extra": "allow"}
@model_validator(mode="after")
def _normalize(self):
names = self.instrument_names or self.instruments
if not names:
raise ValueError("instrument_names (or instruments) is required")
self.instrument_names = names
return self
class GetInstrumentsReq(BaseModel):
currency: str
kind: str | None = None
expiry_from: str | None = None
expiry_to: str | None = None
strike_min: float | None = None
strike_max: float | None = None
min_open_interest: float | None = None
limit: int = 100
offset: int = 0
class GetOrderbookReq(BaseModel):
instrument_name: str
depth: int = 10
class OrderbookImbalanceReq(BaseModel):
instrument_name: str
depth: int = 10
class GetPositionsReq(BaseModel):
currency: str = "USDC"
class GetAccountSummaryReq(BaseModel):
currency: str = "USDC"
class GetTradeHistoryReq(BaseModel):
limit: int = 100
instrument_name: str | None = None
class GetHistoricalReq(BaseModel):
instrument: str
start_date: str
end_date: str
resolution: str = "1h"
class GetDvolReq(BaseModel):
currency: str = "BTC"
start_date: str
end_date: str
resolution: str = "1D"
class GetDvolHistoryReq(BaseModel):
currency: str = "BTC"
lookback_days: int = 90
class GetIvRankReq(BaseModel):
instrument: str
class GetRealizedVolReq(BaseModel):
currency: str = "BTC"
windows: list[int] = [14, 30]
class GetGexReq(BaseModel):
currency: str
expiry_from: str | None = None
expiry_to: str | None = None
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
class GetSkew25dReq(BaseModel):
currency: str
expiry: str
class GetTermStructureReq(BaseModel):
currency: str
class CalculateSpreadPayoffReq(BaseModel):
legs: list[dict]
quote_currency: str = "USD"
class RunBacktestReq(BaseModel):
strategy_name: str
underlying: str = "BTC"
lookback_days: int = 30
resolution: str = "4h"
entry_rules: dict | None = None
exit_rules: dict | None = None
class FindByDeltaReq(BaseModel):
currency: str
expiry: str
target_delta: float
option_type: str
max_results: int = 3
min_open_interest: float = 100.0
min_volume_24h: float = 20.0
class GetIndicatorsReq(BaseModel):
instrument: str
indicators: list[str]
start_date: str
end_date: str
resolution: str = "1h"
@field_validator("indicators", mode="before")
@classmethod
def _coerce_indicators(cls, v):
if isinstance(v, str):
import json
s = v.strip()
if s.startswith("["):
try:
parsed = json.loads(s)
if isinstance(parsed, list):
return [str(x).strip() for x in parsed if str(x).strip()]
except json.JSONDecodeError:
pass
return [x.strip() for x in s.split(",") if x.strip()]
if isinstance(v, list):
return v
raise ValueError(
"indicators must be a list like ['rsi','atr','macd'] "
"or a comma-separated string like 'rsi,atr,macd'"
)
class PlaceOrderReq(BaseModel):
instrument_name: str
side: str # "buy" | "sell"
amount: float
type: str = "limit"
price: float | None = None
reduce_only: bool = False
post_only: bool = False
label: str | None = None
leverage: int | None = None # CER-016: None → default cap (3x)
model_config = {
"json_schema_extra": {
"examples": [
{
"summary": "Market buy 0.1 BTC perpetual",
"value": {
"instrument_name": "BTC-PERPETUAL",
"type": "market",
"amount": 0.1,
"side": "buy",
},
}
]
}
}
class ComboLeg(BaseModel):
instrument_name: str
direction: str # "buy" | "sell"
ratio: int = 1
class PlaceComboOrderReq(BaseModel):
legs: list[ComboLeg]
side: str # "buy" | "sell"
amount: float
type: str = "limit"
price: float | None = None
label: str | None = None
leverage: int | None = None
@model_validator(mode="after")
def _at_least_two_legs(self):
if len(self.legs) < 2:
raise ValueError("combo requires at least 2 legs")
return self
class CancelOrderReq(BaseModel):
order_id: str
class SetStopLossReq(BaseModel):
order_id: str
stop_price: float
class SetTakeProfitReq(BaseModel):
order_id: str
tp_price: float
class ClosePositionReq(BaseModel):
instrument_name: str
# === Tools (reads) ===
async def is_testnet(client: DeribitClient) -> dict:
return client.is_testnet()
async def environment_info(
client: DeribitClient, *, creds: dict, env_info: Any | None = None
) -> dict:
if env_info is None:
return {
"exchange": "deribit",
"environment": "testnet" if client.is_testnet().get("testnet") else "mainnet",
"source": "credentials",
"env_value": None,
"base_url": client.base_url,
"max_leverage": get_max_leverage(creds),
}
return {
"exchange": env_info.exchange,
"environment": env_info.environment,
"source": env_info.source,
"env_value": env_info.env_value,
"base_url": env_info.base_url,
"max_leverage": get_max_leverage(creds),
}
async def get_ticker(client: DeribitClient, params: GetTickerReq) -> dict:
assert params.instrument_name is not None # validator garantisce non-None
return await client.get_ticker(params.instrument_name)
async def get_ticker_batch(client: DeribitClient, params: GetTickerBatchReq) -> dict:
assert params.instrument_names is not None # validator garantisce non-None
return await client.get_ticker_batch(params.instrument_names)
async def get_instruments(client: DeribitClient, params: GetInstrumentsReq) -> dict:
return await client.get_instruments(
currency=params.currency,
kind=params.kind,
expiry_from=params.expiry_from,
expiry_to=params.expiry_to,
strike_min=params.strike_min,
strike_max=params.strike_max,
min_open_interest=params.min_open_interest,
limit=params.limit,
offset=params.offset,
)
async def get_orderbook(client: DeribitClient, params: GetOrderbookReq) -> dict:
return await client.get_orderbook(params.instrument_name, params.depth)
async def get_orderbook_imbalance(
client: DeribitClient, params: OrderbookImbalanceReq
) -> dict:
return await client.get_orderbook_imbalance(params.instrument_name, params.depth)
async def get_positions(client: DeribitClient, params: GetPositionsReq) -> list:
return await client.get_positions(params.currency)
async def get_account_summary(
client: DeribitClient, params: GetAccountSummaryReq
) -> dict:
return await client.get_account_summary(params.currency)
async def get_trade_history(client: DeribitClient, params: GetTradeHistoryReq) -> list:
return await client.get_trade_history(params.limit, params.instrument_name)
async def get_historical(client: DeribitClient, params: GetHistoricalReq) -> dict:
return await client.get_historical(
params.instrument, params.start_date, params.end_date, params.resolution
)
async def get_dvol(client: DeribitClient, params: GetDvolReq) -> dict:
return await client.get_dvol(
params.currency, params.start_date, params.end_date, params.resolution
)
async def get_gex(client: DeribitClient, params: GetGexReq) -> dict:
return await client.get_gex(
params.currency, params.expiry_from, params.expiry_to, params.top_n_strikes
)
async def get_dealer_gamma_profile(
client: DeribitClient, params: OptionFlowReq
) -> dict:
return await client.get_dealer_gamma_profile(
params.currency, params.expiry_from, params.expiry_to, params.top_n_strikes
)
async def get_vanna_charm(client: DeribitClient, params: OptionFlowReq) -> dict:
return await client.get_vanna_charm(
params.currency, params.expiry_from, params.expiry_to, params.top_n_strikes
)
async def get_oi_weighted_skew(client: DeribitClient, params: OptionFlowReq) -> dict:
return await client.get_oi_weighted_skew(
params.currency, params.expiry_from, params.expiry_to, params.top_n_strikes
)
async def get_smile_asymmetry(client: DeribitClient, params: OptionFlowReq) -> dict:
return await client.get_smile_asymmetry(
params.currency, params.expiry_from, params.expiry_to, params.top_n_strikes
)
async def get_atm_vs_wings_vol(client: DeribitClient, params: OptionFlowReq) -> dict:
return await client.get_atm_vs_wings_vol(
params.currency, params.expiry_from, params.expiry_to, params.top_n_strikes
)
async def get_pc_ratio(client: DeribitClient, params: GetPcRatioReq) -> dict:
return await client.get_pc_ratio(params.currency)
async def get_skew_25d(client: DeribitClient, params: GetSkew25dReq) -> dict:
return await client.get_skew_25d(params.currency, params.expiry)
async def get_term_structure(
client: DeribitClient, params: GetTermStructureReq
) -> dict:
return await client.get_term_structure(params.currency)
async def run_backtest(client: DeribitClient, params: RunBacktestReq) -> dict:
return await client.run_backtest(
strategy_name=params.strategy_name,
underlying=params.underlying,
lookback_days=params.lookback_days,
resolution=params.resolution,
entry_rules=params.entry_rules,
exit_rules=params.exit_rules,
)
async def calculate_spread_payoff(
client: DeribitClient, params: CalculateSpreadPayoffReq
) -> dict:
return await client.calculate_spread_payoff(params.legs, params.quote_currency)
async def find_by_delta(client: DeribitClient, params: FindByDeltaReq) -> dict:
return await client.find_by_delta(
currency=params.currency,
expiry=params.expiry,
target_delta=params.target_delta,
option_type=params.option_type,
max_results=params.max_results,
min_open_interest=params.min_open_interest,
min_volume_24h=params.min_volume_24h,
)
async def get_iv_rank(client: DeribitClient, params: GetIvRankReq) -> dict:
return await client.get_iv_rank(params.instrument)
async def get_dvol_history(client: DeribitClient, params: GetDvolHistoryReq) -> dict:
return await client.get_dvol_history(params.currency, params.lookback_days)
async def get_realized_vol(client: DeribitClient, params: GetRealizedVolReq) -> dict:
return await client.get_realized_vol(params.currency, params.windows)
async def get_technical_indicators(
client: DeribitClient, params: GetIndicatorsReq
) -> dict:
return await client.get_technical_indicators(
params.instrument,
params.indicators,
params.start_date,
params.end_date,
params.resolution,
)
# === Tools (writes) ===
async def place_order(
client: DeribitClient, params: PlaceOrderReq, *, creds: dict
) -> dict:
cap_default = get_max_leverage(creds)
lev = _enforce_leverage(params.leverage, creds=creds, exchange="deribit")
if lev != cap_default:
with contextlib.suppress(Exception):
await client.set_leverage(params.instrument_name, lev)
result = await client.place_order(
instrument_name=params.instrument_name,
side=params.side,
amount=params.amount,
type=params.type,
price=params.price,
reduce_only=params.reduce_only,
post_only=params.post_only,
label=params.label,
)
# TODO V2: wire audit via request.state.environment in router
return result
async def place_combo_order(
client: DeribitClient, params: PlaceComboOrderReq, *, creds: dict
) -> dict:
cap_default = get_max_leverage(creds)
lev = _enforce_leverage(params.leverage, creds=creds, exchange="deribit")
if lev != cap_default:
for leg in params.legs:
with contextlib.suppress(Exception):
await client.set_leverage(leg.instrument_name, lev)
result = await client.place_combo_order(
legs=[leg.model_dump() for leg in params.legs],
side=params.side,
amount=params.amount,
type=params.type,
price=params.price,
label=params.label,
)
# TODO V2: wire audit via request.state.environment in router
return result
async def cancel_order(client: DeribitClient, params: CancelOrderReq) -> dict:
result = await client.cancel_order(params.order_id)
# TODO V2: wire audit via request.state.environment in router
return result
async def set_stop_loss(client: DeribitClient, params: SetStopLossReq) -> dict:
result = await client.set_stop_loss(params.order_id, params.stop_price)
# TODO V2: wire audit via request.state.environment in router
return result
async def set_take_profit(client: DeribitClient, params: SetTakeProfitReq) -> dict:
result = await client.set_take_profit(params.order_id, params.tp_price)
# TODO V2: wire audit via request.state.environment in router
return result
async def close_position(client: DeribitClient, params: ClosePositionReq) -> dict:
result = await client.close_position(params.instrument_name)
# TODO V2: wire audit via request.state.environment in router
return result