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:
@@ -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
|
||||
Reference in New Issue
Block a user