#!/usr/bin/env python3 """ ADAPTER: HazelcastDataFeed ========================== Implementation of DataFeedPort using Hazelcast. Current implementation - uses DolphinNG6 data feed. All data (price + eigenvalues) from single source, same timestamp. """ import json import logging from datetime import datetime from typing import Optional, Callable, Dict, Any # Port interface import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent)) from ports.data_feed import DataFeedPort, MarketSnapshot, ACBUpdate logger = logging.getLogger("HazelcastDataFeed") class HazelcastDataFeed(DataFeedPort): """ ADAPTER: Hazelcast implementation of DataFeedPort. Reads from DolphinNG6 output via Hazelcast maps: - DOLPHIN_FEATURES: Price + eigenvalues (ALWAYS SYNCED) - DOLPHIN_SAFETY: Posture/mode - DOLPHIN_STATE_*: Portfolio state No sync issues - all data written atomically by DolphinNG6. """ def __init__(self, config: Dict[str, Any]): self.config = config self.hz_client = None self.features_map = None self.safety_map = None self._last_snapshot: Optional[MarketSnapshot] = None self._latency_ms = 0.0 async def connect(self) -> bool: """Connect to Hazelcast cluster.""" try: import hazelcast hz_config = self.config.get('hazelcast', {}) cluster = hz_config.get('cluster', 'dolphin') host = hz_config.get('host', 'localhost:5701') logger.info(f"Connecting to Hazelcast: {host} (cluster: {cluster})") self.hz_client = hazelcast.HazelcastClient( cluster_name=cluster, cluster_members=[host], ) # Get reference to maps self.features_map = self.hz_client.get_map('DOLPHIN_FEATURES').blocking() self.safety_map = self.hz_client.get_map('DOLPHIN_SAFETY').blocking() # Test connection size = self.features_map.size() logger.info(f"[✓] Connected. Features map: {size} entries") return True except Exception as e: logger.error(f"[✗] Connection failed: {e}") return False async def disconnect(self): """Clean disconnect.""" if self.hz_client: self.hz_client.shutdown() logger.info("[✓] Disconnected from Hazelcast") async def get_latest_snapshot(self, symbol: str = "BTCUSDT") -> Optional[MarketSnapshot]: """ Get latest synchronized snapshot from Hazelcast. Reads 'latest_eigen_scan' which contains: - prices[]: Array of prices for all assets - eigenvalues[]: Computed eigenvalues - assets[]: Asset symbols - scan_number: Sequence number - timestamp: Unix timestamp All fields from SAME 5s pulse - GUARANTEED SYNCED. """ try: start = datetime.utcnow() raw = self.features_map.get("latest_eigen_scan") if not raw: return self._last_snapshot # Return cached if available data = json.loads(raw) # Find index for requested symbol assets = data.get('assets', []) if symbol not in assets: logger.warning(f"Symbol {symbol} not in assets list: {assets[:5]}...") return None idx = assets.index(symbol) prices = data.get('asset_prices', []) # Note: field is asset_prices, not prices eigenvalues = data.get('asset_loadings', []) # Note: field is asset_loadings # Build snapshot snapshot = MarketSnapshot( timestamp=datetime.utcnow(), # Or parse from data['timestamp'] symbol=symbol, price=float(prices[idx]) if idx < len(prices) else 0.0, eigenvalues=[float(e) for e in eigenvalues] if eigenvalues else [], velocity_divergence=data.get('vel_div'), irp_alignment=data.get('irp_alignment'), scan_number=data.get('scan_number'), source="hazelcast" ) self._last_snapshot = snapshot # Calculate latency self._latency_ms = (datetime.utcnow() - start).total_seconds() * 1000 return snapshot except Exception as e: logger.error(f"Error reading snapshot: {e}") return self._last_snapshot async def subscribe_snapshots(self, callback: Callable[[MarketSnapshot], None]): """ Subscribe to snapshot updates via polling (listener not critical). Polling every 5s matches DolphinNG6 pulse. """ logger.info("[✓] Snapshot subscription ready (polling mode)") async def get_acb_update(self) -> Optional[ACBUpdate]: """Get ACB update from Hazelcast.""" try: # ACB might be in features or separate map raw = self.features_map.get("latest_acb") if raw: data = json.loads(raw) return ACBUpdate( timestamp=datetime.utcnow(), boost=data.get('boost', 1.0), beta=data.get('beta', 0.5), cut=data.get('cut', 0.0), posture=data.get('posture', 'APEX') ) except Exception as e: logger.error(f"ACB read error: {e}") return None def get_latency_ms(self) -> float: """Return last measured latency.""" return self._latency_ms def health_check(self) -> bool: """Check Hazelcast connection health.""" if not self.hz_client: return False try: # Quick ping self.features_map.size() return True except: return False