"""PollState — process-wide singleton.""" from collections import deque from dataclasses import dataclass, field from datetime import datetime @dataclass class PollState: portfolio: dict | None = None news_sentiment: dict | None = None screener_preview: dict | None = None minute_bars: dict[str, deque] = field(default_factory=dict) asking_price: dict[str, dict] = field(default_factory=dict) # Phase 3b additions daily_ohlcv: dict[str, list[dict]] = field(default_factory=dict) chronos_predictions: dict[str, dict] = field(default_factory=dict) minute_momentum: dict[str, str] = field(default_factory=dict) signals: dict[str, dict] = field(default_factory=dict) # F5 lifecycle signal_cycle_id: int = 0 last_updated: dict[str, str] = field(default_factory=dict) fetch_errors: dict[str, int] = field(default_factory=dict) def get_active_signals(self, now: datetime) -> list[dict]: """expires_at > now 인 신호만 반환. expires_at 없거나 파싱 실패는 expired 취급.""" active: list[dict] = [] for sig in self.signals.values(): expires_at = sig.get("expires_at") if not expires_at: continue try: exp_dt = datetime.fromisoformat(expires_at) except ValueError: continue if exp_dt > now: active.append(sig) return active def purge_expired_signals(self, now: datetime) -> int: """만료된 signal 제거. expires_at 없거나 파싱 실패도 제거. 제거 개수 반환.""" to_drop = [] for ticker, sig in self.signals.items(): expires_at = sig.get("expires_at") if not expires_at: to_drop.append(ticker) continue try: exp_dt = datetime.fromisoformat(expires_at) except ValueError: to_drop.append(ticker) continue if exp_dt <= now: to_drop.append(ticker) for t in to_drop: del self.signals[t] return len(to_drop) state = PollState()