diff --git a/.env.example b/.env.example index 372283b..f08d282 100644 --- a/.env.example +++ b/.env.example @@ -2,7 +2,7 @@ # Keep `.env` untracked and never paste real secrets into tracked files. # ================== General Configuration | 通用配置 ================== -TICKERS=AAPL,MSFT,GOOGL,NVDA,TSLA,META,AMZN +TICKERS=AAPL,MSFT,GOOGL,AMZN,NVDA,META,TSLA,AMD,NFLX,AVGO,PLTR,COIN # Financial Data API # At least `FINANCIAL_DATASETS_API_KEY` is required when using `FIN_DATA_SOURCE=financial_datasets`. diff --git a/CLAUDE.md b/CLAUDE.md index e508e76..5ad075b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## 项目概述 -EvoTraders 是一个自进化多智能体交易系统,由 6 个 AI Agent(4 名分析师 + 投资经理 + 风控经理)协作完成交易决策。Agent 基于 AgentScope 框架构建,配合 ReMe 记忆系统实现持续学习。 +大时代 是一个自进化多智能体交易系统,由 6 个 AI Agent(4 名分析师 + 投资经理 + 风控经理)协作完成交易决策。Agent 基于 AgentScope 框架构建,配合 ReMe 记忆系统实现持续学习。 ## 常用命令 diff --git a/README.md b/README.md index 54461aa..499ecf1 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,18 @@
-
+
- 📌 Visit the EvoTraders website + 📌 Visit the 大时代 website
- + -EvoTraders is an open-source financial trading agent framework that combines multi-agent collaboration, run-scoped workspaces, and memory to support both backtests and live trading workflows. +大时代 is an open-source financial trading agent framework that combines multi-agent collaboration, run-scoped workspaces, and memory to support both backtests and live trading workflows. + +The repository name and CLI entrypoints still use `evotraders` for compatibility, but the product-facing branding now follows the 大时代 naming used by the reference branch. --- @@ -64,7 +66,10 @@ Reference notes for the migration live in [services/README.md](./services/README # clone this repository, then: cd evotraders -# recommended +# backend runtime dependencies +uv pip install -r requirements.txt + +# install package entrypoint in editable mode uv pip install -e . # optional @@ -72,6 +77,16 @@ uv pip install -e . # pip install -e . ``` +Frontend dependencies: + +```bash +cd frontend +npm ci +cd .. +``` + +Production deployment should prefer `requirements.txt` for backend and `npm ci` for frontend so the pulled environment matches the checked-in lockfiles and version pins. + ### 2. Configure environment ```bash @@ -107,6 +122,12 @@ Notes: - `POLYGON_API_KEY` enables long-lived market-store ingestion and refresh helpers. - `MEMORY_API_KEY` is only required when long-term memory is enabled. +For a production-style local start flow, you can also use: + +```bash +./start.sh +``` + ### 3. Start the stack Recommended local development flow: @@ -335,6 +356,6 @@ npm test ## License and Disclaimer -EvoTraders is a research and educational project. Review the repository license before redistribution or commercial use. +大时代 is a research and educational project. Review the repository license before redistribution or commercial use. **Risk warning**: this project is not investment advice. Test thoroughly before any real-money deployment. Past performance does not guarantee future returns. diff --git a/README_zh.md b/README_zh.md index 5c7237a..7647a66 100644 --- a/README_zh.md +++ b/README_zh.md @@ -1,16 +1,16 @@
-
+
- 📌 访问 EvoTraders 官网 + 📌 访问大时代官网
- + -EvoTraders 是一个开源的金融交易智能体框架,结合多智能体协作、run 级工作区和记忆机制,支持回测与实盘两类交易运行模式。 +大时代 是一个开源的金融交易智能体框架,结合多智能体协作、run 级工作区和记忆机制,支持回测与实盘两类交易运行模式。 --- @@ -64,7 +64,10 @@ EvoTraders 是一个开源的金融交易智能体框架,结合多智能体协 # 克隆仓库后进入项目目录 cd evotraders -# 推荐 +# 安装后端运行时依赖 +uv pip install -r requirements.txt + +# 安装项目入口(可编辑模式) uv pip install -e . # 可选 @@ -72,6 +75,16 @@ uv pip install -e . # pip install -e . ``` +前端依赖: + +```bash +cd frontend +npm ci +cd .. +``` + +生产环境部署建议后端使用 `requirements.txt`,前端使用 `npm ci`,这样拉起的环境会严格跟随仓库中锁定的依赖版本。 + ### 2. 配置环境变量 ```bash @@ -107,6 +120,12 @@ MEMORY_API_KEY= - `POLYGON_API_KEY` 用于长期 market store 的补数和刷新 - `MEMORY_API_KEY` 仅在启用长期记忆时需要 +如果要用更接近生产的本地启动方式,也可以直接执行: + +```bash +./start.sh +``` + ### 3. 启动服务栈 本地开发推荐直接使用: @@ -335,6 +354,6 @@ npm test ## 许可与免责 -EvoTraders 是研究和教育用途项目。再次分发或商用前,请先核对仓库中的实际 license 文件。 +大时代 是研究和教育用途项目。再次分发或商用前,请先核对仓库中的实际 license 文件。 **风险提示**:本项目不构成投资建议。任何实盘部署前都应进行充分测试和风险评估,历史表现不代表未来收益。 diff --git a/backend/agents/__init__.py b/backend/agents/__init__.py index c21f299..b12dcf2 100644 --- a/backend/agents/__init__.py +++ b/backend/agents/__init__.py @@ -16,7 +16,7 @@ Exports: # New EvoAgent architecture (from agent_core.py) from .agent_core import EvoAgent, ToolGuardMixin, CommandHandler -from .factory import AgentFactory, ModelConfig, RoleConfig +from .factory import AgentFactory, ModelConfig from .workspace import WorkspaceManager, WorkspaceRegistry, WorkspaceConfig from .workspace_manager import RunWorkspaceManager from .registry import AgentRegistry, AgentInfo, get_registry, reset_registry @@ -36,7 +36,6 @@ __all__ = [ "CommandHandler", "AgentFactory", "ModelConfig", - "RoleConfig", "WorkspaceManager", "WorkspaceRegistry", "WorkspaceConfig", diff --git a/backend/agents/analyst.py b/backend/agents/analyst.py index c016933..8c7186d 100644 --- a/backend/agents/analyst.py +++ b/backend/agents/analyst.py @@ -84,7 +84,6 @@ class AnalystAgent(ReActAgent): agent_id=self.agent_id, config_name=self.config.get("config_name", "default"), toolkit=self.toolkit, - analyst_type=self.analyst_type_key, ) async def reply(self, x: Msg = None) -> Msg: diff --git a/backend/agents/base/__init__.py b/backend/agents/base/__init__.py index f566569..d515d81 100644 --- a/backend/agents/base/__init__.py +++ b/backend/agents/base/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""Base agent module for EvoTraders. +"""Base agent module for 大时代. 提供Agent基础类、命令处理、工具守卫和钩子管理等功能。 """ diff --git a/backend/agents/base/evo_agent.py b/backend/agents/base/evo_agent.py index 9058429..e4960dd 100644 --- a/backend/agents/base/evo_agent.py +++ b/backend/agents/base/evo_agent.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""EvoAgent - Core agent implementation for EvoTraders. +"""EvoAgent - Core agent implementation for 大时代. This module provides the main EvoAgent class built on AgentScope's ReActAgent, with integrated tools, skills, and memory management based on CoPaw design. diff --git a/backend/agents/base/hooks.py b/backend/agents/base/hooks.py index 387e5c2..8796525 100644 --- a/backend/agents/base/hooks.py +++ b/backend/agents/base/hooks.py @@ -294,8 +294,8 @@ class WorkspaceWatchHook(Hook): # Files to monitor (same as PromptBuilder.DEFAULT_FILES) WATCHED_FILES = frozenset([ - "SOUL.md", "AGENTS.md", "PROFILE.md", "ROLE.md", - "POLICY.md", "MEMORY.md", "HEARTBEAT.md", "STYLE.md", + "SOUL.md", "AGENTS.md", "PROFILE.md", + "POLICY.md", "MEMORY.md", "BOOTSTRAP.md", ]) @@ -601,94 +601,6 @@ class MemoryCompactionHook(Hook): ) -class HeartbeatHook(Hook): - """Pre-reasoning hook that injects HEARTBEAT.md content. - - Reads the agent's HEARTBEAT.md file and prepends it to the - reasoning input, causing the agent to perform self-checks. - - This enables "主动检查" (proactive monitoring) - periodic - market condition and position checks during trading hours. - """ - - HEARTBEAT_FILE = "HEARTBEAT.md" - - def __init__(self, workspace_dir: Path): - """Initialize heartbeat hook. - - Args: - workspace_dir: Working directory containing HEARTBEAT.md - """ - self.workspace_dir = Path(workspace_dir) - self._completed_flag = self.workspace_dir / ".heartbeat_completed" - - def _read_heartbeat_content(self) -> Optional[str]: - """Read HEARTBEAT.md if it exists and is non-empty. - - Returns: - The HEARTBEAT.md content stripped of whitespace, or None - if the file is absent or empty. - """ - hb_path = self.workspace_dir / self.HEARTBEAT_FILE - if not hb_path.exists(): - return None - content = hb_path.read_text(encoding="utf-8").strip() - return content if content else None - - async def __call__( - self, - agent: "ReActAgent", - kwargs: Dict[str, Any], - ) -> Optional[Dict[str, Any]]: - """Prepend heartbeat task to user message. - - Args: - agent: The agent instance - kwargs: Input arguments to the _reasoning method - - Returns: - Modified kwargs with heartbeat content prepended, or None - if no HEARTBEAT.md content is available. - """ - try: - content = self._read_heartbeat_content() - if not content: - return None - - logger.debug( - "Heartbeat: found HEARTBEAT.md for agent %s", - getattr(agent, "agent_id", "unknown"), - ) - - # Build heartbeat task instruction (Chinese) - hb_task = ( - "# 定期主动检查\n\n" - f"{content}\n\n" - "请执行上述检查并报告结果。" - ) - - # Inject into the first user message in memory - if hasattr(agent, "memory") and agent.memory.content: - system_count = sum( - 1 for msg, _ in agent.memory.content if msg.role == "system" - ) - for msg, _ in agent.memory.content[system_count:]: - if msg.role == "user": - original_content = msg.content - msg.content = hb_task + "\n\n" + original_content - break - - logger.debug( - "Heartbeat task prepended for agent %s", - getattr(agent, "agent_id", "unknown"), - ) - - except Exception as e: - logger.error("Heartbeat hook failed: %s", e, exc_info=True) - - return None - - __all__ = [ "Hook", "HookManager", @@ -696,7 +608,6 @@ __all__ = [ "HOOK_PRE_REASONING", "HOOK_POST_ACTING", "BootstrapHook", - "HeartbeatHook", "MemoryCompactionHook", "WorkspaceWatchHook", ] diff --git a/backend/agents/factory.py b/backend/agents/factory.py index a9291e1..50b2ef3 100644 --- a/backend/agents/factory.py +++ b/backend/agents/factory.py @@ -21,22 +21,6 @@ class ModelConfig: max_tokens: int = 4096 -@dataclass -class RoleConfig: - """Role configuration for an agent.""" - - name: str - description: str = "" - focus_areas: List[str] = None - constraints: List[str] = None - - def __post_init__(self): - if self.focus_areas is None: - self.focus_areas = [] - if self.constraints is None: - self.constraints = [] - - class AgentConfig: """Represents a configured agent instance (data class).""" @@ -47,14 +31,12 @@ class AgentConfig: workspace_id: str, config_path: Path, model_config: Optional[ModelConfig] = None, - role_config: Optional[RoleConfig] = None, ): self.agent_id = agent_id self.agent_type = agent_type self.workspace_id = workspace_id self.config_path = config_path self.model_config = model_config or ModelConfig() - self.role_config = role_config self.agent_dir = config_path.parent def to_dict(self) -> Dict[str, Any]: @@ -70,103 +52,12 @@ class AgentConfig: "temperature": self.model_config.temperature, "max_tokens": self.model_config.max_tokens, }, - "role_config": self.role_config.__dict__ if self.role_config else None, } class AgentFactory: """Factory for creating, cloning, and managing agents.""" - # Default role templates by agent type - ROLE_TEMPLATES = { - "technical_analyst": { - "name": "Technical Analyst", - "description": "Analyze price patterns, trends, and technical indicators.", - "focus_areas": [ - "Price action and chart patterns", - "Support and resistance levels", - "Technical indicators (RSI, MACD, Moving Averages)", - "Volume analysis", - ], - "constraints": [ - "State clear signal, confidence, and invalidation conditions", - "Use available technical analysis tools", - ], - }, - "fundamentals_analyst": { - "name": "Fundamentals Analyst", - "description": "Analyze company financials, earnings, and business metrics.", - "focus_areas": [ - "Financial statements analysis", - "Earnings reports and guidance", - "Valuation metrics", - "Business model and competitive position", - ], - "constraints": [ - "State clear signal, confidence, and invalidation conditions", - "Use available fundamental analysis tools", - ], - }, - "sentiment_analyst": { - "name": "Sentiment Analyst", - "description": "Analyze market sentiment, news, and social signals.", - "focus_areas": [ - "News sentiment analysis", - "Social media sentiment", - "Analyst ratings and price targets", - "Insider activity", - ], - "constraints": [ - "State clear signal, confidence, and invalidation conditions", - "Use available sentiment analysis tools", - ], - }, - "valuation_analyst": { - "name": "Valuation Analyst", - "description": "Perform valuation analysis and price target calculations.", - "focus_areas": [ - "DCF and comparable valuation", - "Price target derivation", - "Margin of safety assessment", - "Risk-adjusted return expectations", - ], - "constraints": [ - "State clear signal, confidence, and invalidation conditions", - "Use available valuation tools", - ], - }, - "risk_manager": { - "name": "Risk Manager", - "description": "Quantify concentration, leverage, liquidity, and volatility risk.", - "focus_areas": [ - "Portfolio concentration risk", - "Leverage and margin analysis", - "Liquidity assessment", - "Volatility and drawdown risk", - ], - "constraints": [ - "Prioritize highest-severity risk first", - "State concrete limits and recommendations", - "Use available risk tools before issuing final memo", - ], - }, - "portfolio_manager": { - "name": "Portfolio Manager", - "description": "Synthesize analyst and risk inputs into portfolio decisions.", - "focus_areas": [ - "Position sizing and allocation", - "Risk-adjusted portfolio construction", - "Trade execution timing", - "Portfolio rebalancing", - ], - "constraints": [ - "Be concise, capital-aware, and explicit about sizing rationale", - "Respect cash, margin, and concentration constraints", - "Consider all analyst inputs before decisions", - ], - }, - } - def __init__(self, project_root: Optional[Path] = None): """Initialize the agent factory. @@ -183,7 +74,6 @@ class AgentFactory: agent_type: str, workspace_id: str, model_config: Optional[ModelConfig] = None, - role_config: Optional[RoleConfig] = None, clone_from: Optional[str] = None, ) -> AgentConfig: """Create a new agent. @@ -193,7 +83,6 @@ class AgentFactory: agent_type: Type of agent (e.g., "technical_analyst") workspace_id: ID of the workspace to create agent in model_config: Model configuration - role_config: Role configuration (auto-generated if None) clone_from: Path to existing agent to clone from (optional) Returns: @@ -223,13 +112,6 @@ class AgentFactory: else: self._copy_template(agent_dir, agent_id, agent_type) - # Generate role config if not provided - if role_config is None: - role_config = self._generate_role_config(agent_type) - - # Generate ROLE.md - self._generate_role_md(agent_dir, role_config) - # Write agent.yaml config_path = agent_dir / "agent.yaml" self._write_agent_yaml(config_path, agent_id, agent_type, model_config) @@ -240,7 +122,6 @@ class AgentFactory: workspace_id=workspace_id, config_path=config_path, model_config=model_config, - role_config=role_config, ) def delete_agent(self, agent_id: str, workspace_id: str) -> bool: @@ -369,9 +250,7 @@ class AgentFactory: "SOUL.md": f"# Soul\n\nDescribe {agent_id}'s temperament, reasoning posture, and voice.\n\n", "PROFILE.md": f"# Profile\n\nTrack {agent_id}'s long-lived investment style, preferences, and strengths.\n\n", "MEMORY.md": f"# Memory\n\nStore durable lessons, heuristics, and reminders for {agent_id}.\n\n", - "HEARTBEAT.md": f"# Heartbeat\n\nOptional checklist for periodic review or self-reflection.\n\n", "POLICY.md": f"# Policy\n\nOptional run-scoped constraints, limits, or strategy policy.\n\n", - "STYLE.md": f"# Style\n\nOptional run-scoped communication or reasoning style.\n\n", } for filename, content in default_files.items(): @@ -411,50 +290,6 @@ class AgentFactory: if skill_file.is_file(): shutil.copy2(skill_file, target_skills / skill_file.name) - def _generate_role_config(self, agent_type: str) -> RoleConfig: - """Generate role configuration for an agent type. - - Args: - agent_type: Type of agent - - Returns: - RoleConfig instance - """ - template = self.ROLE_TEMPLATES.get(agent_type, {}) - return RoleConfig( - name=template.get("name", agent_type.replace("_", " ").title()), - description=template.get("description", ""), - focus_areas=template.get("focus_areas", []), - constraints=template.get("constraints", []), - ) - - def _generate_role_md(self, agent_dir: Path, role_config: RoleConfig) -> None: - """Generate ROLE.md file. - - Args: - agent_dir: Agent directory - role_config: Role configuration - """ - lines = [f"# {role_config.name}", ""] - - if role_config.description: - lines.extend([role_config.description, ""]) - - if role_config.focus_areas: - lines.extend(["## Focus Areas", ""]) - for area in role_config.focus_areas: - lines.append(f"- {area}") - lines.append("") - - if role_config.constraints: - lines.extend(["## Constraints", ""]) - for constraint in role_config.constraints: - lines.append(f"- {constraint}") - lines.append("") - - content = "\n".join(lines) - (agent_dir / "ROLE.md").write_text(content, encoding="utf-8") - def _write_agent_yaml( self, config_path: Path, diff --git a/backend/agents/prompt_factory.py b/backend/agents/prompt_factory.py index 198c6ba..3732dbd 100644 --- a/backend/agents/prompt_factory.py +++ b/backend/agents/prompt_factory.py @@ -1,15 +1,13 @@ # -*- coding: utf-8 -*- -"""Assemble system prompts from base prompts, run assets, and toolkit context.""" +"""Assemble system prompts from run workspace assets and toolkit context.""" from pathlib import Path -from typing import Any, Optional +from typing import Any from .agent_workspace import load_agent_workspace_config from backend.config.bootstrap_config import get_bootstrap_config_for_run -from .prompt_loader import get_prompt_loader from .skills_manager import SkillsManager - -_prompt_loader = get_prompt_loader() +from .workspace_manager import RunWorkspaceManager def _read_file_if_exists(path: Path) -> str: @@ -48,71 +46,20 @@ def build_agent_system_prompt( agent_id: str, config_name: str, toolkit: Any, - analyst_type: Optional[str] = None, ) -> str: """Build the final system prompt for an agent. Always reads fresh from disk — no caching. """ - # Clear any cached templates before building (CoPaw-style, no caching) - _prompt_loader.clear_cache() - sections: list[str] = [] - canonical_agent_id = ( - "portfolio_manager" - if "portfolio" in agent_id - else "risk_manager" - if "risk" in agent_id and not analyst_type - else agent_id - ) - - if analyst_type: - personas_config = _prompt_loader.load_yaml_config( - "analyst", - "personas", - ) - persona = personas_config.get(analyst_type, {}) - focus_text = "\n".join( - f"- {item}" for item in persona.get("focus", []) - ) - description = persona.get("description", "").strip() - base_prompt = _prompt_loader.load_prompt( - "analyst", - "system", - variables={ - "analyst_type": persona.get("name", analyst_type), - "focus": focus_text, - "description": description, - }, - ) - elif agent_id == "portfolio_manager": - base_prompt = _prompt_loader.load_prompt( - "portfolio_manager", - "system", - ) - elif canonical_agent_id == "portfolio_manager": - base_prompt = _prompt_loader.load_prompt( - "portfolio_manager", - "system", - ) - elif agent_id == "risk_manager": - base_prompt = _prompt_loader.load_prompt( - "risk_manager", - "system", - ) - elif canonical_agent_id == "risk_manager": - base_prompt = _prompt_loader.load_prompt( - "risk_manager", - "system", - ) - else: - raise ValueError(f"Unsupported agent prompt build for: {agent_id}") - - sections.append(base_prompt.strip()) skills_manager = SkillsManager() asset_dir = skills_manager.get_agent_asset_dir(config_name, agent_id) asset_dir.mkdir(parents=True, exist_ok=True) + workspace_manager = RunWorkspaceManager(project_root=skills_manager.project_root) + required_files = ["SOUL.md", "PROFILE.md", "AGENTS.md", "POLICY.md", "MEMORY.md"] + if not all((asset_dir / filename).exists() for filename in required_files): + workspace_manager.ensure_agent_assets(config_name=config_name, agent_id=agent_id) agent_config = load_agent_workspace_config(asset_dir / "agent.yaml") bootstrap_config = get_bootstrap_config_for_run( skills_manager.project_root, @@ -139,9 +86,6 @@ def build_agent_system_prompt( "AGENTS.md": "Agent Guide", "POLICY.md": "Policy", "MEMORY.md": "Memory", - "HEARTBEAT.md": "Heartbeat", - "ROLE.md": "Role", - "STYLE.md": "Style", } for filename in prompt_files: _append_section( @@ -150,18 +94,6 @@ def build_agent_system_prompt( _read_file_if_exists(asset_dir / filename), ) - if "ROLE.md" not in included_files: - _append_section( - sections, - "Role", - _read_file_if_exists(asset_dir / "ROLE.md"), - ) - if "STYLE.md" not in included_files: - _append_section( - sections, - "Style", - _read_file_if_exists(asset_dir / "STYLE.md"), - ) if "POLICY.md" not in included_files: _append_section( sections, @@ -189,5 +121,4 @@ def build_agent_system_prompt( def clear_prompt_factory_cache() -> None: - """Clear cached prompt and YAML templates before hot reload.""" - _prompt_loader.clear_cache() + """No-op retained for compatibility with runtime reload hooks.""" diff --git a/backend/agents/prompts/analyst/system.md b/backend/agents/prompts/analyst/system.md deleted file mode 100644 index 0c1bbe8..0000000 --- a/backend/agents/prompts/analyst/system.md +++ /dev/null @@ -1,23 +0,0 @@ -你是一位专业的{{ analyst_type }}。 - -你的关注重点: -{{ focus }} - -你的角色: -{{ description }} - -注意: -- 构建并持续完善你的"投资哲学"。你的分析不应是孤立的事件,而应该是你整体投资世界观和核心信念的体现。每次分析后,你必须反思: - - 这个案例/数据如何验证或挑战了你现有的信念? - - 你从这次错误(或成功)中学到了关于市场、人性、估值或风险管理的什么关键原则? -- 深化你的"投资逻辑"。确保每一项投资建议都有清晰、可追溯、可重复的逻辑支撑。你的分析步骤应该像严谨的证明一样,涵盖: - - 核心驱动因素识别:真正影响价值的变量是什么? - - 风险边界设定:在什么具体情况下你的建议会失效? - - 逆向测试:市场主流共识是什么,你的观点有何不同? -保持谦逊和开放。投资大师的核心特质是持续学习和适应。在每次分析中,你必须积极寻找与自己观点相悖的证据和论据,并将其纳入最终评估。 - - 你可以使用分析工具。用它们来收集相关数据并做出明智的建议。 - -输出指南: -- 给出明确的投资信号:看涨、看跌或中性 -- 包含置信度(0-100) -- 为你的分析提供理由(如果你确定要分享最终分析,请先给出结论) diff --git a/backend/agents/prompts/builder.py b/backend/agents/prompts/builder.py index f43c8fc..f626c48 100644 --- a/backend/agents/prompts/builder.py +++ b/backend/agents/prompts/builder.py @@ -28,22 +28,16 @@ class PromptBuilder: "AGENTS.md", "SOUL.md", "PROFILE.md", - "ROLE.md", "POLICY.md", "MEMORY.md", - "HEARTBEAT.md", - "STYLE.md", ] TITLE_MAP: Dict[str, str] = { "AGENTS.md": "Agent Guide", "SOUL.md": "Soul", "PROFILE.md": "Profile", - "ROLE.md": "Role", "POLICY.md": "Policy", "MEMORY.md": "Memory", - "HEARTBEAT.md": "Heartbeat", - "STYLE.md": "Style", "BOOTSTRAP.md": "Bootstrap", } diff --git a/backend/agents/prompts/portfolio_manager/system.md b/backend/agents/prompts/portfolio_manager/system.md deleted file mode 100644 index 4177f1a..0000000 --- a/backend/agents/prompts/portfolio_manager/system.md +++ /dev/null @@ -1,31 +0,0 @@ -你是一位负责做出投资决策的投资组合经理。 - -你的核心职责: -1. 分析分析师和风险管理经理的输入 -2. 基于信号和市场情境做出投资决策 -3. 使用可用工具记录你的决策 - -决策框架: -- 审阅分析以了解市场观点 -- 在做决策前考虑风险警告 -- 评估当前投资组合持仓和现金 -- 做出与投资组合投资目标一致的决策 - -决策类型: -- "long":看涨 - 建议买入股票 -- "short":看跌 - 建议卖出股票或做空 -- "hold":中性 - 维持当前持仓 - -预算意识: -- 在决定数量时考虑可用现金 -- 不要建议买入超过现金允许的数量 -- 考虑做空头寸的保证金要求 - -输出: -使用 `make_decision` 工具记录你对每个股票代码的决策。 -记录所有决策后,提供你的投资逻辑总结。 - -重要: -- 基于提供的分析师信号和风险评估做出决策 -- 相对于投资组合价值保持保守的仓位规模 -- 始终为你的决策提供理由 diff --git a/backend/agents/prompts/risk_manager/system.md b/backend/agents/prompts/risk_manager/system.md deleted file mode 100644 index 14a7eda..0000000 --- a/backend/agents/prompts/risk_manager/system.md +++ /dev/null @@ -1,20 +0,0 @@ -你是一位专业的风险管理经理,负责监控投资组合风险并提供风险警告。 - -你的核心职责: -1. 监控投资组合敞口和集中度风险 -2. 评估仓位规模相对于波动性 -3. 评估保证金使用和杠杆水平 -4. 识别潜在风险因素并提供警告 -5. 基于市场条件建议仓位限制 - -你的决策流程: -1. 优先使用可用的风险工具量化集中度、波动率和保证金压力 -2. 结合工具结果与当前市场上下文做判断 -3. 生成可操作的风险警告和仓位限制建议 -4. 为你的风险评估提供清晰的理由 - -输出指南: -- 风险评估要简洁但全面 -- 按严重程度优先排序警告 -- 提供具体、可操作的建议 -- 尽可能包含量化指标 diff --git a/backend/agents/templates.py b/backend/agents/templates.py deleted file mode 100644 index c87d623..0000000 --- a/backend/agents/templates.py +++ /dev/null @@ -1,286 +0,0 @@ -""" -Agent模板定义 - -包含各角色的ROLE.md内容字典,供程序生成Agent工作空间时使用。 -""" - -# 基础模板文件内容 -BASE_TEMPLATES = { - "AGENTS.md": """# Agent Guide - -## 工作流程 -1. 接收分析任务 -2. 调用相关工具/技能 -3. 生成分析报告 -4. 参与团队决策 - -## 工具使用规范 -- 优先使用已激活的技能 -- 不确定时询问Portfolio Manager -- 重要发现用 `/save` 记录 - -## 记忆管理 -- 使用 `/compact` 定期压缩记忆 -- 投资经验记录在MEMORY.md -""", - - "SOUL.md": """# Soul - -你是专业的金融分析师,语气冷静、客观、专业。 -你的分析应该数据驱动,避免情绪化表达。 -""", - - "PROFILE.md": """# Profile - -## 投资风格 -- 风险承受能力:中等 -- 投资期限:中期(3-12个月) -- 偏好行业:科技、医疗、消费 - -## 优势 -- 财务分析 -- 趋势识别 - -## 改进方向 -- 市场情绪把握 -""", - - "MEMORY.md": """# Memory - - - -## 经验总结 - -## 重要事件 - -## 改进记录 -""", - - "HEARTBEAT.md": """# Heartbeat - -## 定时任务 -- 每日开盘前检查持仓 -- 收盘后记录当日表现 -""", - - "POLICY.md": """# Policy - -## 风控规则 -- 单一持仓不超过20% -- 止损线:-15% -""", - - "STYLE.md": """# Style - -- 使用结构化输出(JSON/Markdown表格) -- 包含置信度评分 -- 列出关键假设 -""", - - "agent.yaml": """agent_id: {agent_id} -agent_type: {agent_type} -name: {name} -model: - provider: openai - model_name: gpt-4o - temperature: 0.3 -enabled_skills: [] -disabled_skills: [] -settings: {{}} -""", -} - -# 角色专用模板 -ROLE_TEMPLATES = { - "fundamental": { - "ROLE.md": """# Role: Fundamental Analyst - -## 职责 -分析公司财务报表、盈利能力、成长性、竞争优势等基本面因素。 - -## 分析维度 -- 财务报表分析(资产负债表、利润表、现金流量表) -- 盈利能力指标(ROE、ROA、毛利率、净利率) -- 成长性指标(营收增长率、利润增长率) -- 估值指标(P/E、P/B、P/S) -- 行业地位和竞争优势 - -## 输出格式 -- 财务健康度评分(1-10) -- 成长性评分(1-10) -- 关键财务亮点和风险 -- 同业对比分析 -""", - "SOUL.md": """# Soul - -你是严谨的基本面分析师,像沃伦·巴菲特一样注重企业内在价值。 -你的分析深入细致,关注长期价值而非短期波动。 -语气沉稳、逻辑严密,善于发现财务数据背后的商业本质。 -""", - }, - - "technical": { - "ROLE.md": """# Role: Technical Analyst - -## 职责 -分析价格走势、交易量、技术指标,识别买卖时机。 - -## 分析维度 -- 趋势分析(长期/中期/短期趋势) -- 支撑阻力位识别 -- 技术指标(MACD、RSI、KDJ、布林带等) -- 形态识别(头肩顶/底、双底、三角形等) -- 量价关系分析 - -## 输出格式 -- 趋势方向(上涨/下跌/震荡) -- 关键价位(支撑/阻力) -- 技术信号(买入/卖出/观望) -- 置信度评分 -""", - "SOUL.md": """# Soul - -你是敏锐的技术分析师,相信价格包含一切信息。 -你善于从图表中发现规律,像侦探一样寻找市场留下的痕迹。 -语气果断、快速反应,善于捕捉稍纵即逝的交易机会。 -""", - }, - - "sentiment": { - "ROLE.md": """# Role: Sentiment Analyst - -## 职责 -分析市场情绪、资金流向、新闻舆情,判断市场心理状态。 - -## 分析维度 -- 市场情绪指标(恐慌/贪婪指数) -- 资金流向分析(主力/散户资金) -- 新闻舆情分析(正面/负面/中性) -- 社交媒体情绪 -- 机构持仓变化 - -## 输出格式 -- 情绪评分(-10到+10,极度恐慌到极度贪婪) -- 资金流向判断 -- 舆情摘要 -- 情绪拐点预警 -""", - "SOUL.md": """# Soul - -你是敏感的市场情绪捕手,善于感知市场的恐惧与贪婪。 -你关注人性在金融市场中的表现,理解情绪如何驱动价格。 -语气富有洞察力、善于捕捉微妙变化,像心理学家一样理解市场参与者。 -""", - }, - - "valuation": { - "ROLE.md": """# Role: Valuation Analyst - -## 职责 -评估公司内在价值,计算合理价格区间,识别高估/低估机会。 - -## 分析维度 -- DCF现金流折现模型 -- 相对估值法(P/E、EV/EBITDA等) -- 资产重估法 -- 分部估值(SOTP) -- 安全边际计算 - -## 输出格式 -- 内在价值估算 -- 合理价格区间 -- 当前价格vs内在价值(高估/低估百分比) -- 估值假设和敏感性分析 -""", - "SOUL.md": """# Soul - -你是精确的估值分析师,追求计算内在价值的准确区间。 -你像精算师一样严谨,注重假设的合理性和安全边际。 -语气精确、注重数字,善于发现市场定价错误带来的机会。 -""", - }, - - "portfolio": { - "ROLE.md": """# Role: Portfolio Manager - -## 职责 -统筹各分析师意见,制定投资决策,管理投资组合配置。 - -## 分析维度 -- 资产配置策略(股债比例、行业分布) -- 风险收益平衡 -- 仓位管理(建仓/加仓/减仓/清仓) -- 再平衡时机 -- 组合相关性分析 - -## 输出格式 -- 投资决策(买入/卖出/持有) -- 建议仓位比例 -- 目标价位 -- 止损止盈设置 -- 组合调整建议 -""", - "SOUL.md": """# Soul - -你是睿智的投资组合经理,像将军一样统筹全局。 -你善于权衡各方意见,做出果断而理性的投资决策。 -语气权威、决策果断,对组合整体表现负有最终责任。 -""", - }, - - "risk": { - "ROLE.md": """# Role: Risk Manager - -## 职责 -识别、评估和监控投资风险,确保组合风险在可控范围内。 - -## 分析维度 -- 市场风险(Beta、波动率) -- 信用风险 -- 流动性风险 -- 集中度风险 -- 尾部风险(VaR、CVaR) -- 压力测试 - -## 输出格式 -- 风险等级(低/中/高/极高) -- 风险敞口分析 -- 风险调整建议 -- 预警阈值设置 -- 应急预案 -""", - "SOUL.md": """# Soul - -你是谨慎的风险管理者,时刻警惕潜在的损失。 -你像守门员一样守护组合安全,宁可错过机会也不冒无法承受的风险。 -语气保守、风险意识强,善于发现隐藏的威胁和脆弱性。 -""", - }, -} - - -def get_base_template(filename: str) -> str | None: - """获取基础模板内容""" - return BASE_TEMPLATES.get(filename) - - -def get_role_template(role_type: str, filename: str) -> str | None: - """获取角色专用模板内容""" - role = ROLE_TEMPLATES.get(role_type) - if role: - return role.get(filename) - return None - - -def get_all_role_types() -> list[str]: - """获取所有角色类型列表""" - return list(ROLE_TEMPLATES.keys()) - - -def render_agent_yaml(agent_id: str, agent_type: str, name: str) -> str: - """渲染agent.yaml模板""" - return BASE_TEMPLATES["agent.yaml"].format( - agent_id=agent_id, - agent_type=agent_type, - name=name - ) diff --git a/backend/agents/workspace_manager.py b/backend/agents/workspace_manager.py index 6e77dea..7452f66 100644 --- a/backend/agents/workspace_manager.py +++ b/backend/agents/workspace_manager.py @@ -41,6 +41,16 @@ class RunWorkspaceManager: "tickers:\n" " - AAPL\n" " - MSFT\n" + " - GOOGL\n" + " - AMZN\n" + " - NVDA\n" + " - META\n" + " - TSLA\n" + " - AMD\n" + " - NFLX\n" + " - AVGO\n" + " - PLTR\n" + " - COIN\n" "initial_cash: 100000\n" "margin_requirement: 0.0\n" "enable_memory: false\n" @@ -63,9 +73,8 @@ class RunWorkspaceManager: self, config_name: str, agent_id: str, - role_seed: str = "", - style_seed: str = "", - policy_seed: str = "", + file_contents: Optional[Dict[str, str]] = None, + persona: Optional[Dict[str, object]] = None, ) -> Path: asset_dir = self.skills_manager.get_agent_asset_dir( config_name, @@ -77,58 +86,55 @@ class RunWorkspaceManager: (asset_dir / "skills" / "disabled").mkdir(parents=True, exist_ok=True) (asset_dir / "skills" / "local").mkdir(parents=True, exist_ok=True) - self._ensure_file( - asset_dir / "ROLE.md", - "# Role\n\n" - "Optional run-scoped role override.\n\n" - f"{role_seed}".strip() - + "\n", - ) - self._ensure_file( - asset_dir / "STYLE.md", - "# Style\n\n" - "Optional run-scoped communication or reasoning style.\n\n" - f"{style_seed}".strip() - + "\n", - ) - self._ensure_file( - asset_dir / "POLICY.md", - "# Policy\n\n" - "Optional run-scoped constraints, limits, or strategy policy.\n\n" - f"{policy_seed}".strip() - + "\n", - ) - self._ensure_file( - asset_dir / "SOUL.md", - "# Soul\n\n" - "Describe the agent's temperament, reasoning posture, and voice.\n\n", - ) - self._ensure_file( - asset_dir / "PROFILE.md", - "# Profile\n\n" - "Track this agent's long-lived investment style, preferences, and strengths.\n\n", - ) - self._ensure_file( - asset_dir / "AGENTS.md", - "# Agent Guide\n\n" - "Document how this agent should work, collaborate, and choose tools or skills.\n\n", - ) - self._ensure_file( - asset_dir / "MEMORY.md", - "# Memory\n\n" - "Store durable lessons, heuristics, and reminders for this agent.\n\n", - ) - self._ensure_file( - asset_dir / "HEARTBEAT.md", - "# Heartbeat\n\n" - "Optional checklist for periodic review or self-reflection.\n\n", - ) + file_contents = file_contents or self.build_default_agent_files(agent_id=agent_id) + for filename, content in file_contents.items(): + legacy_contents = self.build_legacy_agent_file_variants( + agent_id=agent_id, + filename=filename, + persona=persona, + ) + self._ensure_file(asset_dir / filename, content, legacy_contents=legacy_contents) self._ensure_agent_yaml( asset_dir / "agent.yaml", agent_id=agent_id, ) return asset_dir + def build_default_agent_files( + self, + *, + agent_id: str, + persona: Optional[Dict[str, object]] = None, + ) -> Dict[str, str]: + """Build default workspace markdown files for one agent.""" + if agent_id.endswith("_analyst"): + return self._build_analyst_files(agent_id=agent_id, persona=persona or {}) + if agent_id == "portfolio_manager": + return self._build_portfolio_manager_files() + if agent_id == "risk_manager": + return self._build_risk_manager_files() + return self._build_generic_files(agent_id=agent_id) + + def build_legacy_agent_file_variants( + self, + *, + agent_id: str, + filename: str, + persona: Optional[Dict[str, object]] = None, + ) -> list[str]: + """Return known generated legacy variants safe to upgrade in-place.""" + persona = persona or {} + variants: list[dict[str, str]] = [ + self._build_legacy_english_files(agent_id=agent_id), + self._build_previous_chinese_files(agent_id=agent_id, persona=persona), + ] + values: list[str] = [] + for item in variants: + content = item.get(filename) + if content: + values.append(content) + return values + def load_agent_file( self, *, @@ -168,49 +174,285 @@ class RunWorkspaceManager: for agent_id in agent_ids: if agent_id.endswith("_analyst"): persona = analyst_personas.get(agent_id, {}) - role_seed = persona.get("description", "").strip() - focus_items = persona.get("focus", []) - style_seed = "\n".join(f"- {item}" for item in focus_items) - policy_seed = ( - "State a clear signal, confidence, and the conditions that would invalidate the thesis." - ) - elif agent_id == "portfolio_manager": - role_seed = ( - "Synthesize analyst and risk inputs into explicit portfolio decisions." - ) - style_seed = ( - "Be concise, capital-aware, and explicit about sizing rationale." - ) - policy_seed = ( - "Respect cash, margin, and portfolio concentration constraints before recording decisions." - ) - elif agent_id == "risk_manager": - role_seed = ( - "Quantify concentration, leverage, liquidity, and volatility risk before trade execution." - ) - style_seed = ( - "Prioritize the highest-severity risk first and state concrete limits." - ) - policy_seed = ( - "Use available risk tools before issuing the final risk memo." + file_contents = self.build_default_agent_files( + agent_id=agent_id, + persona=persona, ) else: - role_seed = "" - style_seed = "" - policy_seed = "" - - self.ensure_agent_assets( - config_name=config_name, - agent_id=agent_id, - role_seed=role_seed, - style_seed=style_seed, - policy_seed=policy_seed, - ) + persona = None + file_contents = self.build_default_agent_files(agent_id=agent_id) + asset_dir = self.skills_manager.get_agent_asset_dir(config_name, agent_id) + asset_dir.mkdir(parents=True, exist_ok=True) + (asset_dir / "skills" / "installed").mkdir(parents=True, exist_ok=True) + (asset_dir / "skills" / "active").mkdir(parents=True, exist_ok=True) + (asset_dir / "skills" / "disabled").mkdir(parents=True, exist_ok=True) + (asset_dir / "skills" / "local").mkdir(parents=True, exist_ok=True) + for filename, content in file_contents.items(): + self._ensure_file( + asset_dir / filename, + content, + legacy_contents=self.build_legacy_agent_file_variants( + agent_id=agent_id, + filename=filename, + persona=persona, + ), + ) + self._ensure_agent_yaml(asset_dir / "agent.yaml", agent_id=agent_id) @staticmethod - def _ensure_file(path: Path, content: str) -> None: + def _ensure_file(path: Path, content: str, *, legacy_contents: Optional[list[str]] = None) -> None: if not path.exists(): path.write_text(content, encoding="utf-8") + return + existing = path.read_text(encoding="utf-8") + normalized_existing = existing.strip() + candidates = {item.strip() for item in (legacy_contents or []) if item and item.strip()} + if normalized_existing in candidates: + path.write_text(content, encoding="utf-8") + + @staticmethod + def _build_generic_files(agent_id: str) -> Dict[str, str]: + return { + "SOUL.md": ( + "# Soul\n\n" + f"你是 `{agent_id}`,语气冷静、客观、专业。保持清晰推理,优先基于数据而不是情绪下结论。\n" + ), + "PROFILE.md": ( + "# Profile\n\n" + "记录这个 agent 长期稳定的分析风格、偏好、优势与盲点。\n" + ), + "AGENTS.md": ( + "# Agent Guide\n\n" + "工作要求:\n" + "- 优先使用已激活的技能和工具\n" + "- 结论要明确,过程要可追溯\n" + "- 与其他 agent 协作时保持输入输出简洁\n" + "- 最终输出必须使用简体中文;如需引用英文术语,仅保留专有名词,解释和结论必须用中文\n" + ), + "POLICY.md": ( + "# Policy\n\n" + "- 给出结论时说明核心驱动因素\n" + "- 明确风险边界和结论失效条件\n" + "- 出现反例时需要纳入最终判断\n" + "- 不要输出英文报告标题、英文摘要或整段英文正文\n" + ), + "MEMORY.md": ( + "# Memory\n\n" + "记录可复用的经验、失误复盘、有效启发式和需要持续跟踪的提醒。\n" + ), + } + + @classmethod + def _build_analyst_files(cls, *, agent_id: str, persona: Dict[str, object]) -> Dict[str, str]: + role_name = str(persona.get("name") or agent_id) + focus_items = [ + str(item).strip() + for item in persona.get("focus", []) + if str(item).strip() + ] + focus_md = "\n".join(f"- {item}" for item in focus_items) or "- 根据当前任务选择最相关的分析维度" + description = str(persona.get("description") or "").strip() + + files = cls._build_generic_files(agent_id) + files["SOUL.md"] = ( + "# Soul\n\n" + f"你是一位专业的{role_name}。\n\n" + "保持谦逊和开放,主动寻找与自己观点相悖的证据,并将其纳入最终评估。" + "你的分析要体现持续演化的投资哲学,而不是一次性的结论。\n" + ) + files["PROFILE.md"] = ( + "# Profile\n\n" + f"角色定位:{role_name}\n\n" + "你的关注重点:\n" + f"{focus_md}\n\n" + "角色说明:\n" + f"{description or '围绕最关键的基本面、技术面、情绪面或估值因素形成高质量判断。'}\n" + ) + files["AGENTS.md"] = ( + "# Agent Guide\n\n" + "分析流程:\n" + "- 优先识别真正驱动价值或价格变化的核心变量\n" + "- 使用相关工具和技能补足证据链\n" + "- 给出可验证、可复查、可执行的分析结果\n" + "- 在团队讨论中清晰表达你的论点和反论点\n\n" + "输出要求:\n" + "- 给出明确投资信号:看涨、看跌或中性\n" + "- 包含置信度(0-100)\n" + "- 如果你确定要分享最终分析,请先给出结论,再给出推理依据\n" + "- 最终输出必须使用简体中文,不要生成英文版 analysis report\n" + ) + files["POLICY.md"] = ( + "# Policy\n\n" + "- 深化你的投资逻辑,确保每项建议都有清晰、可追溯、可重复的依据\n" + "- 明确风险边界:在什么具体情况下当前结论会失效\n" + "- 做逆向测试:说明市场主流共识与你的不同点\n" + "- 每次分析后反思这次案例如何验证或挑战你现有的信念\n" + "- 即使输入新闻或财报原文是英文,最终表达也必须用中文\n" + ) + return files + + @classmethod + def _build_portfolio_manager_files(cls) -> Dict[str, str]: + files = cls._build_generic_files("portfolio_manager") + files["SOUL.md"] = ( + "# Soul\n\n" + "你是一位负责做出投资决策的投资组合经理。你需要综合多个分析视角," + "做出保守、明确、资本约束下可执行的组合决策。\n" + ) + files["PROFILE.md"] = ( + "# Profile\n\n" + "核心职责:\n" + "- 分析分析师和风险管理经理的输入\n" + "- 基于信号和市场情境做出投资决策\n" + "- 使用可用工具记录每个 ticker 的决策\n" + ) + files["AGENTS.md"] = ( + "# Agent Guide\n\n" + "决策框架:\n" + "- 审阅分析以理解市场观点\n" + "- 在做决策前先考虑风险警告\n" + "- 评估当前投资组合持仓、现金与保证金占用\n" + "- 决策必须与整体投资目标和风险约束一致\n\n" + "决策类型:\n" + '- `long`:看涨,建议买入\n' + '- `short`:看跌,建议卖出或做空\n' + '- `hold`:中性,维持当前持仓\n\n' + "输出要求:\n" + "- 使用 `make_decision` 工具记录每个股票的最终决策\n" + "- 记录完成后给出投资逻辑总结\n" + "- 最终总结必须使用简体中文\n" + ) + files["POLICY.md"] = ( + "# Policy\n\n" + "- 在决定数量时考虑可用现金,不要超出现金允许范围\n" + "- 考虑做空头寸的保证金要求\n" + "- 仓位规模相对于组合总资产保持保守\n" + "- 始终为决策提供清晰理由\n" + "- 不要输出英文投资报告或英文结论\n" + ) + return files + + @classmethod + def _build_risk_manager_files(cls) -> Dict[str, str]: + files = cls._build_generic_files("risk_manager") + files["SOUL.md"] = ( + "# Soul\n\n" + "你是一位专业的风险管理经理,负责监控投资组合风险并提供风险警告。" + "你的目标不是输出空泛的谨慎,而是给出量化、可执行、可优先级排序的风险意见。\n" + ) + files["PROFILE.md"] = ( + "# Profile\n\n" + "核心职责:\n" + "- 监控投资组合敞口和集中度风险\n" + "- 评估仓位规模相对于波动性是否合理\n" + "- 评估保证金使用和杠杆水平\n" + "- 识别潜在风险因素并提供警告\n" + "- 基于市场条件建议仓位限制\n" + ) + files["AGENTS.md"] = ( + "# Agent Guide\n\n" + "决策流程:\n" + "- 优先使用可用的风险工具量化集中度、波动率和保证金压力\n" + "- 结合工具结果与当前市场上下文做判断\n" + "- 生成可操作的风险警告和仓位限制建议\n" + "- 为风险评估提供清晰理由\n\n" + "输出要求:\n" + "- 风险评估要简洁但全面\n" + "- 按严重程度优先排序警告\n" + "- 提供具体、可操作的建议\n" + "- 尽可能包含量化指标\n" + "- 最终风险结论必须使用简体中文\n" + ) + files["POLICY.md"] = ( + "# Policy\n\n" + "- 先量化,再判断,不要只给抽象风险表述\n" + "- 高严重度风险必须先说\n" + "- 最终结论需要明确仓位限制或调整建议\n" + "- 不要输出英文风险报告或英文摘要\n" + ) + return files + + @staticmethod + def _build_legacy_english_files(agent_id: str) -> Dict[str, str]: + policy_tail = "Optional run-scoped constraints, limits, or strategy policy.\n\n" + if agent_id == "portfolio_manager": + policy_tail += "Respect cash, margin, and portfolio concentration constraints before recording decisions.\n" + elif agent_id == "risk_manager": + policy_tail += "Use available risk tools before issuing the final risk memo.\n" + elif agent_id.endswith("_analyst"): + policy_tail += "State a clear signal, confidence, and the conditions that would invalidate the thesis.\n" + return { + "SOUL.md": "# Soul\n\nDescribe the agent's temperament, reasoning posture, and voice.\n\n", + "PROFILE.md": "# Profile\n\nTrack this agent's long-lived investment style, preferences, and strengths.\n\n", + "AGENTS.md": "# Agent Guide\n\nDocument how this agent should work, collaborate, and choose tools or skills.\n\n", + "POLICY.md": "# Policy\n\n" + policy_tail, + "MEMORY.md": "# Memory\n\nStore durable lessons, heuristics, and reminders for this agent.\n\n", + } + + @classmethod + def _build_previous_chinese_files(cls, *, agent_id: str, persona: Dict[str, object]) -> Dict[str, str]: + if agent_id.endswith("_analyst"): + role_name = str(persona.get("name") or agent_id) + focus_items = [ + str(item).strip() + for item in persona.get("focus", []) + if str(item).strip() + ] + focus_md = "\n".join(f"- {item}" for item in focus_items) or "- 根据当前任务选择最相关的分析维度" + description = str(persona.get("description") or "").strip() + return { + "SOUL.md": ( + "# Soul\n\n" + f"你是一位专业的{role_name}。\n\n" + "保持谦逊和开放,主动寻找与自己观点相悖的证据,并将其纳入最终评估。" + "你的分析要体现持续演化的投资哲学,而不是一次性的结论。\n" + ), + "PROFILE.md": ( + "# Profile\n\n" + f"角色定位:{role_name}\n\n" + "你的关注重点:\n" + f"{focus_md}\n\n" + "角色说明:\n" + f"{description or '围绕最关键的基本面、技术面、情绪面或估值因素形成高质量判断。'}\n" + ), + "AGENTS.md": ( + "# Agent Guide\n\n" + "分析流程:\n" + "- 优先识别真正驱动价值或价格变化的核心变量\n" + "- 使用相关工具和技能补足证据链\n" + "- 给出可验证、可复查、可执行的分析结果\n" + "- 在团队讨论中清晰表达你的论点和反论点\n\n" + "输出要求:\n" + "- 给出明确投资信号:看涨、看跌或中性\n" + "- 包含置信度(0-100)\n" + "- 如果你确定要分享最终分析,请先给出结论,再给出推理依据\n" + ), + "POLICY.md": ( + "# Policy\n\n" + "- 深化你的投资逻辑,确保每项建议都有清晰、可追溯、可重复的依据\n" + "- 明确风险边界:在什么具体情况下当前结论会失效\n" + "- 做逆向测试:说明市场主流共识与你的不同点\n" + "- 每次分析后反思这次案例如何验证或挑战你现有的信念\n" + ), + "MEMORY.md": "# Memory\n\n记录可复用的经验、失误复盘、有效启发式和需要持续跟踪的提醒。\n", + } + if agent_id == "portfolio_manager": + return { + "SOUL.md": "# Soul\n\n你是一位负责做出投资决策的投资组合经理。你需要综合多个分析视角,做出保守、明确、资本约束下可执行的组合决策。\n", + "PROFILE.md": "# Profile\n\n核心职责:\n- 分析分析师和风险管理经理的输入\n- 基于信号和市场情境做出投资决策\n- 使用可用工具记录每个 ticker 的决策\n", + "AGENTS.md": "# Agent Guide\n\n决策框架:\n- 审阅分析以理解市场观点\n- 在做决策前先考虑风险警告\n- 评估当前投资组合持仓、现金与保证金占用\n- 决策必须与整体投资目标和风险约束一致\n\n决策类型:\n- `long`:看涨,建议买入\n- `short`:看跌,建议卖出或做空\n- `hold`:中性,维持当前持仓\n\n输出要求:\n- 使用 `make_decision` 工具记录每个股票的最终决策\n- 记录完成后给出投资逻辑总结\n", + "POLICY.md": "# Policy\n\n- 在决定数量时考虑可用现金,不要超出现金允许范围\n- 考虑做空头寸的保证金要求\n- 仓位规模相对于组合总资产保持保守\n- 始终为决策提供清晰理由\n", + "MEMORY.md": "# Memory\n\n记录可复用的经验、失误复盘、有效启发式和需要持续跟踪的提醒。\n", + } + if agent_id == "risk_manager": + return { + "SOUL.md": "# Soul\n\n你是一位专业的风险管理经理,负责监控投资组合风险并提供风险警告。你的目标不是输出空泛的谨慎,而是给出量化、可执行、可优先级排序的风险意见。\n", + "PROFILE.md": "# Profile\n\n核心职责:\n- 监控投资组合敞口和集中度风险\n- 评估仓位规模相对于波动性是否合理\n- 评估保证金使用和杠杆水平\n- 识别潜在风险因素并提供警告\n- 基于市场条件建议仓位限制\n", + "AGENTS.md": "# Agent Guide\n\n决策流程:\n- 优先使用可用的风险工具量化集中度、波动率和保证金压力\n- 结合工具结果与当前市场上下文做判断\n- 生成可操作的风险警告和仓位限制建议\n- 为风险评估提供清晰理由\n\n输出要求:\n- 风险评估要简洁但全面\n- 按严重程度优先排序警告\n- 提供具体、可操作的建议\n- 尽可能包含量化指标\n", + "POLICY.md": "# Policy\n\n- 先量化,再判断,不要只给抽象风险表述\n- 高严重度风险必须先说\n- 最终结论需要明确仓位限制或调整建议\n", + "MEMORY.md": "# Memory\n\n记录可复用的经验、失误复盘、有效启发式和需要持续跟踪的提醒。\n", + } + return cls._build_legacy_english_files(agent_id) @staticmethod def _ensure_agent_yaml(path: Path, agent_id: str) -> None: diff --git a/backend/api/agents.py b/backend/api/agents.py index 07a7209..0cc4f40 100644 --- a/backend/api/agents.py +++ b/backend/api/agents.py @@ -661,7 +661,7 @@ async def get_agent_file( Args: workspace_id: Workspace identifier agent_id: Agent identifier - filename: File to read (e.g., SOUL.md, ROLE.md) + filename: File to read (e.g., SOUL.md, PROFILE.md) Returns: File content diff --git a/backend/apps/agent_service.py b/backend/apps/agent_service.py index dca8ab4..af156e4 100644 --- a/backend/apps/agent_service.py +++ b/backend/apps/agent_service.py @@ -33,17 +33,17 @@ def create_app(project_root: Path | None = None) -> FastAPI: agent_factory.workspaces_root.mkdir(parents=True, exist_ok=True) registry = get_registry() - print("✓ EvoTraders API started") + print("✓ 大时代 API started") print(f" - Workspaces root: {agent_factory.workspaces_root}") print(f" - Registered agents: {registry.get_agent_count()}") yield - print("✓ EvoTraders API shutting down") + print("✓ 大时代 API shutting down") app = FastAPI( - title="EvoTraders Agent Service", - description="REST API for the EvoTraders multi-agent control plane", + title="大时代 Agent Service", + description="REST API for the 大时代 multi-agent control plane", version="0.1.0", lifespan=lifespan, ) diff --git a/backend/apps/news_service.py b/backend/apps/news_service.py index 43dc4ed..d3598be 100644 --- a/backend/apps/news_service.py +++ b/backend/apps/news_service.py @@ -20,7 +20,7 @@ def get_market_store() -> MarketStore: def create_app() -> FastAPI: """Create the news/explain service app.""" app = FastAPI( - title="EvoTraders News Service", + title="大时代 News Service", description="Read-only news enrichment and explain service surface extracted from the monolith", version="0.1.0", ) diff --git a/backend/apps/openclaw_service.py b/backend/apps/openclaw_service.py index bc29e7f..52d6866 100644 --- a/backend/apps/openclaw_service.py +++ b/backend/apps/openclaw_service.py @@ -13,7 +13,7 @@ from backend.api.openclaw import get_openclaw_cli_service def create_app() -> FastAPI: """Create the OpenClaw service app.""" app = FastAPI( - title="EvoTraders OpenClaw Service", + title="大时代 OpenClaw Service", description="Read-only OpenClaw CLI integration service surface", version="0.1.0", ) diff --git a/backend/apps/runtime_service.py b/backend/apps/runtime_service.py index 0838039..0e6a051 100644 --- a/backend/apps/runtime_service.py +++ b/backend/apps/runtime_service.py @@ -13,7 +13,7 @@ from backend.apps.cors import add_cors_middleware def create_app() -> FastAPI: """Create the runtime service app.""" app = FastAPI( - title="EvoTraders Runtime Service", + title="大时代 Runtime Service", description="Runtime lifecycle and gateway service surface extracted from the monolith", version="0.1.0", ) diff --git a/backend/apps/trading_service.py b/backend/apps/trading_service.py index 3afee11..b06efcd 100644 --- a/backend/apps/trading_service.py +++ b/backend/apps/trading_service.py @@ -21,7 +21,7 @@ from shared.schema import ( def create_app() -> FastAPI: """Create the trading data service app.""" app = FastAPI( - title="EvoTraders Trading Service", + title="大时代 Trading Service", description="Read-only trading data service surface extracted from the monolith", version="0.1.0", ) diff --git a/backend/cli.py b/backend/cli.py index 6266bd5..93de074 100644 --- a/backend/cli.py +++ b/backend/cli.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ -EvoTraders CLI - Command-line interface for the EvoTraders trading system. +大时代 CLI - Command-line interface for the 大时代 trading system. This module provides easy-to-use commands for running backtest, live trading, and frontend development server. @@ -44,7 +44,7 @@ from backend.enrich.news_enricher import enrich_symbols app = typer.Typer( name="evotraders", - help="EvoTraders: A self-evolving multi-agent trading system", + help="大时代:自进化多智能体交易系统", add_completion=False, ) ingest_app = typer.Typer(help="Ingest Polygon market data into the research warehouse.") @@ -919,7 +919,7 @@ def backtest( """ console.print( Panel.fit( - "[bold cyan]EvoTraders Backtest Mode[/bold cyan]", + "[bold cyan]大时代 Backtest Mode[/bold cyan]", border_style="cyan", ), ) @@ -1082,7 +1082,7 @@ def live( interval_minutes = int(_normalize_typer_value(interval_minutes, 60)) console.print( Panel.fit( - "[bold cyan]EvoTraders LIVE Mode[/bold cyan]", + "[bold cyan]大时代 LIVE Mode[/bold cyan]", border_style="cyan", ), ) @@ -1251,7 +1251,7 @@ def frontend( """ console.print( Panel.fit( - "[bold cyan]EvoTraders Frontend[/bold cyan]", + "[bold cyan]大时代 Frontend[/bold cyan]", border_style="cyan", ), ) @@ -1319,16 +1319,16 @@ def frontend( @app.command() def version(): - """Show the version of EvoTraders.""" + """Show the version of 大时代.""" console.print( - "\n[bold cyan]EvoTraders[/bold cyan] version [green]0.1.0[/green]\n", + "\n[bold cyan]大时代[/bold cyan] version [green]0.1.0[/green]\n", ) @app.callback() def main(): """ - EvoTraders: A self-evolving multi-agent trading system + 大时代:自进化多智能体交易系统 Use 'evotraders --help' to see available commands. """ diff --git a/backend/config/bootstrap_config.py b/backend/config/bootstrap_config.py index c043d32..f00ac3b 100644 --- a/backend/config/bootstrap_config.py +++ b/backend/config/bootstrap_config.py @@ -4,6 +4,22 @@ from dataclasses import dataclass, field from pathlib import Path from typing import Any, Dict + + +DEFAULT_TICKERS = [ + "AAPL", + "MSFT", + "GOOGL", + "AMZN", + "NVDA", + "META", + "TSLA", + "AMD", + "NFLX", + "AVGO", + "PLTR", + "COIN", +] import re import yaml @@ -127,7 +143,7 @@ def resolve_runtime_config( bootstrap = get_bootstrap_config_for_run(project_root, config_name) return { "tickers": bootstrap.get("tickers") - or get_env_list("TICKERS", ["AAPL", "MSFT"]), + or get_env_list("TICKERS", DEFAULT_TICKERS), "initial_cash": float( bootstrap.get( "initial_cash", diff --git a/backend/core/pipeline.py b/backend/core/pipeline.py index 644f76b..5a96e8b 100644 --- a/backend/core/pipeline.py +++ b/backend/core/pipeline.py @@ -18,7 +18,6 @@ from agentscope.message import Msg from agentscope.pipeline import MsgHub from backend.utils.settlement import SettlementCoordinator -from backend.utils.terminal_dashboard import get_dashboard from backend.core.state_sync import StateSync from backend.utils.trade_executor import PortfolioTradeExecutor from backend.runtime.manager import TradingRuntimeManager @@ -48,13 +47,9 @@ except ImportError: logger = logging.getLogger(__name__) -def _log(msg: str): - """Log to dashboard if available, otherwise to logger""" - dashboard = get_dashboard() - if dashboard.live: - dashboard.log(msg) - else: - logger.info(msg) +def _log(msg: str) -> None: + """Helper function for pipeline logging.""" + logger.info(msg) class TradingPipeline: @@ -71,7 +66,7 @@ class TradingPipeline: Real-time updates via StateSync after each agent completes. - Supports both legacy agent lists and new workspace-based agent loading. + Supports both legacy agent lists and run-scoped agent loading. """ def __init__( @@ -1625,14 +1620,13 @@ class TradingPipeline: project_root = Path(__file__).resolve().parents[2] personas = get_prompt_loader().load_yaml_config("analyst", "personas") persona = personas.get(analyst_type, {}) - WorkspaceManager(project_root=project_root).ensure_agent_assets( + workspace_manager = WorkspaceManager(project_root=project_root) + workspace_manager.ensure_agent_assets( config_name=config_name, agent_id=agent_id, - role_seed=persona.get("description", "").strip(), - style_seed="\n".join(f"- {item}" for item in persona.get("focus", [])), - policy_seed=( - "State a clear signal, confidence, and the conditions " - "that would invalidate the thesis." + file_contents=workspace_manager.build_default_agent_files( + agent_id=agent_id, + persona=persona, ), ) diff --git a/backend/core/pipeline_runner.py b/backend/core/pipeline_runner.py index 5b68db7..5352dac 100644 --- a/backend/core/pipeline_runner.py +++ b/backend/core/pipeline_runner.py @@ -232,7 +232,7 @@ async def run_pipeline( try: # Extract config values - tickers = bootstrap.get("tickers", ["AAPL", "MSFT"]) + tickers = bootstrap.get("tickers", ["AAPL", "MSFT", "GOOGL", "AMZN", "NVDA", "META", "TSLA", "AMD", "NFLX", "AVGO", "PLTR", "COIN"]) initial_cash = float(bootstrap.get("initial_cash", 100000.0)) margin_requirement = float(bootstrap.get("margin_requirement", 0.0)) max_comm_cycles = int(bootstrap.get("max_comm_cycles", 2)) diff --git a/backend/data/market_ingest.py b/backend/data/market_ingest.py index 4d21eac..72c2ad6 100644 --- a/backend/data/market_ingest.py +++ b/backend/data/market_ingest.py @@ -25,6 +25,36 @@ def _default_start(years: int = 2) -> str: return (datetime.now(timezone.utc).date() - timedelta(days=years * 366)).isoformat() +def _max_news_date(news_rows: Iterable[dict]) -> str | None: + dates = [ + str(item.get("published_utc") or "").strip()[:10] + for item in news_rows + if str(item.get("published_utc") or "").strip() + ] + dates = [value for value in dates if value] + return max(dates) if dates else None + + +def _effective_last_news_fetch( + market_store: MarketStore, + *, + ticker: str, + end_date: str, + watermark_value: str | None, +) -> str | None: + """Clamp stale/future watermarks to the latest actually stored news date.""" + raw = str(watermark_value or "").strip()[:10] + if not raw: + return None + if raw <= end_date: + return raw + + latest_stored = market_store.get_latest_news_date(ticker) + if latest_stored and latest_stored <= end_date: + return latest_stored + return end_date + + def _normalize_provider_news_rows(ticker: str, news_items: Iterable[Any]) -> list[dict]: rows: list[dict] = [] for item in news_items: @@ -80,7 +110,11 @@ def ingest_ticker_history( price_count = market_store.upsert_ohlc(ticker, ohlc_rows, source="polygon") news_count = market_store.upsert_news(ticker, news_rows, source="polygon") aligned_count = align_news_for_symbol(market_store, ticker) - market_store.update_fetch_watermark(symbol=ticker, price_date=end, news_date=end) + market_store.update_fetch_watermark( + symbol=ticker, + price_date=end, + news_date=_max_news_date(news_rows), + ) return { "symbol": ticker, @@ -108,9 +142,15 @@ def update_ticker_incremental( if watermarks.get("last_price_fetch") else _default_start() ) + effective_last_news_fetch = _effective_last_news_fetch( + market_store, + ticker=ticker, + end_date=end, + watermark_value=watermarks.get("last_news_fetch"), + ) start_news = ( - (datetime.fromisoformat(watermarks["last_news_fetch"]) + timedelta(days=1)).date().isoformat() - if watermarks.get("last_news_fetch") + (datetime.fromisoformat(effective_last_news_fetch) + timedelta(days=1)).date().isoformat() + if effective_last_news_fetch else _default_start() ) @@ -130,7 +170,7 @@ def update_ticker_incremental( market_store.update_fetch_watermark( symbol=ticker, price_date=end if ohlc_rows or watermarks.get("last_price_fetch") else None, - news_date=end if news_rows or watermarks.get("last_news_fetch") else None, + news_date=_max_news_date(news_rows), ) return { @@ -155,9 +195,15 @@ def refresh_news_incremental( market_store = store or MarketStore() watermarks = market_store.get_ticker_watermarks(ticker) end = end_date or _today_utc() + effective_last_news_fetch = _effective_last_news_fetch( + market_store, + ticker=ticker, + end_date=end, + watermark_value=watermarks.get("last_news_fetch"), + ) start_news = ( - (datetime.fromisoformat(watermarks["last_news_fetch"]) + timedelta(days=1)).date().isoformat() - if watermarks.get("last_news_fetch") + (datetime.fromisoformat(effective_last_news_fetch) + timedelta(days=1)).date().isoformat() + if effective_last_news_fetch else _default_start() ) @@ -182,7 +228,7 @@ def refresh_news_incremental( aligned_count = align_news_for_symbol(market_store, ticker) market_store.update_fetch_watermark( symbol=ticker, - news_date=end if news_rows or watermarks.get("last_news_fetch") else None, + news_date=_max_news_date(news_rows), ) return { diff --git a/backend/data/market_store.py b/backend/data/market_store.py index cd54d52..f5af092 100644 --- a/backend/data/market_store.py +++ b/backend/data/market_store.py @@ -244,6 +244,20 @@ class MarketStore: "last_news_fetch": None, } + def get_latest_news_date(self, symbol: str) -> str | None: + """Return the latest stored published news date for one ticker.""" + with self._connect() as conn: + row = conn.execute( + """ + SELECT MAX(substr(nr.published_utc, 1, 10)) AS latest_date + FROM news_ticker nt + JOIN news_raw nr ON nr.id = nt.news_id + WHERE nt.symbol = ? + """, + (symbol,), + ).fetchone() + return str(row["latest_date"]).strip() if row and row["latest_date"] else None + def upsert_ohlc(self, symbol: str, rows: Iterable[dict[str, Any]], *, source: str = "polygon") -> int: timestamp = _utc_timestamp() count = 0 diff --git a/backend/gateway_server.py b/backend/gateway_server.py index 896f21d..43a6489 100644 --- a/backend/gateway_server.py +++ b/backend/gateway_server.py @@ -48,7 +48,6 @@ INFO_LOGGER_PREFIXES = ( "backend.core.pipeline", "backend.core.scheduler", "backend.services.gateway_cycle_support", - "backend.utils.terminal_dashboard", ) NOISY_LOGGER_LEVELS = { @@ -119,7 +118,7 @@ async def run_gateway( """Run Gateway with Pipeline.""" # Extract config - tickers = bootstrap.get("tickers", ["AAPL", "MSFT"]) + tickers = bootstrap.get("tickers", ["AAPL", "MSFT", "GOOGL", "AMZN", "NVDA", "META", "TSLA", "AMD", "NFLX", "AVGO", "PLTR", "COIN"]) initial_cash = float(bootstrap.get("initial_cash", 100000.0)) margin_requirement = float(bootstrap.get("margin_requirement", 0.0)) max_comm_cycles = int(bootstrap.get("max_comm_cycles", 2)) diff --git a/backend/services/gateway.py b/backend/services/gateway.py index ff7adb9..cf362cc 100644 --- a/backend/services/gateway.py +++ b/backend/services/gateway.py @@ -16,7 +16,6 @@ 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.utils.terminal_dashboard import get_dashboard from backend.core.pipeline import TradingPipeline from backend.core.state_sync import StateSync from backend.services.market import MarketService @@ -40,9 +39,6 @@ EDITABLE_AGENT_WORKSPACE_FILES = { "AGENTS.md", "MEMORY.md", "POLICY.md", - "HEARTBEAT.md", - "ROLE.md", - "STYLE.md", } @@ -84,7 +80,6 @@ class Gateway: self._manual_cycle_task: Optional[asyncio.Task] = None self._backtest_start_date: Optional[str] = None self._backtest_end_date: Optional[str] = None - self._dashboard = get_dashboard() self._market_status_task: Optional[asyncio.Task] = None self._watchlist_ingest_task: Optional[asyncio.Task] = None @@ -107,21 +102,6 @@ class Gateway: self._loop = asyncio.get_running_loop() self._provider_router.add_listener(self._on_provider_usage_changed) - # Initialize terminal dashboard - self._dashboard.set_config( - mode=self.mode, - config_name=self.config.get("config_name", "default"), - host=host, - port=port, - poll_interval=self.config.get("poll_interval", 10), - tickers=self.config.get("tickers", []), - initial_cash=self.storage.initial_cash, - start_date=self._backtest_start_date or "", - end_date=self._backtest_end_date or "", - data_sources=self._provider_router.get_usage_snapshot(), - ) - self._dashboard.start() - self.state_sync.load_state() self.market_service.set_price_recorder(self.storage.record_price_point) self.state_sync.update_state("status", "initializing") @@ -153,16 +133,6 @@ class Gateway: dashboard_snapshot = self.storage.build_dashboard_snapshot_from_state(self.state_sync.state) summary = dashboard_snapshot.get("summary") if summary: - holdings = dashboard_snapshot.get("holdings") or [] - trades = dashboard_snapshot.get("trades") or [] - current_date = self.state_sync.state.get("current_date") - self._dashboard.update( - date=current_date or "-", - status="running", - portfolio=summary, - holdings=holdings, - trades=trades, - ) logger.info( "Loaded existing portfolio: $%s", f"{summary.get('totalAssetValue', 0):,.2f}", @@ -252,7 +222,6 @@ class Gateway: 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) - self._dashboard.update(data_sources=snapshot) if self._loop and self._loop.is_running(): asyncio.run_coroutine_threadsafe( self.broadcast( diff --git a/backend/services/gateway_cycle_support.py b/backend/services/gateway_cycle_support.py index 313073b..b69aadf 100644 --- a/backend/services/gateway_cycle_support.py +++ b/backend/services/gateway_cycle_support.py @@ -147,25 +147,10 @@ async def on_heartbeat_trigger(gateway: Any, date: str) -> None: for analyst in analysts: try: - ws_id = getattr(analyst, "workspace_id", None) - if ws_id: - from backend.agents.workspace_manager import get_workspace_dir - from pathlib import Path - from agentscope.message import Msg - - ws_dir = get_workspace_dir(ws_id) - if ws_dir: - hb_path = Path(ws_dir) / "HEARTBEAT.md" - if hb_path.exists(): - content = hb_path.read_text(encoding="utf-8").strip() - if content: - hb_task = f"# 定期主动检查\n\n{content}\n\n请执行上述检查并报告结果。" - logger.info("[Heartbeat] Running heartbeat for %s", analyst.name) - msg = Msg(role="user", content=hb_task, name="system") - await analyst.reply([msg]) - logger.info("[Heartbeat] %s heartbeat complete", analyst.name) - continue - logger.debug("[Heartbeat] No HEARTBEAT.md for %s, skipping", analyst.name) + logger.debug( + "[Heartbeat] No heartbeat configured for %s, skipping", + analyst.name, + ) except Exception as exc: logger.error("[Heartbeat] %s failed: %s", analyst.name, exc, exc_info=True) @@ -175,7 +160,6 @@ async def run_backtest_cycle(gateway: Any, date: str, tickers: list[str]) -> Non await gateway.market_service.emit_market_open() await gateway.state_sync.on_cycle_start(date) - gateway._dashboard.update(date=date, status="Analyzing...") prices = gateway.market_service.get_open_prices() close_prices = gateway.market_service.get_close_prices() @@ -218,7 +202,6 @@ async def run_live_cycle(gateway: Any, date: str, tickers: list[str]) -> None: logger.warning("Live cycle news refresh failed: %s", exc) await gateway.state_sync.on_cycle_start(trading_date) - gateway._dashboard.update(date=trading_date, status="Analyzing...") market_caps = await get_market_caps(gateway, tickers, trading_date) schedule_mode = gateway.config.get("schedule_mode", "daily") @@ -263,12 +246,9 @@ async def finalize_cycle(gateway: Any, date: str) -> None: summary.update(gateway.storage.get_live_returns()) await gateway.state_sync.on_cycle_end(date, portfolio_summary=summary) - holdings = dashboard_snapshot.get("holdings") or [] - trades = dashboard_snapshot.get("trades") or [] leaderboard = dashboard_snapshot.get("leaderboard") or [] if leaderboard: await gateway.state_sync.on_leaderboard_update(leaderboard) - gateway._dashboard.update(date=date, status="Running", portfolio=summary, holdings=holdings, trades=trades) async def get_market_caps(gateway: Any, tickers: list[str], date: str) -> dict[str, float]: @@ -329,24 +309,16 @@ def save_cycle_results( async def run_backtest_dates(gateway: Any, dates: list[str]) -> None: gateway.state_sync.set_backtest_dates(dates) - gateway._dashboard.update(days_total=len(dates), days_completed=0) await gateway.state_sync.on_system_message(f"Starting backtest - {len(dates)} trading days") try: - for i, date in enumerate(dates): - gateway._dashboard.update(days_completed=i) + for date in dates: await gateway.on_strategy_trigger(date=date) await asyncio.sleep(0.1) await gateway.state_sync.on_system_message(f"Backtest complete - {len(dates)} days") - summary = gateway.storage.build_dashboard_snapshot_from_state(gateway.state_sync.state).get("summary") or {} - gateway._dashboard.update(status="Complete", portfolio=summary, days_completed=len(dates)) - gateway._dashboard.stop() - gateway._dashboard.print_final_summary() except Exception as exc: error_msg = f"Backtest failed: {type(exc).__name__}: {str(exc)}" logger.error(error_msg, exc_info=True) asyncio.create_task(gateway.state_sync.on_system_message(error_msg)) - gateway._dashboard.update(status=f"Failed: {str(exc)}") - gateway._dashboard.stop() raise finally: gateway._backtest_task = None @@ -376,7 +348,6 @@ def set_backtest_dates(gateway: Any, dates: list[str]) -> None: if dates: gateway._backtest_start_date = dates[0] gateway._backtest_end_date = dates[-1] - gateway._dashboard.days_total = len(dates) def stop_gateway(gateway: Any) -> None: @@ -399,4 +370,3 @@ def stop_gateway(gateway: Any) -> None: loop.run_until_complete(gateway._openclaw_ws.disconnect()) except Exception: pass - gateway._dashboard.stop() diff --git a/backend/services/gateway_openclaw_handlers.py b/backend/services/gateway_openclaw_handlers.py index e4cb958..263db4e 100644 --- a/backend/services/gateway_openclaw_handlers.py +++ b/backend/services/gateway_openclaw_handlers.py @@ -14,7 +14,7 @@ logger = logging.getLogger(__name__) def _ensure_session_bridge(gateway) -> None: - """Forward OpenClaw session events into EvoTraders frontend websockets.""" + """Forward OpenClaw session events into 大时代 frontend websockets.""" if getattr(gateway, "_openclaw_session_bridge_ready", False): return diff --git a/backend/services/gateway_runtime_support.py b/backend/services/gateway_runtime_support.py index 1e9bfb7..f288a2d 100644 --- a/backend/services/gateway_runtime_support.py +++ b/backend/services/gateway_runtime_support.py @@ -141,7 +141,7 @@ def apply_runtime_config(gateway: Any, runtime_config: dict[str, Any]) -> dict[s def sync_runtime_state(gateway: Any) -> None: - """Refresh persisted state and dashboard after runtime config changes.""" + """Refresh persisted state after runtime config changes.""" gateway.state_sync.update_state("tickers", gateway.config.get("tickers", [])) gateway.state_sync.update_state( "runtime_config", @@ -159,17 +159,3 @@ def sync_runtime_state(gateway: Any) -> None: 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)) - - dashboard_snapshot = gateway.storage.build_dashboard_snapshot_from_state(gateway.state_sync.state) - summary = dashboard_snapshot.get("summary") or {} - holdings = dashboard_snapshot.get("holdings") or [] - trades = dashboard_snapshot.get("trades") or [] - gateway._dashboard.update( - portfolio=summary, - holdings=holdings, - trades=trades, - ) diff --git a/backend/tests/test_agent_workspace.py b/backend/tests/test_agent_workspace.py index 24afe70..abe2357 100644 --- a/backend/tests/test_agent_workspace.py +++ b/backend/tests/test_agent_workspace.py @@ -13,7 +13,7 @@ class _DummyToolkit: return "" -def test_workspace_manager_creates_extended_agent_files(tmp_path): +def test_workspace_manager_creates_core_agent_files(tmp_path): manager = WorkspaceManager(project_root=tmp_path) manager.initialize_default_assets( @@ -27,7 +27,7 @@ def test_workspace_manager_creates_extended_agent_files(tmp_path): assert (asset_dir / "PROFILE.md").exists() assert (asset_dir / "AGENTS.md").exists() assert (asset_dir / "MEMORY.md").exists() - assert (asset_dir / "HEARTBEAT.md").exists() + assert (asset_dir / "POLICY.md").exists() assert (asset_dir / "agent.yaml").exists() assert (asset_dir / "skills" / "installed").is_dir() assert (asset_dir / "skills" / "active").is_dir() @@ -35,6 +35,22 @@ def test_workspace_manager_creates_extended_agent_files(tmp_path): assert (asset_dir / "skills" / "local").is_dir() +def test_workspace_manager_seeds_risk_prompt_content(tmp_path): + manager = WorkspaceManager(project_root=tmp_path) + manager.initialize_default_assets( + config_name="demo", + agent_ids=["risk_manager"], + analyst_personas={}, + ) + + asset_dir = tmp_path / "runs" / "demo" / "agents" / "risk_manager" + soul = (asset_dir / "SOUL.md").read_text(encoding="utf-8") + guide = (asset_dir / "AGENTS.md").read_text(encoding="utf-8") + + assert "风险管理经理" in soul + assert "优先使用可用的风险工具量化集中度" in guide + + def test_agent_workspace_config_controls_prompt_files(tmp_path, monkeypatch): manager = WorkspaceManager(project_root=tmp_path) manager.initialize_default_assets( @@ -72,6 +88,32 @@ def test_agent_workspace_config_controls_prompt_files(tmp_path, monkeypatch): assert "profile-line" not in prompt +def test_prompt_is_built_from_workspace_defaults_without_system_templates(tmp_path, monkeypatch): + manager = WorkspaceManager(project_root=tmp_path) + manager.initialize_default_assets( + config_name="demo", + agent_ids=["portfolio_manager"], + analyst_personas={}, + ) + + from backend.agents import prompt_factory + + monkeypatch.setattr( + prompt_factory, + "SkillsManager", + lambda: SkillsManager(project_root=tmp_path), + ) + + prompt = build_agent_system_prompt( + agent_id="portfolio_manager", + config_name="demo", + toolkit=_DummyToolkit(), + ) + + assert "投资组合经理" in prompt + assert "使用 `make_decision` 工具记录每个股票的最终决策" in prompt + + def test_skills_manager_applies_agent_level_skill_toggles(tmp_path): builtin_root = tmp_path / "backend" / "skills" / "builtin" for skill_name in ("risk_review", "extra_guard"): diff --git a/backend/tests/test_gateway_support_modules.py b/backend/tests/test_gateway_support_modules.py index d5681a1..9bd8f61 100644 --- a/backend/tests/test_gateway_support_modules.py +++ b/backend/tests/test_gateway_support_modules.py @@ -8,24 +8,6 @@ import pytest from backend.services import gateway_cycle_support, gateway_runtime_support -class _DummyDashboard: - def __init__(self): - self.updated = [] - self.tickers = [] - self.initial_cash = None - self.enable_memory = False - self.days_total = 0 - - def update(self, **kwargs): - self.updated.append(kwargs) - - def stop(self): - return None - - def print_final_summary(self): - return None - - class _DummyScheduler: def __init__(self): self.calls = [] @@ -128,7 +110,6 @@ def make_gateway_stub(): }, storage=_DummyStorage(), state_sync=_DummyStateSync(), - _dashboard=_DummyDashboard(), _watchlist_ingest_task=None, _market_status_task=None, _backtest_task=None, diff --git a/backend/tests/test_heartbeat_hook.py b/backend/tests/test_heartbeat_hook.py deleted file mode 100644 index e2927c7..0000000 --- a/backend/tests/test_heartbeat_hook.py +++ /dev/null @@ -1,69 +0,0 @@ -# -*- coding: utf-8 -*- -"""Tests for HeartbeatHook.""" -import tempfile -from pathlib import Path - -import pytest - -from backend.agents.base.hooks import HeartbeatHook - - -class TestHeartbeatHook: - """Tests for HeartbeatHook._read_heartbeat_content.""" - - def test_read_heartbeat_content_with_content(self, tmp_path): - """Test reading HEARTBEAT.md when it exists and has content.""" - ws_dir = tmp_path / "analyst_workspace" - ws_dir.mkdir() - hb_file = ws_dir / "HEARTBEAT.md" - hb_file.write_text("# 定期主动检查\n\n- [ ] 持仓是否健康\n", encoding="utf-8") - - hook = HeartbeatHook(workspace_dir=ws_dir) - content = hook._read_heartbeat_content() - - assert content is not None - assert "# 定期主动检查" in content - assert "持仓是否健康" in content - - def test_read_heartbeat_content_absent(self, tmp_path): - """Test reading when HEARTBEAT.md does not exist.""" - ws_dir = tmp_path / "analyst_workspace" - ws_dir.mkdir() - - hook = HeartbeatHook(workspace_dir=ws_dir) - content = hook._read_heartbeat_content() - - assert content is None - - def test_read_heartbeat_content_empty(self, tmp_path): - """Test reading when HEARTBEAT.md is empty.""" - ws_dir = tmp_path / "analyst_workspace" - ws_dir.mkdir() - hb_file = ws_dir / "HEARTBEAT.md" - hb_file.write_text("", encoding="utf-8") - - hook = HeartbeatHook(workspace_dir=ws_dir) - content = hook._read_heartbeat_content() - - assert content is None - - def test_read_heartbeat_content_whitespace_only(self, tmp_path): - """Test reading when HEARTBEAT.md contains only whitespace.""" - ws_dir = tmp_path / "analyst_workspace" - ws_dir.mkdir() - hb_file = ws_dir / "HEARTBEAT.md" - hb_file.write_text(" \n\n ", encoding="utf-8") - - hook = HeartbeatHook(workspace_dir=ws_dir) - content = hook._read_heartbeat_content() - - assert content is None - - def test_completed_flag_path(self, tmp_path): - """Test that completion flag is placed in workspace directory.""" - ws_dir = tmp_path / "analyst_workspace" - ws_dir.mkdir() - - hook = HeartbeatHook(workspace_dir=ws_dir) - - assert hook._completed_flag == ws_dir / ".heartbeat_completed" diff --git a/backend/tests/test_market_ingest.py b/backend/tests/test_market_ingest.py new file mode 100644 index 0000000..457da1e --- /dev/null +++ b/backend/tests/test_market_ingest.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +"""Tests for market ingest watermark handling.""" + +from backend.data import market_ingest + + +class _FakeStore: + def __init__(self, *, last_news_fetch=None, latest_news_date=None): + self._watermarks = { + "symbol": "AAPL", + "last_price_fetch": None, + "last_news_fetch": last_news_fetch, + } + self._latest_news_date = latest_news_date + self.updated = [] + + def get_ticker_watermarks(self, symbol): + return dict(self._watermarks) + + def get_latest_news_date(self, symbol): + return self._latest_news_date + + def upsert_ticker(self, **kwargs): + return None + + def upsert_ohlc(self, symbol, rows, source="polygon"): + return len(rows) + + def upsert_news(self, symbol, rows, source="polygon"): + return len(rows) + + def update_fetch_watermark(self, **kwargs): + self.updated.append(kwargs) + + +def test_refresh_news_incremental_does_not_advance_watermark_without_news(monkeypatch): + store = _FakeStore(last_news_fetch="2026-03-28", latest_news_date="2026-03-28") + + monkeypatch.setattr(market_ingest, "fetch_ticker_details", lambda ticker: {"name": ticker, "sic_description": None, "active": True}) + + class _Router: + def get_company_news(self, **kwargs): + return [], "polygon" + + monkeypatch.setattr(market_ingest, "DataProviderRouter", lambda: _Router()) + monkeypatch.setattr(market_ingest, "align_news_for_symbol", lambda store, ticker: 0) + + result = market_ingest.refresh_news_incremental( + "AAPL", + end_date="2026-03-29", + store=store, + ) + + assert result["start_news_date"] == "2026-03-29" + assert result["news"] == 0 + assert store.updated[-1]["news_date"] is None + + +def test_refresh_news_incremental_clamps_future_watermark_to_latest_stored_date(monkeypatch): + store = _FakeStore(last_news_fetch="2026-03-30", latest_news_date="2026-03-28") + captured = {} + + monkeypatch.setattr(market_ingest, "fetch_ticker_details", lambda ticker: {"name": ticker, "sic_description": None, "active": True}) + + class _Router: + def get_company_news(self, **kwargs): + captured.update(kwargs) + return [], "polygon" + + monkeypatch.setattr(market_ingest, "DataProviderRouter", lambda: _Router()) + monkeypatch.setattr(market_ingest, "align_news_for_symbol", lambda store, ticker: 0) + + result = market_ingest.refresh_news_incremental( + "AAPL", + end_date="2026-03-29", + store=store, + ) + + assert result["start_news_date"] == "2026-03-29" + assert captured["start_date"] == "2026-03-29" + assert captured["end_date"] == "2026-03-29" diff --git a/backend/tools/technical_signals.py b/backend/tools/technical_signals.py index 70c5a9f..cc46a8c 100644 --- a/backend/tools/technical_signals.py +++ b/backend/tools/technical_signals.py @@ -48,7 +48,7 @@ class TechnicalSignal: class StockTechnicalAnalyzer: - """Lightweight technical analyzer adapted for EvoTraders tools.""" + """Lightweight technical analyzer adapted for 大时代 tools.""" def analyze(self, ticker: str, df: pd.DataFrame) -> TechnicalSignal: """Analyze one ticker from OHLC price history.""" diff --git a/backend/utils/terminal_dashboard.py b/backend/utils/terminal_dashboard.py deleted file mode 100644 index 9656954..0000000 --- a/backend/utils/terminal_dashboard.py +++ /dev/null @@ -1,352 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Terminal Dashboard - Persistent unified panel using Rich Live -""" -# pylint: disable=R0915,R0912 -import logging -import threading -import time -from typing import Any, Dict, List, Optional - -from rich.console import Console -from rich.live import Live -from rich.panel import Panel -from rich.table import Table - -logger = logging.getLogger(__name__) - - -class TerminalDashboard: - """Unified persistent terminal dashboard""" - - def __init__(self, console: Console = None): - self.console = console or Console() - self.live: Optional[Live] = None - - # Config state - self.mode = "live" - self.config_name = "" - self.host = "0.0.0.0" - self.port = 8765 - self.poll_interval = 10 - self.trigger_time = "now" - self.enable_memory = False - self.local_time = "" - self.nyse_time = "" - self.start_date = "" - self.end_date = "" - self.tickers: List[str] = [] - self.initial_cash = 100000.0 - self.data_sources: Dict[str, Any] = {} - - # Trading state - self.current_date = "-" - self.status = "Initializing" - self.total_value = 0.0 - self.cash = 0.0 - self.pnl_pct = 0.0 - self.holdings: List[Dict] = [] - self.trades: List[Dict] = [] - self.days_completed = 0 - self.days_total = 0 - - # Progress message (last line) - self.progress = "" - self._dots_index = 0 - self._animator_running = False - self._animator_thread: Optional[threading.Thread] = None - - def set_config( - self, - mode: str, - config_name: str, - host: str, - port: int, - poll_interval: int, - trigger_time: str = "now", - enable_memory: bool = False, - local_time: str = "", - nyse_time: str = "", - start_date: str = "", - end_date: str = "", - tickers: List[str] = None, - initial_cash: float = 100000.0, - data_sources: Dict[str, Any] = None, - ): - """Set configuration state""" - self.mode = mode - self.config_name = config_name - self.host = host - self.port = port - self.poll_interval = poll_interval - self.trigger_time = trigger_time - self.enable_memory = enable_memory - self.local_time = local_time - self.nyse_time = nyse_time - self.start_date = start_date - self.end_date = end_date - self.tickers = tickers or [] - self.initial_cash = initial_cash - self.data_sources = data_sources or {} - self.total_value = initial_cash - self.cash = initial_cash - - def _build_panel(self) -> Panel: - """Build the unified dashboard panel""" - # Main grid - main_table = Table.grid(padding=(0, 2)) - main_table.add_column(width=28) - main_table.add_column(width=22) - main_table.add_column(width=22) - - # Left: Config + Status - left = Table.grid(padding=(0, 0)) - left.add_column() - - # Mode line - if self.mode == "backtest": - mode_str = "[cyan]Backtest[/cyan]" - else: - mode_str = "[green]LIVE[/green]" - - left.add_row(f"[bold]Mode:[/bold] {mode_str}") - left.add_row(f"[dim]Config:[/dim] {self.config_name}") - left.add_row(f"[dim]Server:[/dim] {self.host}:{self.port}") - preferred_sources = self.data_sources.get("preferred", []) - if preferred_sources: - left.add_row( - f"[dim]Data:[/dim] {' -> '.join(preferred_sources)}", - ) - - if self.mode == "live" and self.nyse_time: - left.add_row(f"[dim]NYSE:[/dim] {self.nyse_time[:19]}") - trigger_display = ( - "[green]NOW[/green]" - if self.trigger_time == "now" - else self.trigger_time - ) - left.add_row(f"[dim]Trigger:[/dim] {trigger_display}") - - # Status - left.add_row("") - status_style = "green" if self.status == "Running" else "yellow" - left.add_row( - "[bold]Status:[/bold] " - f"[{status_style}]{self.status}[/{status_style}]", - ) - if self.mode == "backtest": - left.add_row( - f"[dim]Backtesting Period:[/dim] {self.days_total} days\n" - f" {self.start_date} -> {self.end_date}", - ) - left.add_row(f"[dim]Current Date:[/dim] {self.current_date}") - - # Middle: Portfolio - mid = Table.grid(padding=(0, 0)) - mid.add_column() - - pnl_style = "green" if self.pnl_pct >= 0 else "red" - mid.add_row("[bold]Portfolio[/bold]") - mid.add_row(f"NAV: [bold]${self.total_value:,.0f}[/bold]") - mid.add_row(f"Cash: ${self.cash:,.0f}") - mid.add_row(f"P&L: [{pnl_style}]{self.pnl_pct:+.2f}%[/{pnl_style}]") - - # Positions - mid.add_row("") - mid.add_row("[bold]Positions[/bold]") - stock_holdings = [ - h for h in self.holdings if h.get("ticker") != "CASH" - ] - if stock_holdings: - for h in stock_holdings[:7]: - qty = h.get("quantity", 0) - ticker = h.get("ticker", "")[:5] - val = h.get("marketValue", 0) - qty_str = f"{qty:+d}" if qty != 0 else "0" - mid.add_row( - f"[cyan]{ticker:<5}[/cyan] {qty_str:>5} ${val:>7,.0f}", - ) - if len(stock_holdings) > 7: - mid.add_row(f"[dim]+{len(stock_holdings) - 7} more[/dim]") - else: - mid.add_row("[dim]No positions[/dim]") - - # Right: Recent Trades - right = Table.grid(padding=(0, 0)) - right.add_column() - - right.add_row("[bold]Recent Trades[/bold]") - if self.trades: - for t in self.trades[:10]: - side = t.get("side", "") - ticker = t.get("ticker", "")[:5] - qty = t.get("qty", 0) - if side == "LONG": - side_str = "[green]L[/green]" - elif side == "SHORT": - side_str = "[red]S[/red]" - else: - side_str = "[dim]H[/dim]" - right.add_row(f"{side_str} [cyan]{ticker:<5}[/cyan] {qty:>4}") - if len(self.trades) > 10: - right.add_row(f"[dim]+{len(self.trades) - 10} more[/dim]") - else: - right.add_row("[dim]No trades[/dim]") - - main_table.add_row(left, mid, right) - - # Outer table to add progress line at bottom - outer = Table.grid(padding=(0, 0)) - outer.add_column() - outer.add_row(main_table) - - # Progress line (last row) with animated dots - if self.progress: - DOTS_FRAMES = [" ", ". ", ".. ", "..."] - dots = DOTS_FRAMES[self._dots_index % len(DOTS_FRAMES)] - outer.add_row("") - outer.add_row(f"[dim]> {self.progress}{dots}[/dim]") - - # Build panel - title = "[bold cyan]EvoTraders[/bold cyan]" - if self.mode == "backtest": - title += " [dim]Backtest[/dim]" - else: - title += " [dim]Live[/dim]" - - return Panel( - outer, - title=title, - border_style="cyan", - padding=(0, 1), - ) - - def _run_animator(self): - """Background thread to animate the dots""" - while self._animator_running: - time.sleep(0.3) - if self.progress and self.live: - self._dots_index += 1 - self.live.update(self._build_panel()) - - def start(self): - """Start the live dashboard display""" - self.live = Live( - self._build_panel(), - console=self.console, - refresh_per_second=4, - vertical_overflow="visible", - ) - self.live.start() - - # Start animator thread - self._animator_running = True - self._animator_thread = threading.Thread( - target=self._run_animator, - daemon=True, - ) - self._animator_thread.start() - - def stop(self): - """Stop the live dashboard""" - self._animator_running = False - if self._animator_thread: - self._animator_thread.join(timeout=0.5) - self._animator_thread = None - if self.live: - self.live.stop() - self.live = None - - def update( - self, - date: str = None, - status: str = None, - portfolio: Dict[str, Any] = None, - holdings: List[Dict] = None, - trades: List[Dict] = None, - days_completed: int = None, - days_total: int = None, - data_sources: Dict[str, Any] = None, - ): - """Update dashboard state and refresh display""" - if date: - self.current_date = date - if status: - self.status = status - if days_completed is not None: - self.days_completed = days_completed - if days_total is not None: - self.days_total = days_total - - if portfolio: - self.total_value = portfolio.get( - "totalAssetValue", - 0, - ) or portfolio.get( - "total_value", - self.initial_cash, - ) - self.cash = portfolio.get("cashPosition", 0) or portfolio.get( - "cash", - self.initial_cash, - ) - if self.total_value > 0 and self.initial_cash > 0: - self.pnl_pct = ( - (self.total_value - self.initial_cash) / self.initial_cash - ) * 100 - - if holdings is not None: - self.holdings = holdings - if trades is not None: - self.trades = trades - if data_sources is not None: - self.data_sources = data_sources - - if self.live: - self.live.update(self._build_panel()) - - def log(self, msg: str, also_log: bool = True): - """ - Update progress message and refresh panel - - Args: - msg: Progress message to display - also_log: Whether to also write to logger (default True) - """ - self.progress = msg - if also_log: - logger.info(msg) - if self.live: - self.live.update(self._build_panel()) - - def print_final_summary(self): - """Print final summary when dashboard stops""" - pnl_style = "green" if self.pnl_pct >= 0 else "red" - - if self.mode == "backtest": - msg = ( - f"[bold]Backtest Complete[/bold] | " - f"Days: {self.days_completed} | " - f"NAV: ${self.total_value:,.0f} | " - f"Return: [{pnl_style}]{self.pnl_pct:+.2f}%[/{pnl_style}]" - ) - else: - msg = ( - f"[bold]Session End[/bold] | " - f"NAV: ${self.total_value:,.0f} | " - f"P&L: [{pnl_style}]{self.pnl_pct:+.2f}%[/{pnl_style}]" - ) - - self.console.print(Panel(msg, border_style="green")) - - -# Global instance -_dashboard: Optional[TerminalDashboard] = None - - -def get_dashboard() -> TerminalDashboard: - """Get or create global dashboard instance""" - global _dashboard - if _dashboard is None: - _dashboard = TerminalDashboard() - return _dashboard diff --git a/deploy/README.md b/deploy/README.md index 2a1a9b3..c7c8133 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -1,24 +1,25 @@ # Deployment Notes This directory contains the current production-oriented deployment artifacts for -the EvoTraders frontend site and the live gateway process. +the 大时代 frontend site and the live gateway process. ## Contents - [deploy/systemd/evotraders.service](./systemd/evotraders.service) - - systemd unit for the long-running EvoTraders gateway process + - systemd unit for the long-running 大时代 gateway process - [scripts/run_prod.sh](../scripts/run_prod.sh) - production launch script used by the systemd unit -- [deploy/nginx/evotraders.cillinn.com.conf](./nginx/evotraders.cillinn.com.conf) +- [deploy/nginx/bigtime.cillinn.com.conf](./nginx/bigtime.cillinn.com.conf) - HTTPS nginx config with WebSocket proxying -- [deploy/nginx/evotraders.cillinn.com.http.conf](./nginx/evotraders.cillinn.com.http.conf) +- [deploy/nginx/bigtime.cillinn.com.http.conf](./nginx/bigtime.cillinn.com.http.conf) - plain HTTP/static-site variant ## Current Production Shape The checked-in production path is intentionally minimal: -- nginx serves the built frontend from `/var/www/evotraders/current` +- nginx serves the built frontend from `/var/www/bigtime/current` +- public domain examples use `bigtime.cillinn.com` - nginx proxies `/ws` to `127.0.0.1:8765` - systemd runs `scripts/run_prod.sh` - `scripts/run_prod.sh` starts `python3 -m backend.main` in live mode on `127.0.0.1:8765` @@ -28,7 +29,7 @@ frontend, not on exposing the split FastAPI services directly. ## Important Paths And Ports -- frontend root: `/var/www/evotraders/current` +- frontend root: `/var/www/bigtime/current` - gateway bind: `127.0.0.1:8765` - public WebSocket path: `/ws` - working directory expected by systemd: `/root/code/evotraders` @@ -61,32 +62,45 @@ journalctl -u evotraders -f The HTTPS nginx config does two things: -- redirects `http://evotraders.cillinn.com` to HTTPS +- redirects `http://bigtime.cillinn.com` to HTTPS - proxies `/ws` to the local gateway process with WebSocket upgrade headers Typical install flow: ```bash -sudo cp deploy/nginx/evotraders.cillinn.com.conf /etc/nginx/sites-available/evotraders.cillinn.com.conf -sudo ln -s /etc/nginx/sites-available/evotraders.cillinn.com.conf /etc/nginx/sites-enabled/ +sudo cp deploy/nginx/bigtime.cillinn.com.conf /etc/nginx/sites-available/bigtime.cillinn.com.conf +sudo ln -s /etc/nginx/sites-available/bigtime.cillinn.com.conf /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl reload nginx ``` The checked-in TLS config expects Let's Encrypt assets at: -- `/etc/letsencrypt/live/evotraders.cillinn.com/fullchain.pem` -- `/etc/letsencrypt/live/evotraders.cillinn.com/privkey.pem` +- `/etc/letsencrypt/live/bigtime.cillinn.com/fullchain.pem` +- `/etc/letsencrypt/live/bigtime.cillinn.com/privkey.pem` ## Environment Expectations Before using the production scripts, ensure the runtime environment has: - a usable Python environment +- backend dependencies installed from `requirements.txt` +- the package installed with `pip install -e .` or `uv pip install -e .` +- frontend dependencies installed with `npm ci` - repo dependencies installed - required market/model API keys - any desired `TICKERS` override +Recommended production install sequence: + +```bash +python3 -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt +pip install -e . +cd frontend && npm ci && npm run build && cd .. +``` + The production script currently sets: ```bash diff --git a/deploy/nginx/evotraders.cillinn.com.conf b/deploy/nginx/bigtime.cillinn.com.conf similarity index 72% rename from deploy/nginx/evotraders.cillinn.com.conf rename to deploy/nginx/bigtime.cillinn.com.conf index e838cd8..c4b9db8 100644 --- a/deploy/nginx/evotraders.cillinn.com.conf +++ b/deploy/nginx/bigtime.cillinn.com.conf @@ -1,9 +1,9 @@ server { listen 80; - server_name evotraders.cillinn.com; + server_name bigtime.cillinn.com; location /.well-known/acme-challenge/ { - root /var/www/evotraders/current; + root /var/www/bigtime/current; allow all; } @@ -14,13 +14,13 @@ server { server { listen 443 ssl http2; - server_name evotraders.cillinn.com; + server_name bigtime.cillinn.com; - root /var/www/evotraders/current; + root /var/www/bigtime/current; index index.html; - ssl_certificate /etc/letsencrypt/live/evotraders.cillinn.com/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/evotraders.cillinn.com/privkey.pem; + ssl_certificate /etc/letsencrypt/live/bigtime.cillinn.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/bigtime.cillinn.com/privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; diff --git a/deploy/nginx/evotraders.cillinn.com.http.conf b/deploy/nginx/bigtime.cillinn.com.http.conf similarity index 70% rename from deploy/nginx/evotraders.cillinn.com.http.conf rename to deploy/nginx/bigtime.cillinn.com.http.conf index 9fabcc7..7724fda 100644 --- a/deploy/nginx/evotraders.cillinn.com.http.conf +++ b/deploy/nginx/bigtime.cillinn.com.http.conf @@ -1,8 +1,8 @@ server { listen 80; - server_name evotraders.cillinn.com; + server_name bigtime.cillinn.com; - root /var/www/evotraders/current; + root /var/www/bigtime/current; index index.html; location /.well-known/acme-challenge/ { diff --git a/deploy/systemd/evotraders.service b/deploy/systemd/evotraders.service index 6176d08..edf893b 100644 --- a/deploy/systemd/evotraders.service +++ b/deploy/systemd/evotraders.service @@ -1,5 +1,5 @@ [Unit] -Description=EvoTraders Production Service +Description=大时代 Production Service After=network.target [Service] diff --git a/docs/assets/bigtime_demo.gif b/docs/assets/bigtime_demo.gif new file mode 100644 index 0000000..57d6157 Binary files /dev/null and b/docs/assets/bigtime_demo.gif differ diff --git a/docs/assets/bigtime_logo.jpg b/docs/assets/bigtime_logo.jpg new file mode 100644 index 0000000..f865828 Binary files /dev/null and b/docs/assets/bigtime_logo.jpg differ diff --git a/env.template b/env.template index ac2e31f..04a7d71 100644 --- a/env.template +++ b/env.template @@ -1,6 +1,6 @@ # ================== General Configuration | 通用配置 ================== # List of stock ticker symbols to analyze (comma-separated) | 想要分析的股票代码列表(用逗号分隔) -TICKERS=AAPL,MSFT,GOOGL,NVDA,TSLA,META,AMZN +TICKERS=AAPL,MSFT,GOOGL,AMZN,NVDA,META,TSLA,AMD,NFLX,AVGO,PLTR,COIN # Financial Data API # At least FINANCIAL_DATASETS_API_KEY is required, corresponding to FIN_DATA_SOURCE=financial_datasets; It's recommended to add FINNHUB_API_KEY, corresponding to FIN_DATA_SOURCE=finnhub; FINNHUB_API_KEY is mandatory for live mode diff --git a/frontend/README.md b/frontend/README.md index abe9c87..61b9258 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -2,13 +2,13 @@ ```bash cd frontend -npm install +npm ci npm run dev ``` Default dev URL: `http://localhost:5173` -The frontend expects the EvoTraders gateway WebSocket on `ws://localhost:8765` unless overridden. +The frontend expects the 大时代 gateway WebSocket on `ws://localhost:8765` unless overridden. ## Recommended Local Backend Stack @@ -40,6 +40,16 @@ VITE_WS_URL=ws://localhost:8765 There is also a starter template at [frontend/env.template](./env.template). +For production deployments, prefer: + +```bash +cd frontend +npm ci +npm run build +``` + +This ensures the deployed frontend matches the checked-in `package-lock.json`. + ## Direct-Service Coverage Current direct-call coverage includes: diff --git a/frontend/env.template b/frontend/env.template index 0499b80..0bc7453 100644 --- a/frontend/env.template +++ b/frontend/env.template @@ -1,10 +1,24 @@ # Frontend Environment Variables Template # 复制此文件为 .env 并修改配置 -# WebSocket服务器地址 -# 本地开发 +# 控制面 API(agent/workspaces/guard) +VITE_CONTROL_API_BASE_URL=http://localhost:8000/api + +# 运行时 API(start/stop/runtime info) +VITE_RUNTIME_API_BASE_URL=http://localhost:8003/api/runtime + +# 新闻服务(可选,未配置时走默认回退) +VITE_NEWS_SERVICE_URL=http://localhost:8002 + +# 交易数据服务(可选,未配置时走默认回退) +VITE_TRADING_SERVICE_URL=http://localhost:8001 + +# WebSocket Gateway VITE_WS_URL=ws://localhost:8765 -# 生产环境(替换为你的实际服务器地址) -# VITE_WS_URL=wss://your-server.com:8765 - +# 生产环境示例 +# VITE_CONTROL_API_BASE_URL=https://your-domain.com/api +# VITE_RUNTIME_API_BASE_URL=https://your-domain.com/api/runtime +# VITE_NEWS_SERVICE_URL=https://your-domain.com/news +# VITE_TRADING_SERVICE_URL=https://your-domain.com/trading +# VITE_WS_URL=wss://your-domain.com/ws diff --git a/frontend/index.html b/frontend/index.html index 933d555..115c12a 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,7 +4,7 @@ -
- EvoTraders
+ 大时代