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:
@@ -423,6 +423,64 @@ async def test_place_order_linear_no_link_id(client, mock_http):
|
||||
assert "orderLinkId" not in kwargs
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_place_combo_order_batch_option(client, mock_http):
|
||||
"""Combo order via place_batch_order su category=option (atomic, 1 round-trip)."""
|
||||
mock_http.place_batch_order.return_value = {
|
||||
"retCode": 0,
|
||||
"result": {
|
||||
"list": [
|
||||
{"orderId": "ord-1", "orderLinkId": "cerbero-leg1"},
|
||||
{"orderId": "ord-2", "orderLinkId": "cerbero-leg2"},
|
||||
]
|
||||
},
|
||||
}
|
||||
legs = [
|
||||
{"symbol": "BTC-30APR26-75000-C-USDT", "side": "Buy", "qty": 0.01, "order_type": "Limit", "price": 5.0},
|
||||
{"symbol": "BTC-30APR26-80000-C-USDT", "side": "Sell", "qty": 0.01, "order_type": "Limit", "price": 3.0},
|
||||
]
|
||||
out = await client.place_combo_order(category="option", legs=legs)
|
||||
assert len(out["orders"]) == 2
|
||||
assert out["orders"][0]["order_id"] == "ord-1"
|
||||
kwargs = mock_http.place_batch_order.call_args.kwargs
|
||||
assert kwargs["category"] == "option"
|
||||
request = kwargs["request"]
|
||||
assert len(request) == 2
|
||||
assert request[0]["symbol"] == "BTC-30APR26-75000-C-USDT"
|
||||
assert request[0]["qty"] == "0.01"
|
||||
assert request[0]["orderType"] == "Limit"
|
||||
# CER: orderLinkId obbligatorio per option
|
||||
assert "orderLinkId" in request[0]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_place_combo_order_error(client, mock_http):
|
||||
mock_http.place_batch_order.return_value = {"retCode": 10001, "retMsg": "invalid leg"}
|
||||
out = await client.place_combo_order(
|
||||
category="option",
|
||||
legs=[
|
||||
{"symbol": "X", "side": "Buy", "qty": 1, "order_type": "Limit", "price": 1.0},
|
||||
{"symbol": "Y", "side": "Sell", "qty": 1, "order_type": "Limit", "price": 1.0},
|
||||
],
|
||||
)
|
||||
assert out["error"] == "invalid leg"
|
||||
assert out["code"] == 10001
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_place_combo_order_rejects_non_option(client, mock_http):
|
||||
"""Bybit batch_order è disponibile solo su option category."""
|
||||
import pytest as _pytest
|
||||
with _pytest.raises(ValueError, match="option"):
|
||||
await client.place_combo_order(
|
||||
category="linear",
|
||||
legs=[
|
||||
{"symbol": "BTCUSDT", "side": "Buy", "qty": 0.01, "order_type": "Market"},
|
||||
{"symbol": "ETHUSDT", "side": "Sell", "qty": 0.01, "order_type": "Market"},
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cancel_order(client, mock_http):
|
||||
mock_http.cancel_order.return_value = {"retCode": 0, "result": {"orderId": "ord1"}}
|
||||
|
||||
Reference in New Issue
Block a user