697d118522
- 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.
531 lines
15 KiB
Python
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
|