Align branding, prompts, and deployment tooling
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
# Keep `.env` untracked and never paste real secrets into tracked files.
|
# Keep `.env` untracked and never paste real secrets into tracked files.
|
||||||
|
|
||||||
# ================== General Configuration | 通用配置 ==================
|
# ================== 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
|
# Financial Data API
|
||||||
# At least `FINANCIAL_DATASETS_API_KEY` is required when using `FIN_DATA_SOURCE=financial_datasets`.
|
# 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">
|
<p align="center">
|
||||||
<img src="./docs/assets/evotraders_logo.jpg" width="45%">
|
<img src="./docs/assets/bigtime_logo.jpg" width="45%">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2 align="center">EvoTraders: A Self-Evolving Multi-Agent Trading System</h2>
|
<h2 align="center">大时代:自进化多智能体交易系统</h2>
|
||||||
|
|
||||||
<p align="center">
|
<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>
|
</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:
|
# clone this repository, then:
|
||||||
cd evotraders
|
cd evotraders
|
||||||
|
|
||||||
# recommended
|
# backend runtime dependencies
|
||||||
|
uv pip install -r requirements.txt
|
||||||
|
|
||||||
|
# install package entrypoint in editable mode
|
||||||
uv pip install -e .
|
uv pip install -e .
|
||||||
|
|
||||||
# optional
|
# optional
|
||||||
@@ -72,6 +77,16 @@ uv pip install -e .
|
|||||||
# 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
|
### 2. Configure environment
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -107,6 +122,12 @@ Notes:
|
|||||||
- `POLYGON_API_KEY` enables long-lived market-store ingestion and refresh helpers.
|
- `POLYGON_API_KEY` enables long-lived market-store ingestion and refresh helpers.
|
||||||
- `MEMORY_API_KEY` is only required when long-term memory is enabled.
|
- `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
|
### 3. Start the stack
|
||||||
|
|
||||||
Recommended local development flow:
|
Recommended local development flow:
|
||||||
@@ -335,6 +356,6 @@ npm test
|
|||||||
|
|
||||||
## License and Disclaimer
|
## 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.
|
**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">
|
<p align="center">
|
||||||
<img src="./docs/assets/evotraders_logo.jpg" width="45%">
|
<img src="./docs/assets/bigtime_logo.jpg" width="45%">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2 align="center">EvoTraders:自我进化的多智能体交易系统</h2>
|
<h2 align="center">大时代:自进化多智能体交易系统</h2>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
📌 <a href="http://trading.evoagents.cn">访问 EvoTraders 官网</a>
|
📌 <a href="http://trading.evoagents.cn">访问大时代官网</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
EvoTraders 是一个开源的金融交易智能体框架,结合多智能体协作、run 级工作区和记忆机制,支持回测与实盘两类交易运行模式。
|
大时代 是一个开源的金融交易智能体框架,结合多智能体协作、run 级工作区和记忆机制,支持回测与实盘两类交易运行模式。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -64,7 +64,10 @@ EvoTraders 是一个开源的金融交易智能体框架,结合多智能体协
|
|||||||
# 克隆仓库后进入项目目录
|
# 克隆仓库后进入项目目录
|
||||||
cd evotraders
|
cd evotraders
|
||||||
|
|
||||||
# 推荐
|
# 安装后端运行时依赖
|
||||||
|
uv pip install -r requirements.txt
|
||||||
|
|
||||||
|
# 安装项目入口(可编辑模式)
|
||||||
uv pip install -e .
|
uv pip install -e .
|
||||||
|
|
||||||
# 可选
|
# 可选
|
||||||
@@ -72,6 +75,16 @@ uv pip install -e .
|
|||||||
# pip install -e .
|
# pip install -e .
|
||||||
```
|
```
|
||||||
|
|
||||||
|
前端依赖:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm ci
|
||||||
|
cd ..
|
||||||
|
```
|
||||||
|
|
||||||
|
生产环境部署建议后端使用 `requirements.txt`,前端使用 `npm ci`,这样拉起的环境会严格跟随仓库中锁定的依赖版本。
|
||||||
|
|
||||||
### 2. 配置环境变量
|
### 2. 配置环境变量
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -107,6 +120,12 @@ MEMORY_API_KEY=
|
|||||||
- `POLYGON_API_KEY` 用于长期 market store 的补数和刷新
|
- `POLYGON_API_KEY` 用于长期 market store 的补数和刷新
|
||||||
- `MEMORY_API_KEY` 仅在启用长期记忆时需要
|
- `MEMORY_API_KEY` 仅在启用长期记忆时需要
|
||||||
|
|
||||||
|
如果要用更接近生产的本地启动方式,也可以直接执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./start.sh
|
||||||
|
```
|
||||||
|
|
||||||
### 3. 启动服务栈
|
### 3. 启动服务栈
|
||||||
|
|
||||||
本地开发推荐直接使用:
|
本地开发推荐直接使用:
|
||||||
@@ -335,6 +354,6 @@ npm test
|
|||||||
|
|
||||||
## 许可与免责
|
## 许可与免责
|
||||||
|
|
||||||
EvoTraders 是研究和教育用途项目。再次分发或商用前,请先核对仓库中的实际 license 文件。
|
大时代 是研究和教育用途项目。再次分发或商用前,请先核对仓库中的实际 license 文件。
|
||||||
|
|
||||||
**风险提示**:本项目不构成投资建议。任何实盘部署前都应进行充分测试和风险评估,历史表现不代表未来收益。
|
**风险提示**:本项目不构成投资建议。任何实盘部署前都应进行充分测试和风险评估,历史表现不代表未来收益。
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ Exports:
|
|||||||
|
|
||||||
# New EvoAgent architecture (from agent_core.py)
|
# New EvoAgent architecture (from agent_core.py)
|
||||||
from .agent_core import EvoAgent, ToolGuardMixin, CommandHandler
|
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 import WorkspaceManager, WorkspaceRegistry, WorkspaceConfig
|
||||||
from .workspace_manager import RunWorkspaceManager
|
from .workspace_manager import RunWorkspaceManager
|
||||||
from .registry import AgentRegistry, AgentInfo, get_registry, reset_registry
|
from .registry import AgentRegistry, AgentInfo, get_registry, reset_registry
|
||||||
@@ -36,7 +36,6 @@ __all__ = [
|
|||||||
"CommandHandler",
|
"CommandHandler",
|
||||||
"AgentFactory",
|
"AgentFactory",
|
||||||
"ModelConfig",
|
"ModelConfig",
|
||||||
"RoleConfig",
|
|
||||||
"WorkspaceManager",
|
"WorkspaceManager",
|
||||||
"WorkspaceRegistry",
|
"WorkspaceRegistry",
|
||||||
"WorkspaceConfig",
|
"WorkspaceConfig",
|
||||||
|
|||||||
@@ -84,7 +84,6 @@ class AnalystAgent(ReActAgent):
|
|||||||
agent_id=self.agent_id,
|
agent_id=self.agent_id,
|
||||||
config_name=self.config.get("config_name", "default"),
|
config_name=self.config.get("config_name", "default"),
|
||||||
toolkit=self.toolkit,
|
toolkit=self.toolkit,
|
||||||
analyst_type=self.analyst_type_key,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def reply(self, x: Msg = None) -> Msg:
|
async def reply(self, x: Msg = None) -> Msg:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""Base agent module for EvoTraders.
|
"""Base agent module for 大时代.
|
||||||
|
|
||||||
提供Agent基础类、命令处理、工具守卫和钩子管理等功能。
|
提供Agent基础类、命令处理、工具守卫和钩子管理等功能。
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- 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,
|
This module provides the main EvoAgent class built on AgentScope's ReActAgent,
|
||||||
with integrated tools, skills, and memory management based on CoPaw design.
|
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)
|
# Files to monitor (same as PromptBuilder.DEFAULT_FILES)
|
||||||
WATCHED_FILES = frozenset([
|
WATCHED_FILES = frozenset([
|
||||||
"SOUL.md", "AGENTS.md", "PROFILE.md", "ROLE.md",
|
"SOUL.md", "AGENTS.md", "PROFILE.md",
|
||||||
"POLICY.md", "MEMORY.md", "HEARTBEAT.md", "STYLE.md",
|
"POLICY.md", "MEMORY.md",
|
||||||
"BOOTSTRAP.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__ = [
|
__all__ = [
|
||||||
"Hook",
|
"Hook",
|
||||||
"HookManager",
|
"HookManager",
|
||||||
@@ -696,7 +608,6 @@ __all__ = [
|
|||||||
"HOOK_PRE_REASONING",
|
"HOOK_PRE_REASONING",
|
||||||
"HOOK_POST_ACTING",
|
"HOOK_POST_ACTING",
|
||||||
"BootstrapHook",
|
"BootstrapHook",
|
||||||
"HeartbeatHook",
|
|
||||||
"MemoryCompactionHook",
|
"MemoryCompactionHook",
|
||||||
"WorkspaceWatchHook",
|
"WorkspaceWatchHook",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -21,22 +21,6 @@ class ModelConfig:
|
|||||||
max_tokens: int = 4096
|
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:
|
class AgentConfig:
|
||||||
"""Represents a configured agent instance (data class)."""
|
"""Represents a configured agent instance (data class)."""
|
||||||
|
|
||||||
@@ -47,14 +31,12 @@ class AgentConfig:
|
|||||||
workspace_id: str,
|
workspace_id: str,
|
||||||
config_path: Path,
|
config_path: Path,
|
||||||
model_config: Optional[ModelConfig] = None,
|
model_config: Optional[ModelConfig] = None,
|
||||||
role_config: Optional[RoleConfig] = None,
|
|
||||||
):
|
):
|
||||||
self.agent_id = agent_id
|
self.agent_id = agent_id
|
||||||
self.agent_type = agent_type
|
self.agent_type = agent_type
|
||||||
self.workspace_id = workspace_id
|
self.workspace_id = workspace_id
|
||||||
self.config_path = config_path
|
self.config_path = config_path
|
||||||
self.model_config = model_config or ModelConfig()
|
self.model_config = model_config or ModelConfig()
|
||||||
self.role_config = role_config
|
|
||||||
self.agent_dir = config_path.parent
|
self.agent_dir = config_path.parent
|
||||||
|
|
||||||
def to_dict(self) -> Dict[str, Any]:
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
@@ -70,103 +52,12 @@ class AgentConfig:
|
|||||||
"temperature": self.model_config.temperature,
|
"temperature": self.model_config.temperature,
|
||||||
"max_tokens": self.model_config.max_tokens,
|
"max_tokens": self.model_config.max_tokens,
|
||||||
},
|
},
|
||||||
"role_config": self.role_config.__dict__ if self.role_config else None,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class AgentFactory:
|
class AgentFactory:
|
||||||
"""Factory for creating, cloning, and managing agents."""
|
"""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):
|
def __init__(self, project_root: Optional[Path] = None):
|
||||||
"""Initialize the agent factory.
|
"""Initialize the agent factory.
|
||||||
|
|
||||||
@@ -183,7 +74,6 @@ class AgentFactory:
|
|||||||
agent_type: str,
|
agent_type: str,
|
||||||
workspace_id: str,
|
workspace_id: str,
|
||||||
model_config: Optional[ModelConfig] = None,
|
model_config: Optional[ModelConfig] = None,
|
||||||
role_config: Optional[RoleConfig] = None,
|
|
||||||
clone_from: Optional[str] = None,
|
clone_from: Optional[str] = None,
|
||||||
) -> AgentConfig:
|
) -> AgentConfig:
|
||||||
"""Create a new agent.
|
"""Create a new agent.
|
||||||
@@ -193,7 +83,6 @@ class AgentFactory:
|
|||||||
agent_type: Type of agent (e.g., "technical_analyst")
|
agent_type: Type of agent (e.g., "technical_analyst")
|
||||||
workspace_id: ID of the workspace to create agent in
|
workspace_id: ID of the workspace to create agent in
|
||||||
model_config: Model configuration
|
model_config: Model configuration
|
||||||
role_config: Role configuration (auto-generated if None)
|
|
||||||
clone_from: Path to existing agent to clone from (optional)
|
clone_from: Path to existing agent to clone from (optional)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -223,13 +112,6 @@ class AgentFactory:
|
|||||||
else:
|
else:
|
||||||
self._copy_template(agent_dir, agent_id, agent_type)
|
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
|
# Write agent.yaml
|
||||||
config_path = agent_dir / "agent.yaml"
|
config_path = agent_dir / "agent.yaml"
|
||||||
self._write_agent_yaml(config_path, agent_id, agent_type, model_config)
|
self._write_agent_yaml(config_path, agent_id, agent_type, model_config)
|
||||||
@@ -240,7 +122,6 @@ class AgentFactory:
|
|||||||
workspace_id=workspace_id,
|
workspace_id=workspace_id,
|
||||||
config_path=config_path,
|
config_path=config_path,
|
||||||
model_config=model_config,
|
model_config=model_config,
|
||||||
role_config=role_config,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def delete_agent(self, agent_id: str, workspace_id: str) -> bool:
|
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",
|
"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",
|
"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",
|
"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",
|
"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():
|
for filename, content in default_files.items():
|
||||||
@@ -411,50 +290,6 @@ class AgentFactory:
|
|||||||
if skill_file.is_file():
|
if skill_file.is_file():
|
||||||
shutil.copy2(skill_file, target_skills / skill_file.name)
|
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(
|
def _write_agent_yaml(
|
||||||
self,
|
self,
|
||||||
config_path: Path,
|
config_path: Path,
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- 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 pathlib import Path
|
||||||
from typing import Any, Optional
|
from typing import Any
|
||||||
|
|
||||||
from .agent_workspace import load_agent_workspace_config
|
from .agent_workspace import load_agent_workspace_config
|
||||||
from backend.config.bootstrap_config import get_bootstrap_config_for_run
|
from backend.config.bootstrap_config import get_bootstrap_config_for_run
|
||||||
from .prompt_loader import get_prompt_loader
|
|
||||||
from .skills_manager import SkillsManager
|
from .skills_manager import SkillsManager
|
||||||
|
from .workspace_manager import RunWorkspaceManager
|
||||||
_prompt_loader = get_prompt_loader()
|
|
||||||
|
|
||||||
|
|
||||||
def _read_file_if_exists(path: Path) -> str:
|
def _read_file_if_exists(path: Path) -> str:
|
||||||
@@ -48,71 +46,20 @@ def build_agent_system_prompt(
|
|||||||
agent_id: str,
|
agent_id: str,
|
||||||
config_name: str,
|
config_name: str,
|
||||||
toolkit: Any,
|
toolkit: Any,
|
||||||
analyst_type: Optional[str] = None,
|
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Build the final system prompt for an agent.
|
"""Build the final system prompt for an agent.
|
||||||
|
|
||||||
Always reads fresh from disk — no caching.
|
Always reads fresh from disk — no caching.
|
||||||
"""
|
"""
|
||||||
# Clear any cached templates before building (CoPaw-style, no caching)
|
|
||||||
_prompt_loader.clear_cache()
|
|
||||||
|
|
||||||
sections: list[str] = []
|
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()
|
skills_manager = SkillsManager()
|
||||||
asset_dir = skills_manager.get_agent_asset_dir(config_name, agent_id)
|
asset_dir = skills_manager.get_agent_asset_dir(config_name, agent_id)
|
||||||
asset_dir.mkdir(parents=True, exist_ok=True)
|
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")
|
agent_config = load_agent_workspace_config(asset_dir / "agent.yaml")
|
||||||
bootstrap_config = get_bootstrap_config_for_run(
|
bootstrap_config = get_bootstrap_config_for_run(
|
||||||
skills_manager.project_root,
|
skills_manager.project_root,
|
||||||
@@ -139,9 +86,6 @@ def build_agent_system_prompt(
|
|||||||
"AGENTS.md": "Agent Guide",
|
"AGENTS.md": "Agent Guide",
|
||||||
"POLICY.md": "Policy",
|
"POLICY.md": "Policy",
|
||||||
"MEMORY.md": "Memory",
|
"MEMORY.md": "Memory",
|
||||||
"HEARTBEAT.md": "Heartbeat",
|
|
||||||
"ROLE.md": "Role",
|
|
||||||
"STYLE.md": "Style",
|
|
||||||
}
|
}
|
||||||
for filename in prompt_files:
|
for filename in prompt_files:
|
||||||
_append_section(
|
_append_section(
|
||||||
@@ -150,18 +94,6 @@ def build_agent_system_prompt(
|
|||||||
_read_file_if_exists(asset_dir / filename),
|
_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:
|
if "POLICY.md" not in included_files:
|
||||||
_append_section(
|
_append_section(
|
||||||
sections,
|
sections,
|
||||||
@@ -189,5 +121,4 @@ def build_agent_system_prompt(
|
|||||||
|
|
||||||
|
|
||||||
def clear_prompt_factory_cache() -> None:
|
def clear_prompt_factory_cache() -> None:
|
||||||
"""Clear cached prompt and YAML templates before hot reload."""
|
"""No-op retained for compatibility with runtime reload hooks."""
|
||||||
_prompt_loader.clear_cache()
|
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
你是一位专业的{{ analyst_type }}。
|
|
||||||
|
|
||||||
你的关注重点:
|
|
||||||
{{ focus }}
|
|
||||||
|
|
||||||
你的角色:
|
|
||||||
{{ description }}
|
|
||||||
|
|
||||||
注意:
|
|
||||||
- 构建并持续完善你的"投资哲学"。你的分析不应是孤立的事件,而应该是你整体投资世界观和核心信念的体现。每次分析后,你必须反思:
|
|
||||||
- 这个案例/数据如何验证或挑战了你现有的信念?
|
|
||||||
- 你从这次错误(或成功)中学到了关于市场、人性、估值或风险管理的什么关键原则?
|
|
||||||
- 深化你的"投资逻辑"。确保每一项投资建议都有清晰、可追溯、可重复的逻辑支撑。你的分析步骤应该像严谨的证明一样,涵盖:
|
|
||||||
- 核心驱动因素识别:真正影响价值的变量是什么?
|
|
||||||
- 风险边界设定:在什么具体情况下你的建议会失效?
|
|
||||||
- 逆向测试:市场主流共识是什么,你的观点有何不同?
|
|
||||||
保持谦逊和开放。投资大师的核心特质是持续学习和适应。在每次分析中,你必须积极寻找与自己观点相悖的证据和论据,并将其纳入最终评估。
|
|
||||||
- 你可以使用分析工具。用它们来收集相关数据并做出明智的建议。
|
|
||||||
|
|
||||||
输出指南:
|
|
||||||
- 给出明确的投资信号:看涨、看跌或中性
|
|
||||||
- 包含置信度(0-100)
|
|
||||||
- 为你的分析提供理由(如果你确定要分享最终分析,请先给出结论)
|
|
||||||
@@ -28,22 +28,16 @@ class PromptBuilder:
|
|||||||
"AGENTS.md",
|
"AGENTS.md",
|
||||||
"SOUL.md",
|
"SOUL.md",
|
||||||
"PROFILE.md",
|
"PROFILE.md",
|
||||||
"ROLE.md",
|
|
||||||
"POLICY.md",
|
"POLICY.md",
|
||||||
"MEMORY.md",
|
"MEMORY.md",
|
||||||
"HEARTBEAT.md",
|
|
||||||
"STYLE.md",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
TITLE_MAP: Dict[str, str] = {
|
TITLE_MAP: Dict[str, str] = {
|
||||||
"AGENTS.md": "Agent Guide",
|
"AGENTS.md": "Agent Guide",
|
||||||
"SOUL.md": "Soul",
|
"SOUL.md": "Soul",
|
||||||
"PROFILE.md": "Profile",
|
"PROFILE.md": "Profile",
|
||||||
"ROLE.md": "Role",
|
|
||||||
"POLICY.md": "Policy",
|
"POLICY.md": "Policy",
|
||||||
"MEMORY.md": "Memory",
|
"MEMORY.md": "Memory",
|
||||||
"HEARTBEAT.md": "Heartbeat",
|
|
||||||
"STYLE.md": "Style",
|
|
||||||
"BOOTSTRAP.md": "Bootstrap",
|
"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"
|
"tickers:\n"
|
||||||
" - AAPL\n"
|
" - AAPL\n"
|
||||||
" - MSFT\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"
|
"initial_cash: 100000\n"
|
||||||
"margin_requirement: 0.0\n"
|
"margin_requirement: 0.0\n"
|
||||||
"enable_memory: false\n"
|
"enable_memory: false\n"
|
||||||
@@ -63,9 +73,8 @@ class RunWorkspaceManager:
|
|||||||
self,
|
self,
|
||||||
config_name: str,
|
config_name: str,
|
||||||
agent_id: str,
|
agent_id: str,
|
||||||
role_seed: str = "",
|
file_contents: Optional[Dict[str, str]] = None,
|
||||||
style_seed: str = "",
|
persona: Optional[Dict[str, object]] = None,
|
||||||
policy_seed: str = "",
|
|
||||||
) -> Path:
|
) -> Path:
|
||||||
asset_dir = self.skills_manager.get_agent_asset_dir(
|
asset_dir = self.skills_manager.get_agent_asset_dir(
|
||||||
config_name,
|
config_name,
|
||||||
@@ -77,58 +86,55 @@ class RunWorkspaceManager:
|
|||||||
(asset_dir / "skills" / "disabled").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)
|
(asset_dir / "skills" / "local").mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
self._ensure_file(
|
file_contents = file_contents or self.build_default_agent_files(agent_id=agent_id)
|
||||||
asset_dir / "ROLE.md",
|
for filename, content in file_contents.items():
|
||||||
"# Role\n\n"
|
legacy_contents = self.build_legacy_agent_file_variants(
|
||||||
"Optional run-scoped role override.\n\n"
|
agent_id=agent_id,
|
||||||
f"{role_seed}".strip()
|
filename=filename,
|
||||||
+ "\n",
|
persona=persona,
|
||||||
)
|
|
||||||
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",
|
|
||||||
)
|
)
|
||||||
|
self._ensure_file(asset_dir / filename, content, legacy_contents=legacy_contents)
|
||||||
self._ensure_agent_yaml(
|
self._ensure_agent_yaml(
|
||||||
asset_dir / "agent.yaml",
|
asset_dir / "agent.yaml",
|
||||||
agent_id=agent_id,
|
agent_id=agent_id,
|
||||||
)
|
)
|
||||||
return asset_dir
|
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(
|
def load_agent_file(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -168,49 +174,285 @@ class RunWorkspaceManager:
|
|||||||
for agent_id in agent_ids:
|
for agent_id in agent_ids:
|
||||||
if agent_id.endswith("_analyst"):
|
if agent_id.endswith("_analyst"):
|
||||||
persona = analyst_personas.get(agent_id, {})
|
persona = analyst_personas.get(agent_id, {})
|
||||||
role_seed = persona.get("description", "").strip()
|
file_contents = self.build_default_agent_files(
|
||||||
focus_items = persona.get("focus", [])
|
agent_id=agent_id,
|
||||||
style_seed = "\n".join(f"- {item}" for item in focus_items)
|
persona=persona,
|
||||||
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."
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
role_seed = ""
|
persona = None
|
||||||
style_seed = ""
|
file_contents = self.build_default_agent_files(agent_id=agent_id)
|
||||||
policy_seed = ""
|
asset_dir = self.skills_manager.get_agent_asset_dir(config_name, agent_id)
|
||||||
|
asset_dir.mkdir(parents=True, exist_ok=True)
|
||||||
self.ensure_agent_assets(
|
(asset_dir / "skills" / "installed").mkdir(parents=True, exist_ok=True)
|
||||||
config_name=config_name,
|
(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,
|
agent_id=agent_id,
|
||||||
role_seed=role_seed,
|
filename=filename,
|
||||||
style_seed=style_seed,
|
persona=persona,
|
||||||
policy_seed=policy_seed,
|
),
|
||||||
)
|
)
|
||||||
|
self._ensure_agent_yaml(asset_dir / "agent.yaml", agent_id=agent_id)
|
||||||
|
|
||||||
@staticmethod
|
@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():
|
if not path.exists():
|
||||||
path.write_text(content, encoding="utf-8")
|
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
|
@staticmethod
|
||||||
def _ensure_agent_yaml(path: Path, agent_id: str) -> None:
|
def _ensure_agent_yaml(path: Path, agent_id: str) -> None:
|
||||||
|
|||||||
@@ -661,7 +661,7 @@ async def get_agent_file(
|
|||||||
Args:
|
Args:
|
||||||
workspace_id: Workspace identifier
|
workspace_id: Workspace identifier
|
||||||
agent_id: Agent 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:
|
Returns:
|
||||||
File content
|
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)
|
agent_factory.workspaces_root.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
registry = get_registry()
|
registry = get_registry()
|
||||||
print("✓ EvoTraders API started")
|
print("✓ 大时代 API started")
|
||||||
print(f" - Workspaces root: {agent_factory.workspaces_root}")
|
print(f" - Workspaces root: {agent_factory.workspaces_root}")
|
||||||
print(f" - Registered agents: {registry.get_agent_count()}")
|
print(f" - Registered agents: {registry.get_agent_count()}")
|
||||||
|
|
||||||
yield
|
yield
|
||||||
|
|
||||||
print("✓ EvoTraders API shutting down")
|
print("✓ 大时代 API shutting down")
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="EvoTraders Agent Service",
|
title="大时代 Agent Service",
|
||||||
description="REST API for the EvoTraders multi-agent control plane",
|
description="REST API for the 大时代 multi-agent control plane",
|
||||||
version="0.1.0",
|
version="0.1.0",
|
||||||
lifespan=lifespan,
|
lifespan=lifespan,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ def get_market_store() -> MarketStore:
|
|||||||
def create_app() -> FastAPI:
|
def create_app() -> FastAPI:
|
||||||
"""Create the news/explain service app."""
|
"""Create the news/explain service app."""
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="EvoTraders News Service",
|
title="大时代 News Service",
|
||||||
description="Read-only news enrichment and explain service surface extracted from the monolith",
|
description="Read-only news enrichment and explain service surface extracted from the monolith",
|
||||||
version="0.1.0",
|
version="0.1.0",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from backend.api.openclaw import get_openclaw_cli_service
|
|||||||
def create_app() -> FastAPI:
|
def create_app() -> FastAPI:
|
||||||
"""Create the OpenClaw service app."""
|
"""Create the OpenClaw service app."""
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="EvoTraders OpenClaw Service",
|
title="大时代 OpenClaw Service",
|
||||||
description="Read-only OpenClaw CLI integration service surface",
|
description="Read-only OpenClaw CLI integration service surface",
|
||||||
version="0.1.0",
|
version="0.1.0",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from backend.apps.cors import add_cors_middleware
|
|||||||
def create_app() -> FastAPI:
|
def create_app() -> FastAPI:
|
||||||
"""Create the runtime service app."""
|
"""Create the runtime service app."""
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="EvoTraders Runtime Service",
|
title="大时代 Runtime Service",
|
||||||
description="Runtime lifecycle and gateway service surface extracted from the monolith",
|
description="Runtime lifecycle and gateway service surface extracted from the monolith",
|
||||||
version="0.1.0",
|
version="0.1.0",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ from shared.schema import (
|
|||||||
def create_app() -> FastAPI:
|
def create_app() -> FastAPI:
|
||||||
"""Create the trading data service app."""
|
"""Create the trading data service app."""
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="EvoTraders Trading Service",
|
title="大时代 Trading Service",
|
||||||
description="Read-only trading data service surface extracted from the monolith",
|
description="Read-only trading data service surface extracted from the monolith",
|
||||||
version="0.1.0",
|
version="0.1.0",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- 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,
|
This module provides easy-to-use commands for running backtest, live trading,
|
||||||
and frontend development server.
|
and frontend development server.
|
||||||
@@ -44,7 +44,7 @@ from backend.enrich.news_enricher import enrich_symbols
|
|||||||
|
|
||||||
app = typer.Typer(
|
app = typer.Typer(
|
||||||
name="evotraders",
|
name="evotraders",
|
||||||
help="EvoTraders: A self-evolving multi-agent trading system",
|
help="大时代:自进化多智能体交易系统",
|
||||||
add_completion=False,
|
add_completion=False,
|
||||||
)
|
)
|
||||||
ingest_app = typer.Typer(help="Ingest Polygon market data into the research warehouse.")
|
ingest_app = typer.Typer(help="Ingest Polygon market data into the research warehouse.")
|
||||||
@@ -919,7 +919,7 @@ def backtest(
|
|||||||
"""
|
"""
|
||||||
console.print(
|
console.print(
|
||||||
Panel.fit(
|
Panel.fit(
|
||||||
"[bold cyan]EvoTraders Backtest Mode[/bold cyan]",
|
"[bold cyan]大时代 Backtest Mode[/bold cyan]",
|
||||||
border_style="cyan",
|
border_style="cyan",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -1082,7 +1082,7 @@ def live(
|
|||||||
interval_minutes = int(_normalize_typer_value(interval_minutes, 60))
|
interval_minutes = int(_normalize_typer_value(interval_minutes, 60))
|
||||||
console.print(
|
console.print(
|
||||||
Panel.fit(
|
Panel.fit(
|
||||||
"[bold cyan]EvoTraders LIVE Mode[/bold cyan]",
|
"[bold cyan]大时代 LIVE Mode[/bold cyan]",
|
||||||
border_style="cyan",
|
border_style="cyan",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -1251,7 +1251,7 @@ def frontend(
|
|||||||
"""
|
"""
|
||||||
console.print(
|
console.print(
|
||||||
Panel.fit(
|
Panel.fit(
|
||||||
"[bold cyan]EvoTraders Frontend[/bold cyan]",
|
"[bold cyan]大时代 Frontend[/bold cyan]",
|
||||||
border_style="cyan",
|
border_style="cyan",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -1319,16 +1319,16 @@ def frontend(
|
|||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def version():
|
def version():
|
||||||
"""Show the version of EvoTraders."""
|
"""Show the version of 大时代."""
|
||||||
console.print(
|
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()
|
@app.callback()
|
||||||
def main():
|
def main():
|
||||||
"""
|
"""
|
||||||
EvoTraders: A self-evolving multi-agent trading system
|
大时代:自进化多智能体交易系统
|
||||||
|
|
||||||
Use 'evotraders --help' to see available commands.
|
Use 'evotraders --help' to see available commands.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -4,6 +4,22 @@
|
|||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_TICKERS = [
|
||||||
|
"AAPL",
|
||||||
|
"MSFT",
|
||||||
|
"GOOGL",
|
||||||
|
"AMZN",
|
||||||
|
"NVDA",
|
||||||
|
"META",
|
||||||
|
"TSLA",
|
||||||
|
"AMD",
|
||||||
|
"NFLX",
|
||||||
|
"AVGO",
|
||||||
|
"PLTR",
|
||||||
|
"COIN",
|
||||||
|
]
|
||||||
import re
|
import re
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
@@ -127,7 +143,7 @@ def resolve_runtime_config(
|
|||||||
bootstrap = get_bootstrap_config_for_run(project_root, config_name)
|
bootstrap = get_bootstrap_config_for_run(project_root, config_name)
|
||||||
return {
|
return {
|
||||||
"tickers": bootstrap.get("tickers")
|
"tickers": bootstrap.get("tickers")
|
||||||
or get_env_list("TICKERS", ["AAPL", "MSFT"]),
|
or get_env_list("TICKERS", DEFAULT_TICKERS),
|
||||||
"initial_cash": float(
|
"initial_cash": float(
|
||||||
bootstrap.get(
|
bootstrap.get(
|
||||||
"initial_cash",
|
"initial_cash",
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ from agentscope.message import Msg
|
|||||||
from agentscope.pipeline import MsgHub
|
from agentscope.pipeline import MsgHub
|
||||||
|
|
||||||
from backend.utils.settlement import SettlementCoordinator
|
from backend.utils.settlement import SettlementCoordinator
|
||||||
from backend.utils.terminal_dashboard import get_dashboard
|
|
||||||
from backend.core.state_sync import StateSync
|
from backend.core.state_sync import StateSync
|
||||||
from backend.utils.trade_executor import PortfolioTradeExecutor
|
from backend.utils.trade_executor import PortfolioTradeExecutor
|
||||||
from backend.runtime.manager import TradingRuntimeManager
|
from backend.runtime.manager import TradingRuntimeManager
|
||||||
@@ -48,12 +47,8 @@ except ImportError:
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def _log(msg: str):
|
def _log(msg: str) -> None:
|
||||||
"""Log to dashboard if available, otherwise to logger"""
|
"""Helper function for pipeline logging."""
|
||||||
dashboard = get_dashboard()
|
|
||||||
if dashboard.live:
|
|
||||||
dashboard.log(msg)
|
|
||||||
else:
|
|
||||||
logger.info(msg)
|
logger.info(msg)
|
||||||
|
|
||||||
|
|
||||||
@@ -71,7 +66,7 @@ class TradingPipeline:
|
|||||||
|
|
||||||
Real-time updates via StateSync after each agent completes.
|
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__(
|
def __init__(
|
||||||
@@ -1625,14 +1620,13 @@ class TradingPipeline:
|
|||||||
project_root = Path(__file__).resolve().parents[2]
|
project_root = Path(__file__).resolve().parents[2]
|
||||||
personas = get_prompt_loader().load_yaml_config("analyst", "personas")
|
personas = get_prompt_loader().load_yaml_config("analyst", "personas")
|
||||||
persona = personas.get(analyst_type, {})
|
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,
|
config_name=config_name,
|
||||||
agent_id=agent_id,
|
agent_id=agent_id,
|
||||||
role_seed=persona.get("description", "").strip(),
|
file_contents=workspace_manager.build_default_agent_files(
|
||||||
style_seed="\n".join(f"- {item}" for item in persona.get("focus", [])),
|
agent_id=agent_id,
|
||||||
policy_seed=(
|
persona=persona,
|
||||||
"State a clear signal, confidence, and the conditions "
|
|
||||||
"that would invalidate the thesis."
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -232,7 +232,7 @@ async def run_pipeline(
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Extract config values
|
# 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))
|
initial_cash = float(bootstrap.get("initial_cash", 100000.0))
|
||||||
margin_requirement = float(bootstrap.get("margin_requirement", 0.0))
|
margin_requirement = float(bootstrap.get("margin_requirement", 0.0))
|
||||||
max_comm_cycles = int(bootstrap.get("max_comm_cycles", 2))
|
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()
|
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]:
|
def _normalize_provider_news_rows(ticker: str, news_items: Iterable[Any]) -> list[dict]:
|
||||||
rows: list[dict] = []
|
rows: list[dict] = []
|
||||||
for item in news_items:
|
for item in news_items:
|
||||||
@@ -80,7 +110,11 @@ def ingest_ticker_history(
|
|||||||
price_count = market_store.upsert_ohlc(ticker, ohlc_rows, source="polygon")
|
price_count = market_store.upsert_ohlc(ticker, ohlc_rows, source="polygon")
|
||||||
news_count = market_store.upsert_news(ticker, news_rows, source="polygon")
|
news_count = market_store.upsert_news(ticker, news_rows, source="polygon")
|
||||||
aligned_count = align_news_for_symbol(market_store, ticker)
|
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 {
|
return {
|
||||||
"symbol": ticker,
|
"symbol": ticker,
|
||||||
@@ -108,9 +142,15 @@ def update_ticker_incremental(
|
|||||||
if watermarks.get("last_price_fetch")
|
if watermarks.get("last_price_fetch")
|
||||||
else _default_start()
|
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 = (
|
start_news = (
|
||||||
(datetime.fromisoformat(watermarks["last_news_fetch"]) + timedelta(days=1)).date().isoformat()
|
(datetime.fromisoformat(effective_last_news_fetch) + timedelta(days=1)).date().isoformat()
|
||||||
if watermarks.get("last_news_fetch")
|
if effective_last_news_fetch
|
||||||
else _default_start()
|
else _default_start()
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -130,7 +170,7 @@ def update_ticker_incremental(
|
|||||||
market_store.update_fetch_watermark(
|
market_store.update_fetch_watermark(
|
||||||
symbol=ticker,
|
symbol=ticker,
|
||||||
price_date=end if ohlc_rows or watermarks.get("last_price_fetch") else None,
|
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 {
|
return {
|
||||||
@@ -155,9 +195,15 @@ def refresh_news_incremental(
|
|||||||
market_store = store or MarketStore()
|
market_store = store or MarketStore()
|
||||||
watermarks = market_store.get_ticker_watermarks(ticker)
|
watermarks = market_store.get_ticker_watermarks(ticker)
|
||||||
end = end_date or _today_utc()
|
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 = (
|
start_news = (
|
||||||
(datetime.fromisoformat(watermarks["last_news_fetch"]) + timedelta(days=1)).date().isoformat()
|
(datetime.fromisoformat(effective_last_news_fetch) + timedelta(days=1)).date().isoformat()
|
||||||
if watermarks.get("last_news_fetch")
|
if effective_last_news_fetch
|
||||||
else _default_start()
|
else _default_start()
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -182,7 +228,7 @@ def refresh_news_incremental(
|
|||||||
aligned_count = align_news_for_symbol(market_store, ticker)
|
aligned_count = align_news_for_symbol(market_store, ticker)
|
||||||
market_store.update_fetch_watermark(
|
market_store.update_fetch_watermark(
|
||||||
symbol=ticker,
|
symbol=ticker,
|
||||||
news_date=end if news_rows or watermarks.get("last_news_fetch") else None,
|
news_date=_max_news_date(news_rows),
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -244,6 +244,20 @@ class MarketStore:
|
|||||||
"last_news_fetch": None,
|
"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:
|
def upsert_ohlc(self, symbol: str, rows: Iterable[dict[str, Any]], *, source: str = "polygon") -> int:
|
||||||
timestamp = _utc_timestamp()
|
timestamp = _utc_timestamp()
|
||||||
count = 0
|
count = 0
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ INFO_LOGGER_PREFIXES = (
|
|||||||
"backend.core.pipeline",
|
"backend.core.pipeline",
|
||||||
"backend.core.scheduler",
|
"backend.core.scheduler",
|
||||||
"backend.services.gateway_cycle_support",
|
"backend.services.gateway_cycle_support",
|
||||||
"backend.utils.terminal_dashboard",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
NOISY_LOGGER_LEVELS = {
|
NOISY_LOGGER_LEVELS = {
|
||||||
@@ -119,7 +118,7 @@ async def run_gateway(
|
|||||||
"""Run Gateway with Pipeline."""
|
"""Run Gateway with Pipeline."""
|
||||||
|
|
||||||
# Extract config
|
# 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))
|
initial_cash = float(bootstrap.get("initial_cash", 100000.0))
|
||||||
margin_requirement = float(bootstrap.get("margin_requirement", 0.0))
|
margin_requirement = float(bootstrap.get("margin_requirement", 0.0))
|
||||||
max_comm_cycles = int(bootstrap.get("max_comm_cycles", 2))
|
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.data.provider_utils import normalize_symbol
|
||||||
from backend.domains import news as news_domain
|
from backend.domains import news as news_domain
|
||||||
from backend.llm.models import get_agent_model_info
|
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.pipeline import TradingPipeline
|
||||||
from backend.core.state_sync import StateSync
|
from backend.core.state_sync import StateSync
|
||||||
from backend.services.market import MarketService
|
from backend.services.market import MarketService
|
||||||
@@ -40,9 +39,6 @@ EDITABLE_AGENT_WORKSPACE_FILES = {
|
|||||||
"AGENTS.md",
|
"AGENTS.md",
|
||||||
"MEMORY.md",
|
"MEMORY.md",
|
||||||
"POLICY.md",
|
"POLICY.md",
|
||||||
"HEARTBEAT.md",
|
|
||||||
"ROLE.md",
|
|
||||||
"STYLE.md",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -84,7 +80,6 @@ class Gateway:
|
|||||||
self._manual_cycle_task: Optional[asyncio.Task] = None
|
self._manual_cycle_task: Optional[asyncio.Task] = None
|
||||||
self._backtest_start_date: Optional[str] = None
|
self._backtest_start_date: Optional[str] = None
|
||||||
self._backtest_end_date: Optional[str] = None
|
self._backtest_end_date: Optional[str] = None
|
||||||
self._dashboard = get_dashboard()
|
|
||||||
self._market_status_task: Optional[asyncio.Task] = None
|
self._market_status_task: Optional[asyncio.Task] = None
|
||||||
self._watchlist_ingest_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._loop = asyncio.get_running_loop()
|
||||||
self._provider_router.add_listener(self._on_provider_usage_changed)
|
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.state_sync.load_state()
|
||||||
self.market_service.set_price_recorder(self.storage.record_price_point)
|
self.market_service.set_price_recorder(self.storage.record_price_point)
|
||||||
self.state_sync.update_state("status", "initializing")
|
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)
|
dashboard_snapshot = self.storage.build_dashboard_snapshot_from_state(self.state_sync.state)
|
||||||
summary = dashboard_snapshot.get("summary")
|
summary = dashboard_snapshot.get("summary")
|
||||||
if 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(
|
logger.info(
|
||||||
"Loaded existing portfolio: $%s",
|
"Loaded existing portfolio: $%s",
|
||||||
f"{summary.get('totalAssetValue', 0):,.2f}",
|
f"{summary.get('totalAssetValue', 0):,.2f}",
|
||||||
@@ -252,7 +222,6 @@ class Gateway:
|
|||||||
def _on_provider_usage_changed(self, snapshot: Dict[str, Any]):
|
def _on_provider_usage_changed(self, snapshot: Dict[str, Any]):
|
||||||
"""Handle provider routing updates from the shared router."""
|
"""Handle provider routing updates from the shared router."""
|
||||||
self.state_sync.update_state("data_sources", snapshot)
|
self.state_sync.update_state("data_sources", snapshot)
|
||||||
self._dashboard.update(data_sources=snapshot)
|
|
||||||
if self._loop and self._loop.is_running():
|
if self._loop and self._loop.is_running():
|
||||||
asyncio.run_coroutine_threadsafe(
|
asyncio.run_coroutine_threadsafe(
|
||||||
self.broadcast(
|
self.broadcast(
|
||||||
|
|||||||
@@ -147,25 +147,10 @@ async def on_heartbeat_trigger(gateway: Any, date: str) -> None:
|
|||||||
|
|
||||||
for analyst in analysts:
|
for analyst in analysts:
|
||||||
try:
|
try:
|
||||||
ws_id = getattr(analyst, "workspace_id", None)
|
logger.debug(
|
||||||
if ws_id:
|
"[Heartbeat] No heartbeat configured for %s, skipping",
|
||||||
from backend.agents.workspace_manager import get_workspace_dir
|
analyst.name,
|
||||||
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)
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.error("[Heartbeat] %s failed: %s", analyst.name, exc, exc_info=True)
|
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.market_service.emit_market_open()
|
||||||
|
|
||||||
await gateway.state_sync.on_cycle_start(date)
|
await gateway.state_sync.on_cycle_start(date)
|
||||||
gateway._dashboard.update(date=date, status="Analyzing...")
|
|
||||||
|
|
||||||
prices = gateway.market_service.get_open_prices()
|
prices = gateway.market_service.get_open_prices()
|
||||||
close_prices = gateway.market_service.get_close_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)
|
logger.warning("Live cycle news refresh failed: %s", exc)
|
||||||
|
|
||||||
await gateway.state_sync.on_cycle_start(trading_date)
|
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)
|
market_caps = await get_market_caps(gateway, tickers, trading_date)
|
||||||
schedule_mode = gateway.config.get("schedule_mode", "daily")
|
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())
|
summary.update(gateway.storage.get_live_returns())
|
||||||
|
|
||||||
await gateway.state_sync.on_cycle_end(date, portfolio_summary=summary)
|
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 []
|
leaderboard = dashboard_snapshot.get("leaderboard") or []
|
||||||
if leaderboard:
|
if leaderboard:
|
||||||
await gateway.state_sync.on_leaderboard_update(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]:
|
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:
|
async def run_backtest_dates(gateway: Any, dates: list[str]) -> None:
|
||||||
gateway.state_sync.set_backtest_dates(dates)
|
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")
|
await gateway.state_sync.on_system_message(f"Starting backtest - {len(dates)} trading days")
|
||||||
try:
|
try:
|
||||||
for i, date in enumerate(dates):
|
for date in dates:
|
||||||
gateway._dashboard.update(days_completed=i)
|
|
||||||
await gateway.on_strategy_trigger(date=date)
|
await gateway.on_strategy_trigger(date=date)
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
await gateway.state_sync.on_system_message(f"Backtest complete - {len(dates)} days")
|
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:
|
except Exception as exc:
|
||||||
error_msg = f"Backtest failed: {type(exc).__name__}: {str(exc)}"
|
error_msg = f"Backtest failed: {type(exc).__name__}: {str(exc)}"
|
||||||
logger.error(error_msg, exc_info=True)
|
logger.error(error_msg, exc_info=True)
|
||||||
asyncio.create_task(gateway.state_sync.on_system_message(error_msg))
|
asyncio.create_task(gateway.state_sync.on_system_message(error_msg))
|
||||||
gateway._dashboard.update(status=f"Failed: {str(exc)}")
|
|
||||||
gateway._dashboard.stop()
|
|
||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
gateway._backtest_task = None
|
gateway._backtest_task = None
|
||||||
@@ -376,7 +348,6 @@ def set_backtest_dates(gateway: Any, dates: list[str]) -> None:
|
|||||||
if dates:
|
if dates:
|
||||||
gateway._backtest_start_date = dates[0]
|
gateway._backtest_start_date = dates[0]
|
||||||
gateway._backtest_end_date = dates[-1]
|
gateway._backtest_end_date = dates[-1]
|
||||||
gateway._dashboard.days_total = len(dates)
|
|
||||||
|
|
||||||
|
|
||||||
def stop_gateway(gateway: Any) -> None:
|
def stop_gateway(gateway: Any) -> None:
|
||||||
@@ -399,4 +370,3 @@ def stop_gateway(gateway: Any) -> None:
|
|||||||
loop.run_until_complete(gateway._openclaw_ws.disconnect())
|
loop.run_until_complete(gateway._openclaw_ws.disconnect())
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
gateway._dashboard.stop()
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
def _ensure_session_bridge(gateway) -> None:
|
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):
|
if getattr(gateway, "_openclaw_session_bridge_ready", False):
|
||||||
return
|
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:
|
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("tickers", gateway.config.get("tickers", []))
|
||||||
gateway.state_sync.update_state(
|
gateway.state_sync.update_state(
|
||||||
"runtime_config",
|
"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.storage.update_server_state_from_dashboard(gateway.state_sync.state)
|
||||||
gateway.state_sync.save_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 ""
|
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 = WorkspaceManager(project_root=tmp_path)
|
||||||
|
|
||||||
manager.initialize_default_assets(
|
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 / "PROFILE.md").exists()
|
||||||
assert (asset_dir / "AGENTS.md").exists()
|
assert (asset_dir / "AGENTS.md").exists()
|
||||||
assert (asset_dir / "MEMORY.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 / "agent.yaml").exists()
|
||||||
assert (asset_dir / "skills" / "installed").is_dir()
|
assert (asset_dir / "skills" / "installed").is_dir()
|
||||||
assert (asset_dir / "skills" / "active").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()
|
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):
|
def test_agent_workspace_config_controls_prompt_files(tmp_path, monkeypatch):
|
||||||
manager = WorkspaceManager(project_root=tmp_path)
|
manager = WorkspaceManager(project_root=tmp_path)
|
||||||
manager.initialize_default_assets(
|
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
|
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):
|
def test_skills_manager_applies_agent_level_skill_toggles(tmp_path):
|
||||||
builtin_root = tmp_path / "backend" / "skills" / "builtin"
|
builtin_root = tmp_path / "backend" / "skills" / "builtin"
|
||||||
for skill_name in ("risk_review", "extra_guard"):
|
for skill_name in ("risk_review", "extra_guard"):
|
||||||
|
|||||||
@@ -8,24 +8,6 @@ import pytest
|
|||||||
from backend.services import gateway_cycle_support, gateway_runtime_support
|
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:
|
class _DummyScheduler:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.calls = []
|
self.calls = []
|
||||||
@@ -128,7 +110,6 @@ def make_gateway_stub():
|
|||||||
},
|
},
|
||||||
storage=_DummyStorage(),
|
storage=_DummyStorage(),
|
||||||
state_sync=_DummyStateSync(),
|
state_sync=_DummyStateSync(),
|
||||||
_dashboard=_DummyDashboard(),
|
|
||||||
_watchlist_ingest_task=None,
|
_watchlist_ingest_task=None,
|
||||||
_market_status_task=None,
|
_market_status_task=None,
|
||||||
_backtest_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:
|
class StockTechnicalAnalyzer:
|
||||||
"""Lightweight technical analyzer adapted for EvoTraders tools."""
|
"""Lightweight technical analyzer adapted for 大时代 tools."""
|
||||||
|
|
||||||
def analyze(self, ticker: str, df: pd.DataFrame) -> TechnicalSignal:
|
def analyze(self, ticker: str, df: pd.DataFrame) -> TechnicalSignal:
|
||||||
"""Analyze one ticker from OHLC price history."""
|
"""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
|
# Deployment Notes
|
||||||
|
|
||||||
This directory contains the current production-oriented deployment artifacts for
|
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
|
## Contents
|
||||||
|
|
||||||
- [deploy/systemd/evotraders.service](./systemd/evotraders.service)
|
- [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)
|
- [scripts/run_prod.sh](../scripts/run_prod.sh)
|
||||||
- production launch script used by the systemd unit
|
- 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
|
- 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
|
- plain HTTP/static-site variant
|
||||||
|
|
||||||
## Current Production Shape
|
## Current Production Shape
|
||||||
|
|
||||||
The checked-in production path is intentionally minimal:
|
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`
|
- nginx proxies `/ws` to `127.0.0.1:8765`
|
||||||
- systemd runs `scripts/run_prod.sh`
|
- systemd runs `scripts/run_prod.sh`
|
||||||
- `scripts/run_prod.sh` starts `python3 -m backend.main` in live mode on `127.0.0.1:8765`
|
- `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
|
## Important Paths And Ports
|
||||||
|
|
||||||
- frontend root: `/var/www/evotraders/current`
|
- frontend root: `/var/www/bigtime/current`
|
||||||
- gateway bind: `127.0.0.1:8765`
|
- gateway bind: `127.0.0.1:8765`
|
||||||
- public WebSocket path: `/ws`
|
- public WebSocket path: `/ws`
|
||||||
- working directory expected by systemd: `/root/code/evotraders`
|
- working directory expected by systemd: `/root/code/evotraders`
|
||||||
@@ -61,32 +62,45 @@ journalctl -u evotraders -f
|
|||||||
|
|
||||||
The HTTPS nginx config does two things:
|
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
|
- proxies `/ws` to the local gateway process with WebSocket upgrade headers
|
||||||
|
|
||||||
Typical install flow:
|
Typical install flow:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo cp deploy/nginx/evotraders.cillinn.com.conf /etc/nginx/sites-available/evotraders.cillinn.com.conf
|
sudo cp deploy/nginx/bigtime.cillinn.com.conf /etc/nginx/sites-available/bigtime.cillinn.com.conf
|
||||||
sudo ln -s /etc/nginx/sites-available/evotraders.cillinn.com.conf /etc/nginx/sites-enabled/
|
sudo ln -s /etc/nginx/sites-available/bigtime.cillinn.com.conf /etc/nginx/sites-enabled/
|
||||||
sudo nginx -t
|
sudo nginx -t
|
||||||
sudo systemctl reload nginx
|
sudo systemctl reload nginx
|
||||||
```
|
```
|
||||||
|
|
||||||
The checked-in TLS config expects Let's Encrypt assets at:
|
The checked-in TLS config expects Let's Encrypt assets at:
|
||||||
|
|
||||||
- `/etc/letsencrypt/live/evotraders.cillinn.com/fullchain.pem`
|
- `/etc/letsencrypt/live/bigtime.cillinn.com/fullchain.pem`
|
||||||
- `/etc/letsencrypt/live/evotraders.cillinn.com/privkey.pem`
|
- `/etc/letsencrypt/live/bigtime.cillinn.com/privkey.pem`
|
||||||
|
|
||||||
## Environment Expectations
|
## Environment Expectations
|
||||||
|
|
||||||
Before using the production scripts, ensure the runtime environment has:
|
Before using the production scripts, ensure the runtime environment has:
|
||||||
|
|
||||||
- a usable Python environment
|
- 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
|
- repo dependencies installed
|
||||||
- required market/model API keys
|
- required market/model API keys
|
||||||
- any desired `TICKERS` override
|
- 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:
|
The production script currently sets:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name evotraders.cillinn.com;
|
server_name bigtime.cillinn.com;
|
||||||
|
|
||||||
location /.well-known/acme-challenge/ {
|
location /.well-known/acme-challenge/ {
|
||||||
root /var/www/evotraders/current;
|
root /var/www/bigtime/current;
|
||||||
allow all;
|
allow all;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -14,13 +14,13 @@ server {
|
|||||||
|
|
||||||
server {
|
server {
|
||||||
listen 443 ssl http2;
|
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;
|
index index.html;
|
||||||
|
|
||||||
ssl_certificate /etc/letsencrypt/live/evotraders.cillinn.com/fullchain.pem;
|
ssl_certificate /etc/letsencrypt/live/bigtime.cillinn.com/fullchain.pem;
|
||||||
ssl_certificate_key /etc/letsencrypt/live/evotraders.cillinn.com/privkey.pem;
|
ssl_certificate_key /etc/letsencrypt/live/bigtime.cillinn.com/privkey.pem;
|
||||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||||
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
server {
|
server {
|
||||||
listen 80;
|
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;
|
index index.html;
|
||||||
|
|
||||||
location /.well-known/acme-challenge/ {
|
location /.well-known/acme-challenge/ {
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=EvoTraders Production Service
|
Description=大时代 Production Service
|
||||||
After=network.target
|
After=network.target
|
||||||
|
|
||||||
[Service]
|
[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 | 通用配置 ==================
|
# ================== General Configuration | 通用配置 ==================
|
||||||
# List of stock ticker symbols to analyze (comma-separated) | 想要分析的股票代码列表(用逗号分隔)
|
# 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
|
# 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
|
# 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
|
```bash
|
||||||
cd frontend
|
cd frontend
|
||||||
npm install
|
npm ci
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Default dev URL: `http://localhost:5173`
|
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
|
## 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).
|
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
|
## Direct-Service Coverage
|
||||||
|
|
||||||
Current direct-call coverage includes:
|
Current direct-call coverage includes:
|
||||||
|
|||||||
@@ -1,10 +1,24 @@
|
|||||||
# Frontend Environment Variables Template
|
# Frontend Environment Variables Template
|
||||||
# 复制此文件为 .env 并修改配置
|
# 复制此文件为 .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=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" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/png" href="/trading_logo.png" />
|
<link rel="icon" type="image/png" href="/trading_logo.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>EvoTraders</title>
|
<title>大时代</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20",
|
||||||
|
"npm": ">=10"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
@@ -15,7 +19,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dicebear/collection": "^9.4.2",
|
"@dicebear/collection": "^9.4.2",
|
||||||
"@dicebear/core": "^9.4.2",
|
"@dicebear/core": "^9.4.2",
|
||||||
"@lobehub/icons": "^5.0.1",
|
"@lobehub/icons": "^5.2.0",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
|
|||||||
@@ -21,10 +21,7 @@ const EDITABLE_AGENT_WORKSPACE_FILES = [
|
|||||||
'PROFILE.md',
|
'PROFILE.md',
|
||||||
'AGENTS.md',
|
'AGENTS.md',
|
||||||
'MEMORY.md',
|
'MEMORY.md',
|
||||||
'POLICY.md',
|
'POLICY.md'
|
||||||
'HEARTBEAT.md',
|
|
||||||
'ROLE.md',
|
|
||||||
'STYLE.md'
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function LiveTradingApp() {
|
export default function LiveTradingApp() {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import React, { Suspense, lazy, useRef, useEffect, useMemo } from 'react';
|
|||||||
import GlobalStyles from '../styles/GlobalStyles';
|
import GlobalStyles from '../styles/GlobalStyles';
|
||||||
import Header from './Header.jsx';
|
import Header from './Header.jsx';
|
||||||
import RuntimeSettingsPanel from './RuntimeSettingsPanel.jsx';
|
import RuntimeSettingsPanel from './RuntimeSettingsPanel.jsx';
|
||||||
import StockLogo from './StockLogo.jsx';
|
|
||||||
import NetValueChart from './NetValueChart.jsx';
|
import NetValueChart from './NetValueChart.jsx';
|
||||||
import { AGENTS } from '../config/constants';
|
import { AGENTS } from '../config/constants';
|
||||||
import { useRuntimeStore } from '../store/runtimeStore';
|
import { useRuntimeStore } from '../store/runtimeStore';
|
||||||
@@ -322,7 +321,6 @@ export default function AppShell({
|
|||||||
<div key={groupIdx} className="ticker-group">
|
<div key={groupIdx} className="ticker-group">
|
||||||
{displayTickers.map(ticker => (
|
{displayTickers.map(ticker => (
|
||||||
<div key={`${ticker.symbol}-${groupIdx}`} className="ticker-item">
|
<div key={`${ticker.symbol}-${groupIdx}`} className="ticker-item">
|
||||||
<StockLogo ticker={ticker.symbol} size={16} />
|
|
||||||
<span className="ticker-symbol">{ticker.symbol}</span>
|
<span className="ticker-symbol">{ticker.symbol}</span>
|
||||||
<span className="ticker-price">
|
<span className="ticker-price">
|
||||||
<span className={`ticker-price-value ${rollingTickers[ticker.symbol] ? 'rolling' : ''}`}>
|
<span className={`ticker-price-value ${rollingTickers[ticker.symbol] ? 'rolling' : ''}`}>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Header Component
|
* Header Component
|
||||||
* Reusable header brand for EvoTraders.
|
* Reusable header brand for 大时代.
|
||||||
*/
|
*/
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
return (
|
return (
|
||||||
@@ -19,10 +19,10 @@ export default function Header() {
|
|||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src="/trading_logo.png"
|
src="/trading_logo.png"
|
||||||
alt="EvoTraders Logo"
|
alt="大时代 Logo"
|
||||||
style={{ height: '24px', width: 'auto' }}
|
style={{ height: '24px', width: 'auto' }}
|
||||||
/>
|
/>
|
||||||
EvoTraders
|
大时代
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ export default function NetValueChart({ equity, baseline, baseline_vw, momentum,
|
|||||||
|
|
||||||
// Legend descriptions
|
// Legend descriptions
|
||||||
const legendDescriptions = {
|
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 (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',
|
'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',
|
'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
|
<Line
|
||||||
type="linear"
|
type="linear"
|
||||||
dataKey="portfolio"
|
dataKey="portfolio"
|
||||||
name="EvoTraders"
|
name="大时代"
|
||||||
stroke="#00C853"
|
stroke="#00C853"
|
||||||
strokeWidth={2.5}
|
strokeWidth={2.5}
|
||||||
dot={(props) => <CustomDot {...props} dataKey="portfolio" />}
|
dot={(props) => <CustomDot {...props} dataKey="portfolio" />}
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
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 ModelIcon from "@lobehub/icons/es/features/ModelIcon";
|
||||||
import { useOpenClawStore } from "../store/openclawStore";
|
import { useOpenClawStore } from "../store/openclawStore";
|
||||||
import { useOpenClawPanel } from "../hooks/useOpenClawPanel";
|
import { useOpenClawPanel } from "../hooks/useOpenClawPanel";
|
||||||
@@ -27,6 +25,7 @@ const AGENT_COLORS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const OPENCLAW_EXPANDED_PANEL_MAX_HEIGHT = 420;
|
const OPENCLAW_EXPANDED_PANEL_MAX_HEIGHT = 420;
|
||||||
|
const OPENCLAW_AVATAR_POOL = Array.from({ length: 101 }, (_, index) => `/images/${index + 1}.png`);
|
||||||
|
|
||||||
function getAgentColor(agentId) {
|
function getAgentColor(agentId) {
|
||||||
let hash = 0;
|
let hash = 0;
|
||||||
@@ -37,6 +36,16 @@ function getAgentColor(agentId) {
|
|||||||
return AGENT_COLORS[Math.abs(hash) % AGENT_COLORS.length].accent;
|
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) {
|
function agentStateFromPresence(presence, agentId) {
|
||||||
const p = presence?.[agentId];
|
const p = presence?.[agentId];
|
||||||
if (!p) return "idle";
|
if (!p) return "idle";
|
||||||
@@ -50,15 +59,7 @@ function agentStateFromPresence(presence, agentId) {
|
|||||||
|
|
||||||
function AvatarIcon({ agentId, size = 56, borderRadius = 14 }) {
|
function AvatarIcon({ agentId, size = 56, borderRadius = 14 }) {
|
||||||
const color = getAgentColor(agentId);
|
const color = getAgentColor(agentId);
|
||||||
const avatarUri = useMemo(() => {
|
const avatarPath = useMemo(() => getStableAvatarPath(agentId), [agentId]);
|
||||||
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]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
@@ -75,7 +76,7 @@ function AvatarIcon({ agentId, size = 56, borderRadius = 14 }) {
|
|||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
}}>
|
}}>
|
||||||
<img
|
<img
|
||||||
src={avatarUri}
|
src={avatarPath}
|
||||||
alt={agentId || "agent"}
|
alt={agentId || "agent"}
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
@@ -1041,7 +1042,7 @@ export function OpenClawStatus() {
|
|||||||
/>
|
/>
|
||||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 8 }}>
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 8 }}>
|
||||||
<div style={{ fontSize: 10, color: store.chatError ? "#EF4444" : "#9CA3AF" }}>
|
<div style={{ fontSize: 10, color: store.chatError ? "#EF4444" : "#9CA3AF" }}>
|
||||||
{store.chatError || "消息将通过 EvoTraders Gateway 转发到 OpenClaw Gateway"}
|
{store.chatError || "消息将通过 大时代 Gateway 转发到 OpenClaw Gateway"}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
disabled={!selectedSession || !(chatDraftBySession[selectedSessionKey || "__none__"] || "").trim()}
|
disabled={!selectedSession || !(chatDraftBySession[selectedSessionKey || "__none__"] || "").trim()}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import StockLogo from './StockLogo';
|
|
||||||
import { formatNumber, formatDateTime } from '../utils/formatters';
|
import { formatNumber, formatDateTime } from '../utils/formatters';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -497,7 +496,6 @@ export default function StatisticsView({ trades, holdings, stats, baseline_vw, e
|
|||||||
<tr key={h.ticker}>
|
<tr key={h.ticker}>
|
||||||
<td>
|
<td>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
<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>
|
<span style={{ fontWeight: 700, color: '#000000' }}>{h.ticker}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -623,7 +621,6 @@ export default function StatisticsView({ trades, holdings, stats, baseline_vw, e
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
<StockLogo ticker={t.ticker} size={16} />
|
|
||||||
<span style={{ fontWeight: 700, color: '#000000' }}>{t.ticker}</span>
|
<span style={{ fontWeight: 700, color: '#000000' }}>{t.ticker}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -1,21 +1,10 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
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 ExplainNewsSection from './explain/ExplainNewsSection';
|
||||||
import ExplainPriceSection from './explain/ExplainPriceSection';
|
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 ExplainInsiderSection from './explain/ExplainInsiderSection';
|
||||||
import ExplainTechnicalSection from './explain/ExplainTechnicalSection';
|
import ExplainTechnicalSection from './explain/ExplainTechnicalSection';
|
||||||
import { EVENT_CATEGORY_META, eventDateKey } from './explain/explainUtils';
|
|
||||||
import useExplainModel from './explain/useExplainModel';
|
import useExplainModel from './explain/useExplainModel';
|
||||||
import { formatDateTime, formatNumber, formatTickerPrice } from '../utils/formatters';
|
import { formatNumber, formatTickerPrice } from '../utils/formatters';
|
||||||
|
|
||||||
export default function StockExplainView({
|
export default function StockExplainView({
|
||||||
tickers,
|
tickers,
|
||||||
@@ -28,74 +17,34 @@ export default function StockExplainView({
|
|||||||
selectedSymbol,
|
selectedSymbol,
|
||||||
onSelectedSymbolChange,
|
onSelectedSymbolChange,
|
||||||
selectedHistorySource,
|
selectedHistorySource,
|
||||||
explainEventsSnapshot,
|
|
||||||
newsSnapshot,
|
newsSnapshot,
|
||||||
insiderTradesSnapshot,
|
insiderTradesSnapshot,
|
||||||
technicalIndicatorsSnapshot,
|
technicalIndicatorsSnapshot,
|
||||||
onRequestRangeExplain,
|
|
||||||
onRequestHistory,
|
onRequestHistory,
|
||||||
onRequestExplainEvents,
|
|
||||||
onRequestNews,
|
onRequestNews,
|
||||||
onRequestNewsForDate,
|
|
||||||
onRequestStory,
|
|
||||||
onRequestInsiderTrades,
|
onRequestInsiderTrades,
|
||||||
onRequestTechnicalIndicators,
|
onRequestTechnicalIndicators,
|
||||||
currentDate,
|
|
||||||
onRequestSimilarDays,
|
|
||||||
onRequestStockEnrich
|
|
||||||
}) {
|
}) {
|
||||||
const [selectedEventDate, setSelectedEventDate] = useState('');
|
|
||||||
const [activeEventCategory, setActiveEventCategory] = useState('all');
|
|
||||||
const [activeNewsCategory, setActiveNewsCategory] = useState('all');
|
const [activeNewsCategory, setActiveNewsCategory] = useState('all');
|
||||||
const [activeNewsSentiment, setActiveNewsSentiment] = useState('all');
|
const [activeNewsSentiment, setActiveNewsSentiment] = useState('all');
|
||||||
const [isPriceOpen, setIsPriceOpen] = useState(true);
|
const [isPriceOpen, setIsPriceOpen] = useState(true);
|
||||||
const [isSummaryOpen, setIsSummaryOpen] = useState(true);
|
|
||||||
const [isSignalsOpen, setIsSignalsOpen] = useState(true);
|
|
||||||
const [isNewsOpen, setIsNewsOpen] = 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 [isInsiderOpen, setIsInsiderOpen] = useState(false);
|
||||||
const [isTechnicalOpen, setIsTechnicalOpen] = useState(true);
|
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 {
|
const {
|
||||||
availableSymbols,
|
availableSymbols,
|
||||||
selectedTicker,
|
selectedTicker,
|
||||||
holding,
|
holding,
|
||||||
tickerSignals,
|
|
||||||
signalSummary,
|
|
||||||
tickerTrades,
|
|
||||||
recentMentions,
|
|
||||||
tickerNews,
|
tickerNews,
|
||||||
visibleNews,
|
visibleNews,
|
||||||
newsCategories,
|
newsCategories,
|
||||||
visibleNewsByCategory,
|
visibleNewsByCategory,
|
||||||
selectedNewsFreshness,
|
selectedNewsFreshness,
|
||||||
selectedRangeWindow,
|
|
||||||
selectedRangeExplain,
|
|
||||||
latestSignal,
|
|
||||||
priceColor,
|
priceColor,
|
||||||
exposureWeight,
|
exposureWeight,
|
||||||
recentTrade,
|
|
||||||
ohlcSeries,
|
ohlcSeries,
|
||||||
priceSeries,
|
priceSeries,
|
||||||
explainSummary,
|
|
||||||
selectedStory,
|
|
||||||
selectedSimilarDays,
|
|
||||||
explainTimeline,
|
|
||||||
availableEventDates,
|
|
||||||
eventCategoryCounts,
|
|
||||||
visibleExplainEvents,
|
|
||||||
chartModel
|
chartModel
|
||||||
} = useExplainModel({
|
} = useExplainModel({
|
||||||
tickers,
|
tickers,
|
||||||
@@ -106,10 +55,9 @@ export default function StockExplainView({
|
|||||||
priceHistoryByTicker,
|
priceHistoryByTicker,
|
||||||
ohlcHistoryByTicker,
|
ohlcHistoryByTicker,
|
||||||
selectedSymbol,
|
selectedSymbol,
|
||||||
explainEventsSnapshot,
|
|
||||||
newsSnapshot,
|
newsSnapshot,
|
||||||
selectedEventDate,
|
selectedEventDate: '',
|
||||||
activeEventCategory,
|
activeEventCategory: 'all',
|
||||||
activeNewsCategory,
|
activeNewsCategory,
|
||||||
activeNewsSentiment
|
activeNewsSentiment
|
||||||
});
|
});
|
||||||
@@ -125,25 +73,10 @@ export default function StockExplainView({
|
|||||||
}
|
}
|
||||||
}, [availableSymbols, onSelectedSymbolChange, selectedSymbol]);
|
}, [availableSymbols, onSelectedSymbolChange, selectedSymbol]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!availableEventDates.length) {
|
|
||||||
setSelectedEventDate('');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!selectedEventDate || !availableEventDates.includes(selectedEventDate)) {
|
|
||||||
setSelectedEventDate(availableEventDates[0]);
|
|
||||||
}
|
|
||||||
}, [availableEventDates, selectedEventDate]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setActiveEventCategory('all');
|
|
||||||
}, [selectedSymbol]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setActiveNewsCategory('all');
|
setActiveNewsCategory('all');
|
||||||
setActiveNewsSentiment('all');
|
setActiveNewsSentiment('all');
|
||||||
}, [selectedSymbol, selectedEventDate]);
|
}, [selectedSymbol]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedSymbol) {
|
if (!selectedSymbol) {
|
||||||
@@ -154,53 +87,17 @@ export default function StockExplainView({
|
|||||||
onRequestHistory(selectedSymbol);
|
onRequestHistory(selectedSymbol);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onRequestExplainEvents && !explainEventsSnapshot) {
|
|
||||||
onRequestExplainEvents(selectedSymbol);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (onRequestNews && (!Array.isArray(newsSnapshot?.items) || newsSnapshot.items.length === 0)) {
|
if (onRequestNews && (!Array.isArray(newsSnapshot?.items) || newsSnapshot.items.length === 0)) {
|
||||||
onRequestNews(selectedSymbol);
|
onRequestNews(selectedSymbol);
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
explainEventsSnapshot,
|
|
||||||
newsSnapshot,
|
newsSnapshot,
|
||||||
ohlcHistoryByTicker,
|
ohlcHistoryByTicker,
|
||||||
onRequestExplainEvents,
|
|
||||||
onRequestHistory,
|
onRequestHistory,
|
||||||
onRequestNews,
|
onRequestNews,
|
||||||
selectedSymbol,
|
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(() => {
|
useEffect(() => {
|
||||||
if (!selectedSymbol || !onRequestTechnicalIndicators) {
|
if (!selectedSymbol || !onRequestTechnicalIndicators) {
|
||||||
return;
|
return;
|
||||||
@@ -211,67 +108,6 @@ export default function StockExplainView({
|
|||||||
onRequestTechnicalIndicators(selectedSymbol);
|
onRequestTechnicalIndicators(selectedSymbol);
|
||||||
}, [selectedSymbol, onRequestTechnicalIndicators, technicalIndicatorsSnapshot]);
|
}, [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 (
|
return (
|
||||||
<div className="performance-page">
|
<div className="performance-page">
|
||||||
<div className="section">
|
<div className="section">
|
||||||
@@ -285,7 +121,6 @@ export default function StockExplainView({
|
|||||||
onClick={() => onSelectedSymbolChange?.(symbol)}
|
onClick={() => onSelectedSymbolChange?.(symbol)}
|
||||||
style={{ display: 'flex', alignItems: 'center', gap: 6 }}
|
style={{ display: 'flex', alignItems: 'center', gap: 6 }}
|
||||||
>
|
>
|
||||||
<StockLogo ticker={symbol} size={14} />
|
|
||||||
<span>{symbol}</span>
|
<span>{symbol}</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
@@ -326,15 +161,6 @@ export default function StockExplainView({
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -347,22 +173,10 @@ export default function StockExplainView({
|
|||||||
selectedHistorySource={selectedHistorySource}
|
selectedHistorySource={selectedHistorySource}
|
||||||
chartModel={chartModel}
|
chartModel={chartModel}
|
||||||
selectedTicker={selectedTicker}
|
selectedTicker={selectedTicker}
|
||||||
onSelectEventDate={setSelectedEventDate}
|
|
||||||
isOpen={isPriceOpen}
|
isOpen={isPriceOpen}
|
||||||
onToggle={() => setIsPriceOpen((prev) => !prev)}
|
onToggle={() => setIsPriceOpen((prev) => !prev)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ExplainSummarySection
|
|
||||||
explainSummary={explainSummary}
|
|
||||||
tickerSignals={tickerSignals}
|
|
||||||
recentMentions={recentMentions}
|
|
||||||
tickerTrades={tickerTrades}
|
|
||||||
tickerNews={tickerNews}
|
|
||||||
selectedSymbol={selectedSymbol}
|
|
||||||
isOpen={isSummaryOpen}
|
|
||||||
onToggle={() => setIsSummaryOpen((prev) => !prev)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ExplainNewsSection
|
<ExplainNewsSection
|
||||||
newsSnapshot={newsSnapshot}
|
newsSnapshot={newsSnapshot}
|
||||||
visibleNewsByCategory={visibleNewsByCategory}
|
visibleNewsByCategory={visibleNewsByCategory}
|
||||||
@@ -378,45 +192,6 @@ export default function StockExplainView({
|
|||||||
onToggle={() => setIsNewsOpen((prev) => !prev)}
|
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
|
<ExplainInsiderSection
|
||||||
insiderTrades={insiderTradesSnapshot?.trades || []}
|
insiderTrades={insiderTradesSnapshot?.trades || []}
|
||||||
selectedSymbol={selectedSymbol}
|
selectedSymbol={selectedSymbol}
|
||||||
@@ -431,50 +206,6 @@ export default function StockExplainView({
|
|||||||
isOpen={isTechnicalOpen}
|
isOpen={isTechnicalOpen}
|
||||||
onToggle={() => setIsTechnicalOpen((prev) => !prev)}
|
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>
|
</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线'}
|
图表说明:{ohlcSeries.length > 1 ? '历史日线K线' : '基于盘中价格点聚合的简化K线'}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 11, color: '#2563eb' }}>蓝点:新闻日期</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>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import React from 'react';
|
|||||||
|
|
||||||
export default function ExplainSummarySection({
|
export default function ExplainSummarySection({
|
||||||
explainSummary,
|
explainSummary,
|
||||||
tickerSignals,
|
|
||||||
recentMentions,
|
|
||||||
tickerTrades,
|
tickerTrades,
|
||||||
tickerNews,
|
tickerNews,
|
||||||
selectedSymbol,
|
selectedSymbol,
|
||||||
@@ -16,7 +14,7 @@ export default function ExplainSummarySection({
|
|||||||
<h2 className="section-title">分析摘要</h2>
|
<h2 className="section-title">分析摘要</h2>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
|
||||||
<div style={{ fontSize: 11, color: '#666666' }}>
|
<div style={{ fontSize: 11, color: '#666666' }}>
|
||||||
基于当前持仓、信号和讨论自动汇总
|
基于当前持仓、成交和新闻自动汇总
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={onToggle}
|
onClick={onToggle}
|
||||||
@@ -55,17 +53,9 @@ export default function ExplainSummarySection({
|
|||||||
|
|
||||||
<div style={{ border: '1px solid #000000', background: '#ffffff', padding: 16 }}>
|
<div style={{ border: '1px solid #000000', background: '#ffffff', padding: 16 }}>
|
||||||
<div style={{ fontSize: 11, color: '#666666', marginBottom: 12, textTransform: 'uppercase', letterSpacing: 1 }}>
|
<div style={{ fontSize: 11, color: '#666666', marginBottom: 12, textTransform: 'uppercase', letterSpacing: 1 }}>
|
||||||
信号密度
|
分析概览
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'grid', gap: 10 }}>
|
<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 }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12 }}>
|
||||||
<span>成交记录</span>
|
<span>成交记录</span>
|
||||||
<strong>{tickerTrades.length}</strong>
|
<strong>{tickerTrades.length}</strong>
|
||||||
@@ -76,7 +66,7 @@ export default function ExplainSummarySection({
|
|||||||
</div>
|
</div>
|
||||||
<div style={{ height: 1, background: '#e0e0e0', margin: '4px 0' }} />
|
<div style={{ height: 1, background: '#e0e0e0', margin: '4px 0' }} />
|
||||||
<div style={{ fontSize: 12, lineHeight: 1.7, color: '#666666' }}>
|
<div style={{ fontSize: 12, lineHeight: 1.7, color: '#666666' }}>
|
||||||
当前分析优先读取已落库的历史记录,缺失时再回退到本次运行中的实时事件。
|
当前分析综合读取信号、成交、新闻与已生成的解释结果。
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,28 +1,16 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { formatDateTime } from '../../utils/formatters';
|
|
||||||
import {
|
import {
|
||||||
aggregatePriceSeriesToCandles,
|
aggregatePriceSeriesToCandles,
|
||||||
buildLinePath,
|
buildLinePath,
|
||||||
eventDateKey,
|
eventDateKey,
|
||||||
flattenFeedMessages,
|
|
||||||
includesTicker,
|
|
||||||
normalizeMentionRow,
|
|
||||||
normalizeNewsRow,
|
normalizeNewsRow,
|
||||||
normalizeNewsTimelineRow,
|
normalizeNewsTimelineRow,
|
||||||
normalizeSignalDirection,
|
normalizeSignalDirection,
|
||||||
normalizeSignalRow,
|
normalizeSignalRow,
|
||||||
normalizeTradeRow,
|
|
||||||
parsePointTime,
|
parsePointTime,
|
||||||
resolveEventCategory,
|
resolveEventCategory
|
||||||
snippetText
|
|
||||||
} from './explainUtils';
|
} from './explainUtils';
|
||||||
|
|
||||||
function tradeSideLabel(value) {
|
|
||||||
if (value === 'LONG') return '做多';
|
|
||||||
if (value === 'SHORT') return '做空';
|
|
||||||
return value || '交易';
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function useExplainModel({
|
export default function useExplainModel({
|
||||||
tickers,
|
tickers,
|
||||||
holdings,
|
holdings,
|
||||||
@@ -55,13 +43,6 @@ export default function useExplainModel({
|
|||||||
[holdings, selectedSymbol]
|
[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 tickerSignals = useMemo(() => {
|
||||||
const snapshotSignals = Array.isArray(explainEventsSnapshot?.signals)
|
const snapshotSignals = Array.isArray(explainEventsSnapshot?.signals)
|
||||||
? explainEventsSnapshot.signals.map((signal, index) => normalizeSignalRow(signal, index)).filter(Boolean)
|
? 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());
|
}).sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
||||||
}, [explainEventsSnapshot, leaderboard, selectedSymbol]);
|
}, [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 tickerNews = useMemo(() => {
|
||||||
const items = Array.isArray(newsSnapshot?.items)
|
const items = Array.isArray(newsSnapshot?.items)
|
||||||
? newsSnapshot.items.map((item, index) => normalizeNewsRow(item, index)).filter(Boolean)
|
? 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);
|
return rows.map((item, index) => normalizeNewsRow(item, index)).filter(Boolean);
|
||||||
}, [newsSnapshot, selectedEventDate]);
|
}, [newsSnapshot, selectedEventDate]);
|
||||||
|
|
||||||
const visibleNews = useMemo(() => {
|
const visibleNews = useMemo(() => tickerNews, [tickerNews]);
|
||||||
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 tickerNewsTimeline = useMemo(() => {
|
const tickerNewsTimeline = useMemo(() => {
|
||||||
const items = Array.isArray(newsSnapshot?.timeline)
|
const items = Array.isArray(newsSnapshot?.timeline)
|
||||||
@@ -215,28 +148,13 @@ export default function useExplainModel({
|
|||||||
return storyCache[keys[keys.length - 1]] || null;
|
return storyCache[keys[keys.length - 1]] || null;
|
||||||
}, [newsSnapshot]);
|
}, [newsSnapshot]);
|
||||||
|
|
||||||
const selectedSimilarDays = useMemo(() => {
|
const selectedNewsFreshness = useMemo(
|
||||||
if (!selectedEventDate) {
|
() => newsSnapshot?.freshness || newsSnapshot?.categoriesFreshness || newsSnapshot?.timelineFreshness || null,
|
||||||
return null;
|
[newsSnapshot]
|
||||||
}
|
);
|
||||||
const similarCache = newsSnapshot?.similarDaysCache;
|
|
||||||
if (!similarCache || typeof similarCache !== 'object') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return similarCache[selectedEventDate] || null;
|
|
||||||
}, [newsSnapshot, selectedEventDate]);
|
|
||||||
|
|
||||||
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 priceColor = selectedTicker?.change > 0 ? '#00C853' : selectedTicker?.change < 0 ? '#FF1744' : '#000000';
|
||||||
const exposureWeight = holding && Number.isFinite(Number(holding.weight)) ? Number(holding.weight) * 100 : null;
|
const exposureWeight = holding && Number.isFinite(Number(holding.weight)) ? Number(holding.weight) * 100 : null;
|
||||||
const recentTrade = tickerTrades[0] || null;
|
|
||||||
|
|
||||||
const ohlcSeries = useMemo(() => {
|
const ohlcSeries = useMemo(() => {
|
||||||
const raw = ohlcHistoryByTicker?.[selectedSymbol];
|
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) : [];
|
return Array.isArray(raw) ? raw.filter((point) => Number.isFinite(Number(point.price))).slice(-60) : [];
|
||||||
}, [priceHistoryByTicker, selectedSymbol]);
|
}, [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 explainTimeline = useMemo(() => {
|
||||||
const signalEvents = tickerSignals.slice(0, 12).map((signal, index) => ({
|
const signalEvents = tickerSignals.slice(0, 12).map((signal, index) => ({
|
||||||
id: `signal-${signal.agentId}-${signal.date}-${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'
|
tone: signal.normalizedDirection === 'bullish' ? 'positive' : signal.normalizedDirection === 'bearish' ? 'negative' : 'neutral'
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const mentionEvents = recentMentions.slice(0, 12).map((message, index) => ({
|
const fallbackTimeline = [...signalEvents]
|
||||||
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]
|
|
||||||
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
|
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
|
||||||
.slice(0, 24)
|
.slice(0, 24)
|
||||||
.map((event) => ({
|
.map((event) => ({
|
||||||
@@ -356,49 +222,7 @@ export default function useExplainModel({
|
|||||||
})
|
})
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
const dbMentionEvents = (Array.isArray(explainEventsSnapshot.events) ? explainEventsSnapshot.events : [])
|
const dbEvents = [...dbSignalEvents]
|
||||||
.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
|
|
||||||
]
|
|
||||||
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
|
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
|
||||||
.slice(0, 24)
|
.slice(0, 24)
|
||||||
.map((event) => ({
|
.map((event) => ({
|
||||||
@@ -408,7 +232,7 @@ export default function useExplainModel({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
return dbEvents.length > 0 ? dbEvents : fallbackTimeline;
|
return dbEvents.length > 0 ? dbEvents : fallbackTimeline;
|
||||||
}, [explainEventsSnapshot, recentMentions, selectedSymbol, tickerSignals, tickerTrades]);
|
}, [explainEventsSnapshot, selectedSymbol, tickerSignals]);
|
||||||
|
|
||||||
const availableEventDates = useMemo(
|
const availableEventDates = useMemo(
|
||||||
() => Array.from(new Set(explainTimeline.map((event) => event.dateKey).filter(Boolean))),
|
() => Array.from(new Set(explainTimeline.map((event) => event.dateKey).filter(Boolean))),
|
||||||
@@ -644,9 +468,6 @@ export default function useExplainModel({
|
|||||||
selectedTicker,
|
selectedTicker,
|
||||||
holding,
|
holding,
|
||||||
tickerSignals,
|
tickerSignals,
|
||||||
signalSummary,
|
|
||||||
tickerTrades,
|
|
||||||
recentMentions,
|
|
||||||
tickerNews,
|
tickerNews,
|
||||||
visibleNews,
|
visibleNews,
|
||||||
newsCategories,
|
newsCategories,
|
||||||
@@ -655,14 +476,10 @@ export default function useExplainModel({
|
|||||||
selectedRangeWindow,
|
selectedRangeWindow,
|
||||||
selectedRangeExplain,
|
selectedRangeExplain,
|
||||||
selectedStory,
|
selectedStory,
|
||||||
selectedSimilarDays,
|
|
||||||
latestSignal,
|
|
||||||
priceColor,
|
priceColor,
|
||||||
exposureWeight,
|
exposureWeight,
|
||||||
recentTrade,
|
|
||||||
ohlcSeries,
|
ohlcSeries,
|
||||||
priceSeries,
|
priceSeries,
|
||||||
explainSummary,
|
|
||||||
explainTimeline,
|
explainTimeline,
|
||||||
availableEventDates,
|
availableEventDates,
|
||||||
eventCategoryCounts,
|
eventCategoryCounts,
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ describe('useExplainModel', () => {
|
|||||||
const { result } = renderHook(() => useExplainModel(buildBaseProps()));
|
const { result } = renderHook(() => useExplainModel(buildBaseProps()));
|
||||||
|
|
||||||
expect(result.current.availableSymbols).toEqual(['AAPL']);
|
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).toHaveLength(1);
|
||||||
expect(result.current.visibleNewsByCategory[0].id).toBe('news-1');
|
expect(result.current.visibleNewsByCategory[0].id).toBe('news-1');
|
||||||
expect(result.current.selectedRangeWindow).toEqual({
|
expect(result.current.selectedRangeWindow).toEqual({
|
||||||
@@ -127,18 +127,12 @@ describe('useExplainModel', () => {
|
|||||||
expect(result.current.selectedRangeExplain).toEqual({
|
expect(result.current.selectedRangeExplain).toEqual({
|
||||||
summary: '区间内主要由财报催化推动。'
|
summary: '区间内主要由财报催化推动。'
|
||||||
});
|
});
|
||||||
expect(result.current.selectedSimilarDays?.items).toHaveLength(1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('builds timeline, counts, and chart markers from explain data', () => {
|
it('builds timeline, counts, and chart markers from explain data', () => {
|
||||||
const { result } = renderHook(() => useExplainModel(buildBaseProps()));
|
const { result } = renderHook(() => useExplainModel(buildBaseProps()));
|
||||||
|
|
||||||
expect(result.current.availableEventDates).toContain('2026-03-10');
|
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.markers.length).toBeGreaterThan(0);
|
||||||
expect(result.current.chartModel.path).toMatch(/^M/);
|
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",
|
"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",
|
"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
|
// Derived asset shortcuts
|
||||||
@@ -54,9 +45,6 @@ export const ASSETS = {
|
|||||||
remeLogo: CDN_ASSETS.companyRoom.reme_logo,
|
remeLogo: CDN_ASSETS.companyRoom.reme_logo,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Stock logos mapping
|
|
||||||
export const STOCK_LOGOS = { ...CDN_ASSETS.stockLogos };
|
|
||||||
|
|
||||||
// Scene dimensions (actual image size)
|
// Scene dimensions (actual image size)
|
||||||
export const SCENE_NATIVE = { width: 1248, height: 832 };
|
export const SCENE_NATIVE = { width: 1248, height: 832 };
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Global CSS Styles for the EvoTraders Platform
|
* Global CSS Styles for the 大时代 Platform
|
||||||
* Terminal-inspired, minimal, monochrome design
|
* Terminal-inspired, minimal, monochrome design
|
||||||
*/
|
*/
|
||||||
export default function GlobalStyles() {
|
export default function GlobalStyles() {
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ build-backend = "setuptools.build_meta"
|
|||||||
[project]
|
[project]
|
||||||
name = "evotraders"
|
name = "evotraders"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "EvoTraders: A self-evolving multi-agent trading system"
|
description = "大时代: A self-evolving multi-agent trading system"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
license = {text = "MIT"}
|
license = {text = "MIT"}
|
||||||
authors = [
|
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"]
|
keywords = ["trading", "ai", "multi-agent", "fintech", "algorithmic-trading"]
|
||||||
classifiers = [
|
classifiers = [
|
||||||
@@ -28,11 +28,16 @@ classifiers = [
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"agentscope>=1.0.8",
|
"agentscope>=1.0.8",
|
||||||
"reme-ai>=0.2.0.4",
|
"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",
|
"rich>=13.6.0",
|
||||||
"websockets>=12.0",
|
"websockets>=12.0",
|
||||||
"websocket-client>=1.6.0",
|
"websocket-client>=1.6.0",
|
||||||
|
"httpx>=0.27.0",
|
||||||
|
"cryptography>=43.0.0",
|
||||||
"python-dotenv>=1.0.0",
|
"python-dotenv>=1.0.0",
|
||||||
|
"PyYAML>=6.0.0",
|
||||||
"finnhub-python>=2.4.25",
|
"finnhub-python>=2.4.25",
|
||||||
"numpy>=1.24.0",
|
"numpy>=1.24.0",
|
||||||
"pandas>=2.0.0",
|
"pandas>=2.0.0",
|
||||||
@@ -42,8 +47,6 @@ dependencies = [
|
|||||||
"typer>=0.12.5",
|
"typer>=0.12.5",
|
||||||
"openai>=2.9.0",
|
"openai>=2.9.0",
|
||||||
"anthropic>=0.20.0",
|
"anthropic>=0.20.0",
|
||||||
"dotenv",
|
|
||||||
"typer",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -63,13 +66,8 @@ Documentation = "https://github.com/agentscope-ai/agentscope-samples/evotraders/
|
|||||||
[project.scripts]
|
[project.scripts]
|
||||||
evotraders = "backend.cli:app"
|
evotraders = "backend.cli:app"
|
||||||
|
|
||||||
[tool.setuptools]
|
[tool.setuptools.packages.find]
|
||||||
packages = ["backend", "backend.agents", "backend.config",
|
include = ["backend*", "shared*"]
|
||||||
"backend.apps",
|
|
||||||
"backend.domains",
|
|
||||||
"backend.data", "backend.llm",
|
|
||||||
"backend.tools", "backend.utils", "backend.services",
|
|
||||||
"backend.explain", "backend.enrich"]
|
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
line-length = 88
|
line-length = 88
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# Policy
|
# 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
|
# Policy
|
||||||
|
|
||||||
Optional run-scoped constraints, limits, or strategy policy.
|
- 在决定数量时考虑可用现金,不要超出现金允许范围
|
||||||
|
- 考虑做空头寸的保证金要求
|
||||||
Respect cash, margin, and portfolio concentration constraints before recording decisions.
|
- 仓位规模相对于组合总资产保持保守
|
||||||
|
- 始终为决策提供清晰理由
|
||||||
|
- 不要输出英文投资报告或英文结论
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# Policy
|
# Policy
|
||||||
|
|
||||||
Optional run-scoped constraints, limits, or strategy policy.
|
- 先量化,再判断,不要只给抽象风险表述
|
||||||
|
- 高严重度风险必须先说
|
||||||
Use available risk tools before issuing the final risk memo.
|
- 最终结论需要明确仓位限制或调整建议
|
||||||
|
- 不要输出英文风险报告或英文摘要
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# Policy
|
# 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
|
# 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
|
# 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
|
# 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
|
# Policy
|
||||||
|
|
||||||
Optional run-scoped constraints, limits, or strategy policy.
|
- 在决定数量时考虑可用现金,不要超出现金允许范围
|
||||||
|
- 考虑做空头寸的保证金要求
|
||||||
Respect cash, margin, and portfolio concentration constraints before recording decisions.
|
- 仓位规模相对于组合总资产保持保守
|
||||||
|
- 始终为决策提供清晰理由
|
||||||
|
- 不要输出英文投资报告或英文结论
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# Policy
|
# Policy
|
||||||
|
|
||||||
Optional run-scoped constraints, limits, or strategy policy.
|
- 先量化,再判断,不要只给抽象风险表述
|
||||||
|
- 高严重度风险必须先说
|
||||||
Use available risk tools before issuing the final risk memo.
|
- 最终结论需要明确仓位限制或调整建议
|
||||||
|
- 不要输出英文风险报告或英文摘要
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# Policy
|
# 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
|
# 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
|
# 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
|
# 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
|
# Policy
|
||||||
|
|
||||||
Optional run-scoped constraints, limits, or strategy policy.
|
- 在决定数量时考虑可用现金,不要超出现金允许范围
|
||||||
|
- 考虑做空头寸的保证金要求
|
||||||
Respect cash, margin, and portfolio concentration constraints before recording decisions.
|
- 仓位规模相对于组合总资产保持保守
|
||||||
|
- 始终为决策提供清晰理由
|
||||||
|
- 不要输出英文投资报告或英文结论
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# Policy
|
# Policy
|
||||||
|
|
||||||
Optional run-scoped constraints, limits, or strategy policy.
|
- 先量化,再判断,不要只给抽象风险表述
|
||||||
|
- 高严重度风险必须先说
|
||||||
Use available risk tools before issuing the final risk memo.
|
- 最终结论需要明确仓位限制或调整建议
|
||||||
|
- 不要输出英文风险报告或英文摘要
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# Policy
|
# 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
|
# 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
|
# 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
|
This repository is in a split-first state: local development now assumes
|
||||||
separate app surfaces and a dedicated WebSocket gateway instead of a single
|
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.runtime_service` | `8003` | Runtime lifecycle APIs under `/api/runtime/*` |
|
||||||
| `backend.apps.openclaw_service` | `8004` | Read-only OpenClaw REST facade |
|
| `backend.apps.openclaw_service` | `8004` | Read-only OpenClaw REST facade |
|
||||||
| Gateway (`backend.main`) | `8765` | WebSocket feed, runtime event stream, legacy/compat orchestration path |
|
| 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
|
## What Runs By Default In Dev
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ That script starts:
|
|||||||
- `trading_service` on `8001`
|
- `trading_service` on `8001`
|
||||||
- `news_service` on `8002`
|
- `news_service` on `8002`
|
||||||
- `runtime_service` on `8003`
|
- `runtime_service` on `8003`
|
||||||
- EvoTraders gateway on `8765`
|
- 大时代 gateway on `8765`
|
||||||
|
|
||||||
It does **not** start `openclaw_service` on `8004`.
|
It does **not** start `openclaw_service` on `8004`.
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import sys
|
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
@@ -245,11 +244,6 @@ class OpenClawWebSocketClient:
|
|||||||
# Build connect params
|
# Build connect params
|
||||||
connect_params = self._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
|
# Send connect request and wait for hello-ok
|
||||||
hello_event = await self._send_request("connect", connect_params, _allow_handshake=True)
|
hello_event = await self._send_request("connect", connect_params, _allow_handshake=True)
|
||||||
self._hello = GatewayHello(
|
self._hello = GatewayHello(
|
||||||
@@ -346,11 +340,6 @@ class OpenClawWebSocketClient:
|
|||||||
"scopes": scopes.split(","),
|
"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
|
return params
|
||||||
|
|
||||||
async def _recv_loop(self) -> None:
|
async def _recv_loop(self) -> None:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""Shared schema package for EvoTraders services."""
|
"""Shared schema package for 大时代 services."""
|
||||||
|
|
||||||
from shared.schema.price import Price, PriceResponse
|
from shared.schema.price import Price, PriceResponse
|
||||||
from shared.schema.financial import (
|
from shared.schema.financial import (
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# EvoTraders Development Startup Script
|
# 大时代 Development Startup Script
|
||||||
# Split-service mode only
|
# Split-service mode only
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
echo "=========================================="
|
echo "=========================================="
|
||||||
echo "EvoTraders Development Environment"
|
echo "大时代 Development Environment"
|
||||||
echo "=========================================="
|
echo "=========================================="
|
||||||
|
|
||||||
# Colors for output
|
# Colors for output
|
||||||
@@ -180,7 +180,7 @@ export OPENCLAW_SERVICE_URL="${OPENCLAW_SERVICE_URL:-http://localhost:18789}"
|
|||||||
check_openclaw_gateway
|
check_openclaw_gateway
|
||||||
|
|
||||||
echo ""
|
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 " agent_service: http://localhost:8000"
|
||||||
echo " runtime_service: http://localhost:8003"
|
echo " runtime_service: http://localhost:8003"
|
||||||
echo " openclaw_gateway: ws://localhost:18789"
|
echo " openclaw_gateway: ws://localhost:18789"
|
||||||
|
|||||||
316
start.sh
Normal file
316
start.sh
Normal file
@@ -0,0 +1,316 @@
|
|||||||
|
#!/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
|
||||||
|
(cd frontend && npm install --legacy-peer-deps && npm run build)
|
||||||
|
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