feat(mcp-deribit,mcp-bybit): add place_combo_order

Deribit: private/create_combo + place_order sul combo instrument → una
sola crociata di spread invece di N (slippage atteso ridotto su
strutture liquide). ACL core + leverage cap su tutti i leg.

Bybit: place_batch_order su category=option (atomic multi-leg, 1
round-trip API). Reject su category != option (perp/linear non
supportano batch nativo). orderLinkId auto-generato per leg.

Tutti i test: deribit 48/48, bybit 123/123.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
AdrianoDev
2026-04-27 23:12:09 +02:00
parent bacd5aab33
commit c2fd8330ca
9 changed files with 358 additions and 1 deletions
@@ -412,6 +412,51 @@ class BybitClient:
"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,
+25 -1
View File
@@ -7,7 +7,7 @@ from mcp_common.auth import Principal, TokenStore, require_principal
from mcp_common.environment import EnvironmentInfo
from mcp_common.mcp_bridge import mount_mcp_endpoint
from mcp_common.server import build_app
from pydantic import BaseModel
from pydantic import BaseModel, Field
from mcp_bybit.client import BybitClient
from mcp_bybit.leverage_cap import enforce_leverage as _enforce_leverage
@@ -114,6 +114,21 @@ class PlaceOrderReq(BaseModel):
position_idx: int | None = None
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
@@ -306,6 +321,14 @@ def create_app(
body.order_type, body.price, body.tif, body.reduce_only, body.position_idx,
)
@app.post("/tools/place_combo_order", tags=["writes"])
async def t_place_combo_order(body: PlaceComboOrderReq, principal: Principal = Depends(require_principal)):
_check(principal, core=True)
return await client.place_combo_order(
category=body.category,
legs=[leg.model_dump() for leg in body.legs],
)
@app.post("/tools/amend_order", tags=["writes"])
async def t_amend_order(body: AmendOrderReq, principal: Principal = Depends(require_principal)):
_check(principal, core=True)
@@ -381,6 +404,7 @@ def create_app(
{"name": "get_open_orders", "description": "Ordini pending."},
{"name": "get_basis_spot_perp", "description": "Basis spot vs linear perp."},
{"name": "place_order", "description": "Invia ordine (CORE only)."},
{"name": "place_combo_order", "description": "Multi-leg atomico via place_batch_order (solo category=option)."},
{"name": "amend_order", "description": "Modifica ordine esistente."},
{"name": "cancel_order", "description": "Cancella ordine."},
{"name": "cancel_all_orders", "description": "Cancella tutti ordini."},