# -*- coding: utf-8 -*- """ WebSocket Gateway for frontend communication """ import asyncio import json import logging import os from datetime import datetime from pathlib import Path from typing import Any, Callable, Dict, List, Optional, Set import websockets from websockets.asyncio.server import ServerConnection from backend.data.provider_utils import normalize_symbol from backend.domains import news as news_domain from backend.llm.models import get_agent_model_info from backend.core.pipeline import TradingPipeline from backend.core.state_sync import StateSync from backend.services.market import MarketService from backend.services.storage import StorageService from backend.data.provider_router import get_provider_router from backend.tools.technical_signals import StockTechnicalAnalyzer from backend.core.scheduler import Scheduler from backend.services import gateway_admin_handlers from backend.services import gateway_cycle_support from backend.services import gateway_openclaw_handlers from backend.services import gateway_runtime_support from backend.services import gateway_stock_handlers from shared.client import NewsServiceClient from shared.client import TradingServiceClient from shared.client.openclaw_websocket_client import OpenClawWebSocketClient, DEFAULT_GATEWAY_URL as OPENCLAW_WS_URL logger = logging.getLogger(__name__) EDITABLE_AGENT_WORKSPACE_FILES = { "SOUL.md", "PROFILE.md", "AGENTS.md", "MEMORY.md", "POLICY.md", } class Gateway: """WebSocket Gateway for frontend communication""" def __init__( self, market_service: MarketService, storage_service: StorageService, pipeline: TradingPipeline, state_sync: Optional[StateSync] = None, scheduler_callback: Optional[Callable] = None, scheduler: Optional[Scheduler] = None, config: Dict[str, Any] = None, ): self.market_service = market_service self.storage = storage_service self.pipeline = pipeline self.scheduler_callback = scheduler_callback self.scheduler = scheduler self.config = config or {} self.mode = self.config.get("mode", "live") self.is_backtest = self.mode == "backtest" or self.config.get( "backtest_mode", False, ) self.state_sync = state_sync or StateSync(storage=storage_service) # self.state_sync.set_mode(self.is_backtest) self.state_sync.set_broadcast_fn(self.broadcast) self.pipeline.state_sync = self.state_sync self.connected_clients: Set[ServerConnection] = set() self.lock = asyncio.Lock() self._cycle_lock = asyncio.Lock() self._backtest_task: Optional[asyncio.Task] = None self._manual_cycle_task: Optional[asyncio.Task] = None self._backtest_start_date: Optional[str] = None self._backtest_end_date: Optional[str] = None self._market_status_task: Optional[asyncio.Task] = None self._watchlist_ingest_task: Optional[asyncio.Task] = None # Session tracking for live returns self._session_start_portfolio_value: Optional[float] = None self._provider_router = get_provider_router() self._loop: Optional[asyncio.AbstractEventLoop] = None self._project_root = Path(__file__).resolve().parents[2] self._technical_analyzer = StockTechnicalAnalyzer() self._openclaw_ws: OpenClawWebSocketClient | None = None async def start(self, host: str = "0.0.0.0", port: int = 8766): """Start gateway server with proper initialization order. Phase 1: Start WebSocket server first so frontend can connect immediately Phase 2: Start market data service (pushes data to connected clients) Phase 3: Start scheduler last (triggers trading cycles) """ logger.info(f"Starting gateway on {host}:{port}") self._loop = asyncio.get_running_loop() self._provider_router.add_listener(self._on_provider_usage_changed) self.state_sync.load_state() self.market_service.set_price_recorder(self.storage.record_price_point) self.state_sync.update_state("status", "initializing") self.state_sync.update_state("server_mode", self.mode) self.state_sync.update_state("is_backtest", self.is_backtest) self.state_sync.update_state("tickers", self.config.get("tickers", [])) self.state_sync.update_state( "runtime_config", { "tickers": self.config.get("tickers", []), "schedule_mode": self.config.get("schedule_mode", "daily"), "interval_minutes": self.config.get("interval_minutes", 60), "trigger_time": self.config.get("trigger_time", "09:30"), "initial_cash": self.config.get( "initial_cash", self.storage.initial_cash, ), "margin_requirement": self.config.get("margin_requirement"), "max_comm_cycles": self.config.get("max_comm_cycles"), "enable_memory": self.config.get("enable_memory", False), }, ) self.state_sync.update_state( "data_sources", self._provider_router.get_usage_snapshot(), ) # Load and display existing portfolio state if available dashboard_snapshot = self.storage.build_dashboard_snapshot_from_state(self.state_sync.state) summary = dashboard_snapshot.get("summary") if summary: logger.info( "Loaded existing portfolio: $%s", f"{summary.get('totalAssetValue', 0):,.2f}", ) # ====================================================================== # PHASE 1: Start WebSocket server first # This allows frontend to connect immediately and receive status updates # ====================================================================== logger.info("[Phase 1/4] Starting WebSocket server...") self.state_sync.update_state("status", "websocket_ready") # Create server but don't block yet - we'll serve inside the context manager server = await websockets.serve( self.handle_client, host, port, ping_interval=30, ping_timeout=60, ) logger.info(f"WebSocket server ready: ws://{host}:{port}") # Give a brief moment for any existing clients to reconnect await asyncio.sleep(0.1) # Connect to OpenClaw Gateway (18789) via WebSocket logger.info("Connecting to OpenClaw Gateway...") try: self._openclaw_ws = OpenClawWebSocketClient( url=OPENCLAW_WS_URL, client_name="gateway-client", client_version="1.0.0", ) await self._openclaw_ws.connect() logger.info("OpenClaw Gateway WebSocket connected") except Exception as e: logger.warning("Failed to connect to OpenClaw Gateway: %s", e) self._openclaw_ws = None # ====================================================================== # PHASE 2: Start market data service # Now frontend is connected, start pushing price updates # ====================================================================== logger.info("[Phase 2/4] Starting market data service...") self.state_sync.update_state("status", "market_service_starting") await self.market_service.start(broadcast_func=self.broadcast) self.state_sync.update_state("status", "market_service_ready") logger.info("Market data service ready - price updates active") # ====================================================================== # PHASE 3: Start market status monitoring # Monitors market open/close and broadcasts status # ====================================================================== logger.info("[Phase 3/4] Starting market status monitoring...") if not self.is_backtest: self._market_status_task = asyncio.create_task( self._market_status_monitor(), ) # ====================================================================== # PHASE 4: Start scheduler last # Only start trading after everything else is ready # ====================================================================== logger.info("[Phase 4/4] Starting scheduler...") self.state_sync.update_state("status", "scheduler_starting") if self.scheduler: # Wire up heartbeat callback if heartbeat is configured heartbeat_interval = self.config.get("heartbeat_interval", 0) if heartbeat_interval and heartbeat_interval > 0: self.scheduler.set_heartbeat_callback(self.on_heartbeat_trigger) logger.info( f"[Heartbeat] Registered heartbeat callback (interval={heartbeat_interval}s)", ) await self.scheduler.start(self.on_strategy_trigger) elif self.scheduler_callback: await self.scheduler_callback(callback=self.on_strategy_trigger) self.state_sync.update_state("status", "running") logger.info( f"Gateway fully operational: ws://{host}:{port}, mode={self.mode}", ) # Keep server running await asyncio.Future() def _on_provider_usage_changed(self, snapshot: Dict[str, Any]): """Handle provider routing updates from the shared router.""" self.state_sync.update_state("data_sources", snapshot) if self._loop and self._loop.is_running(): asyncio.run_coroutine_threadsafe( self.broadcast( { "type": "data_sources_update", "data_sources": snapshot, }, ), self._loop, ) @property def state(self) -> Dict[str, Any]: return self.state_sync.state @staticmethod def _news_rows_need_enrichment(rows: List[Dict[str, Any]]) -> bool: return news_domain.news_rows_need_enrichment(rows) def _news_service_url(self) -> str | None: """Return configured news-service base URL, if any.""" candidate = self.config.get("news_service_url") or os.getenv( "NEWS_SERVICE_URL", "", ) value = str(candidate or "").strip() return value or None def _trading_service_url(self) -> str | None: """Return configured trading-service base URL, if any.""" candidate = self.config.get("trading_service_url") or os.getenv( "TRADING_SERVICE_URL", "", ) value = str(candidate or "").strip() return value or None async def _call_news_service( self, action: str, callback: Callable[[NewsServiceClient], Any], ) -> Any | None: """Call news-service when configured, otherwise return None.""" service_url = self._news_service_url() if not service_url: return None try: async with NewsServiceClient(service_url) as client: return await callback(client) except Exception as exc: logger.warning("news-service %s failed: %s", action, exc) return None async def _call_trading_service( self, action: str, callback: Callable[[TradingServiceClient], Any], ) -> Any | None: """Call trading-service when configured, otherwise return None.""" service_url = self._trading_service_url() if not service_url: return None try: async with TradingServiceClient(service_url) as client: return await callback(client) except Exception as exc: logger.warning("trading-service %s failed: %s", action, exc) return None async def handle_client(self, websocket: ServerConnection): """Handle WebSocket client connection""" async with self.lock: self.connected_clients.add(websocket) await self._send_initial_state(websocket) await self._handle_client_messages(websocket) async with self.lock: self.connected_clients.discard(websocket) async def _send_initial_state(self, websocket: ServerConnection): try: logger.info("[Gateway] Sending initial state to client...") state_payload = self.state_sync.get_initial_state_payload( include_dashboard=True, ) state_payload["data_sources"] = ( self._provider_router.get_usage_snapshot() ) # Include market status in initial state state_payload[ "market_status" ] = self.market_service.get_market_status() # Include live returns if session is active if self.storage.is_live_session_active: live_returns = self.storage.get_live_returns() if "portfolio" in state_payload: state_payload["portfolio"].update(live_returns) await websocket.send( json.dumps( {"type": "initial_state", "state": state_payload}, ensure_ascii=False, default=str, ), ) logger.info("[Gateway] Initial state sent successfully") except Exception as e: logger.exception(f"[Gateway] Failed to send initial state: {e}") # Send error response so client knows something went wrong try: await websocket.send( json.dumps( {"type": "error", "message": "Failed to load initial state"}, ensure_ascii=False, ), ) except Exception as e: logger.warning(f"Failed to send error response to client: {e}") async def _handle_client_messages( self, websocket: ServerConnection, ): try: async for message in websocket: data = json.loads(message) msg_type = data.get("type", "unknown") if msg_type == "ping": await websocket.send( json.dumps( { "type": "pong", "timestamp": datetime.now().isoformat(), }, ensure_ascii=False, ), ) elif msg_type == "get_state": await self._send_initial_state(websocket) elif msg_type == "start_backtest": await self._handle_start_backtest(data) elif msg_type == "trigger_strategy": await self._handle_manual_trigger(websocket, data) elif msg_type == "update_runtime_config": await self._handle_update_runtime_config(websocket, data) elif msg_type == "reload_runtime_assets": await self._handle_reload_runtime_assets() elif msg_type == "update_watchlist": await self._handle_update_watchlist(websocket, data) elif msg_type == "get_agent_skills": await self._handle_get_agent_skills(websocket, data) elif msg_type == "get_agent_profile": await self._handle_get_agent_profile(websocket, data) elif msg_type == "get_skill_detail": await self._handle_get_skill_detail(websocket, data) elif msg_type == "create_agent_local_skill": await self._handle_create_agent_local_skill(websocket, data) elif msg_type == "update_agent_local_skill": await self._handle_update_agent_local_skill(websocket, data) elif msg_type == "delete_agent_local_skill": await self._handle_delete_agent_local_skill(websocket, data) elif msg_type == "remove_agent_skill": await self._handle_remove_agent_skill(websocket, data) elif msg_type == "update_agent_skill": await self._handle_update_agent_skill(websocket, data) elif msg_type == "get_agent_workspace_file": await self._handle_get_agent_workspace_file(websocket, data) elif msg_type == "update_agent_workspace_file": await self._handle_update_agent_workspace_file(websocket, data) elif msg_type == "get_stock_history": await self._handle_get_stock_history(websocket, data) elif msg_type == "get_stock_explain_events": await self._handle_get_stock_explain_events(websocket, data) elif msg_type == "get_stock_news": await self._handle_get_stock_news(websocket, data) elif msg_type == "get_stock_news_for_date": await self._handle_get_stock_news_for_date(websocket, data) elif msg_type == "get_stock_news_timeline": await self._handle_get_stock_news_timeline(websocket, data) elif msg_type == "get_stock_news_categories": await self._handle_get_stock_news_categories(websocket, data) elif msg_type == "get_stock_range_explain": await self._handle_get_stock_range_explain(websocket, data) elif msg_type == "get_stock_insider_trades": await self._handle_get_stock_insider_trades(websocket, data) elif msg_type == "get_stock_story": await self._handle_get_stock_story(websocket, data) elif msg_type == "get_stock_similar_days": await self._handle_get_stock_similar_days(websocket, data) elif msg_type == "get_stock_technical_indicators": await self._handle_get_stock_technical_indicators(websocket, data) elif msg_type == "run_stock_enrich": await self._handle_run_stock_enrich(websocket, data) elif msg_type == "get_openclaw_status": await self._handle_get_openclaw_status(websocket, data) elif msg_type == "get_openclaw_sessions": await self._handle_get_openclaw_sessions(websocket, data) elif msg_type == "get_openclaw_session_detail": await self._handle_get_openclaw_session_detail(websocket, data) elif msg_type == "get_openclaw_session_history": await self._handle_get_openclaw_session_history(websocket, data) elif msg_type == "get_openclaw_cron": await self._handle_get_openclaw_cron(websocket, data) elif msg_type == "get_openclaw_approvals": await self._handle_get_openclaw_approvals(websocket, data) elif msg_type == "get_openclaw_agents": await self._handle_get_openclaw_agents(websocket, data) elif msg_type == "get_openclaw_agents_presence": await self._handle_get_openclaw_agents_presence(websocket, data) elif msg_type == "get_openclaw_skills": await self._handle_get_openclaw_skills(websocket, data) elif msg_type == "get_openclaw_models": await self._handle_get_openclaw_models(websocket, data) elif msg_type == "get_openclaw_hooks": await gateway_openclaw_handlers.handle_get_openclaw_hooks(self, websocket, data) elif msg_type == "get_openclaw_plugins": await gateway_openclaw_handlers.handle_get_openclaw_plugins(self, websocket, data) elif msg_type == "get_openclaw_secrets_audit": await gateway_openclaw_handlers.handle_get_openclaw_secrets_audit(self, websocket, data) elif msg_type == "get_openclaw_security_audit": await gateway_openclaw_handlers.handle_get_openclaw_security_audit(self, websocket, data) elif msg_type == "get_openclaw_daemon_status": await gateway_openclaw_handlers.handle_get_openclaw_daemon_status(self, websocket, data) elif msg_type == "get_openclaw_pairing": await gateway_openclaw_handlers.handle_get_openclaw_pairing(self, websocket, data) elif msg_type == "get_openclaw_qr": await gateway_openclaw_handlers.handle_get_openclaw_qr(self, websocket, data) elif msg_type == "get_openclaw_update_status": await gateway_openclaw_handlers.handle_get_openclaw_update_status(self, websocket, data) elif msg_type == "get_openclaw_models_aliases": await gateway_openclaw_handlers.handle_get_openclaw_models_aliases(self, websocket, data) elif msg_type == "get_openclaw_models_fallbacks": await gateway_openclaw_handlers.handle_get_openclaw_models_fallbacks(self, websocket, data) elif msg_type == "get_openclaw_models_image_fallbacks": await gateway_openclaw_handlers.handle_get_openclaw_models_image_fallbacks(self, websocket, data) elif msg_type == "get_openclaw_skill_update": await gateway_openclaw_handlers.handle_get_openclaw_skill_update(self, websocket, data) elif msg_type == "get_openclaw_workspace_files": await gateway_openclaw_handlers.handle_get_openclaw_workspace_files(self, websocket, data) elif msg_type == "get_openclaw_workspace_file": await gateway_openclaw_handlers.handle_get_openclaw_workspace_file(self, websocket, data) elif msg_type == "openclaw_resolve_session": await gateway_openclaw_handlers.handle_openclaw_resolve_session(self, websocket, data) elif msg_type == "openclaw_create_session": await gateway_openclaw_handlers.handle_openclaw_create_session(self, websocket, data) elif msg_type == "openclaw_send_message": await gateway_openclaw_handlers.handle_openclaw_send_message(self, websocket, data) elif msg_type == "openclaw_subscribe_session": await gateway_openclaw_handlers.handle_openclaw_subscribe_session(self, websocket, data) elif msg_type == "openclaw_unsubscribe_session": await gateway_openclaw_handlers.handle_openclaw_unsubscribe_session(self, websocket, data) elif msg_type == "openclaw_reset_session": await gateway_openclaw_handlers.handle_openclaw_reset_session(self, websocket, data) elif msg_type == "openclaw_delete_session": await gateway_openclaw_handlers.handle_openclaw_delete_session(self, websocket, data) except websockets.ConnectionClosed: pass except json.JSONDecodeError: pass finally: subscriber_map = getattr(self, "_openclaw_session_subscribers", None) if isinstance(subscriber_map, dict): subscriber_map.pop(websocket, None) async def _handle_get_stock_history( self, websocket: ServerConnection, data: Dict[str, Any], ): await gateway_stock_handlers.handle_get_stock_history(self, websocket, data) async def _handle_get_stock_explain_events( self, websocket: ServerConnection, data: Dict[str, Any], ): await gateway_stock_handlers.handle_get_stock_explain_events(self, websocket, data) async def _handle_get_stock_news( self, websocket: ServerConnection, data: Dict[str, Any], ): await gateway_stock_handlers.handle_get_stock_news(self, websocket, data) async def _handle_get_stock_news_for_date( self, websocket: ServerConnection, data: Dict[str, Any], ): await gateway_stock_handlers.handle_get_stock_news_for_date(self, websocket, data) async def _handle_get_stock_news_timeline( self, websocket: ServerConnection, data: Dict[str, Any], ): await gateway_stock_handlers.handle_get_stock_news_timeline(self, websocket, data) async def _handle_get_stock_news_categories( self, websocket: ServerConnection, data: Dict[str, Any], ): await gateway_stock_handlers.handle_get_stock_news_categories(self, websocket, data) async def _handle_get_stock_range_explain( self, websocket: ServerConnection, data: Dict[str, Any], ): await gateway_stock_handlers.handle_get_stock_range_explain(self, websocket, data) async def _handle_get_stock_insider_trades( self, websocket: ServerConnection, data: Dict[str, Any], ): await gateway_stock_handlers.handle_get_stock_insider_trades(self, websocket, data) async def _handle_get_stock_story( self, websocket: ServerConnection, data: Dict[str, Any], ): await gateway_stock_handlers.handle_get_stock_story(self, websocket, data) async def _handle_get_stock_similar_days( self, websocket: ServerConnection, data: Dict[str, Any], ): await gateway_stock_handlers.handle_get_stock_similar_days(self, websocket, data) async def _handle_get_stock_technical_indicators( self, websocket: ServerConnection, data: Dict[str, Any], ): await gateway_stock_handlers.handle_get_stock_technical_indicators(self, websocket, data) async def _handle_run_stock_enrich( self, websocket: ServerConnection, data: Dict[str, Any], ): await gateway_stock_handlers.handle_run_stock_enrich(self, websocket, data) async def _handle_start_backtest(self, data: Dict[str, Any]): if not self.is_backtest: return dates = data.get("dates", []) if dates and self._backtest_task is None: task = asyncio.create_task( self._run_backtest_dates(dates), ) task.add_done_callback(self._handle_backtest_exception) self._backtest_task = task async def _handle_manual_trigger( self, websocket: ServerConnection, data: Dict[str, Any], ) -> None: """Run one live trading cycle on demand.""" if self.is_backtest: await websocket.send( json.dumps( { "type": "error", "message": "Manual trigger is only available in live mode.", }, ensure_ascii=False, ), ) return if ( self._cycle_lock.locked() or ( self._manual_cycle_task is not None and not self._manual_cycle_task.done() ) ): await websocket.send( json.dumps( { "type": "error", "message": "A trading cycle is already running.", }, ensure_ascii=False, ), ) await self.state_sync.on_system_message("已有任务在运行,已忽略手动触发") return requested_date = data.get("date") await self.state_sync.on_system_message("收到手动触发请求,准备开始新一轮分析与决策") task = asyncio.create_task( self.on_strategy_trigger( date=requested_date or datetime.now().strftime("%Y-%m-%d"), ), ) task.add_done_callback(self._handle_manual_cycle_exception) self._manual_cycle_task = task async def _handle_reload_runtime_assets(self): await gateway_admin_handlers.handle_reload_runtime_assets(self) async def _handle_update_runtime_config( self, websocket: ServerConnection, data: Dict[str, Any], ) -> None: await gateway_admin_handlers.handle_update_runtime_config(self, websocket, data) async def _handle_update_watchlist( self, websocket: ServerConnection, data: Dict[str, Any], ) -> None: await gateway_admin_handlers.handle_update_watchlist(self, websocket, data) async def _handle_get_agent_skills( self, websocket: ServerConnection, data: Dict[str, Any], ) -> None: await gateway_admin_handlers.handle_get_agent_skills(self, websocket, data) async def _handle_get_agent_profile( self, websocket: ServerConnection, data: Dict[str, Any], ) -> None: await gateway_admin_handlers.handle_get_agent_profile(self, websocket, data) async def _handle_get_skill_detail( self, websocket: ServerConnection, data: Dict[str, Any], ) -> None: await gateway_admin_handlers.handle_get_skill_detail(self, websocket, data) async def _handle_create_agent_local_skill( self, websocket: ServerConnection, data: Dict[str, Any], ) -> None: await gateway_admin_handlers.handle_create_agent_local_skill(self, websocket, data) async def _handle_update_agent_local_skill( self, websocket: ServerConnection, data: Dict[str, Any], ) -> None: await gateway_admin_handlers.handle_update_agent_local_skill(self, websocket, data) async def _handle_delete_agent_local_skill( self, websocket: ServerConnection, data: Dict[str, Any], ) -> None: await gateway_admin_handlers.handle_delete_agent_local_skill(self, websocket, data) async def _handle_remove_agent_skill( self, websocket: ServerConnection, data: Dict[str, Any], ) -> None: await gateway_admin_handlers.handle_remove_agent_skill(self, websocket, data) async def _handle_update_agent_skill( self, websocket: ServerConnection, data: Dict[str, Any], ) -> None: await gateway_admin_handlers.handle_update_agent_skill(self, websocket, data) async def _handle_get_agent_workspace_file( self, websocket: ServerConnection, data: Dict[str, Any], ) -> None: await gateway_admin_handlers.handle_get_agent_workspace_file(self, websocket, data) async def _handle_update_agent_workspace_file( self, websocket: ServerConnection, data: Dict[str, Any], ) -> None: await gateway_admin_handlers.handle_update_agent_workspace_file(self, websocket, data) async def _handle_get_openclaw_status( self, websocket: ServerConnection, data: Dict[str, Any], ) -> None: await gateway_openclaw_handlers.handle_get_openclaw_status(self, websocket, data) async def _handle_get_openclaw_sessions( self, websocket: ServerConnection, data: Dict[str, Any], ) -> None: await gateway_openclaw_handlers.handle_get_openclaw_sessions(self, websocket, data) async def _handle_get_openclaw_session_detail( self, websocket: ServerConnection, data: Dict[str, Any], ) -> None: await gateway_openclaw_handlers.handle_get_openclaw_session_detail(self, websocket, data) async def _handle_get_openclaw_session_history( self, websocket: ServerConnection, data: Dict[str, Any], ) -> None: await gateway_openclaw_handlers.handle_get_openclaw_session_history(self, websocket, data) async def _handle_get_openclaw_cron( self, websocket: ServerConnection, data: Dict[str, Any], ) -> None: await gateway_openclaw_handlers.handle_get_openclaw_cron(self, websocket, data) async def _handle_get_openclaw_approvals( self, websocket: ServerConnection, data: Dict[str, Any], ) -> None: await gateway_openclaw_handlers.handle_get_openclaw_approvals(self, websocket, data) async def _handle_get_openclaw_agents( self, websocket: ServerConnection, data: Dict[str, Any], ) -> None: await gateway_openclaw_handlers.handle_get_openclaw_agents(self, websocket, data) async def _handle_get_openclaw_agents_presence( self, websocket: ServerConnection, data: Dict[str, Any], ) -> None: await gateway_openclaw_handlers.handle_get_openclaw_agents_presence(self, websocket, data) async def _handle_get_openclaw_skills( self, websocket: ServerConnection, data: Dict[str, Any], ) -> None: await gateway_openclaw_handlers.handle_get_openclaw_skills(self, websocket, data) async def _handle_get_openclaw_models( self, websocket: ServerConnection, data: Dict[str, Any], ) -> None: await gateway_openclaw_handlers.handle_get_openclaw_models(self, websocket, data) async def _handle_get_openclaw_workspace_files( self, websocket: ServerConnection, data: Dict[str, Any], ) -> None: await gateway_openclaw_handlers.handle_get_openclaw_workspace_files(self, websocket, data) @staticmethod def _normalize_watchlist(raw_tickers: Any) -> List[str]: return gateway_runtime_support.normalize_watchlist(raw_tickers) @staticmethod def _normalize_agent_workspace_filename(raw_name: Any) -> Optional[str]: return gateway_runtime_support.normalize_agent_workspace_filename( raw_name, allowlist=EDITABLE_AGENT_WORKSPACE_FILES, ) def _apply_runtime_config( self, runtime_config: Dict[str, Any], ) -> Dict[str, Any]: return gateway_runtime_support.apply_runtime_config(self, runtime_config) def _sync_runtime_state(self) -> None: gateway_runtime_support.sync_runtime_state(self) def _schedule_watchlist_market_store_refresh( self, tickers: List[str], ) -> None: gateway_cycle_support.schedule_watchlist_market_store_refresh(self, tickers) async def _refresh_market_store_for_watchlist( self, tickers: List[str], ) -> None: await gateway_cycle_support.refresh_market_store_for_watchlist(self, tickers) async def broadcast(self, message: Dict[str, Any]): """Broadcast message to all connected clients""" if not self.connected_clients: return message_json = json.dumps(message, ensure_ascii=False, default=str) async with self.lock: tasks = [ self._send_to_client(client, message_json) for client in self.connected_clients.copy() ] if tasks: await asyncio.gather(*tasks, return_exceptions=True) async def _send_to_client( self, client: ServerConnection, message: str, ): try: await client.send(message) except websockets.ConnectionClosed: async with self.lock: self.connected_clients.discard(client) async def _market_status_monitor(self): await gateway_cycle_support.market_status_monitor(self) async def _update_and_broadcast_live_returns(self): await gateway_cycle_support.update_and_broadcast_live_returns(self) async def on_strategy_trigger(self, date: str): await gateway_cycle_support.on_strategy_trigger(self, date) async def on_heartbeat_trigger(self, date: str): await gateway_cycle_support.on_heartbeat_trigger(self, date) async def _run_backtest_cycle(self, date: str, tickers: List[str]): await gateway_cycle_support.run_backtest_cycle(self, date, tickers) async def _run_live_cycle(self, date: str, tickers: List[str]): await gateway_cycle_support.run_live_cycle(self, date, tickers) async def _finalize_cycle(self, date: str): await gateway_cycle_support.finalize_cycle(self, date) async def _get_market_caps( self, tickers: List[str], date: str, ) -> Dict[str, float]: return await gateway_cycle_support.get_market_caps(self, tickers, date) async def _broadcast_portfolio_updates( self, result: Dict[str, Any], prices: Dict[str, float], ): await gateway_cycle_support.broadcast_portfolio_updates(self, result, prices) def _save_cycle_results( self, result: Dict[str, Any], date: str, prices: Dict[str, float], settlement_result: Optional[Dict[str, Any]] = None, ): gateway_cycle_support.save_cycle_results( self, result, date, prices, settlement_result, ) async def _run_backtest_dates(self, dates: List[str]): await gateway_cycle_support.run_backtest_dates(self, dates) def _handle_backtest_exception(self, task: asyncio.Task): gateway_cycle_support.handle_backtest_exception(self, task) def _handle_manual_cycle_exception(self, task: asyncio.Task): gateway_cycle_support.handle_manual_cycle_exception(self, task) def set_backtest_dates(self, dates: List[str]): gateway_cycle_support.set_backtest_dates(self, dates) def stop(self): gateway_cycle_support.stop_gateway(self)