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"