Files
Cerbero-Bite/src/cerbero_bite/runtime/dependencies.py
T
Adriano Dal Pastro 839285d75b fix(mcp): allinea il client all'interfaccia unificata MCP V2
Cerbero MCP V2 ha spostato get_instruments/get_historical/get_indicators
sul router unificato /mcp (rimossi dagli endpoint per-exchange) e ha
rinominato get_technical_indicators in get_indicators. Il client chiamava
ancora questi tool su /mcp-deribit -> 404 -> kill-switch armato, raccolta
option-chain ferma e bias direzionale rotto.

- mcp_endpoints: nuovo endpoint `unified` (CERBERO_BITE_MCP_UNIFIED_URL,
  default http://cerbero-mcp:9000/mcp)
- deribit: i 3 tool dati passano dal client unificato con exchange="deribit",
  interval minuscolo (1D->1d) e parsing del nuovo shape (symbol + native.*);
  adx_14 usa get_indicators (indicators.adx.adx)
- dependencies/gui: iniettano il client unificato in DeribitClient
- docker-compose: default in-cluster per l'endpoint /mcp

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 19:55:04 +00:00

186 lines
5.5 KiB
Python

"""Runtime dependency container.
Builds and wires together every long-lived object the engine needs:
HTTP clients, repository, audit log, kill switch, alert manager. The
:func:`build_runtime` factory returns a frozen :class:`RuntimeContext`
that the orchestrator and the cycle modules pass around — no global
state, no implicit singletons.
"""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from datetime import UTC, datetime
from pathlib import Path
import httpx
from cerbero_bite.clients._base import DEFAULT_BOT_TAG, HttpToolClient
from cerbero_bite.clients.deribit import DeribitClient
from cerbero_bite.clients.hyperliquid import HyperliquidClient
from cerbero_bite.clients.macro import MacroClient
from cerbero_bite.clients.portfolio import PortfolioClient
from cerbero_bite.clients.sentiment import SentimentClient
from cerbero_bite.clients.telegram import TelegramClient, load_telegram_credentials
from cerbero_bite.config.mcp_endpoints import McpEndpoints
from cerbero_bite.config.schema import StrategyConfig
from cerbero_bite.runtime.alert_manager import AlertManager
from cerbero_bite.safety.audit_log import AuditLog
from cerbero_bite.safety.kill_switch import KillSwitch
from cerbero_bite.state import (
Repository,
connect,
run_migrations,
transaction,
)
__all__ = ["RuntimeContext", "build_runtime"]
@dataclass(frozen=True)
class RuntimeContext:
"""Bag of every wired object used by the runtime."""
cfg: StrategyConfig
db_path: Path
audit_path: Path
repository: Repository
audit_log: AuditLog
kill_switch: KillSwitch
alert_manager: AlertManager
deribit: DeribitClient
macro: MacroClient
sentiment: SentimentClient
hyperliquid: HyperliquidClient
portfolio: PortfolioClient
telegram: TelegramClient
http_client: httpx.AsyncClient
clock: Callable[[], datetime]
async def aclose(self) -> None:
"""Close the shared HTTP client. Idempotent."""
await self.http_client.aclose()
def _utc_now() -> datetime:
return datetime.now(UTC)
def build_runtime(
*,
cfg: StrategyConfig,
endpoints: McpEndpoints,
token: str,
db_path: Path | str,
audit_path: Path | str,
bot_tag: str = DEFAULT_BOT_TAG,
timeout_s: float = 8.0,
retry_max: int = 3,
clock: Callable[[], datetime] | None = None,
) -> RuntimeContext:
"""Wire every dependency the runtime needs.
The SQLite database is migrated and the system_state singleton is
initialised eagerly so the orchestrator can assume both are
present.
"""
db_path = Path(db_path)
audit_path = Path(audit_path)
clk = clock or _utc_now
repository = Repository()
conn = connect(db_path)
try:
run_migrations(conn)
with transaction(conn):
repository.init_system_state(
conn, config_version=cfg.config_version, now=clk()
)
finally:
conn.close()
def _persist_audit_anchor(line_hash: str) -> None:
"""Mirror the latest audit chain hash into ``system_state``.
Best-effort: if SQLite is locked by another writer the audit
log itself is still consistent, the anchor will catch up on
the next append.
"""
anchor_conn = connect(db_path)
try:
with transaction(anchor_conn):
repository.set_last_audit_hash(anchor_conn, hex_hash=line_hash)
except Exception: # pragma: no cover — durability is best-effort
pass
finally:
anchor_conn.close()
audit_log = AuditLog(audit_path, on_append=_persist_audit_anchor)
kill_switch = KillSwitch(
connection_factory=lambda: connect(db_path),
repository=repository,
audit_log=audit_log,
clock=clk,
)
# Single long-lived AsyncClient shared by every wrapper. httpx pools
# connections per host so the snapshot stage of the entry cycle
# avoids paying TLS/TCP handshakes on each call.
http_client = httpx.AsyncClient(
timeout=httpx.Timeout(timeout_s),
limits=httpx.Limits(max_connections=20, max_keepalive_connections=10),
)
def _client(service: str) -> HttpToolClient:
return HttpToolClient(
service=service,
base_url=endpoints.for_service(service),
token=token,
bot_tag=bot_tag,
timeout_s=timeout_s,
retry_max=retry_max,
client=http_client,
)
bot_token, chat_id = load_telegram_credentials()
telegram = TelegramClient(
bot_token=bot_token,
chat_id=chat_id,
http_client=http_client,
timeout_s=timeout_s,
)
alert_manager = AlertManager(
telegram=telegram, audit_log=audit_log, kill_switch=kill_switch
)
deribit = DeribitClient(_client("deribit"), _client("unified"))
macro = MacroClient(_client("macro"))
sentiment = SentimentClient(_client("sentiment"))
hyperliquid = HyperliquidClient(_client("hyperliquid"))
portfolio = PortfolioClient(
deribit=deribit, hyperliquid=hyperliquid, macro=macro
)
return RuntimeContext(
cfg=cfg,
db_path=db_path,
audit_path=audit_path,
repository=repository,
audit_log=audit_log,
kill_switch=kill_switch,
alert_manager=alert_manager,
deribit=deribit,
macro=macro,
sentiment=sentiment,
hyperliquid=hyperliquid,
portfolio=portfolio,
telegram=telegram,
http_client=http_client,
clock=clk,
)