"""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, )