后端: - 拆分出 agent_service, runtime_service, trading_service, news_service - Gateway 模块化拆分 (gateway_*.py) - 添加 domains/ 领域层 - 新增 control_client, runtime_client - 更新 start-dev.sh 支持 split 服务模式 前端: - 完善 API 服务层 (newsApi, tradingApi) - 更新 vite.config.js - Explain 组件优化 测试: - 添加多个服务 app 测试 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
175 lines
6.5 KiB
Python
175 lines
6.5 KiB
Python
# -*- 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_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"] = 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 and dashboard 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()
|
|
|
|
gateway._dashboard.tickers = list(gateway.config.get("tickers", []))
|
|
gateway._dashboard.initial_cash = gateway.storage.initial_cash
|
|
gateway._dashboard.enable_memory = bool(gateway.config.get("enable_memory", False))
|
|
|
|
summary = gateway.storage.load_file("summary") or {}
|
|
holdings = gateway.storage.load_file("holdings") or []
|
|
trades = gateway.storage.load_file("trades") or []
|
|
gateway._dashboard.update(
|
|
portfolio=summary,
|
|
holdings=holdings,
|
|
trades=trades,
|
|
)
|