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,21 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from cerbero_mcp.exchanges.bybit.client import BybitClient
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_http():
|
||||
return MagicMock(name="pybit_HTTP")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(mock_http):
|
||||
return BybitClient(
|
||||
api_key="test_key",
|
||||
api_secret="test_secret",
|
||||
testnet=True,
|
||||
http=mock_http,
|
||||
)
|
||||
@@ -0,0 +1,588 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from cerbero_mcp.exchanges.bybit.client import BybitClient
|
||||
|
||||
|
||||
def test_client_init_stores_attrs(client, mock_http):
|
||||
assert client.testnet is True
|
||||
assert client._http is mock_http
|
||||
|
||||
|
||||
def test_client_init_default_http(monkeypatch):
|
||||
created = {}
|
||||
|
||||
class FakeHTTP:
|
||||
def __init__(self, **kwargs):
|
||||
created.update(kwargs)
|
||||
|
||||
monkeypatch.setattr("cerbero_mcp.exchanges.bybit.client.HTTP", FakeHTTP)
|
||||
BybitClient(api_key="k", api_secret="s", testnet=False)
|
||||
assert created["api_key"] == "k"
|
||||
assert created["api_secret"] == "s"
|
||||
assert created["testnet"] is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_ticker(client, mock_http):
|
||||
mock_http.get_tickers.return_value = {
|
||||
"retCode": 0,
|
||||
"result": {
|
||||
"list": [{
|
||||
"symbol": "BTCUSDT",
|
||||
"lastPrice": "60000",
|
||||
"markPrice": "60010",
|
||||
"bid1Price": "59995",
|
||||
"ask1Price": "60005",
|
||||
"volume24h": "1500.5",
|
||||
"turnover24h": "90000000",
|
||||
"fundingRate": "0.0001",
|
||||
"openInterest": "50000",
|
||||
}]
|
||||
},
|
||||
}
|
||||
t = await client.get_ticker("BTCUSDT", category="linear")
|
||||
mock_http.get_tickers.assert_called_once_with(category="linear", symbol="BTCUSDT")
|
||||
assert t["symbol"] == "BTCUSDT"
|
||||
assert t["last_price"] == 60000.0
|
||||
assert t["mark_price"] == 60010.0
|
||||
assert t["bid"] == 59995.0
|
||||
assert t["ask"] == 60005.0
|
||||
assert t["volume_24h"] == 1500.5
|
||||
assert t["funding_rate"] == 0.0001
|
||||
assert t["open_interest"] == 50000.0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_ticker_batch(client, mock_http):
|
||||
def side_effect(**kwargs):
|
||||
symbol = kwargs["symbol"]
|
||||
return {"retCode": 0, "result": {"list": [{
|
||||
"symbol": symbol, "lastPrice": "1", "markPrice": "1",
|
||||
"bid1Price": "1", "ask1Price": "1", "volume24h": "0",
|
||||
"turnover24h": "0", "fundingRate": "0", "openInterest": "0",
|
||||
}]}}
|
||||
mock_http.get_tickers.side_effect = side_effect
|
||||
out = await client.get_ticker_batch(["BTCUSDT", "ETHUSDT"], category="linear")
|
||||
assert set(out.keys()) == {"BTCUSDT", "ETHUSDT"}
|
||||
assert mock_http.get_tickers.call_count == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_ticker_not_found(client, mock_http):
|
||||
mock_http.get_tickers.return_value = {"retCode": 0, "result": {"list": []}}
|
||||
t = await client.get_ticker("UNKNOWNUSDT", category="linear")
|
||||
assert t == {"symbol": "UNKNOWNUSDT", "error": "not_found"}
|
||||
|
||||
|
||||
def test_parse_helpers():
|
||||
from cerbero_mcp.exchanges.bybit.client import _f, _i
|
||||
assert _f("1.5") == 1.5
|
||||
assert _f("") is None
|
||||
assert _f(None) is None
|
||||
assert _i("42") == 42
|
||||
assert _i("") is None
|
||||
assert _i(None) is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_orderbook(client, mock_http):
|
||||
mock_http.get_orderbook.return_value = {
|
||||
"retCode": 0,
|
||||
"result": {
|
||||
"s": "BTCUSDT",
|
||||
"b": [["59990", "0.5"], ["59980", "1.0"]],
|
||||
"a": [["60010", "0.3"], ["60020", "0.7"]],
|
||||
"ts": 1700000000000,
|
||||
},
|
||||
}
|
||||
ob = await client.get_orderbook("BTCUSDT", category="linear", limit=25)
|
||||
mock_http.get_orderbook.assert_called_once_with(
|
||||
category="linear", symbol="BTCUSDT", limit=25
|
||||
)
|
||||
assert ob["symbol"] == "BTCUSDT"
|
||||
assert ob["bids"] == [[59990.0, 0.5], [59980.0, 1.0]]
|
||||
assert ob["asks"] == [[60010.0, 0.3], [60020.0, 0.7]]
|
||||
assert ob["timestamp"] == 1700000000000
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_historical(client, mock_http):
|
||||
mock_http.get_kline.return_value = {
|
||||
"retCode": 0,
|
||||
"result": {
|
||||
"list": [
|
||||
["1700000000000", "60000", "60500", "59500", "60200", "100", "6020000"],
|
||||
["1700003600000", "60200", "60700", "60000", "60400", "80", "4832000"],
|
||||
]
|
||||
},
|
||||
}
|
||||
out = await client.get_historical(
|
||||
"BTCUSDT", category="linear", interval="60",
|
||||
start=1700000000000, end=1700003600000,
|
||||
)
|
||||
mock_http.get_kline.assert_called_once_with(
|
||||
category="linear", symbol="BTCUSDT", interval="60",
|
||||
start=1700000000000, end=1700003600000, limit=1000,
|
||||
)
|
||||
assert len(out["candles"]) == 2
|
||||
c0 = out["candles"][0]
|
||||
assert c0["timestamp"] == 1700000000000
|
||||
assert c0["open"] == 60000.0
|
||||
assert c0["high"] == 60500.0
|
||||
assert c0["low"] == 59500.0
|
||||
assert c0["close"] == 60200.0
|
||||
assert c0["volume"] == 100.0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_indicators(client, mock_http):
|
||||
rows = [
|
||||
[str(1700000000000 + i * 3600_000),
|
||||
str(60000 + i * 10), str(60000 + i * 10 + 5),
|
||||
str(60000 + i * 10 - 5), str(60000 + i * 10 + 2),
|
||||
"100", "6000000"]
|
||||
for i in range(35)
|
||||
]
|
||||
mock_http.get_kline.return_value = {"retCode": 0, "result": {"list": rows}}
|
||||
out = await client.get_indicators(
|
||||
"BTCUSDT", category="linear",
|
||||
indicators=["rsi", "atr", "macd", "adx"],
|
||||
interval="60",
|
||||
)
|
||||
assert "rsi" in out and out["rsi"] is not None
|
||||
assert "atr" in out and out["atr"] is not None
|
||||
assert "macd" in out and out["macd"]["macd"] is not None
|
||||
assert "adx" in out and out["adx"]["adx"] is not None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_funding_rate(client, mock_http):
|
||||
mock_http.get_tickers.return_value = {
|
||||
"retCode": 0,
|
||||
"result": {"list": [{
|
||||
"symbol": "BTCUSDT", "fundingRate": "0.0001",
|
||||
"nextFundingTime": "1700003600000",
|
||||
"lastPrice": "60000", "markPrice": "60000",
|
||||
"bid1Price": "0", "ask1Price": "0",
|
||||
"volume24h": "0", "turnover24h": "0", "openInterest": "0",
|
||||
}]},
|
||||
}
|
||||
out = await client.get_funding_rate("BTCUSDT", category="linear")
|
||||
assert out["symbol"] == "BTCUSDT"
|
||||
assert out["funding_rate"] == 0.0001
|
||||
assert out["next_funding_time"] == 1700003600000
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_funding_history(client, mock_http):
|
||||
mock_http.get_funding_rate_history.return_value = {
|
||||
"retCode": 0,
|
||||
"result": {"list": [
|
||||
{"symbol": "BTCUSDT", "fundingRate": "0.0001", "fundingRateTimestamp": "1700000000000"},
|
||||
{"symbol": "BTCUSDT", "fundingRate": "0.00008", "fundingRateTimestamp": "1699996400000"},
|
||||
]},
|
||||
}
|
||||
out = await client.get_funding_history("BTCUSDT", category="linear", limit=50)
|
||||
mock_http.get_funding_rate_history.assert_called_once_with(
|
||||
category="linear", symbol="BTCUSDT", limit=50
|
||||
)
|
||||
assert len(out["history"]) == 2
|
||||
assert out["history"][0]["rate"] == 0.0001
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_open_interest(client, mock_http):
|
||||
mock_http.get_open_interest.return_value = {
|
||||
"retCode": 0,
|
||||
"result": {"list": [
|
||||
{"openInterest": "50000", "timestamp": "1700000000000"},
|
||||
{"openInterest": "49000", "timestamp": "1699996400000"},
|
||||
]},
|
||||
}
|
||||
out = await client.get_open_interest("BTCUSDT", category="linear", interval="5min", limit=100)
|
||||
mock_http.get_open_interest.assert_called_once_with(
|
||||
category="linear", symbol="BTCUSDT", intervalTime="5min", limit=100
|
||||
)
|
||||
assert len(out["points"]) == 2
|
||||
assert out["current_oi"] == 50000.0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_instruments(client, mock_http):
|
||||
mock_http.get_instruments_info.return_value = {
|
||||
"retCode": 0,
|
||||
"result": {"list": [
|
||||
{"symbol": "BTCUSDT", "status": "Trading", "baseCoin": "BTC",
|
||||
"quoteCoin": "USDT", "priceFilter": {"tickSize": "0.1"},
|
||||
"lotSizeFilter": {"qtyStep": "0.001", "minOrderQty": "0.001"}},
|
||||
]},
|
||||
}
|
||||
out = await client.get_instruments(category="linear")
|
||||
mock_http.get_instruments_info.assert_called_once_with(category="linear")
|
||||
assert len(out["instruments"]) == 1
|
||||
inst = out["instruments"][0]
|
||||
assert inst["symbol"] == "BTCUSDT"
|
||||
assert inst["tick_size"] == 0.1
|
||||
assert inst["qty_step"] == 0.001
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_option_chain(client, mock_http):
|
||||
mock_http.get_instruments_info.return_value = {
|
||||
"retCode": 0,
|
||||
"result": {"list": [
|
||||
{"symbol": "BTC-30JUN25-50000-C", "baseCoin": "BTC",
|
||||
"settleCoin": "USDC", "optionsType": "Call",
|
||||
"launchTime": "1700000000000", "deliveryTime": "1719734400000"},
|
||||
{"symbol": "BTC-30JUN25-50000-P", "baseCoin": "BTC",
|
||||
"settleCoin": "USDC", "optionsType": "Put",
|
||||
"launchTime": "1700000000000", "deliveryTime": "1719734400000"},
|
||||
]},
|
||||
}
|
||||
out = await client.get_option_chain(base_coin="BTC")
|
||||
mock_http.get_instruments_info.assert_called_once_with(category="option", baseCoin="BTC")
|
||||
assert len(out["options"]) == 2
|
||||
assert out["options"][0]["type"] == "Call"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_positions(client, mock_http):
|
||||
mock_http.get_positions.return_value = {
|
||||
"retCode": 0,
|
||||
"result": {"list": [
|
||||
{"symbol": "BTCUSDT", "side": "Buy", "size": "0.1",
|
||||
"avgPrice": "60000", "unrealisedPnl": "50",
|
||||
"leverage": "10", "liqPrice": "50000", "positionValue": "6000"},
|
||||
]},
|
||||
}
|
||||
out = await client.get_positions(category="linear")
|
||||
mock_http.get_positions.assert_called_once_with(category="linear", settleCoin="USDT")
|
||||
assert len(out) == 1
|
||||
p = out[0]
|
||||
assert p["symbol"] == "BTCUSDT"
|
||||
assert p["side"] == "Buy"
|
||||
assert p["size"] == 0.1
|
||||
assert p["entry_price"] == 60000.0
|
||||
assert p["liquidation_price"] == 50000.0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_account_summary(client, mock_http):
|
||||
mock_http.get_wallet_balance.return_value = {
|
||||
"retCode": 0,
|
||||
"result": {"list": [{
|
||||
"accountType": "UNIFIED",
|
||||
"totalEquity": "10000",
|
||||
"totalWalletBalance": "9500",
|
||||
"totalMarginBalance": "9800",
|
||||
"totalAvailableBalance": "9000",
|
||||
"totalPerpUPL": "200",
|
||||
"coin": [
|
||||
{"coin": "USDT", "walletBalance": "9500", "equity": "9700"}
|
||||
],
|
||||
}]},
|
||||
}
|
||||
out = await client.get_account_summary()
|
||||
mock_http.get_wallet_balance.assert_called_once_with(accountType="UNIFIED")
|
||||
assert out["equity"] == 10000.0
|
||||
assert out["available_balance"] == 9000.0
|
||||
assert out["unrealized_pnl"] == 200.0
|
||||
assert len(out["coins"]) == 1
|
||||
assert out["coins"][0]["coin"] == "USDT"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_trade_history(client, mock_http):
|
||||
mock_http.get_executions.return_value = {
|
||||
"retCode": 0,
|
||||
"result": {"list": [
|
||||
{"symbol": "BTCUSDT", "side": "Buy", "execQty": "0.01",
|
||||
"execPrice": "60000", "execFee": "0.1",
|
||||
"execTime": "1700000000000", "orderId": "abc"},
|
||||
]},
|
||||
}
|
||||
out = await client.get_trade_history(category="linear", limit=50)
|
||||
mock_http.get_executions.assert_called_once_with(category="linear", limit=50)
|
||||
assert len(out) == 1
|
||||
assert out[0]["symbol"] == "BTCUSDT"
|
||||
assert out[0]["size"] == 0.01
|
||||
assert out[0]["price"] == 60000.0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_open_orders(client, mock_http):
|
||||
mock_http.get_open_orders.return_value = {
|
||||
"retCode": 0,
|
||||
"result": {"list": [
|
||||
{"symbol": "BTCUSDT", "orderId": "o1", "side": "Buy",
|
||||
"qty": "0.1", "price": "59000", "orderType": "Limit",
|
||||
"orderStatus": "New", "reduceOnly": False},
|
||||
]},
|
||||
}
|
||||
out = await client.get_open_orders(category="linear")
|
||||
mock_http.get_open_orders.assert_called_once_with(category="linear", settleCoin="USDT")
|
||||
assert len(out) == 1
|
||||
assert out[0]["order_id"] == "o1"
|
||||
assert out[0]["price"] == 59000.0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_basis_spot_perp(client, mock_http):
|
||||
def side(**kwargs):
|
||||
if kwargs["category"] == "spot":
|
||||
return {"retCode": 0, "result": {"list": [{
|
||||
"symbol": "BTCUSDT", "lastPrice": "60000", "markPrice": "60000",
|
||||
"bid1Price": "59995", "ask1Price": "60005",
|
||||
"volume24h": "0", "turnover24h": "0",
|
||||
"fundingRate": "0", "openInterest": "0",
|
||||
}]}}
|
||||
else:
|
||||
return {"retCode": 0, "result": {"list": [{
|
||||
"symbol": "BTCUSDT", "lastPrice": "60120", "markPrice": "60120",
|
||||
"bid1Price": "60115", "ask1Price": "60125",
|
||||
"volume24h": "0", "turnover24h": "0",
|
||||
"fundingRate": "0.0001", "openInterest": "0",
|
||||
}]}}
|
||||
mock_http.get_tickers.side_effect = side
|
||||
out = await client.get_basis_spot_perp("BTC")
|
||||
assert out["asset"] == "BTC"
|
||||
assert out["spot_price"] == 60000.0
|
||||
assert out["perp_price"] == 60120.0
|
||||
assert out["basis_abs"] == 120.0
|
||||
assert round(out["basis_pct"], 3) == 0.2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_place_order_limit(client, mock_http):
|
||||
mock_http.place_order.return_value = {
|
||||
"retCode": 0,
|
||||
"result": {"orderId": "ord123", "orderLinkId": ""},
|
||||
}
|
||||
out = await client.place_order(
|
||||
category="linear", symbol="BTCUSDT", side="Buy",
|
||||
qty=0.01, order_type="Limit", price=60000.0, tif="GTC",
|
||||
)
|
||||
assert out["order_id"] == "ord123"
|
||||
kwargs = mock_http.place_order.call_args.kwargs
|
||||
assert kwargs["category"] == "linear"
|
||||
assert kwargs["symbol"] == "BTCUSDT"
|
||||
assert kwargs["side"] == "Buy"
|
||||
assert kwargs["qty"] == "0.01"
|
||||
assert kwargs["orderType"] == "Limit"
|
||||
assert kwargs["price"] == "60000.0"
|
||||
assert kwargs["timeInForce"] == "GTC"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_place_order_error(client, mock_http):
|
||||
mock_http.place_order.return_value = {"retCode": 10001, "retMsg": "insufficient balance"}
|
||||
out = await client.place_order(
|
||||
category="linear", symbol="BTCUSDT", side="Buy", qty=0.01, order_type="Market"
|
||||
)
|
||||
assert out.get("error") == "insufficient balance"
|
||||
assert out.get("code") == 10001
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_amend_order(client, mock_http):
|
||||
mock_http.amend_order.return_value = {"retCode": 0, "result": {"orderId": "ord1"}}
|
||||
out = await client.amend_order(
|
||||
category="linear", symbol="BTCUSDT", order_id="ord1", new_qty=0.02
|
||||
)
|
||||
assert out["order_id"] == "ord1"
|
||||
kwargs = mock_http.amend_order.call_args.kwargs
|
||||
assert kwargs["orderId"] == "ord1"
|
||||
assert kwargs["qty"] == "0.02"
|
||||
assert "price" not in kwargs
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_place_order_option_adds_link_id(client, mock_http):
|
||||
mock_http.place_order.return_value = {
|
||||
"retCode": 0,
|
||||
"result": {"orderId": "opt1", "orderLinkId": "cerbero-abc"},
|
||||
}
|
||||
await client.place_order(
|
||||
category="option", symbol="BTC-24APR26-96000-C-USDT",
|
||||
side="Buy", qty=0.01, order_type="Limit", price=5.0,
|
||||
)
|
||||
kwargs = mock_http.place_order.call_args.kwargs
|
||||
assert "orderLinkId" in kwargs
|
||||
assert kwargs["orderLinkId"].startswith("cerbero-")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_place_order_linear_no_link_id(client, mock_http):
|
||||
mock_http.place_order.return_value = {"retCode": 0, "result": {"orderId": "x"}}
|
||||
await client.place_order(
|
||||
category="linear", symbol="BTCUSDT", side="Buy", qty=0.01, order_type="Market"
|
||||
)
|
||||
kwargs = mock_http.place_order.call_args.kwargs
|
||||
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"}}
|
||||
out = await client.cancel_order(category="linear", symbol="BTCUSDT", order_id="ord1")
|
||||
mock_http.cancel_order.assert_called_once_with(
|
||||
category="linear", symbol="BTCUSDT", orderId="ord1"
|
||||
)
|
||||
assert out["order_id"] == "ord1"
|
||||
assert out["status"] == "cancelled"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cancel_all_orders(client, mock_http):
|
||||
mock_http.cancel_all_orders.return_value = {
|
||||
"retCode": 0,
|
||||
"result": {"list": [{"orderId": "o1"}, {"orderId": "o2"}]},
|
||||
}
|
||||
out = await client.cancel_all_orders(category="linear", symbol="BTCUSDT")
|
||||
mock_http.cancel_all_orders.assert_called_once_with(
|
||||
category="linear", symbol="BTCUSDT"
|
||||
)
|
||||
assert out["cancelled_ids"] == ["o1", "o2"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_stop_loss(client, mock_http):
|
||||
mock_http.set_trading_stop.return_value = {"retCode": 0, "result": {}}
|
||||
out = await client.set_stop_loss(
|
||||
category="linear", symbol="BTCUSDT", stop_loss=55000.0
|
||||
)
|
||||
mock_http.set_trading_stop.assert_called_once()
|
||||
kwargs = mock_http.set_trading_stop.call_args.kwargs
|
||||
assert kwargs["category"] == "linear"
|
||||
assert kwargs["symbol"] == "BTCUSDT"
|
||||
assert kwargs["stopLoss"] == "55000.0"
|
||||
assert kwargs.get("positionIdx", 0) == 0
|
||||
assert out["status"] == "stop_loss_set"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_take_profit(client, mock_http):
|
||||
mock_http.set_trading_stop.return_value = {"retCode": 0, "result": {}}
|
||||
out = await client.set_take_profit(
|
||||
category="linear", symbol="BTCUSDT", take_profit=65000.0
|
||||
)
|
||||
kwargs = mock_http.set_trading_stop.call_args.kwargs
|
||||
assert kwargs["takeProfit"] == "65000.0"
|
||||
assert out["status"] == "take_profit_set"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_close_position(client, mock_http):
|
||||
mock_http.get_positions.return_value = {
|
||||
"retCode": 0, "result": {"list": [
|
||||
{"symbol": "BTCUSDT", "side": "Buy", "size": "0.1",
|
||||
"avgPrice": "60000", "unrealisedPnl": "0",
|
||||
"leverage": "10", "liqPrice": "0", "positionValue": "6000"},
|
||||
]},
|
||||
}
|
||||
mock_http.place_order.return_value = {
|
||||
"retCode": 0, "result": {"orderId": "closeord", "orderLinkId": ""},
|
||||
}
|
||||
out = await client.close_position(category="linear", symbol="BTCUSDT")
|
||||
assert out["status"] == "submitted"
|
||||
kwargs = mock_http.place_order.call_args.kwargs
|
||||
assert kwargs["side"] == "Sell"
|
||||
assert kwargs["qty"] == "0.1"
|
||||
assert kwargs["reduceOnly"] is True
|
||||
assert kwargs["orderType"] == "Market"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_leverage(client, mock_http):
|
||||
mock_http.set_leverage.return_value = {"retCode": 0, "result": {}}
|
||||
out = await client.set_leverage(category="linear", symbol="BTCUSDT", leverage=5)
|
||||
mock_http.set_leverage.assert_called_once_with(
|
||||
category="linear", symbol="BTCUSDT", buyLeverage="5", sellLeverage="5"
|
||||
)
|
||||
assert out["status"] == "leverage_set"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_switch_position_mode(client, mock_http):
|
||||
mock_http.switch_position_mode.return_value = {"retCode": 0, "result": {}}
|
||||
out = await client.switch_position_mode(
|
||||
category="linear", symbol="BTCUSDT", mode="hedge"
|
||||
)
|
||||
kwargs = mock_http.switch_position_mode.call_args.kwargs
|
||||
assert kwargs["mode"] == 3
|
||||
assert out["status"] == "mode_switched"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_transfer_asset(client, mock_http):
|
||||
mock_http.create_internal_transfer.return_value = {
|
||||
"retCode": 0, "result": {"transferId": "tx123"},
|
||||
}
|
||||
out = await client.transfer_asset(
|
||||
coin="USDT", amount=100.0, from_type="UNIFIED", to_type="FUND"
|
||||
)
|
||||
kwargs = mock_http.create_internal_transfer.call_args.kwargs
|
||||
assert kwargs["coin"] == "USDT"
|
||||
assert kwargs["amount"] == "100.0"
|
||||
assert kwargs["fromAccountType"] == "UNIFIED"
|
||||
assert kwargs["toAccountType"] == "FUND"
|
||||
assert out["transfer_id"] == "tx123"
|
||||
@@ -0,0 +1,50 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
from cerbero_mcp.exchanges.bybit.leverage_cap import enforce_leverage, get_max_leverage
|
||||
|
||||
|
||||
def test_get_max_leverage_returns_creds_value():
|
||||
creds = {"max_leverage": 5}
|
||||
assert get_max_leverage(creds) == 5
|
||||
|
||||
|
||||
def test_get_max_leverage_default_when_missing():
|
||||
"""Default 1 (cash) se il secret non ha max_leverage."""
|
||||
assert get_max_leverage({}) == 1
|
||||
|
||||
|
||||
def test_enforce_leverage_pass_under_cap():
|
||||
creds = {"max_leverage": 3}
|
||||
enforce_leverage(2, creds=creds, exchange="bybit") # no raise
|
||||
|
||||
|
||||
def test_enforce_leverage_pass_at_cap():
|
||||
creds = {"max_leverage": 3}
|
||||
enforce_leverage(3, creds=creds, exchange="bybit") # no raise
|
||||
|
||||
|
||||
def test_enforce_leverage_reject_over_cap():
|
||||
creds = {"max_leverage": 3}
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
enforce_leverage(10, creds=creds, exchange="bybit")
|
||||
assert exc.value.status_code == 403
|
||||
assert exc.value.detail["error"] == "LEVERAGE_CAP_EXCEEDED"
|
||||
assert exc.value.detail["exchange"] == "bybit"
|
||||
assert exc.value.detail["requested"] == 10
|
||||
assert exc.value.detail["max"] == 3
|
||||
|
||||
|
||||
def test_enforce_leverage_reject_when_below_one():
|
||||
creds = {"max_leverage": 3}
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
enforce_leverage(0, creds=creds, exchange="bybit")
|
||||
assert exc.value.status_code == 403
|
||||
|
||||
|
||||
def test_enforce_leverage_default_when_none():
|
||||
"""Se requested è None, applica il cap come default."""
|
||||
creds = {"max_leverage": 3}
|
||||
result = enforce_leverage(None, creds=creds, exchange="bybit")
|
||||
assert result == 3
|
||||
Reference in New Issue
Block a user