"""Tool bybit V2: pydantic schemas + async functions. Ogni funzione prende (client: BybitClient, params: ) 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 from typing import Any from pydantic import BaseModel, Field from cerbero_mcp.exchanges.bybit.client import BybitClient from cerbero_mcp.exchanges.bybit.leverage_cap import ( enforce_leverage as _enforce_leverage, ) from cerbero_mcp.exchanges.bybit.leverage_cap import get_max_leverage # === Schemas: reads === class TickerReq(BaseModel): symbol: str category: str = "linear" class TickerBatchReq(BaseModel): symbols: list[str] category: str = "linear" class OrderbookReq(BaseModel): symbol: str category: str = "linear" limit: int = 50 class HistoricalReq(BaseModel): symbol: str category: str = "linear" interval: str = "60" start: int | None = None end: int | None = None limit: int = 1000 class IndicatorsReq(BaseModel): symbol: str category: str = "linear" indicators: list[str] = ["rsi", "atr", "macd", "adx"] interval: str = "60" start: int | None = None end: int | None = None class FundingRateReq(BaseModel): symbol: str category: str = "linear" class FundingHistoryReq(BaseModel): symbol: str category: str = "linear" limit: int = 100 class OpenInterestReq(BaseModel): symbol: str category: str = "linear" interval: str = "5min" limit: int = 288 class InstrumentsReq(BaseModel): category: str = "linear" symbol: str | None = None class OptionChainReq(BaseModel): base_coin: str expiry: str | None = None class PositionsReq(BaseModel): category: str = "linear" class AccountSummaryReq(BaseModel): pass class TradeHistoryReq(BaseModel): category: str = "linear" limit: int = 50 class OpenOrdersReq(BaseModel): category: str = "linear" symbol: str | None = None class BasisSpotPerpReq(BaseModel): asset: str class OrderbookImbalanceReq(BaseModel): symbol: str category: str = "linear" depth: int = 10 class BasisTermStructureReq(BaseModel): asset: str # === Schemas: writes === class PlaceOrderReq(BaseModel): category: str symbol: str side: str qty: float order_type: str = "Limit" price: float | None = None tif: str = "GTC" reduce_only: bool = False position_idx: int | None = None model_config = { "json_schema_extra": { "examples": [ { "summary": "Market buy 0.01 BTCUSDT linear perp", "value": { "category": "linear", "symbol": "BTCUSDT", "side": "Buy", "qty": 0.01, "order_type": "Market", }, } ] } } class ComboLegReq(BaseModel): symbol: str side: str qty: float order_type: str = "Limit" price: float | None = None tif: str = "GTC" reduce_only: bool = False class PlaceComboOrderReq(BaseModel): category: str = "option" legs: list[ComboLegReq] = Field(..., min_length=2) class AmendOrderReq(BaseModel): category: str symbol: str order_id: str new_qty: float | None = None new_price: float | None = None class CancelOrderReq(BaseModel): category: str symbol: str order_id: str class CancelAllReq(BaseModel): category: str symbol: str | None = None class SetStopLossReq(BaseModel): category: str symbol: str stop_loss: float position_idx: int = 0 class SetTakeProfitReq(BaseModel): category: str symbol: str take_profit: float position_idx: int = 0 class ClosePositionReq(BaseModel): category: str symbol: str class SetLeverageReq(BaseModel): category: str symbol: str leverage: int class SwitchModeReq(BaseModel): category: str symbol: str mode: str class TransferReq(BaseModel): coin: str amount: float from_type: str to_type: str # === Tools (reads) === async def environment_info( client: BybitClient, *, creds: dict, env_info: Any | None = None ) -> dict: if env_info is None: return { "exchange": "bybit", "environment": "testnet" if client.testnet else "mainnet", "source": "credentials", "env_value": None, "base_url": getattr(client, "base_url", None), "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: BybitClient, params: TickerReq) -> dict: return await client.get_ticker(params.symbol, params.category) async def get_ticker_batch(client: BybitClient, params: TickerBatchReq) -> dict: return await client.get_ticker_batch(params.symbols, params.category) async def get_orderbook(client: BybitClient, params: OrderbookReq) -> dict: return await client.get_orderbook(params.symbol, params.category, params.limit) async def get_historical(client: BybitClient, params: HistoricalReq) -> dict: return await client.get_historical( params.symbol, params.category, params.interval, params.start, params.end, params.limit, ) async def get_indicators(client: BybitClient, params: IndicatorsReq) -> dict: return await client.get_indicators( params.symbol, params.category, params.indicators, params.interval, params.start, params.end, ) async def get_funding_rate(client: BybitClient, params: FundingRateReq) -> dict: return await client.get_funding_rate(params.symbol, params.category) async def get_funding_history(client: BybitClient, params: FundingHistoryReq) -> dict: return await client.get_funding_history( params.symbol, params.category, params.limit ) async def get_open_interest(client: BybitClient, params: OpenInterestReq) -> dict: return await client.get_open_interest( params.symbol, params.category, params.interval, params.limit ) async def get_instruments(client: BybitClient, params: InstrumentsReq) -> dict: return await client.get_instruments(params.category, params.symbol) async def get_option_chain(client: BybitClient, params: OptionChainReq) -> dict: return await client.get_option_chain(params.base_coin, params.expiry) async def get_positions(client: BybitClient, params: PositionsReq) -> dict: return {"positions": await client.get_positions(params.category)} async def get_account_summary( client: BybitClient, params: AccountSummaryReq ) -> dict: return await client.get_account_summary() async def get_trade_history(client: BybitClient, params: TradeHistoryReq) -> dict: return { "trades": await client.get_trade_history(params.category, params.limit) } async def get_open_orders(client: BybitClient, params: OpenOrdersReq) -> dict: return { "orders": await client.get_open_orders(params.category, params.symbol) } async def get_basis_spot_perp(client: BybitClient, params: BasisSpotPerpReq) -> dict: return await client.get_basis_spot_perp(params.asset) async def get_orderbook_imbalance( client: BybitClient, params: OrderbookImbalanceReq ) -> dict: return await client.get_orderbook_imbalance( params.symbol, params.category, params.depth ) async def get_basis_term_structure( client: BybitClient, params: BasisTermStructureReq ) -> dict: return await client.get_basis_term_structure(params.asset) # === Tools (writes) === async def place_order( client: BybitClient, params: PlaceOrderReq, *, creds: dict ) -> dict: # Bybit non ha leverage_cap parametro per place_order; cap applicato a set_leverage. result = await client.place_order( category=params.category, symbol=params.symbol, side=params.side, qty=params.qty, order_type=params.order_type, price=params.price, tif=params.tif, reduce_only=params.reduce_only, position_idx=params.position_idx, ) return result async def place_combo_order( client: BybitClient, params: PlaceComboOrderReq, *, creds: dict ) -> dict: result = await client.place_combo_order( category=params.category, legs=[leg.model_dump() for leg in params.legs], ) return result async def amend_order(client: BybitClient, params: AmendOrderReq) -> dict: result = await client.amend_order( params.category, params.symbol, params.order_id, params.new_qty, params.new_price, ) return result async def cancel_order(client: BybitClient, params: CancelOrderReq) -> dict: result = await client.cancel_order( params.category, params.symbol, params.order_id ) return result async def cancel_all_orders(client: BybitClient, params: CancelAllReq) -> dict: result = await client.cancel_all_orders(params.category, params.symbol) return result async def set_stop_loss(client: BybitClient, params: SetStopLossReq) -> dict: result = await client.set_stop_loss( params.category, params.symbol, params.stop_loss, params.position_idx ) return result async def set_take_profit(client: BybitClient, params: SetTakeProfitReq) -> dict: result = await client.set_take_profit( params.category, params.symbol, params.take_profit, params.position_idx ) return result async def close_position(client: BybitClient, params: ClosePositionReq) -> dict: result = await client.close_position(params.category, params.symbol) return result async def set_leverage( client: BybitClient, params: SetLeverageReq, *, creds: dict ) -> dict: _enforce_leverage(params.leverage, creds=creds, exchange="bybit") result = await client.set_leverage( params.category, params.symbol, params.leverage ) return result async def switch_position_mode( client: BybitClient, params: SwitchModeReq ) -> dict: result = await client.switch_position_mode( params.category, params.symbol, params.mode ) return result async def transfer_asset(client: BybitClient, params: TransferReq) -> dict: result = await client.transfer_asset( params.coin, params.amount, params.from_type, params.to_type ) return result