from __future__ import annotations import re import pytest from cerbero_mcp.exchanges.hyperliquid.client import HyperliquidClient from pytest_httpx import HTTPXMock @pytest.fixture def client(): return HyperliquidClient( wallet_address="0xDeadBeef", private_key="0x" + "a" * 64, testnet=True, ) # Shared mock responses META_AND_CTX = [ { "universe": [ {"name": "BTC", "maxLeverage": 50}, {"name": "ETH", "maxLeverage": 25}, ] }, [ { "markPx": "50000.0", "funding": "0.0001", "openInterest": "1000.0", "dayNtlVlm": "500000.0", }, { "markPx": "3000.0", "funding": "0.00005", "openInterest": "500.0", "dayNtlVlm": "200000.0", }, ], ] CLEARINGHOUSE_STATE = { "marginSummary": { "accountValue": "1500.0", "totalRawUsd": "1200.0", "totalMarginUsed": "300.0", "totalNtlPos": "50.0", }, "assetPositions": [ { "position": { "coin": "BTC", "szi": "0.1", "entryPx": "48000.0", "unrealizedPnl": "200.0", "leverage": {"value": "10"}, "liquidationPx": "40000.0", } } ], } SPOT_STATE = {"balances": [{"coin": "USDC", "total": "500.0"}]} @pytest.mark.asyncio async def test_get_markets(httpx_mock: HTTPXMock, client: HyperliquidClient): httpx_mock.add_response( url=re.compile(r"https://api\.hyperliquid-testnet\.xyz/info"), json=META_AND_CTX, ) markets = await client.get_markets() assert len(markets) == 2 assert markets[0]["asset"] == "BTC" assert markets[0]["mark_price"] == 50000.0 assert markets[0]["max_leverage"] == 50 @pytest.mark.asyncio async def test_get_ticker(httpx_mock: HTTPXMock, client: HyperliquidClient): httpx_mock.add_response( url=re.compile(r"https://api\.hyperliquid-testnet\.xyz/info"), json=META_AND_CTX, ) result = await client.get_ticker("BTC") assert result["asset"] == "BTC" assert result["mark_price"] == 50000.0 assert result["funding_rate"] == 0.0001 @pytest.mark.asyncio async def test_get_ticker_not_found(httpx_mock: HTTPXMock, client: HyperliquidClient): httpx_mock.add_response( url=re.compile(r"https://api\.hyperliquid-testnet\.xyz/info"), json=META_AND_CTX, ) result = await client.get_ticker("SOL") assert "error" in result @pytest.mark.asyncio async def test_get_orderbook(httpx_mock: HTTPXMock, client: HyperliquidClient): httpx_mock.add_response( url=re.compile(r"https://api\.hyperliquid-testnet\.xyz/info"), json={ "levels": [ [{"px": "49990.0", "sz": "0.5"}, {"px": "49980.0", "sz": "1.0"}], [{"px": "50010.0", "sz": "0.3"}, {"px": "50020.0", "sz": "0.8"}], ] }, ) result = await client.get_orderbook("BTC", depth=2) assert result["asset"] == "BTC" assert len(result["bids"]) == 2 assert len(result["asks"]) == 2 assert result["bids"][0]["price"] == 49990.0 @pytest.mark.asyncio async def test_get_positions(httpx_mock: HTTPXMock, client: HyperliquidClient): httpx_mock.add_response( url=re.compile(r"https://api\.hyperliquid-testnet\.xyz/info"), json=CLEARINGHOUSE_STATE, ) positions = await client.get_positions() assert len(positions) == 1 assert positions[0]["asset"] == "BTC" assert positions[0]["direction"] == "long" assert positions[0]["size"] == 0.1 assert positions[0]["leverage"] == 10.0 @pytest.mark.asyncio async def test_get_account_summary(httpx_mock: HTTPXMock, client: HyperliquidClient): # get_account_summary calls /info twice (perp + spot) httpx_mock.add_response( url=re.compile(r"https://api\.hyperliquid-testnet\.xyz/info"), json=CLEARINGHOUSE_STATE, ) httpx_mock.add_response( url=re.compile(r"https://api\.hyperliquid-testnet\.xyz/info"), json=SPOT_STATE, ) result = await client.get_account_summary() assert result["perps_equity"] == 1500.0 assert result["spot_usdc"] == 500.0 assert result["equity"] == 2000.0 @pytest.mark.asyncio async def test_get_trade_history(httpx_mock: HTTPXMock, client: HyperliquidClient): httpx_mock.add_response( url=re.compile(r"https://api\.hyperliquid-testnet\.xyz/info"), json=[ {"coin": "BTC", "side": "B", "sz": "0.1", "px": "50000", "fee": "0.5", "time": 1000}, {"coin": "ETH", "side": "A", "sz": "1.0", "px": "3000", "fee": "0.3", "time": 2000}, ], ) trades = await client.get_trade_history(limit=10) assert len(trades) == 2 assert trades[0]["asset"] == "BTC" assert trades[0]["price"] == 50000.0 @pytest.mark.asyncio async def test_get_open_orders(httpx_mock: HTTPXMock, client: HyperliquidClient): httpx_mock.add_response( url=re.compile(r"https://api\.hyperliquid-testnet\.xyz/info"), json=[ { "oid": 12345, "coin": "BTC", "side": "B", "sz": "0.05", "limitPx": "49000", "orderType": "Limit", } ], ) orders = await client.get_open_orders() assert len(orders) == 1 assert orders[0]["oid"] == 12345 assert orders[0]["asset"] == "BTC" @pytest.mark.asyncio async def test_get_historical(httpx_mock: HTTPXMock, client: HyperliquidClient): httpx_mock.add_response( url=re.compile(r"https://api\.hyperliquid-testnet\.xyz/info"), json=[ {"t": 1000000, "o": "49000", "h": "51000", "l": "48500", "c": "50000", "v": "100"}, ], ) result = await client.get_historical("BTC", "2024-01-01", "2024-01-02", "1h") assert len(result["candles"]) == 1 assert result["candles"][0]["close"] == 50000.0 @pytest.mark.asyncio async def test_health_ok(httpx_mock: HTTPXMock, client: HyperliquidClient): httpx_mock.add_response( url=re.compile(r"https://api\.hyperliquid-testnet\.xyz/info"), json={"universe": []}, ) result = await client.health() assert result["status"] in ("ok", "healthy") assert result["testnet"] is True @pytest.mark.asyncio async def test_place_order_sdk_unavailable(client: HyperliquidClient): """place_order raises RuntimeError when SDK is not available (mocked).""" import cerbero_mcp.exchanges.hyperliquid.client as mod original = mod._SDK_AVAILABLE mod._SDK_AVAILABLE = False client._exchange = None try: result = await client.place_order("BTC", "buy", 0.1, price=50000.0) # Should return error dict or raise RuntimeError assert "error" in result or result.get("status") == "error" except RuntimeError as exc: assert "not installed" in str(exc).lower() or "sdk" in str(exc).lower() finally: mod._SDK_AVAILABLE = original