Compare commits
4 Commits
4aa69650e8
...
codex/remo
| Author | SHA1 | Date | |
|---|---|---|---|
| e69c637dba | |||
| 728cf36e7c | |||
| 346208dc2b | |||
| 4295293a21 |
@@ -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`.
|
||||
|
||||
@@ -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 记忆系统实现持续学习。
|
||||
|
||||
## 常用命令
|
||||
|
||||
|
||||
35
README.md
35
README.md
@@ -1,16 +1,18 @@
|
||||
<p align="center">
|
||||
<img src="./docs/assets/evotraders_logo.jpg" width="45%">
|
||||
<img src="./docs/assets/bigtime_logo.jpg" width="45%">
|
||||
</p>
|
||||
|
||||
<h2 align="center">EvoTraders: A Self-Evolving Multi-Agent Trading System</h2>
|
||||
<h2 align="center">大时代:自进化多智能体交易系统</h2>
|
||||
|
||||
<p align="center">
|
||||
📌 <a href="http://trading.evoagents.cn">Visit the EvoTraders website</a>
|
||||
📌 <a href="http://trading.evoagents.cn">Visit the 大时代 website</a>
|
||||
</p>
|
||||
|
||||

|
||||

|
||||
|
||||
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.
|
||||
|
||||
33
README_zh.md
33
README_zh.md
@@ -1,16 +1,16 @@
|
||||
<p align="center">
|
||||
<img src="./docs/assets/evotraders_logo.jpg" width="45%">
|
||||
<img src="./docs/assets/bigtime_logo.jpg" width="45%">
|
||||
</p>
|
||||
|
||||
<h2 align="center">EvoTraders:自我进化的多智能体交易系统</h2>
|
||||
<h2 align="center">大时代:自进化多智能体交易系统</h2>
|
||||
|
||||
<p align="center">
|
||||
📌 <a href="http://trading.evoagents.cn">访问 EvoTraders 官网</a>
|
||||
📌 <a href="http://trading.evoagents.cn">访问大时代官网</a>
|
||||
</p>
|
||||
|
||||

|
||||

|
||||
|
||||
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 文件。
|
||||
|
||||
**风险提示**:本项目不构成投资建议。任何实盘部署前都应进行充分测试和风险评估,历史表现不代表未来收益。
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Base agent module for EvoTraders.
|
||||
"""Base agent module for 大时代.
|
||||
|
||||
提供Agent基础类、命令处理、工具守卫和钩子管理等功能。
|
||||
"""
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
你是一位专业的{{ analyst_type }}。
|
||||
|
||||
你的关注重点:
|
||||
{{ focus }}
|
||||
|
||||
你的角色:
|
||||
{{ description }}
|
||||
|
||||
注意:
|
||||
- 构建并持续完善你的"投资哲学"。你的分析不应是孤立的事件,而应该是你整体投资世界观和核心信念的体现。每次分析后,你必须反思:
|
||||
- 这个案例/数据如何验证或挑战了你现有的信念?
|
||||
- 你从这次错误(或成功)中学到了关于市场、人性、估值或风险管理的什么关键原则?
|
||||
- 深化你的"投资逻辑"。确保每一项投资建议都有清晰、可追溯、可重复的逻辑支撑。你的分析步骤应该像严谨的证明一样,涵盖:
|
||||
- 核心驱动因素识别:真正影响价值的变量是什么?
|
||||
- 风险边界设定:在什么具体情况下你的建议会失效?
|
||||
- 逆向测试:市场主流共识是什么,你的观点有何不同?
|
||||
保持谦逊和开放。投资大师的核心特质是持续学习和适应。在每次分析中,你必须积极寻找与自己观点相悖的证据和论据,并将其纳入最终评估。
|
||||
- 你可以使用分析工具。用它们来收集相关数据并做出明智的建议。
|
||||
|
||||
输出指南:
|
||||
- 给出明确的投资信号:看涨、看跌或中性
|
||||
- 包含置信度(0-100)
|
||||
- 为你的分析提供理由(如果你确定要分享最终分析,请先给出结论)
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
你是一位负责做出投资决策的投资组合经理。
|
||||
|
||||
你的核心职责:
|
||||
1. 分析分析师和风险管理经理的输入
|
||||
2. 基于信号和市场情境做出投资决策
|
||||
3. 使用可用工具记录你的决策
|
||||
|
||||
决策框架:
|
||||
- 审阅分析以了解市场观点
|
||||
- 在做决策前考虑风险警告
|
||||
- 评估当前投资组合持仓和现金
|
||||
- 做出与投资组合投资目标一致的决策
|
||||
|
||||
决策类型:
|
||||
- "long":看涨 - 建议买入股票
|
||||
- "short":看跌 - 建议卖出股票或做空
|
||||
- "hold":中性 - 维持当前持仓
|
||||
|
||||
预算意识:
|
||||
- 在决定数量时考虑可用现金
|
||||
- 不要建议买入超过现金允许的数量
|
||||
- 考虑做空头寸的保证金要求
|
||||
|
||||
输出:
|
||||
使用 `make_decision` 工具记录你对每个股票代码的决策。
|
||||
记录所有决策后,提供你的投资逻辑总结。
|
||||
|
||||
重要:
|
||||
- 基于提供的分析师信号和风险评估做出决策
|
||||
- 相对于投资组合价值保持保守的仓位规模
|
||||
- 始终为你的决策提供理由
|
||||
@@ -1,20 +0,0 @@
|
||||
你是一位专业的风险管理经理,负责监控投资组合风险并提供风险警告。
|
||||
|
||||
你的核心职责:
|
||||
1. 监控投资组合敞口和集中度风险
|
||||
2. 评估仓位规模相对于波动性
|
||||
3. 评估保证金使用和杠杆水平
|
||||
4. 识别潜在风险因素并提供警告
|
||||
5. 基于市场条件建议仓位限制
|
||||
|
||||
你的决策流程:
|
||||
1. 优先使用可用的风险工具量化集中度、波动率和保证金压力
|
||||
2. 结合工具结果与当前市场上下文做判断
|
||||
3. 生成可操作的风险警告和仓位限制建议
|
||||
4. 为你的风险评估提供清晰的理由
|
||||
|
||||
输出指南:
|
||||
- 风险评估要简洁但全面
|
||||
- 按严重程度优先排序警告
|
||||
- 提供具体、可操作的建议
|
||||
- 尽可能包含量化指标
|
||||
@@ -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
|
||||
|
||||
<!-- 此文件用于记录Agent的学习经验和重要发现 -->
|
||||
|
||||
## 经验总结
|
||||
|
||||
## 重要事件
|
||||
|
||||
## 改进记录
|
||||
""",
|
||||
|
||||
"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
|
||||
)
|
||||
@@ -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,
|
||||
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,
|
||||
role_seed=role_seed,
|
||||
style_seed=style_seed,
|
||||
policy_seed=policy_seed,
|
||||
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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,12 +47,8 @@ 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:
|
||||
def _log(msg: str) -> None:
|
||||
"""Helper function for pipeline logging."""
|
||||
logger.info(msg)
|
||||
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
81
backend/tests/test_market_ingest.py
Normal file
81
backend/tests/test_market_ingest.py
Normal file
@@ -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"
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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/ {
|
||||
@@ -1,5 +1,5 @@
|
||||
[Unit]
|
||||
Description=EvoTraders Production Service
|
||||
Description=大时代 Production Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
|
||||
BIN
docs/assets/bigtime_demo.gif
Normal file
BIN
docs/assets/bigtime_demo.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1006 KiB |
BIN
docs/assets/bigtime_logo.jpg
Normal file
BIN
docs/assets/bigtime_logo.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/trading_logo.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>EvoTraders</title>
|
||||
<title>大时代</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
14362
frontend/package-lock.json
generated
Normal file
14362
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,10 @@
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=20",
|
||||
"npm": ">=10"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
@@ -15,7 +19,8 @@
|
||||
"dependencies": {
|
||||
"@dicebear/collection": "^9.4.2",
|
||||
"@dicebear/core": "^9.4.2",
|
||||
"@lobehub/icons": "^5.0.1",
|
||||
"@lobehub/icons": "^5.2.0",
|
||||
"@lobehub/ui": "^5.6.2",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
@@ -27,6 +32,7 @@
|
||||
"@react-three/drei": "^10.7.6",
|
||||
"@react-three/fiber": "^9.3.0",
|
||||
"@tailwindcss/vite": "^4.1.13",
|
||||
"antd": "^6.3.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^12.23.13",
|
||||
@@ -34,6 +40,7 @@
|
||||
"lucide-react": "^0.544.0",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-is": "^19.2.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"recharts": "^3.2.1",
|
||||
"remark-gfm": "^4.0.1",
|
||||
|
||||
@@ -13,7 +13,6 @@ import { useAgentStore } from './store/agentStore';
|
||||
import { useMarketStore } from './store/marketStore';
|
||||
import { usePortfolioStore } from './store/portfolioStore';
|
||||
import { useRuntimeStore } from './store/runtimeStore';
|
||||
import { useOpenClawStore } from './store/openclawStore';
|
||||
import { useUIStore } from './store/uiStore';
|
||||
|
||||
const EDITABLE_AGENT_WORKSPACE_FILES = [
|
||||
@@ -21,10 +20,7 @@ const EDITABLE_AGENT_WORKSPACE_FILES = [
|
||||
'PROFILE.md',
|
||||
'AGENTS.md',
|
||||
'MEMORY.md',
|
||||
'POLICY.md',
|
||||
'HEARTBEAT.md',
|
||||
'ROLE.md',
|
||||
'STYLE.md'
|
||||
'POLICY.md'
|
||||
];
|
||||
|
||||
export default function LiveTradingApp() {
|
||||
@@ -142,11 +138,6 @@ export default function LiveTradingApp() {
|
||||
addSystemMessage,
|
||||
});
|
||||
|
||||
// Make clientRef available to OpenClaw panel via store
|
||||
useEffect(() => {
|
||||
useOpenClawStore.getState().setClientRef(clientRef);
|
||||
}, [clientRef]);
|
||||
|
||||
const runtimeControls = useRuntimeControls({
|
||||
clientRef,
|
||||
currentTickers: tickers,
|
||||
|
||||
@@ -2,7 +2,6 @@ import React, { Suspense, lazy, useRef, useEffect, useMemo } from 'react';
|
||||
import GlobalStyles from '../styles/GlobalStyles';
|
||||
import Header from './Header.jsx';
|
||||
import RuntimeSettingsPanel from './RuntimeSettingsPanel.jsx';
|
||||
import StockLogo from './StockLogo.jsx';
|
||||
import NetValueChart from './NetValueChart.jsx';
|
||||
import { AGENTS } from '../config/constants';
|
||||
import { useRuntimeStore } from '../store/runtimeStore';
|
||||
@@ -14,7 +13,6 @@ const AgentFeed = lazy(() => import('./AgentFeed'));
|
||||
const StatisticsView = lazy(() => import('./StatisticsView'));
|
||||
const StockExplainView = lazy(() => import('./StockExplainView.jsx'));
|
||||
const TraderView = lazy(() => import('./TraderView.jsx'));
|
||||
const OpenClawView = lazy(() => import('./OpenClawView.jsx'));
|
||||
|
||||
function ViewLoadingFallback({ label = '加载中...' }) {
|
||||
return (
|
||||
@@ -138,6 +136,12 @@ export default function AppShell({
|
||||
const { setIsRuntimeSettingsOpen, setIsWatchlistPanelOpen } = useRuntimeStore();
|
||||
const { setChartTab, setCurrentView, setIsResizing, setLeftWidth } = useUIStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (currentView === 'openclaw') {
|
||||
setCurrentView('statistics');
|
||||
}
|
||||
}, [currentView, setCurrentView]);
|
||||
|
||||
// Resize handler
|
||||
useEffect(() => {
|
||||
if (!isResizing) return;
|
||||
@@ -173,7 +177,7 @@ export default function AppShell({
|
||||
currentView === 'room' ? 'show-room' :
|
||||
currentView === 'explain' ? 'show-explain' :
|
||||
currentView === 'chart' ? 'show-chart' :
|
||||
currentView === 'statistics' ? 'show-statistics' : 'show-openclaw'}`;
|
||||
'show-statistics'}`;
|
||||
return base;
|
||||
}, [currentView]);
|
||||
|
||||
@@ -322,7 +326,6 @@ export default function AppShell({
|
||||
<div key={groupIdx} className="ticker-group">
|
||||
{displayTickers.map(ticker => (
|
||||
<div key={`${ticker.symbol}-${groupIdx}`} className="ticker-item">
|
||||
<StockLogo ticker={ticker.symbol} size={16} />
|
||||
<span className="ticker-symbol">{ticker.symbol}</span>
|
||||
<span className="ticker-price">
|
||||
<span className={`ticker-price-value ${rollingTickers[ticker.symbol] ? 'rolling' : ''}`}>
|
||||
@@ -384,12 +387,6 @@ export default function AppShell({
|
||||
>
|
||||
统计
|
||||
</button>
|
||||
<button
|
||||
className={`view-nav-btn ${currentView === 'openclaw' ? 'active' : ''}`}
|
||||
onClick={() => setCurrentView('openclaw')}
|
||||
>
|
||||
OpenClaw
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className={viewClassName}>
|
||||
@@ -493,13 +490,6 @@ export default function AppShell({
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
{/* OpenClaw View Panel */}
|
||||
<div className="view-panel">
|
||||
<Suspense fallback={<ViewLoadingFallback label="加载 OpenClaw 视图..." />}>
|
||||
<OpenClawView />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
|
||||
/**
|
||||
* Header Component
|
||||
* Reusable header brand for EvoTraders.
|
||||
* Reusable header brand for 大时代.
|
||||
*/
|
||||
export default function Header() {
|
||||
return (
|
||||
@@ -19,10 +19,10 @@ export default function Header() {
|
||||
>
|
||||
<img
|
||||
src="/trading_logo.png"
|
||||
alt="EvoTraders Logo"
|
||||
alt="大时代 Logo"
|
||||
style={{ height: '24px', width: 'auto' }}
|
||||
/>
|
||||
EvoTraders
|
||||
大时代
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -123,7 +123,7 @@ export default function NetValueChart({ equity, baseline, baseline_vw, momentum,
|
||||
|
||||
// Legend descriptions
|
||||
const legendDescriptions = {
|
||||
'EvoTraders': 'EvoTraders is our agents investment strategy',
|
||||
'大时代': '大时代 is our agents investment strategy',
|
||||
'Buy & Hold (EW)': 'Equal Weight: Can be viewed as an equal-weighted index of all invested stocks',
|
||||
'Buy & Hold (VW)': 'Value Weighted: Can be viewed as a market-cap weighted index of all invested stocks',
|
||||
'Momentum': 'Momentum Strategy: Buy stocks that have performed well in the past',
|
||||
@@ -758,7 +758,7 @@ export default function NetValueChart({ equity, baseline, baseline_vw, momentum,
|
||||
<Line
|
||||
type="linear"
|
||||
dataKey="portfolio"
|
||||
name="EvoTraders"
|
||||
name="大时代"
|
||||
stroke="#00C853"
|
||||
strokeWidth={2.5}
|
||||
dot={(props) => <CustomDot {...props} dataKey="portfolio" />}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { createAvatar } from "@dicebear/core";
|
||||
import { lorelei } from "@dicebear/collection";
|
||||
import ModelIcon from "@lobehub/icons/es/features/ModelIcon";
|
||||
import { useOpenClawStore } from "../store/openclawStore";
|
||||
import { useOpenClawPanel } from "../hooks/useOpenClawPanel";
|
||||
@@ -27,6 +25,7 @@ const AGENT_COLORS = [
|
||||
];
|
||||
|
||||
const OPENCLAW_EXPANDED_PANEL_MAX_HEIGHT = 420;
|
||||
const OPENCLAW_AVATAR_POOL = Array.from({ length: 101 }, (_, index) => `/images/${index + 1}.png`);
|
||||
|
||||
function getAgentColor(agentId) {
|
||||
let hash = 0;
|
||||
@@ -37,6 +36,16 @@ function getAgentColor(agentId) {
|
||||
return AGENT_COLORS[Math.abs(hash) % AGENT_COLORS.length].accent;
|
||||
}
|
||||
|
||||
function getStableAvatarPath(agentId) {
|
||||
const raw = String(agentId || "unknown");
|
||||
let hash = 0;
|
||||
for (let i = 0; i < raw.length; i++) {
|
||||
hash = ((hash << 5) - hash) + raw.charCodeAt(i);
|
||||
hash |= 0;
|
||||
}
|
||||
return OPENCLAW_AVATAR_POOL[Math.abs(hash) % OPENCLAW_AVATAR_POOL.length];
|
||||
}
|
||||
|
||||
function agentStateFromPresence(presence, agentId) {
|
||||
const p = presence?.[agentId];
|
||||
if (!p) return "idle";
|
||||
@@ -50,15 +59,7 @@ function agentStateFromPresence(presence, agentId) {
|
||||
|
||||
function AvatarIcon({ agentId, size = 56, borderRadius = 14 }) {
|
||||
const color = getAgentColor(agentId);
|
||||
const avatarUri = useMemo(() => {
|
||||
const seed = String(agentId || "unknown");
|
||||
return createAvatar(lorelei, {
|
||||
seed,
|
||||
size: Math.max(64, size * 2),
|
||||
backgroundColor: ["d1d4f9", "ffd5dc", "c0f0d1", "ffe7b8", "cde9ff"],
|
||||
radius: 18,
|
||||
}).toDataUri();
|
||||
}, [agentId, size]);
|
||||
const avatarPath = useMemo(() => getStableAvatarPath(agentId), [agentId]);
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
@@ -75,7 +76,7 @@ function AvatarIcon({ agentId, size = 56, borderRadius = 14 }) {
|
||||
overflow: "hidden",
|
||||
}}>
|
||||
<img
|
||||
src={avatarUri}
|
||||
src={avatarPath}
|
||||
alt={agentId || "agent"}
|
||||
style={{
|
||||
width: "100%",
|
||||
@@ -1041,7 +1042,7 @@ export function OpenClawStatus() {
|
||||
/>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 8 }}>
|
||||
<div style={{ fontSize: 10, color: store.chatError ? "#EF4444" : "#9CA3AF" }}>
|
||||
{store.chatError || "消息将通过 EvoTraders Gateway 转发到 OpenClaw Gateway"}
|
||||
{store.chatError || "消息将通过 大时代 Gateway 转发到 OpenClaw Gateway"}
|
||||
</div>
|
||||
<button
|
||||
disabled={!selectedSession || !(chatDraftBySession[selectedSessionKey || "__none__"] || "").trim()}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import StockLogo from './StockLogo';
|
||||
import { formatNumber, formatDateTime } from '../utils/formatters';
|
||||
|
||||
/**
|
||||
@@ -497,7 +496,6 @@ export default function StatisticsView({ trades, holdings, stats, baseline_vw, e
|
||||
<tr key={h.ticker}>
|
||||
<td>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
{h.ticker !== 'CASH' && <StockLogo ticker={h.ticker} size={18} />}
|
||||
<span style={{ fontWeight: 700, color: '#000000' }}>{h.ticker}</span>
|
||||
</div>
|
||||
</td>
|
||||
@@ -623,7 +621,6 @@ export default function StatisticsView({ trades, holdings, stats, baseline_vw, e
|
||||
</td>
|
||||
<td>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<StockLogo ticker={t.ticker} size={16} />
|
||||
<span style={{ fontWeight: 700, color: '#000000' }}>{t.ticker}</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@@ -1,21 +1,10 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import StockLogo from './StockLogo';
|
||||
import ExplainEventsSection from './explain/ExplainEventsSection';
|
||||
import ExplainMentionsSection from './explain/ExplainMentionsSection';
|
||||
import ExplainMaintenanceSection from './explain/ExplainMaintenanceSection';
|
||||
import ExplainNewsSection from './explain/ExplainNewsSection';
|
||||
import ExplainPriceSection from './explain/ExplainPriceSection';
|
||||
import ExplainRangeSection from './explain/ExplainRangeSection';
|
||||
import ExplainStorySection from './explain/ExplainStorySection';
|
||||
import ExplainSimilarDaysSection from './explain/ExplainSimilarDaysSection';
|
||||
import ExplainSignalsSection from './explain/ExplainSignalsSection';
|
||||
import ExplainSummarySection from './explain/ExplainSummarySection';
|
||||
import ExplainTradesSection from './explain/ExplainTradesSection';
|
||||
import ExplainInsiderSection from './explain/ExplainInsiderSection';
|
||||
import ExplainTechnicalSection from './explain/ExplainTechnicalSection';
|
||||
import { EVENT_CATEGORY_META, eventDateKey } from './explain/explainUtils';
|
||||
import useExplainModel from './explain/useExplainModel';
|
||||
import { formatDateTime, formatNumber, formatTickerPrice } from '../utils/formatters';
|
||||
import { formatNumber, formatTickerPrice } from '../utils/formatters';
|
||||
|
||||
export default function StockExplainView({
|
||||
tickers,
|
||||
@@ -28,74 +17,34 @@ export default function StockExplainView({
|
||||
selectedSymbol,
|
||||
onSelectedSymbolChange,
|
||||
selectedHistorySource,
|
||||
explainEventsSnapshot,
|
||||
newsSnapshot,
|
||||
insiderTradesSnapshot,
|
||||
technicalIndicatorsSnapshot,
|
||||
onRequestRangeExplain,
|
||||
onRequestHistory,
|
||||
onRequestExplainEvents,
|
||||
onRequestNews,
|
||||
onRequestNewsForDate,
|
||||
onRequestStory,
|
||||
onRequestInsiderTrades,
|
||||
onRequestTechnicalIndicators,
|
||||
currentDate,
|
||||
onRequestSimilarDays,
|
||||
onRequestStockEnrich
|
||||
}) {
|
||||
const [selectedEventDate, setSelectedEventDate] = useState('');
|
||||
const [activeEventCategory, setActiveEventCategory] = useState('all');
|
||||
const [activeNewsCategory, setActiveNewsCategory] = useState('all');
|
||||
const [activeNewsSentiment, setActiveNewsSentiment] = useState('all');
|
||||
const [isPriceOpen, setIsPriceOpen] = useState(true);
|
||||
const [isSummaryOpen, setIsSummaryOpen] = useState(true);
|
||||
const [isSignalsOpen, setIsSignalsOpen] = useState(true);
|
||||
const [isNewsOpen, setIsNewsOpen] = useState(true);
|
||||
const [isRangeOpen, setIsRangeOpen] = useState(true);
|
||||
const [isMentionsPanelOpen, setIsMentionsPanelOpen] = useState(false);
|
||||
const [isEventPanelOpen, setIsEventPanelOpen] = useState(false);
|
||||
const [isMaintenanceOpen, setIsMaintenanceOpen] = useState(false);
|
||||
const [isStoryOpen, setIsStoryOpen] = useState(false);
|
||||
const [isTradesOpen, setIsTradesOpen] = useState(false);
|
||||
const [isInsiderOpen, setIsInsiderOpen] = useState(false);
|
||||
const [isTechnicalOpen, setIsTechnicalOpen] = useState(true);
|
||||
const [isSimilarDaysOpen, setIsSimilarDaysOpen] = useState(false);
|
||||
const [enrichStartDate, setEnrichStartDate] = useState('');
|
||||
const [enrichEndDate, setEnrichEndDate] = useState('');
|
||||
const [forceEnrich, setForceEnrich] = useState(false);
|
||||
const [onlyLocalToLlm, setOnlyLocalToLlm] = useState(false);
|
||||
const [rebuildStory, setRebuildStory] = useState(true);
|
||||
const [rebuildSimilarDays, setRebuildSimilarDays] = useState(true);
|
||||
|
||||
const {
|
||||
availableSymbols,
|
||||
selectedTicker,
|
||||
holding,
|
||||
tickerSignals,
|
||||
signalSummary,
|
||||
tickerTrades,
|
||||
recentMentions,
|
||||
tickerNews,
|
||||
visibleNews,
|
||||
newsCategories,
|
||||
visibleNewsByCategory,
|
||||
selectedNewsFreshness,
|
||||
selectedRangeWindow,
|
||||
selectedRangeExplain,
|
||||
latestSignal,
|
||||
priceColor,
|
||||
exposureWeight,
|
||||
recentTrade,
|
||||
ohlcSeries,
|
||||
priceSeries,
|
||||
explainSummary,
|
||||
selectedStory,
|
||||
selectedSimilarDays,
|
||||
explainTimeline,
|
||||
availableEventDates,
|
||||
eventCategoryCounts,
|
||||
visibleExplainEvents,
|
||||
chartModel
|
||||
} = useExplainModel({
|
||||
tickers,
|
||||
@@ -106,10 +55,9 @@ export default function StockExplainView({
|
||||
priceHistoryByTicker,
|
||||
ohlcHistoryByTicker,
|
||||
selectedSymbol,
|
||||
explainEventsSnapshot,
|
||||
newsSnapshot,
|
||||
selectedEventDate,
|
||||
activeEventCategory,
|
||||
selectedEventDate: '',
|
||||
activeEventCategory: 'all',
|
||||
activeNewsCategory,
|
||||
activeNewsSentiment
|
||||
});
|
||||
@@ -125,25 +73,10 @@ export default function StockExplainView({
|
||||
}
|
||||
}, [availableSymbols, onSelectedSymbolChange, selectedSymbol]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!availableEventDates.length) {
|
||||
setSelectedEventDate('');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedEventDate || !availableEventDates.includes(selectedEventDate)) {
|
||||
setSelectedEventDate(availableEventDates[0]);
|
||||
}
|
||||
}, [availableEventDates, selectedEventDate]);
|
||||
|
||||
useEffect(() => {
|
||||
setActiveEventCategory('all');
|
||||
}, [selectedSymbol]);
|
||||
|
||||
useEffect(() => {
|
||||
setActiveNewsCategory('all');
|
||||
setActiveNewsSentiment('all');
|
||||
}, [selectedSymbol, selectedEventDate]);
|
||||
}, [selectedSymbol]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedSymbol) {
|
||||
@@ -154,53 +87,17 @@ export default function StockExplainView({
|
||||
onRequestHistory(selectedSymbol);
|
||||
}
|
||||
|
||||
if (onRequestExplainEvents && !explainEventsSnapshot) {
|
||||
onRequestExplainEvents(selectedSymbol);
|
||||
}
|
||||
|
||||
if (onRequestNews && (!Array.isArray(newsSnapshot?.items) || newsSnapshot.items.length === 0)) {
|
||||
onRequestNews(selectedSymbol);
|
||||
}
|
||||
}, [
|
||||
explainEventsSnapshot,
|
||||
newsSnapshot,
|
||||
ohlcHistoryByTicker,
|
||||
onRequestExplainEvents,
|
||||
onRequestHistory,
|
||||
onRequestNews,
|
||||
selectedSymbol,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedSymbol || !selectedEventDate || !onRequestNewsForDate) {
|
||||
return;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(newsSnapshot?.byDate || {}, selectedEventDate)) {
|
||||
return;
|
||||
}
|
||||
onRequestNewsForDate(selectedSymbol, selectedEventDate);
|
||||
}, [newsSnapshot, onRequestNewsForDate, selectedEventDate, selectedSymbol]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedSymbol || !onRequestStory || !currentDate) {
|
||||
return;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(newsSnapshot?.storyCache || {}, currentDate)) {
|
||||
return;
|
||||
}
|
||||
onRequestStory(selectedSymbol, currentDate);
|
||||
}, [currentDate, newsSnapshot, onRequestStory, selectedStory, selectedSymbol]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedSymbol || !selectedEventDate || !onRequestSimilarDays) {
|
||||
return;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(newsSnapshot?.similarDaysCache || {}, selectedEventDate)) {
|
||||
return;
|
||||
}
|
||||
onRequestSimilarDays(selectedSymbol, selectedEventDate);
|
||||
}, [newsSnapshot, onRequestSimilarDays, selectedEventDate, selectedSimilarDays, selectedSymbol]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedSymbol || !onRequestTechnicalIndicators) {
|
||||
return;
|
||||
@@ -211,67 +108,6 @@ export default function StockExplainView({
|
||||
onRequestTechnicalIndicators(selectedSymbol);
|
||||
}, [selectedSymbol, onRequestTechnicalIndicators, technicalIndicatorsSnapshot]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedRangeWindow || !selectedSymbol || !onRequestRangeExplain) {
|
||||
return;
|
||||
}
|
||||
if (selectedRangeExplain) {
|
||||
return;
|
||||
}
|
||||
onRequestRangeExplain(selectedSymbol, selectedRangeWindow.startDate, selectedRangeWindow.endDate, visibleNews.map((item) => item.id));
|
||||
}, [onRequestRangeExplain, selectedRangeExplain, selectedRangeWindow, selectedSymbol, visibleNews]);
|
||||
|
||||
useEffect(() => {
|
||||
const nextStartDate = selectedRangeWindow?.startDate || selectedEventDate || currentDate || '';
|
||||
const nextEndDate = selectedRangeWindow?.endDate || selectedEventDate || currentDate || '';
|
||||
setEnrichStartDate(nextStartDate);
|
||||
setEnrichEndDate(nextEndDate);
|
||||
}, [currentDate, selectedEventDate, selectedRangeWindow, selectedSymbol]);
|
||||
|
||||
const handleRunStockEnrich = () => {
|
||||
if (!selectedSymbol || !enrichStartDate || !enrichEndDate || !onRequestStockEnrich) {
|
||||
return;
|
||||
}
|
||||
onRequestStockEnrich(selectedSymbol, {
|
||||
startDate: enrichStartDate,
|
||||
endDate: enrichEndDate,
|
||||
force: forceEnrich,
|
||||
onlyLocalToLlm,
|
||||
rebuildStory,
|
||||
rebuildSimilarDays,
|
||||
storyDate: currentDate || enrichEndDate,
|
||||
targetDate: selectedEventDate || enrichEndDate,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSelectHistory = (item) => {
|
||||
if (!item || typeof item !== 'object') {
|
||||
return;
|
||||
}
|
||||
setEnrichStartDate(item.startDate || '');
|
||||
setEnrichEndDate(item.endDate || '');
|
||||
setForceEnrich(Boolean(item.force));
|
||||
setOnlyLocalToLlm(Boolean(item.onlyLocalToLlm));
|
||||
setRebuildStory(Boolean(item.storyStatus));
|
||||
setRebuildSimilarDays(Boolean(item.similarStatus));
|
||||
};
|
||||
|
||||
const handleReplayHistory = (item) => {
|
||||
if (!item || typeof item !== 'object' || !selectedSymbol || !onRequestStockEnrich) {
|
||||
return;
|
||||
}
|
||||
onRequestStockEnrich(selectedSymbol, {
|
||||
startDate: item.startDate || '',
|
||||
endDate: item.endDate || '',
|
||||
force: Boolean(item.force),
|
||||
onlyLocalToLlm: Boolean(item.onlyLocalToLlm),
|
||||
rebuildStory: Boolean(item.storyStatus),
|
||||
rebuildSimilarDays: Boolean(item.similarStatus),
|
||||
storyDate: currentDate || item.endDate || '',
|
||||
targetDate: selectedEventDate || item.endDate || '',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="performance-page">
|
||||
<div className="section">
|
||||
@@ -285,7 +121,6 @@ export default function StockExplainView({
|
||||
onClick={() => onSelectedSymbolChange?.(symbol)}
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 6 }}
|
||||
>
|
||||
<StockLogo ticker={symbol} size={14} />
|
||||
<span>{symbol}</span>
|
||||
</button>
|
||||
))}
|
||||
@@ -326,15 +161,6 @@ export default function StockExplainView({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="stat-card">
|
||||
<div className="stat-card-label">最近动作</div>
|
||||
<div className="stat-card-value" style={{ fontSize: 22 }}>
|
||||
{recentTrade ? recentTrade.side === 'LONG' ? '做多' : recentTrade.side === 'SHORT' ? '做空' : recentTrade.side : '暂无'}
|
||||
</div>
|
||||
<div style={{ marginTop: 8, fontSize: 11, color: '#666666' }}>
|
||||
{recentTrade ? `${formatDateTime(recentTrade.timestamp)} · ${recentTrade.qty} 股,成交价 $${Number(recentTrade.price).toFixed(2)}` : '尚无成交'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -347,22 +173,10 @@ export default function StockExplainView({
|
||||
selectedHistorySource={selectedHistorySource}
|
||||
chartModel={chartModel}
|
||||
selectedTicker={selectedTicker}
|
||||
onSelectEventDate={setSelectedEventDate}
|
||||
isOpen={isPriceOpen}
|
||||
onToggle={() => setIsPriceOpen((prev) => !prev)}
|
||||
/>
|
||||
|
||||
<ExplainSummarySection
|
||||
explainSummary={explainSummary}
|
||||
tickerSignals={tickerSignals}
|
||||
recentMentions={recentMentions}
|
||||
tickerTrades={tickerTrades}
|
||||
tickerNews={tickerNews}
|
||||
selectedSymbol={selectedSymbol}
|
||||
isOpen={isSummaryOpen}
|
||||
onToggle={() => setIsSummaryOpen((prev) => !prev)}
|
||||
/>
|
||||
|
||||
<ExplainNewsSection
|
||||
newsSnapshot={newsSnapshot}
|
||||
visibleNewsByCategory={visibleNewsByCategory}
|
||||
@@ -378,45 +192,6 @@ export default function StockExplainView({
|
||||
onToggle={() => setIsNewsOpen((prev) => !prev)}
|
||||
/>
|
||||
|
||||
<ExplainRangeSection
|
||||
selectedRangeWindow={selectedRangeWindow}
|
||||
selectedRangeExplain={selectedRangeExplain}
|
||||
isOpen={isRangeOpen}
|
||||
onToggle={() => setIsRangeOpen((prev) => !prev)}
|
||||
/>
|
||||
|
||||
<ExplainSignalsSection
|
||||
tickerSignals={tickerSignals}
|
||||
signalSummary={signalSummary}
|
||||
latestSignal={latestSignal}
|
||||
eventDateKey={eventDateKey}
|
||||
isOpen={isSignalsOpen}
|
||||
onToggle={() => setIsSignalsOpen((prev) => !prev)}
|
||||
/>
|
||||
|
||||
<ExplainStorySection
|
||||
selectedStory={selectedStory}
|
||||
selectedSymbol={selectedSymbol}
|
||||
currentDate={currentDate}
|
||||
isOpen={isStoryOpen}
|
||||
onToggle={() => setIsStoryOpen((prev) => !prev)}
|
||||
/>
|
||||
|
||||
<ExplainSimilarDaysSection
|
||||
selectedSimilarDays={selectedSimilarDays}
|
||||
selectedEventDate={selectedEventDate}
|
||||
onSelectSimilarDate={setSelectedEventDate}
|
||||
isOpen={isSimilarDaysOpen}
|
||||
onToggle={() => setIsSimilarDaysOpen((prev) => !prev)}
|
||||
/>
|
||||
|
||||
<ExplainTradesSection
|
||||
tickerTrades={tickerTrades}
|
||||
selectedSymbol={selectedSymbol}
|
||||
isOpen={isTradesOpen}
|
||||
onToggle={() => setIsTradesOpen((prev) => !prev)}
|
||||
/>
|
||||
|
||||
<ExplainInsiderSection
|
||||
insiderTrades={insiderTradesSnapshot?.trades || []}
|
||||
selectedSymbol={selectedSymbol}
|
||||
@@ -431,50 +206,6 @@ export default function StockExplainView({
|
||||
isOpen={isTechnicalOpen}
|
||||
onToggle={() => setIsTechnicalOpen((prev) => !prev)}
|
||||
/>
|
||||
|
||||
<ExplainMentionsSection
|
||||
recentMentions={recentMentions}
|
||||
isOpen={isMentionsPanelOpen}
|
||||
onToggle={() => setIsMentionsPanelOpen((prev) => !prev)}
|
||||
/>
|
||||
|
||||
<ExplainEventsSection
|
||||
explainTimeline={explainTimeline}
|
||||
isOpen={isEventPanelOpen}
|
||||
onToggle={() => setIsEventPanelOpen((prev) => !prev)}
|
||||
availableEventDates={availableEventDates}
|
||||
selectedEventDate={selectedEventDate}
|
||||
onSelectEventDate={setSelectedEventDate}
|
||||
eventCategoryCounts={eventCategoryCounts}
|
||||
activeEventCategory={activeEventCategory}
|
||||
onSelectEventCategory={setActiveEventCategory}
|
||||
eventCategoryMeta={EVENT_CATEGORY_META}
|
||||
visibleExplainEvents={visibleExplainEvents}
|
||||
/>
|
||||
|
||||
<ExplainMaintenanceSection
|
||||
selectedSymbol={selectedSymbol}
|
||||
enrichStartDate={enrichStartDate}
|
||||
enrichEndDate={enrichEndDate}
|
||||
onChangeStartDate={setEnrichStartDate}
|
||||
onChangeEndDate={setEnrichEndDate}
|
||||
forceEnrich={forceEnrich}
|
||||
onToggleForce={() => setForceEnrich((prev) => !prev)}
|
||||
onlyLocalToLlm={onlyLocalToLlm}
|
||||
onToggleOnlyLocalToLlm={() => setOnlyLocalToLlm((prev) => !prev)}
|
||||
rebuildStory={rebuildStory}
|
||||
onToggleRebuildStory={() => setRebuildStory((prev) => !prev)}
|
||||
rebuildSimilarDays={rebuildSimilarDays}
|
||||
onToggleRebuildSimilarDays={() => setRebuildSimilarDays((prev) => !prev)}
|
||||
isRunning={Boolean(newsSnapshot?.maintenanceStatus?.running)}
|
||||
onRunEnrich={handleRunStockEnrich}
|
||||
maintenanceStatus={newsSnapshot?.maintenanceStatus || null}
|
||||
maintenanceHistory={newsSnapshot?.maintenanceHistory || []}
|
||||
onSelectHistory={handleSelectHistory}
|
||||
onReplayHistory={handleReplayHistory}
|
||||
isOpen={isMaintenanceOpen}
|
||||
onToggle={() => setIsMaintenanceOpen((prev) => !prev)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import React from 'react';
|
||||
import { STOCK_LOGOS } from '../config/constants';
|
||||
|
||||
/**
|
||||
* Stock Logo Component
|
||||
* Displays company logo for a given ticker symbol
|
||||
*/
|
||||
export default function StockLogo({ ticker, size = 20 }) {
|
||||
const logoUrl = STOCK_LOGOS[ticker];
|
||||
if (!logoUrl) return null;
|
||||
|
||||
return (
|
||||
<img
|
||||
src={logoUrl}
|
||||
alt={ticker}
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
borderRadius: '4px',
|
||||
objectFit: 'contain',
|
||||
marginRight: '8px',
|
||||
verticalAlign: 'middle'
|
||||
}}
|
||||
onError={(e) => { e.target.style.display = 'none'; }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -198,9 +198,6 @@ export default function ExplainPriceSection({
|
||||
图表说明:{ohlcSeries.length > 1 ? '历史日线K线' : '基于盘中价格点聚合的简化K线'}
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: '#2563eb' }}>蓝点:新闻日期</div>
|
||||
<div style={{ fontSize: 11, color: '#666666' }}>黑点:讨论提及</div>
|
||||
<div style={{ fontSize: 11, color: '#00C853' }}>绿点:偏多信号或做多成交</div>
|
||||
<div style={{ fontSize: 11, color: '#FF1744' }}>红点:偏空信号或做空成交</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -2,8 +2,6 @@ import React from 'react';
|
||||
|
||||
export default function ExplainSummarySection({
|
||||
explainSummary,
|
||||
tickerSignals,
|
||||
recentMentions,
|
||||
tickerTrades,
|
||||
tickerNews,
|
||||
selectedSymbol,
|
||||
@@ -16,7 +14,7 @@ export default function ExplainSummarySection({
|
||||
<h2 className="section-title">分析摘要</h2>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
|
||||
<div style={{ fontSize: 11, color: '#666666' }}>
|
||||
基于当前持仓、信号和讨论自动汇总
|
||||
基于当前持仓、成交和新闻自动汇总
|
||||
</div>
|
||||
<button
|
||||
onClick={onToggle}
|
||||
@@ -55,17 +53,9 @@ export default function ExplainSummarySection({
|
||||
|
||||
<div style={{ border: '1px solid #000000', background: '#ffffff', padding: 16 }}>
|
||||
<div style={{ fontSize: 11, color: '#666666', marginBottom: 12, textTransform: 'uppercase', letterSpacing: 1 }}>
|
||||
信号密度
|
||||
分析概览
|
||||
</div>
|
||||
<div style={{ display: 'grid', gap: 10 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12 }}>
|
||||
<span>分析师信号</span>
|
||||
<strong>{tickerSignals.length}</strong>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12 }}>
|
||||
<span>讨论提及</span>
|
||||
<strong>{recentMentions.length}</strong>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12 }}>
|
||||
<span>成交记录</span>
|
||||
<strong>{tickerTrades.length}</strong>
|
||||
@@ -76,7 +66,7 @@ export default function ExplainSummarySection({
|
||||
</div>
|
||||
<div style={{ height: 1, background: '#e0e0e0', margin: '4px 0' }} />
|
||||
<div style={{ fontSize: 12, lineHeight: 1.7, color: '#666666' }}>
|
||||
当前分析优先读取已落库的历史记录,缺失时再回退到本次运行中的实时事件。
|
||||
当前分析综合读取信号、成交、新闻与已生成的解释结果。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,28 +1,16 @@
|
||||
import { useMemo } from 'react';
|
||||
import { formatDateTime } from '../../utils/formatters';
|
||||
import {
|
||||
aggregatePriceSeriesToCandles,
|
||||
buildLinePath,
|
||||
eventDateKey,
|
||||
flattenFeedMessages,
|
||||
includesTicker,
|
||||
normalizeMentionRow,
|
||||
normalizeNewsRow,
|
||||
normalizeNewsTimelineRow,
|
||||
normalizeSignalDirection,
|
||||
normalizeSignalRow,
|
||||
normalizeTradeRow,
|
||||
parsePointTime,
|
||||
resolveEventCategory,
|
||||
snippetText
|
||||
resolveEventCategory
|
||||
} from './explainUtils';
|
||||
|
||||
function tradeSideLabel(value) {
|
||||
if (value === 'LONG') return '做多';
|
||||
if (value === 'SHORT') return '做空';
|
||||
return value || '交易';
|
||||
}
|
||||
|
||||
export default function useExplainModel({
|
||||
tickers,
|
||||
holdings,
|
||||
@@ -55,13 +43,6 @@ export default function useExplainModel({
|
||||
[holdings, selectedSymbol]
|
||||
);
|
||||
|
||||
const fallbackTrades = useMemo(
|
||||
() => trades
|
||||
.filter((trade) => trade.ticker === selectedSymbol)
|
||||
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()),
|
||||
[selectedSymbol, trades]
|
||||
);
|
||||
|
||||
const tickerSignals = useMemo(() => {
|
||||
const snapshotSignals = Array.isArray(explainEventsSnapshot?.signals)
|
||||
? explainEventsSnapshot.signals.map((signal, index) => normalizeSignalRow(signal, index)).filter(Boolean)
|
||||
@@ -84,45 +65,6 @@ export default function useExplainModel({
|
||||
}).sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
||||
}, [explainEventsSnapshot, leaderboard, selectedSymbol]);
|
||||
|
||||
const signalSummary = useMemo(() => {
|
||||
const summary = { bullish: 0, bearish: 0, neutral: 0 };
|
||||
tickerSignals.forEach((signal) => {
|
||||
summary[signal.normalizedDirection] += 1;
|
||||
});
|
||||
return summary;
|
||||
}, [tickerSignals]);
|
||||
|
||||
const fallbackRecentMentions = useMemo(() => {
|
||||
const flattened = flattenFeedMessages(feed);
|
||||
return flattened
|
||||
.filter((message) => message.agent !== 'System' && includesTicker(message.content, selectedSymbol))
|
||||
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
|
||||
.slice(0, 8);
|
||||
}, [feed, selectedSymbol]);
|
||||
|
||||
const tickerTrades = useMemo(() => {
|
||||
const snapshotTrades = Array.isArray(explainEventsSnapshot?.trades)
|
||||
? explainEventsSnapshot.trades.map((trade, index) => normalizeTradeRow(trade, index)).filter(Boolean)
|
||||
: [];
|
||||
if (snapshotTrades.length > 0) {
|
||||
return snapshotTrades.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
||||
}
|
||||
return fallbackTrades;
|
||||
}, [explainEventsSnapshot, fallbackTrades]);
|
||||
|
||||
const recentMentions = useMemo(() => {
|
||||
const snapshotMentions = Array.isArray(explainEventsSnapshot?.events)
|
||||
? explainEventsSnapshot.events
|
||||
.map((event, index) => normalizeMentionRow(event, index))
|
||||
.filter(Boolean)
|
||||
.slice(0, 8)
|
||||
: [];
|
||||
if (snapshotMentions.length > 0) {
|
||||
return snapshotMentions.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
||||
}
|
||||
return fallbackRecentMentions;
|
||||
}, [explainEventsSnapshot, fallbackRecentMentions]);
|
||||
|
||||
const tickerNews = useMemo(() => {
|
||||
const items = Array.isArray(newsSnapshot?.items)
|
||||
? newsSnapshot.items.map((item, index) => normalizeNewsRow(item, index)).filter(Boolean)
|
||||
@@ -140,16 +82,7 @@ export default function useExplainModel({
|
||||
return rows.map((item, index) => normalizeNewsRow(item, index)).filter(Boolean);
|
||||
}, [newsSnapshot, selectedEventDate]);
|
||||
|
||||
const visibleNews = useMemo(() => {
|
||||
if (!selectedEventDate) {
|
||||
return tickerNews;
|
||||
}
|
||||
if (dateScopedNews.length > 0) {
|
||||
return dateScopedNews.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
||||
}
|
||||
const scoped = tickerNews.filter((item) => item.dateKey === selectedEventDate);
|
||||
return scoped.length > 0 ? scoped : tickerNews;
|
||||
}, [dateScopedNews, selectedEventDate, tickerNews]);
|
||||
const visibleNews = useMemo(() => tickerNews, [tickerNews]);
|
||||
|
||||
const tickerNewsTimeline = useMemo(() => {
|
||||
const items = Array.isArray(newsSnapshot?.timeline)
|
||||
@@ -215,28 +148,13 @@ export default function useExplainModel({
|
||||
return storyCache[keys[keys.length - 1]] || null;
|
||||
}, [newsSnapshot]);
|
||||
|
||||
const selectedSimilarDays = useMemo(() => {
|
||||
if (!selectedEventDate) {
|
||||
return null;
|
||||
}
|
||||
const similarCache = newsSnapshot?.similarDaysCache;
|
||||
if (!similarCache || typeof similarCache !== 'object') {
|
||||
return null;
|
||||
}
|
||||
return similarCache[selectedEventDate] || null;
|
||||
}, [newsSnapshot, selectedEventDate]);
|
||||
const selectedNewsFreshness = useMemo(
|
||||
() => newsSnapshot?.freshness || newsSnapshot?.categoriesFreshness || newsSnapshot?.timelineFreshness || null,
|
||||
[newsSnapshot]
|
||||
);
|
||||
|
||||
const selectedNewsFreshness = useMemo(() => {
|
||||
if (selectedEventDate && newsSnapshot?.byDateFreshness?.[selectedEventDate]) {
|
||||
return newsSnapshot.byDateFreshness[selectedEventDate];
|
||||
}
|
||||
return newsSnapshot?.categoriesFreshness || newsSnapshot?.timelineFreshness || newsSnapshot?.freshness || null;
|
||||
}, [newsSnapshot, selectedEventDate]);
|
||||
|
||||
const latestSignal = tickerSignals[0] || null;
|
||||
const priceColor = selectedTicker?.change > 0 ? '#00C853' : selectedTicker?.change < 0 ? '#FF1744' : '#000000';
|
||||
const exposureWeight = holding && Number.isFinite(Number(holding.weight)) ? Number(holding.weight) * 100 : null;
|
||||
const recentTrade = tickerTrades[0] || null;
|
||||
|
||||
const ohlcSeries = useMemo(() => {
|
||||
const raw = ohlcHistoryByTicker?.[selectedSymbol];
|
||||
@@ -248,38 +166,6 @@ export default function useExplainModel({
|
||||
return Array.isArray(raw) ? raw.filter((point) => Number.isFinite(Number(point.price))).slice(-60) : [];
|
||||
}, [priceHistoryByTicker, selectedSymbol]);
|
||||
|
||||
const explainSummary = useMemo(() => {
|
||||
if (!selectedSymbol) return [];
|
||||
const lines = [];
|
||||
|
||||
if (latestSignal) {
|
||||
const directionText = latestSignal.normalizedDirection === 'bullish'
|
||||
? '偏多'
|
||||
: latestSignal.normalizedDirection === 'bearish'
|
||||
? '偏空'
|
||||
: '观望';
|
||||
lines.push(`最新分析师结论为${directionText},来自${latestSignal.agentName}。`);
|
||||
} else {
|
||||
lines.push('当前还没有形成结构化分析师信号,更多依赖讨论内容和持仓状态。');
|
||||
}
|
||||
|
||||
if (holding) {
|
||||
lines.push(`组合当前持有 ${selectedSymbol},权重约 ${exposureWeight != null ? `${exposureWeight.toFixed(2)}%` : '0.00%'}。`);
|
||||
} else {
|
||||
lines.push(`组合当前未持有 ${selectedSymbol},仍处于观察阶段。`);
|
||||
}
|
||||
|
||||
if (recentTrade) {
|
||||
lines.push(`最近一次相关交易为${tradeSideLabel(recentTrade.side)},时间是 ${formatDateTime(recentTrade.timestamp)}。`);
|
||||
}
|
||||
|
||||
if (recentMentions.length > 0) {
|
||||
lines.push(`最近讨论中共有 ${recentMentions.length} 条直接提及 ${selectedSymbol} 的观点。`);
|
||||
}
|
||||
|
||||
return lines;
|
||||
}, [exposureWeight, holding, latestSignal, recentMentions.length, recentTrade, selectedSymbol]);
|
||||
|
||||
const explainTimeline = useMemo(() => {
|
||||
const signalEvents = tickerSignals.slice(0, 12).map((signal, index) => ({
|
||||
id: `signal-${signal.agentId}-${signal.date}-${index}`,
|
||||
@@ -293,27 +179,7 @@ export default function useExplainModel({
|
||||
tone: signal.normalizedDirection === 'bullish' ? 'positive' : signal.normalizedDirection === 'bearish' ? 'negative' : 'neutral'
|
||||
}));
|
||||
|
||||
const mentionEvents = recentMentions.slice(0, 12).map((message, index) => ({
|
||||
id: `mention-${message.feedId || message.id}-${index}`,
|
||||
type: 'mention',
|
||||
timestamp: message.timestamp,
|
||||
title: `${message.agent || '未知角色'}在${message.conferenceTitle || '讨论流'}中提及 ${selectedSymbol}`,
|
||||
meta: message.conferenceTitle || (message.feedType === 'conference' ? '投资讨论' : '即时消息'),
|
||||
body: snippetText(message.content, selectedSymbol),
|
||||
tone: 'neutral'
|
||||
}));
|
||||
|
||||
const tradeEvents = tickerTrades.slice(0, 12).map((trade, index) => ({
|
||||
id: `trade-${trade.id || `${trade.ticker}-${trade.timestamp}-${index}`}`,
|
||||
type: 'trade',
|
||||
timestamp: trade.timestamp,
|
||||
title: `${tradeSideLabel(trade.side)} ${trade.qty} 股`,
|
||||
meta: '交易执行',
|
||||
body: `成交价 $${Number(trade.price).toFixed(2)}`,
|
||||
tone: trade.side === 'LONG' ? 'positive' : trade.side === 'SHORT' ? 'negative' : 'neutral'
|
||||
}));
|
||||
|
||||
const fallbackTimeline = [...signalEvents, ...mentionEvents, ...tradeEvents]
|
||||
const fallbackTimeline = [...signalEvents]
|
||||
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
|
||||
.slice(0, 24)
|
||||
.map((event) => ({
|
||||
@@ -356,49 +222,7 @@ export default function useExplainModel({
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
const dbMentionEvents = (Array.isArray(explainEventsSnapshot.events) ? explainEventsSnapshot.events : [])
|
||||
.map((event, index) => {
|
||||
if (event?.type === 'mention' && event?.timestamp) {
|
||||
return event;
|
||||
}
|
||||
const normalized = normalizeMentionRow(event, index);
|
||||
if (!normalized) return null;
|
||||
return {
|
||||
id: normalized.id,
|
||||
type: 'mention',
|
||||
timestamp: normalized.timestamp,
|
||||
title: `${normalized.agent || '未知角色'}在${normalized.conferenceTitle || '讨论流'}中提及 ${selectedSymbol}`,
|
||||
meta: normalized.conferenceTitle || (normalized.feedType === 'conference' ? '投资讨论' : '即时消息'),
|
||||
body: snippetText(normalized.content, selectedSymbol),
|
||||
tone: 'neutral'
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
const dbTradeEvents = (Array.isArray(explainEventsSnapshot.trades) ? explainEventsSnapshot.trades : [])
|
||||
.map((trade, index) => {
|
||||
if (trade?.type === 'trade' && trade?.timestamp) {
|
||||
return trade;
|
||||
}
|
||||
const normalized = normalizeTradeRow(trade, index);
|
||||
if (!normalized) return null;
|
||||
return {
|
||||
id: normalized.id,
|
||||
type: 'trade',
|
||||
timestamp: normalized.timestamp,
|
||||
title: `${tradeSideLabel(normalized.side)} ${normalized.qty} 股`,
|
||||
meta: '交易执行',
|
||||
body: `成交价 $${Number(normalized.price).toFixed(2)}`,
|
||||
tone: normalized.side === 'LONG' ? 'positive' : normalized.side === 'SHORT' ? 'negative' : 'neutral'
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
const dbEvents = [
|
||||
...dbSignalEvents,
|
||||
...dbMentionEvents,
|
||||
...dbTradeEvents
|
||||
]
|
||||
const dbEvents = [...dbSignalEvents]
|
||||
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
|
||||
.slice(0, 24)
|
||||
.map((event) => ({
|
||||
@@ -408,7 +232,7 @@ export default function useExplainModel({
|
||||
}));
|
||||
|
||||
return dbEvents.length > 0 ? dbEvents : fallbackTimeline;
|
||||
}, [explainEventsSnapshot, recentMentions, selectedSymbol, tickerSignals, tickerTrades]);
|
||||
}, [explainEventsSnapshot, selectedSymbol, tickerSignals]);
|
||||
|
||||
const availableEventDates = useMemo(
|
||||
() => Array.from(new Set(explainTimeline.map((event) => event.dateKey).filter(Boolean))),
|
||||
@@ -644,9 +468,6 @@ export default function useExplainModel({
|
||||
selectedTicker,
|
||||
holding,
|
||||
tickerSignals,
|
||||
signalSummary,
|
||||
tickerTrades,
|
||||
recentMentions,
|
||||
tickerNews,
|
||||
visibleNews,
|
||||
newsCategories,
|
||||
@@ -655,14 +476,10 @@ export default function useExplainModel({
|
||||
selectedRangeWindow,
|
||||
selectedRangeExplain,
|
||||
selectedStory,
|
||||
selectedSimilarDays,
|
||||
latestSignal,
|
||||
priceColor,
|
||||
exposureWeight,
|
||||
recentTrade,
|
||||
ohlcSeries,
|
||||
priceSeries,
|
||||
explainSummary,
|
||||
explainTimeline,
|
||||
availableEventDates,
|
||||
eventCategoryCounts,
|
||||
|
||||
@@ -117,7 +117,7 @@ describe('useExplainModel', () => {
|
||||
const { result } = renderHook(() => useExplainModel(buildBaseProps()));
|
||||
|
||||
expect(result.current.availableSymbols).toEqual(['AAPL']);
|
||||
expect(result.current.visibleNews).toHaveLength(1);
|
||||
expect(result.current.visibleNews).toHaveLength(2);
|
||||
expect(result.current.visibleNewsByCategory).toHaveLength(1);
|
||||
expect(result.current.visibleNewsByCategory[0].id).toBe('news-1');
|
||||
expect(result.current.selectedRangeWindow).toEqual({
|
||||
@@ -127,18 +127,12 @@ describe('useExplainModel', () => {
|
||||
expect(result.current.selectedRangeExplain).toEqual({
|
||||
summary: '区间内主要由财报催化推动。'
|
||||
});
|
||||
expect(result.current.selectedSimilarDays?.items).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('builds timeline, counts, and chart markers from explain data', () => {
|
||||
const { result } = renderHook(() => useExplainModel(buildBaseProps()));
|
||||
|
||||
expect(result.current.availableEventDates).toContain('2026-03-10');
|
||||
expect(result.current.eventCategoryCounts.all).toBe(3);
|
||||
expect(result.current.eventCategoryCounts.technical).toBe(1);
|
||||
expect(result.current.eventCategoryCounts.discussion).toBe(1);
|
||||
expect(result.current.eventCategoryCounts.trade).toBe(1);
|
||||
expect(result.current.visibleExplainEvents).toHaveLength(3);
|
||||
expect(result.current.chartModel.markers.length).toBeGreaterThan(0);
|
||||
expect(result.current.chartModel.path).toMatch(/^M/);
|
||||
});
|
||||
|
||||
@@ -36,15 +36,6 @@ export const CDN_ASSETS = {
|
||||
"Groq": "https://img.alicdn.com/imgextra/i1/O1CN01WxASMc1QjXzhVl3eQ_!!6000000002012-2-tps-170-148.png",
|
||||
"Ollama": "https://img.alicdn.com/imgextra/i1/O1CN01pN615e1i4vxLkQjVd_!!6000000004360-2-tps-204-192.png",
|
||||
},
|
||||
stockLogos: {
|
||||
"TSLA": "https://img.alicdn.com/imgextra/i4/O1CN01Pch4DD1DDrad8BQAQ_!!6000000000183-2-tps-128-128.png",
|
||||
"AMZN": "https://img.alicdn.com/imgextra/i3/O1CN01KMsfnU25Wd4MGSgue_!!6000000007534-2-tps-128-128.png",
|
||||
"NVDA": "https://img.alicdn.com/imgextra/i4/O1CN01Lq1eJr1mLeslgx6a0_!!6000000004938-2-tps-128-128.png",
|
||||
"GOOGL": "https://img.alicdn.com/imgextra/i2/O1CN01kjJJbb25B6SESkOCn_!!6000000007487-2-tps-128-128.png",
|
||||
"MSFT": "https://img.alicdn.com/imgextra/i4/O1CN01tdlNtQ1aFS7vHYfMG_!!6000000003300-2-tps-128-128.png",
|
||||
"AAPL": "https://img.alicdn.com/imgextra/i4/O1CN01r0GH0q1diiHHOwxiO_!!6000000003770-2-tps-128-128.png",
|
||||
"META": "https://img.alicdn.com/imgextra/i3/O1CN01pWAvHt1IkRqZoUG96_!!6000000000931-2-tps-130-96.png",
|
||||
}
|
||||
};
|
||||
|
||||
// Derived asset shortcuts
|
||||
@@ -54,9 +45,6 @@ export const ASSETS = {
|
||||
remeLogo: CDN_ASSETS.companyRoom.reme_logo,
|
||||
};
|
||||
|
||||
// Stock logos mapping
|
||||
export const STOCK_LOGOS = { ...CDN_ASSETS.stockLogos };
|
||||
|
||||
// Scene dimensions (actual image size)
|
||||
export const SCENE_NATIVE = { width: 1248, height: 832 };
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ const resolveValue = (updater, currentValue) => (
|
||||
*/
|
||||
export const useUIStore = create((set) => ({
|
||||
// Current view
|
||||
currentView: 'traders', // 'traders' | 'room' | 'explain' | 'chart' | 'statistics' | 'openclaw' | 'runtime'
|
||||
currentView: 'traders', // 'traders' | 'room' | 'explain' | 'chart' | 'statistics' | 'runtime'
|
||||
setCurrentView: (currentView) => set((state) => ({ currentView: resolveValue(currentView, state.currentView) })),
|
||||
|
||||
// Chart tab
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Global CSS Styles for the EvoTraders Platform
|
||||
* Global CSS Styles for the 大时代 Platform
|
||||
* Terminal-inspired, minimal, monochrome design
|
||||
*/
|
||||
export default function GlobalStyles() {
|
||||
@@ -1098,10 +1098,6 @@ export default function GlobalStyles() {
|
||||
transform: translateX(-80%);
|
||||
}
|
||||
|
||||
.view-slider-five.show-openclaw {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.view-panel {
|
||||
flex: 0 0 33.333%;
|
||||
width: 33.333%;
|
||||
|
||||
@@ -5,12 +5,12 @@ build-backend = "setuptools.build_meta"
|
||||
[project]
|
||||
name = "evotraders"
|
||||
version = "0.1.0"
|
||||
description = "EvoTraders: A self-evolving multi-agent trading system"
|
||||
description = "大时代: A self-evolving multi-agent trading system"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.9"
|
||||
license = {text = "MIT"}
|
||||
authors = [
|
||||
{name = "EvoTraders Team <dengjiaji.djj@alibaba-inc.com>"}
|
||||
{name = "大时代 Team", email = "dengjiaji.djj@alibaba-inc.com"}
|
||||
]
|
||||
keywords = ["trading", "ai", "multi-agent", "fintech", "algorithmic-trading"]
|
||||
classifiers = [
|
||||
@@ -28,11 +28,16 @@ classifiers = [
|
||||
dependencies = [
|
||||
"agentscope>=1.0.8",
|
||||
"reme-ai>=0.2.0.4",
|
||||
"asyncio>=3.4.3",
|
||||
"fastapi>=0.115.0",
|
||||
"uvicorn>=0.30.0",
|
||||
"pydantic>=2.8.0",
|
||||
"rich>=13.6.0",
|
||||
"websockets>=12.0",
|
||||
"websocket-client>=1.6.0",
|
||||
"httpx>=0.27.0",
|
||||
"cryptography>=43.0.0",
|
||||
"python-dotenv>=1.0.0",
|
||||
"PyYAML>=6.0.0",
|
||||
"finnhub-python>=2.4.25",
|
||||
"numpy>=1.24.0",
|
||||
"pandas>=2.0.0",
|
||||
@@ -42,8 +47,6 @@ dependencies = [
|
||||
"typer>=0.12.5",
|
||||
"openai>=2.9.0",
|
||||
"anthropic>=0.20.0",
|
||||
"dotenv",
|
||||
"typer",
|
||||
]
|
||||
|
||||
|
||||
@@ -63,13 +66,8 @@ Documentation = "https://github.com/agentscope-ai/agentscope-samples/evotraders/
|
||||
[project.scripts]
|
||||
evotraders = "backend.cli:app"
|
||||
|
||||
[tool.setuptools]
|
||||
packages = ["backend", "backend.agents", "backend.config",
|
||||
"backend.apps",
|
||||
"backend.domains",
|
||||
"backend.data", "backend.llm",
|
||||
"backend.tools", "backend.utils", "backend.services",
|
||||
"backend.explain", "backend.enrich"]
|
||||
[tool.setuptools.packages.find]
|
||||
include = ["backend*", "shared*"]
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 88
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Policy
|
||||
|
||||
Optional run-scoped constraints, limits, or strategy policy.
|
||||
|
||||
State a clear signal, confidence, and the conditions that would invalidate the thesis.
|
||||
- 深化你的投资逻辑,确保每项建议都有清晰、可追溯、可重复的依据
|
||||
- 明确风险边界:在什么具体情况下当前结论会失效
|
||||
- 做逆向测试:说明市场主流共识与你的不同点
|
||||
- 每次分析后反思这次案例如何验证或挑战你现有的信念
|
||||
- 即使输入新闻或财报原文是英文,最终表达也必须用中文
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Policy
|
||||
|
||||
Optional run-scoped constraints, limits, or strategy policy.
|
||||
|
||||
Respect cash, margin, and portfolio concentration constraints before recording decisions.
|
||||
- 在决定数量时考虑可用现金,不要超出现金允许范围
|
||||
- 考虑做空头寸的保证金要求
|
||||
- 仓位规模相对于组合总资产保持保守
|
||||
- 始终为决策提供清晰理由
|
||||
- 不要输出英文投资报告或英文结论
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Policy
|
||||
|
||||
Optional run-scoped constraints, limits, or strategy policy.
|
||||
|
||||
Use available risk tools before issuing the final risk memo.
|
||||
- 先量化,再判断,不要只给抽象风险表述
|
||||
- 高严重度风险必须先说
|
||||
- 最终结论需要明确仓位限制或调整建议
|
||||
- 不要输出英文风险报告或英文摘要
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Policy
|
||||
|
||||
Optional run-scoped constraints, limits, or strategy policy.
|
||||
|
||||
State a clear signal, confidence, and the conditions that would invalidate the thesis.
|
||||
- 深化你的投资逻辑,确保每项建议都有清晰、可追溯、可重复的依据
|
||||
- 明确风险边界:在什么具体情况下当前结论会失效
|
||||
- 做逆向测试:说明市场主流共识与你的不同点
|
||||
- 每次分析后反思这次案例如何验证或挑战你现有的信念
|
||||
- 即使输入新闻或财报原文是英文,最终表达也必须用中文
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Policy
|
||||
|
||||
Optional run-scoped constraints, limits, or strategy policy.
|
||||
|
||||
State a clear signal, confidence, and the conditions that would invalidate the thesis.
|
||||
- 深化你的投资逻辑,确保每项建议都有清晰、可追溯、可重复的依据
|
||||
- 明确风险边界:在什么具体情况下当前结论会失效
|
||||
- 做逆向测试:说明市场主流共识与你的不同点
|
||||
- 每次分析后反思这次案例如何验证或挑战你现有的信念
|
||||
- 即使输入新闻或财报原文是英文,最终表达也必须用中文
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Policy
|
||||
|
||||
Optional run-scoped constraints, limits, or strategy policy.
|
||||
|
||||
State a clear signal, confidence, and the conditions that would invalidate the thesis.
|
||||
- 深化你的投资逻辑,确保每项建议都有清晰、可追溯、可重复的依据
|
||||
- 明确风险边界:在什么具体情况下当前结论会失效
|
||||
- 做逆向测试:说明市场主流共识与你的不同点
|
||||
- 每次分析后反思这次案例如何验证或挑战你现有的信念
|
||||
- 即使输入新闻或财报原文是英文,最终表达也必须用中文
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Policy
|
||||
|
||||
Optional run-scoped constraints, limits, or strategy policy.
|
||||
|
||||
State a clear signal, confidence, and the conditions that would invalidate the thesis.
|
||||
- 深化你的投资逻辑,确保每项建议都有清晰、可追溯、可重复的依据
|
||||
- 明确风险边界:在什么具体情况下当前结论会失效
|
||||
- 做逆向测试:说明市场主流共识与你的不同点
|
||||
- 每次分析后反思这次案例如何验证或挑战你现有的信念
|
||||
- 即使输入新闻或财报原文是英文,最终表达也必须用中文
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Policy
|
||||
|
||||
Optional run-scoped constraints, limits, or strategy policy.
|
||||
|
||||
Respect cash, margin, and portfolio concentration constraints before recording decisions.
|
||||
- 在决定数量时考虑可用现金,不要超出现金允许范围
|
||||
- 考虑做空头寸的保证金要求
|
||||
- 仓位规模相对于组合总资产保持保守
|
||||
- 始终为决策提供清晰理由
|
||||
- 不要输出英文投资报告或英文结论
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Policy
|
||||
|
||||
Optional run-scoped constraints, limits, or strategy policy.
|
||||
|
||||
Use available risk tools before issuing the final risk memo.
|
||||
- 先量化,再判断,不要只给抽象风险表述
|
||||
- 高严重度风险必须先说
|
||||
- 最终结论需要明确仓位限制或调整建议
|
||||
- 不要输出英文风险报告或英文摘要
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Policy
|
||||
|
||||
Optional run-scoped constraints, limits, or strategy policy.
|
||||
|
||||
State a clear signal, confidence, and the conditions that would invalidate the thesis.
|
||||
- 深化你的投资逻辑,确保每项建议都有清晰、可追溯、可重复的依据
|
||||
- 明确风险边界:在什么具体情况下当前结论会失效
|
||||
- 做逆向测试:说明市场主流共识与你的不同点
|
||||
- 每次分析后反思这次案例如何验证或挑战你现有的信念
|
||||
- 即使输入新闻或财报原文是英文,最终表达也必须用中文
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Policy
|
||||
|
||||
Optional run-scoped constraints, limits, or strategy policy.
|
||||
|
||||
State a clear signal, confidence, and the conditions that would invalidate the thesis.
|
||||
- 深化你的投资逻辑,确保每项建议都有清晰、可追溯、可重复的依据
|
||||
- 明确风险边界:在什么具体情况下当前结论会失效
|
||||
- 做逆向测试:说明市场主流共识与你的不同点
|
||||
- 每次分析后反思这次案例如何验证或挑战你现有的信念
|
||||
- 即使输入新闻或财报原文是英文,最终表达也必须用中文
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Policy
|
||||
|
||||
Optional run-scoped constraints, limits, or strategy policy.
|
||||
|
||||
State a clear signal, confidence, and the conditions that would invalidate the thesis.
|
||||
- 深化你的投资逻辑,确保每项建议都有清晰、可追溯、可重复的依据
|
||||
- 明确风险边界:在什么具体情况下当前结论会失效
|
||||
- 做逆向测试:说明市场主流共识与你的不同点
|
||||
- 每次分析后反思这次案例如何验证或挑战你现有的信念
|
||||
- 即使输入新闻或财报原文是英文,最终表达也必须用中文
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Policy
|
||||
|
||||
Optional run-scoped constraints, limits, or strategy policy.
|
||||
|
||||
State a clear signal, confidence, and the conditions that would invalidate the thesis.
|
||||
- 深化你的投资逻辑,确保每项建议都有清晰、可追溯、可重复的依据
|
||||
- 明确风险边界:在什么具体情况下当前结论会失效
|
||||
- 做逆向测试:说明市场主流共识与你的不同点
|
||||
- 每次分析后反思这次案例如何验证或挑战你现有的信念
|
||||
- 即使输入新闻或财报原文是英文,最终表达也必须用中文
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Policy
|
||||
|
||||
Optional run-scoped constraints, limits, or strategy policy.
|
||||
|
||||
Respect cash, margin, and portfolio concentration constraints before recording decisions.
|
||||
- 在决定数量时考虑可用现金,不要超出现金允许范围
|
||||
- 考虑做空头寸的保证金要求
|
||||
- 仓位规模相对于组合总资产保持保守
|
||||
- 始终为决策提供清晰理由
|
||||
- 不要输出英文投资报告或英文结论
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Policy
|
||||
|
||||
Optional run-scoped constraints, limits, or strategy policy.
|
||||
|
||||
Use available risk tools before issuing the final risk memo.
|
||||
- 先量化,再判断,不要只给抽象风险表述
|
||||
- 高严重度风险必须先说
|
||||
- 最终结论需要明确仓位限制或调整建议
|
||||
- 不要输出英文风险报告或英文摘要
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Policy
|
||||
|
||||
Optional run-scoped constraints, limits, or strategy policy.
|
||||
|
||||
State a clear signal, confidence, and the conditions that would invalidate the thesis.
|
||||
- 深化你的投资逻辑,确保每项建议都有清晰、可追溯、可重复的依据
|
||||
- 明确风险边界:在什么具体情况下当前结论会失效
|
||||
- 做逆向测试:说明市场主流共识与你的不同点
|
||||
- 每次分析后反思这次案例如何验证或挑战你现有的信念
|
||||
- 即使输入新闻或财报原文是英文,最终表达也必须用中文
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Policy
|
||||
|
||||
Optional run-scoped constraints, limits, or strategy policy.
|
||||
|
||||
State a clear signal, confidence, and the conditions that would invalidate the thesis.
|
||||
- 深化你的投资逻辑,确保每项建议都有清晰、可追溯、可重复的依据
|
||||
- 明确风险边界:在什么具体情况下当前结论会失效
|
||||
- 做逆向测试:说明市场主流共识与你的不同点
|
||||
- 每次分析后反思这次案例如何验证或挑战你现有的信念
|
||||
- 即使输入新闻或财报原文是英文,最终表达也必须用中文
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Policy
|
||||
|
||||
Optional run-scoped constraints, limits, or strategy policy.
|
||||
|
||||
State a clear signal, confidence, and the conditions that would invalidate the thesis.
|
||||
- 深化你的投资逻辑,确保每项建议都有清晰、可追溯、可重复的依据
|
||||
- 明确风险边界:在什么具体情况下当前结论会失效
|
||||
- 做逆向测试:说明市场主流共识与你的不同点
|
||||
- 每次分析后反思这次案例如何验证或挑战你现有的信念
|
||||
- 即使输入新闻或财报原文是英文,最终表达也必须用中文
|
||||
|
||||
148
scripts/check-prod-env.sh
Normal file
148
scripts/check-prod-env.sh
Normal file
@@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env bash
|
||||
# ============================================================
|
||||
# 大时代 生产环境检查脚本
|
||||
#
|
||||
# 用法:
|
||||
# ./scripts/check-prod-env.sh
|
||||
# ./scripts/check-prod-env.sh --strict
|
||||
#
|
||||
# 检查内容:
|
||||
# - Python / Node / npm 是否可用
|
||||
# - 后端关键 Python 模块是否已安装
|
||||
# - frontend/package-lock.json 与 npm ci 是否可消费
|
||||
# - .env 是否存在以及关键变量是否配置
|
||||
# - 前端是否可构建
|
||||
# ============================================================
|
||||
set -euo pipefail
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
STRICT=false
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--strict) STRICT=true ;;
|
||||
*) echo -e "${YELLOW}忽略未知参数: ${arg}${NC}" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
cd "${PROJECT_ROOT}"
|
||||
|
||||
WARNINGS=0
|
||||
|
||||
ok() {
|
||||
echo -e "${GREEN}✔${NC} $1"
|
||||
}
|
||||
|
||||
warn() {
|
||||
WARNINGS=$((WARNINGS + 1))
|
||||
echo -e "${YELLOW}⚠${NC} $1"
|
||||
}
|
||||
|
||||
fail() {
|
||||
echo -e "${RED}✘${NC} $1"
|
||||
exit 1
|
||||
}
|
||||
|
||||
require_cmd() {
|
||||
local cmd="$1"
|
||||
command -v "${cmd}" >/dev/null 2>&1 || fail "未找到命令: ${cmd}"
|
||||
}
|
||||
|
||||
check_python_modules() {
|
||||
python - <<'PY'
|
||||
mods = [
|
||||
'fastapi', 'uvicorn', 'yaml', 'httpx', 'cryptography', 'websockets',
|
||||
'rich', 'dotenv', 'pandas_market_calendars', 'finnhub', 'openai',
|
||||
'anthropic', 'agentscope', 'pydantic'
|
||||
]
|
||||
missing = []
|
||||
for m in mods:
|
||||
try:
|
||||
__import__(m)
|
||||
except Exception as exc:
|
||||
missing.append((m, f"{type(exc).__name__}: {exc}"))
|
||||
|
||||
if missing:
|
||||
for name, err in missing:
|
||||
print(f"MISSING {name} {err}")
|
||||
raise SystemExit(1)
|
||||
print("OK")
|
||||
PY
|
||||
}
|
||||
|
||||
check_env_file() {
|
||||
if [ ! -f .env ]; then
|
||||
if ${STRICT}; then
|
||||
fail "未找到 .env,生产环境请先基于 env.template 配置"
|
||||
fi
|
||||
warn "未找到 .env,生产部署前需要补齐"
|
||||
return
|
||||
fi
|
||||
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
source .env
|
||||
set +a
|
||||
|
||||
[ -n "${MODEL_NAME:-}" ] || warn "MODEL_NAME 未配置"
|
||||
[ -n "${OPENAI_API_KEY:-}" ] || warn "OPENAI_API_KEY 未配置"
|
||||
[ -n "${FINNHUB_API_KEY:-}" ] || warn "FINNHUB_API_KEY 未配置(live 模式必需)"
|
||||
ok ".env 已加载"
|
||||
}
|
||||
|
||||
check_frontend_install() {
|
||||
[ -f frontend/package-lock.json ] || fail "frontend/package-lock.json 缺失,生产部署建议保留锁文件"
|
||||
(
|
||||
cd frontend
|
||||
npm ci --dry-run >/tmp/bigtime-npm-ci.log 2>&1 || {
|
||||
cat /tmp/bigtime-npm-ci.log
|
||||
exit 1
|
||||
}
|
||||
)
|
||||
if rg -n "@emoji-mart/react|@lobehub/ui|ERESOLVE overriding peer dependency" /tmp/bigtime-npm-ci.log >/dev/null 2>&1; then
|
||||
warn "frontend npm ci 存在已知非阻塞 peer warning(@lobehub/icons 依赖链),可忽略"
|
||||
elif rg -n "npm warn" /tmp/bigtime-npm-ci.log >/dev/null 2>&1; then
|
||||
warn "frontend npm ci 存在 warning,建议查看 /tmp/bigtime-npm-ci.log"
|
||||
else
|
||||
ok "frontend npm ci --dry-run 通过"
|
||||
fi
|
||||
}
|
||||
|
||||
check_frontend_build() {
|
||||
(
|
||||
cd frontend
|
||||
npm run build >/tmp/bigtime-frontend-build.log 2>&1 || {
|
||||
cat /tmp/bigtime-frontend-build.log
|
||||
exit 1
|
||||
}
|
||||
)
|
||||
ok "frontend 构建通过"
|
||||
}
|
||||
|
||||
echo -e "${CYAN}大时代 · 生产环境检查${NC}"
|
||||
|
||||
require_cmd python
|
||||
require_cmd node
|
||||
require_cmd npm
|
||||
|
||||
ok "python: $(python -V 2>&1)"
|
||||
ok "node: $(node -v)"
|
||||
ok "npm: $(npm -v)"
|
||||
|
||||
check_python_modules && ok "后端关键 Python 模块已安装"
|
||||
check_env_file
|
||||
check_frontend_install
|
||||
check_frontend_build
|
||||
|
||||
if [ "${WARNINGS}" -gt 0 ]; then
|
||||
echo -e "${YELLOW}检查完成:有 ${WARNINGS} 项 warning${NC}"
|
||||
${STRICT} && exit 1 || exit 0
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}检查完成:环境可用于生产部署${NC}"
|
||||
@@ -1,4 +1,4 @@
|
||||
# EvoTraders Service Surfaces
|
||||
# 大时代 Service Surfaces
|
||||
|
||||
This repository is in a split-first state: local development now assumes
|
||||
separate app surfaces and a dedicated WebSocket gateway instead of a single
|
||||
@@ -14,7 +14,7 @@ combined backend entrypoint.
|
||||
| `backend.apps.runtime_service` | `8003` | Runtime lifecycle APIs under `/api/runtime/*` |
|
||||
| `backend.apps.openclaw_service` | `8004` | Read-only OpenClaw REST facade |
|
||||
| Gateway (`backend.main`) | `8765` | WebSocket feed, runtime event stream, legacy/compat orchestration path |
|
||||
| OpenClaw Gateway | `18789` | External OpenClaw WebSocket endpoint consumed by EvoTraders gateway |
|
||||
| OpenClaw Gateway | `18789` | External OpenClaw WebSocket endpoint consumed by 大时代 gateway |
|
||||
|
||||
## What Runs By Default In Dev
|
||||
|
||||
@@ -30,7 +30,7 @@ That script starts:
|
||||
- `trading_service` on `8001`
|
||||
- `news_service` on `8002`
|
||||
- `runtime_service` on `8003`
|
||||
- EvoTraders gateway on `8765`
|
||||
- 大时代 gateway on `8765`
|
||||
|
||||
It does **not** start `openclaw_service` on `8004`.
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
import time
|
||||
import uuid
|
||||
from dataclasses import dataclass, field
|
||||
@@ -245,11 +244,6 @@ class OpenClawWebSocketClient:
|
||||
# Build connect params
|
||||
connect_params = self._build_connect_params()
|
||||
|
||||
# Debug: log connect params
|
||||
import logging
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
logger.debug(f"Connect params: {connect_params}")
|
||||
|
||||
# Send connect request and wait for hello-ok
|
||||
hello_event = await self._send_request("connect", connect_params, _allow_handshake=True)
|
||||
self._hello = GatewayHello(
|
||||
@@ -346,11 +340,6 @@ class OpenClawWebSocketClient:
|
||||
"scopes": scopes.split(","),
|
||||
}
|
||||
|
||||
# Debug output
|
||||
print(f"DEBUG: nonce={self._nonce}", file=sys.stderr)
|
||||
print(f"DEBUG: auth_payload={auth_payload}", file=sys.stderr)
|
||||
print(f"DEBUG: connect params = {json.dumps(params, indent=2)}", file=sys.stderr)
|
||||
|
||||
return params
|
||||
|
||||
async def _recv_loop(self) -> None:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Shared schema package for EvoTraders services."""
|
||||
"""Shared schema package for 大时代 services."""
|
||||
|
||||
from shared.schema.price import Price, PriceResponse
|
||||
from shared.schema.financial import (
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
# EvoTraders Development Startup Script
|
||||
# 大时代 Development Startup Script
|
||||
# Split-service mode only
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
echo "=========================================="
|
||||
echo "EvoTraders Development Environment"
|
||||
echo "大时代 Development Environment"
|
||||
echo "=========================================="
|
||||
|
||||
# Colors for output
|
||||
@@ -180,7 +180,7 @@ export OPENCLAW_SERVICE_URL="${OPENCLAW_SERVICE_URL:-http://localhost:18789}"
|
||||
check_openclaw_gateway
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}Starting EvoTraders split services (default mode)...${NC}"
|
||||
echo -e "${GREEN}Starting 大时代 split services (default mode)...${NC}"
|
||||
echo " agent_service: http://localhost:8000"
|
||||
echo " runtime_service: http://localhost:8003"
|
||||
echo " openclaw_gateway: ws://localhost:18789"
|
||||
|
||||
320
start.sh
Normal file
320
start.sh
Normal file
@@ -0,0 +1,320 @@
|
||||
#!/usr/bin/env bash
|
||||
# ============================================================
|
||||
# 大时代 生产环境启动脚本
|
||||
#
|
||||
# 用法:
|
||||
# ./start.sh # 构建前端 + 后台启动全部服务 (默认)
|
||||
# ./start.sh --no-build # 跳过前端构建
|
||||
# ./start.sh --no-daemon # 前台运行 (不使用 nohup)
|
||||
# ./start.sh --gateway-only # 仅启动 Gateway (配合 nginx)
|
||||
# ./start.sh stop # 停止所有后台服务
|
||||
# ./start.sh status # 查看服务状态
|
||||
#
|
||||
# 环境变量:
|
||||
# WORKERS=2 # uvicorn worker 数 (默认: 2)
|
||||
# GATEWAY_HOST=0.0.0.0 # Gateway 绑定地址
|
||||
# GATEWAY_PORT=8765 # Gateway 端口
|
||||
# ============================================================
|
||||
set -euo pipefail
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "${SCRIPT_DIR}"
|
||||
|
||||
WORKERS="${WORKERS:-2}"
|
||||
GATEWAY_HOST="${GATEWAY_HOST:-0.0.0.0}"
|
||||
GATEWAY_PORT="${GATEWAY_PORT:-8765}"
|
||||
PID_DIR="${SCRIPT_DIR}/.pids"
|
||||
LOG_DIR="${SCRIPT_DIR}/logs"
|
||||
FRONTEND_DIST="${SCRIPT_DIR}/frontend/dist"
|
||||
|
||||
DAEMON=true
|
||||
BUILD_FRONTEND=true
|
||||
GATEWAY_ONLY=false
|
||||
ACTION="start"
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--no-daemon) DAEMON=false ;;
|
||||
--no-build) BUILD_FRONTEND=false ;;
|
||||
--gateway-only) GATEWAY_ONLY=true ;;
|
||||
stop) ACTION="stop" ;;
|
||||
status) ACTION="status" ;;
|
||||
*) echo -e "${YELLOW}忽略未知参数: ${arg}${NC}" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
ensure_dirs() {
|
||||
mkdir -p "${PID_DIR}" "${LOG_DIR}"
|
||||
}
|
||||
|
||||
save_pid() {
|
||||
local name="$1" pid="$2"
|
||||
echo "${pid}" > "${PID_DIR}/${name}.pid"
|
||||
}
|
||||
|
||||
read_pid() {
|
||||
local name="$1"
|
||||
local pidfile="${PID_DIR}/${name}.pid"
|
||||
if [ -f "${pidfile}" ]; then
|
||||
cat "${pidfile}"
|
||||
fi
|
||||
}
|
||||
|
||||
is_running() {
|
||||
local pid="$1"
|
||||
[ -n "${pid}" ] && kill -0 "${pid}" 2>/dev/null
|
||||
}
|
||||
|
||||
stop_service() {
|
||||
local name="$1"
|
||||
local pid
|
||||
pid="$(read_pid "${name}")"
|
||||
if is_running "${pid}"; then
|
||||
echo -e " ${YELLOW}停止${NC} ${name} (PID: ${pid})"
|
||||
kill "${pid}" 2>/dev/null || true
|
||||
local count=0
|
||||
while is_running "${pid}" && [ "${count}" -lt 20 ]; do
|
||||
sleep 0.5
|
||||
count=$((count + 1))
|
||||
done
|
||||
if is_running "${pid}"; then
|
||||
echo -e " ${RED}强制终止${NC} ${name}"
|
||||
kill -9 "${pid}" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
rm -f "${PID_DIR}/${name}.pid"
|
||||
}
|
||||
|
||||
print_status() {
|
||||
local name="$1" port="$2"
|
||||
local pid
|
||||
pid="$(read_pid "${name}")"
|
||||
if is_running "${pid}"; then
|
||||
echo -e " ${GREEN}●${NC} ${name} (PID: ${pid}, 端口: ${port})"
|
||||
else
|
||||
echo -e " ${RED}○${NC} ${name} (未运行)"
|
||||
fi
|
||||
}
|
||||
|
||||
load_env() {
|
||||
if [ -f .env ]; then
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
source .env
|
||||
set +a
|
||||
else
|
||||
echo -e "${YELLOW}警告: 未检测到 .env,将使用环境变量或默认值${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
check_prereqs() {
|
||||
if [ -z "${VIRTUAL_ENV:-}" ] && [ -f ".venv/bin/activate" ]; then
|
||||
# shellcheck disable=SC1091
|
||||
source .venv/bin/activate
|
||||
fi
|
||||
|
||||
command -v python >/dev/null 2>&1 || {
|
||||
echo -e "${RED}未找到 python${NC}"
|
||||
exit 1
|
||||
}
|
||||
command -v lsof >/dev/null 2>&1 || {
|
||||
echo -e "${RED}未找到 lsof${NC}"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
kill_port() {
|
||||
local port="$1"
|
||||
local pids
|
||||
pids="$(lsof -ti :"${port}" 2>/dev/null || true)"
|
||||
if [ -n "${pids}" ]; then
|
||||
echo -e "${YELLOW}端口 ${port} 已被占用,清理 PID:${NC} ${pids}"
|
||||
echo "${pids}" | xargs kill -9 2>/dev/null || true
|
||||
sleep 0.5
|
||||
fi
|
||||
}
|
||||
|
||||
do_stop() {
|
||||
echo -e "${CYAN}停止所有服务...${NC}"
|
||||
for svc in gateway agent_service trading_service news_service runtime_service openclaw_service; do
|
||||
stop_service "${svc}"
|
||||
done
|
||||
echo -e "${GREEN}已停止${NC}"
|
||||
}
|
||||
|
||||
do_status() {
|
||||
echo ""
|
||||
echo -e "${CYAN}服务状态${NC}"
|
||||
print_status "agent_service" 8000
|
||||
print_status "trading_service" 8001
|
||||
print_status "news_service" 8002
|
||||
print_status "runtime_service" 8003
|
||||
print_status "openclaw_service" 8004
|
||||
print_status "gateway" "${GATEWAY_PORT}"
|
||||
echo ""
|
||||
|
||||
if [ -d "${FRONTEND_DIST}" ]; then
|
||||
echo -e " ${GREEN}✔${NC} 前端已构建: ${FRONTEND_DIST}"
|
||||
else
|
||||
echo -e " ${YELLOW}⚠${NC} 前端未构建,运行: cd frontend && npm run build"
|
||||
fi
|
||||
echo ""
|
||||
}
|
||||
|
||||
build_frontend() {
|
||||
if ! ${BUILD_FRONTEND}; then
|
||||
return
|
||||
fi
|
||||
echo ""
|
||||
echo -e "${CYAN}构建前端...${NC}"
|
||||
if [ -d "frontend" ] && command -v npm >/dev/null 2>&1; then
|
||||
if [ -f "frontend/package-lock.json" ]; then
|
||||
(cd frontend && npm ci && npm run build)
|
||||
else
|
||||
(cd frontend && npm install && npm run build)
|
||||
fi
|
||||
echo -e "${GREEN}前端构建完成: ${FRONTEND_DIST}${NC}"
|
||||
else
|
||||
echo -e "${RED}前端构建失败: 需要 npm 和 frontend 目录${NC}"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
start_single_daemon() {
|
||||
local name="$1" app_path="$2" port="$3"
|
||||
echo -e " ${GREEN}▶${NC} ${name} → :${port} (${WORKERS} workers)"
|
||||
nohup env SERVICE_NAME="${name}" python -m uvicorn "${app_path}" \
|
||||
--host 0.0.0.0 \
|
||||
--port "${port}" \
|
||||
--workers "${WORKERS}" \
|
||||
--log-level warning \
|
||||
--no-access-log \
|
||||
>> "${LOG_DIR}/${name}.log" 2>&1 &
|
||||
save_pid "${name}" $!
|
||||
}
|
||||
|
||||
start_daemon() {
|
||||
if ! ${GATEWAY_ONLY}; then
|
||||
start_single_daemon "agent_service" "backend.apps.agent_service:app" 8000
|
||||
start_single_daemon "trading_service" "backend.apps.trading_service:app" 8001
|
||||
start_single_daemon "news_service" "backend.apps.news_service:app" 8002
|
||||
start_single_daemon "runtime_service" "backend.apps.runtime_service:app" 8003
|
||||
start_single_daemon "openclaw_service" "backend.apps.openclaw_service:app" 8004
|
||||
fi
|
||||
|
||||
echo -e " ${GREEN}▶${NC} gateway → ws://${GATEWAY_HOST}:${GATEWAY_PORT}"
|
||||
nohup env SERVICE_NAME="gateway" python -m backend.main \
|
||||
--mode live \
|
||||
--host "${GATEWAY_HOST}" \
|
||||
--port "${GATEWAY_PORT}" \
|
||||
>> "${LOG_DIR}/gateway.log" 2>&1 &
|
||||
save_pid "gateway" $!
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}所有服务已在后台启动${NC}"
|
||||
echo " 日志目录: ${LOG_DIR}/"
|
||||
echo " PID 目录: ${PID_DIR}/"
|
||||
echo ""
|
||||
echo " 查看状态: ./start.sh status"
|
||||
echo " 查看日志: tail -f ${LOG_DIR}/gateway.log"
|
||||
echo " 停止服务: ./start.sh stop"
|
||||
echo ""
|
||||
}
|
||||
|
||||
PIDS=()
|
||||
|
||||
cleanup_foreground() {
|
||||
echo ""
|
||||
echo -e "${YELLOW}正在停止所有服务...${NC}"
|
||||
if [ "${#PIDS[@]}" -gt 0 ]; then
|
||||
kill "${PIDS[@]}" 2>/dev/null || true
|
||||
wait "${PIDS[@]}" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
start_single_foreground() {
|
||||
local name="$1" app_path="$2" port="$3"
|
||||
echo -e " ${GREEN}▶${NC} ${name} → :${port}"
|
||||
env SERVICE_NAME="${name}" python -m uvicorn "${app_path}" \
|
||||
--host 0.0.0.0 \
|
||||
--port "${port}" \
|
||||
--log-level warning \
|
||||
--no-access-log &
|
||||
PIDS+=($!)
|
||||
}
|
||||
|
||||
start_foreground() {
|
||||
trap cleanup_foreground EXIT INT TERM
|
||||
|
||||
if ! ${GATEWAY_ONLY}; then
|
||||
start_single_foreground "agent_service" "backend.apps.agent_service:app" 8000
|
||||
start_single_foreground "trading_service" "backend.apps.trading_service:app" 8001
|
||||
start_single_foreground "news_service" "backend.apps.news_service:app" 8002
|
||||
start_single_foreground "runtime_service" "backend.apps.runtime_service:app" 8003
|
||||
start_single_foreground "openclaw_service" "backend.apps.openclaw_service:app" 8004
|
||||
fi
|
||||
|
||||
echo -e " ${GREEN}▶${NC} gateway → ws://${GATEWAY_HOST}:${GATEWAY_PORT}"
|
||||
env SERVICE_NAME="gateway" python -m backend.main \
|
||||
--mode live \
|
||||
--host "${GATEWAY_HOST}" \
|
||||
--port "${GATEWAY_PORT}" &
|
||||
PIDS+=($!)
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}服务以前台模式运行。按 Ctrl+C 停止。${NC}"
|
||||
wait
|
||||
}
|
||||
|
||||
do_start() {
|
||||
ensure_dirs
|
||||
check_prereqs
|
||||
load_env
|
||||
|
||||
export TRADING_SERVICE_URL="${TRADING_SERVICE_URL:-http://localhost:8001}"
|
||||
export NEWS_SERVICE_URL="${NEWS_SERVICE_URL:-http://localhost:8002}"
|
||||
export RUNTIME_SERVICE_URL="${RUNTIME_SERVICE_URL:-http://localhost:8003}"
|
||||
export OPENCLAW_SERVICE_URL="${OPENCLAW_SERVICE_URL:-http://localhost:8004}"
|
||||
|
||||
build_frontend
|
||||
|
||||
echo ""
|
||||
echo -e "${CYAN}停止已有服务...${NC}"
|
||||
for svc in gateway agent_service trading_service news_service runtime_service openclaw_service; do
|
||||
stop_service "${svc}"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo -e "${CYAN}══════════════════════════════════════════${NC}"
|
||||
echo -e "${CYAN} 大时代 · 生产环境启动${NC}"
|
||||
echo -e "${CYAN}══════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
|
||||
if ${DAEMON}; then
|
||||
start_daemon
|
||||
else
|
||||
start_foreground
|
||||
fi
|
||||
}
|
||||
|
||||
case "${ACTION}" in
|
||||
start)
|
||||
do_start
|
||||
;;
|
||||
stop)
|
||||
do_stop
|
||||
;;
|
||||
status)
|
||||
do_status
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}未知动作: ${ACTION}${NC}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Reference in New Issue
Block a user