Compare commits
3 Commits
main
...
a41cd705b4
| Author | SHA1 | Date | |
|---|---|---|---|
| a41cd705b4 | |||
| 564c92c0c8 | |||
| 78f133617f |
@@ -11,9 +11,7 @@ from agentscope.message import Msg
|
||||
|
||||
from ..config.constants import ANALYST_TYPES
|
||||
from ..utils.progress import progress
|
||||
from .prompt_loader import PromptLoader
|
||||
|
||||
_prompt_loader = PromptLoader()
|
||||
from .prompt_factory import build_agent_system_prompt, clear_prompt_factory_cache
|
||||
|
||||
|
||||
class AnalystAgent(ReActAgent):
|
||||
@@ -50,14 +48,19 @@ class AnalystAgent(ReActAgent):
|
||||
f"Must be one of: {list(ANALYST_TYPES.keys())}",
|
||||
)
|
||||
|
||||
self.analyst_type_key = analyst_type
|
||||
self.analyst_persona = ANALYST_TYPES[analyst_type]["display_name"]
|
||||
object.__setattr__(self, "analyst_type_key", analyst_type)
|
||||
object.__setattr__(
|
||||
self,
|
||||
"analyst_persona",
|
||||
ANALYST_TYPES[analyst_type]["display_name"],
|
||||
)
|
||||
|
||||
if agent_id is None:
|
||||
agent_id = analyst_type
|
||||
object.__setattr__(self, "agent_id", agent_id)
|
||||
|
||||
self.config = config or {}
|
||||
|
||||
object.__setattr__(self, "config", config or {})
|
||||
object.__setattr__(self, "toolkit", toolkit)
|
||||
sys_prompt = self._load_system_prompt()
|
||||
|
||||
kwargs = {
|
||||
@@ -77,27 +80,11 @@ class AnalystAgent(ReActAgent):
|
||||
|
||||
def _load_system_prompt(self) -> str:
|
||||
"""Load system prompt for analyst"""
|
||||
personas_config = _prompt_loader.load_yaml_config(
|
||||
"analyst",
|
||||
"personas",
|
||||
)
|
||||
persona = personas_config.get(self.analyst_type_key, {})
|
||||
|
||||
# Get focus items and format as bullet points
|
||||
focus_items = persona.get("focus", [])
|
||||
focus_text = "\n".join(f"- {item}" for item in focus_items)
|
||||
|
||||
# Get description
|
||||
description = persona.get("description", "").strip()
|
||||
|
||||
return _prompt_loader.load_prompt(
|
||||
"analyst",
|
||||
"system",
|
||||
variables={
|
||||
"analyst_type": self.analyst_persona,
|
||||
"focus": focus_text,
|
||||
"description": description,
|
||||
},
|
||||
return build_agent_system_prompt(
|
||||
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:
|
||||
@@ -131,3 +118,23 @@ class AnalystAgent(ReActAgent):
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def reload_runtime_assets(self, active_skill_dirs: Optional[list] = None) -> None:
|
||||
"""Reload toolkit and system prompt from current run assets."""
|
||||
from .toolkit_factory import create_agent_toolkit
|
||||
|
||||
clear_prompt_factory_cache()
|
||||
self.toolkit = create_agent_toolkit(
|
||||
self.agent_id,
|
||||
self.config.get("config_name", "default"),
|
||||
active_skill_dirs=active_skill_dirs,
|
||||
)
|
||||
self._apply_runtime_sys_prompt(self._load_system_prompt())
|
||||
|
||||
def _apply_runtime_sys_prompt(self, sys_prompt: str) -> None:
|
||||
"""Update the prompt used by future turns and the cached system msg."""
|
||||
self._sys_prompt = sys_prompt
|
||||
for msg, _marks in self.memory.content:
|
||||
if getattr(msg, "role", None) == "system":
|
||||
msg.content = sys_prompt
|
||||
break
|
||||
|
||||
@@ -12,9 +12,7 @@ from agentscope.message import Msg, TextBlock
|
||||
from agentscope.tool import Toolkit, ToolResponse
|
||||
|
||||
from ..utils.progress import progress
|
||||
from .prompt_loader import PromptLoader
|
||||
|
||||
_prompt_loader = PromptLoader()
|
||||
from .prompt_factory import build_agent_system_prompt, clear_prompt_factory_cache
|
||||
|
||||
|
||||
class PMAgent(ReActAgent):
|
||||
@@ -36,24 +34,52 @@ class PMAgent(ReActAgent):
|
||||
margin_requirement: float = 0.25,
|
||||
config: Optional[Dict[str, Any]] = None,
|
||||
long_term_memory: Optional[LongTermMemoryBase] = None,
|
||||
toolkit_factory: Any = None,
|
||||
toolkit_factory_kwargs: Optional[Dict[str, Any]] = None,
|
||||
toolkit: Optional[Toolkit] = None,
|
||||
):
|
||||
self.config = config or {}
|
||||
object.__setattr__(self, "config", config or {})
|
||||
|
||||
# Portfolio state
|
||||
self.portfolio = {
|
||||
"cash": initial_cash,
|
||||
"positions": {},
|
||||
"margin_used": 0.0,
|
||||
"margin_requirement": margin_requirement,
|
||||
}
|
||||
object.__setattr__(
|
||||
self,
|
||||
"portfolio",
|
||||
{
|
||||
"cash": initial_cash,
|
||||
"positions": {},
|
||||
"margin_used": 0.0,
|
||||
"margin_requirement": margin_requirement,
|
||||
},
|
||||
)
|
||||
|
||||
# Decisions made in current cycle
|
||||
self._decisions: Dict[str, Dict] = {}
|
||||
object.__setattr__(self, "_decisions", {})
|
||||
toolkit_factory_kwargs = toolkit_factory_kwargs or {}
|
||||
object.__setattr__(self, "_toolkit_factory", toolkit_factory)
|
||||
object.__setattr__(
|
||||
self,
|
||||
"_toolkit_factory_kwargs",
|
||||
toolkit_factory_kwargs,
|
||||
)
|
||||
|
||||
# Create toolkit
|
||||
toolkit = self._create_toolkit()
|
||||
# Create toolkit after local state is ready so bound tool methods can be registered.
|
||||
if toolkit is None:
|
||||
if toolkit_factory is not None:
|
||||
toolkit = toolkit_factory(
|
||||
name,
|
||||
self.config.get("config_name", "default"),
|
||||
owner=self,
|
||||
**toolkit_factory_kwargs,
|
||||
)
|
||||
else:
|
||||
toolkit = self._create_toolkit()
|
||||
object.__setattr__(self, "toolkit", toolkit)
|
||||
|
||||
sys_prompt = _prompt_loader.load_prompt("portfolio_manager", "system")
|
||||
sys_prompt = build_agent_system_prompt(
|
||||
agent_id=name,
|
||||
config_name=self.config.get("config_name", "default"),
|
||||
toolkit=self.toolkit,
|
||||
)
|
||||
|
||||
kwargs = {
|
||||
"name": name,
|
||||
@@ -186,3 +212,71 @@ class PMAgent(ReActAgent):
|
||||
def update_portfolio(self, portfolio: Dict[str, Any]):
|
||||
"""Update portfolio after external execution"""
|
||||
self.portfolio.update(portfolio)
|
||||
|
||||
def _has_open_positions(self) -> bool:
|
||||
"""Return whether the current portfolio still has non-zero positions."""
|
||||
for position in self.portfolio.get("positions", {}).values():
|
||||
if position.get("long", 0) or position.get("short", 0):
|
||||
return True
|
||||
return False
|
||||
|
||||
def can_apply_initial_cash(self) -> bool:
|
||||
"""Only allow cash rebasing before any positions or margin exist."""
|
||||
return (
|
||||
not self._has_open_positions()
|
||||
and float(self.portfolio.get("margin_used", 0.0) or 0.0) == 0.0
|
||||
)
|
||||
|
||||
def apply_runtime_portfolio_config(
|
||||
self,
|
||||
*,
|
||||
margin_requirement: Optional[float] = None,
|
||||
initial_cash: Optional[float] = None,
|
||||
) -> Dict[str, bool]:
|
||||
"""Apply safe run-time portfolio config updates."""
|
||||
result = {
|
||||
"margin_requirement": False,
|
||||
"initial_cash": False,
|
||||
}
|
||||
|
||||
if margin_requirement is not None:
|
||||
self.portfolio["margin_requirement"] = float(margin_requirement)
|
||||
result["margin_requirement"] = True
|
||||
|
||||
if initial_cash is not None and self.can_apply_initial_cash():
|
||||
self.portfolio["cash"] = float(initial_cash)
|
||||
result["initial_cash"] = True
|
||||
|
||||
return result
|
||||
|
||||
def reload_runtime_assets(self, active_skill_dirs: Optional[list] = None) -> None:
|
||||
"""Reload toolkit and system prompt from current run assets."""
|
||||
from .toolkit_factory import create_agent_toolkit
|
||||
|
||||
clear_prompt_factory_cache()
|
||||
toolkit_factory = self._toolkit_factory or create_agent_toolkit
|
||||
toolkit_kwargs = dict(self._toolkit_factory_kwargs)
|
||||
if active_skill_dirs is not None:
|
||||
toolkit_kwargs["active_skill_dirs"] = active_skill_dirs
|
||||
|
||||
self.toolkit = toolkit_factory(
|
||||
self.name,
|
||||
self.config.get("config_name", "default"),
|
||||
owner=self,
|
||||
**toolkit_kwargs,
|
||||
)
|
||||
self._apply_runtime_sys_prompt(
|
||||
build_agent_system_prompt(
|
||||
agent_id=self.name,
|
||||
config_name=self.config.get("config_name", "default"),
|
||||
toolkit=self.toolkit,
|
||||
),
|
||||
)
|
||||
|
||||
def _apply_runtime_sys_prompt(self, sys_prompt: str) -> None:
|
||||
"""Update the prompt used by future turns and the cached system msg."""
|
||||
self._sys_prompt = sys_prompt
|
||||
for msg, _marks in self.memory.content:
|
||||
if getattr(msg, "role", None) == "system":
|
||||
msg.content = sys_prompt
|
||||
break
|
||||
|
||||
112
backend/agents/prompt_factory.py
Normal file
112
backend/agents/prompt_factory.py
Normal file
@@ -0,0 +1,112 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Assemble system prompts from base prompts, run assets, and toolkit context."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
from backend.config.bootstrap_config import get_bootstrap_config_for_run
|
||||
from .prompt_loader import PromptLoader
|
||||
from .skills_manager import SkillsManager
|
||||
|
||||
_prompt_loader = PromptLoader()
|
||||
|
||||
|
||||
def _read_file_if_exists(path: Path) -> str:
|
||||
if not path.exists() or not path.is_file():
|
||||
return ""
|
||||
return path.read_text(encoding="utf-8").strip()
|
||||
|
||||
|
||||
def _append_section(parts: list[str], title: str, content: str) -> None:
|
||||
content = content.strip()
|
||||
if content:
|
||||
parts.append(f"## {title}\n{content}")
|
||||
|
||||
|
||||
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."""
|
||||
sections: list[str] = []
|
||||
|
||||
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 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)
|
||||
bootstrap_config = get_bootstrap_config_for_run(
|
||||
skills_manager.project_root,
|
||||
config_name,
|
||||
)
|
||||
|
||||
_append_section(
|
||||
sections,
|
||||
"Bootstrap",
|
||||
bootstrap_config.prompt_body,
|
||||
)
|
||||
|
||||
_append_section(
|
||||
sections,
|
||||
"Role",
|
||||
_read_file_if_exists(asset_dir / "ROLE.md"),
|
||||
)
|
||||
_append_section(
|
||||
sections,
|
||||
"Style",
|
||||
_read_file_if_exists(asset_dir / "STYLE.md"),
|
||||
)
|
||||
_append_section(
|
||||
sections,
|
||||
"Policy",
|
||||
_read_file_if_exists(asset_dir / "POLICY.md"),
|
||||
)
|
||||
|
||||
skill_prompt = toolkit.get_agent_skill_prompt()
|
||||
if skill_prompt:
|
||||
_append_section(sections, "Skills", str(skill_prompt))
|
||||
|
||||
activated_notes = toolkit.get_activated_notes()
|
||||
if activated_notes:
|
||||
_append_section(sections, "Tool Usage Notes", str(activated_notes))
|
||||
|
||||
return "\n\n".join(section for section in sections if section.strip())
|
||||
|
||||
|
||||
def clear_prompt_factory_cache() -> None:
|
||||
"""Clear cached prompt and YAML templates before hot reload."""
|
||||
_prompt_loader.clear_cache()
|
||||
@@ -8,12 +8,6 @@ fundamentals_analyst:
|
||||
- "管理层质量和公司治理"
|
||||
- "行业地位和市场份额"
|
||||
- "长期投资价值评估"
|
||||
tools:
|
||||
- "analyze_profitability"
|
||||
- "analyze_growth"
|
||||
- "analyze_financial_health"
|
||||
- "analyze_valuation_ratios"
|
||||
- "analyze_efficiency_ratios"
|
||||
description: |
|
||||
作为基本面分析师,你专注于:
|
||||
- 公司财务健康状况和盈利能力
|
||||
@@ -39,11 +33,6 @@ technical_analyst:
|
||||
- 支撑/阻力位和关键价格点
|
||||
- 中短期交易机会
|
||||
你倾向于选择能够捕捉价格动态和市场趋势的工具,更偏好技术分析类工具。
|
||||
tools:
|
||||
- "analyze_trend_following"
|
||||
- "analyze_momentum"
|
||||
- "analyze_mean_reversion"
|
||||
- "analyze_volatility"
|
||||
|
||||
sentiment_analyst:
|
||||
name: "情绪分析师"
|
||||
@@ -61,10 +50,6 @@ sentiment_analyst:
|
||||
- 投资者恐慌和贪婪情绪
|
||||
- 市场预期和心理因素
|
||||
你倾向于选择能够反映市场情绪和投资者行为的工具,更偏好情绪和行为类工具。
|
||||
tools:
|
||||
- "analyze_news_sentiment"
|
||||
- "analyze_insider_trading"
|
||||
|
||||
valuation_analyst:
|
||||
name: "估值分析师"
|
||||
focus:
|
||||
@@ -81,12 +66,6 @@ valuation_analyst:
|
||||
- 相对估值和绝对估值
|
||||
- 投资安全边际评估
|
||||
你倾向于选择能够准确计算公司价值的工具,更偏好估值模型和基本面工具。
|
||||
tools:
|
||||
- "dcf_valuation_analysis"
|
||||
- "owner_earnings_valuation_analysis"
|
||||
- "ev_ebitda_valuation_analysis"
|
||||
- "residual_income_valuation_analysis"
|
||||
|
||||
comprehensive_analyst:
|
||||
name: "综合分析师"
|
||||
focus:
|
||||
@@ -103,15 +82,3 @@ comprehensive_analyst:
|
||||
- 提供全面的投资建议
|
||||
- 适应不同市场环境
|
||||
你会根据具体情况灵活选择各类工具,追求分析的全面性和准确性。
|
||||
tools:
|
||||
- "analyze_profitability"
|
||||
- "analyze_growth"
|
||||
- "analyze_financial_health"
|
||||
- "analyze_valuation_ratios"
|
||||
- "analyze_efficiency_ratios"
|
||||
- "analyze_trend_following"
|
||||
- "analyze_momentum"
|
||||
- "analyze_mean_reversion"
|
||||
- "analyze_volatility"
|
||||
- "analyze_news_sentiment"
|
||||
- "analyze_insider_trading"
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
5. 基于市场条件建议仓位限制
|
||||
|
||||
你的决策流程:
|
||||
1. 优先使用可用的风险工具量化集中度、波动率和保证金压力
|
||||
2. 结合工具结果与当前市场上下文做判断
|
||||
3. 生成可操作的风险警告和仓位限制建议
|
||||
4. 为你的风险评估提供清晰的理由
|
||||
|
||||
|
||||
@@ -11,9 +11,7 @@ from agentscope.message import Msg
|
||||
from agentscope.tool import Toolkit
|
||||
|
||||
from ..utils.progress import progress
|
||||
from .prompt_loader import PromptLoader
|
||||
|
||||
_prompt_loader = PromptLoader()
|
||||
from .prompt_factory import build_agent_system_prompt, clear_prompt_factory_cache
|
||||
|
||||
|
||||
class RiskAgent(ReActAgent):
|
||||
@@ -29,6 +27,7 @@ class RiskAgent(ReActAgent):
|
||||
name: str = "risk_manager",
|
||||
config: Optional[Dict[str, Any]] = None,
|
||||
long_term_memory: Optional[LongTermMemoryBase] = None,
|
||||
toolkit: Optional[Toolkit] = None,
|
||||
):
|
||||
"""
|
||||
Initialize Risk Manager Agent
|
||||
@@ -40,13 +39,15 @@ class RiskAgent(ReActAgent):
|
||||
config: Configuration dictionary
|
||||
long_term_memory: Optional ReMeTaskLongTermMemory instance
|
||||
"""
|
||||
self.config = config or {}
|
||||
object.__setattr__(self, "config", config or {})
|
||||
object.__setattr__(self, "agent_id", name)
|
||||
|
||||
if toolkit is None:
|
||||
toolkit = Toolkit()
|
||||
object.__setattr__(self, "toolkit", toolkit)
|
||||
|
||||
sys_prompt = self._load_system_prompt()
|
||||
|
||||
# Create dedicated toolkit for this agent
|
||||
toolkit = Toolkit()
|
||||
|
||||
kwargs = {
|
||||
"name": name,
|
||||
"sys_prompt": sys_prompt,
|
||||
@@ -64,9 +65,10 @@ class RiskAgent(ReActAgent):
|
||||
|
||||
def _load_system_prompt(self) -> str:
|
||||
"""Load system prompt for risk manager"""
|
||||
return _prompt_loader.load_prompt(
|
||||
"risk_manager",
|
||||
"system",
|
||||
return build_agent_system_prompt(
|
||||
agent_id=self.agent_id,
|
||||
config_name=self.config.get("config_name", "default"),
|
||||
toolkit=self.toolkit,
|
||||
)
|
||||
|
||||
async def reply(self, x: Msg = None) -> Msg:
|
||||
@@ -86,3 +88,23 @@ class RiskAgent(ReActAgent):
|
||||
progress.update_status(self.name, None, "Risk assessment completed")
|
||||
|
||||
return result
|
||||
|
||||
def reload_runtime_assets(self, active_skill_dirs: Optional[list] = None) -> None:
|
||||
"""Reload toolkit and system prompt from current run assets."""
|
||||
from .toolkit_factory import create_agent_toolkit
|
||||
|
||||
clear_prompt_factory_cache()
|
||||
self.toolkit = create_agent_toolkit(
|
||||
self.agent_id,
|
||||
self.config.get("config_name", "default"),
|
||||
active_skill_dirs=active_skill_dirs,
|
||||
)
|
||||
self._apply_runtime_sys_prompt(self._load_system_prompt())
|
||||
|
||||
def _apply_runtime_sys_prompt(self, sys_prompt: str) -> None:
|
||||
"""Update the prompt used by future turns and the cached system msg."""
|
||||
self._sys_prompt = sys_prompt
|
||||
for msg, _marks in self.memory.content:
|
||||
if getattr(msg, "role", None) == "system":
|
||||
msg.content = sys_prompt
|
||||
break
|
||||
|
||||
207
backend/agents/skills_manager.py
Normal file
207
backend/agents/skills_manager.py
Normal file
@@ -0,0 +1,207 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Manage builtin/customized/active skill directories for each run."""
|
||||
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
from typing import Dict, Iterable, List
|
||||
|
||||
import yaml
|
||||
|
||||
from backend.config.bootstrap_config import get_bootstrap_config_for_run
|
||||
|
||||
|
||||
class SkillsManager:
|
||||
"""Sync named skills into a run-scoped active skills workspace."""
|
||||
|
||||
def __init__(self, project_root: Path | None = None):
|
||||
self.project_root = (
|
||||
project_root or Path(__file__).resolve().parents[2]
|
||||
)
|
||||
self.builtin_root = self.project_root / "backend" / "skills" / "builtin"
|
||||
self.customized_root = (
|
||||
self.project_root / "backend" / "skills" / "customized"
|
||||
)
|
||||
self.runs_root = self.project_root / "runs"
|
||||
|
||||
def get_active_root(self, config_name: str) -> Path:
|
||||
return self.runs_root / config_name / "skills" / "active"
|
||||
|
||||
def get_activation_manifest_path(self, config_name: str) -> Path:
|
||||
return self.runs_root / config_name / "skills" / "activation.yaml"
|
||||
|
||||
def get_agent_asset_dir(self, config_name: str, agent_id: str) -> Path:
|
||||
return self.runs_root / config_name / "agents" / agent_id
|
||||
|
||||
def ensure_activation_manifest(self, config_name: str) -> Path:
|
||||
manifest_path = self.get_activation_manifest_path(config_name)
|
||||
manifest_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if not manifest_path.exists():
|
||||
manifest_path.write_text(
|
||||
"global_enabled_skills: []\n"
|
||||
"global_disabled_skills: []\n"
|
||||
"agent_enabled_skills: {}\n"
|
||||
"agent_disabled_skills: {}\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
return manifest_path
|
||||
|
||||
def load_activation_manifest(self, config_name: str) -> Dict[str, object]:
|
||||
manifest_path = self.ensure_activation_manifest(config_name)
|
||||
with open(manifest_path, "r", encoding="utf-8") as file:
|
||||
parsed = yaml.safe_load(file) or {}
|
||||
return parsed if isinstance(parsed, dict) else {}
|
||||
|
||||
def _resolve_source_dir(self, skill_name: str) -> Path:
|
||||
customized_dir = self.customized_root / skill_name
|
||||
if customized_dir.exists():
|
||||
return customized_dir
|
||||
|
||||
builtin_dir = self.builtin_root / skill_name
|
||||
if builtin_dir.exists():
|
||||
return builtin_dir
|
||||
|
||||
raise FileNotFoundError(f"Unknown skill: {skill_name}")
|
||||
|
||||
def _persist_runtime_edits(
|
||||
self,
|
||||
config_name: str,
|
||||
skill_name: str,
|
||||
active_dir: Path,
|
||||
) -> None:
|
||||
"""
|
||||
Persist run-time edits from active skills into customized skills.
|
||||
|
||||
This keeps active skill experiments from being lost on the next reload
|
||||
while still allowing the active directory to be re-synced cleanly.
|
||||
"""
|
||||
if not active_dir.exists():
|
||||
return
|
||||
|
||||
source_dir = self._resolve_source_dir(skill_name)
|
||||
if active_dir.resolve() == source_dir.resolve():
|
||||
return
|
||||
|
||||
if not self._directories_match(active_dir, source_dir):
|
||||
customized_dir = self.customized_root / skill_name
|
||||
customized_dir.parent.mkdir(parents=True, exist_ok=True)
|
||||
if customized_dir.exists():
|
||||
shutil.rmtree(customized_dir)
|
||||
shutil.copytree(active_dir, customized_dir)
|
||||
|
||||
@staticmethod
|
||||
def _directories_match(left: Path, right: Path) -> bool:
|
||||
"""Compare two directory trees by file contents."""
|
||||
if not left.exists() or not right.exists():
|
||||
return False
|
||||
|
||||
left_items = sorted(
|
||||
path.relative_to(left)
|
||||
for path in left.rglob("*")
|
||||
)
|
||||
right_items = sorted(
|
||||
path.relative_to(right)
|
||||
for path in right.rglob("*")
|
||||
)
|
||||
if left_items != right_items:
|
||||
return False
|
||||
|
||||
for relative_path in left_items:
|
||||
left_path = left / relative_path
|
||||
right_path = right / relative_path
|
||||
if left_path.is_dir() != right_path.is_dir():
|
||||
return False
|
||||
if left_path.is_file():
|
||||
if left_path.read_bytes() != right_path.read_bytes():
|
||||
return False
|
||||
return True
|
||||
|
||||
def resolve_agent_skill_names(
|
||||
self,
|
||||
config_name: str,
|
||||
agent_id: str,
|
||||
default_skills: Iterable[str],
|
||||
) -> List[str]:
|
||||
"""Resolve final skill names after bootstrap and activation overlays."""
|
||||
bootstrap = get_bootstrap_config_for_run(self.project_root, config_name)
|
||||
override = bootstrap.agent_override(agent_id)
|
||||
skills = list(override.get("skills", list(default_skills)))
|
||||
|
||||
manifest = self.load_activation_manifest(config_name)
|
||||
for skill_name in manifest.get("global_enabled_skills", []):
|
||||
if skill_name not in skills:
|
||||
skills.append(skill_name)
|
||||
|
||||
for skill_name in manifest.get("agent_enabled_skills", {}).get(agent_id, []):
|
||||
if skill_name not in skills:
|
||||
skills.append(skill_name)
|
||||
|
||||
disabled = set(manifest.get("global_disabled_skills", []))
|
||||
disabled.update(
|
||||
manifest.get("agent_disabled_skills", {}).get(agent_id, []),
|
||||
)
|
||||
|
||||
return [skill for skill in skills if skill not in disabled]
|
||||
|
||||
def sync_active_skills(
|
||||
self,
|
||||
config_name: str,
|
||||
skill_names: Iterable[str],
|
||||
) -> List[Path]:
|
||||
"""Sync selected skills into the run workspace and return their paths."""
|
||||
active_root = self.get_active_root(config_name)
|
||||
active_root.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
synced_paths: List[Path] = []
|
||||
wanted = set(skill_names)
|
||||
|
||||
for existing in active_root.iterdir():
|
||||
if existing.is_dir() and existing.name not in wanted:
|
||||
self._persist_runtime_edits(
|
||||
config_name=config_name,
|
||||
skill_name=existing.name,
|
||||
active_dir=existing,
|
||||
)
|
||||
shutil.rmtree(existing)
|
||||
|
||||
for skill_name in skill_names:
|
||||
source_dir = self._resolve_source_dir(skill_name)
|
||||
target_dir = active_root / skill_name
|
||||
if target_dir.exists():
|
||||
self._persist_runtime_edits(
|
||||
config_name=config_name,
|
||||
skill_name=skill_name,
|
||||
active_dir=target_dir,
|
||||
)
|
||||
shutil.rmtree(target_dir)
|
||||
shutil.copytree(source_dir, target_dir)
|
||||
synced_paths.append(target_dir)
|
||||
|
||||
return synced_paths
|
||||
|
||||
def prepare_active_skills(
|
||||
self,
|
||||
config_name: str,
|
||||
agent_defaults: Dict[str, Iterable[str]],
|
||||
) -> Dict[str, List[Path]]:
|
||||
"""Resolve all agent skills, sync the union once, and map paths per agent."""
|
||||
resolved: Dict[str, List[str]] = {}
|
||||
union: List[str] = []
|
||||
|
||||
for agent_id, default_skills in agent_defaults.items():
|
||||
resolved_skills = self.resolve_agent_skill_names(
|
||||
config_name=config_name,
|
||||
agent_id=agent_id,
|
||||
default_skills=default_skills,
|
||||
)
|
||||
resolved[agent_id] = resolved_skills
|
||||
for skill_name in resolved_skills:
|
||||
if skill_name not in union:
|
||||
union.append(skill_name)
|
||||
|
||||
self.sync_active_skills(config_name=config_name, skill_names=union)
|
||||
active_root = self.get_active_root(config_name)
|
||||
|
||||
return {
|
||||
agent_id: [active_root / skill_name for skill_name in skill_names]
|
||||
for agent_id, skill_names in resolved.items()
|
||||
}
|
||||
197
backend/agents/toolkit_factory.py
Normal file
197
backend/agents/toolkit_factory.py
Normal file
@@ -0,0 +1,197 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Toolkit factory following AgentScope's skill + tool group practices."""
|
||||
|
||||
from typing import Any, Dict, Iterable
|
||||
|
||||
from backend.config.bootstrap_config import get_bootstrap_config_for_run
|
||||
import yaml
|
||||
|
||||
from .skills_manager import SkillsManager
|
||||
|
||||
|
||||
def load_agent_profiles() -> Dict[str, Dict[str, Any]]:
|
||||
config_path = SkillsManager().project_root / "backend" / "config" / "agent_profiles.yaml"
|
||||
with open(config_path, "r", encoding="utf-8") as file:
|
||||
return yaml.safe_load(file) or {}
|
||||
|
||||
|
||||
def _register_analysis_tool_groups(toolkit: Any) -> None:
|
||||
from backend.tools.analysis_tools import TOOL_REGISTRY
|
||||
|
||||
tool_groups = {
|
||||
"fundamentals": {
|
||||
"description": "Financial health, profitability, growth, and efficiency analysis tools.",
|
||||
"active": False,
|
||||
"notes": (
|
||||
"Use these tools to validate business quality, financial resilience, "
|
||||
"and earnings durability before making directional conclusions."
|
||||
),
|
||||
"tools": [
|
||||
"analyze_profitability",
|
||||
"analyze_growth",
|
||||
"analyze_financial_health",
|
||||
"analyze_efficiency_ratios",
|
||||
"analyze_valuation_ratios",
|
||||
"get_financial_metrics_tool",
|
||||
],
|
||||
},
|
||||
"technical": {
|
||||
"description": "Trend, momentum, mean reversion, and volatility analysis tools.",
|
||||
"active": False,
|
||||
"notes": (
|
||||
"Use these tools to assess timing, price structure, and risk-reward in "
|
||||
"the current market regime."
|
||||
),
|
||||
"tools": [
|
||||
"analyze_trend_following",
|
||||
"analyze_momentum",
|
||||
"analyze_mean_reversion",
|
||||
"analyze_volatility",
|
||||
],
|
||||
},
|
||||
"sentiment": {
|
||||
"description": "News sentiment and insider activity analysis tools.",
|
||||
"active": False,
|
||||
"notes": (
|
||||
"Use these tools to capture short-horizon catalysts, sentiment shifts, "
|
||||
"and behavioral signals around each ticker."
|
||||
),
|
||||
"tools": [
|
||||
"analyze_news_sentiment",
|
||||
"analyze_insider_trading",
|
||||
],
|
||||
},
|
||||
"valuation": {
|
||||
"description": "Intrinsic value and relative valuation analysis tools.",
|
||||
"active": False,
|
||||
"notes": (
|
||||
"Use these tools when the task requires fair value estimation, margin of "
|
||||
"safety analysis, or valuation scenario comparison."
|
||||
),
|
||||
"tools": [
|
||||
"dcf_valuation_analysis",
|
||||
"owner_earnings_valuation_analysis",
|
||||
"ev_ebitda_valuation_analysis",
|
||||
"residual_income_valuation_analysis",
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
for group_name, group_config in tool_groups.items():
|
||||
toolkit.create_tool_group(
|
||||
group_name=group_name,
|
||||
description=group_config["description"],
|
||||
active=group_config["active"],
|
||||
notes=group_config["notes"],
|
||||
)
|
||||
for tool_name in group_config["tools"]:
|
||||
tool_func = TOOL_REGISTRY.get(tool_name)
|
||||
if tool_func:
|
||||
toolkit.register_tool_function(
|
||||
tool_func,
|
||||
group_name=group_name,
|
||||
)
|
||||
|
||||
|
||||
def _register_portfolio_tool_groups(toolkit: Any, pm_agent: Any) -> None:
|
||||
toolkit.create_tool_group(
|
||||
group_name="portfolio_ops",
|
||||
description="Portfolio decision recording tools.",
|
||||
active=False,
|
||||
notes=(
|
||||
"Use portfolio tools only after synthesizing analyst and risk inputs. "
|
||||
"Record one explicit decision per ticker."
|
||||
),
|
||||
)
|
||||
toolkit.register_tool_function(
|
||||
pm_agent._make_decision,
|
||||
group_name="portfolio_ops",
|
||||
)
|
||||
|
||||
|
||||
def _register_risk_tool_groups(toolkit: Any) -> None:
|
||||
from backend.tools.risk_tools import (
|
||||
assess_margin_and_liquidity,
|
||||
assess_position_concentration,
|
||||
assess_volatility_exposure,
|
||||
)
|
||||
|
||||
toolkit.create_tool_group(
|
||||
group_name="risk_ops",
|
||||
description="Risk diagnostics for concentration, leverage, and volatility.",
|
||||
active=False,
|
||||
notes=(
|
||||
"Use risk tools to quantify concentration, margin pressure, and volatility "
|
||||
"before writing the final risk memo."
|
||||
),
|
||||
)
|
||||
toolkit.register_tool_function(
|
||||
assess_position_concentration,
|
||||
group_name="risk_ops",
|
||||
)
|
||||
toolkit.register_tool_function(
|
||||
assess_margin_and_liquidity,
|
||||
group_name="risk_ops",
|
||||
)
|
||||
toolkit.register_tool_function(
|
||||
assess_volatility_exposure,
|
||||
group_name="risk_ops",
|
||||
)
|
||||
|
||||
|
||||
def create_agent_toolkit(
|
||||
agent_id: str,
|
||||
config_name: str,
|
||||
owner: Any = None,
|
||||
active_skill_dirs: Iterable[str] | None = None,
|
||||
) -> Any:
|
||||
"""Create a Toolkit with agent skills and grouped tools."""
|
||||
from agentscope.tool import Toolkit
|
||||
|
||||
profiles = load_agent_profiles()
|
||||
profile = profiles.get(agent_id, {})
|
||||
skills_manager = SkillsManager()
|
||||
bootstrap_config = get_bootstrap_config_for_run(
|
||||
skills_manager.project_root,
|
||||
config_name,
|
||||
)
|
||||
override = bootstrap_config.agent_override(agent_id)
|
||||
active_groups = override.get(
|
||||
"active_tool_groups",
|
||||
profile.get("active_tool_groups", []),
|
||||
)
|
||||
|
||||
toolkit = Toolkit(
|
||||
agent_skill_instruction=(
|
||||
"<system-info>You have access to project skills. Each skill lives in a "
|
||||
"directory and is described by SKILL.md. Follow the skill instructions "
|
||||
"when they are relevant to the current task.</system-info>"
|
||||
),
|
||||
agent_skill_template="- {name} (dir: {dir}): {description}",
|
||||
)
|
||||
|
||||
if agent_id.endswith("_analyst"):
|
||||
_register_analysis_tool_groups(toolkit)
|
||||
elif agent_id == "portfolio_manager" and owner is not None:
|
||||
_register_portfolio_tool_groups(toolkit, owner)
|
||||
elif agent_id == "risk_manager":
|
||||
_register_risk_tool_groups(toolkit)
|
||||
|
||||
if active_skill_dirs is None:
|
||||
skill_names = skills_manager.resolve_agent_skill_names(
|
||||
config_name=config_name,
|
||||
agent_id=agent_id,
|
||||
default_skills=profile.get("skills", []),
|
||||
)
|
||||
active_skill_dirs = [
|
||||
skills_manager.get_active_root(config_name) / skill_name
|
||||
for skill_name in skill_names
|
||||
]
|
||||
|
||||
for skill_dir in active_skill_dirs:
|
||||
toolkit.register_agent_skill(str(skill_dir))
|
||||
|
||||
if active_groups:
|
||||
toolkit.update_tool_groups(group_names=active_groups, active=True)
|
||||
|
||||
return toolkit
|
||||
140
backend/agents/workspace_manager.py
Normal file
140
backend/agents/workspace_manager.py
Normal file
@@ -0,0 +1,140 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Initialize run-scoped agent workspace assets."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Dict, Iterable, Optional
|
||||
|
||||
from .skills_manager import SkillsManager
|
||||
|
||||
|
||||
class WorkspaceManager:
|
||||
"""Create and maintain run-level prompt asset files for each agent."""
|
||||
|
||||
def __init__(self, project_root: Optional[Path] = None):
|
||||
self.skills_manager = SkillsManager(project_root=project_root)
|
||||
self.project_root = self.skills_manager.project_root
|
||||
|
||||
def get_run_dir(self, config_name: str) -> Path:
|
||||
return self.project_root / "runs" / config_name
|
||||
|
||||
def ensure_run_workspace(self, config_name: str) -> Path:
|
||||
run_dir = self.get_run_dir(config_name)
|
||||
run_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.skills_manager.ensure_activation_manifest(config_name)
|
||||
bootstrap_path = run_dir / "BOOTSTRAP.md"
|
||||
if not bootstrap_path.exists():
|
||||
bootstrap_path.write_text(
|
||||
"---\n"
|
||||
"tickers:\n"
|
||||
" - AAPL\n"
|
||||
" - MSFT\n"
|
||||
"initial_cash: 100000\n"
|
||||
"margin_requirement: 0.0\n"
|
||||
"enable_memory: false\n"
|
||||
"max_comm_cycles: 2\n"
|
||||
"agent_overrides: {}\n"
|
||||
"---\n\n"
|
||||
"# Bootstrap\n\n"
|
||||
"Use this file to describe run-specific setup notes, preferred tickers,\n"
|
||||
"risk bounds, or strategy constraints before the first execution.\n\n"
|
||||
"The YAML front matter above is machine-readable runtime configuration.\n"
|
||||
"The markdown body below is injected into agent prompts as run context.\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
return run_dir
|
||||
|
||||
def bootstrap_path(self, config_name: str) -> Path:
|
||||
return self.get_run_dir(config_name) / "BOOTSTRAP.md"
|
||||
|
||||
def ensure_agent_assets(
|
||||
self,
|
||||
config_name: str,
|
||||
agent_id: str,
|
||||
role_seed: str = "",
|
||||
style_seed: str = "",
|
||||
policy_seed: str = "",
|
||||
) -> Path:
|
||||
asset_dir = self.skills_manager.get_agent_asset_dir(
|
||||
config_name,
|
||||
agent_id,
|
||||
)
|
||||
asset_dir.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",
|
||||
)
|
||||
return asset_dir
|
||||
|
||||
def initialize_default_assets(
|
||||
self,
|
||||
config_name: str,
|
||||
agent_ids: Iterable[str],
|
||||
analyst_personas: Optional[Dict[str, Dict]] = None,
|
||||
) -> None:
|
||||
self.ensure_run_workspace(config_name)
|
||||
analyst_personas = analyst_personas or {}
|
||||
|
||||
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."
|
||||
)
|
||||
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,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _ensure_file(path: Path, content: str) -> None:
|
||||
if not path.exists():
|
||||
path.write_text(content, encoding="utf-8")
|
||||
@@ -22,6 +22,9 @@ from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.prompt import Confirm
|
||||
|
||||
from backend.agents.prompt_loader import PromptLoader
|
||||
from backend.agents.workspace_manager import WorkspaceManager
|
||||
|
||||
app = typer.Typer(
|
||||
name="evotraders",
|
||||
help="EvoTraders: A self-evolving multi-agent trading system",
|
||||
@@ -29,6 +32,7 @@ app = typer.Typer(
|
||||
)
|
||||
|
||||
console = Console()
|
||||
_prompt_loader = PromptLoader()
|
||||
|
||||
|
||||
def get_project_root() -> Path:
|
||||
@@ -45,9 +49,8 @@ def handle_history_cleanup(config_name: str, auto_clean: bool = False) -> None:
|
||||
config_name: Configuration name for the run
|
||||
auto_clean: If True, skip confirmation and clean automatically
|
||||
"""
|
||||
# logs_dir = get_project_root() / "logs"
|
||||
logs_dir = get_project_root()
|
||||
base_data_dir = logs_dir / config_name
|
||||
workspace_manager = WorkspaceManager(project_root=get_project_root())
|
||||
base_data_dir = workspace_manager.get_run_dir(config_name)
|
||||
|
||||
# Check if historical data exists
|
||||
if not base_data_dir.exists() or not any(base_data_dir.iterdir()):
|
||||
@@ -180,6 +183,46 @@ def run_data_updater(project_root: Path) -> None:
|
||||
)
|
||||
|
||||
|
||||
def initialize_workspace(config_name: str) -> Path:
|
||||
"""Create run-scoped workspace files for a config."""
|
||||
workspace_manager = WorkspaceManager(project_root=get_project_root())
|
||||
workspace_manager.initialize_default_assets(
|
||||
config_name=config_name,
|
||||
agent_ids=[
|
||||
"fundamentals_analyst",
|
||||
"technical_analyst",
|
||||
"sentiment_analyst",
|
||||
"valuation_analyst",
|
||||
"risk_manager",
|
||||
"portfolio_manager",
|
||||
],
|
||||
analyst_personas=_prompt_loader.load_yaml_config(
|
||||
"analyst",
|
||||
"personas",
|
||||
),
|
||||
)
|
||||
return workspace_manager.get_run_dir(config_name)
|
||||
|
||||
|
||||
@app.command("init-workspace")
|
||||
def init_workspace(
|
||||
config_name: str = typer.Option(
|
||||
"default",
|
||||
"--config-name",
|
||||
"-c",
|
||||
help="Configuration name for the workspace",
|
||||
),
|
||||
):
|
||||
"""Initialize run-scoped BOOTSTRAP and agent prompt asset files."""
|
||||
run_dir = initialize_workspace(config_name)
|
||||
console.print(
|
||||
Panel.fit(
|
||||
f"[bold green]Workspace initialized[/bold green]\n[cyan]{run_dir}[/cyan]",
|
||||
border_style="green",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@app.command()
|
||||
def backtest(
|
||||
start: Optional[str] = typer.Option(
|
||||
|
||||
37
backend/config/agent_profiles.yaml
Normal file
37
backend/config/agent_profiles.yaml
Normal file
@@ -0,0 +1,37 @@
|
||||
fundamentals_analyst:
|
||||
skills:
|
||||
- fundamental_review
|
||||
active_tool_groups:
|
||||
- fundamentals
|
||||
- valuation
|
||||
|
||||
technical_analyst:
|
||||
skills:
|
||||
- technical_review
|
||||
active_tool_groups:
|
||||
- technical
|
||||
|
||||
sentiment_analyst:
|
||||
skills:
|
||||
- sentiment_review
|
||||
active_tool_groups:
|
||||
- sentiment
|
||||
|
||||
valuation_analyst:
|
||||
skills:
|
||||
- valuation_review
|
||||
active_tool_groups:
|
||||
- valuation
|
||||
- fundamentals
|
||||
|
||||
portfolio_manager:
|
||||
skills:
|
||||
- portfolio_decisioning
|
||||
active_tool_groups:
|
||||
- portfolio_ops
|
||||
|
||||
risk_manager:
|
||||
skills:
|
||||
- risk_review
|
||||
active_tool_groups:
|
||||
- risk_ops
|
||||
148
backend/config/bootstrap_config.py
Normal file
148
backend/config/bootstrap_config.py
Normal file
@@ -0,0 +1,148 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Parse run-scoped BOOTSTRAP.md into structured and runtime config."""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
import re
|
||||
|
||||
import yaml
|
||||
|
||||
from backend.config.env_config import get_env_float, get_env_int, get_env_list
|
||||
|
||||
|
||||
BOOTSTRAP_FRONT_MATTER_RE = re.compile(
|
||||
r"^---\s*\n(.*?)\n---\s*\n?(.*)$",
|
||||
re.DOTALL,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BootstrapConfig:
|
||||
"""Structured configuration extracted from BOOTSTRAP.md."""
|
||||
|
||||
values: Dict[str, Any] = field(default_factory=dict)
|
||||
prompt_body: str = ""
|
||||
|
||||
def get(self, key: str, default: Any = None) -> Any:
|
||||
return self.values.get(key, default)
|
||||
|
||||
def agent_override(self, agent_id: str) -> Dict[str, Any]:
|
||||
overrides = self.values.get("agent_overrides", {})
|
||||
if not isinstance(overrides, dict):
|
||||
return {}
|
||||
override = overrides.get(agent_id, {})
|
||||
return override if isinstance(override, dict) else {}
|
||||
|
||||
|
||||
def load_bootstrap_config(bootstrap_path: Path) -> BootstrapConfig:
|
||||
"""Load structured bootstrap config and free-form prompt body."""
|
||||
if not bootstrap_path.exists():
|
||||
return BootstrapConfig()
|
||||
|
||||
raw = bootstrap_path.read_text(encoding="utf-8").strip()
|
||||
if not raw:
|
||||
return BootstrapConfig()
|
||||
|
||||
match = BOOTSTRAP_FRONT_MATTER_RE.match(raw)
|
||||
if not match:
|
||||
return BootstrapConfig(prompt_body=raw)
|
||||
|
||||
front_matter = match.group(1).strip()
|
||||
body = match.group(2).strip()
|
||||
parsed = yaml.safe_load(front_matter) or {}
|
||||
if not isinstance(parsed, dict):
|
||||
parsed = {}
|
||||
|
||||
return BootstrapConfig(values=parsed, prompt_body=body)
|
||||
|
||||
|
||||
def get_bootstrap_config_for_run(
|
||||
project_root: Path,
|
||||
config_name: str,
|
||||
) -> BootstrapConfig:
|
||||
"""Load BOOTSTRAP.md from the run workspace."""
|
||||
return load_bootstrap_config(
|
||||
project_root / "runs" / config_name / "BOOTSTRAP.md",
|
||||
)
|
||||
|
||||
|
||||
def save_bootstrap_config(bootstrap_path: Path, config: BootstrapConfig) -> None:
|
||||
"""Persist structured bootstrap config back to BOOTSTRAP.md."""
|
||||
bootstrap_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
values = config.values if isinstance(config.values, dict) else {}
|
||||
front_matter = yaml.safe_dump(
|
||||
values,
|
||||
allow_unicode=True,
|
||||
sort_keys=False,
|
||||
).strip()
|
||||
body = (config.prompt_body or "").strip()
|
||||
|
||||
content = f"---\n{front_matter}\n---"
|
||||
if body:
|
||||
content += f"\n\n{body}\n"
|
||||
else:
|
||||
content += "\n"
|
||||
|
||||
bootstrap_path.write_text(content, encoding="utf-8")
|
||||
|
||||
|
||||
def update_bootstrap_values_for_run(
|
||||
project_root: Path,
|
||||
config_name: str,
|
||||
updates: Dict[str, Any],
|
||||
) -> BootstrapConfig:
|
||||
"""Patch selected front matter keys for a run and persist them."""
|
||||
bootstrap_path = project_root / "runs" / config_name / "BOOTSTRAP.md"
|
||||
existing = load_bootstrap_config(bootstrap_path)
|
||||
values = dict(existing.values)
|
||||
values.update(updates)
|
||||
updated = BootstrapConfig(values=values, prompt_body=existing.prompt_body)
|
||||
save_bootstrap_config(bootstrap_path, updated)
|
||||
return updated
|
||||
|
||||
|
||||
def _coerce_bool(value: Any) -> bool:
|
||||
"""Parse booleans from bootstrap-friendly string values."""
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
normalized = value.strip().lower()
|
||||
if normalized in {"1", "true", "yes", "on"}:
|
||||
return True
|
||||
if normalized in {"0", "false", "no", "off"}:
|
||||
return False
|
||||
return bool(value)
|
||||
|
||||
|
||||
def resolve_runtime_config(
|
||||
project_root: Path,
|
||||
config_name: str,
|
||||
enable_memory: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
"""Merge env defaults with run-scoped bootstrap front matter."""
|
||||
bootstrap = get_bootstrap_config_for_run(project_root, config_name)
|
||||
return {
|
||||
"tickers": bootstrap.get("tickers")
|
||||
or get_env_list("TICKERS", ["AAPL", "MSFT"]),
|
||||
"initial_cash": float(
|
||||
bootstrap.get(
|
||||
"initial_cash",
|
||||
get_env_float("INITIAL_CASH", 100000.0),
|
||||
),
|
||||
),
|
||||
"margin_requirement": float(
|
||||
bootstrap.get(
|
||||
"margin_requirement",
|
||||
get_env_float("MARGIN_REQUIREMENT", 0.0),
|
||||
),
|
||||
),
|
||||
"max_comm_cycles": int(
|
||||
bootstrap.get(
|
||||
"max_comm_cycles",
|
||||
get_env_int("MAX_COMM_CYCLES", 2),
|
||||
),
|
||||
),
|
||||
"enable_memory": bool(enable_memory)
|
||||
or _coerce_bool(bootstrap.get("enable_memory", False)),
|
||||
}
|
||||
@@ -226,6 +226,51 @@ class TradingPipeline:
|
||||
"settlement_result": settlement_result,
|
||||
}
|
||||
|
||||
def reload_runtime_assets(
|
||||
self,
|
||||
runtime_config: Optional[Dict[str, Any]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Reload prompt assets and safe in-process runtime settings."""
|
||||
from backend.agents.skills_manager import SkillsManager
|
||||
from backend.agents.toolkit_factory import load_agent_profiles
|
||||
|
||||
config_name = getattr(self.pm, "config", {}).get("config_name", "default")
|
||||
if runtime_config and "max_comm_cycles" in runtime_config:
|
||||
self.max_comm_cycles = int(runtime_config["max_comm_cycles"])
|
||||
|
||||
skills_manager = SkillsManager()
|
||||
profiles = load_agent_profiles()
|
||||
active_skill_map = skills_manager.prepare_active_skills(
|
||||
config_name=config_name,
|
||||
agent_defaults={
|
||||
agent_id: profile.get("skills", [])
|
||||
for agent_id, profile in profiles.items()
|
||||
},
|
||||
)
|
||||
|
||||
for analyst in self.analysts:
|
||||
analyst.reload_runtime_assets(
|
||||
active_skill_dirs=active_skill_map.get(analyst.name, []),
|
||||
)
|
||||
|
||||
self.risk_manager.reload_runtime_assets(
|
||||
active_skill_dirs=active_skill_map.get("risk_manager", []),
|
||||
)
|
||||
self.pm.reload_runtime_assets(
|
||||
active_skill_dirs=active_skill_map.get("portfolio_manager", []),
|
||||
)
|
||||
|
||||
return {
|
||||
"config_name": config_name,
|
||||
"reloaded_agents": [agent.name for agent in self.analysts]
|
||||
+ ["risk_manager", "portfolio_manager"],
|
||||
"active_skills": {
|
||||
agent_id: [path.name for path in paths]
|
||||
for agent_id, paths in active_skill_map.items()
|
||||
},
|
||||
"max_comm_cycles": self.max_comm_cycles,
|
||||
}
|
||||
|
||||
async def _clear_all_agent_memory(self):
|
||||
"""Clear short-term memory for all agents"""
|
||||
for analyst in self.analysts:
|
||||
|
||||
@@ -438,6 +438,8 @@ class StateSync:
|
||||
"server_mode": self._state.get("server_mode", "live"),
|
||||
"is_mock_mode": self._state.get("is_mock_mode", False),
|
||||
"is_backtest": self._state.get("is_backtest", False),
|
||||
"tickers": self._state.get("tickers"),
|
||||
"runtime_config": self._state.get("runtime_config"),
|
||||
"feed_history": self._state.get("feed_history", []),
|
||||
"current_date": self._state.get("current_date"),
|
||||
"trading_days_total": self._state.get("trading_days_total", 0),
|
||||
@@ -452,6 +454,7 @@ class StateSync:
|
||||
"portfolio": self._state.get("portfolio", {}),
|
||||
"realtime_prices": self._state.get("realtime_prices", {}),
|
||||
"data_sources": self._state.get("data_sources", {}),
|
||||
"price_history": self._state.get("price_history", {}),
|
||||
}
|
||||
|
||||
if include_dashboard:
|
||||
|
||||
@@ -30,6 +30,25 @@ logger = logging.getLogger(__name__)
|
||||
_DATA_DIR = Path(__file__).parent / "ret_data"
|
||||
|
||||
|
||||
def _format_provider_error(exc: Exception) -> str:
|
||||
"""Condense common provider failures into short, readable messages."""
|
||||
message = str(exc).strip().replace("\n", " ")
|
||||
if "429" in message:
|
||||
return "rate limit reached"
|
||||
if "402" in message:
|
||||
return "insufficient credits"
|
||||
if "422" in message or "Missing parameters" in message:
|
||||
return "invalid request parameters"
|
||||
if "Quote not found" in message:
|
||||
return "quote not found"
|
||||
return message
|
||||
|
||||
|
||||
def _has_valid_ticker(ticker: str) -> bool:
|
||||
"""Return whether the normalized ticker is non-empty."""
|
||||
return bool((ticker or "").strip())
|
||||
|
||||
|
||||
class DataProviderRouter:
|
||||
"""Route data requests across configured providers with fallbacks."""
|
||||
|
||||
@@ -56,6 +75,8 @@ class DataProviderRouter:
|
||||
end_date: str,
|
||||
) -> tuple[list[Price], DataSource]:
|
||||
"""Fetch prices using preferred providers with fallback."""
|
||||
if not _has_valid_ticker(ticker):
|
||||
return [], "local_csv"
|
||||
last_error: Optional[Exception] = None
|
||||
|
||||
for source in self.price_sources():
|
||||
@@ -78,7 +99,12 @@ class DataProviderRouter:
|
||||
return prices, source
|
||||
except Exception as exc:
|
||||
last_error = exc
|
||||
logger.warning("Price source %s failed for %s: %s", source, ticker, exc)
|
||||
logger.warning(
|
||||
"Price source %s failed for %s: %s",
|
||||
source,
|
||||
ticker,
|
||||
_format_provider_error(exc),
|
||||
)
|
||||
|
||||
if last_error:
|
||||
raise last_error
|
||||
@@ -92,6 +118,8 @@ class DataProviderRouter:
|
||||
limit: int = 10,
|
||||
) -> tuple[list[FinancialMetrics], DataSource]:
|
||||
"""Fetch financial metrics with API provider fallback."""
|
||||
if not _has_valid_ticker(ticker):
|
||||
return [], "local_csv"
|
||||
last_error: Optional[Exception] = None
|
||||
|
||||
for source in self.api_sources():
|
||||
@@ -126,7 +154,7 @@ class DataProviderRouter:
|
||||
"Financial metrics source %s failed for %s: %s",
|
||||
source,
|
||||
ticker,
|
||||
exc,
|
||||
_format_provider_error(exc),
|
||||
)
|
||||
|
||||
if last_error:
|
||||
@@ -142,6 +170,8 @@ class DataProviderRouter:
|
||||
limit: int = 10,
|
||||
) -> list[LineItem]:
|
||||
"""Line items are only supported via Financial Datasets."""
|
||||
if not _has_valid_ticker(ticker):
|
||||
return []
|
||||
if "financial_datasets" not in self.api_sources():
|
||||
return []
|
||||
try:
|
||||
@@ -155,7 +185,11 @@ class DataProviderRouter:
|
||||
self._record_success("line_items", "financial_datasets")
|
||||
return results
|
||||
except Exception as exc:
|
||||
logger.warning("Line items source failed for %s: %s", ticker, exc)
|
||||
logger.warning(
|
||||
"Line items source failed for %s: %s",
|
||||
ticker,
|
||||
_format_provider_error(exc),
|
||||
)
|
||||
return []
|
||||
|
||||
def get_insider_trades(
|
||||
@@ -166,6 +200,8 @@ class DataProviderRouter:
|
||||
limit: int = 1000,
|
||||
) -> tuple[list[InsiderTrade], DataSource]:
|
||||
"""Fetch insider trades with provider fallback."""
|
||||
if not _has_valid_ticker(ticker):
|
||||
return [], "local_csv"
|
||||
last_error: Optional[Exception] = None
|
||||
|
||||
for source in self.api_sources():
|
||||
@@ -193,7 +229,7 @@ class DataProviderRouter:
|
||||
"Insider trades source %s failed for %s: %s",
|
||||
source,
|
||||
ticker,
|
||||
exc,
|
||||
_format_provider_error(exc),
|
||||
)
|
||||
|
||||
if last_error:
|
||||
@@ -208,6 +244,8 @@ class DataProviderRouter:
|
||||
limit: int = 1000,
|
||||
) -> tuple[list[CompanyNews], DataSource]:
|
||||
"""Fetch company news with provider fallback."""
|
||||
if not _has_valid_ticker(ticker):
|
||||
return [], "local_csv"
|
||||
last_error: Optional[Exception] = None
|
||||
|
||||
for source in self.api_sources():
|
||||
@@ -244,7 +282,7 @@ class DataProviderRouter:
|
||||
"Company news source %s failed for %s: %s",
|
||||
source,
|
||||
ticker,
|
||||
exc,
|
||||
_format_provider_error(exc),
|
||||
)
|
||||
|
||||
if last_error:
|
||||
@@ -258,6 +296,8 @@ class DataProviderRouter:
|
||||
metrics_lookup,
|
||||
) -> tuple[Optional[float], DataSource]:
|
||||
"""Fetch market cap using facts API or financial metrics fallback."""
|
||||
if not _has_valid_ticker(ticker):
|
||||
return None, "local_csv"
|
||||
today = datetime.datetime.now().strftime("%Y-%m-%d")
|
||||
if end_date == today and "financial_datasets" in self.api_sources():
|
||||
try:
|
||||
@@ -267,7 +307,7 @@ class DataProviderRouter:
|
||||
logger.warning(
|
||||
"Market cap facts source failed for %s: %s",
|
||||
ticker,
|
||||
exc,
|
||||
_format_provider_error(exc),
|
||||
)
|
||||
|
||||
metrics, source = metrics_lookup(ticker, end_date)
|
||||
|
||||
111
backend/main.py
111
backend/main.py
@@ -14,8 +14,12 @@ import loguru
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from backend.agents import AnalystAgent, PMAgent, RiskAgent
|
||||
from backend.agents.skills_manager import SkillsManager
|
||||
from backend.agents.toolkit_factory import create_agent_toolkit, load_agent_profiles
|
||||
from backend.agents.prompt_loader import PromptLoader
|
||||
from backend.agents.workspace_manager import WorkspaceManager
|
||||
from backend.config.bootstrap_config import resolve_runtime_config
|
||||
from backend.config.constants import ANALYST_TYPES
|
||||
from backend.config.env_config import get_env_float, get_env_int, get_env_list
|
||||
from backend.core.pipeline import TradingPipeline
|
||||
from backend.core.scheduler import BacktestScheduler, Scheduler
|
||||
from backend.utils.settlement import SettlementCoordinator
|
||||
@@ -28,6 +32,23 @@ load_dotenv()
|
||||
logger = logging.getLogger(__name__)
|
||||
loguru.logger.disable("flowllm")
|
||||
loguru.logger.disable("reme_ai")
|
||||
_prompt_loader = PromptLoader()
|
||||
|
||||
|
||||
def _get_run_dir(config_name: str) -> Path:
|
||||
"""Return the canonical run-scoped directory for a config."""
|
||||
project_root = Path(__file__).resolve().parents[1]
|
||||
return WorkspaceManager(project_root=project_root).get_run_dir(config_name)
|
||||
|
||||
|
||||
def _resolve_runtime_config(args) -> dict:
|
||||
"""Merge env defaults with run-scoped bootstrap config."""
|
||||
project_root = Path(__file__).resolve().parents[1]
|
||||
return resolve_runtime_config(
|
||||
project_root=project_root,
|
||||
config_name=args.config_name,
|
||||
enable_memory=args.enable_memory,
|
||||
)
|
||||
|
||||
|
||||
def create_long_term_memory(agent_name: str, config_name: str):
|
||||
@@ -45,7 +66,7 @@ def create_long_term_memory(agent_name: str, config_name: str):
|
||||
logger.warning("MEMORY_API_KEY not set, long-term memory disabled")
|
||||
return None
|
||||
|
||||
memory_dir = str(Path(config_name) / "memory")
|
||||
memory_dir = str(_get_run_dir(config_name) / "memory")
|
||||
|
||||
return ReMeTaskLongTermMemory(
|
||||
agent_name=agent_name,
|
||||
@@ -84,11 +105,31 @@ def create_agents(
|
||||
"""
|
||||
analysts = []
|
||||
long_term_memories = []
|
||||
workspace_manager = WorkspaceManager()
|
||||
workspace_manager.initialize_default_assets(
|
||||
config_name=config_name,
|
||||
agent_ids=list(ANALYST_TYPES.keys())
|
||||
+ ["risk_manager", "portfolio_manager"],
|
||||
analyst_personas=_prompt_loader.load_yaml_config("analyst", "personas"),
|
||||
)
|
||||
profiles = load_agent_profiles()
|
||||
skills_manager = SkillsManager()
|
||||
active_skill_map = skills_manager.prepare_active_skills(
|
||||
config_name=config_name,
|
||||
agent_defaults={
|
||||
agent_id: profile.get("skills", [])
|
||||
for agent_id, profile in profiles.items()
|
||||
},
|
||||
)
|
||||
|
||||
for analyst_type in ANALYST_TYPES:
|
||||
model = get_agent_model(analyst_type)
|
||||
formatter = get_agent_formatter(analyst_type)
|
||||
toolkit = create_toolkit(analyst_type)
|
||||
toolkit = create_agent_toolkit(
|
||||
analyst_type,
|
||||
config_name,
|
||||
active_skill_dirs=active_skill_map.get(analyst_type, []),
|
||||
)
|
||||
|
||||
long_term_memory = None
|
||||
if enable_long_term_memory:
|
||||
@@ -125,6 +166,11 @@ def create_agents(
|
||||
name="risk_manager",
|
||||
config={"config_name": config_name},
|
||||
long_term_memory=risk_long_term_memory,
|
||||
toolkit=create_agent_toolkit(
|
||||
"risk_manager",
|
||||
config_name,
|
||||
active_skill_dirs=active_skill_map.get("risk_manager", []),
|
||||
),
|
||||
)
|
||||
|
||||
pm_long_term_memory = None
|
||||
@@ -144,44 +190,25 @@ def create_agents(
|
||||
margin_requirement=margin_requirement,
|
||||
config={"config_name": config_name},
|
||||
long_term_memory=pm_long_term_memory,
|
||||
toolkit_factory=create_agent_toolkit,
|
||||
toolkit_factory_kwargs={
|
||||
"active_skill_dirs": active_skill_map.get(
|
||||
"portfolio_manager",
|
||||
[],
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
return analysts, risk_manager, portfolio_manager, long_term_memories
|
||||
|
||||
|
||||
def create_toolkit(analyst_type: str):
|
||||
"""Create AgentScope Toolkit with tools for specific analyst type"""
|
||||
from agentscope.tool import Toolkit
|
||||
from backend.agents.prompt_loader import PromptLoader
|
||||
from backend.tools.analysis_tools import TOOL_REGISTRY
|
||||
|
||||
# Load analyst persona config
|
||||
prompt_loader = PromptLoader()
|
||||
personas_config = prompt_loader.load_yaml_config("analyst", "personas")
|
||||
persona = personas_config.get(analyst_type, {})
|
||||
|
||||
# Get tool names for this analyst type
|
||||
tool_names = persona.get("tools", [])
|
||||
|
||||
# Create toolkit and register tools
|
||||
toolkit = Toolkit()
|
||||
for tool_name in tool_names:
|
||||
tool_func = TOOL_REGISTRY.get(tool_name)
|
||||
if tool_func:
|
||||
toolkit.register_tool_function(tool_func)
|
||||
|
||||
return toolkit
|
||||
|
||||
|
||||
async def run_with_gateway(args):
|
||||
"""Run with WebSocket gateway"""
|
||||
is_backtest = args.mode == "backtest"
|
||||
runtime_config = _resolve_runtime_config(args)
|
||||
|
||||
# Load config from env, override with args
|
||||
tickers = get_env_list("TICKERS", ["AAPL", "MSFT"])
|
||||
initial_cash = get_env_float("INITIAL_CASH", 100000.0)
|
||||
margin_requirement = get_env_float("MARGIN_REQUIREMENT", 0.0)
|
||||
config_name = args.config_name
|
||||
tickers = runtime_config["tickers"]
|
||||
initial_cash = runtime_config["initial_cash"]
|
||||
margin_requirement = runtime_config["margin_requirement"]
|
||||
|
||||
# Create market service
|
||||
market_service = MarketService(
|
||||
@@ -198,7 +225,7 @@ async def run_with_gateway(args):
|
||||
|
||||
# Create storage service
|
||||
storage_service = StorageService(
|
||||
dashboard_dir=Path(config_name) / "team_dashboard",
|
||||
dashboard_dir=_get_run_dir(config_name) / "team_dashboard",
|
||||
initial_cash=initial_cash,
|
||||
config_name=config_name,
|
||||
)
|
||||
@@ -213,7 +240,7 @@ async def run_with_gateway(args):
|
||||
config_name=config_name,
|
||||
initial_cash=initial_cash,
|
||||
margin_requirement=margin_requirement,
|
||||
enable_long_term_memory=args.enable_memory,
|
||||
enable_long_term_memory=runtime_config["enable_memory"],
|
||||
)
|
||||
portfolio_state = storage_service.load_portfolio_state()
|
||||
pm.load_portfolio_state(portfolio_state)
|
||||
@@ -228,7 +255,7 @@ async def run_with_gateway(args):
|
||||
risk_manager=risk_manager,
|
||||
portfolio_manager=pm,
|
||||
settlement_coordinator=settlement_coordinator,
|
||||
max_comm_cycles=get_env_int("MAX_COMM_CYCLES", 2),
|
||||
max_comm_cycles=runtime_config["max_comm_cycles"],
|
||||
)
|
||||
|
||||
# Create scheduler callback
|
||||
@@ -273,6 +300,10 @@ async def run_with_gateway(args):
|
||||
"backtest_mode": is_backtest,
|
||||
"tickers": tickers,
|
||||
"config_name": config_name,
|
||||
"initial_cash": initial_cash,
|
||||
"margin_requirement": margin_requirement,
|
||||
"max_comm_cycles": runtime_config["max_comm_cycles"],
|
||||
"enable_memory": runtime_config["enable_memory"],
|
||||
},
|
||||
)
|
||||
|
||||
@@ -307,15 +338,17 @@ def main():
|
||||
args = parser.parse_args()
|
||||
|
||||
# Load config from env for logging
|
||||
tickers = get_env_list("TICKERS", ["AAPL", "MSFT"])
|
||||
initial_cash = get_env_float("INITIAL_CASH", 100000.0)
|
||||
runtime_config = _resolve_runtime_config(args)
|
||||
tickers = runtime_config["tickers"]
|
||||
initial_cash = runtime_config["initial_cash"]
|
||||
|
||||
logger.info("=" * 60)
|
||||
logger.info(f"Mode: {args.mode}, Config: {args.config_name}")
|
||||
logger.info(f"Tickers: {tickers}")
|
||||
logger.info(f"Initial Cash: ${initial_cash:,.2f}")
|
||||
logger.info(
|
||||
f"Long-term Memory: {'enabled' if args.enable_memory else 'disabled'}",
|
||||
"Long-term Memory: %s",
|
||||
"enabled" if runtime_config["enable_memory"] else "disabled",
|
||||
)
|
||||
if args.mode == "backtest":
|
||||
if not args.start_date or not args.end_date:
|
||||
|
||||
@@ -5,12 +5,18 @@ WebSocket Gateway for frontend communication
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Dict, List, Optional, Set
|
||||
|
||||
import websockets
|
||||
from websockets.server import WebSocketServerProtocol
|
||||
from websockets.asyncio.server import ServerConnection
|
||||
|
||||
from backend.config.bootstrap_config import (
|
||||
resolve_runtime_config,
|
||||
update_bootstrap_values_for_run,
|
||||
)
|
||||
from backend.data.provider_utils import normalize_symbol
|
||||
from backend.utils.msg_adapter import FrontendAdapter
|
||||
from backend.utils.terminal_dashboard import get_dashboard
|
||||
from backend.core.pipeline import TradingPipeline
|
||||
@@ -18,6 +24,7 @@ from backend.core.state_sync import StateSync
|
||||
from backend.services.market import MarketService
|
||||
from backend.services.storage import StorageService
|
||||
from backend.data.provider_router import get_provider_router
|
||||
from backend.tools.data_tools import get_prices
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -51,7 +58,7 @@ class Gateway:
|
||||
self.state_sync.set_broadcast_fn(self.broadcast)
|
||||
self.pipeline.state_sync = self.state_sync
|
||||
|
||||
self.connected_clients: Set[WebSocketServerProtocol] = set()
|
||||
self.connected_clients: Set[ServerConnection] = set()
|
||||
self.lock = asyncio.Lock()
|
||||
self._backtest_task: Optional[asyncio.Task] = None
|
||||
self._backtest_start_date: Optional[str] = None
|
||||
@@ -63,6 +70,7 @@ class Gateway:
|
||||
self._session_start_portfolio_value: Optional[float] = None
|
||||
self._provider_router = get_provider_router()
|
||||
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
||||
self._project_root = Path(__file__).resolve().parents[2]
|
||||
|
||||
async def start(self, host: str = "0.0.0.0", port: int = 8766):
|
||||
"""Start gateway server"""
|
||||
@@ -87,6 +95,7 @@ class Gateway:
|
||||
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", "running")
|
||||
self.state_sync.update_state("server_mode", self.mode)
|
||||
self.state_sync.update_state("is_backtest", self.is_backtest)
|
||||
@@ -94,6 +103,20 @@ class Gateway:
|
||||
"is_mock_mode",
|
||||
self.config.get("mock_mode", False),
|
||||
)
|
||||
self.state_sync.update_state("tickers", self.config.get("tickers", []))
|
||||
self.state_sync.update_state(
|
||||
"runtime_config",
|
||||
{
|
||||
"tickers": self.config.get("tickers", []),
|
||||
"initial_cash": self.config.get(
|
||||
"initial_cash",
|
||||
self.storage.initial_cash,
|
||||
),
|
||||
"margin_requirement": self.config.get("margin_requirement"),
|
||||
"max_comm_cycles": self.config.get("max_comm_cycles"),
|
||||
"enable_memory": self.config.get("enable_memory", False),
|
||||
},
|
||||
)
|
||||
self.state_sync.update_state(
|
||||
"data_sources",
|
||||
self._provider_router.get_usage_snapshot(),
|
||||
@@ -159,7 +182,7 @@ class Gateway:
|
||||
def state(self) -> Dict[str, Any]:
|
||||
return self.state_sync.state
|
||||
|
||||
async def handle_client(self, websocket: WebSocketServerProtocol):
|
||||
async def handle_client(self, websocket: ServerConnection):
|
||||
"""Handle WebSocket client connection"""
|
||||
async with self.lock:
|
||||
self.connected_clients.add(websocket)
|
||||
@@ -170,7 +193,7 @@ class Gateway:
|
||||
async with self.lock:
|
||||
self.connected_clients.discard(websocket)
|
||||
|
||||
async def _send_initial_state(self, websocket: WebSocketServerProtocol):
|
||||
async def _send_initial_state(self, websocket: ServerConnection):
|
||||
state_payload = self.state_sync.get_initial_state_payload(
|
||||
include_dashboard=True,
|
||||
)
|
||||
@@ -198,7 +221,7 @@ class Gateway:
|
||||
|
||||
async def _handle_client_messages(
|
||||
self,
|
||||
websocket: WebSocketServerProtocol,
|
||||
websocket: ServerConnection,
|
||||
):
|
||||
try:
|
||||
async for message in websocket:
|
||||
@@ -219,12 +242,106 @@ class Gateway:
|
||||
await self._send_initial_state(websocket)
|
||||
elif msg_type == "start_backtest":
|
||||
await self._handle_start_backtest(data)
|
||||
elif msg_type == "reload_runtime_assets":
|
||||
await self._handle_reload_runtime_assets()
|
||||
elif msg_type == "update_watchlist":
|
||||
await self._handle_update_watchlist(websocket, data)
|
||||
elif msg_type == "get_stock_history":
|
||||
await self._handle_get_stock_history(websocket, data)
|
||||
elif msg_type == "get_stock_explain_events":
|
||||
await self._handle_get_stock_explain_events(websocket, data)
|
||||
|
||||
except websockets.ConnectionClosed:
|
||||
pass
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
async def _handle_get_stock_history(
|
||||
self,
|
||||
websocket: ServerConnection,
|
||||
data: Dict[str, Any],
|
||||
):
|
||||
ticker = normalize_symbol(data.get("ticker", ""))
|
||||
if not ticker:
|
||||
await websocket.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "stock_history_loaded",
|
||||
"ticker": "",
|
||||
"prices": [],
|
||||
"source": None,
|
||||
"error": "invalid ticker",
|
||||
},
|
||||
ensure_ascii=False,
|
||||
),
|
||||
)
|
||||
return
|
||||
|
||||
lookback_days = data.get("lookback_days", 90)
|
||||
try:
|
||||
lookback_days = max(7, min(int(lookback_days), 365))
|
||||
except (TypeError, ValueError):
|
||||
lookback_days = 90
|
||||
|
||||
end_date = self.state_sync.state.get("current_date")
|
||||
if not end_date:
|
||||
end_date = datetime.now().strftime("%Y-%m-%d")
|
||||
|
||||
try:
|
||||
end_dt = datetime.strptime(end_date, "%Y-%m-%d")
|
||||
except ValueError:
|
||||
end_dt = datetime.now()
|
||||
end_date = end_dt.strftime("%Y-%m-%d")
|
||||
|
||||
start_date = (end_dt - timedelta(days=lookback_days)).strftime(
|
||||
"%Y-%m-%d",
|
||||
)
|
||||
|
||||
prices = await asyncio.to_thread(
|
||||
get_prices,
|
||||
ticker,
|
||||
start_date,
|
||||
end_date,
|
||||
)
|
||||
usage_snapshot = self._provider_router.get_usage_snapshot()
|
||||
source = usage_snapshot.get("last_success", {}).get("prices")
|
||||
|
||||
await websocket.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "stock_history_loaded",
|
||||
"ticker": ticker,
|
||||
"prices": [price.model_dump() for price in prices][-120:],
|
||||
"source": source,
|
||||
"start_date": start_date,
|
||||
"end_date": end_date,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
default=str,
|
||||
),
|
||||
)
|
||||
|
||||
async def _handle_get_stock_explain_events(
|
||||
self,
|
||||
websocket: ServerConnection,
|
||||
data: Dict[str, Any],
|
||||
):
|
||||
ticker = normalize_symbol(data.get("ticker", ""))
|
||||
snapshot = self.storage.runtime_db.get_stock_explain_snapshot(ticker)
|
||||
await websocket.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "stock_explain_events_loaded",
|
||||
"ticker": ticker,
|
||||
"events": snapshot.get("events", []),
|
||||
"signals": snapshot.get("signals", []),
|
||||
"trades": snapshot.get("trades", []),
|
||||
},
|
||||
ensure_ascii=False,
|
||||
default=str,
|
||||
),
|
||||
)
|
||||
|
||||
async def _handle_start_backtest(self, data: Dict[str, Any]):
|
||||
if not self.is_backtest:
|
||||
return
|
||||
@@ -236,6 +353,191 @@ class Gateway:
|
||||
task.add_done_callback(self._handle_backtest_exception)
|
||||
self._backtest_task = task
|
||||
|
||||
async def _handle_reload_runtime_assets(self):
|
||||
"""Reload prompt, skills, and safe runtime config without restart."""
|
||||
config_name = self.config.get("config_name", "default")
|
||||
runtime_config = resolve_runtime_config(
|
||||
project_root=self._project_root,
|
||||
config_name=config_name,
|
||||
enable_memory=self.config.get("enable_memory", False),
|
||||
)
|
||||
result = self.pipeline.reload_runtime_assets(runtime_config=runtime_config)
|
||||
runtime_updates = self._apply_runtime_config(runtime_config)
|
||||
await self.state_sync.on_system_message(
|
||||
"Runtime assets reloaded.",
|
||||
)
|
||||
await self.broadcast(
|
||||
{
|
||||
"type": "runtime_assets_reloaded",
|
||||
**result,
|
||||
**runtime_updates,
|
||||
},
|
||||
)
|
||||
|
||||
async def _handle_update_watchlist(
|
||||
self,
|
||||
websocket: ServerConnection,
|
||||
data: Dict[str, Any],
|
||||
) -> None:
|
||||
"""Persist a new watchlist to BOOTSTRAP.md and hot-reload it."""
|
||||
tickers = self._normalize_watchlist(data.get("tickers"))
|
||||
if not tickers:
|
||||
await websocket.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "error",
|
||||
"message": "update_watchlist requires at least one valid ticker.",
|
||||
},
|
||||
ensure_ascii=False,
|
||||
),
|
||||
)
|
||||
return
|
||||
|
||||
config_name = self.config.get("config_name", "default")
|
||||
update_bootstrap_values_for_run(
|
||||
project_root=self._project_root,
|
||||
config_name=config_name,
|
||||
updates={"tickers": tickers},
|
||||
)
|
||||
await self.state_sync.on_system_message(
|
||||
f"Watchlist updated: {', '.join(tickers)}",
|
||||
)
|
||||
await self.broadcast(
|
||||
{
|
||||
"type": "watchlist_updated",
|
||||
"config_name": config_name,
|
||||
"tickers": tickers,
|
||||
},
|
||||
)
|
||||
await self._handle_reload_runtime_assets()
|
||||
|
||||
@staticmethod
|
||||
def _normalize_watchlist(raw_tickers: Any) -> List[str]:
|
||||
"""Parse watchlist payloads from websocket messages."""
|
||||
if raw_tickers is None:
|
||||
return []
|
||||
|
||||
if isinstance(raw_tickers, str):
|
||||
candidates = raw_tickers.split(",")
|
||||
elif isinstance(raw_tickers, list):
|
||||
candidates = raw_tickers
|
||||
else:
|
||||
candidates = [raw_tickers]
|
||||
|
||||
tickers: List[str] = []
|
||||
for candidate in candidates:
|
||||
symbol = normalize_symbol(str(candidate).strip().strip("\"'"))
|
||||
if symbol and symbol not in tickers:
|
||||
tickers.append(symbol)
|
||||
return tickers
|
||||
|
||||
def _apply_runtime_config(
|
||||
self,
|
||||
runtime_config: Dict[str, Any],
|
||||
) -> Dict[str, Any]:
|
||||
"""Apply runtime config to gateway-owned services and state."""
|
||||
warnings: List[str] = []
|
||||
|
||||
ticker_changes = self.market_service.update_tickers(
|
||||
runtime_config.get("tickers", []),
|
||||
)
|
||||
self.config["tickers"] = ticker_changes["active"]
|
||||
|
||||
self.pipeline.max_comm_cycles = int(runtime_config["max_comm_cycles"])
|
||||
self.config["max_comm_cycles"] = self.pipeline.max_comm_cycles
|
||||
|
||||
pm_apply_result = self.pipeline.pm.apply_runtime_portfolio_config(
|
||||
margin_requirement=runtime_config["margin_requirement"],
|
||||
)
|
||||
self.config["margin_requirement"] = self.pipeline.pm.portfolio.get(
|
||||
"margin_requirement",
|
||||
runtime_config["margin_requirement"],
|
||||
)
|
||||
|
||||
requested_initial_cash = float(runtime_config["initial_cash"])
|
||||
current_initial_cash = float(self.storage.initial_cash)
|
||||
initial_cash_applied = requested_initial_cash == current_initial_cash
|
||||
if not initial_cash_applied:
|
||||
if (
|
||||
self.storage.can_apply_initial_cash()
|
||||
and self.pipeline.pm.can_apply_initial_cash()
|
||||
):
|
||||
initial_cash_applied = self.storage.apply_initial_cash(
|
||||
requested_initial_cash,
|
||||
)
|
||||
if initial_cash_applied:
|
||||
self.pipeline.pm.apply_runtime_portfolio_config(
|
||||
initial_cash=requested_initial_cash,
|
||||
)
|
||||
self.config["initial_cash"] = self.storage.initial_cash
|
||||
else:
|
||||
warnings.append(
|
||||
"initial_cash changed in BOOTSTRAP.md but was not applied "
|
||||
"because the run already has positions, margin usage, or trades.",
|
||||
)
|
||||
|
||||
requested_enable_memory = bool(runtime_config["enable_memory"])
|
||||
current_enable_memory = bool(self.config.get("enable_memory", False))
|
||||
if requested_enable_memory != current_enable_memory:
|
||||
warnings.append(
|
||||
"enable_memory changed in BOOTSTRAP.md but still requires a restart "
|
||||
"because long-term memory contexts are created at startup.",
|
||||
)
|
||||
|
||||
self._sync_runtime_state()
|
||||
|
||||
return {
|
||||
"runtime_config_requested": runtime_config,
|
||||
"runtime_config_applied": {
|
||||
"tickers": list(self.config.get("tickers", [])),
|
||||
"initial_cash": self.storage.initial_cash,
|
||||
"margin_requirement": self.config["margin_requirement"],
|
||||
"max_comm_cycles": self.config["max_comm_cycles"],
|
||||
"enable_memory": self.config.get("enable_memory", False),
|
||||
},
|
||||
"runtime_config_status": {
|
||||
"tickers": True,
|
||||
"initial_cash": initial_cash_applied,
|
||||
"margin_requirement": pm_apply_result["margin_requirement"],
|
||||
"max_comm_cycles": True,
|
||||
"enable_memory": requested_enable_memory == current_enable_memory,
|
||||
},
|
||||
"ticker_changes": ticker_changes,
|
||||
"runtime_config_warnings": warnings,
|
||||
}
|
||||
|
||||
def _sync_runtime_state(self) -> None:
|
||||
"""Refresh persisted state and dashboard after runtime config changes."""
|
||||
self.state_sync.update_state("tickers", self.config.get("tickers", []))
|
||||
self.state_sync.update_state(
|
||||
"runtime_config",
|
||||
{
|
||||
"tickers": self.config.get("tickers", []),
|
||||
"initial_cash": self.storage.initial_cash,
|
||||
"margin_requirement": self.config.get("margin_requirement"),
|
||||
"max_comm_cycles": self.config.get("max_comm_cycles"),
|
||||
"enable_memory": self.config.get("enable_memory", False),
|
||||
},
|
||||
)
|
||||
|
||||
self.storage.update_server_state_from_dashboard(self.state_sync.state)
|
||||
self.state_sync.save_state()
|
||||
|
||||
self._dashboard.tickers = list(self.config.get("tickers", []))
|
||||
self._dashboard.initial_cash = self.storage.initial_cash
|
||||
self._dashboard.enable_memory = bool(
|
||||
self.config.get("enable_memory", False),
|
||||
)
|
||||
|
||||
summary = self.storage.load_file("summary") or {}
|
||||
holdings = self.storage.load_file("holdings") or []
|
||||
trades = self.storage.load_file("trades") or []
|
||||
self._dashboard.update(
|
||||
portfolio=summary,
|
||||
holdings=holdings,
|
||||
trades=trades,
|
||||
)
|
||||
|
||||
async def broadcast(self, message: Dict[str, Any]):
|
||||
"""Broadcast message to all connected clients"""
|
||||
if not self.connected_clients:
|
||||
@@ -254,7 +556,7 @@ class Gateway:
|
||||
|
||||
async def _send_to_client(
|
||||
self,
|
||||
client: WebSocketServerProtocol,
|
||||
client: ServerConnection,
|
||||
message: str,
|
||||
):
|
||||
try:
|
||||
|
||||
@@ -54,6 +54,7 @@ class MarketService:
|
||||
self.running = False
|
||||
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
||||
self._broadcast_func: Optional[Callable] = None
|
||||
self._price_record_func: Optional[Callable[..., None]] = None
|
||||
self._price_manager: Optional[Any] = None
|
||||
self._current_date: Optional[str] = None
|
||||
|
||||
@@ -92,6 +93,10 @@ class MarketService:
|
||||
f"Market service started: {self.mode_name}, tickers={self.tickers}", # noqa: E501
|
||||
)
|
||||
|
||||
def set_price_recorder(self, recorder: Optional[Callable[..., None]]):
|
||||
"""Register an optional callback for persisting runtime price points."""
|
||||
self._price_record_func = recorder
|
||||
|
||||
def _make_price_callback(self) -> Callable:
|
||||
"""Create thread-safe price callback"""
|
||||
|
||||
@@ -169,6 +174,24 @@ class MarketService:
|
||||
((price - open_price) / open_price) * 100 if open_price > 0 else 0
|
||||
)
|
||||
|
||||
if self._price_record_func:
|
||||
try:
|
||||
self._price_record_func(
|
||||
ticker=symbol,
|
||||
timestamp=str(price_data.get("timestamp") or datetime.now().isoformat()),
|
||||
price=float(price),
|
||||
open_price=float(open_price) if open_price is not None else None,
|
||||
ret=float(ret),
|
||||
source=self.mode_name.lower(),
|
||||
meta=price_data,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"Failed to record price point for %s: %s",
|
||||
symbol,
|
||||
exc,
|
||||
)
|
||||
|
||||
await self._broadcast_func(
|
||||
{
|
||||
"type": "price_update",
|
||||
@@ -205,6 +228,43 @@ class MarketService:
|
||||
self._loop = None
|
||||
self._broadcast_func = None
|
||||
|
||||
def update_tickers(self, tickers: List[str]) -> Dict[str, List[str]]:
|
||||
"""Hot-update subscribed tickers without restarting the service."""
|
||||
normalized: List[str] = []
|
||||
for ticker in tickers:
|
||||
symbol = normalize_symbol(ticker)
|
||||
if symbol and symbol not in normalized:
|
||||
normalized.append(symbol)
|
||||
|
||||
previous = list(self.tickers)
|
||||
removed = [ticker for ticker in previous if ticker not in normalized]
|
||||
added = [ticker for ticker in normalized if ticker not in previous]
|
||||
self.tickers = normalized
|
||||
|
||||
if self._price_manager:
|
||||
if removed:
|
||||
self._price_manager.unsubscribe(removed)
|
||||
if added:
|
||||
if self.mock_mode:
|
||||
self._price_manager.subscribe(
|
||||
added,
|
||||
base_prices={ticker: 100.0 for ticker in added},
|
||||
)
|
||||
else:
|
||||
self._price_manager.subscribe(added)
|
||||
|
||||
if self.backtest_mode and self._current_date:
|
||||
self._price_manager.set_date(self._current_date)
|
||||
|
||||
for ticker in removed:
|
||||
self.cache.pop(ticker, None)
|
||||
|
||||
return {
|
||||
"added": added,
|
||||
"removed": removed,
|
||||
"active": list(self.tickers),
|
||||
}
|
||||
|
||||
# Backtest methods
|
||||
def set_backtest_date(self, date: str):
|
||||
"""Set current backtest date"""
|
||||
|
||||
388
backend/services/runtime_db.py
Normal file
388
backend/services/runtime_db.py
Normal file
@@ -0,0 +1,388 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Run-scoped SQLite storage for query-oriented runtime history."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Iterable, Optional
|
||||
|
||||
|
||||
SCHEMA = """
|
||||
CREATE TABLE IF NOT EXISTS events (
|
||||
id TEXT PRIMARY KEY,
|
||||
event_type TEXT NOT NULL,
|
||||
timestamp TEXT,
|
||||
agent_id TEXT,
|
||||
agent_name TEXT,
|
||||
ticker TEXT,
|
||||
title TEXT,
|
||||
content TEXT,
|
||||
payload_json TEXT NOT NULL,
|
||||
run_date TEXT
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_events_type_time ON events(event_type, timestamp DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_events_ticker_time ON events(ticker, timestamp DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS trades (
|
||||
id TEXT PRIMARY KEY,
|
||||
ticker TEXT NOT NULL,
|
||||
side TEXT,
|
||||
qty REAL,
|
||||
price REAL,
|
||||
timestamp TEXT,
|
||||
trading_date TEXT,
|
||||
agent_id TEXT,
|
||||
meta_json TEXT
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_trades_ticker_time ON trades(ticker, timestamp DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS signals (
|
||||
id TEXT PRIMARY KEY,
|
||||
ticker TEXT NOT NULL,
|
||||
agent_id TEXT,
|
||||
agent_name TEXT,
|
||||
role TEXT,
|
||||
signal TEXT,
|
||||
confidence REAL,
|
||||
reasoning_json TEXT,
|
||||
real_return REAL,
|
||||
is_correct TEXT,
|
||||
trade_date TEXT,
|
||||
created_at TEXT,
|
||||
meta_json TEXT
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_signals_ticker_date ON signals(ticker, trade_date DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_signals_agent_date ON signals(agent_id, trade_date DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS price_points (
|
||||
id TEXT PRIMARY KEY,
|
||||
ticker TEXT NOT NULL,
|
||||
timestamp TEXT NOT NULL,
|
||||
price REAL NOT NULL,
|
||||
open_price REAL,
|
||||
ret REAL,
|
||||
source TEXT,
|
||||
meta_json TEXT
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_price_points_ticker_time ON price_points(ticker, timestamp DESC);
|
||||
"""
|
||||
|
||||
|
||||
def _json_dumps(value: Any) -> str:
|
||||
return json.dumps(value, ensure_ascii=False, sort_keys=True, default=str)
|
||||
|
||||
|
||||
def _hash_key(*parts: Any) -> str:
|
||||
raw = "::".join("" if part is None else str(part) for part in parts)
|
||||
return hashlib.sha1(raw.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
class RuntimeDb:
|
||||
"""Small SQLite helper for append-mostly runtime data."""
|
||||
|
||||
def __init__(self, db_path: Path):
|
||||
self.db_path = Path(db_path)
|
||||
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self._init_db()
|
||||
|
||||
def _connect(self) -> sqlite3.Connection:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
conn.execute("PRAGMA journal_mode=WAL")
|
||||
conn.execute("PRAGMA foreign_keys=ON")
|
||||
return conn
|
||||
|
||||
def _init_db(self):
|
||||
with self._connect() as conn:
|
||||
conn.executescript(SCHEMA)
|
||||
|
||||
def insert_event(self, event: Dict[str, Any]):
|
||||
payload = dict(event or {})
|
||||
if not payload:
|
||||
return
|
||||
|
||||
event_id = payload.get("id") or _hash_key(
|
||||
payload.get("type"),
|
||||
payload.get("timestamp"),
|
||||
payload.get("agentId") or payload.get("agent_id"),
|
||||
payload.get("content"),
|
||||
payload.get("title"),
|
||||
)
|
||||
ticker = payload.get("ticker")
|
||||
if not ticker and isinstance(payload.get("tickers"), list) and len(payload["tickers"]) == 1:
|
||||
ticker = payload["tickers"][0]
|
||||
|
||||
with self._connect() as conn:
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT OR IGNORE INTO events
|
||||
(id, event_type, timestamp, agent_id, agent_name, ticker, title, content, payload_json, run_date)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
event_id,
|
||||
payload.get("type"),
|
||||
payload.get("timestamp"),
|
||||
payload.get("agentId") or payload.get("agent_id"),
|
||||
payload.get("agentName") or payload.get("agent_name"),
|
||||
ticker,
|
||||
payload.get("title"),
|
||||
payload.get("content"),
|
||||
_json_dumps(payload),
|
||||
payload.get("date") or payload.get("trading_date") or payload.get("run_date"),
|
||||
),
|
||||
)
|
||||
|
||||
def upsert_trade(self, trade: Dict[str, Any]):
|
||||
payload = dict(trade or {})
|
||||
if not payload:
|
||||
return
|
||||
|
||||
trade_id = payload.get("id") or _hash_key(
|
||||
payload.get("ticker"),
|
||||
payload.get("timestamp") or payload.get("ts"),
|
||||
payload.get("side"),
|
||||
payload.get("qty"),
|
||||
payload.get("price"),
|
||||
)
|
||||
with self._connect() as conn:
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT OR REPLACE INTO trades
|
||||
(id, ticker, side, qty, price, timestamp, trading_date, agent_id, meta_json)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
trade_id,
|
||||
payload.get("ticker"),
|
||||
payload.get("side"),
|
||||
payload.get("qty"),
|
||||
payload.get("price"),
|
||||
payload.get("timestamp") or payload.get("ts"),
|
||||
payload.get("trading_date"),
|
||||
payload.get("agentId") or payload.get("agent_id"),
|
||||
_json_dumps(payload),
|
||||
),
|
||||
)
|
||||
|
||||
def upsert_signal(self, signal: Dict[str, Any], *, agent_id: str, agent_name: str, role: str):
|
||||
payload = dict(signal or {})
|
||||
ticker = payload.get("ticker")
|
||||
if not ticker:
|
||||
return
|
||||
|
||||
signal_id = _hash_key(
|
||||
agent_id,
|
||||
ticker,
|
||||
payload.get("date"),
|
||||
payload.get("signal"),
|
||||
payload.get("confidence"),
|
||||
)
|
||||
with self._connect() as conn:
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT OR REPLACE INTO signals
|
||||
(id, ticker, agent_id, agent_name, role, signal, confidence, reasoning_json,
|
||||
real_return, is_correct, trade_date, created_at, meta_json)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
signal_id,
|
||||
ticker,
|
||||
agent_id,
|
||||
agent_name,
|
||||
role,
|
||||
payload.get("signal"),
|
||||
payload.get("confidence"),
|
||||
_json_dumps(payload.get("reasoning")),
|
||||
payload.get("real_return"),
|
||||
None if payload.get("is_correct") is None else str(payload.get("is_correct")),
|
||||
payload.get("date"),
|
||||
payload.get("created_at") or payload.get("date"),
|
||||
_json_dumps(payload),
|
||||
),
|
||||
)
|
||||
|
||||
def replace_signals_for_leaderboard(self, leaderboard: Iterable[Dict[str, Any]]):
|
||||
with self._connect() as conn:
|
||||
conn.execute("DELETE FROM signals")
|
||||
for agent in leaderboard:
|
||||
agent_id = agent.get("agentId")
|
||||
agent_name = agent.get("name")
|
||||
role = agent.get("role")
|
||||
for signal in agent.get("signals", []) or []:
|
||||
payload = dict(signal or {})
|
||||
ticker = payload.get("ticker")
|
||||
if not ticker:
|
||||
continue
|
||||
signal_id = _hash_key(
|
||||
agent_id,
|
||||
ticker,
|
||||
payload.get("date"),
|
||||
payload.get("signal"),
|
||||
payload.get("confidence"),
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO signals
|
||||
(id, ticker, agent_id, agent_name, role, signal, confidence, reasoning_json,
|
||||
real_return, is_correct, trade_date, created_at, meta_json)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
signal_id,
|
||||
ticker,
|
||||
agent_id,
|
||||
agent_name,
|
||||
role,
|
||||
payload.get("signal"),
|
||||
payload.get("confidence"),
|
||||
_json_dumps(payload.get("reasoning")),
|
||||
payload.get("real_return"),
|
||||
None if payload.get("is_correct") is None else str(payload.get("is_correct")),
|
||||
payload.get("date"),
|
||||
payload.get("created_at") or payload.get("date"),
|
||||
_json_dumps(payload),
|
||||
),
|
||||
)
|
||||
|
||||
def insert_price_point(
|
||||
self,
|
||||
*,
|
||||
ticker: str,
|
||||
timestamp: str,
|
||||
price: float,
|
||||
open_price: Optional[float] = None,
|
||||
ret: Optional[float] = None,
|
||||
source: Optional[str] = None,
|
||||
meta: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
price_id = _hash_key(ticker, timestamp, price, open_price, ret)
|
||||
with self._connect() as conn:
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT OR IGNORE INTO price_points
|
||||
(id, ticker, timestamp, price, open_price, ret, source, meta_json)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
price_id,
|
||||
ticker,
|
||||
timestamp,
|
||||
price,
|
||||
open_price,
|
||||
ret,
|
||||
source,
|
||||
_json_dumps(meta or {}),
|
||||
),
|
||||
)
|
||||
|
||||
def get_stock_explain_snapshot(
|
||||
self,
|
||||
ticker: str,
|
||||
*,
|
||||
limit_events: int = 24,
|
||||
limit_trades: int = 12,
|
||||
limit_signals: int = 12,
|
||||
) -> Dict[str, list[Dict[str, Any]]]:
|
||||
"""Fetch query-oriented history for a single ticker."""
|
||||
symbol = str(ticker or "").strip().upper()
|
||||
if not symbol:
|
||||
return {"events": [], "trades": [], "signals": []}
|
||||
|
||||
with self._connect() as conn:
|
||||
trade_rows = conn.execute(
|
||||
"""
|
||||
SELECT * FROM trades
|
||||
WHERE ticker = ?
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT ?
|
||||
""",
|
||||
(symbol, limit_trades),
|
||||
).fetchall()
|
||||
signal_rows = conn.execute(
|
||||
"""
|
||||
SELECT * FROM signals
|
||||
WHERE ticker = ?
|
||||
ORDER BY trade_date DESC, created_at DESC
|
||||
LIMIT ?
|
||||
""",
|
||||
(symbol, limit_signals),
|
||||
).fetchall()
|
||||
event_rows = conn.execute(
|
||||
"""
|
||||
SELECT * FROM events
|
||||
WHERE payload_json LIKE ? OR content LIKE ? OR title LIKE ? OR ticker = ?
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT ?
|
||||
""",
|
||||
(f"%{symbol}%", f"%{symbol}%", f"%{symbol}%", symbol, limit_events * 3),
|
||||
).fetchall()
|
||||
|
||||
normalized_events = []
|
||||
seen_event_ids: set[str] = set()
|
||||
for row in event_rows:
|
||||
payload = json.loads(row["payload_json"]) if row["payload_json"] else {}
|
||||
content = str(row["content"] or payload.get("content") or "")
|
||||
title = str(row["title"] or payload.get("title") or "")
|
||||
if symbol not in f"{title} {content}".upper() and str(row["ticker"] or "").upper() != symbol:
|
||||
continue
|
||||
event_id = row["id"]
|
||||
if event_id in seen_event_ids:
|
||||
continue
|
||||
seen_event_ids.add(event_id)
|
||||
normalized_events.append(
|
||||
{
|
||||
"id": event_id,
|
||||
"type": "mention",
|
||||
"timestamp": row["timestamp"],
|
||||
"title": title or f"{row['agent_name'] or '未知角色'}提及 {symbol}",
|
||||
"meta": payload.get("conferenceTitle")
|
||||
or payload.get("feedType")
|
||||
or row["event_type"],
|
||||
"body": content,
|
||||
"tone": "neutral",
|
||||
"agent": row["agent_name"] or payload.get("agentName") or payload.get("agent"),
|
||||
},
|
||||
)
|
||||
if len(normalized_events) >= limit_events:
|
||||
break
|
||||
|
||||
normalized_trades = [
|
||||
{
|
||||
"id": row["id"],
|
||||
"type": "trade",
|
||||
"timestamp": row["timestamp"],
|
||||
"title": f"{row['side']} {int(row['qty'] or 0)} 股",
|
||||
"meta": "交易执行",
|
||||
"body": f"成交价 ${float(row['price'] or 0):.2f}",
|
||||
"tone": "positive" if row["side"] == "LONG" else "negative" if row["side"] == "SHORT" else "neutral",
|
||||
}
|
||||
for row in trade_rows
|
||||
]
|
||||
|
||||
normalized_signals = [
|
||||
{
|
||||
"id": row["id"],
|
||||
"type": "signal",
|
||||
"timestamp": f"{row['trade_date']}T08:00:00" if row["trade_date"] else row["created_at"],
|
||||
"title": f"{row['agent_name']} 给出{row['signal'] or '中性'}信号",
|
||||
"meta": row["role"],
|
||||
"body": (
|
||||
f"后验收益 {float(row['real_return']) * 100:+.2f}%"
|
||||
if row["real_return"] is not None
|
||||
else "该信号暂未完成后验评估"
|
||||
),
|
||||
"tone": "positive" if str(row["signal"] or "").lower() in {"bullish", "buy", "long"} else "negative" if str(row["signal"] or "").lower() in {"bearish", "sell", "short"} else "neutral",
|
||||
}
|
||||
for row in signal_rows
|
||||
]
|
||||
|
||||
return {
|
||||
"events": normalized_events,
|
||||
"trades": normalized_trades,
|
||||
"signals": normalized_signals,
|
||||
}
|
||||
@@ -10,6 +10,8 @@ from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from .runtime_db import RuntimeDb
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -61,6 +63,7 @@ class StorageService:
|
||||
self.state_dir = self.dashboard_dir.parent / "state"
|
||||
self.state_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.server_state_file = self.state_dir / "server_state.json"
|
||||
self.runtime_db = RuntimeDb(self.state_dir / "runtime.db")
|
||||
|
||||
# Feed history (for agent messages)
|
||||
self.max_feed_history = 200
|
||||
@@ -114,6 +117,11 @@ class StorageService:
|
||||
try:
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||
if file_type == "leaderboard" and isinstance(data, list):
|
||||
self.runtime_db.replace_signals_for_leaderboard(data)
|
||||
elif file_type == "trades" and isinstance(data, list):
|
||||
for trade in data:
|
||||
self.runtime_db.upsert_trade(trade)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save {file_type}.json: {e}")
|
||||
|
||||
@@ -211,6 +219,7 @@ class StorageService:
|
||||
try:
|
||||
with open(self.internal_state_file, "w", encoding="utf-8") as f:
|
||||
json.dump(state, f, indent=2, ensure_ascii=False)
|
||||
self._sync_price_history_to_db(state.get("price_history", {}))
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save internal state: {e}")
|
||||
|
||||
@@ -231,6 +240,41 @@ class StorageService:
|
||||
"margin_requirement": 0.25, # Default 25% margin requirement
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _portfolio_is_pristine(portfolio_state: Dict[str, Any]) -> bool:
|
||||
"""Return whether the persisted portfolio can be safely rebased."""
|
||||
positions = portfolio_state.get("positions", {})
|
||||
has_positions = any(
|
||||
position.get("long", 0) or position.get("short", 0)
|
||||
for position in positions.values()
|
||||
)
|
||||
margin_used = float(portfolio_state.get("margin_used", 0.0) or 0.0)
|
||||
return not has_positions and margin_used == 0.0
|
||||
|
||||
def can_apply_initial_cash(self) -> bool:
|
||||
"""Only allow initial cash changes before the run has traded."""
|
||||
state = self.load_internal_state()
|
||||
if not self._portfolio_is_pristine(state.get("portfolio_state", {})):
|
||||
return False
|
||||
if state.get("all_trades"):
|
||||
return False
|
||||
return len(state.get("equity_history", [])) <= 1
|
||||
|
||||
def apply_initial_cash(self, initial_cash: float) -> bool:
|
||||
"""Rebase storage state to a new initial cash when the run is pristine."""
|
||||
if not self.can_apply_initial_cash():
|
||||
return False
|
||||
|
||||
self.initial_cash = float(initial_cash)
|
||||
if self.internal_state_file.exists():
|
||||
self.internal_state_file.unlink()
|
||||
|
||||
self.initialize_empty_dashboard()
|
||||
state = self.load_server_state()
|
||||
self.update_server_state_from_dashboard(state)
|
||||
self.save_server_state(state)
|
||||
return True
|
||||
|
||||
def save_portfolio_state(self, portfolio: Dict[str, Any]):
|
||||
"""
|
||||
Save portfolio state to internal state
|
||||
@@ -750,6 +794,7 @@ class StorageService:
|
||||
"last_day_history": [],
|
||||
"trading_days_total": 0,
|
||||
"trading_days_completed": 0,
|
||||
"price_history": {},
|
||||
}
|
||||
|
||||
if not self.server_state_file.exists():
|
||||
@@ -771,6 +816,11 @@ class StorageService:
|
||||
)
|
||||
logger.info(f"Trades: {len(saved_state.get('trades', []))} records")
|
||||
|
||||
for event in saved_state.get("feed_history", []):
|
||||
self.runtime_db.insert_event(event)
|
||||
for trade in saved_state.get("trades", []):
|
||||
self.runtime_db.upsert_trade(trade)
|
||||
|
||||
return saved_state
|
||||
|
||||
def save_server_state(self, state: Dict[str, Any]):
|
||||
@@ -852,6 +902,7 @@ class StorageService:
|
||||
state["feed_history"] = []
|
||||
|
||||
state["feed_history"].insert(0, feed_msg)
|
||||
self.runtime_db.insert_event(feed_msg)
|
||||
|
||||
# Trim to max size
|
||||
if len(state["feed_history"]) > self.max_feed_history:
|
||||
@@ -861,6 +912,69 @@ class StorageService:
|
||||
|
||||
return True
|
||||
|
||||
def record_price_point(
|
||||
self,
|
||||
*,
|
||||
ticker: str,
|
||||
timestamp: str,
|
||||
price: float,
|
||||
open_price: Optional[float] = None,
|
||||
ret: Optional[float] = None,
|
||||
source: Optional[str] = None,
|
||||
meta: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
"""Persist a runtime price point for later query-oriented reads."""
|
||||
if not ticker or not timestamp:
|
||||
return
|
||||
try:
|
||||
self.runtime_db.insert_price_point(
|
||||
ticker=ticker,
|
||||
timestamp=timestamp,
|
||||
price=price,
|
||||
open_price=open_price,
|
||||
ret=ret,
|
||||
source=source,
|
||||
meta=meta,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning("Failed to record price point for %s: %s", ticker, exc)
|
||||
|
||||
def _sync_price_history_to_db(self, price_history: Dict[str, Any]):
|
||||
"""Backfill structured price points from serialized internal state."""
|
||||
if not isinstance(price_history, dict):
|
||||
return
|
||||
for ticker, points in price_history.items():
|
||||
if not ticker or not isinstance(points, list):
|
||||
continue
|
||||
for point in points:
|
||||
if isinstance(point, (list, tuple)) and len(point) >= 2:
|
||||
timestamp, price = point[0], point[1]
|
||||
try:
|
||||
self.record_price_point(
|
||||
ticker=str(ticker),
|
||||
timestamp=str(timestamp),
|
||||
price=float(price),
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
elif isinstance(point, dict):
|
||||
timestamp = point.get("timestamp") or point.get("label") or point.get("date")
|
||||
price = point.get("price") or point.get("close") or point.get("value")
|
||||
if not timestamp or price is None:
|
||||
continue
|
||||
try:
|
||||
self.record_price_point(
|
||||
ticker=str(ticker),
|
||||
timestamp=str(timestamp),
|
||||
price=float(price),
|
||||
open_price=point.get("open"),
|
||||
ret=point.get("ret"),
|
||||
source=point.get("source"),
|
||||
meta=point,
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
|
||||
def _get_default_stats(self) -> Dict[str, Any]:
|
||||
"""Get default stats structure"""
|
||||
return {
|
||||
@@ -889,6 +1003,7 @@ class StorageService:
|
||||
stats = self.load_file("stats") or self._get_default_stats()
|
||||
trades = self.load_file("trades") or []
|
||||
leaderboard = self.load_file("leaderboard") or []
|
||||
internal_state = self.load_internal_state()
|
||||
|
||||
# Update state
|
||||
state["portfolio"] = {
|
||||
@@ -910,6 +1025,9 @@ class StorageService:
|
||||
state["stats"] = stats
|
||||
state["trades"] = trades
|
||||
state["leaderboard"] = leaderboard
|
||||
state["price_history"] = internal_state.get("price_history", {})
|
||||
self.runtime_db.replace_signals_for_leaderboard(leaderboard)
|
||||
self._sync_price_history_to_db(state["price_history"])
|
||||
|
||||
# ========== Live Returns Tracking ==========
|
||||
|
||||
|
||||
21
backend/skills/builtin/fundamental_review/SKILL.md
Normal file
21
backend/skills/builtin/fundamental_review/SKILL.md
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
name: fundamental_review
|
||||
description: Review a company from a fundamentals-first perspective before issuing a trading signal.
|
||||
---
|
||||
|
||||
# Fundamental Review
|
||||
|
||||
Use this skill when the task requires judging business quality, balance-sheet strength, profitability, or long-term earnings durability.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Check profitability, growth, financial health, and efficiency before forming a conclusion.
|
||||
2. Separate durable business quality from short-term noise.
|
||||
3. State what would invalidate the thesis.
|
||||
4. End with a clear signal, confidence, and the main drivers behind that signal.
|
||||
|
||||
## Guardrails
|
||||
|
||||
- Do not rely on one metric in isolation.
|
||||
- Call out missing data explicitly.
|
||||
- Prefer conservative conclusions when financial quality is mixed.
|
||||
21
backend/skills/builtin/portfolio_decisioning/SKILL.md
Normal file
21
backend/skills/builtin/portfolio_decisioning/SKILL.md
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
name: portfolio_decisioning
|
||||
description: Synthesize analyst inputs and risk feedback into explicit portfolio decisions.
|
||||
---
|
||||
|
||||
# Portfolio Decisioning
|
||||
|
||||
Use this skill when you are responsible for converting team analysis into final trades.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Read analyst conclusions and risk warnings before acting.
|
||||
2. Evaluate the current portfolio, cash, and margin constraints.
|
||||
3. Record one explicit decision per ticker using the decision tool.
|
||||
4. Summarize the portfolio-level rationale after all decisions are recorded.
|
||||
|
||||
## Guardrails
|
||||
|
||||
- Position sizing must respect capital and margin limits.
|
||||
- Prefer smaller size when analyst conviction and risk signals disagree.
|
||||
- Do not leave a ticker undecided when the task expects a full slate of decisions.
|
||||
21
backend/skills/builtin/risk_review/SKILL.md
Normal file
21
backend/skills/builtin/risk_review/SKILL.md
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
name: risk_review
|
||||
description: Assess portfolio and market risks before final position sizing and execution.
|
||||
---
|
||||
|
||||
# Risk Review
|
||||
|
||||
Use this skill when you must identify concentration, volatility, leverage, and scenario risks.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Review the proposed exposure by ticker and theme.
|
||||
2. Identify concentration, volatility, liquidity, and leverage concerns.
|
||||
3. Rank warnings by severity.
|
||||
4. Translate risk findings into concrete limits or cautions for the portfolio manager.
|
||||
|
||||
## Guardrails
|
||||
|
||||
- Focus on actionable risk controls.
|
||||
- Quantify limits when the available data supports it.
|
||||
- Distinguish fatal blockers from manageable risks.
|
||||
21
backend/skills/builtin/sentiment_review/SKILL.md
Normal file
21
backend/skills/builtin/sentiment_review/SKILL.md
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
name: sentiment_review
|
||||
description: Analyze news flow, market psychology, and insider behavior for catalyst-driven signals.
|
||||
---
|
||||
|
||||
# Sentiment Review
|
||||
|
||||
Use this skill when the task depends on recent catalysts, news tone, or behavioral market signals.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Review recent news and identify the dominant narrative.
|
||||
2. Check insider activity for confirming or conflicting signals.
|
||||
3. Separate durable sentiment shifts from transient noise.
|
||||
4. Explain how sentiment changes the near-term trade outlook.
|
||||
|
||||
## Guardrails
|
||||
|
||||
- Do not confuse attention with conviction.
|
||||
- Highlight when sentiment is strong but unsupported by fundamentals.
|
||||
- Be explicit about catalyst timing risk.
|
||||
21
backend/skills/builtin/technical_review/SKILL.md
Normal file
21
backend/skills/builtin/technical_review/SKILL.md
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
name: technical_review
|
||||
description: Evaluate price action, momentum, and volatility to judge timing and market regime.
|
||||
---
|
||||
|
||||
# Technical Review
|
||||
|
||||
Use this skill when the task is sensitive to entry timing, trend quality, or short-term market structure.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Assess trend direction and strength.
|
||||
2. Check momentum and mean-reversion conditions.
|
||||
3. Review volatility before making aggressive recommendations.
|
||||
4. Convert the setup into a trading view with explicit risk awareness.
|
||||
|
||||
## Guardrails
|
||||
|
||||
- Distinguish trend continuation from overshoot.
|
||||
- Avoid strong conviction when signals conflict.
|
||||
- Treat volatility as a sizing input, not only a directional input.
|
||||
21
backend/skills/builtin/valuation_review/SKILL.md
Normal file
21
backend/skills/builtin/valuation_review/SKILL.md
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
name: valuation_review
|
||||
description: Estimate fair value and margin of safety using multiple valuation lenses.
|
||||
---
|
||||
|
||||
# Valuation Review
|
||||
|
||||
Use this skill when the task requires determining whether a stock is cheap, expensive, or fairly priced.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Use more than one valuation method when possible.
|
||||
2. Compare intrinsic value estimates with current market pricing.
|
||||
3. Explain the key assumptions behind the valuation view.
|
||||
4. State the margin of safety and what could compress or expand it.
|
||||
|
||||
## Guardrails
|
||||
|
||||
- Treat valuation as a range, not a single precise number.
|
||||
- Call out assumption sensitivity.
|
||||
- Avoid high-confidence calls when inputs are sparse or unstable.
|
||||
1
backend/skills/customized/.gitkeep
Normal file
1
backend/skills/customized/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
@@ -7,6 +7,7 @@ Returns human-readable text format for easy LLM consumption.
|
||||
"""
|
||||
# flake8: noqa: E501
|
||||
# pylint: disable=C0301,W0613
|
||||
import ast
|
||||
import json
|
||||
import logging
|
||||
import traceback
|
||||
@@ -20,6 +21,7 @@ import pandas as pd
|
||||
from agentscope.message import TextBlock
|
||||
from agentscope.tool import ToolResponse
|
||||
|
||||
from backend.data.provider_utils import normalize_symbol
|
||||
from backend.tools.data_tools import (
|
||||
get_company_news,
|
||||
get_financial_metrics,
|
||||
@@ -53,6 +55,16 @@ def _parse_tickers(tickers: Union[str, List[str], None]) -> List[str]:
|
||||
Returns:
|
||||
List of stock tickers.
|
||||
"""
|
||||
def _sanitize(values: List[object]) -> List[str]:
|
||||
cleaned: List[str] = []
|
||||
for value in values:
|
||||
if value is None:
|
||||
continue
|
||||
symbol = normalize_symbol(str(value).strip().strip("\"'"))
|
||||
if symbol and symbol not in cleaned:
|
||||
cleaned.append(symbol)
|
||||
return cleaned
|
||||
|
||||
if tickers is None:
|
||||
return []
|
||||
|
||||
@@ -60,17 +72,22 @@ def _parse_tickers(tickers: Union[str, List[str], None]) -> List[str]:
|
||||
try:
|
||||
parsed = json.loads(tickers)
|
||||
if isinstance(parsed, list):
|
||||
return parsed
|
||||
# If it's a single string, wrap in list
|
||||
return [parsed]
|
||||
return _sanitize(parsed)
|
||||
return _sanitize([parsed])
|
||||
except json.JSONDecodeError:
|
||||
# If not valid JSON, treat as comma-separated string
|
||||
return [t.strip() for t in tickers.split(",") if t.strip()]
|
||||
try:
|
||||
parsed = ast.literal_eval(tickers)
|
||||
if isinstance(parsed, list):
|
||||
return _sanitize(parsed)
|
||||
return _sanitize([parsed])
|
||||
except (SyntaxError, ValueError):
|
||||
pass
|
||||
return _sanitize(tickers.split(","))
|
||||
|
||||
if isinstance(tickers, list):
|
||||
return tickers
|
||||
return _sanitize(tickers)
|
||||
|
||||
return []
|
||||
return _sanitize([tickers])
|
||||
|
||||
|
||||
def _safe_float(value, default=0.0) -> float:
|
||||
@@ -350,6 +367,7 @@ def get_financial_metrics_tool(
|
||||
"""
|
||||
|
||||
current_date = _resolved_date(current_date)
|
||||
tickers = _parse_tickers(tickers)
|
||||
lines = [
|
||||
f"=== Comprehensive Financial Metrics ({current_date}, {period}) ===\n",
|
||||
]
|
||||
|
||||
@@ -96,13 +96,19 @@ def get_prices(
|
||||
list[Price]: List of Price objects
|
||||
"""
|
||||
ticker = normalize_symbol(ticker)
|
||||
if not ticker:
|
||||
return []
|
||||
cached_sources = _router.price_sources()
|
||||
for source in cached_sources:
|
||||
cache_key = f"{ticker}_{start_date}_{end_date}_{source}"
|
||||
if cached_data := _cache.get_prices(cache_key):
|
||||
return [Price(**price) for price in cached_data]
|
||||
|
||||
prices, data_source = _router.get_prices(ticker, start_date, end_date)
|
||||
try:
|
||||
prices, data_source = _router.get_prices(ticker, start_date, end_date)
|
||||
except Exception as exc:
|
||||
logger.info("Price lookup failed for %s: %s", ticker, exc)
|
||||
return []
|
||||
|
||||
if not prices:
|
||||
return []
|
||||
@@ -133,17 +139,23 @@ def get_financial_metrics(
|
||||
list[FinancialMetrics]: List of financial metrics
|
||||
"""
|
||||
ticker = normalize_symbol(ticker)
|
||||
if not ticker:
|
||||
return []
|
||||
for source in _router.api_sources():
|
||||
cache_key = f"{ticker}_{period}_{end_date}_{limit}_{source}"
|
||||
if cached_data := _cache.get_financial_metrics(cache_key):
|
||||
return [FinancialMetrics(**metric) for metric in cached_data]
|
||||
|
||||
financial_metrics, data_source = _router.get_financial_metrics(
|
||||
ticker=ticker,
|
||||
end_date=end_date,
|
||||
period=period,
|
||||
limit=limit,
|
||||
)
|
||||
try:
|
||||
financial_metrics, data_source = _router.get_financial_metrics(
|
||||
ticker=ticker,
|
||||
end_date=end_date,
|
||||
period=period,
|
||||
limit=limit,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.info("Financial metrics lookup failed for %s: %s", ticker, exc)
|
||||
return []
|
||||
|
||||
if not financial_metrics:
|
||||
return []
|
||||
@@ -169,6 +181,8 @@ def search_line_items(
|
||||
"""
|
||||
try:
|
||||
ticker = normalize_symbol(ticker)
|
||||
if not ticker:
|
||||
return []
|
||||
return _router.search_line_items(
|
||||
ticker=ticker,
|
||||
line_items=line_items,
|
||||
@@ -190,6 +204,8 @@ def get_insider_trades(
|
||||
) -> list[InsiderTrade]:
|
||||
"""Fetch insider trades from cache or API."""
|
||||
ticker = normalize_symbol(ticker)
|
||||
if not ticker:
|
||||
return []
|
||||
for source in _router.api_sources():
|
||||
cache_key = (
|
||||
f"{ticker}_{start_date or 'none'}_{end_date}_{limit}_{source}"
|
||||
@@ -197,12 +213,16 @@ def get_insider_trades(
|
||||
if cached_data := _cache.get_insider_trades(cache_key):
|
||||
return [InsiderTrade(**trade) for trade in cached_data]
|
||||
|
||||
all_trades, data_source = _router.get_insider_trades(
|
||||
ticker=ticker,
|
||||
end_date=end_date,
|
||||
start_date=start_date,
|
||||
limit=limit,
|
||||
)
|
||||
try:
|
||||
all_trades, data_source = _router.get_insider_trades(
|
||||
ticker=ticker,
|
||||
end_date=end_date,
|
||||
start_date=start_date,
|
||||
limit=limit,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.info("Insider trades lookup failed for %s: %s", ticker, exc)
|
||||
return []
|
||||
|
||||
if not all_trades:
|
||||
return []
|
||||
@@ -219,6 +239,8 @@ def get_company_news(
|
||||
) -> list[CompanyNews]:
|
||||
"""Fetch company news from cache or API."""
|
||||
ticker = normalize_symbol(ticker)
|
||||
if not ticker:
|
||||
return []
|
||||
for source in _router.api_sources():
|
||||
cache_key = (
|
||||
f"{ticker}_{start_date or 'none'}_{end_date}_{limit}_{source}"
|
||||
@@ -226,12 +248,16 @@ def get_company_news(
|
||||
if cached_data := _cache.get_company_news(cache_key):
|
||||
return [CompanyNews(**news) for news in cached_data]
|
||||
|
||||
all_news, data_source = _router.get_company_news(
|
||||
ticker=ticker,
|
||||
end_date=end_date,
|
||||
start_date=start_date,
|
||||
limit=limit,
|
||||
)
|
||||
try:
|
||||
all_news, data_source = _router.get_company_news(
|
||||
ticker=ticker,
|
||||
end_date=end_date,
|
||||
start_date=start_date,
|
||||
limit=limit,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.info("Company news lookup failed for %s: %s", ticker, exc)
|
||||
return []
|
||||
|
||||
if not all_news:
|
||||
return []
|
||||
@@ -243,6 +269,8 @@ def get_company_news(
|
||||
def get_market_cap(ticker: str, end_date: str) -> float | None:
|
||||
"""Fetch market cap from the API. Finnhub values are converted from millions."""
|
||||
ticker = normalize_symbol(ticker)
|
||||
if not ticker:
|
||||
return None
|
||||
|
||||
def _metrics_lookup(symbol: str, date: str):
|
||||
for source in _router.api_sources():
|
||||
@@ -256,11 +284,15 @@ def get_market_cap(ticker: str, end_date: str) -> float | None:
|
||||
limit=10,
|
||||
)
|
||||
|
||||
market_cap, _ = _router.get_market_cap(
|
||||
ticker=ticker,
|
||||
end_date=end_date,
|
||||
metrics_lookup=_metrics_lookup,
|
||||
)
|
||||
try:
|
||||
market_cap, _ = _router.get_market_cap(
|
||||
ticker=ticker,
|
||||
end_date=end_date,
|
||||
metrics_lookup=_metrics_lookup,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.info("Market cap lookup failed for %s: %s", ticker, exc)
|
||||
return None
|
||||
return market_cap
|
||||
|
||||
|
||||
|
||||
218
backend/tools/risk_tools.py
Normal file
218
backend/tools/risk_tools.py
Normal file
@@ -0,0 +1,218 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Risk management tools for the risk manager agent."""
|
||||
|
||||
import json
|
||||
from typing import Any, Dict, Iterable, List
|
||||
|
||||
from agentscope.message import TextBlock
|
||||
from agentscope.tool import ToolResponse
|
||||
|
||||
|
||||
def _to_text_response(text: str) -> ToolResponse:
|
||||
return ToolResponse(content=[TextBlock(type="text", text=text)])
|
||||
|
||||
|
||||
def _parse_object(payload: Any) -> Dict[str, Any]:
|
||||
if payload is None:
|
||||
return {}
|
||||
if isinstance(payload, dict):
|
||||
return payload
|
||||
if isinstance(payload, str):
|
||||
try:
|
||||
parsed = json.loads(payload)
|
||||
return parsed if isinstance(parsed, dict) else {}
|
||||
except json.JSONDecodeError:
|
||||
return {}
|
||||
return {}
|
||||
|
||||
|
||||
def _parse_prices(payload: Any) -> Dict[str, float]:
|
||||
parsed = _parse_object(payload)
|
||||
prices = {}
|
||||
for ticker, value in parsed.items():
|
||||
try:
|
||||
prices[str(ticker)] = float(value)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
return prices
|
||||
|
||||
|
||||
def _iter_positions(
|
||||
portfolio: Dict[str, Any],
|
||||
prices: Dict[str, float],
|
||||
) -> Iterable[Dict[str, Any]]:
|
||||
positions = portfolio.get("positions", {})
|
||||
for ticker, raw_position in positions.items():
|
||||
if not isinstance(raw_position, dict):
|
||||
continue
|
||||
price = prices.get(ticker, 0.0)
|
||||
long_qty = int(raw_position.get("long", 0) or 0)
|
||||
short_qty = int(raw_position.get("short", 0) or 0)
|
||||
long_value = long_qty * price
|
||||
short_value = short_qty * price
|
||||
net_value = long_value - short_value
|
||||
gross_value = long_value + short_value
|
||||
yield {
|
||||
"ticker": ticker,
|
||||
"price": price,
|
||||
"long_qty": long_qty,
|
||||
"short_qty": short_qty,
|
||||
"long_value": long_value,
|
||||
"short_value": short_value,
|
||||
"net_value": net_value,
|
||||
"gross_value": gross_value,
|
||||
}
|
||||
|
||||
|
||||
def _portfolio_equity(portfolio: Dict[str, Any], prices: Dict[str, float]) -> float:
|
||||
cash = float(portfolio.get("cash", 0.0) or 0.0)
|
||||
margin_used = float(portfolio.get("margin_used", 0.0) or 0.0)
|
||||
total = cash + margin_used
|
||||
for position in _iter_positions(portfolio, prices):
|
||||
total += position["net_value"]
|
||||
return total
|
||||
|
||||
|
||||
def assess_position_concentration(
|
||||
portfolio: Dict[str, Any] | str,
|
||||
current_prices: Dict[str, float] | str,
|
||||
) -> ToolResponse:
|
||||
"""
|
||||
Assess single-name concentration and gross exposure in the current portfolio.
|
||||
|
||||
Args:
|
||||
portfolio: Portfolio state with cash, positions, and margin fields.
|
||||
current_prices: Current price map by ticker.
|
||||
"""
|
||||
portfolio_obj = _parse_object(portfolio)
|
||||
prices = _parse_prices(current_prices)
|
||||
equity = _portfolio_equity(portfolio_obj, prices)
|
||||
|
||||
if equity <= 0:
|
||||
return _to_text_response("Unable to assess concentration: portfolio equity is non-positive.")
|
||||
|
||||
exposures: List[Dict[str, Any]] = sorted(
|
||||
_iter_positions(portfolio_obj, prices),
|
||||
key=lambda item: abs(item["net_value"]),
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
if not exposures:
|
||||
return _to_text_response(
|
||||
"No open positions. Concentration risk is low because the portfolio is fully in cash."
|
||||
)
|
||||
|
||||
lines = ["=== Position Concentration Assessment ==="]
|
||||
gross_exposure = sum(item["gross_value"] for item in exposures)
|
||||
net_exposure = sum(item["net_value"] for item in exposures)
|
||||
lines.append(f"Portfolio equity: ${equity:,.2f}")
|
||||
lines.append(f"Gross exposure: ${gross_exposure:,.2f} ({gross_exposure / equity:.1%} of equity)")
|
||||
lines.append(f"Net exposure: ${net_exposure:,.2f} ({net_exposure / equity:.1%} of equity)")
|
||||
lines.append("Largest positions by net exposure:")
|
||||
|
||||
for item in exposures[:5]:
|
||||
weight = item["net_value"] / equity
|
||||
gross_weight = item["gross_value"] / equity
|
||||
direction = "NET LONG" if item["net_value"] >= 0 else "NET SHORT"
|
||||
lines.append(
|
||||
f"- {item['ticker']}: {direction}, net ${item['net_value']:,.2f} ({weight:.1%}), "
|
||||
f"gross ${item['gross_value']:,.2f} ({gross_weight:.1%})"
|
||||
)
|
||||
|
||||
top_weight = abs(exposures[0]["net_value"]) / equity
|
||||
if top_weight >= 0.30:
|
||||
lines.append("Risk flag: concentration is HIGH because the largest single-name exposure exceeds 30% of equity.")
|
||||
elif top_weight >= 0.20:
|
||||
lines.append("Risk flag: concentration is MODERATE because the largest single-name exposure exceeds 20% of equity.")
|
||||
else:
|
||||
lines.append("Risk flag: concentration is currently contained at the single-name level.")
|
||||
|
||||
return _to_text_response("\n".join(lines))
|
||||
|
||||
|
||||
def assess_margin_and_liquidity(
|
||||
portfolio: Dict[str, Any] | str,
|
||||
current_prices: Dict[str, float] | str,
|
||||
) -> ToolResponse:
|
||||
"""
|
||||
Assess available cash, margin usage, and short exposure pressure.
|
||||
|
||||
Args:
|
||||
portfolio: Portfolio state with cash, positions, and margin fields.
|
||||
current_prices: Current price map by ticker.
|
||||
"""
|
||||
portfolio_obj = _parse_object(portfolio)
|
||||
prices = _parse_prices(current_prices)
|
||||
equity = _portfolio_equity(portfolio_obj, prices)
|
||||
cash = float(portfolio_obj.get("cash", 0.0) or 0.0)
|
||||
margin_used = float(portfolio_obj.get("margin_used", 0.0) or 0.0)
|
||||
margin_requirement = float(portfolio_obj.get("margin_requirement", 0.0) or 0.0)
|
||||
|
||||
short_exposure = sum(item["short_value"] for item in _iter_positions(portfolio_obj, prices))
|
||||
margin_buffer = cash - margin_used
|
||||
|
||||
lines = ["=== Margin And Liquidity Assessment ==="]
|
||||
lines.append(f"Portfolio equity: ${equity:,.2f}")
|
||||
lines.append(f"Cash available: ${cash:,.2f}")
|
||||
lines.append(f"Margin used: ${margin_used:,.2f}")
|
||||
lines.append(f"Margin requirement: {margin_requirement:.1%}")
|
||||
lines.append(f"Short exposure: ${short_exposure:,.2f}")
|
||||
lines.append(f"Margin buffer (cash - used): ${margin_buffer:,.2f}")
|
||||
|
||||
if equity > 0:
|
||||
lines.append(f"Margin used / equity: {margin_used / equity:.1%}")
|
||||
lines.append(f"Short exposure / equity: {short_exposure / equity:.1%}")
|
||||
|
||||
if margin_buffer < 0:
|
||||
lines.append("Risk flag: HIGH. Margin usage exceeds available cash buffer.")
|
||||
elif equity > 0 and margin_used / equity > 0.35:
|
||||
lines.append("Risk flag: MODERATE to HIGH. Margin usage is above 35% of equity.")
|
||||
else:
|
||||
lines.append("Risk flag: margin pressure is currently manageable.")
|
||||
|
||||
return _to_text_response("\n".join(lines))
|
||||
|
||||
|
||||
def assess_volatility_exposure(
|
||||
tickers: List[str] | str,
|
||||
current_date: str | None = None,
|
||||
) -> ToolResponse:
|
||||
"""
|
||||
Assess per-ticker volatility and risk level for the current watchlist.
|
||||
|
||||
Args:
|
||||
tickers: List of stock tickers or JSON list string.
|
||||
current_date: Analysis date in YYYY-MM-DD format.
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from backend.tools.analysis_tools import _parse_tickers, _resolved_date
|
||||
from backend.tools.data_tools import get_prices, prices_to_df
|
||||
from backend.tools.technical_signals import StockTechnicalAnalyzer
|
||||
|
||||
tickers_list = _parse_tickers(tickers)
|
||||
current_date = _resolved_date(current_date)
|
||||
end_dt = datetime.strptime(current_date, "%Y-%m-%d")
|
||||
start_date = (end_dt - timedelta(days=90)).strftime("%Y-%m-%d")
|
||||
analyzer = StockTechnicalAnalyzer()
|
||||
lines = [f"=== Volatility Exposure Assessment ({current_date}) ==="]
|
||||
|
||||
for ticker in tickers_list:
|
||||
prices = get_prices(
|
||||
ticker=ticker,
|
||||
start_date=start_date,
|
||||
end_date=current_date,
|
||||
)
|
||||
if not prices or len(prices) < 5:
|
||||
lines.append(f"- {ticker}: insufficient price data")
|
||||
continue
|
||||
signal = analyzer.analyze(ticker=ticker, df=prices_to_df(prices))
|
||||
lines.append(
|
||||
f"- {ticker}: annualized volatility {signal.annualized_volatility_pct:.1f}%, "
|
||||
f"RSI14 {signal.rsi14:.1f}, trend {signal.trend}, risk level {signal.risk_level}"
|
||||
)
|
||||
|
||||
if len(lines) == 1:
|
||||
lines.append("No tickers provided.")
|
||||
|
||||
return _to_text_response("\n".join(lines))
|
||||
@@ -1,7 +1,6 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/trading_logo.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
@@ -19,9 +19,9 @@ import AgentFeed from './components/AgentFeed';
|
||||
import StockLogo from './components/StockLogo';
|
||||
import StatisticsView from './components/StatisticsView';
|
||||
import PerformanceView from './components/PerformanceView';
|
||||
import AboutModal from './components/AboutModal';
|
||||
import RulesView from './components/RulesView';
|
||||
import StockExplainView from './components/StockExplainView.jsx';
|
||||
import Header from './components/Header.jsx';
|
||||
import WatchlistPanel from './components/WatchlistPanel.jsx';
|
||||
|
||||
// Utils
|
||||
import { formatNumber, formatTickerPrice } from './utils/formatters';
|
||||
@@ -39,9 +39,8 @@ export default function LiveTradingApp() {
|
||||
const [currentDate, setCurrentDate] = useState(null);
|
||||
const [progress, setProgress] = useState({ current: 0, total: 0 });
|
||||
const [now, setNow] = useState(() => new Date());
|
||||
const [showAboutModal, setShowAboutModal] = useState(false);
|
||||
|
||||
// View toggle: 'rules' | 'room' | 'chart' | 'statistics'
|
||||
// View toggle: 'room' | 'explain' | 'chart' | 'statistics'
|
||||
const [currentView, setCurrentView] = useState('chart'); // Start with chart, then animate to room
|
||||
const [isInitialAnimating, setIsInitialAnimating] = useState(true);
|
||||
const [lastUpdate, setLastUpdate] = useState(new Date());
|
||||
@@ -71,6 +70,11 @@ export default function LiveTradingApp() {
|
||||
// Ticker prices (now from real-time data)
|
||||
const [tickers, setTickers] = useState(INITIAL_TICKERS);
|
||||
const [rollingTickers, setRollingTickers] = useState({});
|
||||
const [priceHistoryByTicker, setPriceHistoryByTicker] = useState({});
|
||||
const [ohlcHistoryByTicker, setOhlcHistoryByTicker] = useState({});
|
||||
const [explainEventsByTicker, setExplainEventsByTicker] = useState({});
|
||||
const [selectedExplainSymbol, setSelectedExplainSymbol] = useState('');
|
||||
const [historySourceByTicker, setHistorySourceByTicker] = useState({});
|
||||
|
||||
// Room bubbles
|
||||
const [bubbles, setBubbles] = useState({});
|
||||
@@ -84,10 +88,18 @@ export default function LiveTradingApp() {
|
||||
const [marketStatus, setMarketStatus] = useState(null); // { status, status_text, ... }
|
||||
const [virtualTime, setVirtualTime] = useState(null); // Virtual time from server (for mock mode)
|
||||
const [dataSources, setDataSources] = useState(null);
|
||||
const [runtimeConfig, setRuntimeConfig] = useState(null);
|
||||
const [isWatchlistPanelOpen, setIsWatchlistPanelOpen] = useState(false);
|
||||
const [watchlistDraftSymbols, setWatchlistDraftSymbols] = useState([]);
|
||||
const [watchlistInputValue, setWatchlistInputValue] = useState('');
|
||||
const [watchlistFeedback, setWatchlistFeedback] = useState(null);
|
||||
const [isWatchlistSaving, setIsWatchlistSaving] = useState(false);
|
||||
|
||||
const clientRef = useRef(null);
|
||||
const containerRef = useRef(null);
|
||||
const agentFeedRef = useRef(null);
|
||||
const isWatchlistSavingRef = useRef(false);
|
||||
const requestedStockHistoryRef = useRef(new Set());
|
||||
|
||||
// Track last virtual time update to calculate increment
|
||||
const lastVirtualTimeRef = useRef(null);
|
||||
@@ -96,12 +108,311 @@ export default function LiveTradingApp() {
|
||||
// Last day history for replay
|
||||
const [lastDayHistory, setLastDayHistory] = useState([]);
|
||||
|
||||
const buildTickersFromSymbols = useCallback((symbols, previousTickers = []) => {
|
||||
if (!Array.isArray(symbols) || symbols.length === 0) {
|
||||
return previousTickers;
|
||||
}
|
||||
|
||||
return symbols
|
||||
.filter((symbol) => typeof symbol === 'string' && symbol.trim())
|
||||
.map((symbol) => {
|
||||
const normalized = symbol.trim().toUpperCase();
|
||||
const existing = previousTickers.find((ticker) => ticker.symbol === normalized);
|
||||
return existing || {
|
||||
symbol: normalized,
|
||||
price: null,
|
||||
change: null
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const normalizePriceHistory = useCallback((payload) => {
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
return {};
|
||||
}
|
||||
|
||||
const normalized = {};
|
||||
Object.entries(payload).forEach(([symbol, points]) => {
|
||||
const ticker = String(symbol || '').trim().toUpperCase();
|
||||
if (!ticker || !Array.isArray(points)) {
|
||||
return;
|
||||
}
|
||||
|
||||
normalized[ticker] = points
|
||||
.map((point) => {
|
||||
if (Array.isArray(point) && point.length >= 2) {
|
||||
const [label, value] = point;
|
||||
const price = Number(value);
|
||||
if (!label || !Number.isFinite(price)) return null;
|
||||
return {
|
||||
timestamp: String(label),
|
||||
label: String(label),
|
||||
price
|
||||
};
|
||||
}
|
||||
|
||||
if (point && typeof point === 'object') {
|
||||
const rawTimestamp = point.timestamp ?? point.t ?? point.date ?? point.label;
|
||||
const price = Number(point.price ?? point.v ?? point.value ?? point.close);
|
||||
if (!rawTimestamp || !Number.isFinite(price)) return null;
|
||||
return {
|
||||
timestamp: String(rawTimestamp),
|
||||
label: String(rawTimestamp),
|
||||
price
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.slice(-120);
|
||||
});
|
||||
|
||||
return normalized;
|
||||
}, []);
|
||||
|
||||
// Determine if LIVE tab should be enabled
|
||||
const isLiveEnabled = useMemo(() => {
|
||||
if (!marketStatus) return false;
|
||||
return marketStatus.status === 'open';
|
||||
}, [marketStatus]);
|
||||
|
||||
const displayTickers = useMemo(() => {
|
||||
const symbols = runtimeConfig?.tickers;
|
||||
if (Array.isArray(symbols) && symbols.length > 0) {
|
||||
return buildTickersFromSymbols(symbols, tickers);
|
||||
}
|
||||
return tickers;
|
||||
}, [buildTickersFromSymbols, runtimeConfig, tickers]);
|
||||
|
||||
const runtimeWatchlistSymbols = useMemo(() => {
|
||||
const symbols = runtimeConfig?.tickers;
|
||||
if (Array.isArray(symbols) && symbols.length > 0) {
|
||||
return symbols
|
||||
.filter((symbol) => typeof symbol === 'string' && symbol.trim())
|
||||
.map((symbol) => symbol.trim().toUpperCase());
|
||||
}
|
||||
|
||||
return displayTickers
|
||||
.map((ticker) => ticker.symbol)
|
||||
.filter((symbol) => typeof symbol === 'string' && symbol.trim());
|
||||
}, [displayTickers, runtimeConfig]);
|
||||
|
||||
useEffect(() => {
|
||||
const symbols = displayTickers
|
||||
.map((ticker) => ticker.symbol)
|
||||
.filter((symbol) => typeof symbol === 'string' && symbol.trim());
|
||||
|
||||
if (!symbols.length) {
|
||||
setSelectedExplainSymbol('');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedExplainSymbol || !symbols.includes(selectedExplainSymbol)) {
|
||||
setSelectedExplainSymbol(symbols[0]);
|
||||
}
|
||||
}, [displayTickers, selectedExplainSymbol]);
|
||||
|
||||
const watchlistSuggestions = useMemo(
|
||||
() => INITIAL_TICKERS.map((ticker) => ticker.symbol).filter((symbol, index, list) => list.indexOf(symbol) === index),
|
||||
[]
|
||||
);
|
||||
|
||||
const isWatchlistDraftDirty = useMemo(() => {
|
||||
if (watchlistInputValue.trim()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (watchlistDraftSymbols.length !== runtimeWatchlistSymbols.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return watchlistDraftSymbols.some((symbol, index) => symbol !== runtimeWatchlistSymbols[index]);
|
||||
}, [runtimeWatchlistSymbols, watchlistDraftSymbols, watchlistInputValue]);
|
||||
|
||||
const marketStatusLabel = useMemo(() => {
|
||||
if (!marketStatus) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const raw = typeof marketStatus.status_text === 'string' ? marketStatus.status_text.trim() : '';
|
||||
const normalized = raw.toLowerCase();
|
||||
if (normalized === 'market closed (non-trading day)') {
|
||||
return '休市';
|
||||
}
|
||||
if (normalized === 'market open') {
|
||||
return '开盘';
|
||||
}
|
||||
if (normalized === 'market closed') {
|
||||
return '收盘';
|
||||
}
|
||||
|
||||
return raw || (marketStatus.status === 'open' ? '开盘' : '收盘');
|
||||
}, [marketStatus]);
|
||||
|
||||
const priceSourceLabel = useMemo(() => {
|
||||
const source = dataSources?.last_success?.prices;
|
||||
if (!source) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalized = String(source).trim().toLowerCase();
|
||||
const labels = {
|
||||
yfinance: '数据源 Yahoo',
|
||||
finnhub: '数据源 Finnhub',
|
||||
financial_datasets: '数据源 Financial Datasets',
|
||||
local_csv: '数据源 CSV'
|
||||
};
|
||||
|
||||
return labels[normalized] || `数据源 ${String(source).trim()}`;
|
||||
}, [dataSources]);
|
||||
|
||||
const parseWatchlistInput = useCallback((value) => {
|
||||
if (typeof value !== 'string') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Array.from(
|
||||
new Set(
|
||||
value
|
||||
.split(/[\s,]+/)
|
||||
.map((symbol) => symbol.trim().toUpperCase())
|
||||
.filter(Boolean)
|
||||
)
|
||||
);
|
||||
}, []);
|
||||
|
||||
const commitWatchlistInput = useCallback((value) => {
|
||||
const parsed = parseWatchlistInput(value);
|
||||
if (parsed.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
setWatchlistDraftSymbols((prev) => Array.from(new Set([...prev, ...parsed])));
|
||||
setWatchlistInputValue('');
|
||||
if (watchlistFeedback) {
|
||||
setWatchlistFeedback(null);
|
||||
}
|
||||
return parsed;
|
||||
}, [parseWatchlistInput, watchlistFeedback]);
|
||||
|
||||
const handleWatchlistRemove = useCallback((symbolToRemove) => {
|
||||
setWatchlistDraftSymbols((prev) => prev.filter((symbol) => symbol !== symbolToRemove));
|
||||
if (watchlistFeedback) {
|
||||
setWatchlistFeedback(null);
|
||||
}
|
||||
}, [watchlistFeedback]);
|
||||
|
||||
const handleWatchlistPanelToggle = useCallback(() => {
|
||||
setIsWatchlistPanelOpen((open) => {
|
||||
const nextOpen = !open;
|
||||
if (nextOpen) {
|
||||
setWatchlistDraftSymbols(runtimeWatchlistSymbols);
|
||||
setWatchlistInputValue('');
|
||||
setWatchlistFeedback(null);
|
||||
}
|
||||
return nextOpen;
|
||||
});
|
||||
}, [runtimeWatchlistSymbols]);
|
||||
|
||||
const handleWatchlistInputChange = useCallback((value) => {
|
||||
setWatchlistInputValue(value);
|
||||
if (watchlistFeedback) {
|
||||
setWatchlistFeedback(null);
|
||||
}
|
||||
}, [watchlistFeedback]);
|
||||
|
||||
const handleWatchlistInputKeyDown = useCallback((e) => {
|
||||
if (e.key === 'Enter' || e.key === ',') {
|
||||
e.preventDefault();
|
||||
commitWatchlistInput(watchlistInputValue);
|
||||
}
|
||||
}, [commitWatchlistInput, watchlistInputValue]);
|
||||
|
||||
const handleWatchlistSuggestionClick = useCallback((symbol) => {
|
||||
if (watchlistDraftSymbols.includes(symbol)) {
|
||||
return;
|
||||
}
|
||||
setWatchlistDraftSymbols((prev) => [...prev, symbol]);
|
||||
if (watchlistFeedback) {
|
||||
setWatchlistFeedback(null);
|
||||
}
|
||||
}, [watchlistDraftSymbols, watchlistFeedback]);
|
||||
|
||||
const handleWatchlistRestoreCurrent = useCallback(() => {
|
||||
setWatchlistDraftSymbols(runtimeWatchlistSymbols);
|
||||
setWatchlistInputValue('');
|
||||
setWatchlistFeedback(null);
|
||||
}, [runtimeWatchlistSymbols]);
|
||||
|
||||
const handleWatchlistRestoreDefault = useCallback(() => {
|
||||
setWatchlistDraftSymbols(watchlistSuggestions);
|
||||
setWatchlistInputValue('');
|
||||
setWatchlistFeedback(null);
|
||||
}, [watchlistSuggestions]);
|
||||
|
||||
const handleWatchlistSave = useCallback(() => {
|
||||
const pendingTickers = parseWatchlistInput(watchlistInputValue);
|
||||
const nextTickers = Array.from(new Set([...watchlistDraftSymbols, ...pendingTickers]));
|
||||
if (nextTickers.length === 0) {
|
||||
setWatchlistFeedback({ type: 'error', text: '至少输入 1 个有效股票代码' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!clientRef.current) {
|
||||
setWatchlistFeedback({ type: 'error', text: '连接未就绪,稍后重试' });
|
||||
return;
|
||||
}
|
||||
|
||||
setIsWatchlistSaving(true);
|
||||
setWatchlistFeedback(null);
|
||||
setWatchlistDraftSymbols(nextTickers);
|
||||
setWatchlistInputValue('');
|
||||
const success = clientRef.current.send({
|
||||
type: 'update_watchlist',
|
||||
tickers: nextTickers
|
||||
});
|
||||
|
||||
if (!success) {
|
||||
setIsWatchlistSaving(false);
|
||||
setWatchlistFeedback({ type: 'error', text: '发送失败,请检查连接状态' });
|
||||
}
|
||||
}, [parseWatchlistInput, watchlistDraftSymbols, watchlistInputValue]);
|
||||
|
||||
const requestStockHistory = useCallback((symbol, { force = false } = {}) => {
|
||||
const normalized = typeof symbol === 'string' ? symbol.trim().toUpperCase() : '';
|
||||
if (!normalized || !clientRef.current) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!force && requestedStockHistoryRef.current.has(normalized)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const success = clientRef.current.send({
|
||||
type: 'get_stock_history',
|
||||
ticker: normalized,
|
||||
lookback_days: 120
|
||||
});
|
||||
|
||||
if (success) {
|
||||
requestedStockHistoryRef.current.add(normalized);
|
||||
}
|
||||
|
||||
return success;
|
||||
}, []);
|
||||
|
||||
const requestStockExplainEvents = useCallback((symbol) => {
|
||||
const normalized = typeof symbol === 'string' ? symbol.trim().toUpperCase() : '';
|
||||
if (!normalized || !clientRef.current) {
|
||||
return false;
|
||||
}
|
||||
return clientRef.current.send({
|
||||
type: 'get_stock_explain_events',
|
||||
ticker: normalized
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Switch away from LIVE tab when market closes
|
||||
useEffect(() => {
|
||||
if (!isLiveEnabled && chartTab === 'live') {
|
||||
@@ -109,6 +420,27 @@ export default function LiveTradingApp() {
|
||||
}
|
||||
}, [isLiveEnabled, chartTab]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isWatchlistPanelOpen || !isWatchlistDraftDirty) {
|
||||
setWatchlistDraftSymbols(runtimeWatchlistSymbols);
|
||||
if (!isWatchlistPanelOpen) {
|
||||
setWatchlistInputValue('');
|
||||
}
|
||||
}
|
||||
}, [isWatchlistDraftDirty, isWatchlistPanelOpen, runtimeWatchlistSymbols]);
|
||||
|
||||
useEffect(() => {
|
||||
isWatchlistSavingRef.current = isWatchlistSaving;
|
||||
}, [isWatchlistSaving]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentView !== 'explain' || !selectedExplainSymbol) {
|
||||
return;
|
||||
}
|
||||
requestStockHistory(selectedExplainSymbol);
|
||||
requestStockExplainEvents(selectedExplainSymbol);
|
||||
}, [currentView, requestStockExplainEvents, requestStockHistory, selectedExplainSymbol]);
|
||||
|
||||
// Clock - use virtual time if available (for mock mode)
|
||||
useEffect(() => {
|
||||
if (virtualTime) {
|
||||
@@ -253,6 +585,10 @@ export default function LiveTradingApp() {
|
||||
// Error response (for fast forward errors)
|
||||
error: (e) => {
|
||||
console.error('[Error]', e.message);
|
||||
if (isWatchlistSavingRef.current) {
|
||||
setIsWatchlistSaving(false);
|
||||
setWatchlistFeedback({ type: 'error', text: e.message || '更新 watchlist 失败' });
|
||||
}
|
||||
|
||||
// Handle fast forward errors
|
||||
if (e.message && e.message.includes('fast forward')) {
|
||||
@@ -307,6 +643,12 @@ export default function LiveTradingApp() {
|
||||
if (state.data_sources) {
|
||||
setDataSources(state.data_sources);
|
||||
}
|
||||
if (state.runtime_config) {
|
||||
setRuntimeConfig(state.runtime_config);
|
||||
}
|
||||
if (Array.isArray(state.tickers) && state.tickers.length > 0) {
|
||||
setTickers(prevTickers => buildTickersFromSymbols(state.tickers, prevTickers));
|
||||
}
|
||||
// 检查是否是mock模式
|
||||
const isMockMode = state.is_mock_mode === true;
|
||||
if (state.market_status) {
|
||||
@@ -356,6 +698,9 @@ export default function LiveTradingApp() {
|
||||
if (state.dashboard.leaderboard) setLeaderboard(state.dashboard.leaderboard);
|
||||
}
|
||||
if (state.realtime_prices) updateTickersFromPrices(state.realtime_prices);
|
||||
if (state.price_history) {
|
||||
setPriceHistoryByTicker(normalizePriceHistory(state.price_history));
|
||||
}
|
||||
|
||||
// Load and process historical feed data
|
||||
if (state.feed_history && Array.isArray(state.feed_history)) {
|
||||
@@ -388,6 +733,75 @@ export default function LiveTradingApp() {
|
||||
}
|
||||
},
|
||||
|
||||
runtime_assets_reloaded: (e) => {
|
||||
if (e.runtime_config_applied) {
|
||||
setRuntimeConfig(e.runtime_config_applied);
|
||||
}
|
||||
if (Array.isArray(e.runtime_config_applied?.tickers)) {
|
||||
setTickers(prevTickers => buildTickersFromSymbols(e.runtime_config_applied.tickers, prevTickers));
|
||||
setWatchlistDraftSymbols(e.runtime_config_applied.tickers.map((symbol) => String(symbol).trim().toUpperCase()));
|
||||
setWatchlistInputValue('');
|
||||
}
|
||||
if (isWatchlistSavingRef.current) {
|
||||
setIsWatchlistSaving(false);
|
||||
}
|
||||
addSystemMessage('运行时配置已热更新');
|
||||
},
|
||||
|
||||
watchlist_updated: (e) => {
|
||||
if (Array.isArray(e.tickers)) {
|
||||
const normalizedTickers = e.tickers.map((symbol) => String(symbol).trim().toUpperCase());
|
||||
requestedStockHistoryRef.current = new Set(
|
||||
Array.from(requestedStockHistoryRef.current).filter((symbol) => normalizedTickers.includes(symbol))
|
||||
);
|
||||
setRuntimeConfig((prev) => ({
|
||||
...(prev || {}),
|
||||
tickers: normalizedTickers
|
||||
}));
|
||||
setTickers((prevTickers) => buildTickersFromSymbols(normalizedTickers, prevTickers));
|
||||
setWatchlistDraftSymbols(normalizedTickers);
|
||||
setWatchlistInputValue('');
|
||||
}
|
||||
setIsWatchlistSaving(false);
|
||||
setWatchlistFeedback({
|
||||
type: 'success',
|
||||
text: `已更新为 ${Array.isArray(e.tickers) ? e.tickers.join(', ') : '最新列表'}`
|
||||
});
|
||||
},
|
||||
|
||||
stock_history_loaded: (e) => {
|
||||
const symbol = typeof e.ticker === 'string' ? e.ticker.trim().toUpperCase() : '';
|
||||
if (!symbol) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(e.prices)) {
|
||||
setOhlcHistoryByTicker((prev) => ({
|
||||
...prev,
|
||||
[symbol]: e.prices
|
||||
}));
|
||||
setHistorySourceByTicker((prev) => ({
|
||||
...prev,
|
||||
[symbol]: e.source || null
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
stock_explain_events_loaded: (e) => {
|
||||
const symbol = typeof e.ticker === 'string' ? e.ticker.trim().toUpperCase() : '';
|
||||
if (!symbol) {
|
||||
return;
|
||||
}
|
||||
setExplainEventsByTicker((prev) => ({
|
||||
...prev,
|
||||
[symbol]: {
|
||||
events: Array.isArray(e.events) ? e.events : [],
|
||||
signals: Array.isArray(e.signals) ? e.signals : [],
|
||||
trades: Array.isArray(e.trades) ? e.trades : []
|
||||
}
|
||||
}));
|
||||
},
|
||||
|
||||
// Real-time price updates
|
||||
price_update: (e) => {
|
||||
try {
|
||||
@@ -402,6 +816,24 @@ export default function LiveTradingApp() {
|
||||
setIsConnected(true);
|
||||
console.log(`[Price Update] ${symbol}: $${price} (ret: ${ret !== undefined ? ret.toFixed(2) : 'N/A'}%)`);
|
||||
|
||||
setPriceHistoryByTicker((prev) => {
|
||||
const ticker = String(symbol).trim().toUpperCase();
|
||||
const nextPoint = {
|
||||
timestamp: new Date().toISOString(),
|
||||
label: now.toISOString(),
|
||||
price: Number(price)
|
||||
};
|
||||
const existing = Array.isArray(prev[ticker]) ? prev[ticker] : [];
|
||||
const lastPoint = existing[existing.length - 1];
|
||||
if (lastPoint && Number(lastPoint.price) === Number(nextPoint.price)) {
|
||||
return prev;
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
[ticker]: [...existing, nextPoint].slice(-120)
|
||||
};
|
||||
});
|
||||
|
||||
// Update ticker price with animation
|
||||
setTickers(prevTickers => {
|
||||
return prevTickers.map(ticker => {
|
||||
@@ -714,7 +1146,7 @@ export default function LiveTradingApp() {
|
||||
clientRef.current.disconnect();
|
||||
}
|
||||
};
|
||||
}, []); // Empty dependency array - only run once on mount
|
||||
}, [addSystemMessage, buildTickersFromSymbols, processFeedEvent, processHistoricalFeed]); // Only reconnect if handlers change
|
||||
|
||||
// Resizing handlers
|
||||
const handleMouseDown = (e) => {
|
||||
@@ -755,10 +1187,7 @@ export default function LiveTradingApp() {
|
||||
|
||||
{/* Header */}
|
||||
<div className="header">
|
||||
<Header
|
||||
onEvoTradersClick={() => setShowAboutModal(true)}
|
||||
evoTradersLinkStyle="default"
|
||||
/>
|
||||
<Header />
|
||||
|
||||
<div className="header-right" style={{ display: 'flex', alignItems: 'center', gap: 24, marginLeft: 'auto', flexWrap: 'wrap', minWidth: 0 }}>
|
||||
{/* Mock Mode Indicator */}
|
||||
@@ -885,21 +1314,41 @@ export default function LiveTradingApp() {
|
||||
<>
|
||||
<span className="status-sep">·</span>
|
||||
<span className={`market-text ${serverMode === 'backtest' ? 'backtest' : (marketStatus.status === 'open' ? 'open' : 'closed')}`}>
|
||||
{marketStatus.status_text || (marketStatus.status === 'open' ? '开盘' : '收盘')}
|
||||
{marketStatusLabel}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{dataSources?.last_success?.prices && (
|
||||
{priceSourceLabel && (
|
||||
<>
|
||||
<span className="status-sep">·</span>
|
||||
<span className="market-text backtest">
|
||||
DATA {String(dataSources.last_success.prices).toUpperCase()}
|
||||
{priceSourceLabel}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
<span className="status-sep">·</span>
|
||||
<span className="time-text">{lastUpdate.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false })}</span>
|
||||
</div>
|
||||
|
||||
<WatchlistPanel
|
||||
isOpen={isWatchlistPanelOpen}
|
||||
isConnected={isConnected}
|
||||
isSaving={isWatchlistSaving}
|
||||
draftSymbols={watchlistDraftSymbols}
|
||||
inputValue={watchlistInputValue}
|
||||
feedback={watchlistFeedback}
|
||||
suggestions={watchlistSuggestions}
|
||||
onToggle={handleWatchlistPanelToggle}
|
||||
onClose={() => setIsWatchlistPanelOpen(false)}
|
||||
onInputChange={handleWatchlistInputChange}
|
||||
onInputKeyDown={handleWatchlistInputKeyDown}
|
||||
onAdd={() => commitWatchlistInput(watchlistInputValue)}
|
||||
onRemove={handleWatchlistRemove}
|
||||
onRestoreCurrent={handleWatchlistRestoreCurrent}
|
||||
onRestoreDefault={handleWatchlistRestoreDefault}
|
||||
onSuggestionClick={handleWatchlistSuggestionClick}
|
||||
onSave={handleWatchlistSave}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -910,7 +1359,7 @@ export default function LiveTradingApp() {
|
||||
<div className="ticker-track">
|
||||
{[0, 1].map((groupIdx) => (
|
||||
<div key={groupIdx} className="ticker-group">
|
||||
{tickers.map(ticker => (
|
||||
{displayTickers.map(ticker => (
|
||||
<div key={`${ticker.symbol}-${groupIdx}`} className="ticker-item">
|
||||
<StockLogo ticker={ticker.symbol} size={16} />
|
||||
<span className="ticker-symbol">{ticker.symbol}</span>
|
||||
@@ -947,13 +1396,6 @@ export default function LiveTradingApp() {
|
||||
<div className="chart-section">
|
||||
<div className="view-container">
|
||||
<div className="view-nav-bar">
|
||||
<button
|
||||
className={`view-nav-btn ${currentView === 'rules' ? 'active' : ''}`}
|
||||
onClick={() => setCurrentView('rules')}
|
||||
>
|
||||
规则
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={`view-nav-btn ${currentView === 'room' ? 'active' : ''}`}
|
||||
onClick={() => setCurrentView('room')}
|
||||
@@ -961,6 +1403,13 @@ export default function LiveTradingApp() {
|
||||
交易室
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={`view-nav-btn ${currentView === 'explain' ? 'active' : ''}`}
|
||||
onClick={() => setCurrentView('explain')}
|
||||
>
|
||||
个股解释
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={`view-nav-btn ${currentView === 'chart' ? 'active' : ''}`}
|
||||
onClick={() => setCurrentView('chart')}
|
||||
@@ -977,12 +1426,15 @@ export default function LiveTradingApp() {
|
||||
</div>
|
||||
|
||||
{/* Slider container with four views */}
|
||||
<div className={`view-slider-four ${currentView === 'rules' ? 'show-rules' : currentView === 'room' ? 'show-room' : currentView === 'statistics' ? 'show-statistics' : 'show-chart'} ${!isInitialAnimating ? 'normal-speed' : ''}`}>
|
||||
{/* Rules View Panel */}
|
||||
<div className="view-panel">
|
||||
<RulesView />
|
||||
</div>
|
||||
|
||||
<div className={`view-slider-four ${
|
||||
currentView === 'room'
|
||||
? 'show-room'
|
||||
: currentView === 'explain'
|
||||
? 'show-explain'
|
||||
: currentView === 'statistics'
|
||||
? 'show-statistics'
|
||||
: 'show-chart'
|
||||
} ${!isInitialAnimating ? 'normal-speed' : ''}`}>
|
||||
{/* Room View Panel */}
|
||||
<div className="view-panel">
|
||||
<RoomView
|
||||
@@ -994,6 +1446,23 @@ export default function LiveTradingApp() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Stock Explain View Panel */}
|
||||
<div className="view-panel">
|
||||
<StockExplainView
|
||||
tickers={displayTickers}
|
||||
holdings={holdings}
|
||||
trades={trades}
|
||||
leaderboard={leaderboard}
|
||||
feed={feed}
|
||||
priceHistoryByTicker={priceHistoryByTicker}
|
||||
ohlcHistoryByTicker={ohlcHistoryByTicker}
|
||||
selectedSymbol={selectedExplainSymbol}
|
||||
onSelectedSymbolChange={setSelectedExplainSymbol}
|
||||
selectedHistorySource={historySourceByTicker[selectedExplainSymbol] || null}
|
||||
explainEventsSnapshot={explainEventsByTicker[selectedExplainSymbol] || null}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Chart View Panel */}
|
||||
<div className="view-panel">
|
||||
<div className="chart-container">
|
||||
@@ -1059,9 +1528,6 @@ export default function LiveTradingApp() {
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
{/* About Modal */}
|
||||
{showAboutModal && <AboutModal onClose={() => setShowAboutModal(false)} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,294 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import Header from './Header.jsx';
|
||||
|
||||
export default function AboutModal({ onClose }) {
|
||||
const [isClosing, setIsClosing] = useState(false);
|
||||
const [language] = useState('zh');
|
||||
|
||||
const handleClose = () => {
|
||||
setIsClosing(true);
|
||||
// Wait for animation to complete before actually closing
|
||||
setTimeout(() => {
|
||||
onClose();
|
||||
}, 600); // Match animation duration
|
||||
};
|
||||
|
||||
const overlayStyle = {
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: '#ffffff',
|
||||
zIndex: 9999,
|
||||
animation: isClosing
|
||||
? 'collapseUp 0.6s cubic-bezier(0.4, 0, 0.2, 1) forwards'
|
||||
: 'expandDown 0.6s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
transformOrigin: 'top center',
|
||||
overflowY: 'auto'
|
||||
};
|
||||
|
||||
const contentStyle = {
|
||||
maxWidth: '900px',
|
||||
width: '90%',
|
||||
margin: '0 auto',
|
||||
textAlign: 'left',
|
||||
fontFamily: "'IBM Plex Mono', monospace",
|
||||
color: '#000000',
|
||||
lineHeight: 1.8,
|
||||
fontSize: '14px',
|
||||
letterSpacing: '0.01em',
|
||||
padding: '60px 20px 80px',
|
||||
animation: isClosing
|
||||
? 'fadeOutContent 0.4s ease forwards'
|
||||
: 'fadeInContent 0.8s ease 0.3s backwards'
|
||||
};
|
||||
|
||||
const highlight = {
|
||||
color: '#615CED',
|
||||
fontWeight: 600
|
||||
};
|
||||
|
||||
const linkStyle = {
|
||||
color: '#615CED',
|
||||
textDecoration: 'none',
|
||||
borderBottom: '1px solid #615CED',
|
||||
transition: 'all 0.2s'
|
||||
};
|
||||
|
||||
const closeHintStyle = {
|
||||
marginTop: '50px',
|
||||
fontSize: '11px',
|
||||
color: '#999',
|
||||
cursor: 'pointer',
|
||||
textAlign: 'center'
|
||||
};
|
||||
|
||||
const languageSwitchStyle = {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginBottom: '25px',
|
||||
marginTop: '10px',
|
||||
gap: '0px',
|
||||
fontSize: '11px',
|
||||
fontFamily: "'IBM Plex Mono', monospace"
|
||||
};
|
||||
|
||||
const getLangStyle = (isActive) => ({
|
||||
padding: '3px 8px',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
background: isActive ? '#000' : '#fff',
|
||||
color: isActive ? '#fff' : '#000',
|
||||
border: 'none'
|
||||
});
|
||||
|
||||
const content = {
|
||||
en: {
|
||||
|
||||
question: "What happens if AI models don't compete with each other, but instead trade like a ",
|
||||
questionHighlight: "well-coordinated, high-performance team",
|
||||
questionEnd: "?",
|
||||
|
||||
intro: "Not arena, but TEAM. We Hope that AI is no longer entering the financial markets as isolated models—it is stepping in as ",
|
||||
introHighlight1: "teams",
|
||||
introContinue: ", collaborating in one of the most challenging and noise-filled ",
|
||||
introHighlight2: "real-time environments",
|
||||
introContinue2: ".",
|
||||
|
||||
|
||||
point1Highlight: "✦ Complementary skills",
|
||||
point1: " - across multiple agents—data analysis, strategy generation, risk management—working together like a real trading desk, exchanging information through notifications and meetings.",
|
||||
|
||||
point2Highlight: "✦ An agent system that continually evolves",
|
||||
point2: " — with memory modules that retain experience, learn from market feedback, reflect, and develop their own methodology over time.",
|
||||
|
||||
point3Highlight: "✦ AI teams interacting with live markets",
|
||||
point3: " — learning from real-time data and making immediate decisions, not just theoretical simulations."
|
||||
},
|
||||
zh: {
|
||||
intro: "如果不是让模型彼此竞争,而是像一支高效协作的团队一样进行实时交易,会发生什么?",
|
||||
question: "这里不是竞技场,而是团队。我们希望Agents不再单打独斗,而是「组团」进入实时金融市场——这一十分困难且充满噪声的环境。",
|
||||
trying: "我们正在探索多智能体协作在实时金融交易中的可能性。",
|
||||
|
||||
title1: "✦ 多智能体的技能互补",
|
||||
point1: "不同模型、不同角色的智能体像真实的金融团队一样协作,各自承担数据分析、策略生成、风险控制等职责。",
|
||||
point1Sub: "通过通知和会议机制进行信息交换,实现高效协作。",
|
||||
|
||||
title2: "✦ 能够持续进化的智能体系统",
|
||||
point2: "依托「记忆」模块,每个智能体都能跨回合保留经验,不断学习、反思与调整。我们希望能看到在长期实时交易中,Agent形成自己的独特方法论,而不是一次性偶然的推理。",
|
||||
point2Sub: "ReMe 记忆框架帮助 Agents 持续改进。",
|
||||
|
||||
title3: "✦ 实时参与市场的 AI Agents",
|
||||
point3: "Agents从实时行情中学习,并给予即时决策;不是纸上谈兵,而是面对市场的真实波动。"
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>{`
|
||||
@keyframes expandDown {
|
||||
from {
|
||||
transform: scaleY(0);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: scaleY(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes collapseUp {
|
||||
from {
|
||||
transform: scaleY(1);
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
transform: scaleY(0);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInContent {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeOutContent {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
|
||||
<div style={overlayStyle} onClick={handleClose}>
|
||||
{/* Header */}
|
||||
<div className="header" style={{
|
||||
animation: isClosing
|
||||
? 'fadeOutContent 0.4s ease forwards'
|
||||
: 'fadeInContent 0.8s ease 0.3s backwards'
|
||||
}} onClick={(e) => e.stopPropagation()}>
|
||||
<Header
|
||||
onEvoTradersClick={handleClose}
|
||||
evoTradersLinkStyle="close"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div style={contentStyle} onClick={(e) => e.stopPropagation()}>
|
||||
<div style={languageSwitchStyle}>
|
||||
<span
|
||||
style={getLangStyle(true)}
|
||||
>
|
||||
中文
|
||||
</span>
|
||||
</div>
|
||||
|
||||
// Chinese Content
|
||||
<>
|
||||
<div style={{ marginBottom: '30px' }}>
|
||||
{content.zh.intro}
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '40px', fontSize: '15px', fontWeight: 600 }}>
|
||||
{content.zh.question}
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '30px', fontSize: '14px', opacity: 0.8 }}>
|
||||
{content.zh.trying}
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '30px' }}>
|
||||
<div style={{ ...highlight, marginBottom: '10px' }}>
|
||||
{content.zh.title1}
|
||||
</div>
|
||||
<div style={{ marginBottom: '10px' }}>
|
||||
{content.zh.point1}
|
||||
</div>
|
||||
<div style={{ fontSize: '13px', opacity: 0.7 }}>
|
||||
{content.zh.point1Sub}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '30px' }}>
|
||||
<div style={{ ...highlight, marginBottom: '10px' }}>
|
||||
{content.zh.title2}
|
||||
</div>
|
||||
<div style={{ marginBottom: '10px' }}>
|
||||
{content.zh.point2}
|
||||
</div>
|
||||
<div style={{ fontSize: '13px', opacity: 0.7 }}>
|
||||
{content.zh.point2Sub}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '30px' }}>
|
||||
<div style={{ ...highlight, marginBottom: '10px' }}>
|
||||
{content.zh.title3}
|
||||
</div>
|
||||
<div>
|
||||
{content.zh.point3}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '10px', opacity: 0.7 }}>
|
||||
我们已经在 GitHub 上开源。
|
||||
</div>
|
||||
<div style={{ marginBottom: '25px', opacity: 0.7 }}>
|
||||
EvoTraders 基于{' '}
|
||||
<a
|
||||
href="https://github.com/agentscope-ai"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={linkStyle}
|
||||
>
|
||||
AgentScope
|
||||
</a>
|
||||
{' '}搭建,并使用其中的{' '}
|
||||
<a
|
||||
href="https://github.com/agentscope-ai/ReMe"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={linkStyle}
|
||||
>
|
||||
ReMe
|
||||
</a>
|
||||
{' '}作为记忆管理核心。
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '10px', fontSize: '14px' }}>
|
||||
你可以在此找到完整项目与示例:
|
||||
</div>
|
||||
</>
|
||||
|
||||
<div style={{ marginTop: '40px' }}>
|
||||
<a
|
||||
href="https://github.com/agentscope-ai/agentscope-samples"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={linkStyle}
|
||||
>
|
||||
github.com/agentscope-ai/agentscope-samples
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div style={closeHintStyle} onClick={handleClose}>
|
||||
点击此处关闭
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -149,7 +149,7 @@ const AgentFeed = forwardRef(({ feed, leaderboard }, ref) => {
|
||||
// Get current selection display info
|
||||
const getCurrentSelectionInfo = () => {
|
||||
if (selectedAgent === 'all') {
|
||||
return { label: 'All Agents', modelInfo: null };
|
||||
return { label: '全部角色', modelInfo: null };
|
||||
}
|
||||
const agentInfo = getAgentInfoByName(selectedAgent);
|
||||
const modelInfo = agentInfo ? getModelIcon(agentInfo.modelName, agentInfo.modelProvider) : null;
|
||||
@@ -191,7 +191,7 @@ const AgentFeed = forwardRef(({ feed, leaderboard }, ref) => {
|
||||
setDropdownOpen(false);
|
||||
}}
|
||||
>
|
||||
<span>全部 Agents</span>
|
||||
<span>全部角色</span>
|
||||
</div>
|
||||
{uniqueAgents.map(agent => {
|
||||
const agentInfo = getAgentInfoByName(agent);
|
||||
@@ -419,17 +419,14 @@ function MemoryItem({ memory, itemId, isHighlighted }) {
|
||||
onMouseEnter={() => setShowTooltip(true)}
|
||||
onMouseLeave={() => setShowTooltip(false)}
|
||||
>
|
||||
<a
|
||||
href="https://github.com/agentscope-ai/ReMe"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
<span
|
||||
style={{ display: 'flex', alignItems: 'center', textDecoration: 'none' }}
|
||||
>
|
||||
<img
|
||||
src={ASSETS.remeLogo}
|
||||
alt="ReMe"
|
||||
alt="Memory"
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
cursor: 'default',
|
||||
height: '12px',
|
||||
width: 'auto',
|
||||
objectFit: 'contain',
|
||||
@@ -449,9 +446,9 @@ function MemoryItem({ memory, itemId, isHighlighted }) {
|
||||
lineHeight: 1,
|
||||
pointerEvents: 'none'
|
||||
}}>
|
||||
↗
|
||||
MEMORY
|
||||
</span>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<span style={{
|
||||
background: 'linear-gradient(90deg, #00C2FF 0%, #5C4CE0 100%)',
|
||||
@@ -497,10 +494,10 @@ function MemoryItem({ memory, itemId, isHighlighted }) {
|
||||
color: 'transparent',
|
||||
display: 'inline-block'
|
||||
}}>
|
||||
Memory powered by AgentScope-ReMe
|
||||
Runtime Memory Layer
|
||||
</div>
|
||||
<div style={{ color: '#475569', opacity: 0.9 }}>
|
||||
Not only retrieves historical memories but also generates suggestions and hints for the current task based on latest context.
|
||||
Retrieves relevant historical context and produces guidance for the current task based on the latest conversation state.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,253 +1,29 @@
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Header Component
|
||||
* Reusable header brand with EvoTraders logo, GitHub link, and Contact Us section
|
||||
*
|
||||
* @param {Function} onEvoTradersClick - Optional callback when EvoTraders is clicked
|
||||
* @param {string} evoTradersLinkStyle - Optional style variant: 'default' | 'close'
|
||||
* Reusable header brand for EvoTraders.
|
||||
*/
|
||||
export default function Header({
|
||||
onEvoTradersClick = null,
|
||||
evoTradersLinkStyle = 'default' // 'default' shows ↗, 'close' shows ↙
|
||||
}) {
|
||||
const [activeContactCard, setActiveContactCard] = useState({ yue: false, jiaji: false });
|
||||
const [clickedContactCard, setClickedContactCard] = useState(null);
|
||||
|
||||
const handleEvoTradersClick = () => {
|
||||
if (onEvoTradersClick) {
|
||||
onEvoTradersClick();
|
||||
}
|
||||
};
|
||||
|
||||
export default function Header() {
|
||||
return (
|
||||
<div className="header-title" style={{ flex: '0 1 auto', minWidth: 0 }}>
|
||||
<span
|
||||
className="header-link"
|
||||
onClick={handleEvoTradersClick}
|
||||
style={{ cursor: 'pointer', padding: '4px 8px', borderRadius: '3px', display: 'inline-flex', alignItems: 'center', gap: '8px' }}
|
||||
style={{
|
||||
padding: '4px 8px',
|
||||
borderRadius: '3px',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src="/trading_logo.png"
|
||||
alt="EvoTraders Logo"
|
||||
style={{ height: '24px', width: 'auto' }}
|
||||
/>
|
||||
EvoTraders {evoTradersLinkStyle === 'close' ? (
|
||||
<span className="link-arrow">↙</span>
|
||||
) : (
|
||||
<span className="link-arrow">↗</span>
|
||||
)}
|
||||
EvoTraders
|
||||
</span>
|
||||
|
||||
<span style={{
|
||||
width: '2px',
|
||||
height: '16px',
|
||||
background: '#666',
|
||||
margin: '0 16px',
|
||||
display: 'inline-block',
|
||||
verticalAlign: 'middle'
|
||||
}} />
|
||||
|
||||
<span style={{
|
||||
padding: '1px 5px',
|
||||
fontSize: '9px',
|
||||
fontWeight: 700,
|
||||
color: '#00C853',
|
||||
background: 'rgba(0, 200, 83, 0.1)',
|
||||
border: '1px solid #00C853',
|
||||
borderRadius: '3px',
|
||||
letterSpacing: '0.5px',
|
||||
marginRight: '0px'
|
||||
}}>
|
||||
开源
|
||||
</span>
|
||||
|
||||
<a
|
||||
href="https://github.com/agentscope-ai/agentscope-samples"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="header-link"
|
||||
style={{ display: 'inline-flex', flexDirection: 'row', alignItems: 'center', gap: '6px' }}
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
style={{ display: 'inline-block' }}
|
||||
>
|
||||
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
|
||||
</svg>
|
||||
<span>agentscope-samples</span>
|
||||
<span className="link-arrow">↗</span>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://github.com/agentscope-ai/ReMe"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="header-link"
|
||||
style={{ display: 'inline-flex', flexDirection: 'row', alignItems: 'center', gap: '6px', marginLeft: '0px' }}
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
style={{ display: 'inline-block' }}
|
||||
>
|
||||
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
|
||||
</svg>
|
||||
<span>agentscope-ReMe</span>
|
||||
<span className="link-arrow">↗</span>
|
||||
</a>
|
||||
|
||||
<span style={{
|
||||
width: '2px',
|
||||
height: '16px',
|
||||
background: '#666',
|
||||
margin: '0 16px',
|
||||
display: 'inline-block',
|
||||
verticalAlign: 'middle'
|
||||
}} />
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
onClick={() => {
|
||||
const bothActive = activeContactCard.yue && activeContactCard.jiaji;
|
||||
if (!bothActive) {
|
||||
setActiveContactCard({ yue: true, jiaji: true });
|
||||
setClickedContactCard('both');
|
||||
} else {
|
||||
setActiveContactCard({ yue: false, jiaji: false });
|
||||
setClickedContactCard(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="header-link">
|
||||
联系我们
|
||||
</span>
|
||||
|
||||
{/* Two contact buttons */}
|
||||
<div style={{ display: 'flex', gap: '6px', alignItems: 'center' }}>
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (activeContactCard.yue) {
|
||||
setActiveContactCard(prev => ({ ...prev, yue: false }));
|
||||
if (clickedContactCard === 'yue' || clickedContactCard === 'both') {
|
||||
setClickedContactCard(null);
|
||||
}
|
||||
} else {
|
||||
setActiveContactCard(prev => ({ ...prev, yue: true }));
|
||||
setClickedContactCard('yue');
|
||||
}
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
if (!clickedContactCard || clickedContactCard === 'yue' || clickedContactCard === 'both') {
|
||||
setActiveContactCard(prev => ({ ...prev, yue: true }));
|
||||
}
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
if (clickedContactCard !== 'yue' && clickedContactCard !== 'both') {
|
||||
setActiveContactCard(prev => ({ ...prev, yue: false }));
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
padding: '4px 8px',
|
||||
background: activeContactCard.yue ? '#615CED' : '#f5f5f5',
|
||||
color: activeContactCard.yue ? '#fff' : '#333',
|
||||
border: '1px solid',
|
||||
borderColor: activeContactCard.yue ? '#615CED' : '#e0e0e0',
|
||||
borderRadius: '3px',
|
||||
fontSize: '10px',
|
||||
fontWeight: 700,
|
||||
fontFamily: "'IBM Plex Mono', monospace",
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
letterSpacing: '0.5px',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
maxWidth: activeContactCard.yue ? '80px' : '32px',
|
||||
minWidth: activeContactCard.yue ? '80px' : '32px'
|
||||
}}
|
||||
>
|
||||
{activeContactCard.yue ? (
|
||||
<a
|
||||
href="https://1mycell.github.io/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: 'inherit', textDecoration: 'none' }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
Yue Wu ↗
|
||||
</a>
|
||||
) : 'YW'}
|
||||
</div>
|
||||
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (activeContactCard.jiaji) {
|
||||
setActiveContactCard(prev => ({ ...prev, jiaji: false }));
|
||||
if (clickedContactCard === 'jiaji' || clickedContactCard === 'both') {
|
||||
setClickedContactCard(null);
|
||||
}
|
||||
} else {
|
||||
setActiveContactCard(prev => ({ ...prev, jiaji: true }));
|
||||
setClickedContactCard('jiaji');
|
||||
}
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
if (!clickedContactCard || clickedContactCard === 'jiaji' || clickedContactCard === 'both') {
|
||||
setActiveContactCard(prev => ({ ...prev, jiaji: true }));
|
||||
}
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
if (clickedContactCard !== 'jiaji' && clickedContactCard !== 'both') {
|
||||
setActiveContactCard(prev => ({ ...prev, jiaji: false }));
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
padding: '4px 8px',
|
||||
background: activeContactCard.jiaji ? '#615CED' : '#f5f5f5',
|
||||
color: activeContactCard.jiaji ? '#fff' : '#333',
|
||||
border: '1px solid',
|
||||
borderColor: activeContactCard.jiaji ? '#615CED' : '#e0e0e0',
|
||||
borderRadius: '3px',
|
||||
fontSize: '10px',
|
||||
fontWeight: 700,
|
||||
fontFamily: "'IBM Plex Mono', monospace",
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
letterSpacing: '0.5px',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
maxWidth: activeContactCard.jiaji ? '100px' : '32px',
|
||||
minWidth: activeContactCard.jiaji ? '100px' : '32px'
|
||||
}}
|
||||
>
|
||||
{activeContactCard.jiaji ? (
|
||||
<a
|
||||
href="https://dengjiaji.github.io/self/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: 'inherit', textDecoration: 'none' }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
Jiaji Deng ↗
|
||||
</a>
|
||||
) : 'JD'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -554,7 +554,7 @@ export default function NetValueChart({ equity, baseline, baseline_vw, momentum,
|
||||
fontFamily: '"Courier New", monospace',
|
||||
fontSize: '12px'
|
||||
}}>
|
||||
NO DATA AVAILABLE
|
||||
暂无图表数据
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -828,4 +828,3 @@ export default function NetValueChart({ equity, baseline, baseline_vw, momentum,
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,360 +0,0 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { LLM_MODEL_LOGOS } from '../config/constants';
|
||||
|
||||
export default function RulesView() {
|
||||
const [language] = useState('zh');
|
||||
const [scale, setScale] = useState(1);
|
||||
const containerRef = useRef(null);
|
||||
const contentRef = useRef(null);
|
||||
|
||||
// Auto-scale content to fit container without scrolling
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (containerRef.current && contentRef.current) {
|
||||
const containerHeight = containerRef.current.clientHeight;
|
||||
const contentHeight = contentRef.current.scrollHeight;
|
||||
|
||||
if (contentHeight > containerHeight) {
|
||||
const newScale = containerHeight / contentHeight;
|
||||
setScale(Math.max(newScale * 0.95, 0.5)); // Min scale 0.5, with 95% of available space
|
||||
} else {
|
||||
setScale(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Initial resize
|
||||
handleResize();
|
||||
|
||||
// Listen to window resize
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
// Observe content changes
|
||||
const observer = new ResizeObserver(handleResize);
|
||||
if (contentRef.current) {
|
||||
observer.observe(contentRef.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [language]);
|
||||
|
||||
const containerStyle = {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: '#FFFFFF',
|
||||
padding: '10px'
|
||||
};
|
||||
|
||||
const contentWrapperStyle = {
|
||||
transform: `scale(${scale})`,
|
||||
transformOrigin: 'center center',
|
||||
transition: 'transform 0.3s ease',
|
||||
width: '100%',
|
||||
maxWidth: '900px'
|
||||
};
|
||||
|
||||
const innerContentStyle = {
|
||||
color: '#000000',
|
||||
fontFamily: "'IBM Plex Mono', monospace",
|
||||
fontSize: '13px',
|
||||
lineHeight: '1.6',
|
||||
letterSpacing: '0.01em',
|
||||
padding: '0 10px'
|
||||
};
|
||||
|
||||
const highlight = {
|
||||
color: '#000000',
|
||||
fontWeight: 700
|
||||
};
|
||||
|
||||
const sectionTitleStyle = {
|
||||
color: '#615CED',
|
||||
fontSize: '16px',
|
||||
fontWeight: 700,
|
||||
marginBottom: '8px',
|
||||
marginTop: '12px',
|
||||
marginLeft: '-10px',
|
||||
marginRight: '-10px',
|
||||
width: 'calc(100% + 20px)',
|
||||
padding: '8px 10px',
|
||||
backgroundColor: '#FFFFFF',
|
||||
letterSpacing: '0.5px',
|
||||
boxSizing: 'border-box'
|
||||
};
|
||||
|
||||
const subsectionStyle = {
|
||||
marginBottom: '8px',
|
||||
paddingLeft: '10px',
|
||||
borderLeft: '2px solid #CCCCCC'
|
||||
};
|
||||
|
||||
const linkStyle = {
|
||||
color: '#615CED',
|
||||
textDecoration: 'none',
|
||||
borderBottom: '1px solid #615CED',
|
||||
transition: 'all 0.2s'
|
||||
};
|
||||
|
||||
const languageSwitchStyle = {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginBottom: '12px',
|
||||
gap: '0px',
|
||||
fontSize: '11px',
|
||||
fontFamily: "'IBM Plex Mono', monospace"
|
||||
};
|
||||
|
||||
const getLangStyle = (isActive) => ({
|
||||
padding: '4px 10px',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
background: isActive ? '#000000' : 'transparent',
|
||||
color: isActive ? '#FFFFFF' : '#666666',
|
||||
border: 'none',
|
||||
borderRadius: '2px'
|
||||
});
|
||||
|
||||
const llmLogos = [
|
||||
{ name: 'Alibaba', file: 'Alibaba.jpeg', label: 'Qwen', url: LLM_MODEL_LOGOS['Alibaba'] },
|
||||
{ name: 'DeepSeek', file: 'DeepSeek.png', label: 'DeepSeek', url: LLM_MODEL_LOGOS['DeepSeek'] },
|
||||
{ name: 'Moonshot', file: 'Moonshot.jpeg', label: 'Moonshot', url: LLM_MODEL_LOGOS['Moonshot'] },
|
||||
{ name: 'Zhipu AI', file: 'Zhipu AI.png', label: 'Zhipu AI', url: LLM_MODEL_LOGOS['Zhipu AI'] }
|
||||
];
|
||||
|
||||
const content = {
|
||||
en: {
|
||||
section1Title: "Agent Setup",
|
||||
pmRole: "Portfolio Manager",
|
||||
pmDesc: "Makes final trading decisions and orchestrates team collaboration",
|
||||
rmRole: "Risk Manager",
|
||||
rmDesc: "Monitors portfolio risk and enforces risk limits",
|
||||
analystsRole: "Analysts",
|
||||
analystsDesc: "Conduct specialized research with different tools and AI models:",
|
||||
analysts: [
|
||||
{ name: "Valuation Analyst", model: "Moonshot", modelKey: "Moonshot" },
|
||||
{ name: "Sentiment Analyst", model: "Qwen", modelKey: "Alibaba" },
|
||||
{ name: "Fundamentals Analyst", model: "DeepSeek", modelKey: "DeepSeek" },
|
||||
{ name: "Technical Analyst", model: "Zhipu AI", modelKey: "Zhipu AI" }
|
||||
],
|
||||
|
||||
section2Title: "Agent Decision Mechanism",
|
||||
|
||||
tradingProcess: "Daily Trading Process",
|
||||
tradingDesc: "Agents trade on a daily frequency while continuously tracking portfolio performance. Before each day's final trading decision, agents go through three key phases:",
|
||||
|
||||
analysisPhase: "• Analysis Phase",
|
||||
analysisDesc: "All agents independently analyze information and form judgments based on their specialized tools.",
|
||||
|
||||
communicationPhase: "• Communication Phase",
|
||||
commIntro: "Multiple communication channels enable effective collaboration: 1v1 Private Chat / 1vN Notification / NvN Conference",
|
||||
|
||||
decisionPhase: "• Decision Phase",
|
||||
decisionDesc: "Portfolio Manager aggregates all information and makes the final team trading decision. The original trading signals from analysts are only used for individual-level ranking.",
|
||||
|
||||
reflectionTitle: "Learning & Evolution",
|
||||
reflectionDesc: "Agents reflect on daily investment performance, summarize insights, and store them in ",
|
||||
remeLink: "ReMe",
|
||||
reflectionDesc2: " memory framework for continuous improvement.",
|
||||
|
||||
section3Title: "Performance Evaluation",
|
||||
|
||||
chartTitle: "• Performance Chart",
|
||||
chartDesc: "Track portfolio equity curve vs. benchmarks (equal-weight, value-weighted, momentum). Use this to assess overall strategy effectiveness.",
|
||||
|
||||
rankingTitle: "• Analyst Rankings",
|
||||
rankingDesc: "Click avatars in Trading Room to view analyst performance (Win Rate, Bull/Bear Win Rate). Use this to understand which analysts provide the most valuable insights.",
|
||||
|
||||
statsTitle: "• Statistics",
|
||||
statsDesc: "Detailed holdings and trade history. Use this for in-depth analysis of position management and execution quality.",
|
||||
|
||||
callToAction: "Fork on ",
|
||||
repoLink: "GitHub",
|
||||
callToAction2: " to customize!"
|
||||
},
|
||||
zh: {
|
||||
section1Title: "Agent 设定",
|
||||
pmRole: "投资经理",
|
||||
pmDesc: "负责最终交易决策和团队协作",
|
||||
rmRole: "风控经理",
|
||||
rmDesc: "监控组合风险并执行风险限制",
|
||||
analystsRole: "分析师",
|
||||
analystsDesc: "使用不同工具和 AI 模型进行专业研究:",
|
||||
analysts: [
|
||||
{ name: "估值分析师", model: "Moonshot", modelKey: "Moonshot" },
|
||||
{ name: "情绪分析师", model: "Qwen", modelKey: "Alibaba" },
|
||||
{ name: "基本面分析师", model: "DeepSeek", modelKey: "DeepSeek" },
|
||||
{ name: "技术分析师", model: "Zhipu AI", modelKey: "Zhipu AI" }
|
||||
],
|
||||
|
||||
section2Title: "Agent 决策机制",
|
||||
|
||||
tradingProcess: "交易流程",
|
||||
tradingDesc: "智能体以日频进行交易并持续跟踪组合净值。每天最终交易决策前,会经历三个关键阶段:",
|
||||
|
||||
analysisPhase: "• 分析阶段",
|
||||
analysisDesc: "所有智能体根据各自的工具和信息独立分析并形成判断。",
|
||||
|
||||
communicationPhase: "• 沟通阶段",
|
||||
commIntro: "提供了多种沟通渠道:1v1 私聊 / 1vN 通知 / NvN 会议",
|
||||
|
||||
decisionPhase: "• 决策阶段",
|
||||
decisionDesc: "由投资经理汇总所有信息,并给出最终的团队交易决策。分析师给出的原始交易信号仅用于个人维度排名。",
|
||||
|
||||
reflectionTitle: "学习与进化",
|
||||
reflectionDesc: "智能体根据当日实际收益反思决策、总结经验,并存入 ",
|
||||
remeLink: "ReMe",
|
||||
reflectionDesc2: " 记忆框架以持续改进。",
|
||||
|
||||
section3Title: "收益评估",
|
||||
|
||||
chartTitle: "• 业绩图表",
|
||||
chartDesc: "追踪组合收益曲线 vs. 基准策略(等权、市值加权、动量)。用于评估整体策略有效性。",
|
||||
|
||||
rankingTitle: "• 分析师排名",
|
||||
rankingDesc: "在交易室点击头像查看分析师表现(胜率、牛/熊市胜率),用来了解哪些分析师提供了最有价值的洞察。",
|
||||
|
||||
statsTitle: "• 统计数据",
|
||||
statsDesc: "详细的持仓和交易历史。用于深入分析仓位管理和执行质量。",
|
||||
|
||||
callToAction: "可在 ",
|
||||
repoLink: "GitHub",
|
||||
callToAction2: " 上 Fork 并自行定制。"
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={containerRef} style={containerStyle}>
|
||||
<div ref={contentRef} style={contentWrapperStyle}>
|
||||
<div style={innerContentStyle}>
|
||||
<div style={languageSwitchStyle}>
|
||||
<span
|
||||
style={getLangStyle(true)}
|
||||
>
|
||||
中文
|
||||
</span>
|
||||
</div>
|
||||
|
||||
// Chinese Content
|
||||
<>
|
||||
{/* 第一部分:Agent 设定 */}
|
||||
<div style={sectionTitleStyle}>{content.zh.section1Title}</div>
|
||||
|
||||
{/* 角色 */}
|
||||
<div style={{ marginBottom: '8px', fontSize: '12px' }}>
|
||||
<div style={{ marginBottom: '3px' }}>
|
||||
<span style={{ fontWeight: 600 }}>{content.zh.pmRole}:</span> {content.zh.pmDesc}
|
||||
</div>
|
||||
<div style={{ marginBottom: '3px' }}>
|
||||
<span style={{ fontWeight: 600 }}>{content.zh.rmRole}:</span> {content.zh.rmDesc}
|
||||
</div>
|
||||
<div style={{ marginBottom: '3px' }}>
|
||||
<span style={{ fontWeight: 600 }}>{content.zh.analystsRole}:</span> {content.zh.analystsDesc}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Analysts 与 AI 模型 */}
|
||||
<div style={{ marginLeft: '10px', marginBottom: '8px', display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '3px 14px', fontSize: '11px' }}>
|
||||
{content.zh.analysts.map(analyst => {
|
||||
const logo = llmLogos.find(l => l.name === analyst.modelKey);
|
||||
return (
|
||||
<div key={analyst.name} style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
}}>
|
||||
{logo && (
|
||||
<img
|
||||
src={logo.url}
|
||||
alt={logo.label}
|
||||
style={{
|
||||
height: '16px',
|
||||
width: 'auto',
|
||||
objectFit: 'contain'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<span style={{ fontWeight: 600 }}>{analyst.name}</span>
|
||||
<span style={{ color: '#666' }}>- {analyst.model}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '10px', fontSize: '11px', fontStyle: 'italic', opacity: 0.8 }}>
|
||||
{content.zh.callToAction}
|
||||
<a
|
||||
href="https://github.com/agentscope-ai/agentscope-samples"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={linkStyle}
|
||||
>
|
||||
{content.zh.repoLink}
|
||||
</a>
|
||||
{content.zh.callToAction2}
|
||||
</div>
|
||||
|
||||
{/* 第二部分:Agent 决策机制 */}
|
||||
<div style={sectionTitleStyle}>{content.zh.section2Title}</div>
|
||||
|
||||
<div style={{ marginBottom: '6px' }}>
|
||||
<div style={{ fontWeight: 600, marginBottom: '3px' }}>{content.zh.tradingProcess}</div>
|
||||
<div style={{ marginBottom: '6px', fontSize: '12px' }}>{content.zh.tradingDesc}</div>
|
||||
|
||||
<div style={subsectionStyle}>
|
||||
<div style={{ marginBottom: '4px', fontSize: '12px' }}>
|
||||
<span style={highlight}>{content.zh.analysisPhase.replace('• ', '')}:</span> {content.zh.analysisDesc}
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '4px', fontSize: '12px' }}>
|
||||
<span style={highlight}>{content.zh.communicationPhase.replace('• ', '')}:</span> {content.zh.commIntro}
|
||||
</div>
|
||||
|
||||
<div style={{ fontSize: '12px' }}>
|
||||
<span style={highlight}>{content.zh.decisionPhase.replace('• ', '')}:</span> {content.zh.decisionDesc}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '10px' }}>
|
||||
<div style={{ fontWeight: 600, marginBottom: '3px' }}>{content.zh.reflectionTitle}</div>
|
||||
<div style={{ fontSize: '12px' }}>
|
||||
{content.zh.reflectionDesc}
|
||||
<a
|
||||
href="https://github.com/agentscope-ai/ReMe"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={linkStyle}
|
||||
>
|
||||
{content.zh.remeLink}
|
||||
</a>
|
||||
{content.zh.reflectionDesc2}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 第三部分:收益评估 */}
|
||||
<div style={sectionTitleStyle}>{content.zh.section3Title}</div>
|
||||
<div style={subsectionStyle}>
|
||||
<div style={{ marginBottom: '3px', fontSize: '12px' }}>
|
||||
<span style={{ fontWeight: 600 }}>{content.zh.chartTitle.replace('• ', '')}:</span> {content.zh.chartDesc}
|
||||
</div>
|
||||
<div style={{ marginBottom: '3px', fontSize: '12px' }}>
|
||||
<span style={{ fontWeight: 600 }}>{content.zh.rankingTitle.replace('• ', '')}:</span> {content.zh.rankingDesc}
|
||||
</div>
|
||||
<div style={{ fontSize: '12px' }}>
|
||||
<span style={{ fontWeight: 600 }}>{content.zh.statsTitle.replace('• ', '')}:</span> {content.zh.statsDesc}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
1170
frontend/src/components/StockExplainView.jsx
Normal file
1170
frontend/src/components/StockExplainView.jsx
Normal file
File diff suppressed because it is too large
Load Diff
244
frontend/src/components/WatchlistPanel.jsx
Normal file
244
frontend/src/components/WatchlistPanel.jsx
Normal file
@@ -0,0 +1,244 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function WatchlistPanel({
|
||||
isOpen,
|
||||
isConnected,
|
||||
isSaving,
|
||||
draftSymbols,
|
||||
inputValue,
|
||||
feedback,
|
||||
suggestions,
|
||||
onToggle,
|
||||
onClose,
|
||||
onInputChange,
|
||||
onInputKeyDown,
|
||||
onAdd,
|
||||
onRemove,
|
||||
onRestoreCurrent,
|
||||
onRestoreDefault,
|
||||
onSuggestionClick,
|
||||
onSave
|
||||
}) {
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, minWidth: 0, position: 'relative' }}>
|
||||
<button
|
||||
onClick={onToggle}
|
||||
style={{
|
||||
padding: '6px 10px',
|
||||
borderRadius: 4,
|
||||
border: '1px solid #333333',
|
||||
background: isOpen ? '#1E1E1E' : '#111111',
|
||||
color: '#FFFFFF',
|
||||
fontSize: '11px',
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.6px',
|
||||
cursor: 'pointer',
|
||||
whiteSpace: 'nowrap'
|
||||
}}
|
||||
>
|
||||
WATCHLIST
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: 'calc(100% + 10px)',
|
||||
right: 0,
|
||||
width: 360,
|
||||
maxWidth: 'min(360px, 92vw)',
|
||||
padding: '14px',
|
||||
borderRadius: 8,
|
||||
border: '1px solid #D9D9D9',
|
||||
background: '#FFFFFF',
|
||||
boxShadow: '0 12px 36px rgba(0, 0, 0, 0.14)',
|
||||
zIndex: 40,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 12
|
||||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 12 }}>
|
||||
<div>
|
||||
<div style={{ fontSize: '12px', fontWeight: 700, color: '#111111', letterSpacing: '0.3px' }}>
|
||||
自选股管理
|
||||
</div>
|
||||
<div style={{ fontSize: '11px', color: '#666666', marginTop: 2 }}>
|
||||
保存后会立即更新当前 run 的 watchlist
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
color: '#666666',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px',
|
||||
lineHeight: 1
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 8,
|
||||
minHeight: 36,
|
||||
padding: '2px 0'
|
||||
}}>
|
||||
{draftSymbols.map((symbol) => (
|
||||
<button
|
||||
key={symbol}
|
||||
onClick={() => onRemove(symbol)}
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
padding: '6px 10px',
|
||||
borderRadius: 999,
|
||||
border: '1px solid #D0D7DE',
|
||||
background: '#F7F9FB',
|
||||
color: '#111111',
|
||||
fontSize: '11px',
|
||||
fontWeight: 700,
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
<span>{symbol}</span>
|
||||
<span style={{ color: '#777777' }}>×</span>
|
||||
</button>
|
||||
))}
|
||||
{draftSymbols.length === 0 && (
|
||||
<div style={{ fontSize: '11px', color: '#888888', padding: '8px 2px' }}>
|
||||
还没有股票,输入代码后回车添加
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<input
|
||||
value={inputValue}
|
||||
onChange={(e) => onInputChange(e.target.value)}
|
||||
onKeyDown={onInputKeyDown}
|
||||
placeholder="输入股票代码,回车添加"
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '9px 10px',
|
||||
borderRadius: 6,
|
||||
border: '1px solid #D0D7DE',
|
||||
background: '#FFFFFF',
|
||||
color: '#111111',
|
||||
fontSize: '12px',
|
||||
fontFamily: '"Courier New", monospace'
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={onAdd}
|
||||
style={{
|
||||
padding: '9px 12px',
|
||||
borderRadius: 6,
|
||||
border: '1px solid #D0D7DE',
|
||||
background: '#F7F9FB',
|
||||
color: '#111111',
|
||||
fontSize: '11px',
|
||||
fontWeight: 700,
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
添加
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
|
||||
{suggestions.map((symbol) => {
|
||||
const active = draftSymbols.includes(symbol);
|
||||
return (
|
||||
<button
|
||||
key={symbol}
|
||||
onClick={() => onSuggestionClick(symbol)}
|
||||
disabled={active}
|
||||
style={{
|
||||
padding: '5px 8px',
|
||||
borderRadius: 999,
|
||||
border: '1px solid',
|
||||
borderColor: active ? '#B6E3C5' : '#D0D7DE',
|
||||
background: active ? '#ECFDF3' : '#FFFFFF',
|
||||
color: active ? '#157347' : '#4A5568',
|
||||
fontSize: '10px',
|
||||
fontWeight: 700,
|
||||
cursor: active ? 'default' : 'pointer'
|
||||
}}
|
||||
>
|
||||
{symbol}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||||
<button
|
||||
onClick={onRestoreCurrent}
|
||||
style={{
|
||||
padding: '8px 10px',
|
||||
borderRadius: 6,
|
||||
border: '1px solid #D0D7DE',
|
||||
background: '#FFFFFF',
|
||||
color: '#111111',
|
||||
fontSize: '11px',
|
||||
fontWeight: 700,
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
恢复当前
|
||||
</button>
|
||||
<button
|
||||
onClick={onRestoreDefault}
|
||||
style={{
|
||||
padding: '8px 10px',
|
||||
borderRadius: 6,
|
||||
border: '1px solid #D0D7DE',
|
||||
background: '#FFFFFF',
|
||||
color: '#111111',
|
||||
fontSize: '11px',
|
||||
fontWeight: 700,
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
恢复默认
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={onSave}
|
||||
disabled={!isConnected || isSaving}
|
||||
style={{
|
||||
padding: '9px 14px',
|
||||
borderRadius: 6,
|
||||
border: '1px solid #1565C0',
|
||||
background: isConnected && !isSaving ? '#0D47A1' : '#94A3B8',
|
||||
color: '#FFFFFF',
|
||||
fontSize: '11px',
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.4px',
|
||||
cursor: isConnected && !isSaving ? 'pointer' : 'not-allowed'
|
||||
}}
|
||||
>
|
||||
{isSaving ? '保存中' : '保存'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{feedback && (
|
||||
<span style={{
|
||||
color: feedback.type === 'success' ? '#00C853' : '#FF5252',
|
||||
fontSize: '11px',
|
||||
fontFamily: '"Courier New", monospace'
|
||||
}}>
|
||||
{feedback.text}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,88 @@ import { AGENTS } from "../config/constants";
|
||||
|
||||
const MAX_FEED_ITEMS = 200;
|
||||
|
||||
const normalizeSystemContent = (content) => {
|
||||
if (typeof content !== "string") {
|
||||
return content;
|
||||
}
|
||||
|
||||
const trimmed = content.trim();
|
||||
if (!trimmed) {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
if (trimmed === "Runtime assets reloaded." || trimmed === "运行时配置已热更新") {
|
||||
return "配置已刷新";
|
||||
}
|
||||
|
||||
if (trimmed.startsWith("Watchlist updated:")) {
|
||||
const symbols = trimmed.replace("Watchlist updated:", "").trim();
|
||||
return symbols ? `自选已更新: ${symbols}` : "自选已更新";
|
||||
}
|
||||
|
||||
if (trimmed === "已连接实时数据服务") {
|
||||
return "已连接";
|
||||
}
|
||||
|
||||
if (trimmed === "正在尝试连接数据服务...") {
|
||||
return "连接中...";
|
||||
}
|
||||
|
||||
if (trimmed.startsWith("day_start:")) {
|
||||
const value = trimmed.replace("day_start:", "").trim();
|
||||
return value ? `交易日开始:${value}` : "交易日开始";
|
||||
}
|
||||
|
||||
if (trimmed.startsWith("day_complete:")) {
|
||||
const value = trimmed.replace("day_complete:", "").trim();
|
||||
return value ? `交易日结束:${value}` : "交易日结束";
|
||||
}
|
||||
|
||||
if (trimmed.startsWith("day_error:")) {
|
||||
const value = trimmed.replace("day_error:", "").trim();
|
||||
return value ? `交易日异常:${value}` : "交易日异常";
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
};
|
||||
|
||||
const normalizeConferenceTitle = (title) => {
|
||||
if (typeof title !== "string") {
|
||||
return "投资讨论";
|
||||
}
|
||||
|
||||
const trimmed = title.trim();
|
||||
if (!trimmed) {
|
||||
return "投资讨论";
|
||||
}
|
||||
|
||||
if (trimmed.startsWith("Investment Discussion -")) {
|
||||
const date = trimmed.replace("Investment Discussion -", "").trim();
|
||||
return date ? `投资讨论 · ${date}` : "投资讨论";
|
||||
}
|
||||
|
||||
if (trimmed === "Team Conference") {
|
||||
return "投资讨论";
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
};
|
||||
|
||||
const normalizeAgentLabel = (agentName, agentId) => {
|
||||
if (typeof agentName === "string") {
|
||||
const trimmed = agentName.trim();
|
||||
if (trimmed.toLowerCase() === "conference summary") {
|
||||
return "会议总结";
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof agentId === "string" && agentId.trim().toLowerCase() === "conference summary") {
|
||||
return "会议总结";
|
||||
}
|
||||
|
||||
return agentName;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a unique ID for feed items
|
||||
*/
|
||||
@@ -26,7 +108,7 @@ const eventToMessage = (evt) => {
|
||||
id: generateId("msg"),
|
||||
timestamp,
|
||||
agentId: evt.agentId,
|
||||
agent: agent?.name || evt.agentName || evt.agentId || "Agent",
|
||||
agent: normalizeAgentLabel(agent?.name || evt.agentName || evt.agentId || "Agent", evt.agentId),
|
||||
role: agent?.role || evt.role || "Agent",
|
||||
content: evt.content
|
||||
};
|
||||
@@ -50,7 +132,7 @@ const eventToMessage = (evt) => {
|
||||
timestamp,
|
||||
agent: "System",
|
||||
role: "System",
|
||||
content: evt.content || `${evt.type}: ${evt.date || ""}`
|
||||
content: normalizeSystemContent(evt.content || `${evt.type}: ${evt.date || ""}`)
|
||||
};
|
||||
|
||||
default:
|
||||
@@ -129,7 +211,7 @@ export function useFeedProcessor() {
|
||||
// Start a new conference
|
||||
currentConference = {
|
||||
id: evt.conferenceId || generateId("conf"),
|
||||
title: evt.title || "Team Conference",
|
||||
title: normalizeConferenceTitle(evt.title || "Team Conference"),
|
||||
startTime: evt.timestamp || evt.ts || Date.now(),
|
||||
endTime: null,
|
||||
isLive: false,
|
||||
@@ -209,7 +291,7 @@ export function useFeedProcessor() {
|
||||
if (evt.type === "conference_start") {
|
||||
const conference = {
|
||||
id: evt.conferenceId || generateId("conf"),
|
||||
title: evt.title || "Team Conference",
|
||||
title: normalizeConferenceTitle(evt.title || "Team Conference"),
|
||||
startTime: evt.timestamp || evt.ts || Date.now(),
|
||||
endTime: null,
|
||||
isLive: true,
|
||||
@@ -312,7 +394,7 @@ export function useFeedProcessor() {
|
||||
timestamp: Date.now(),
|
||||
agent: "System",
|
||||
role: "System",
|
||||
content
|
||||
content: normalizeSystemContent(content)
|
||||
};
|
||||
|
||||
const activeConf = activeConferenceRef.current;
|
||||
|
||||
@@ -1030,8 +1030,9 @@ export default function GlobalStyles() {
|
||||
/* Three-view slider (Room / Chart / Statistics) */
|
||||
.view-slider-three {
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
width: 300%;
|
||||
height: 100%;
|
||||
height: calc(100% - 40px);
|
||||
display: flex;
|
||||
transition: transform 1.6s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
@@ -1052,7 +1053,7 @@ export default function GlobalStyles() {
|
||||
transform: translateX(-66.666%);
|
||||
}
|
||||
|
||||
/* Four-view slider (Rules / Room / Chart / Statistics) */
|
||||
/* Four-view slider (Room / Explain / Chart / Statistics) */
|
||||
.view-slider-four {
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
@@ -1066,11 +1067,11 @@ export default function GlobalStyles() {
|
||||
transition: transform 0.8s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
.view-slider-four.show-rules {
|
||||
.view-slider-four.show-room {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.view-slider-four.show-room {
|
||||
.view-slider-four.show-explain {
|
||||
transform: translateX(-25%);
|
||||
}
|
||||
|
||||
@@ -1873,4 +1874,3 @@ export default function GlobalStyles() {
|
||||
`}</style>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
303
live/state/server_state.json
Normal file
303
live/state/server_state.json
Normal file
File diff suppressed because one or more lines are too long
1
live/team_dashboard/holdings.json
Normal file
1
live/team_dashboard/holdings.json
Normal file
@@ -0,0 +1 @@
|
||||
[]
|
||||
134
live/team_dashboard/leaderboard.json
Normal file
134
live/team_dashboard/leaderboard.json
Normal file
@@ -0,0 +1,134 @@
|
||||
[
|
||||
{
|
||||
"agentId": "portfolio_manager",
|
||||
"name": "投资经理",
|
||||
"role": "投资经理",
|
||||
"avatar": "pm",
|
||||
"rank": null,
|
||||
"winRate": null,
|
||||
"bull": {
|
||||
"n": 0,
|
||||
"win": 0,
|
||||
"unknown": 0
|
||||
},
|
||||
"bear": {
|
||||
"n": 0,
|
||||
"win": 0,
|
||||
"unknown": 0
|
||||
},
|
||||
"logs": [],
|
||||
"signals": [],
|
||||
"modelName": "deepseek-v3.2",
|
||||
"modelProvider": "DASHSCOPE"
|
||||
},
|
||||
{
|
||||
"agentId": "risk_manager",
|
||||
"name": "风控经理",
|
||||
"role": "风控经理",
|
||||
"avatar": "risk",
|
||||
"rank": null,
|
||||
"winRate": null,
|
||||
"bull": {
|
||||
"n": 0,
|
||||
"win": 0,
|
||||
"unknown": 0
|
||||
},
|
||||
"bear": {
|
||||
"n": 0,
|
||||
"win": 0,
|
||||
"unknown": 0
|
||||
},
|
||||
"logs": [],
|
||||
"signals": [],
|
||||
"modelName": "deepseek-v3.2",
|
||||
"modelProvider": "DASHSCOPE"
|
||||
},
|
||||
{
|
||||
"agentId": "sentiment_analyst",
|
||||
"name": "情绪分析师",
|
||||
"role": "情绪分析师",
|
||||
"avatar": "sentiment",
|
||||
"rank": 0,
|
||||
"winRate": null,
|
||||
"bull": {
|
||||
"n": 0,
|
||||
"win": 0,
|
||||
"unknown": 0
|
||||
},
|
||||
"bear": {
|
||||
"n": 0,
|
||||
"win": 0,
|
||||
"unknown": 0
|
||||
},
|
||||
"logs": [],
|
||||
"signals": [],
|
||||
"modelName": "deepseek-v3.2",
|
||||
"modelProvider": "DASHSCOPE"
|
||||
},
|
||||
{
|
||||
"agentId": "technical_analyst",
|
||||
"name": "技术分析师",
|
||||
"role": "技术分析师",
|
||||
"avatar": "technical",
|
||||
"rank": 0,
|
||||
"winRate": null,
|
||||
"bull": {
|
||||
"n": 0,
|
||||
"win": 0,
|
||||
"unknown": 0
|
||||
},
|
||||
"bear": {
|
||||
"n": 0,
|
||||
"win": 0,
|
||||
"unknown": 0
|
||||
},
|
||||
"logs": [],
|
||||
"signals": [],
|
||||
"modelName": "deepseek-v3.2",
|
||||
"modelProvider": "DASHSCOPE"
|
||||
},
|
||||
{
|
||||
"agentId": "fundamentals_analyst",
|
||||
"name": "基本面分析师",
|
||||
"role": "基本面分析师",
|
||||
"avatar": "fundamentals",
|
||||
"rank": 0,
|
||||
"winRate": null,
|
||||
"bull": {
|
||||
"n": 0,
|
||||
"win": 0,
|
||||
"unknown": 0
|
||||
},
|
||||
"bear": {
|
||||
"n": 0,
|
||||
"win": 0,
|
||||
"unknown": 0
|
||||
},
|
||||
"logs": [],
|
||||
"signals": [],
|
||||
"modelName": "deepseek-v3.2",
|
||||
"modelProvider": "DASHSCOPE"
|
||||
},
|
||||
{
|
||||
"agentId": "valuation_analyst",
|
||||
"name": "估值分析师",
|
||||
"role": "估值分析师",
|
||||
"avatar": "valuation",
|
||||
"rank": 0,
|
||||
"winRate": null,
|
||||
"bull": {
|
||||
"n": 0,
|
||||
"win": 0,
|
||||
"unknown": 0
|
||||
},
|
||||
"bear": {
|
||||
"n": 0,
|
||||
"win": 0,
|
||||
"unknown": 0
|
||||
},
|
||||
"logs": [],
|
||||
"signals": [],
|
||||
"modelName": "deepseek-v3.2",
|
||||
"modelProvider": "DASHSCOPE"
|
||||
}
|
||||
]
|
||||
18
live/team_dashboard/stats.json
Normal file
18
live/team_dashboard/stats.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"totalAssetValue": 100000.0,
|
||||
"totalReturn": 0.0,
|
||||
"cashPosition": 100000.0,
|
||||
"tickerWeights": {},
|
||||
"totalTrades": 0,
|
||||
"winRate": 0.0,
|
||||
"bullBear": {
|
||||
"bull": {
|
||||
"n": 0,
|
||||
"win": 0
|
||||
},
|
||||
"bear": {
|
||||
"n": 0,
|
||||
"win": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
13
live/team_dashboard/summary.json
Normal file
13
live/team_dashboard/summary.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"totalAssetValue": 100000.0,
|
||||
"totalReturn": 0.0,
|
||||
"cashPosition": 100000.0,
|
||||
"tickerWeights": {},
|
||||
"totalTrades": 0,
|
||||
"pnlPct": 0.0,
|
||||
"balance": 100000.0,
|
||||
"equity": [],
|
||||
"baseline": [],
|
||||
"baseline_vw": [],
|
||||
"momentum": []
|
||||
}
|
||||
1
live/team_dashboard/trades.json
Normal file
1
live/team_dashboard/trades.json
Normal file
@@ -0,0 +1 @@
|
||||
[]
|
||||
1
reference/PokieTicker
Submodule
1
reference/PokieTicker
Submodule
Submodule reference/PokieTicker added at 4fed7755e5
18
runs/reload_demo/BOOTSTRAP.md
Normal file
18
runs/reload_demo/BOOTSTRAP.md
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
tickers:
|
||||
- NVDA
|
||||
- AMD
|
||||
initial_cash: 250000
|
||||
margin_requirement: 0.5
|
||||
enable_memory: false
|
||||
max_comm_cycles: 1
|
||||
agent_overrides:
|
||||
risk_manager:
|
||||
active_tool_groups:
|
||||
- risk_ops
|
||||
---
|
||||
|
||||
# Bootstrap
|
||||
|
||||
Prefer semiconductor names in this run.
|
||||
Keep decisions terse and explicitly mention concentration risk.
|
||||
5
runs/reload_demo/agents/fundamentals_analyst/POLICY.md
Normal file
5
runs/reload_demo/agents/fundamentals_analyst/POLICY.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Policy
|
||||
|
||||
Optional run-scoped constraints, limits, or strategy policy.
|
||||
|
||||
State a clear signal, confidence, and the conditions that would invalidate the thesis.
|
||||
11
runs/reload_demo/agents/fundamentals_analyst/ROLE.md
Normal file
11
runs/reload_demo/agents/fundamentals_analyst/ROLE.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Role
|
||||
|
||||
Optional run-scoped role override.
|
||||
|
||||
作为基本面分析师,你专注于:
|
||||
- 公司财务健康状况和盈利能力
|
||||
- 商业模式可持续性和竞争优势
|
||||
- 管理层质量和公司治理
|
||||
- 行业地位和市场份额
|
||||
- 长期投资价值评估
|
||||
你倾向于选择能够深入了解公司内在价值的工具,更偏好基本面和估值类工具。
|
||||
9
runs/reload_demo/agents/fundamentals_analyst/STYLE.md
Normal file
9
runs/reload_demo/agents/fundamentals_analyst/STYLE.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Style
|
||||
|
||||
Optional run-scoped communication or reasoning style.
|
||||
|
||||
- 公司财务健康状况和盈利能力
|
||||
- 商业模式可持续性和竞争优势
|
||||
- 管理层质量和公司治理
|
||||
- 行业地位和市场份额
|
||||
- 长期投资价值评估
|
||||
5
runs/reload_demo/agents/portfolio_manager/POLICY.md
Normal file
5
runs/reload_demo/agents/portfolio_manager/POLICY.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Policy
|
||||
|
||||
Optional run-scoped constraints, limits, or strategy policy.
|
||||
|
||||
Respect cash, margin, and portfolio concentration constraints before recording decisions.
|
||||
5
runs/reload_demo/agents/portfolio_manager/ROLE.md
Normal file
5
runs/reload_demo/agents/portfolio_manager/ROLE.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Role
|
||||
|
||||
Optional run-scoped role override.
|
||||
|
||||
Synthesize analyst and risk inputs into explicit portfolio decisions.
|
||||
5
runs/reload_demo/agents/portfolio_manager/STYLE.md
Normal file
5
runs/reload_demo/agents/portfolio_manager/STYLE.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Style
|
||||
|
||||
Optional run-scoped communication or reasoning style.
|
||||
|
||||
Be concise, capital-aware, and explicit about sizing rationale.
|
||||
5
runs/reload_demo/agents/risk_manager/POLICY.md
Normal file
5
runs/reload_demo/agents/risk_manager/POLICY.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Policy
|
||||
|
||||
Optional run-scoped constraints, limits, or strategy policy.
|
||||
|
||||
Use available risk tools before issuing the final risk memo.
|
||||
5
runs/reload_demo/agents/risk_manager/ROLE.md
Normal file
5
runs/reload_demo/agents/risk_manager/ROLE.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Role
|
||||
|
||||
Optional run-scoped role override.
|
||||
|
||||
Quantify concentration, leverage, liquidity, and volatility risk before trade execution.
|
||||
6
runs/reload_demo/agents/risk_manager/STYLE.md
Normal file
6
runs/reload_demo/agents/risk_manager/STYLE.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Style
|
||||
|
||||
Optional run-scoped communication or reasoning style.
|
||||
|
||||
Prioritize the highest-severity semiconductor risk first.
|
||||
Always state one concrete position limit and one liquidation trigger.
|
||||
5
runs/reload_demo/agents/sentiment_analyst/POLICY.md
Normal file
5
runs/reload_demo/agents/sentiment_analyst/POLICY.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Policy
|
||||
|
||||
Optional run-scoped constraints, limits, or strategy policy.
|
||||
|
||||
State a clear signal, confidence, and the conditions that would invalidate the thesis.
|
||||
11
runs/reload_demo/agents/sentiment_analyst/ROLE.md
Normal file
11
runs/reload_demo/agents/sentiment_analyst/ROLE.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Role
|
||||
|
||||
Optional run-scoped role override.
|
||||
|
||||
作为情绪分析师,你专注于:
|
||||
- 市场参与者情绪变化
|
||||
- 新闻舆情和媒体影响
|
||||
- 内部人交易行为
|
||||
- 投资者恐慌和贪婪情绪
|
||||
- 市场预期和心理因素
|
||||
你倾向于选择能够反映市场情绪和投资者行为的工具,更偏好情绪和行为类工具。
|
||||
9
runs/reload_demo/agents/sentiment_analyst/STYLE.md
Normal file
9
runs/reload_demo/agents/sentiment_analyst/STYLE.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Style
|
||||
|
||||
Optional run-scoped communication or reasoning style.
|
||||
|
||||
- 市场参与者情绪变化
|
||||
- 新闻舆情和媒体影响
|
||||
- 内部人交易行为
|
||||
- 投资者恐慌和贪婪情绪
|
||||
- 市场预期和心理因素
|
||||
5
runs/reload_demo/agents/technical_analyst/POLICY.md
Normal file
5
runs/reload_demo/agents/technical_analyst/POLICY.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Policy
|
||||
|
||||
Optional run-scoped constraints, limits, or strategy policy.
|
||||
|
||||
State a clear signal, confidence, and the conditions that would invalidate the thesis.
|
||||
11
runs/reload_demo/agents/technical_analyst/ROLE.md
Normal file
11
runs/reload_demo/agents/technical_analyst/ROLE.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Role
|
||||
|
||||
Optional run-scoped role override.
|
||||
|
||||
作为技术分析师,你专注于:
|
||||
- 价格趋势和图表形态
|
||||
- 技术指标和交易信号
|
||||
- 市场情绪和资金流向
|
||||
- 支撑/阻力位和关键价格点
|
||||
- 中短期交易机会
|
||||
你倾向于选择能够捕捉价格动态和市场趋势的工具,更偏好技术分析类工具。
|
||||
9
runs/reload_demo/agents/technical_analyst/STYLE.md
Normal file
9
runs/reload_demo/agents/technical_analyst/STYLE.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Style
|
||||
|
||||
Optional run-scoped communication or reasoning style.
|
||||
|
||||
- 价格趋势和图表形态
|
||||
- 技术指标和交易信号
|
||||
- 市场情绪和资金流向
|
||||
- 支撑/阻力位和关键价格点
|
||||
- 中短期交易机会
|
||||
5
runs/reload_demo/agents/valuation_analyst/POLICY.md
Normal file
5
runs/reload_demo/agents/valuation_analyst/POLICY.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Policy
|
||||
|
||||
Optional run-scoped constraints, limits, or strategy policy.
|
||||
|
||||
State a clear signal, confidence, and the conditions that would invalidate the thesis.
|
||||
11
runs/reload_demo/agents/valuation_analyst/ROLE.md
Normal file
11
runs/reload_demo/agents/valuation_analyst/ROLE.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Role
|
||||
|
||||
Optional run-scoped role override.
|
||||
|
||||
作为估值分析师,你专注于:
|
||||
- 公司内在价值计算
|
||||
- 不同估值方法的比较
|
||||
- 估值模型假设和敏感性分析
|
||||
- 相对估值和绝对估值
|
||||
- 投资安全边际评估
|
||||
你倾向于选择能够准确计算公司价值的工具,更偏好估值模型和基本面工具。
|
||||
9
runs/reload_demo/agents/valuation_analyst/STYLE.md
Normal file
9
runs/reload_demo/agents/valuation_analyst/STYLE.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Style
|
||||
|
||||
Optional run-scoped communication or reasoning style.
|
||||
|
||||
- 公司内在价值计算
|
||||
- 不同估值方法的比较
|
||||
- 估值模型假设和敏感性分析
|
||||
- 相对估值和绝对估值
|
||||
- 投资安全边际评估
|
||||
8
runs/reload_demo/skills/activation.yaml
Normal file
8
runs/reload_demo/skills/activation.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
global_enabled_skills: []
|
||||
global_disabled_skills: []
|
||||
agent_enabled_skills:
|
||||
risk_manager:
|
||||
- technical_review
|
||||
portfolio_manager:
|
||||
- risk_review
|
||||
agent_disabled_skills: {}
|
||||
21
runs/reload_demo/skills/active/fundamental_review/SKILL.md
Normal file
21
runs/reload_demo/skills/active/fundamental_review/SKILL.md
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
name: fundamental_review
|
||||
description: Review a company from a fundamentals-first perspective before issuing a trading signal.
|
||||
---
|
||||
|
||||
# Fundamental Review
|
||||
|
||||
Use this skill when the task requires judging business quality, balance-sheet strength, profitability, or long-term earnings durability.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Check profitability, growth, financial health, and efficiency before forming a conclusion.
|
||||
2. Separate durable business quality from short-term noise.
|
||||
3. State what would invalidate the thesis.
|
||||
4. End with a clear signal, confidence, and the main drivers behind that signal.
|
||||
|
||||
## Guardrails
|
||||
|
||||
- Do not rely on one metric in isolation.
|
||||
- Call out missing data explicitly.
|
||||
- Prefer conservative conclusions when financial quality is mixed.
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
name: portfolio_decisioning
|
||||
description: Synthesize analyst inputs and risk feedback into explicit portfolio decisions.
|
||||
---
|
||||
|
||||
# Portfolio Decisioning
|
||||
|
||||
Use this skill when you are responsible for converting team analysis into final trades.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Read analyst conclusions and risk warnings before acting.
|
||||
2. Evaluate the current portfolio, cash, and margin constraints.
|
||||
3. Record one explicit decision per ticker using the decision tool.
|
||||
4. Summarize the portfolio-level rationale after all decisions are recorded.
|
||||
|
||||
## Guardrails
|
||||
|
||||
- Position sizing must respect capital and margin limits.
|
||||
- Prefer smaller size when analyst conviction and risk signals disagree.
|
||||
- Do not leave a ticker undecided when the task expects a full slate of decisions.
|
||||
21
runs/reload_demo/skills/active/risk_review/SKILL.md
Normal file
21
runs/reload_demo/skills/active/risk_review/SKILL.md
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
name: risk_review
|
||||
description: Assess portfolio and market risks before final position sizing and execution.
|
||||
---
|
||||
|
||||
# Risk Review
|
||||
|
||||
Use this skill when you must identify concentration, volatility, leverage, and scenario risks.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Review the proposed exposure by ticker and theme.
|
||||
2. Identify concentration, volatility, liquidity, and leverage concerns.
|
||||
3. Rank warnings by severity.
|
||||
4. Translate risk findings into concrete limits or cautions for the portfolio manager.
|
||||
|
||||
## Guardrails
|
||||
|
||||
- Focus on actionable risk controls.
|
||||
- Quantify limits when the available data supports it.
|
||||
- Distinguish fatal blockers from manageable risks.
|
||||
21
runs/reload_demo/skills/active/sentiment_review/SKILL.md
Normal file
21
runs/reload_demo/skills/active/sentiment_review/SKILL.md
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
name: sentiment_review
|
||||
description: Analyze news flow, market psychology, and insider behavior for catalyst-driven signals.
|
||||
---
|
||||
|
||||
# Sentiment Review
|
||||
|
||||
Use this skill when the task depends on recent catalysts, news tone, or behavioral market signals.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Review recent news and identify the dominant narrative.
|
||||
2. Check insider activity for confirming or conflicting signals.
|
||||
3. Separate durable sentiment shifts from transient noise.
|
||||
4. Explain how sentiment changes the near-term trade outlook.
|
||||
|
||||
## Guardrails
|
||||
|
||||
- Do not confuse attention with conviction.
|
||||
- Highlight when sentiment is strong but unsupported by fundamentals.
|
||||
- Be explicit about catalyst timing risk.
|
||||
21
runs/reload_demo/skills/active/technical_review/SKILL.md
Normal file
21
runs/reload_demo/skills/active/technical_review/SKILL.md
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
name: technical_review
|
||||
description: Evaluate price action, momentum, and volatility to judge timing and market regime.
|
||||
---
|
||||
|
||||
# Technical Review
|
||||
|
||||
Use this skill when the task is sensitive to entry timing, trend quality, or short-term market structure.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Assess trend direction and strength.
|
||||
2. Check momentum and mean-reversion conditions.
|
||||
3. Review volatility before making aggressive recommendations.
|
||||
4. Convert the setup into a trading view with explicit risk awareness.
|
||||
|
||||
## Guardrails
|
||||
|
||||
- Distinguish trend continuation from overshoot.
|
||||
- Avoid strong conviction when signals conflict.
|
||||
- Treat volatility as a sizing input, not only a directional input.
|
||||
21
runs/reload_demo/skills/active/valuation_review/SKILL.md
Normal file
21
runs/reload_demo/skills/active/valuation_review/SKILL.md
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
name: valuation_review
|
||||
description: Estimate fair value and margin of safety using multiple valuation lenses.
|
||||
---
|
||||
|
||||
# Valuation Review
|
||||
|
||||
Use this skill when the task requires determining whether a stock is cheap, expensive, or fairly priced.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Use more than one valuation method when possible.
|
||||
2. Compare intrinsic value estimates with current market pricing.
|
||||
3. Explain the key assumptions behind the valuation view.
|
||||
4. State the margin of safety and what could compress or expand it.
|
||||
|
||||
## Guardrails
|
||||
|
||||
- Treat valuation as a range, not a single precise number.
|
||||
- Call out assumption sensitivity.
|
||||
- Avoid high-confidence calls when inputs are sparse or unstable.
|
||||
206
runs/reload_demo/state/server_state.json
Normal file
206
runs/reload_demo/state/server_state.json
Normal file
@@ -0,0 +1,206 @@
|
||||
{
|
||||
"status": "running",
|
||||
"current_date": "2026-03-16",
|
||||
"portfolio": {
|
||||
"total_value": 250000.0,
|
||||
"cash": 250000.0,
|
||||
"pnl_percent": 0.0,
|
||||
"equity": [],
|
||||
"baseline": [],
|
||||
"baseline_vw": [],
|
||||
"momentum": [],
|
||||
"strategies": [],
|
||||
"equity_return": [],
|
||||
"baseline_return": [],
|
||||
"baseline_vw_return": [],
|
||||
"momentum_return": []
|
||||
},
|
||||
"holdings": [],
|
||||
"trades": [],
|
||||
"stats": {
|
||||
"totalAssetValue": 250000.0,
|
||||
"totalReturn": 0.0,
|
||||
"cashPosition": 250000.0,
|
||||
"tickerWeights": {},
|
||||
"totalTrades": 0,
|
||||
"winRate": 0.0,
|
||||
"bullBear": {
|
||||
"bull": {
|
||||
"n": 0,
|
||||
"win": 0
|
||||
},
|
||||
"bear": {
|
||||
"n": 0,
|
||||
"win": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"leaderboard": [
|
||||
{
|
||||
"agentId": "portfolio_manager",
|
||||
"name": "投资经理",
|
||||
"role": "投资经理",
|
||||
"avatar": "pm",
|
||||
"rank": null,
|
||||
"winRate": null,
|
||||
"bull": {
|
||||
"n": 0,
|
||||
"win": 0,
|
||||
"unknown": 0
|
||||
},
|
||||
"bear": {
|
||||
"n": 0,
|
||||
"win": 0,
|
||||
"unknown": 0
|
||||
},
|
||||
"logs": [],
|
||||
"signals": [],
|
||||
"modelName": "deepseek-v3.2",
|
||||
"modelProvider": "DASHSCOPE"
|
||||
},
|
||||
{
|
||||
"agentId": "risk_manager",
|
||||
"name": "风控经理",
|
||||
"role": "风控经理",
|
||||
"avatar": "risk",
|
||||
"rank": null,
|
||||
"winRate": null,
|
||||
"bull": {
|
||||
"n": 0,
|
||||
"win": 0,
|
||||
"unknown": 0
|
||||
},
|
||||
"bear": {
|
||||
"n": 0,
|
||||
"win": 0,
|
||||
"unknown": 0
|
||||
},
|
||||
"logs": [],
|
||||
"signals": [],
|
||||
"modelName": "deepseek-v3.2",
|
||||
"modelProvider": "DASHSCOPE"
|
||||
},
|
||||
{
|
||||
"agentId": "sentiment_analyst",
|
||||
"name": "情绪分析师",
|
||||
"role": "情绪分析师",
|
||||
"avatar": "sentiment",
|
||||
"rank": 0,
|
||||
"winRate": null,
|
||||
"bull": {
|
||||
"n": 0,
|
||||
"win": 0,
|
||||
"unknown": 0
|
||||
},
|
||||
"bear": {
|
||||
"n": 0,
|
||||
"win": 0,
|
||||
"unknown": 0
|
||||
},
|
||||
"logs": [],
|
||||
"signals": [],
|
||||
"modelName": "deepseek-v3.2",
|
||||
"modelProvider": "DASHSCOPE"
|
||||
},
|
||||
{
|
||||
"agentId": "technical_analyst",
|
||||
"name": "技术分析师",
|
||||
"role": "技术分析师",
|
||||
"avatar": "technical",
|
||||
"rank": 0,
|
||||
"winRate": null,
|
||||
"bull": {
|
||||
"n": 0,
|
||||
"win": 0,
|
||||
"unknown": 0
|
||||
},
|
||||
"bear": {
|
||||
"n": 0,
|
||||
"win": 0,
|
||||
"unknown": 0
|
||||
},
|
||||
"logs": [],
|
||||
"signals": [],
|
||||
"modelName": "deepseek-v3.2",
|
||||
"modelProvider": "DASHSCOPE"
|
||||
},
|
||||
{
|
||||
"agentId": "fundamentals_analyst",
|
||||
"name": "基本面分析师",
|
||||
"role": "基本面分析师",
|
||||
"avatar": "fundamentals",
|
||||
"rank": 0,
|
||||
"winRate": null,
|
||||
"bull": {
|
||||
"n": 0,
|
||||
"win": 0,
|
||||
"unknown": 0
|
||||
},
|
||||
"bear": {
|
||||
"n": 0,
|
||||
"win": 0,
|
||||
"unknown": 0
|
||||
},
|
||||
"logs": [],
|
||||
"signals": [],
|
||||
"modelName": "deepseek-v3.2",
|
||||
"modelProvider": "DASHSCOPE"
|
||||
},
|
||||
{
|
||||
"agentId": "valuation_analyst",
|
||||
"name": "估值分析师",
|
||||
"role": "估值分析师",
|
||||
"avatar": "valuation",
|
||||
"rank": 0,
|
||||
"winRate": null,
|
||||
"bull": {
|
||||
"n": 0,
|
||||
"win": 0,
|
||||
"unknown": 0
|
||||
},
|
||||
"bear": {
|
||||
"n": 0,
|
||||
"win": 0,
|
||||
"unknown": 0
|
||||
},
|
||||
"logs": [],
|
||||
"signals": [],
|
||||
"modelName": "deepseek-v3.2",
|
||||
"modelProvider": "DASHSCOPE"
|
||||
}
|
||||
],
|
||||
"realtime_prices": {},
|
||||
"system_started": "2026-03-16T00:38:27.957651",
|
||||
"feed_history": [
|
||||
{
|
||||
"type": "system",
|
||||
"content": "Runtime assets reloaded.",
|
||||
"timestamp": "2026-03-16"
|
||||
},
|
||||
{
|
||||
"type": "day_start",
|
||||
"date": "2026-03-16",
|
||||
"progress": 0.0,
|
||||
"timestamp": "2026-03-16"
|
||||
}
|
||||
],
|
||||
"last_day_history": [],
|
||||
"trading_days_total": 0,
|
||||
"trading_days_completed": 0,
|
||||
"server_mode": "live",
|
||||
"is_backtest": false,
|
||||
"is_mock_mode": true,
|
||||
"data_sources": {
|
||||
"preferred": [
|
||||
"yfinance",
|
||||
"financial_datasets",
|
||||
"finnhub",
|
||||
"local_csv"
|
||||
],
|
||||
"last_success": {
|
||||
"market_cap": "financial_datasets",
|
||||
"financial_metrics": "yfinance"
|
||||
}
|
||||
},
|
||||
"last_saved": "2026-03-16T00:38:59.051101"
|
||||
}
|
||||
1
runs/reload_demo/team_dashboard/holdings.json
Normal file
1
runs/reload_demo/team_dashboard/holdings.json
Normal file
@@ -0,0 +1 @@
|
||||
[]
|
||||
134
runs/reload_demo/team_dashboard/leaderboard.json
Normal file
134
runs/reload_demo/team_dashboard/leaderboard.json
Normal file
@@ -0,0 +1,134 @@
|
||||
[
|
||||
{
|
||||
"agentId": "portfolio_manager",
|
||||
"name": "投资经理",
|
||||
"role": "投资经理",
|
||||
"avatar": "pm",
|
||||
"rank": null,
|
||||
"winRate": null,
|
||||
"bull": {
|
||||
"n": 0,
|
||||
"win": 0,
|
||||
"unknown": 0
|
||||
},
|
||||
"bear": {
|
||||
"n": 0,
|
||||
"win": 0,
|
||||
"unknown": 0
|
||||
},
|
||||
"logs": [],
|
||||
"signals": [],
|
||||
"modelName": "deepseek-v3.2",
|
||||
"modelProvider": "DASHSCOPE"
|
||||
},
|
||||
{
|
||||
"agentId": "risk_manager",
|
||||
"name": "风控经理",
|
||||
"role": "风控经理",
|
||||
"avatar": "risk",
|
||||
"rank": null,
|
||||
"winRate": null,
|
||||
"bull": {
|
||||
"n": 0,
|
||||
"win": 0,
|
||||
"unknown": 0
|
||||
},
|
||||
"bear": {
|
||||
"n": 0,
|
||||
"win": 0,
|
||||
"unknown": 0
|
||||
},
|
||||
"logs": [],
|
||||
"signals": [],
|
||||
"modelName": "deepseek-v3.2",
|
||||
"modelProvider": "DASHSCOPE"
|
||||
},
|
||||
{
|
||||
"agentId": "sentiment_analyst",
|
||||
"name": "情绪分析师",
|
||||
"role": "情绪分析师",
|
||||
"avatar": "sentiment",
|
||||
"rank": 0,
|
||||
"winRate": null,
|
||||
"bull": {
|
||||
"n": 0,
|
||||
"win": 0,
|
||||
"unknown": 0
|
||||
},
|
||||
"bear": {
|
||||
"n": 0,
|
||||
"win": 0,
|
||||
"unknown": 0
|
||||
},
|
||||
"logs": [],
|
||||
"signals": [],
|
||||
"modelName": "deepseek-v3.2",
|
||||
"modelProvider": "DASHSCOPE"
|
||||
},
|
||||
{
|
||||
"agentId": "technical_analyst",
|
||||
"name": "技术分析师",
|
||||
"role": "技术分析师",
|
||||
"avatar": "technical",
|
||||
"rank": 0,
|
||||
"winRate": null,
|
||||
"bull": {
|
||||
"n": 0,
|
||||
"win": 0,
|
||||
"unknown": 0
|
||||
},
|
||||
"bear": {
|
||||
"n": 0,
|
||||
"win": 0,
|
||||
"unknown": 0
|
||||
},
|
||||
"logs": [],
|
||||
"signals": [],
|
||||
"modelName": "deepseek-v3.2",
|
||||
"modelProvider": "DASHSCOPE"
|
||||
},
|
||||
{
|
||||
"agentId": "fundamentals_analyst",
|
||||
"name": "基本面分析师",
|
||||
"role": "基本面分析师",
|
||||
"avatar": "fundamentals",
|
||||
"rank": 0,
|
||||
"winRate": null,
|
||||
"bull": {
|
||||
"n": 0,
|
||||
"win": 0,
|
||||
"unknown": 0
|
||||
},
|
||||
"bear": {
|
||||
"n": 0,
|
||||
"win": 0,
|
||||
"unknown": 0
|
||||
},
|
||||
"logs": [],
|
||||
"signals": [],
|
||||
"modelName": "deepseek-v3.2",
|
||||
"modelProvider": "DASHSCOPE"
|
||||
},
|
||||
{
|
||||
"agentId": "valuation_analyst",
|
||||
"name": "估值分析师",
|
||||
"role": "估值分析师",
|
||||
"avatar": "valuation",
|
||||
"rank": 0,
|
||||
"winRate": null,
|
||||
"bull": {
|
||||
"n": 0,
|
||||
"win": 0,
|
||||
"unknown": 0
|
||||
},
|
||||
"bear": {
|
||||
"n": 0,
|
||||
"win": 0,
|
||||
"unknown": 0
|
||||
},
|
||||
"logs": [],
|
||||
"signals": [],
|
||||
"modelName": "deepseek-v3.2",
|
||||
"modelProvider": "DASHSCOPE"
|
||||
}
|
||||
]
|
||||
18
runs/reload_demo/team_dashboard/stats.json
Normal file
18
runs/reload_demo/team_dashboard/stats.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"totalAssetValue": 250000.0,
|
||||
"totalReturn": 0.0,
|
||||
"cashPosition": 250000.0,
|
||||
"tickerWeights": {},
|
||||
"totalTrades": 0,
|
||||
"winRate": 0.0,
|
||||
"bullBear": {
|
||||
"bull": {
|
||||
"n": 0,
|
||||
"win": 0
|
||||
},
|
||||
"bear": {
|
||||
"n": 0,
|
||||
"win": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
13
runs/reload_demo/team_dashboard/summary.json
Normal file
13
runs/reload_demo/team_dashboard/summary.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"totalAssetValue": 250000.0,
|
||||
"totalReturn": 0.0,
|
||||
"cashPosition": 250000.0,
|
||||
"tickerWeights": {},
|
||||
"totalTrades": 0,
|
||||
"pnlPct": 0.0,
|
||||
"balance": 250000.0,
|
||||
"equity": [],
|
||||
"baseline": [],
|
||||
"baseline_vw": [],
|
||||
"momentum": []
|
||||
}
|
||||
1
runs/reload_demo/team_dashboard/trades.json
Normal file
1
runs/reload_demo/team_dashboard/trades.json
Normal file
@@ -0,0 +1 @@
|
||||
[]
|
||||
13
runs/reload_demo_prompt_check/BOOTSTRAP.md
Normal file
13
runs/reload_demo_prompt_check/BOOTSTRAP.md
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
tickers:
|
||||
- NVDA
|
||||
initial_cash: 100000
|
||||
margin_requirement: 0.5
|
||||
enable_memory: false
|
||||
max_comm_cycles: 2
|
||||
agent_overrides: {}
|
||||
---
|
||||
|
||||
# Bootstrap
|
||||
|
||||
Prompt reload smoke.
|
||||
@@ -0,0 +1,2 @@
|
||||
# POLICY
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# ROLE
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# STYLE
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# POLICY
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# ROLE
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# STYLE
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# POLICY
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# ROLE
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# Style
|
||||
|
||||
STYLE_MARKER_TWO
|
||||
@@ -0,0 +1,2 @@
|
||||
# POLICY
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# ROLE
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# STYLE
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# POLICY
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# ROLE
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# STYLE
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# POLICY
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# ROLE
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# STYLE
|
||||
|
||||
4
runs/reload_demo_prompt_check/skills/activation.yaml
Normal file
4
runs/reload_demo_prompt_check/skills/activation.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
global_enabled_skills: []
|
||||
global_disabled_skills: []
|
||||
agent_enabled_skills: {}
|
||||
agent_disabled_skills: {}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user