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:
@@ -19,4 +19,12 @@ async def build_client(
|
||||
client_secret=settings.deribit.client_secret.get_secret_value(),
|
||||
testnet=(env == "testnet"),
|
||||
)
|
||||
if exchange == "bybit":
|
||||
from cerbero_mcp.exchanges.bybit.client import BybitClient
|
||||
|
||||
return BybitClient(
|
||||
api_key=settings.bybit.api_key,
|
||||
api_secret=settings.bybit.api_secret.get_secret_value(),
|
||||
testnet=(env == "testnet"),
|
||||
)
|
||||
raise ValueError(f"unsupported exchange: {exchange}")
|
||||
|
||||
@@ -0,0 +1,672 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
from cerbero_mcp.common import indicators as ind
|
||||
from cerbero_mcp.common import microstructure as micro
|
||||
from pybit.unified_trading import HTTP
|
||||
|
||||
|
||||
def _f(v: Any) -> float | None:
|
||||
try:
|
||||
return float(v)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _i(v: Any) -> int | None:
|
||||
try:
|
||||
return int(v)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
class BybitClient:
|
||||
def __init__(
|
||||
self,
|
||||
api_key: str,
|
||||
api_secret: str,
|
||||
testnet: bool = True,
|
||||
http: Any | None = None,
|
||||
) -> None:
|
||||
self.api_key = api_key
|
||||
self.api_secret = api_secret
|
||||
self.testnet = testnet
|
||||
self._http = http or HTTP(
|
||||
api_key=api_key,
|
||||
api_secret=api_secret,
|
||||
testnet=testnet,
|
||||
)
|
||||
|
||||
async def _run(self, fn, /, **kwargs):
|
||||
return await asyncio.to_thread(fn, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def _parse_ticker(row: dict) -> dict:
|
||||
return {
|
||||
"symbol": row.get("symbol"),
|
||||
"last_price": _f(row.get("lastPrice")),
|
||||
"mark_price": _f(row.get("markPrice")),
|
||||
"bid": _f(row.get("bid1Price")),
|
||||
"ask": _f(row.get("ask1Price")),
|
||||
"volume_24h": _f(row.get("volume24h")),
|
||||
"turnover_24h": _f(row.get("turnover24h")),
|
||||
"funding_rate": _f(row.get("fundingRate")),
|
||||
"open_interest": _f(row.get("openInterest")),
|
||||
}
|
||||
|
||||
async def get_ticker(self, symbol: str, category: str = "linear") -> dict:
|
||||
resp = await self._run(
|
||||
self._http.get_tickers, category=category, symbol=symbol
|
||||
)
|
||||
rows = (resp.get("result") or {}).get("list") or []
|
||||
if not rows:
|
||||
return {"symbol": symbol, "error": "not_found"}
|
||||
return self._parse_ticker(rows[0])
|
||||
|
||||
async def get_ticker_batch(
|
||||
self, symbols: list[str], category: str = "linear"
|
||||
) -> dict[str, dict]:
|
||||
out: dict[str, dict] = {}
|
||||
for sym in symbols:
|
||||
out[sym] = await self.get_ticker(sym, category=category)
|
||||
return out
|
||||
|
||||
async def get_orderbook(
|
||||
self, symbol: str, category: str = "linear", limit: int = 50
|
||||
) -> dict:
|
||||
resp = await self._run(
|
||||
self._http.get_orderbook, category=category, symbol=symbol, limit=limit
|
||||
)
|
||||
r = resp.get("result") or {}
|
||||
return {
|
||||
"symbol": r.get("s"),
|
||||
"bids": [[float(p), float(q)] for p, q in (r.get("b") or [])],
|
||||
"asks": [[float(p), float(q)] for p, q in (r.get("a") or [])],
|
||||
"timestamp": r.get("ts"),
|
||||
}
|
||||
|
||||
async def get_historical(
|
||||
self,
|
||||
symbol: str,
|
||||
category: str = "linear",
|
||||
interval: str = "60",
|
||||
start: int | None = None,
|
||||
end: int | None = None,
|
||||
limit: int = 1000,
|
||||
) -> dict:
|
||||
kwargs = dict(
|
||||
category=category,
|
||||
symbol=symbol,
|
||||
interval=interval,
|
||||
limit=limit,
|
||||
)
|
||||
if start is not None:
|
||||
kwargs["start"] = start
|
||||
if end is not None:
|
||||
kwargs["end"] = end
|
||||
resp = await self._run(self._http.get_kline, **kwargs)
|
||||
rows = (resp.get("result") or {}).get("list") or []
|
||||
rows_sorted = sorted(rows, key=lambda r: int(r[0]))
|
||||
candles = [
|
||||
{
|
||||
"timestamp": int(r[0]),
|
||||
"open": float(r[1]),
|
||||
"high": float(r[2]),
|
||||
"low": float(r[3]),
|
||||
"close": float(r[4]),
|
||||
"volume": float(r[5]),
|
||||
}
|
||||
for r in rows_sorted
|
||||
]
|
||||
return {"symbol": symbol, "candles": candles}
|
||||
|
||||
async def get_indicators(
|
||||
self,
|
||||
symbol: str,
|
||||
category: str = "linear",
|
||||
indicators: list[str] | None = None,
|
||||
interval: str = "60",
|
||||
start: int | None = None,
|
||||
end: int | None = None,
|
||||
) -> dict:
|
||||
indicators = indicators or ["rsi", "atr", "macd", "adx"]
|
||||
historical = await self.get_historical(
|
||||
symbol, category=category, interval=interval, start=start, end=end
|
||||
)
|
||||
candles = historical.get("candles", [])
|
||||
closes = [c["close"] for c in candles]
|
||||
highs = [c["high"] for c in candles]
|
||||
lows = [c["low"] for c in candles]
|
||||
|
||||
out: dict[str, Any] = {"symbol": symbol, "category": category}
|
||||
for name in indicators:
|
||||
n = name.lower()
|
||||
if n == "sma":
|
||||
out["sma"] = ind.sma(closes, 20)
|
||||
elif n == "rsi":
|
||||
out["rsi"] = ind.rsi(closes)
|
||||
elif n == "atr":
|
||||
out["atr"] = ind.atr(highs, lows, closes)
|
||||
elif n == "macd":
|
||||
out["macd"] = ind.macd(closes)
|
||||
elif n == "adx":
|
||||
out["adx"] = ind.adx(highs, lows, closes)
|
||||
else:
|
||||
out[n] = None
|
||||
return out
|
||||
|
||||
async def get_funding_rate(self, symbol: str, category: str = "linear") -> dict:
|
||||
resp = await self._run(
|
||||
self._http.get_tickers, category=category, symbol=symbol
|
||||
)
|
||||
rows = (resp.get("result") or {}).get("list") or []
|
||||
if not rows:
|
||||
return {"symbol": symbol, "error": "not_found"}
|
||||
row = rows[0]
|
||||
return {
|
||||
"symbol": row.get("symbol"),
|
||||
"funding_rate": _f(row.get("fundingRate")),
|
||||
"next_funding_time": _i(row.get("nextFundingTime")),
|
||||
}
|
||||
|
||||
async def get_funding_history(
|
||||
self, symbol: str, category: str = "linear", limit: int = 100
|
||||
) -> dict:
|
||||
resp = await self._run(
|
||||
self._http.get_funding_rate_history,
|
||||
category=category, symbol=symbol, limit=limit,
|
||||
)
|
||||
rows = (resp.get("result") or {}).get("list") or []
|
||||
hist = [
|
||||
{
|
||||
"timestamp": int(r.get("fundingRateTimestamp", 0)),
|
||||
"rate": float(r.get("fundingRate", 0)),
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
return {"symbol": symbol, "history": hist}
|
||||
|
||||
async def get_open_interest(
|
||||
self,
|
||||
symbol: str,
|
||||
category: str = "linear",
|
||||
interval: str = "5min",
|
||||
limit: int = 288,
|
||||
) -> dict:
|
||||
resp = await self._run(
|
||||
self._http.get_open_interest,
|
||||
category=category, symbol=symbol, intervalTime=interval, limit=limit,
|
||||
)
|
||||
rows = (resp.get("result") or {}).get("list") or []
|
||||
points = [
|
||||
{
|
||||
"timestamp": int(r.get("timestamp", 0)),
|
||||
"oi": float(r.get("openInterest", 0)),
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
current_oi = points[0]["oi"] if points else None
|
||||
return {
|
||||
"symbol": symbol,
|
||||
"category": category,
|
||||
"interval": interval,
|
||||
"current_oi": current_oi,
|
||||
"points": points,
|
||||
}
|
||||
|
||||
async def get_instruments(self, category: str = "linear", symbol: str | None = None) -> dict:
|
||||
kwargs: dict[str, Any] = {"category": category}
|
||||
if symbol:
|
||||
kwargs["symbol"] = symbol
|
||||
resp = await self._run(self._http.get_instruments_info, **kwargs)
|
||||
rows = (resp.get("result") or {}).get("list") or []
|
||||
instruments = []
|
||||
for r in rows:
|
||||
pf = r.get("priceFilter") or {}
|
||||
lf = r.get("lotSizeFilter") or {}
|
||||
instruments.append({
|
||||
"symbol": r.get("symbol"),
|
||||
"status": r.get("status"),
|
||||
"base_coin": r.get("baseCoin"),
|
||||
"quote_coin": r.get("quoteCoin"),
|
||||
"tick_size": _f(pf.get("tickSize")),
|
||||
"qty_step": _f(lf.get("qtyStep")),
|
||||
"min_qty": _f(lf.get("minOrderQty")),
|
||||
})
|
||||
return {"category": category, "instruments": instruments}
|
||||
|
||||
async def get_option_chain(self, base_coin: str, expiry: str | None = None) -> dict:
|
||||
kwargs: dict[str, Any] = {"category": "option", "baseCoin": base_coin.upper()}
|
||||
resp = await self._run(self._http.get_instruments_info, **kwargs)
|
||||
rows = (resp.get("result") or {}).get("list") or []
|
||||
options = []
|
||||
for r in rows:
|
||||
delivery = r.get("deliveryTime")
|
||||
if expiry and expiry not in r.get("symbol", ""):
|
||||
continue
|
||||
options.append({
|
||||
"symbol": r.get("symbol"),
|
||||
"base_coin": r.get("baseCoin"),
|
||||
"settle_coin": r.get("settleCoin"),
|
||||
"type": r.get("optionsType"),
|
||||
"launch_time": int(r.get("launchTime", 0)),
|
||||
"delivery_time": int(delivery) if delivery else None,
|
||||
})
|
||||
return {"base_coin": base_coin.upper(), "options": options}
|
||||
|
||||
async def get_positions(
|
||||
self, category: str = "linear", settle_coin: str = "USDT"
|
||||
) -> list[dict]:
|
||||
kwargs: dict[str, Any] = {"category": category}
|
||||
if category in ("linear", "inverse"):
|
||||
kwargs["settleCoin"] = settle_coin
|
||||
resp = await self._run(self._http.get_positions, **kwargs)
|
||||
rows = (resp.get("result") or {}).get("list") or []
|
||||
out = []
|
||||
for r in rows:
|
||||
out.append({
|
||||
"symbol": r.get("symbol"),
|
||||
"side": r.get("side"),
|
||||
"size": _f(r.get("size")),
|
||||
"entry_price": _f(r.get("avgPrice")),
|
||||
"unrealized_pnl": _f(r.get("unrealisedPnl")),
|
||||
"leverage": _f(r.get("leverage")),
|
||||
"liquidation_price": _f(r.get("liqPrice")),
|
||||
"position_value": _f(r.get("positionValue")),
|
||||
})
|
||||
return out
|
||||
|
||||
async def get_account_summary(self, account_type: str = "UNIFIED") -> dict:
|
||||
resp = await self._run(
|
||||
self._http.get_wallet_balance, accountType=account_type
|
||||
)
|
||||
rows = (resp.get("result") or {}).get("list") or []
|
||||
if not rows:
|
||||
return {"error": "no_account"}
|
||||
a = rows[0]
|
||||
coins = []
|
||||
for c in a.get("coin") or []:
|
||||
coins.append({
|
||||
"coin": c.get("coin"),
|
||||
"wallet_balance": _f(c.get("walletBalance")),
|
||||
"equity": _f(c.get("equity")),
|
||||
})
|
||||
return {
|
||||
"account_type": a.get("accountType"),
|
||||
"equity": _f(a.get("totalEquity")),
|
||||
"wallet_balance": _f(a.get("totalWalletBalance")),
|
||||
"margin_balance": _f(a.get("totalMarginBalance")),
|
||||
"available_balance": _f(a.get("totalAvailableBalance")),
|
||||
"unrealized_pnl": _f(a.get("totalPerpUPL")),
|
||||
"coins": coins,
|
||||
}
|
||||
|
||||
async def get_trade_history(
|
||||
self, category: str = "linear", limit: int = 50
|
||||
) -> list[dict]:
|
||||
resp = await self._run(
|
||||
self._http.get_executions, category=category, limit=limit
|
||||
)
|
||||
rows = (resp.get("result") or {}).get("list") or []
|
||||
return [
|
||||
{
|
||||
"symbol": r.get("symbol"),
|
||||
"side": r.get("side"),
|
||||
"size": _f(r.get("execQty")),
|
||||
"price": _f(r.get("execPrice")),
|
||||
"fee": _f(r.get("execFee")),
|
||||
"timestamp": _i(r.get("execTime")),
|
||||
"order_id": r.get("orderId"),
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
|
||||
async def get_open_orders(
|
||||
self,
|
||||
category: str = "linear",
|
||||
symbol: str | None = None,
|
||||
settle_coin: str = "USDT",
|
||||
) -> list[dict]:
|
||||
kwargs: dict[str, Any] = {"category": category}
|
||||
if category in ("linear", "inverse") and not symbol:
|
||||
kwargs["settleCoin"] = settle_coin
|
||||
if symbol:
|
||||
kwargs["symbol"] = symbol
|
||||
resp = await self._run(self._http.get_open_orders, **kwargs)
|
||||
rows = (resp.get("result") or {}).get("list") or []
|
||||
return [
|
||||
{
|
||||
"order_id": r.get("orderId"),
|
||||
"symbol": r.get("symbol"),
|
||||
"side": r.get("side"),
|
||||
"qty": _f(r.get("qty")),
|
||||
"price": _f(r.get("price")),
|
||||
"type": r.get("orderType"),
|
||||
"status": r.get("orderStatus"),
|
||||
"reduce_only": bool(r.get("reduceOnly")),
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
|
||||
async def get_orderbook_imbalance(
|
||||
self,
|
||||
symbol: str,
|
||||
category: str = "linear",
|
||||
depth: int = 10,
|
||||
) -> dict:
|
||||
"""Microstructure: bid/ask imbalance ratio + microprice + slope."""
|
||||
ob = await self.get_orderbook(symbol=symbol, category=category, limit=max(depth, 50))
|
||||
result = micro.orderbook_imbalance(ob.get("bids") or [], ob.get("asks") or [], depth=depth)
|
||||
return {
|
||||
"symbol": symbol,
|
||||
"category": category,
|
||||
"depth": depth,
|
||||
**result,
|
||||
"timestamp": ob.get("timestamp"),
|
||||
}
|
||||
|
||||
async def get_basis_term_structure(self, asset: str) -> dict:
|
||||
"""Basis curve futures (dated) vs perp + spot. Filtra contratti future
|
||||
BTCUSDT / ETHUSDT con scadenza, calcola annualized basis per ognuno.
|
||||
"""
|
||||
import datetime as _dt
|
||||
|
||||
asset = asset.upper()
|
||||
spot = await self.get_ticker(f"{asset}USDT", category="spot")
|
||||
perp = await self.get_ticker(f"{asset}USDT", category="linear")
|
||||
sp = spot.get("last_price")
|
||||
pp = perp.get("last_price")
|
||||
|
||||
# Lista futures dated (linear/inverse)
|
||||
instr = await self.get_instruments(category="linear")
|
||||
items = (instr.get("instruments") or [])
|
||||
futures = [
|
||||
x for x in items
|
||||
if x.get("symbol", "").startswith(f"{asset}-") or x.get("symbol", "").startswith(f"{asset}USDT-")
|
||||
]
|
||||
|
||||
rows: list[dict[str, Any]] = []
|
||||
if sp:
|
||||
now_ms = int(_dt.datetime.now(_dt.UTC).timestamp() * 1000)
|
||||
for f in futures[:10]:
|
||||
tk = await self.get_ticker(f["symbol"], category="linear")
|
||||
fp = tk.get("last_price")
|
||||
expiry_ms = f.get("delivery_time")
|
||||
if not fp or not expiry_ms:
|
||||
continue
|
||||
days = max((int(expiry_ms) - now_ms) / 86_400_000, 1)
|
||||
basis_pct = 100.0 * (fp - sp) / sp
|
||||
annualized = basis_pct * 365.0 / days
|
||||
rows.append({
|
||||
"symbol": f["symbol"],
|
||||
"expiry_ms": int(expiry_ms),
|
||||
"days_to_expiry": round(days, 2),
|
||||
"future_price": fp,
|
||||
"basis_pct": round(basis_pct, 4),
|
||||
"annualized_basis_pct": round(annualized, 4),
|
||||
})
|
||||
|
||||
rows.sort(key=lambda r: r["days_to_expiry"])
|
||||
return {
|
||||
"asset": asset,
|
||||
"spot_price": sp,
|
||||
"perp_price": pp,
|
||||
"perp_basis_pct": round(100.0 * (pp - sp) / sp, 4) if (sp and pp) else None,
|
||||
"term_structure": rows,
|
||||
"data_timestamp": _dt.datetime.now(_dt.UTC).isoformat(),
|
||||
}
|
||||
|
||||
async def get_basis_spot_perp(self, asset: str) -> dict:
|
||||
asset = asset.upper()
|
||||
symbol = f"{asset}USDT"
|
||||
spot = await self.get_ticker(symbol, category="spot")
|
||||
perp = await self.get_ticker(symbol, category="linear")
|
||||
sp = spot.get("last_price")
|
||||
pp = perp.get("last_price")
|
||||
basis_abs = basis_pct = None
|
||||
if sp and pp:
|
||||
basis_abs = pp - sp
|
||||
basis_pct = 100.0 * basis_abs / sp
|
||||
return {
|
||||
"asset": asset,
|
||||
"symbol": symbol,
|
||||
"spot_price": sp,
|
||||
"perp_price": pp,
|
||||
"basis_abs": basis_abs,
|
||||
"basis_pct": basis_pct,
|
||||
"funding_rate": perp.get("funding_rate"),
|
||||
}
|
||||
|
||||
def _envelope(self, resp: dict, payload: dict) -> dict:
|
||||
code = resp.get("retCode", 0)
|
||||
if code != 0:
|
||||
return {"error": resp.get("retMsg", "bybit_error"), "code": code}
|
||||
return payload
|
||||
|
||||
async def place_order(
|
||||
self,
|
||||
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,
|
||||
) -> dict:
|
||||
kwargs: dict[str, Any] = {
|
||||
"category": category,
|
||||
"symbol": symbol,
|
||||
"side": side,
|
||||
"qty": str(qty),
|
||||
"orderType": order_type,
|
||||
"timeInForce": tif,
|
||||
"reduceOnly": reduce_only,
|
||||
}
|
||||
if price is not None:
|
||||
kwargs["price"] = str(price)
|
||||
if position_idx is not None:
|
||||
kwargs["positionIdx"] = position_idx
|
||||
if category == "option":
|
||||
import uuid
|
||||
kwargs["orderLinkId"] = f"cerbero-{uuid.uuid4().hex[:16]}"
|
||||
resp = await self._run(self._http.place_order, **kwargs)
|
||||
r = resp.get("result") or {}
|
||||
return self._envelope(resp, {
|
||||
"order_id": r.get("orderId"),
|
||||
"order_link_id": r.get("orderLinkId"),
|
||||
"status": "submitted",
|
||||
})
|
||||
|
||||
async def place_combo_order(
|
||||
self,
|
||||
category: str,
|
||||
legs: list[dict[str, Any]],
|
||||
) -> dict:
|
||||
"""Atomic multi-leg via /v5/order/create-batch (Bybit option only).
|
||||
|
||||
Bybit supporta batch_order solo su category='option'. Per perp/linear
|
||||
usare loop di place_order (non atomic).
|
||||
|
||||
legs: [{symbol, side, qty, order_type, price?, tif?, reduce_only?}].
|
||||
"""
|
||||
if category != "option":
|
||||
raise ValueError("place_combo_order: Bybit batch_order è disponibile solo su category='option'")
|
||||
if len(legs) < 2:
|
||||
raise ValueError("combo requires at least 2 legs")
|
||||
|
||||
import uuid
|
||||
request: list[dict[str, Any]] = []
|
||||
for leg in legs:
|
||||
entry: dict[str, Any] = {
|
||||
"symbol": leg["symbol"],
|
||||
"side": leg["side"],
|
||||
"qty": str(leg["qty"]),
|
||||
"orderType": leg.get("order_type", "Limit"),
|
||||
"timeInForce": leg.get("tif", "GTC"),
|
||||
"reduceOnly": leg.get("reduce_only", False),
|
||||
"orderLinkId": f"cerbero-{uuid.uuid4().hex[:16]}",
|
||||
}
|
||||
if leg.get("price") is not None:
|
||||
entry["price"] = str(leg["price"])
|
||||
request.append(entry)
|
||||
|
||||
resp = await self._run(self._http.place_batch_order, category=category, request=request)
|
||||
result_list = (resp.get("result") or {}).get("list") or []
|
||||
orders = [
|
||||
{
|
||||
"order_id": r.get("orderId"),
|
||||
"order_link_id": r.get("orderLinkId"),
|
||||
"status": "submitted",
|
||||
}
|
||||
for r in result_list
|
||||
]
|
||||
return self._envelope(resp, {"orders": orders})
|
||||
|
||||
async def amend_order(
|
||||
self,
|
||||
category: str,
|
||||
symbol: str,
|
||||
order_id: str,
|
||||
new_qty: float | None = None,
|
||||
new_price: float | None = None,
|
||||
) -> dict:
|
||||
kwargs: dict[str, Any] = {
|
||||
"category": category,
|
||||
"symbol": symbol,
|
||||
"orderId": order_id,
|
||||
}
|
||||
if new_qty is not None:
|
||||
kwargs["qty"] = str(new_qty)
|
||||
if new_price is not None:
|
||||
kwargs["price"] = str(new_price)
|
||||
resp = await self._run(self._http.amend_order, **kwargs)
|
||||
r = resp.get("result") or {}
|
||||
return self._envelope(resp, {
|
||||
"order_id": r.get("orderId", order_id),
|
||||
"status": "amended",
|
||||
})
|
||||
|
||||
async def cancel_order(
|
||||
self, category: str, symbol: str, order_id: str
|
||||
) -> dict:
|
||||
resp = await self._run(
|
||||
self._http.cancel_order,
|
||||
category=category, symbol=symbol, orderId=order_id,
|
||||
)
|
||||
r = resp.get("result") or {}
|
||||
return self._envelope(resp, {
|
||||
"order_id": r.get("orderId", order_id),
|
||||
"status": "cancelled",
|
||||
})
|
||||
|
||||
async def cancel_all_orders(
|
||||
self, category: str, symbol: str | None = None
|
||||
) -> dict:
|
||||
kwargs: dict[str, Any] = {"category": category}
|
||||
if symbol:
|
||||
kwargs["symbol"] = symbol
|
||||
resp = await self._run(self._http.cancel_all_orders, **kwargs)
|
||||
r = resp.get("result") or {}
|
||||
ids = [x.get("orderId") for x in (r.get("list") or [])]
|
||||
return self._envelope(resp, {
|
||||
"cancelled_ids": ids,
|
||||
"count": len(ids),
|
||||
})
|
||||
|
||||
async def set_stop_loss(
|
||||
self, category: str, symbol: str, stop_loss: float,
|
||||
position_idx: int = 0,
|
||||
) -> dict:
|
||||
resp = await self._run(
|
||||
self._http.set_trading_stop,
|
||||
category=category, symbol=symbol,
|
||||
stopLoss=str(stop_loss), positionIdx=position_idx,
|
||||
)
|
||||
return self._envelope(resp, {
|
||||
"symbol": symbol, "stop_loss": stop_loss,
|
||||
"status": "stop_loss_set",
|
||||
})
|
||||
|
||||
async def set_take_profit(
|
||||
self, category: str, symbol: str, take_profit: float,
|
||||
position_idx: int = 0,
|
||||
) -> dict:
|
||||
resp = await self._run(
|
||||
self._http.set_trading_stop,
|
||||
category=category, symbol=symbol,
|
||||
takeProfit=str(take_profit), positionIdx=position_idx,
|
||||
)
|
||||
return self._envelope(resp, {
|
||||
"symbol": symbol, "take_profit": take_profit,
|
||||
"status": "take_profit_set",
|
||||
})
|
||||
|
||||
async def close_position(self, category: str, symbol: str) -> dict:
|
||||
positions = await self.get_positions(category=category)
|
||||
target = next((p for p in positions if p["symbol"] == symbol and (p["size"] or 0) > 0), None)
|
||||
if not target:
|
||||
return {"error": "no_open_position", "symbol": symbol}
|
||||
close_side = "Sell" if target["side"] == "Buy" else "Buy"
|
||||
return await self.place_order(
|
||||
category=category,
|
||||
symbol=symbol,
|
||||
side=close_side,
|
||||
qty=target["size"],
|
||||
order_type="Market",
|
||||
reduce_only=True,
|
||||
tif="IOC",
|
||||
)
|
||||
|
||||
async def set_leverage(
|
||||
self, category: str, symbol: str, leverage: int
|
||||
) -> dict:
|
||||
resp = await self._run(
|
||||
self._http.set_leverage,
|
||||
category=category, symbol=symbol,
|
||||
buyLeverage=str(leverage), sellLeverage=str(leverage),
|
||||
)
|
||||
return self._envelope(resp, {
|
||||
"symbol": symbol, "leverage": leverage,
|
||||
"status": "leverage_set",
|
||||
})
|
||||
|
||||
async def switch_position_mode(
|
||||
self, category: str, symbol: str, mode: str
|
||||
) -> dict:
|
||||
mode_code = 3 if mode.lower() == "hedge" else 0
|
||||
resp = await self._run(
|
||||
self._http.switch_position_mode,
|
||||
category=category, symbol=symbol, mode=mode_code,
|
||||
)
|
||||
return self._envelope(resp, {
|
||||
"symbol": symbol, "mode": mode,
|
||||
"status": "mode_switched",
|
||||
})
|
||||
|
||||
async def transfer_asset(
|
||||
self,
|
||||
coin: str,
|
||||
amount: float,
|
||||
from_type: str,
|
||||
to_type: str,
|
||||
) -> dict:
|
||||
import uuid
|
||||
resp = await self._run(
|
||||
self._http.create_internal_transfer,
|
||||
transferId=str(uuid.uuid4()),
|
||||
coin=coin,
|
||||
amount=str(amount),
|
||||
fromAccountType=from_type,
|
||||
toAccountType=to_type,
|
||||
)
|
||||
r = resp.get("result") or {}
|
||||
return self._envelope(resp, {
|
||||
"transfer_id": r.get("transferId"),
|
||||
"coin": coin,
|
||||
"amount": amount,
|
||||
"status": "submitted",
|
||||
})
|
||||
@@ -0,0 +1,56 @@
|
||||
"""Leverage cap server-side per place_order.
|
||||
|
||||
Cap letto dal secret JSON via campo `max_leverage`. Default 1 (cash) se assente.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
|
||||
def get_max_leverage(creds: dict) -> int:
|
||||
"""Legge max_leverage dal secret. Default 1 se mancante."""
|
||||
raw = creds.get("max_leverage", 1)
|
||||
try:
|
||||
value = int(raw)
|
||||
except (TypeError, ValueError):
|
||||
value = 1
|
||||
return max(1, value)
|
||||
|
||||
|
||||
def enforce_leverage(
|
||||
requested: int | float | None,
|
||||
*,
|
||||
creds: dict,
|
||||
exchange: str,
|
||||
) -> int:
|
||||
"""Verifica e applica leverage cap. Ritorna leverage applicabile.
|
||||
|
||||
Solleva HTTPException(403, LEVERAGE_CAP_EXCEEDED) se requested > cap.
|
||||
Se requested is None, applica il cap come default.
|
||||
"""
|
||||
cap = get_max_leverage(creds)
|
||||
if requested is None:
|
||||
return cap
|
||||
lev = int(requested)
|
||||
if lev < 1:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail={
|
||||
"error": "LEVERAGE_CAP_EXCEEDED",
|
||||
"exchange": exchange,
|
||||
"requested": lev,
|
||||
"max": cap,
|
||||
"reason": "leverage must be >= 1",
|
||||
},
|
||||
)
|
||||
if lev > cap:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail={
|
||||
"error": "LEVERAGE_CAP_EXCEEDED",
|
||||
"exchange": exchange,
|
||||
"requested": lev,
|
||||
"max": cap,
|
||||
},
|
||||
)
|
||||
return lev
|
||||
@@ -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
|
||||
@@ -0,0 +1,261 @@
|
||||
"""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
|
||||
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
|
||||
from cerbero_mcp.client_registry import ClientRegistry
|
||||
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 request.state.environment
|
||||
|
||||
|
||||
async def get_bybit_client(
|
||||
request: Request, env: Environment = Depends(get_environment)
|
||||
) -> BybitClient:
|
||||
registry: ClientRegistry = request.app.state.registry
|
||||
return 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 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 t.place_combo_order(client, params, creds=creds)
|
||||
|
||||
@r.post("/tools/amend_order")
|
||||
async def _amend_order(
|
||||
params: t.AmendOrderReq,
|
||||
client: BybitClient = Depends(get_bybit_client),
|
||||
):
|
||||
return await t.amend_order(client, params)
|
||||
|
||||
@r.post("/tools/cancel_order")
|
||||
async def _cancel_order(
|
||||
params: t.CancelOrderReq,
|
||||
client: BybitClient = Depends(get_bybit_client),
|
||||
):
|
||||
return await t.cancel_order(client, params)
|
||||
|
||||
@r.post("/tools/cancel_all_orders")
|
||||
async def _cancel_all_orders(
|
||||
params: t.CancelAllReq,
|
||||
client: BybitClient = Depends(get_bybit_client),
|
||||
):
|
||||
return await t.cancel_all_orders(client, params)
|
||||
|
||||
@r.post("/tools/set_stop_loss")
|
||||
async def _set_stop_loss(
|
||||
params: t.SetStopLossReq,
|
||||
client: BybitClient = Depends(get_bybit_client),
|
||||
):
|
||||
return await t.set_stop_loss(client, params)
|
||||
|
||||
@r.post("/tools/set_take_profit")
|
||||
async def _set_take_profit(
|
||||
params: t.SetTakeProfitReq,
|
||||
client: BybitClient = Depends(get_bybit_client),
|
||||
):
|
||||
return await t.set_take_profit(client, params)
|
||||
|
||||
@r.post("/tools/close_position")
|
||||
async def _close_position(
|
||||
params: t.ClosePositionReq,
|
||||
client: BybitClient = Depends(get_bybit_client),
|
||||
):
|
||||
return await 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 t.set_leverage(client, params, creds=creds)
|
||||
|
||||
@r.post("/tools/switch_position_mode")
|
||||
async def _switch_position_mode(
|
||||
params: t.SwitchModeReq,
|
||||
client: BybitClient = Depends(get_bybit_client),
|
||||
):
|
||||
return await t.switch_position_mode(client, params)
|
||||
|
||||
@r.post("/tools/transfer_asset")
|
||||
async def _transfer_asset(
|
||||
params: t.TransferReq,
|
||||
client: BybitClient = Depends(get_bybit_client),
|
||||
):
|
||||
return await t.transfer_asset(client, params)
|
||||
|
||||
return r
|
||||
Reference in New Issue
Block a user