"""Operator-facing bootstrap helpers for DITAv2. This module keeps the wiring explicit: - control plane selection - Zinc plane selection - projection sink selection - venue adapter selection The defaults stay safe and testable. Real shared-memory or live BingX wiring is only enabled when the caller opts in via arguments or environment. """ from __future__ import annotations from dataclasses import dataclass from enum import Enum import asyncio import inspect import os from pathlib import Path from typing import Any, Optional from dotenv import load_dotenv from prod.bingx.config import BingxExecClientConfig from prod.bingx.config import BingxInstrumentProviderConfig from prod.bingx.enums import BingxEnvironment from .bingx_venue import BingxVenueAdapter from .control import BackendMode from .control import ControlPlane from .control import ControlUpdate from .control import KernelControlSnapshot from .control import KernelMode from .control import KernelVerbosity from .control import build_control_plane from .mock_venue import MockVenueAdapter from .mock_venue import MockVenueScenario from .projection import HazelcastProjection from .projection import build_projection from .real_control_plane import RealZincControlPlane from .real_control_plane import RealZincUnavailable from .real_zinc_plane import RealZincPlane from .real_zinc_plane import RealZincUnavailable as RealZincPlaneUnavailable from .rust_backend import ExecutionKernel from .venue import VenueAdapter from .zinc_plane import InMemoryZincPlane from .zinc_plane import ZincPlane PROJECT_ROOT = Path(__file__).resolve().parents[3] load_dotenv(PROJECT_ROOT / ".env") class LauncherVenueMode(str, Enum): MOCK = "MOCK" BINGX = "BINGX" class LauncherZincMode(str, Enum): IN_MEMORY = "IN_MEMORY" REAL = "REAL" @dataclass class DITAv2LauncherBundle: """Concrete runtime components assembled by the launcher.""" kernel: ExecutionKernel control_plane: ControlPlane projection: HazelcastProjection zinc_plane: ZincPlane venue: VenueAdapter def close(self) -> None: _maybe_close(self.venue) _maybe_close(self.zinc_plane) _maybe_close(self.control_plane) def _env_upper(name: str, default: str = "") -> str: return str(os.environ.get(name, default)).strip().upper() def _env_bool(name: str, default: bool = False) -> bool: raw = os.environ.get(name) if raw is None: return default return str(raw).strip().lower() in {"1", "true", "yes", "on"} def _resolve_control_mode() -> KernelMode | None: raw = _env_upper("DITA_V2_MODE", "") if raw == KernelMode.DEBUG.value: return KernelMode.DEBUG if raw == KernelMode.NORMAL.value: return KernelMode.NORMAL return None def _resolve_control_verbosity() -> KernelVerbosity | None: raw = _env_upper("DITA_V2_VERBOSITY", "") if raw == KernelVerbosity.TRACE.value: return KernelVerbosity.TRACE if raw == KernelVerbosity.VERBOSE.value: return KernelVerbosity.VERBOSE if raw == KernelVerbosity.QUIET.value: return KernelVerbosity.QUIET return None def _resolve_backend_mode() -> BackendMode | None: raw = _env_upper("DITA_V2_BACKEND_MODE", "") if raw == BackendMode.BINGX.value: return BackendMode.BINGX if raw == BackendMode.MOCK.value: return BackendMode.MOCK return None def _control_update_from_env() -> ControlUpdate | None: fields: dict[str, Any] = {} mode = _resolve_control_mode() if mode is not None: fields["mode"] = mode verbosity = _resolve_control_verbosity() if verbosity is not None: fields["verbosity"] = verbosity backend_mode = _resolve_backend_mode() if backend_mode is not None: fields["backend_mode"] = backend_mode raw = os.environ.get("DITA_V2_DEBUG_CLICKHOUSE") if raw is not None: fields["debug_clickhouse_enabled"] = _env_bool("DITA_V2_DEBUG_CLICKHOUSE", True) raw = os.environ.get("DITA_V2_TRACE_TRANSITIONS") if raw is not None: fields["trace_transitions"] = _env_bool("DITA_V2_TRACE_TRANSITIONS", False) raw = os.environ.get("DITA_V2_MIRROR_TO_HAZELCAST") if raw is not None: fields["mirror_to_hazelcast"] = _env_bool("DITA_V2_MIRROR_TO_HAZELCAST", True) raw = os.environ.get("DITA_V2_ACTIVE_SLOT_LIMIT") if raw is not None: try: fields["active_slot_limit"] = max(1, int(str(raw).strip())) except Exception: pass raw = os.environ.get("DITA_V2_RECONCILE_ON_RESTART") if raw is not None: fields["reconcile_on_restart"] = _env_bool("DITA_V2_RECONCILE_ON_RESTART", True) return ControlUpdate(**fields) if fields else None def _resolve_venue_mode(venue_mode: Optional[str] = None) -> LauncherVenueMode: raw = _env_upper("DITA_V2_VENUE", venue_mode or LauncherVenueMode.MOCK.value) if raw == LauncherVenueMode.BINGX.value: return LauncherVenueMode.BINGX return LauncherVenueMode.MOCK def _resolve_zinc_mode(zinc_mode: Optional[str] = None) -> LauncherZincMode: raw = _env_upper("DITA_V2_ZINC", zinc_mode or LauncherZincMode.IN_MEMORY.value) if raw == LauncherZincMode.REAL.value: return LauncherZincMode.REAL return LauncherZincMode.IN_MEMORY def _resolve_hazelcast_real(prefer_real_hazelcast: Optional[bool] = None) -> bool: if prefer_real_hazelcast is not None: return bool(prefer_real_hazelcast) raw = _env_upper("DITA_V2_HAZELCAST", "") return raw in {"REAL", "REAL_HZ", "HAZELCAST"} def build_bingx_exec_client_config( *, environment: Optional[BingxEnvironment] = None, allow_mainnet: Optional[bool] = None, recv_window_ms: Optional[int] = None, default_leverage: Optional[int] = None, exchange_leverage_cap: Optional[int] = None, prefer_websocket: Optional[bool] = None, sizing_mode: Optional[str] = None, ) -> BingxExecClientConfig: """Build the direct BingX config used by the DITAv2 launcher.""" resolved_environment = environment or ( BingxEnvironment.LIVE if _env_upper("DOLPHIN_BINGX_ENV", "VST") == "LIVE" else BingxEnvironment.VST ) resolved_allow_mainnet = _env_bool("DOLPHIN_BINGX_ALLOW_MAINNET", False) if allow_mainnet is None else bool(allow_mainnet) resolved_recv_window = int(os.environ.get("DOLPHIN_BINGX_RECV_WINDOW_MS", "5000")) if recv_window_ms is None else int(recv_window_ms) resolved_default_leverage = int(os.environ.get("DOLPHIN_BINGX_DEFAULT_LEVERAGE", "1")) if default_leverage is None else int(default_leverage) resolved_exchange_cap = int(os.environ.get("DOLPHIN_BINGX_EXCHANGE_LEVERAGE_CAP", "3")) if exchange_leverage_cap is None else int(exchange_leverage_cap) resolved_prefer_ws = _env_bool("DOLPHIN_BINGX_PREFER_WEBSOCKET", False) if prefer_websocket is None else bool(prefer_websocket) resolved_sizing_mode = sizing_mode or os.environ.get("DOLPHIN_BINGX_SIZING_MODE", "testnet") return BingxExecClientConfig( api_key=os.environ.get("BINGX_API_KEY"), secret_key=os.environ.get("BINGX_SECRET_KEY"), environment=resolved_environment, allow_mainnet=resolved_allow_mainnet, recv_window_ms=max(1, resolved_recv_window), default_leverage=max(1, resolved_default_leverage), exchange_leverage_cap=max(1, resolved_exchange_cap), prefer_websocket=resolved_prefer_ws, sizing_mode=resolved_sizing_mode, journal_strategy=os.environ.get("DOLPHIN_BINGX_JOURNAL_STRATEGY", "dita_v2"), journal_db=os.environ.get("DOLPHIN_BINGX_JOURNAL_DB", "dolphin_pink"), instrument_provider=BingxInstrumentProviderConfig(load_all=True), ) def _build_control_plane( *, prefix: str, control_plane: Optional[ControlPlane] = None, ) -> ControlPlane: plane = control_plane or build_control_plane(prefix=prefix) update = _control_update_from_env() if update is not None: plane.update(update) return plane def _build_zinc_plane( *, prefix: str, slot_count: int, zinc_mode: Optional[LauncherZincMode] = None, zinc_plane: Optional[ZincPlane] = None, ) -> ZincPlane: if zinc_plane is not None: return zinc_plane resolved_mode = zinc_mode or _resolve_zinc_mode() if resolved_mode is LauncherZincMode.REAL: try: return RealZincPlane(prefix=prefix, slot_count=slot_count, create=True) except (RealZincPlaneUnavailable, RealZincUnavailable, Exception): pass return InMemoryZincPlane() def _build_venue( *, venue_mode: Optional[LauncherVenueMode] = None, mock_scenario: Optional[MockVenueScenario] = None, bingx_config: Optional[BingxExecClientConfig] = None, bingx_backend: Optional[Any] = None, venue: Optional[VenueAdapter] = None, ) -> VenueAdapter: if venue is not None: return venue resolved_mode = venue_mode or _resolve_venue_mode() if resolved_mode is LauncherVenueMode.BINGX: backend = bingx_backend if backend is None: from prod.clean_arch.adapters.bingx_direct import BingxDirectExecutionAdapter backend = BingxDirectExecutionAdapter(bingx_config or build_bingx_exec_client_config()) return BingxVenueAdapter(backend=backend) return MockVenueAdapter(mock_scenario) def _maybe_close(obj: Any) -> None: for method_name in ("close", "disconnect"): method = getattr(obj, method_name, None) if method is None: continue try: result = method() except TypeError: continue if inspect.isawaitable(result): try: asyncio.run(result) except RuntimeError: pass break def build_launcher_bundle( *, max_slots: int = 10, prefix: Optional[str] = None, control_plane: Optional[ControlPlane] = None, projection: Optional[HazelcastProjection] = None, projection_client: Optional[Any] = None, zinc_plane: Optional[ZincPlane] = None, venue: Optional[VenueAdapter] = None, venue_mode: Optional[LauncherVenueMode | str] = None, zinc_mode: Optional[LauncherZincMode | str] = None, bingx_config: Optional[BingxExecClientConfig] = None, bingx_backend: Optional[Any] = None, mock_scenario: Optional[MockVenueScenario] = None, ) -> DITAv2LauncherBundle: """Build a fully wired DITAv2 runtime bundle. Defaults stay non-destructive: - in-memory Zinc plane - in-process control plane - mock venue - callback projection unless a Hazelcast client is supplied """ resolved_prefix = (prefix or os.environ.get("DITA_V2_PREFIX", "dita_v2")).strip() or "dita_v2" if isinstance(venue_mode, LauncherVenueMode): resolved_venue_mode = venue_mode elif isinstance(venue_mode, str): resolved_venue_mode = LauncherVenueMode(venue_mode.strip().upper()) else: resolved_venue_mode = None if isinstance(zinc_mode, LauncherZincMode): resolved_zinc_mode = zinc_mode elif isinstance(zinc_mode, str): resolved_zinc_mode = LauncherZincMode(zinc_mode.strip().upper()) else: resolved_zinc_mode = None active_control_plane = _build_control_plane(prefix=resolved_prefix, control_plane=control_plane) control_snapshot = active_control_plane.read() active_projection = projection or build_projection( client=projection_client, prefer_real_hazelcast=_resolve_hazelcast_real(), control_snapshot=control_snapshot, ) active_zinc_plane = _build_zinc_plane( prefix=resolved_prefix, slot_count=int(max_slots), zinc_mode=resolved_zinc_mode, zinc_plane=zinc_plane, ) active_venue = _build_venue( venue_mode=resolved_venue_mode, mock_scenario=mock_scenario, bingx_config=bingx_config, bingx_backend=bingx_backend, venue=venue, ) kernel = ExecutionKernel( max_slots=int(max_slots), control_plane=active_control_plane, venue=active_venue, projection=active_projection, projection_client=projection_client, zinc_plane=active_zinc_plane, ) return DITAv2LauncherBundle( kernel=kernel, control_plane=active_control_plane, projection=active_projection, zinc_plane=active_zinc_plane, venue=active_venue, )