abf5a140e2
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>
70 lines
2.4 KiB
Python
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 []
|