feat(V2): migrazione deribit (client, leverage_cap, tools)
Task 6.1 V2.0.0: copia client.py + leverage_cap.py da services/mcp-deribit con import riscritti (mcp_common -> cerbero_mcp.common, mcp_deribit -> cerbero_mcp.exchanges.deribit). Estratte 34 tool async (28 endpoint + is_testnet/environment_info + helpers) in tools.py: pure logica senza FastAPI/ACL. Audit calls per ora rimossi (TODO: cabling via router su request.state.environment). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,528 @@
|
||||
"""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:
|
||||
return await client.get_ticker(params.instrument_name)
|
||||
|
||||
|
||||
async def get_ticker_batch(client: DeribitClient, params: GetTickerBatchReq) -> dict:
|
||||
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) -> dict:
|
||||
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) -> dict:
|
||||
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
|
||||
Reference in New Issue
Block a user