# -*- coding: utf-8 -*- """Runtime/state support helpers extracted from the main Gateway module.""" from __future__ import annotations from typing import Any from backend.data.provider_utils import normalize_symbol def _normalize_schedule_mode(value: Any) -> str: mode = str(value or "daily").strip().lower() if mode == "intraday": return "interval" return mode or "daily" def normalize_watchlist(raw_tickers: Any) -> list[str]: """Parse watchlist payloads from websocket messages.""" if raw_tickers is None: return [] if isinstance(raw_tickers, str): candidates = raw_tickers.split(",") elif isinstance(raw_tickers, list): candidates = raw_tickers else: candidates = [raw_tickers] tickers: list[str] = [] for candidate in candidates: symbol = normalize_symbol(str(candidate).strip().strip("\"'")) if symbol and symbol not in tickers: tickers.append(symbol) return tickers def normalize_agent_workspace_filename( raw_name: Any, *, allowlist: set[str], ) -> str | None: """Restrict editable workspace files to a safe allowlist.""" filename = str(raw_name or "").strip() if filename in allowlist: return filename return None def apply_runtime_config(gateway: Any, runtime_config: dict[str, Any]) -> dict[str, Any]: """Apply runtime config to gateway-owned services and state.""" warnings: list[str] = [] ticker_changes = gateway.market_service.update_tickers( runtime_config.get("tickers", []), ) gateway.config["tickers"] = ticker_changes["active"] gateway.pipeline.max_comm_cycles = int(runtime_config["max_comm_cycles"]) gateway.config["max_comm_cycles"] = gateway.pipeline.max_comm_cycles gateway.config["schedule_mode"] = _normalize_schedule_mode( runtime_config.get( "schedule_mode", gateway.config.get("schedule_mode", "daily"), ), ) gateway.config["interval_minutes"] = int( runtime_config.get( "interval_minutes", gateway.config.get("interval_minutes", 60), ), ) gateway.config["trigger_time"] = runtime_config.get( "trigger_time", gateway.config.get("trigger_time", "09:30"), ) if gateway.scheduler: gateway.scheduler.reconfigure( mode=gateway.config["schedule_mode"], trigger_time=gateway.config["trigger_time"], interval_minutes=gateway.config["interval_minutes"], ) pm_apply_result = gateway.pipeline.pm.apply_runtime_portfolio_config( margin_requirement=runtime_config["margin_requirement"], ) gateway.config["margin_requirement"] = gateway.pipeline.pm.portfolio.get( "margin_requirement", runtime_config["margin_requirement"], ) requested_initial_cash = float(runtime_config["initial_cash"]) current_initial_cash = float(gateway.storage.initial_cash) initial_cash_applied = requested_initial_cash == current_initial_cash if not initial_cash_applied: if ( gateway.storage.can_apply_initial_cash() and gateway.pipeline.pm.can_apply_initial_cash() ): initial_cash_applied = gateway.storage.apply_initial_cash( requested_initial_cash, ) if initial_cash_applied: gateway.pipeline.pm.apply_runtime_portfolio_config( initial_cash=requested_initial_cash, ) gateway.config["initial_cash"] = gateway.storage.initial_cash else: warnings.append( "initial_cash changed in BOOTSTRAP.md but was not applied " "because the run already has positions, margin usage, or trades.", ) requested_enable_memory = bool(runtime_config["enable_memory"]) current_enable_memory = bool(gateway.config.get("enable_memory", False)) if requested_enable_memory != current_enable_memory: warnings.append( "enable_memory changed in BOOTSTRAP.md but still requires a restart " "because long-term memory contexts are created at startup.", ) sync_runtime_state(gateway) return { "runtime_config_requested": runtime_config, "runtime_config_applied": { "tickers": list(gateway.config.get("tickers", [])), "schedule_mode": gateway.config.get("schedule_mode", "daily"), "interval_minutes": gateway.config.get("interval_minutes", 60), "trigger_time": gateway.config.get("trigger_time", "09:30"), "initial_cash": gateway.storage.initial_cash, "margin_requirement": gateway.config["margin_requirement"], "max_comm_cycles": gateway.config["max_comm_cycles"], "enable_memory": gateway.config.get("enable_memory", False), }, "runtime_config_status": { "tickers": True, "schedule_mode": True, "interval_minutes": True, "trigger_time": True, "initial_cash": initial_cash_applied, "margin_requirement": pm_apply_result["margin_requirement"], "max_comm_cycles": True, "enable_memory": requested_enable_memory == current_enable_memory, }, "ticker_changes": ticker_changes, "runtime_config_warnings": warnings, } def sync_runtime_state(gateway: Any) -> None: """Refresh persisted state after runtime config changes.""" gateway.state_sync.update_state("tickers", gateway.config.get("tickers", [])) gateway.state_sync.update_state( "runtime_config", { "tickers": gateway.config.get("tickers", []), "schedule_mode": gateway.config.get("schedule_mode", "daily"), "interval_minutes": gateway.config.get("interval_minutes", 60), "trigger_time": gateway.config.get("trigger_time", "09:30"), "initial_cash": gateway.storage.initial_cash, "margin_requirement": gateway.config.get("margin_requirement"), "max_comm_cycles": gateway.config.get("max_comm_cycles"), "enable_memory": gateway.config.get("enable_memory", False), }, ) gateway.storage.update_server_state_from_dashboard(gateway.state_sync.state) gateway.state_sync.save_state()