feat(V2): migrazione bybit completa (client, tools, router, test, builder)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
AdrianoDev
2026-04-30 18:31:51 +02:00
parent a8d970233e
commit 5e42ce9c69
11 changed files with 2126 additions and 0 deletions
+442
View File
@@ -0,0 +1,442 @@
"""Tool bybit V2: pydantic schemas + async functions.
Ogni funzione prende (client: BybitClient, 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
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,
)
# TODO V2: wire audit via request.state.environment in router
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],
)
# TODO V2: wire audit via request.state.environment in router
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