"""Wrapper around ``mcp-hyperliquid``. Cerbero Bite consumes: * ``get_funding_rate`` — entry filter §2.6 cap on absolute annualised funding rate (``docs/01-strategy-rules.md``). * ``get_account_summary`` and ``get_positions`` — feed the in-process portfolio aggregator (equity + ETH/BTC exposure on the perp side). """ from __future__ import annotations from decimal import Decimal from typing import Any from cerbero_bite.clients._base import HttpToolClient from cerbero_bite.clients._exceptions import McpDataAnomalyError __all__ = ["HOURLY_FUNDING_PERIODS_PER_YEAR", "HyperliquidClient"] HOURLY_FUNDING_PERIODS_PER_YEAR = 24 * 365 # = 8760 class HyperliquidClient: SERVICE = "hyperliquid" def __init__(self, http: HttpToolClient) -> None: if http.service != self.SERVICE: raise ValueError( f"HyperliquidClient requires service '{self.SERVICE}', got '{http.service}'" ) self._http = http async def funding_rate_annualized(self, asset: str) -> Decimal: """Return the latest funding rate of ``asset`` as an annualised fraction.""" raw = await self._http.call( "get_funding_rate", {"instrument": asset.upper()} ) if raw.get("error"): raise McpDataAnomalyError( f"hyperliquid get_funding_rate error: {raw['error']}", service=self.SERVICE, tool="get_funding_rate", ) rate = raw.get("current_funding_rate") if rate is None: raise McpDataAnomalyError( "hyperliquid response missing 'current_funding_rate'", service=self.SERVICE, tool="get_funding_rate", ) return Decimal(str(rate)) * Decimal(HOURLY_FUNDING_PERIODS_PER_YEAR) async def get_account_summary(self) -> dict[str, Any]: """Account equity and balances (USD).""" raw: Any = await self._http.call("get_account_summary", {}) return raw if isinstance(raw, dict) else {} async def get_positions(self) -> list[dict[str, Any]]: """Open perp positions (list of dicts).""" raw: Any = await self._http.call("get_positions", {}) if isinstance(raw, list): return raw if isinstance(raw, dict): inner = raw.get("positions") if isinstance(inner, list): return inner return []