Files
Cerbero-Bite/src/cerbero_bite/clients/hyperliquid.py
T
Adriano abf5a140e2 refactor: telegram + portfolio in-process (drop shared MCP)
Each bot now manages its own notification + portfolio aggregation:

* TelegramClient calls the public Bot API directly via httpx, reading
  CERBERO_BITE_TELEGRAM_BOT_TOKEN / CERBERO_BITE_TELEGRAM_CHAT_ID from
  env. No credentials → silent disabled mode.
* PortfolioClient composes DeribitClient + HyperliquidClient + the new
  MacroClient.get_asset_price/eur_usd_rate to expose equity (EUR) and
  per-asset exposure as the bot's own slice (no cross-bot view).
* mcp-telegram and mcp-portfolio removed from MCP_SERVICES / McpEndpoints
  and the cerbero-bite ping CLI; health_check no longer probes portfolio.

Docs (02/04/06/07) and docker-compose updated to reflect the new
architecture.

353/353 tests pass; ruff clean; mypy src clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 00:31:20 +02:00

70 lines
2.4 KiB
Python

"""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 []