feat(V2): unified /mcp interface; retire Bybit/Alpaca to old/
Add a common cross-exchange interface (/mcp) over the integrated venues
(deribit, hyperliquid):
- get_instruments: uniform schema where each row carries its own
`exchange`, `fees` (maker/taker, live from Deribit, null where the
venue has no per-instrument schedule) and `history_start` (listing
date, live from Deribit creation_timestamp), plus type/tick_size and a
lossless `native` blob. Optional `exchange` filter; fan-out otherwise.
- get_historical: generalized to {exchange, instrument, interval,
start_date, end_date}, returning a single chosen venue's candles.
Consensus merge stays available on /mcp-cross.
New: routers/unified.py, exchanges/cross/instruments.py (normalizers),
UnifiedClient in cross/client.py, schemas in cross/tools.py. Deribit
get_instruments now also surfaces maker/taker_commission and
creation_timestamp (additive).
Retire Bybit and Alpaca from the API surface: move clients, routers,
settings classes and their tests under old/ (history preserved via
git mv); drop them from the builder, /mcp-cross dispatch and symbol_map.
Bybit remains a public funding/OI data source in sentiment (not the
trading client). IBKR is intentionally excluded from /mcp for now.
Docs: rewrite API_REFERENCE.md (remove Bybit/Alpaca, document /mcp,
clarify that data_timestamp is injected globally by middleware).
Tests: add unified-interface coverage; update cross/settings/builder/boot
tests for the reduced venue set. Fix a pre-existing flaky assertion in
the Hyperliquid signing test (r/s use eth_utils.to_hex like the official
SDK, so a leading zero byte yields <66 chars ~1/256 of the time).
323 passed, ruff clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,346 @@
|
||||
"""Router /mcp-bybit/* — DI per env, client e (write) creds.
|
||||
|
||||
Mappa 1:1 i tool di `cerbero_mcp.exchanges.bybit.tools` a endpoint
|
||||
`POST /mcp-bybit/tools/{tool_name}`. L'autenticazione bearer è gestita
|
||||
dal middleware in `cerbero_mcp.auth`; qui leggiamo solo `request.state.environment`.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal, cast
|
||||
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
|
||||
from cerbero_mcp.client_registry import ClientRegistry
|
||||
from cerbero_mcp.common.audit_helpers import audit_call
|
||||
from cerbero_mcp.exchanges.bybit import tools as t
|
||||
from cerbero_mcp.exchanges.bybit.client import BybitClient
|
||||
|
||||
Environment = Literal["testnet", "mainnet"]
|
||||
|
||||
|
||||
def get_environment(request: Request) -> Environment:
|
||||
return cast(Environment, request.state.environment)
|
||||
|
||||
|
||||
async def get_bybit_client(
|
||||
request: Request, env: Environment = Depends(get_environment)
|
||||
) -> BybitClient:
|
||||
registry: ClientRegistry = request.app.state.registry
|
||||
return cast(BybitClient, await registry.get("bybit", env))
|
||||
|
||||
|
||||
def _build_creds(request: Request) -> dict:
|
||||
"""Costruisce dict `creds` minimale per leverage cap / metadata.
|
||||
|
||||
Le credenziali vere sono già iniettate nel client da ClientRegistry;
|
||||
qui passiamo solo il cap di leverage e l'api_key (metadata audit).
|
||||
"""
|
||||
settings = request.app.state.settings
|
||||
return {
|
||||
"max_leverage": settings.bybit.max_leverage,
|
||||
"api_key": settings.bybit.api_key,
|
||||
}
|
||||
|
||||
|
||||
def make_router() -> APIRouter:
|
||||
r = APIRouter(prefix="/mcp-bybit", tags=["bybit"])
|
||||
|
||||
# === READ tools ===
|
||||
|
||||
@r.post("/tools/environment_info")
|
||||
async def _environment_info(
|
||||
request: Request,
|
||||
client: BybitClient = Depends(get_bybit_client),
|
||||
):
|
||||
creds = _build_creds(request)
|
||||
return await t.environment_info(client, creds=creds)
|
||||
|
||||
@r.post("/tools/get_ticker")
|
||||
async def _get_ticker(
|
||||
params: t.TickerReq,
|
||||
client: BybitClient = Depends(get_bybit_client),
|
||||
):
|
||||
return await t.get_ticker(client, params)
|
||||
|
||||
@r.post("/tools/get_ticker_batch")
|
||||
async def _get_ticker_batch(
|
||||
params: t.TickerBatchReq,
|
||||
client: BybitClient = Depends(get_bybit_client),
|
||||
):
|
||||
return await t.get_ticker_batch(client, params)
|
||||
|
||||
@r.post("/tools/get_orderbook")
|
||||
async def _get_orderbook(
|
||||
params: t.OrderbookReq,
|
||||
client: BybitClient = Depends(get_bybit_client),
|
||||
):
|
||||
return await t.get_orderbook(client, params)
|
||||
|
||||
@r.post("/tools/get_historical")
|
||||
async def _get_historical(
|
||||
params: t.HistoricalReq,
|
||||
client: BybitClient = Depends(get_bybit_client),
|
||||
):
|
||||
return await t.get_historical(client, params)
|
||||
|
||||
@r.post("/tools/get_indicators")
|
||||
async def _get_indicators(
|
||||
params: t.IndicatorsReq,
|
||||
client: BybitClient = Depends(get_bybit_client),
|
||||
):
|
||||
return await t.get_indicators(client, params)
|
||||
|
||||
@r.post("/tools/get_funding_rate")
|
||||
async def _get_funding_rate(
|
||||
params: t.FundingRateReq,
|
||||
client: BybitClient = Depends(get_bybit_client),
|
||||
):
|
||||
return await t.get_funding_rate(client, params)
|
||||
|
||||
@r.post("/tools/get_funding_history")
|
||||
async def _get_funding_history(
|
||||
params: t.FundingHistoryReq,
|
||||
client: BybitClient = Depends(get_bybit_client),
|
||||
):
|
||||
return await t.get_funding_history(client, params)
|
||||
|
||||
@r.post("/tools/get_open_interest")
|
||||
async def _get_open_interest(
|
||||
params: t.OpenInterestReq,
|
||||
client: BybitClient = Depends(get_bybit_client),
|
||||
):
|
||||
return await t.get_open_interest(client, params)
|
||||
|
||||
@r.post("/tools/get_instruments")
|
||||
async def _get_instruments(
|
||||
params: t.InstrumentsReq,
|
||||
client: BybitClient = Depends(get_bybit_client),
|
||||
):
|
||||
return await t.get_instruments(client, params)
|
||||
|
||||
@r.post("/tools/get_option_chain")
|
||||
async def _get_option_chain(
|
||||
params: t.OptionChainReq,
|
||||
client: BybitClient = Depends(get_bybit_client),
|
||||
):
|
||||
return await t.get_option_chain(client, params)
|
||||
|
||||
@r.post("/tools/get_positions")
|
||||
async def _get_positions(
|
||||
params: t.PositionsReq,
|
||||
client: BybitClient = Depends(get_bybit_client),
|
||||
):
|
||||
return await t.get_positions(client, params)
|
||||
|
||||
@r.post("/tools/get_account_summary")
|
||||
async def _get_account_summary(
|
||||
params: t.AccountSummaryReq,
|
||||
client: BybitClient = Depends(get_bybit_client),
|
||||
):
|
||||
return await t.get_account_summary(client, params)
|
||||
|
||||
@r.post("/tools/get_trade_history")
|
||||
async def _get_trade_history(
|
||||
params: t.TradeHistoryReq,
|
||||
client: BybitClient = Depends(get_bybit_client),
|
||||
):
|
||||
return await t.get_trade_history(client, params)
|
||||
|
||||
@r.post("/tools/get_open_orders")
|
||||
async def _get_open_orders(
|
||||
params: t.OpenOrdersReq,
|
||||
client: BybitClient = Depends(get_bybit_client),
|
||||
):
|
||||
return await t.get_open_orders(client, params)
|
||||
|
||||
@r.post("/tools/get_basis_spot_perp")
|
||||
async def _get_basis_spot_perp(
|
||||
params: t.BasisSpotPerpReq,
|
||||
client: BybitClient = Depends(get_bybit_client),
|
||||
):
|
||||
return await t.get_basis_spot_perp(client, params)
|
||||
|
||||
@r.post("/tools/get_orderbook_imbalance")
|
||||
async def _get_orderbook_imbalance(
|
||||
params: t.OrderbookImbalanceReq,
|
||||
client: BybitClient = Depends(get_bybit_client),
|
||||
):
|
||||
return await t.get_orderbook_imbalance(client, params)
|
||||
|
||||
@r.post("/tools/get_basis_term_structure")
|
||||
async def _get_basis_term_structure(
|
||||
params: t.BasisTermStructureReq,
|
||||
client: BybitClient = Depends(get_bybit_client),
|
||||
):
|
||||
return await t.get_basis_term_structure(client, params)
|
||||
|
||||
# === WRITE tools (richiedono creds per leverage cap / audit) ===
|
||||
|
||||
@r.post("/tools/place_order")
|
||||
async def _place_order(
|
||||
params: t.PlaceOrderReq,
|
||||
request: Request,
|
||||
client: BybitClient = Depends(get_bybit_client),
|
||||
):
|
||||
creds = _build_creds(request)
|
||||
return await audit_call(
|
||||
request=request,
|
||||
exchange="bybit",
|
||||
action="place_order",
|
||||
target_field="symbol",
|
||||
params=params,
|
||||
tool_fn=lambda: t.place_order(client, params, creds=creds),
|
||||
)
|
||||
|
||||
@r.post("/tools/place_combo_order")
|
||||
async def _place_combo_order(
|
||||
params: t.PlaceComboOrderReq,
|
||||
request: Request,
|
||||
client: BybitClient = Depends(get_bybit_client),
|
||||
):
|
||||
creds = _build_creds(request)
|
||||
return await audit_call(
|
||||
request=request,
|
||||
exchange="bybit",
|
||||
action="place_combo_order",
|
||||
params=params,
|
||||
tool_fn=lambda: t.place_combo_order(client, params, creds=creds),
|
||||
)
|
||||
|
||||
@r.post("/tools/amend_order")
|
||||
async def _amend_order(
|
||||
params: t.AmendOrderReq,
|
||||
request: Request,
|
||||
client: BybitClient = Depends(get_bybit_client),
|
||||
):
|
||||
return await audit_call(
|
||||
request=request,
|
||||
exchange="bybit",
|
||||
action="amend_order",
|
||||
target_field="symbol",
|
||||
params=params,
|
||||
tool_fn=lambda: t.amend_order(client, params),
|
||||
)
|
||||
|
||||
@r.post("/tools/cancel_order")
|
||||
async def _cancel_order(
|
||||
params: t.CancelOrderReq,
|
||||
request: Request,
|
||||
client: BybitClient = Depends(get_bybit_client),
|
||||
):
|
||||
return await audit_call(
|
||||
request=request,
|
||||
exchange="bybit",
|
||||
action="cancel_order",
|
||||
target_field="order_id",
|
||||
params=params,
|
||||
tool_fn=lambda: t.cancel_order(client, params),
|
||||
)
|
||||
|
||||
@r.post("/tools/cancel_all_orders")
|
||||
async def _cancel_all_orders(
|
||||
params: t.CancelAllReq,
|
||||
request: Request,
|
||||
client: BybitClient = Depends(get_bybit_client),
|
||||
):
|
||||
return await audit_call(
|
||||
request=request,
|
||||
exchange="bybit",
|
||||
action="cancel_all_orders",
|
||||
target_field="symbol",
|
||||
params=params,
|
||||
tool_fn=lambda: t.cancel_all_orders(client, params),
|
||||
)
|
||||
|
||||
@r.post("/tools/set_stop_loss")
|
||||
async def _set_stop_loss(
|
||||
params: t.SetStopLossReq,
|
||||
request: Request,
|
||||
client: BybitClient = Depends(get_bybit_client),
|
||||
):
|
||||
return await audit_call(
|
||||
request=request,
|
||||
exchange="bybit",
|
||||
action="set_stop_loss",
|
||||
target_field="symbol",
|
||||
params=params,
|
||||
tool_fn=lambda: t.set_stop_loss(client, params),
|
||||
)
|
||||
|
||||
@r.post("/tools/set_take_profit")
|
||||
async def _set_take_profit(
|
||||
params: t.SetTakeProfitReq,
|
||||
request: Request,
|
||||
client: BybitClient = Depends(get_bybit_client),
|
||||
):
|
||||
return await audit_call(
|
||||
request=request,
|
||||
exchange="bybit",
|
||||
action="set_take_profit",
|
||||
target_field="symbol",
|
||||
params=params,
|
||||
tool_fn=lambda: t.set_take_profit(client, params),
|
||||
)
|
||||
|
||||
@r.post("/tools/close_position")
|
||||
async def _close_position(
|
||||
params: t.ClosePositionReq,
|
||||
request: Request,
|
||||
client: BybitClient = Depends(get_bybit_client),
|
||||
):
|
||||
return await audit_call(
|
||||
request=request,
|
||||
exchange="bybit",
|
||||
action="close_position",
|
||||
target_field="symbol",
|
||||
params=params,
|
||||
tool_fn=lambda: t.close_position(client, params),
|
||||
)
|
||||
|
||||
@r.post("/tools/set_leverage")
|
||||
async def _set_leverage(
|
||||
params: t.SetLeverageReq,
|
||||
request: Request,
|
||||
client: BybitClient = Depends(get_bybit_client),
|
||||
):
|
||||
creds = _build_creds(request)
|
||||
return await audit_call(
|
||||
request=request,
|
||||
exchange="bybit",
|
||||
action="set_leverage",
|
||||
target_field="symbol",
|
||||
params=params,
|
||||
tool_fn=lambda: t.set_leverage(client, params, creds=creds),
|
||||
)
|
||||
|
||||
@r.post("/tools/switch_position_mode")
|
||||
async def _switch_position_mode(
|
||||
params: t.SwitchModeReq,
|
||||
request: Request,
|
||||
client: BybitClient = Depends(get_bybit_client),
|
||||
):
|
||||
return await audit_call(
|
||||
request=request,
|
||||
exchange="bybit",
|
||||
action="switch_position_mode",
|
||||
target_field="symbol",
|
||||
params=params,
|
||||
tool_fn=lambda: t.switch_position_mode(client, params),
|
||||
)
|
||||
|
||||
@r.post("/tools/transfer_asset")
|
||||
async def _transfer_asset(
|
||||
params: t.TransferReq,
|
||||
request: Request,
|
||||
client: BybitClient = Depends(get_bybit_client),
|
||||
):
|
||||
return await audit_call(
|
||||
request=request,
|
||||
exchange="bybit",
|
||||
action="transfer_asset",
|
||||
target_field="coin",
|
||||
params=params,
|
||||
tool_fn=lambda: t.transfer_asset(client, params),
|
||||
)
|
||||
|
||||
return r
|
||||
Reference in New Issue
Block a user