Files
Cerbero-mcp/services/mcp-bybit/tests/test_client.py
T
2026-04-27 17:34:14 +02:00

532 lines
19 KiB
Python

from __future__ import annotations
import pytest
from mcp_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("mcp_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 mcp_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_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"