Compare commits
2 Commits
dev
...
a41cd705b4
| Author | SHA1 | Date | |
|---|---|---|---|
| a41cd705b4 | |||
| 564c92c0c8 |
@@ -48,15 +48,19 @@ class AnalystAgent(ReActAgent):
|
|||||||
f"Must be one of: {list(ANALYST_TYPES.keys())}",
|
f"Must be one of: {list(ANALYST_TYPES.keys())}",
|
||||||
)
|
)
|
||||||
|
|
||||||
self.analyst_type_key = analyst_type
|
object.__setattr__(self, "analyst_type_key", analyst_type)
|
||||||
self.analyst_persona = ANALYST_TYPES[analyst_type]["display_name"]
|
object.__setattr__(
|
||||||
|
self,
|
||||||
|
"analyst_persona",
|
||||||
|
ANALYST_TYPES[analyst_type]["display_name"],
|
||||||
|
)
|
||||||
|
|
||||||
if agent_id is None:
|
if agent_id is None:
|
||||||
agent_id = analyst_type
|
agent_id = analyst_type
|
||||||
self.agent_id = agent_id
|
object.__setattr__(self, "agent_id", agent_id)
|
||||||
|
|
||||||
self.config = config or {}
|
object.__setattr__(self, "config", config or {})
|
||||||
self.toolkit = toolkit
|
object.__setattr__(self, "toolkit", toolkit)
|
||||||
sys_prompt = self._load_system_prompt()
|
sys_prompt = self._load_system_prompt()
|
||||||
|
|
||||||
kwargs = {
|
kwargs = {
|
||||||
@@ -125,4 +129,12 @@ class AnalystAgent(ReActAgent):
|
|||||||
self.config.get("config_name", "default"),
|
self.config.get("config_name", "default"),
|
||||||
active_skill_dirs=active_skill_dirs,
|
active_skill_dirs=active_skill_dirs,
|
||||||
)
|
)
|
||||||
self.sys_prompt = self._load_system_prompt()
|
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
|
||||||
|
|||||||
@@ -38,21 +38,29 @@ class PMAgent(ReActAgent):
|
|||||||
toolkit_factory_kwargs: Optional[Dict[str, Any]] = None,
|
toolkit_factory_kwargs: Optional[Dict[str, Any]] = None,
|
||||||
toolkit: Optional[Toolkit] = None,
|
toolkit: Optional[Toolkit] = None,
|
||||||
):
|
):
|
||||||
self.config = config or {}
|
object.__setattr__(self, "config", config or {})
|
||||||
|
|
||||||
# Portfolio state
|
# Portfolio state
|
||||||
self.portfolio = {
|
object.__setattr__(
|
||||||
|
self,
|
||||||
|
"portfolio",
|
||||||
|
{
|
||||||
"cash": initial_cash,
|
"cash": initial_cash,
|
||||||
"positions": {},
|
"positions": {},
|
||||||
"margin_used": 0.0,
|
"margin_used": 0.0,
|
||||||
"margin_requirement": margin_requirement,
|
"margin_requirement": margin_requirement,
|
||||||
}
|
},
|
||||||
|
)
|
||||||
|
|
||||||
# Decisions made in current cycle
|
# Decisions made in current cycle
|
||||||
self._decisions: Dict[str, Dict] = {}
|
object.__setattr__(self, "_decisions", {})
|
||||||
toolkit_factory_kwargs = toolkit_factory_kwargs or {}
|
toolkit_factory_kwargs = toolkit_factory_kwargs or {}
|
||||||
self._toolkit_factory = toolkit_factory
|
object.__setattr__(self, "_toolkit_factory", toolkit_factory)
|
||||||
self._toolkit_factory_kwargs = toolkit_factory_kwargs
|
object.__setattr__(
|
||||||
|
self,
|
||||||
|
"_toolkit_factory_kwargs",
|
||||||
|
toolkit_factory_kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
# Create toolkit after local state is ready so bound tool methods can be registered.
|
# Create toolkit after local state is ready so bound tool methods can be registered.
|
||||||
if toolkit is None:
|
if toolkit is None:
|
||||||
@@ -65,7 +73,7 @@ class PMAgent(ReActAgent):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
toolkit = self._create_toolkit()
|
toolkit = self._create_toolkit()
|
||||||
self.toolkit = toolkit
|
object.__setattr__(self, "toolkit", toolkit)
|
||||||
|
|
||||||
sys_prompt = build_agent_system_prompt(
|
sys_prompt = build_agent_system_prompt(
|
||||||
agent_id=name,
|
agent_id=name,
|
||||||
@@ -205,6 +213,42 @@ class PMAgent(ReActAgent):
|
|||||||
"""Update portfolio after external execution"""
|
"""Update portfolio after external execution"""
|
||||||
self.portfolio.update(portfolio)
|
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:
|
def reload_runtime_assets(self, active_skill_dirs: Optional[list] = None) -> None:
|
||||||
"""Reload toolkit and system prompt from current run assets."""
|
"""Reload toolkit and system prompt from current run assets."""
|
||||||
from .toolkit_factory import create_agent_toolkit
|
from .toolkit_factory import create_agent_toolkit
|
||||||
@@ -221,8 +265,18 @@ class PMAgent(ReActAgent):
|
|||||||
owner=self,
|
owner=self,
|
||||||
**toolkit_kwargs,
|
**toolkit_kwargs,
|
||||||
)
|
)
|
||||||
self.sys_prompt = build_agent_system_prompt(
|
self._apply_runtime_sys_prompt(
|
||||||
|
build_agent_system_prompt(
|
||||||
agent_id=self.name,
|
agent_id=self.name,
|
||||||
config_name=self.config.get("config_name", "default"),
|
config_name=self.config.get("config_name", "default"),
|
||||||
toolkit=self.toolkit,
|
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
|
||||||
|
|||||||
@@ -39,12 +39,12 @@ class RiskAgent(ReActAgent):
|
|||||||
config: Configuration dictionary
|
config: Configuration dictionary
|
||||||
long_term_memory: Optional ReMeTaskLongTermMemory instance
|
long_term_memory: Optional ReMeTaskLongTermMemory instance
|
||||||
"""
|
"""
|
||||||
self.config = config or {}
|
object.__setattr__(self, "config", config or {})
|
||||||
self.agent_id = name
|
object.__setattr__(self, "agent_id", name)
|
||||||
|
|
||||||
if toolkit is None:
|
if toolkit is None:
|
||||||
toolkit = Toolkit()
|
toolkit = Toolkit()
|
||||||
self.toolkit = toolkit
|
object.__setattr__(self, "toolkit", toolkit)
|
||||||
|
|
||||||
sys_prompt = self._load_system_prompt()
|
sys_prompt = self._load_system_prompt()
|
||||||
|
|
||||||
@@ -99,4 +99,12 @@ class RiskAgent(ReActAgent):
|
|||||||
self.config.get("config_name", "default"),
|
self.config.get("config_name", "default"),
|
||||||
active_skill_dirs=active_skill_dirs,
|
active_skill_dirs=active_skill_dirs,
|
||||||
)
|
)
|
||||||
self.sys_prompt = self._load_system_prompt()
|
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
|
||||||
|
|||||||
@@ -62,6 +62,59 @@ class SkillsManager:
|
|||||||
|
|
||||||
raise FileNotFoundError(f"Unknown skill: {skill_name}")
|
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(
|
def resolve_agent_skill_names(
|
||||||
self,
|
self,
|
||||||
config_name: str,
|
config_name: str,
|
||||||
@@ -103,12 +156,22 @@ class SkillsManager:
|
|||||||
|
|
||||||
for existing in active_root.iterdir():
|
for existing in active_root.iterdir():
|
||||||
if existing.is_dir() and existing.name not in wanted:
|
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)
|
shutil.rmtree(existing)
|
||||||
|
|
||||||
for skill_name in skill_names:
|
for skill_name in skill_names:
|
||||||
source_dir = self._resolve_source_dir(skill_name)
|
source_dir = self._resolve_source_dir(skill_name)
|
||||||
target_dir = active_root / skill_name
|
target_dir = active_root / skill_name
|
||||||
if target_dir.exists():
|
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.rmtree(target_dir)
|
||||||
shutil.copytree(source_dir, target_dir)
|
shutil.copytree(source_dir, target_dir)
|
||||||
synced_paths.append(target_dir)
|
synced_paths.append(target_dir)
|
||||||
|
|||||||
@@ -49,9 +49,8 @@ def handle_history_cleanup(config_name: str, auto_clean: bool = False) -> None:
|
|||||||
config_name: Configuration name for the run
|
config_name: Configuration name for the run
|
||||||
auto_clean: If True, skip confirmation and clean automatically
|
auto_clean: If True, skip confirmation and clean automatically
|
||||||
"""
|
"""
|
||||||
# logs_dir = get_project_root() / "logs"
|
workspace_manager = WorkspaceManager(project_root=get_project_root())
|
||||||
logs_dir = get_project_root()
|
base_data_dir = workspace_manager.get_run_dir(config_name)
|
||||||
base_data_dir = logs_dir / config_name
|
|
||||||
|
|
||||||
# Check if historical data exists
|
# Check if historical data exists
|
||||||
if not base_data_dir.exists() or not any(base_data_dir.iterdir()):
|
if not base_data_dir.exists() or not any(base_data_dir.iterdir()):
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""Parse run-scoped BOOTSTRAP.md into structured configuration."""
|
"""Parse run-scoped BOOTSTRAP.md into structured and runtime config."""
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -8,6 +8,8 @@ import re
|
|||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
from backend.config.env_config import get_env_float, get_env_int, get_env_list
|
||||||
|
|
||||||
|
|
||||||
BOOTSTRAP_FRONT_MATTER_RE = re.compile(
|
BOOTSTRAP_FRONT_MATTER_RE = re.compile(
|
||||||
r"^---\s*\n(.*?)\n---\s*\n?(.*)$",
|
r"^---\s*\n(.*?)\n---\s*\n?(.*)$",
|
||||||
@@ -63,3 +65,84 @@ def get_bootstrap_config_for_run(
|
|||||||
return load_bootstrap_config(
|
return load_bootstrap_config(
|
||||||
project_root / "runs" / config_name / "BOOTSTRAP.md",
|
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,12 +226,18 @@ class TradingPipeline:
|
|||||||
"settlement_result": settlement_result,
|
"settlement_result": settlement_result,
|
||||||
}
|
}
|
||||||
|
|
||||||
def reload_runtime_assets(self) -> Dict[str, Any]:
|
def reload_runtime_assets(
|
||||||
"""Reload prompt assets, bootstrap config, and active skills for all agents."""
|
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.skills_manager import SkillsManager
|
||||||
from backend.agents.toolkit_factory import load_agent_profiles
|
from backend.agents.toolkit_factory import load_agent_profiles
|
||||||
|
|
||||||
config_name = getattr(self.pm, "config", {}).get("config_name", "default")
|
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()
|
skills_manager = SkillsManager()
|
||||||
profiles = load_agent_profiles()
|
profiles = load_agent_profiles()
|
||||||
active_skill_map = skills_manager.prepare_active_skills(
|
active_skill_map = skills_manager.prepare_active_skills(
|
||||||
@@ -262,6 +268,7 @@ class TradingPipeline:
|
|||||||
agent_id: [path.name for path in paths]
|
agent_id: [path.name for path in paths]
|
||||||
for agent_id, paths in active_skill_map.items()
|
for agent_id, paths in active_skill_map.items()
|
||||||
},
|
},
|
||||||
|
"max_comm_cycles": self.max_comm_cycles,
|
||||||
}
|
}
|
||||||
|
|
||||||
async def _clear_all_agent_memory(self):
|
async def _clear_all_agent_memory(self):
|
||||||
|
|||||||
@@ -438,6 +438,8 @@ class StateSync:
|
|||||||
"server_mode": self._state.get("server_mode", "live"),
|
"server_mode": self._state.get("server_mode", "live"),
|
||||||
"is_mock_mode": self._state.get("is_mock_mode", False),
|
"is_mock_mode": self._state.get("is_mock_mode", False),
|
||||||
"is_backtest": self._state.get("is_backtest", 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", []),
|
"feed_history": self._state.get("feed_history", []),
|
||||||
"current_date": self._state.get("current_date"),
|
"current_date": self._state.get("current_date"),
|
||||||
"trading_days_total": self._state.get("trading_days_total", 0),
|
"trading_days_total": self._state.get("trading_days_total", 0),
|
||||||
@@ -452,6 +454,7 @@ class StateSync:
|
|||||||
"portfolio": self._state.get("portfolio", {}),
|
"portfolio": self._state.get("portfolio", {}),
|
||||||
"realtime_prices": self._state.get("realtime_prices", {}),
|
"realtime_prices": self._state.get("realtime_prices", {}),
|
||||||
"data_sources": self._state.get("data_sources", {}),
|
"data_sources": self._state.get("data_sources", {}),
|
||||||
|
"price_history": self._state.get("price_history", {}),
|
||||||
}
|
}
|
||||||
|
|
||||||
if include_dashboard:
|
if include_dashboard:
|
||||||
|
|||||||
@@ -30,6 +30,25 @@ logger = logging.getLogger(__name__)
|
|||||||
_DATA_DIR = Path(__file__).parent / "ret_data"
|
_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:
|
class DataProviderRouter:
|
||||||
"""Route data requests across configured providers with fallbacks."""
|
"""Route data requests across configured providers with fallbacks."""
|
||||||
|
|
||||||
@@ -56,6 +75,8 @@ class DataProviderRouter:
|
|||||||
end_date: str,
|
end_date: str,
|
||||||
) -> tuple[list[Price], DataSource]:
|
) -> tuple[list[Price], DataSource]:
|
||||||
"""Fetch prices using preferred providers with fallback."""
|
"""Fetch prices using preferred providers with fallback."""
|
||||||
|
if not _has_valid_ticker(ticker):
|
||||||
|
return [], "local_csv"
|
||||||
last_error: Optional[Exception] = None
|
last_error: Optional[Exception] = None
|
||||||
|
|
||||||
for source in self.price_sources():
|
for source in self.price_sources():
|
||||||
@@ -78,7 +99,12 @@ class DataProviderRouter:
|
|||||||
return prices, source
|
return prices, source
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
last_error = 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:
|
if last_error:
|
||||||
raise last_error
|
raise last_error
|
||||||
@@ -92,6 +118,8 @@ class DataProviderRouter:
|
|||||||
limit: int = 10,
|
limit: int = 10,
|
||||||
) -> tuple[list[FinancialMetrics], DataSource]:
|
) -> tuple[list[FinancialMetrics], DataSource]:
|
||||||
"""Fetch financial metrics with API provider fallback."""
|
"""Fetch financial metrics with API provider fallback."""
|
||||||
|
if not _has_valid_ticker(ticker):
|
||||||
|
return [], "local_csv"
|
||||||
last_error: Optional[Exception] = None
|
last_error: Optional[Exception] = None
|
||||||
|
|
||||||
for source in self.api_sources():
|
for source in self.api_sources():
|
||||||
@@ -126,7 +154,7 @@ class DataProviderRouter:
|
|||||||
"Financial metrics source %s failed for %s: %s",
|
"Financial metrics source %s failed for %s: %s",
|
||||||
source,
|
source,
|
||||||
ticker,
|
ticker,
|
||||||
exc,
|
_format_provider_error(exc),
|
||||||
)
|
)
|
||||||
|
|
||||||
if last_error:
|
if last_error:
|
||||||
@@ -142,6 +170,8 @@ class DataProviderRouter:
|
|||||||
limit: int = 10,
|
limit: int = 10,
|
||||||
) -> list[LineItem]:
|
) -> list[LineItem]:
|
||||||
"""Line items are only supported via Financial Datasets."""
|
"""Line items are only supported via Financial Datasets."""
|
||||||
|
if not _has_valid_ticker(ticker):
|
||||||
|
return []
|
||||||
if "financial_datasets" not in self.api_sources():
|
if "financial_datasets" not in self.api_sources():
|
||||||
return []
|
return []
|
||||||
try:
|
try:
|
||||||
@@ -155,7 +185,11 @@ class DataProviderRouter:
|
|||||||
self._record_success("line_items", "financial_datasets")
|
self._record_success("line_items", "financial_datasets")
|
||||||
return results
|
return results
|
||||||
except Exception as exc:
|
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 []
|
return []
|
||||||
|
|
||||||
def get_insider_trades(
|
def get_insider_trades(
|
||||||
@@ -166,6 +200,8 @@ class DataProviderRouter:
|
|||||||
limit: int = 1000,
|
limit: int = 1000,
|
||||||
) -> tuple[list[InsiderTrade], DataSource]:
|
) -> tuple[list[InsiderTrade], DataSource]:
|
||||||
"""Fetch insider trades with provider fallback."""
|
"""Fetch insider trades with provider fallback."""
|
||||||
|
if not _has_valid_ticker(ticker):
|
||||||
|
return [], "local_csv"
|
||||||
last_error: Optional[Exception] = None
|
last_error: Optional[Exception] = None
|
||||||
|
|
||||||
for source in self.api_sources():
|
for source in self.api_sources():
|
||||||
@@ -193,7 +229,7 @@ class DataProviderRouter:
|
|||||||
"Insider trades source %s failed for %s: %s",
|
"Insider trades source %s failed for %s: %s",
|
||||||
source,
|
source,
|
||||||
ticker,
|
ticker,
|
||||||
exc,
|
_format_provider_error(exc),
|
||||||
)
|
)
|
||||||
|
|
||||||
if last_error:
|
if last_error:
|
||||||
@@ -208,6 +244,8 @@ class DataProviderRouter:
|
|||||||
limit: int = 1000,
|
limit: int = 1000,
|
||||||
) -> tuple[list[CompanyNews], DataSource]:
|
) -> tuple[list[CompanyNews], DataSource]:
|
||||||
"""Fetch company news with provider fallback."""
|
"""Fetch company news with provider fallback."""
|
||||||
|
if not _has_valid_ticker(ticker):
|
||||||
|
return [], "local_csv"
|
||||||
last_error: Optional[Exception] = None
|
last_error: Optional[Exception] = None
|
||||||
|
|
||||||
for source in self.api_sources():
|
for source in self.api_sources():
|
||||||
@@ -244,7 +282,7 @@ class DataProviderRouter:
|
|||||||
"Company news source %s failed for %s: %s",
|
"Company news source %s failed for %s: %s",
|
||||||
source,
|
source,
|
||||||
ticker,
|
ticker,
|
||||||
exc,
|
_format_provider_error(exc),
|
||||||
)
|
)
|
||||||
|
|
||||||
if last_error:
|
if last_error:
|
||||||
@@ -258,6 +296,8 @@ class DataProviderRouter:
|
|||||||
metrics_lookup,
|
metrics_lookup,
|
||||||
) -> tuple[Optional[float], DataSource]:
|
) -> tuple[Optional[float], DataSource]:
|
||||||
"""Fetch market cap using facts API or financial metrics fallback."""
|
"""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")
|
today = datetime.datetime.now().strftime("%Y-%m-%d")
|
||||||
if end_date == today and "financial_datasets" in self.api_sources():
|
if end_date == today and "financial_datasets" in self.api_sources():
|
||||||
try:
|
try:
|
||||||
@@ -267,7 +307,7 @@ class DataProviderRouter:
|
|||||||
logger.warning(
|
logger.warning(
|
||||||
"Market cap facts source failed for %s: %s",
|
"Market cap facts source failed for %s: %s",
|
||||||
ticker,
|
ticker,
|
||||||
exc,
|
_format_provider_error(exc),
|
||||||
)
|
)
|
||||||
|
|
||||||
metrics, source = metrics_lookup(ticker, end_date)
|
metrics, source = metrics_lookup(ticker, end_date)
|
||||||
|
|||||||
@@ -18,9 +18,8 @@ from backend.agents.skills_manager import SkillsManager
|
|||||||
from backend.agents.toolkit_factory import create_agent_toolkit, load_agent_profiles
|
from backend.agents.toolkit_factory import create_agent_toolkit, load_agent_profiles
|
||||||
from backend.agents.prompt_loader import PromptLoader
|
from backend.agents.prompt_loader import PromptLoader
|
||||||
from backend.agents.workspace_manager import WorkspaceManager
|
from backend.agents.workspace_manager import WorkspaceManager
|
||||||
from backend.config.bootstrap_config import get_bootstrap_config_for_run
|
from backend.config.bootstrap_config import resolve_runtime_config
|
||||||
from backend.config.constants import ANALYST_TYPES
|
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.pipeline import TradingPipeline
|
||||||
from backend.core.scheduler import BacktestScheduler, Scheduler
|
from backend.core.scheduler import BacktestScheduler, Scheduler
|
||||||
from backend.utils.settlement import SettlementCoordinator
|
from backend.utils.settlement import SettlementCoordinator
|
||||||
@@ -36,35 +35,20 @@ loguru.logger.disable("reme_ai")
|
|||||||
_prompt_loader = PromptLoader()
|
_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:
|
def _resolve_runtime_config(args) -> dict:
|
||||||
"""Merge env defaults with run-scoped bootstrap config."""
|
"""Merge env defaults with run-scoped bootstrap config."""
|
||||||
project_root = Path(__file__).resolve().parents[1]
|
project_root = Path(__file__).resolve().parents[1]
|
||||||
bootstrap = get_bootstrap_config_for_run(project_root, args.config_name)
|
return resolve_runtime_config(
|
||||||
|
project_root=project_root,
|
||||||
return {
|
config_name=args.config_name,
|
||||||
"tickers": bootstrap.get("tickers")
|
enable_memory=args.enable_memory,
|
||||||
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": args.enable_memory
|
|
||||||
or bool(bootstrap.get("enable_memory", False)),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def create_long_term_memory(agent_name: str, config_name: str):
|
def create_long_term_memory(agent_name: str, config_name: str):
|
||||||
@@ -82,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")
|
logger.warning("MEMORY_API_KEY not set, long-term memory disabled")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
memory_dir = str(Path(config_name) / "memory")
|
memory_dir = str(_get_run_dir(config_name) / "memory")
|
||||||
|
|
||||||
return ReMeTaskLongTermMemory(
|
return ReMeTaskLongTermMemory(
|
||||||
agent_name=agent_name,
|
agent_name=agent_name,
|
||||||
@@ -241,7 +225,7 @@ async def run_with_gateway(args):
|
|||||||
|
|
||||||
# Create storage service
|
# Create storage service
|
||||||
storage_service = StorageService(
|
storage_service = StorageService(
|
||||||
dashboard_dir=Path(config_name) / "team_dashboard",
|
dashboard_dir=_get_run_dir(config_name) / "team_dashboard",
|
||||||
initial_cash=initial_cash,
|
initial_cash=initial_cash,
|
||||||
config_name=config_name,
|
config_name=config_name,
|
||||||
)
|
)
|
||||||
@@ -316,6 +300,10 @@ async def run_with_gateway(args):
|
|||||||
"backtest_mode": is_backtest,
|
"backtest_mode": is_backtest,
|
||||||
"tickers": tickers,
|
"tickers": tickers,
|
||||||
"config_name": config_name,
|
"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"],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -5,12 +5,18 @@ WebSocket Gateway for frontend communication
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any, Callable, Dict, List, Optional, Set
|
from typing import Any, Callable, Dict, List, Optional, Set
|
||||||
|
|
||||||
import websockets
|
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.msg_adapter import FrontendAdapter
|
||||||
from backend.utils.terminal_dashboard import get_dashboard
|
from backend.utils.terminal_dashboard import get_dashboard
|
||||||
from backend.core.pipeline import TradingPipeline
|
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.market import MarketService
|
||||||
from backend.services.storage import StorageService
|
from backend.services.storage import StorageService
|
||||||
from backend.data.provider_router import get_provider_router
|
from backend.data.provider_router import get_provider_router
|
||||||
|
from backend.tools.data_tools import get_prices
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -51,7 +58,7 @@ class Gateway:
|
|||||||
self.state_sync.set_broadcast_fn(self.broadcast)
|
self.state_sync.set_broadcast_fn(self.broadcast)
|
||||||
self.pipeline.state_sync = self.state_sync
|
self.pipeline.state_sync = self.state_sync
|
||||||
|
|
||||||
self.connected_clients: Set[WebSocketServerProtocol] = set()
|
self.connected_clients: Set[ServerConnection] = set()
|
||||||
self.lock = asyncio.Lock()
|
self.lock = asyncio.Lock()
|
||||||
self._backtest_task: Optional[asyncio.Task] = None
|
self._backtest_task: Optional[asyncio.Task] = None
|
||||||
self._backtest_start_date: Optional[str] = None
|
self._backtest_start_date: Optional[str] = None
|
||||||
@@ -63,6 +70,7 @@ class Gateway:
|
|||||||
self._session_start_portfolio_value: Optional[float] = None
|
self._session_start_portfolio_value: Optional[float] = None
|
||||||
self._provider_router = get_provider_router()
|
self._provider_router = get_provider_router()
|
||||||
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
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):
|
async def start(self, host: str = "0.0.0.0", port: int = 8766):
|
||||||
"""Start gateway server"""
|
"""Start gateway server"""
|
||||||
@@ -87,6 +95,7 @@ class Gateway:
|
|||||||
self._dashboard.start()
|
self._dashboard.start()
|
||||||
|
|
||||||
self.state_sync.load_state()
|
self.state_sync.load_state()
|
||||||
|
self.market_service.set_price_recorder(self.storage.record_price_point)
|
||||||
self.state_sync.update_state("status", "running")
|
self.state_sync.update_state("status", "running")
|
||||||
self.state_sync.update_state("server_mode", self.mode)
|
self.state_sync.update_state("server_mode", self.mode)
|
||||||
self.state_sync.update_state("is_backtest", self.is_backtest)
|
self.state_sync.update_state("is_backtest", self.is_backtest)
|
||||||
@@ -94,6 +103,20 @@ class Gateway:
|
|||||||
"is_mock_mode",
|
"is_mock_mode",
|
||||||
self.config.get("mock_mode", False),
|
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(
|
self.state_sync.update_state(
|
||||||
"data_sources",
|
"data_sources",
|
||||||
self._provider_router.get_usage_snapshot(),
|
self._provider_router.get_usage_snapshot(),
|
||||||
@@ -159,7 +182,7 @@ class Gateway:
|
|||||||
def state(self) -> Dict[str, Any]:
|
def state(self) -> Dict[str, Any]:
|
||||||
return self.state_sync.state
|
return self.state_sync.state
|
||||||
|
|
||||||
async def handle_client(self, websocket: WebSocketServerProtocol):
|
async def handle_client(self, websocket: ServerConnection):
|
||||||
"""Handle WebSocket client connection"""
|
"""Handle WebSocket client connection"""
|
||||||
async with self.lock:
|
async with self.lock:
|
||||||
self.connected_clients.add(websocket)
|
self.connected_clients.add(websocket)
|
||||||
@@ -170,7 +193,7 @@ class Gateway:
|
|||||||
async with self.lock:
|
async with self.lock:
|
||||||
self.connected_clients.discard(websocket)
|
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(
|
state_payload = self.state_sync.get_initial_state_payload(
|
||||||
include_dashboard=True,
|
include_dashboard=True,
|
||||||
)
|
)
|
||||||
@@ -198,7 +221,7 @@ class Gateway:
|
|||||||
|
|
||||||
async def _handle_client_messages(
|
async def _handle_client_messages(
|
||||||
self,
|
self,
|
||||||
websocket: WebSocketServerProtocol,
|
websocket: ServerConnection,
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
async for message in websocket:
|
async for message in websocket:
|
||||||
@@ -221,12 +244,104 @@ class Gateway:
|
|||||||
await self._handle_start_backtest(data)
|
await self._handle_start_backtest(data)
|
||||||
elif msg_type == "reload_runtime_assets":
|
elif msg_type == "reload_runtime_assets":
|
||||||
await self._handle_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:
|
except websockets.ConnectionClosed:
|
||||||
pass
|
pass
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
pass
|
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]):
|
async def _handle_start_backtest(self, data: Dict[str, Any]):
|
||||||
if not self.is_backtest:
|
if not self.is_backtest:
|
||||||
return
|
return
|
||||||
@@ -239,8 +354,15 @@ class Gateway:
|
|||||||
self._backtest_task = task
|
self._backtest_task = task
|
||||||
|
|
||||||
async def _handle_reload_runtime_assets(self):
|
async def _handle_reload_runtime_assets(self):
|
||||||
"""Reload prompt assets and active skills without restarting the server."""
|
"""Reload prompt, skills, and safe runtime config without restart."""
|
||||||
result = self.pipeline.reload_runtime_assets()
|
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(
|
await self.state_sync.on_system_message(
|
||||||
"Runtime assets reloaded.",
|
"Runtime assets reloaded.",
|
||||||
)
|
)
|
||||||
@@ -248,9 +370,174 @@ class Gateway:
|
|||||||
{
|
{
|
||||||
"type": "runtime_assets_reloaded",
|
"type": "runtime_assets_reloaded",
|
||||||
**result,
|
**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]):
|
async def broadcast(self, message: Dict[str, Any]):
|
||||||
"""Broadcast message to all connected clients"""
|
"""Broadcast message to all connected clients"""
|
||||||
if not self.connected_clients:
|
if not self.connected_clients:
|
||||||
@@ -269,7 +556,7 @@ class Gateway:
|
|||||||
|
|
||||||
async def _send_to_client(
|
async def _send_to_client(
|
||||||
self,
|
self,
|
||||||
client: WebSocketServerProtocol,
|
client: ServerConnection,
|
||||||
message: str,
|
message: str,
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ class MarketService:
|
|||||||
self.running = False
|
self.running = False
|
||||||
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
||||||
self._broadcast_func: Optional[Callable] = None
|
self._broadcast_func: Optional[Callable] = None
|
||||||
|
self._price_record_func: Optional[Callable[..., None]] = None
|
||||||
self._price_manager: Optional[Any] = None
|
self._price_manager: Optional[Any] = None
|
||||||
self._current_date: Optional[str] = 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
|
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:
|
def _make_price_callback(self) -> Callable:
|
||||||
"""Create thread-safe price callback"""
|
"""Create thread-safe price callback"""
|
||||||
|
|
||||||
@@ -169,6 +174,24 @@ class MarketService:
|
|||||||
((price - open_price) / open_price) * 100 if open_price > 0 else 0
|
((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(
|
await self._broadcast_func(
|
||||||
{
|
{
|
||||||
"type": "price_update",
|
"type": "price_update",
|
||||||
@@ -205,6 +228,43 @@ class MarketService:
|
|||||||
self._loop = None
|
self._loop = None
|
||||||
self._broadcast_func = 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
|
# Backtest methods
|
||||||
def set_backtest_date(self, date: str):
|
def set_backtest_date(self, date: str):
|
||||||
"""Set current backtest date"""
|
"""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 pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
from .runtime_db import RuntimeDb
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -61,6 +63,7 @@ class StorageService:
|
|||||||
self.state_dir = self.dashboard_dir.parent / "state"
|
self.state_dir = self.dashboard_dir.parent / "state"
|
||||||
self.state_dir.mkdir(parents=True, exist_ok=True)
|
self.state_dir.mkdir(parents=True, exist_ok=True)
|
||||||
self.server_state_file = self.state_dir / "server_state.json"
|
self.server_state_file = self.state_dir / "server_state.json"
|
||||||
|
self.runtime_db = RuntimeDb(self.state_dir / "runtime.db")
|
||||||
|
|
||||||
# Feed history (for agent messages)
|
# Feed history (for agent messages)
|
||||||
self.max_feed_history = 200
|
self.max_feed_history = 200
|
||||||
@@ -114,6 +117,11 @@ class StorageService:
|
|||||||
try:
|
try:
|
||||||
with open(file_path, "w", encoding="utf-8") as f:
|
with open(file_path, "w", encoding="utf-8") as f:
|
||||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Failed to save {file_type}.json: {e}")
|
logger.error(f"Failed to save {file_type}.json: {e}")
|
||||||
|
|
||||||
@@ -211,6 +219,7 @@ class StorageService:
|
|||||||
try:
|
try:
|
||||||
with open(self.internal_state_file, "w", encoding="utf-8") as f:
|
with open(self.internal_state_file, "w", encoding="utf-8") as f:
|
||||||
json.dump(state, f, indent=2, ensure_ascii=False)
|
json.dump(state, f, indent=2, ensure_ascii=False)
|
||||||
|
self._sync_price_history_to_db(state.get("price_history", {}))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to save internal state: {e}")
|
logger.error(f"Failed to save internal state: {e}")
|
||||||
|
|
||||||
@@ -231,6 +240,41 @@ class StorageService:
|
|||||||
"margin_requirement": 0.25, # Default 25% margin requirement
|
"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]):
|
def save_portfolio_state(self, portfolio: Dict[str, Any]):
|
||||||
"""
|
"""
|
||||||
Save portfolio state to internal state
|
Save portfolio state to internal state
|
||||||
@@ -750,6 +794,7 @@ class StorageService:
|
|||||||
"last_day_history": [],
|
"last_day_history": [],
|
||||||
"trading_days_total": 0,
|
"trading_days_total": 0,
|
||||||
"trading_days_completed": 0,
|
"trading_days_completed": 0,
|
||||||
|
"price_history": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
if not self.server_state_file.exists():
|
if not self.server_state_file.exists():
|
||||||
@@ -771,6 +816,11 @@ class StorageService:
|
|||||||
)
|
)
|
||||||
logger.info(f"Trades: {len(saved_state.get('trades', []))} records")
|
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
|
return saved_state
|
||||||
|
|
||||||
def save_server_state(self, state: Dict[str, Any]):
|
def save_server_state(self, state: Dict[str, Any]):
|
||||||
@@ -852,6 +902,7 @@ class StorageService:
|
|||||||
state["feed_history"] = []
|
state["feed_history"] = []
|
||||||
|
|
||||||
state["feed_history"].insert(0, feed_msg)
|
state["feed_history"].insert(0, feed_msg)
|
||||||
|
self.runtime_db.insert_event(feed_msg)
|
||||||
|
|
||||||
# Trim to max size
|
# Trim to max size
|
||||||
if len(state["feed_history"]) > self.max_feed_history:
|
if len(state["feed_history"]) > self.max_feed_history:
|
||||||
@@ -861,6 +912,69 @@ class StorageService:
|
|||||||
|
|
||||||
return True
|
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]:
|
def _get_default_stats(self) -> Dict[str, Any]:
|
||||||
"""Get default stats structure"""
|
"""Get default stats structure"""
|
||||||
return {
|
return {
|
||||||
@@ -889,6 +1003,7 @@ class StorageService:
|
|||||||
stats = self.load_file("stats") or self._get_default_stats()
|
stats = self.load_file("stats") or self._get_default_stats()
|
||||||
trades = self.load_file("trades") or []
|
trades = self.load_file("trades") or []
|
||||||
leaderboard = self.load_file("leaderboard") or []
|
leaderboard = self.load_file("leaderboard") or []
|
||||||
|
internal_state = self.load_internal_state()
|
||||||
|
|
||||||
# Update state
|
# Update state
|
||||||
state["portfolio"] = {
|
state["portfolio"] = {
|
||||||
@@ -910,6 +1025,9 @@ class StorageService:
|
|||||||
state["stats"] = stats
|
state["stats"] = stats
|
||||||
state["trades"] = trades
|
state["trades"] = trades
|
||||||
state["leaderboard"] = leaderboard
|
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 ==========
|
# ========== Live Returns Tracking ==========
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ Returns human-readable text format for easy LLM consumption.
|
|||||||
"""
|
"""
|
||||||
# flake8: noqa: E501
|
# flake8: noqa: E501
|
||||||
# pylint: disable=C0301,W0613
|
# pylint: disable=C0301,W0613
|
||||||
|
import ast
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import traceback
|
import traceback
|
||||||
@@ -20,6 +21,7 @@ import pandas as pd
|
|||||||
from agentscope.message import TextBlock
|
from agentscope.message import TextBlock
|
||||||
from agentscope.tool import ToolResponse
|
from agentscope.tool import ToolResponse
|
||||||
|
|
||||||
|
from backend.data.provider_utils import normalize_symbol
|
||||||
from backend.tools.data_tools import (
|
from backend.tools.data_tools import (
|
||||||
get_company_news,
|
get_company_news,
|
||||||
get_financial_metrics,
|
get_financial_metrics,
|
||||||
@@ -53,6 +55,16 @@ def _parse_tickers(tickers: Union[str, List[str], None]) -> List[str]:
|
|||||||
Returns:
|
Returns:
|
||||||
List of stock tickers.
|
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:
|
if tickers is None:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@@ -60,17 +72,22 @@ def _parse_tickers(tickers: Union[str, List[str], None]) -> List[str]:
|
|||||||
try:
|
try:
|
||||||
parsed = json.loads(tickers)
|
parsed = json.loads(tickers)
|
||||||
if isinstance(parsed, list):
|
if isinstance(parsed, list):
|
||||||
return parsed
|
return _sanitize(parsed)
|
||||||
# If it's a single string, wrap in list
|
return _sanitize([parsed])
|
||||||
return [parsed]
|
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
# If not valid JSON, treat as comma-separated string
|
try:
|
||||||
return [t.strip() for t in tickers.split(",") if t.strip()]
|
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):
|
if isinstance(tickers, list):
|
||||||
return tickers
|
return _sanitize(tickers)
|
||||||
|
|
||||||
return []
|
return _sanitize([tickers])
|
||||||
|
|
||||||
|
|
||||||
def _safe_float(value, default=0.0) -> float:
|
def _safe_float(value, default=0.0) -> float:
|
||||||
@@ -350,6 +367,7 @@ def get_financial_metrics_tool(
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
current_date = _resolved_date(current_date)
|
current_date = _resolved_date(current_date)
|
||||||
|
tickers = _parse_tickers(tickers)
|
||||||
lines = [
|
lines = [
|
||||||
f"=== Comprehensive Financial Metrics ({current_date}, {period}) ===\n",
|
f"=== Comprehensive Financial Metrics ({current_date}, {period}) ===\n",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -96,13 +96,19 @@ def get_prices(
|
|||||||
list[Price]: List of Price objects
|
list[Price]: List of Price objects
|
||||||
"""
|
"""
|
||||||
ticker = normalize_symbol(ticker)
|
ticker = normalize_symbol(ticker)
|
||||||
|
if not ticker:
|
||||||
|
return []
|
||||||
cached_sources = _router.price_sources()
|
cached_sources = _router.price_sources()
|
||||||
for source in cached_sources:
|
for source in cached_sources:
|
||||||
cache_key = f"{ticker}_{start_date}_{end_date}_{source}"
|
cache_key = f"{ticker}_{start_date}_{end_date}_{source}"
|
||||||
if cached_data := _cache.get_prices(cache_key):
|
if cached_data := _cache.get_prices(cache_key):
|
||||||
return [Price(**price) for price in cached_data]
|
return [Price(**price) for price in cached_data]
|
||||||
|
|
||||||
|
try:
|
||||||
prices, data_source = _router.get_prices(ticker, start_date, end_date)
|
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:
|
if not prices:
|
||||||
return []
|
return []
|
||||||
@@ -133,17 +139,23 @@ def get_financial_metrics(
|
|||||||
list[FinancialMetrics]: List of financial metrics
|
list[FinancialMetrics]: List of financial metrics
|
||||||
"""
|
"""
|
||||||
ticker = normalize_symbol(ticker)
|
ticker = normalize_symbol(ticker)
|
||||||
|
if not ticker:
|
||||||
|
return []
|
||||||
for source in _router.api_sources():
|
for source in _router.api_sources():
|
||||||
cache_key = f"{ticker}_{period}_{end_date}_{limit}_{source}"
|
cache_key = f"{ticker}_{period}_{end_date}_{limit}_{source}"
|
||||||
if cached_data := _cache.get_financial_metrics(cache_key):
|
if cached_data := _cache.get_financial_metrics(cache_key):
|
||||||
return [FinancialMetrics(**metric) for metric in cached_data]
|
return [FinancialMetrics(**metric) for metric in cached_data]
|
||||||
|
|
||||||
|
try:
|
||||||
financial_metrics, data_source = _router.get_financial_metrics(
|
financial_metrics, data_source = _router.get_financial_metrics(
|
||||||
ticker=ticker,
|
ticker=ticker,
|
||||||
end_date=end_date,
|
end_date=end_date,
|
||||||
period=period,
|
period=period,
|
||||||
limit=limit,
|
limit=limit,
|
||||||
)
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.info("Financial metrics lookup failed for %s: %s", ticker, exc)
|
||||||
|
return []
|
||||||
|
|
||||||
if not financial_metrics:
|
if not financial_metrics:
|
||||||
return []
|
return []
|
||||||
@@ -169,6 +181,8 @@ def search_line_items(
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
ticker = normalize_symbol(ticker)
|
ticker = normalize_symbol(ticker)
|
||||||
|
if not ticker:
|
||||||
|
return []
|
||||||
return _router.search_line_items(
|
return _router.search_line_items(
|
||||||
ticker=ticker,
|
ticker=ticker,
|
||||||
line_items=line_items,
|
line_items=line_items,
|
||||||
@@ -190,6 +204,8 @@ def get_insider_trades(
|
|||||||
) -> list[InsiderTrade]:
|
) -> list[InsiderTrade]:
|
||||||
"""Fetch insider trades from cache or API."""
|
"""Fetch insider trades from cache or API."""
|
||||||
ticker = normalize_symbol(ticker)
|
ticker = normalize_symbol(ticker)
|
||||||
|
if not ticker:
|
||||||
|
return []
|
||||||
for source in _router.api_sources():
|
for source in _router.api_sources():
|
||||||
cache_key = (
|
cache_key = (
|
||||||
f"{ticker}_{start_date or 'none'}_{end_date}_{limit}_{source}"
|
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):
|
if cached_data := _cache.get_insider_trades(cache_key):
|
||||||
return [InsiderTrade(**trade) for trade in cached_data]
|
return [InsiderTrade(**trade) for trade in cached_data]
|
||||||
|
|
||||||
|
try:
|
||||||
all_trades, data_source = _router.get_insider_trades(
|
all_trades, data_source = _router.get_insider_trades(
|
||||||
ticker=ticker,
|
ticker=ticker,
|
||||||
end_date=end_date,
|
end_date=end_date,
|
||||||
start_date=start_date,
|
start_date=start_date,
|
||||||
limit=limit,
|
limit=limit,
|
||||||
)
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.info("Insider trades lookup failed for %s: %s", ticker, exc)
|
||||||
|
return []
|
||||||
|
|
||||||
if not all_trades:
|
if not all_trades:
|
||||||
return []
|
return []
|
||||||
@@ -219,6 +239,8 @@ def get_company_news(
|
|||||||
) -> list[CompanyNews]:
|
) -> list[CompanyNews]:
|
||||||
"""Fetch company news from cache or API."""
|
"""Fetch company news from cache or API."""
|
||||||
ticker = normalize_symbol(ticker)
|
ticker = normalize_symbol(ticker)
|
||||||
|
if not ticker:
|
||||||
|
return []
|
||||||
for source in _router.api_sources():
|
for source in _router.api_sources():
|
||||||
cache_key = (
|
cache_key = (
|
||||||
f"{ticker}_{start_date or 'none'}_{end_date}_{limit}_{source}"
|
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):
|
if cached_data := _cache.get_company_news(cache_key):
|
||||||
return [CompanyNews(**news) for news in cached_data]
|
return [CompanyNews(**news) for news in cached_data]
|
||||||
|
|
||||||
|
try:
|
||||||
all_news, data_source = _router.get_company_news(
|
all_news, data_source = _router.get_company_news(
|
||||||
ticker=ticker,
|
ticker=ticker,
|
||||||
end_date=end_date,
|
end_date=end_date,
|
||||||
start_date=start_date,
|
start_date=start_date,
|
||||||
limit=limit,
|
limit=limit,
|
||||||
)
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.info("Company news lookup failed for %s: %s", ticker, exc)
|
||||||
|
return []
|
||||||
|
|
||||||
if not all_news:
|
if not all_news:
|
||||||
return []
|
return []
|
||||||
@@ -243,6 +269,8 @@ def get_company_news(
|
|||||||
def get_market_cap(ticker: str, end_date: str) -> float | None:
|
def get_market_cap(ticker: str, end_date: str) -> float | None:
|
||||||
"""Fetch market cap from the API. Finnhub values are converted from millions."""
|
"""Fetch market cap from the API. Finnhub values are converted from millions."""
|
||||||
ticker = normalize_symbol(ticker)
|
ticker = normalize_symbol(ticker)
|
||||||
|
if not ticker:
|
||||||
|
return None
|
||||||
|
|
||||||
def _metrics_lookup(symbol: str, date: str):
|
def _metrics_lookup(symbol: str, date: str):
|
||||||
for source in _router.api_sources():
|
for source in _router.api_sources():
|
||||||
@@ -256,11 +284,15 @@ def get_market_cap(ticker: str, end_date: str) -> float | None:
|
|||||||
limit=10,
|
limit=10,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
market_cap, _ = _router.get_market_cap(
|
market_cap, _ = _router.get_market_cap(
|
||||||
ticker=ticker,
|
ticker=ticker,
|
||||||
end_date=end_date,
|
end_date=end_date,
|
||||||
metrics_lookup=_metrics_lookup,
|
metrics_lookup=_metrics_lookup,
|
||||||
)
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.info("Market cap lookup failed for %s: %s", ticker, exc)
|
||||||
|
return None
|
||||||
return market_cap
|
return market_cap
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/png" href="/trading_logo.png" />
|
<link rel="icon" type="image/png" href="/trading_logo.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ import AgentFeed from './components/AgentFeed';
|
|||||||
import StockLogo from './components/StockLogo';
|
import StockLogo from './components/StockLogo';
|
||||||
import StatisticsView from './components/StatisticsView';
|
import StatisticsView from './components/StatisticsView';
|
||||||
import PerformanceView from './components/PerformanceView';
|
import PerformanceView from './components/PerformanceView';
|
||||||
import AboutModal from './components/AboutModal';
|
import StockExplainView from './components/StockExplainView.jsx';
|
||||||
import RulesView from './components/RulesView';
|
|
||||||
import Header from './components/Header.jsx';
|
import Header from './components/Header.jsx';
|
||||||
|
import WatchlistPanel from './components/WatchlistPanel.jsx';
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
import { formatNumber, formatTickerPrice } from './utils/formatters';
|
import { formatNumber, formatTickerPrice } from './utils/formatters';
|
||||||
@@ -39,9 +39,8 @@ export default function LiveTradingApp() {
|
|||||||
const [currentDate, setCurrentDate] = useState(null);
|
const [currentDate, setCurrentDate] = useState(null);
|
||||||
const [progress, setProgress] = useState({ current: 0, total: 0 });
|
const [progress, setProgress] = useState({ current: 0, total: 0 });
|
||||||
const [now, setNow] = useState(() => new Date());
|
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 [currentView, setCurrentView] = useState('chart'); // Start with chart, then animate to room
|
||||||
const [isInitialAnimating, setIsInitialAnimating] = useState(true);
|
const [isInitialAnimating, setIsInitialAnimating] = useState(true);
|
||||||
const [lastUpdate, setLastUpdate] = useState(new Date());
|
const [lastUpdate, setLastUpdate] = useState(new Date());
|
||||||
@@ -71,6 +70,11 @@ export default function LiveTradingApp() {
|
|||||||
// Ticker prices (now from real-time data)
|
// Ticker prices (now from real-time data)
|
||||||
const [tickers, setTickers] = useState(INITIAL_TICKERS);
|
const [tickers, setTickers] = useState(INITIAL_TICKERS);
|
||||||
const [rollingTickers, setRollingTickers] = useState({});
|
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
|
// Room bubbles
|
||||||
const [bubbles, setBubbles] = useState({});
|
const [bubbles, setBubbles] = useState({});
|
||||||
@@ -84,10 +88,18 @@ export default function LiveTradingApp() {
|
|||||||
const [marketStatus, setMarketStatus] = useState(null); // { status, status_text, ... }
|
const [marketStatus, setMarketStatus] = useState(null); // { status, status_text, ... }
|
||||||
const [virtualTime, setVirtualTime] = useState(null); // Virtual time from server (for mock mode)
|
const [virtualTime, setVirtualTime] = useState(null); // Virtual time from server (for mock mode)
|
||||||
const [dataSources, setDataSources] = useState(null);
|
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 clientRef = useRef(null);
|
||||||
const containerRef = useRef(null);
|
const containerRef = useRef(null);
|
||||||
const agentFeedRef = useRef(null);
|
const agentFeedRef = useRef(null);
|
||||||
|
const isWatchlistSavingRef = useRef(false);
|
||||||
|
const requestedStockHistoryRef = useRef(new Set());
|
||||||
|
|
||||||
// Track last virtual time update to calculate increment
|
// Track last virtual time update to calculate increment
|
||||||
const lastVirtualTimeRef = useRef(null);
|
const lastVirtualTimeRef = useRef(null);
|
||||||
@@ -96,12 +108,311 @@ export default function LiveTradingApp() {
|
|||||||
// Last day history for replay
|
// Last day history for replay
|
||||||
const [lastDayHistory, setLastDayHistory] = useState([]);
|
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
|
// Determine if LIVE tab should be enabled
|
||||||
const isLiveEnabled = useMemo(() => {
|
const isLiveEnabled = useMemo(() => {
|
||||||
if (!marketStatus) return false;
|
if (!marketStatus) return false;
|
||||||
return marketStatus.status === 'open';
|
return marketStatus.status === 'open';
|
||||||
}, [marketStatus]);
|
}, [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
|
// Switch away from LIVE tab when market closes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isLiveEnabled && chartTab === 'live') {
|
if (!isLiveEnabled && chartTab === 'live') {
|
||||||
@@ -109,6 +420,27 @@ export default function LiveTradingApp() {
|
|||||||
}
|
}
|
||||||
}, [isLiveEnabled, chartTab]);
|
}, [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)
|
// Clock - use virtual time if available (for mock mode)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (virtualTime) {
|
if (virtualTime) {
|
||||||
@@ -253,6 +585,10 @@ export default function LiveTradingApp() {
|
|||||||
// Error response (for fast forward errors)
|
// Error response (for fast forward errors)
|
||||||
error: (e) => {
|
error: (e) => {
|
||||||
console.error('[Error]', e.message);
|
console.error('[Error]', e.message);
|
||||||
|
if (isWatchlistSavingRef.current) {
|
||||||
|
setIsWatchlistSaving(false);
|
||||||
|
setWatchlistFeedback({ type: 'error', text: e.message || '更新 watchlist 失败' });
|
||||||
|
}
|
||||||
|
|
||||||
// Handle fast forward errors
|
// Handle fast forward errors
|
||||||
if (e.message && e.message.includes('fast forward')) {
|
if (e.message && e.message.includes('fast forward')) {
|
||||||
@@ -307,6 +643,12 @@ export default function LiveTradingApp() {
|
|||||||
if (state.data_sources) {
|
if (state.data_sources) {
|
||||||
setDataSources(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模式
|
// 检查是否是mock模式
|
||||||
const isMockMode = state.is_mock_mode === true;
|
const isMockMode = state.is_mock_mode === true;
|
||||||
if (state.market_status) {
|
if (state.market_status) {
|
||||||
@@ -356,6 +698,9 @@ export default function LiveTradingApp() {
|
|||||||
if (state.dashboard.leaderboard) setLeaderboard(state.dashboard.leaderboard);
|
if (state.dashboard.leaderboard) setLeaderboard(state.dashboard.leaderboard);
|
||||||
}
|
}
|
||||||
if (state.realtime_prices) updateTickersFromPrices(state.realtime_prices);
|
if (state.realtime_prices) updateTickersFromPrices(state.realtime_prices);
|
||||||
|
if (state.price_history) {
|
||||||
|
setPriceHistoryByTicker(normalizePriceHistory(state.price_history));
|
||||||
|
}
|
||||||
|
|
||||||
// Load and process historical feed data
|
// Load and process historical feed data
|
||||||
if (state.feed_history && Array.isArray(state.feed_history)) {
|
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
|
// Real-time price updates
|
||||||
price_update: (e) => {
|
price_update: (e) => {
|
||||||
try {
|
try {
|
||||||
@@ -402,6 +816,24 @@ export default function LiveTradingApp() {
|
|||||||
setIsConnected(true);
|
setIsConnected(true);
|
||||||
console.log(`[Price Update] ${symbol}: $${price} (ret: ${ret !== undefined ? ret.toFixed(2) : 'N/A'}%)`);
|
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
|
// Update ticker price with animation
|
||||||
setTickers(prevTickers => {
|
setTickers(prevTickers => {
|
||||||
return prevTickers.map(ticker => {
|
return prevTickers.map(ticker => {
|
||||||
@@ -714,7 +1146,7 @@ export default function LiveTradingApp() {
|
|||||||
clientRef.current.disconnect();
|
clientRef.current.disconnect();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, []); // Empty dependency array - only run once on mount
|
}, [addSystemMessage, buildTickersFromSymbols, processFeedEvent, processHistoricalFeed]); // Only reconnect if handlers change
|
||||||
|
|
||||||
// Resizing handlers
|
// Resizing handlers
|
||||||
const handleMouseDown = (e) => {
|
const handleMouseDown = (e) => {
|
||||||
@@ -755,10 +1187,7 @@ export default function LiveTradingApp() {
|
|||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="header">
|
<div className="header">
|
||||||
<Header
|
<Header />
|
||||||
onEvoTradersClick={() => setShowAboutModal(true)}
|
|
||||||
evoTradersLinkStyle="default"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="header-right" style={{ display: 'flex', alignItems: 'center', gap: 24, marginLeft: 'auto', flexWrap: 'wrap', minWidth: 0 }}>
|
<div className="header-right" style={{ display: 'flex', alignItems: 'center', gap: 24, marginLeft: 'auto', flexWrap: 'wrap', minWidth: 0 }}>
|
||||||
{/* Mock Mode Indicator */}
|
{/* Mock Mode Indicator */}
|
||||||
@@ -885,21 +1314,41 @@ export default function LiveTradingApp() {
|
|||||||
<>
|
<>
|
||||||
<span className="status-sep">·</span>
|
<span className="status-sep">·</span>
|
||||||
<span className={`market-text ${serverMode === 'backtest' ? 'backtest' : (marketStatus.status === 'open' ? 'open' : 'closed')}`}>
|
<span className={`market-text ${serverMode === 'backtest' ? 'backtest' : (marketStatus.status === 'open' ? 'open' : 'closed')}`}>
|
||||||
{marketStatus.status_text || (marketStatus.status === 'open' ? '开盘' : '收盘')}
|
{marketStatusLabel}
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{dataSources?.last_success?.prices && (
|
{priceSourceLabel && (
|
||||||
<>
|
<>
|
||||||
<span className="status-sep">·</span>
|
<span className="status-sep">·</span>
|
||||||
<span className="market-text backtest">
|
<span className="market-text backtest">
|
||||||
DATA {String(dataSources.last_success.prices).toUpperCase()}
|
{priceSourceLabel}
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<span className="status-sep">·</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>
|
<span className="time-text">{lastUpdate.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false })}</span>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -910,7 +1359,7 @@ export default function LiveTradingApp() {
|
|||||||
<div className="ticker-track">
|
<div className="ticker-track">
|
||||||
{[0, 1].map((groupIdx) => (
|
{[0, 1].map((groupIdx) => (
|
||||||
<div key={groupIdx} className="ticker-group">
|
<div key={groupIdx} className="ticker-group">
|
||||||
{tickers.map(ticker => (
|
{displayTickers.map(ticker => (
|
||||||
<div key={`${ticker.symbol}-${groupIdx}`} className="ticker-item">
|
<div key={`${ticker.symbol}-${groupIdx}`} className="ticker-item">
|
||||||
<StockLogo ticker={ticker.symbol} size={16} />
|
<StockLogo ticker={ticker.symbol} size={16} />
|
||||||
<span className="ticker-symbol">{ticker.symbol}</span>
|
<span className="ticker-symbol">{ticker.symbol}</span>
|
||||||
@@ -947,13 +1396,6 @@ export default function LiveTradingApp() {
|
|||||||
<div className="chart-section">
|
<div className="chart-section">
|
||||||
<div className="view-container">
|
<div className="view-container">
|
||||||
<div className="view-nav-bar">
|
<div className="view-nav-bar">
|
||||||
<button
|
|
||||||
className={`view-nav-btn ${currentView === 'rules' ? 'active' : ''}`}
|
|
||||||
onClick={() => setCurrentView('rules')}
|
|
||||||
>
|
|
||||||
规则
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className={`view-nav-btn ${currentView === 'room' ? 'active' : ''}`}
|
className={`view-nav-btn ${currentView === 'room' ? 'active' : ''}`}
|
||||||
onClick={() => setCurrentView('room')}
|
onClick={() => setCurrentView('room')}
|
||||||
@@ -961,6 +1403,13 @@ export default function LiveTradingApp() {
|
|||||||
交易室
|
交易室
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className={`view-nav-btn ${currentView === 'explain' ? 'active' : ''}`}
|
||||||
|
onClick={() => setCurrentView('explain')}
|
||||||
|
>
|
||||||
|
个股解释
|
||||||
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className={`view-nav-btn ${currentView === 'chart' ? 'active' : ''}`}
|
className={`view-nav-btn ${currentView === 'chart' ? 'active' : ''}`}
|
||||||
onClick={() => setCurrentView('chart')}
|
onClick={() => setCurrentView('chart')}
|
||||||
@@ -977,12 +1426,15 @@ export default function LiveTradingApp() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Slider container with four views */}
|
{/* 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' : ''}`}>
|
<div className={`view-slider-four ${
|
||||||
{/* Rules View Panel */}
|
currentView === 'room'
|
||||||
<div className="view-panel">
|
? 'show-room'
|
||||||
<RulesView />
|
: currentView === 'explain'
|
||||||
</div>
|
? 'show-explain'
|
||||||
|
: currentView === 'statistics'
|
||||||
|
? 'show-statistics'
|
||||||
|
: 'show-chart'
|
||||||
|
} ${!isInitialAnimating ? 'normal-speed' : ''}`}>
|
||||||
{/* Room View Panel */}
|
{/* Room View Panel */}
|
||||||
<div className="view-panel">
|
<div className="view-panel">
|
||||||
<RoomView
|
<RoomView
|
||||||
@@ -994,6 +1446,23 @@ export default function LiveTradingApp() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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 */}
|
{/* Chart View Panel */}
|
||||||
<div className="view-panel">
|
<div className="view-panel">
|
||||||
<div className="chart-container">
|
<div className="chart-container">
|
||||||
@@ -1059,9 +1528,6 @@ export default function LiveTradingApp() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
||||||
{/* About Modal */}
|
|
||||||
{showAboutModal && <AboutModal onClose={() => setShowAboutModal(false)} />}
|
|
||||||
</div>
|
</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
|
// Get current selection display info
|
||||||
const getCurrentSelectionInfo = () => {
|
const getCurrentSelectionInfo = () => {
|
||||||
if (selectedAgent === 'all') {
|
if (selectedAgent === 'all') {
|
||||||
return { label: 'All Agents', modelInfo: null };
|
return { label: '全部角色', modelInfo: null };
|
||||||
}
|
}
|
||||||
const agentInfo = getAgentInfoByName(selectedAgent);
|
const agentInfo = getAgentInfoByName(selectedAgent);
|
||||||
const modelInfo = agentInfo ? getModelIcon(agentInfo.modelName, agentInfo.modelProvider) : null;
|
const modelInfo = agentInfo ? getModelIcon(agentInfo.modelName, agentInfo.modelProvider) : null;
|
||||||
@@ -191,7 +191,7 @@ const AgentFeed = forwardRef(({ feed, leaderboard }, ref) => {
|
|||||||
setDropdownOpen(false);
|
setDropdownOpen(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>全部 Agents</span>
|
<span>全部角色</span>
|
||||||
</div>
|
</div>
|
||||||
{uniqueAgents.map(agent => {
|
{uniqueAgents.map(agent => {
|
||||||
const agentInfo = getAgentInfoByName(agent);
|
const agentInfo = getAgentInfoByName(agent);
|
||||||
@@ -419,17 +419,14 @@ function MemoryItem({ memory, itemId, isHighlighted }) {
|
|||||||
onMouseEnter={() => setShowTooltip(true)}
|
onMouseEnter={() => setShowTooltip(true)}
|
||||||
onMouseLeave={() => setShowTooltip(false)}
|
onMouseLeave={() => setShowTooltip(false)}
|
||||||
>
|
>
|
||||||
<a
|
<span
|
||||||
href="https://github.com/agentscope-ai/ReMe"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
style={{ display: 'flex', alignItems: 'center', textDecoration: 'none' }}
|
style={{ display: 'flex', alignItems: 'center', textDecoration: 'none' }}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={ASSETS.remeLogo}
|
src={ASSETS.remeLogo}
|
||||||
alt="ReMe"
|
alt="Memory"
|
||||||
style={{
|
style={{
|
||||||
cursor: 'pointer',
|
cursor: 'default',
|
||||||
height: '12px',
|
height: '12px',
|
||||||
width: 'auto',
|
width: 'auto',
|
||||||
objectFit: 'contain',
|
objectFit: 'contain',
|
||||||
@@ -449,9 +446,9 @@ function MemoryItem({ memory, itemId, isHighlighted }) {
|
|||||||
lineHeight: 1,
|
lineHeight: 1,
|
||||||
pointerEvents: 'none'
|
pointerEvents: 'none'
|
||||||
}}>
|
}}>
|
||||||
↗
|
MEMORY
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
<span style={{
|
<span style={{
|
||||||
background: 'linear-gradient(90deg, #00C2FF 0%, #5C4CE0 100%)',
|
background: 'linear-gradient(90deg, #00C2FF 0%, #5C4CE0 100%)',
|
||||||
@@ -497,10 +494,10 @@ function MemoryItem({ memory, itemId, isHighlighted }) {
|
|||||||
color: 'transparent',
|
color: 'transparent',
|
||||||
display: 'inline-block'
|
display: 'inline-block'
|
||||||
}}>
|
}}>
|
||||||
Memory powered by AgentScope-ReMe
|
Runtime Memory Layer
|
||||||
</div>
|
</div>
|
||||||
<div style={{ color: '#475569', opacity: 0.9 }}>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,253 +1,29 @@
|
|||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Header Component
|
* Header Component
|
||||||
* Reusable header brand with EvoTraders logo, GitHub link, and Contact Us section
|
* Reusable header brand for EvoTraders.
|
||||||
*
|
|
||||||
* @param {Function} onEvoTradersClick - Optional callback when EvoTraders is clicked
|
|
||||||
* @param {string} evoTradersLinkStyle - Optional style variant: 'default' | 'close'
|
|
||||||
*/
|
*/
|
||||||
export default function Header({
|
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();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="header-title" style={{ flex: '0 1 auto', minWidth: 0 }}>
|
<div className="header-title" style={{ flex: '0 1 auto', minWidth: 0 }}>
|
||||||
<span
|
<span
|
||||||
className="header-link"
|
className="header-link"
|
||||||
onClick={handleEvoTradersClick}
|
style={{
|
||||||
style={{ cursor: 'pointer', padding: '4px 8px', borderRadius: '3px', display: 'inline-flex', alignItems: 'center', gap: '8px' }}
|
padding: '4px 8px',
|
||||||
|
borderRadius: '3px',
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '8px'
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src="/trading_logo.png"
|
src="/trading_logo.png"
|
||||||
alt="EvoTraders Logo"
|
alt="EvoTraders Logo"
|
||||||
style={{ height: '24px', width: 'auto' }}
|
style={{ height: '24px', width: 'auto' }}
|
||||||
/>
|
/>
|
||||||
EvoTraders {evoTradersLinkStyle === 'close' ? (
|
EvoTraders
|
||||||
<span className="link-arrow">↙</span>
|
|
||||||
) : (
|
|
||||||
<span className="link-arrow">↗</span>
|
|
||||||
)}
|
|
||||||
</span>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -554,7 +554,7 @@ export default function NetValueChart({ equity, baseline, baseline_vw, momentum,
|
|||||||
fontFamily: '"Courier New", monospace',
|
fontFamily: '"Courier New", monospace',
|
||||||
fontSize: '12px'
|
fontSize: '12px'
|
||||||
}}>
|
}}>
|
||||||
NO DATA AVAILABLE
|
暂无图表数据
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -828,4 +828,3 @@ export default function NetValueChart({ equity, baseline, baseline_vw, momentum,
|
|||||||
</ResponsiveContainer>
|
</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 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
|
* Generate a unique ID for feed items
|
||||||
*/
|
*/
|
||||||
@@ -26,7 +108,7 @@ const eventToMessage = (evt) => {
|
|||||||
id: generateId("msg"),
|
id: generateId("msg"),
|
||||||
timestamp,
|
timestamp,
|
||||||
agentId: evt.agentId,
|
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",
|
role: agent?.role || evt.role || "Agent",
|
||||||
content: evt.content
|
content: evt.content
|
||||||
};
|
};
|
||||||
@@ -50,7 +132,7 @@ const eventToMessage = (evt) => {
|
|||||||
timestamp,
|
timestamp,
|
||||||
agent: "System",
|
agent: "System",
|
||||||
role: "System",
|
role: "System",
|
||||||
content: evt.content || `${evt.type}: ${evt.date || ""}`
|
content: normalizeSystemContent(evt.content || `${evt.type}: ${evt.date || ""}`)
|
||||||
};
|
};
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -129,7 +211,7 @@ export function useFeedProcessor() {
|
|||||||
// Start a new conference
|
// Start a new conference
|
||||||
currentConference = {
|
currentConference = {
|
||||||
id: evt.conferenceId || generateId("conf"),
|
id: evt.conferenceId || generateId("conf"),
|
||||||
title: evt.title || "Team Conference",
|
title: normalizeConferenceTitle(evt.title || "Team Conference"),
|
||||||
startTime: evt.timestamp || evt.ts || Date.now(),
|
startTime: evt.timestamp || evt.ts || Date.now(),
|
||||||
endTime: null,
|
endTime: null,
|
||||||
isLive: false,
|
isLive: false,
|
||||||
@@ -209,7 +291,7 @@ export function useFeedProcessor() {
|
|||||||
if (evt.type === "conference_start") {
|
if (evt.type === "conference_start") {
|
||||||
const conference = {
|
const conference = {
|
||||||
id: evt.conferenceId || generateId("conf"),
|
id: evt.conferenceId || generateId("conf"),
|
||||||
title: evt.title || "Team Conference",
|
title: normalizeConferenceTitle(evt.title || "Team Conference"),
|
||||||
startTime: evt.timestamp || evt.ts || Date.now(),
|
startTime: evt.timestamp || evt.ts || Date.now(),
|
||||||
endTime: null,
|
endTime: null,
|
||||||
isLive: true,
|
isLive: true,
|
||||||
@@ -312,7 +394,7 @@ export function useFeedProcessor() {
|
|||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
agent: "System",
|
agent: "System",
|
||||||
role: "System",
|
role: "System",
|
||||||
content
|
content: normalizeSystemContent(content)
|
||||||
};
|
};
|
||||||
|
|
||||||
const activeConf = activeConferenceRef.current;
|
const activeConf = activeConferenceRef.current;
|
||||||
|
|||||||
@@ -1030,8 +1030,9 @@ export default function GlobalStyles() {
|
|||||||
/* Three-view slider (Room / Chart / Statistics) */
|
/* Three-view slider (Room / Chart / Statistics) */
|
||||||
.view-slider-three {
|
.view-slider-three {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
top: 40px;
|
||||||
width: 300%;
|
width: 300%;
|
||||||
height: 100%;
|
height: calc(100% - 40px);
|
||||||
display: flex;
|
display: flex;
|
||||||
transition: transform 1.6s cubic-bezier(0.34, 1.56, 0.64, 1);
|
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%);
|
transform: translateX(-66.666%);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Four-view slider (Rules / Room / Chart / Statistics) */
|
/* Four-view slider (Room / Explain / Chart / Statistics) */
|
||||||
.view-slider-four {
|
.view-slider-four {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 40px;
|
top: 40px;
|
||||||
@@ -1066,11 +1067,11 @@ export default function GlobalStyles() {
|
|||||||
transition: transform 0.8s cubic-bezier(0.34, 1.56, 0.64, 1);
|
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);
|
transform: translateX(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.view-slider-four.show-room {
|
.view-slider-four.show-explain {
|
||||||
transform: translateX(-25%);
|
transform: translateX(-25%);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1873,4 +1874,3 @@ export default function GlobalStyles() {
|
|||||||
`}</style>
|
`}</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: {}
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
14
runs/reload_demo_runtime_cfg/BOOTSTRAP.md
Normal file
14
runs/reload_demo_runtime_cfg/BOOTSTRAP.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
tickers:
|
||||||
|
- AAPL
|
||||||
|
- MSFT
|
||||||
|
initial_cash: 999999
|
||||||
|
margin_requirement: 0.25
|
||||||
|
enable_memory: false
|
||||||
|
max_comm_cycles: 7
|
||||||
|
agent_overrides: {}
|
||||||
|
---
|
||||||
|
|
||||||
|
# Bootstrap
|
||||||
|
|
||||||
|
Body 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
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user