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