2 Commits

Author SHA1 Message Date
a41cd705b4 Prefer SQLite signals in stock explain view 2026-03-16 02:22:59 +08:00
564c92c0c8 确认PokieTicker新闻库数据源 2026-03-16 02:19:25 +08:00
182 changed files with 6530 additions and 1050 deletions

View File

@@ -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

View File

@@ -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__(
"cash": initial_cash, self,
"positions": {}, "portfolio",
"margin_used": 0.0, {
"margin_requirement": margin_requirement, "cash": initial_cash,
} "positions": {},
"margin_used": 0.0,
"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(
agent_id=self.name, build_agent_system_prompt(
config_name=self.config.get("config_name", "default"), agent_id=self.name,
toolkit=self.toolkit, config_name=self.config.get("config_name", "default"),
toolkit=self.toolkit,
),
) )
def _apply_runtime_sys_prompt(self, sys_prompt: str) -> None:
"""Update the prompt used by future turns and the cached system msg."""
self._sys_prompt = sys_prompt
for msg, _marks in self.memory.content:
if getattr(msg, "role", None) == "system":
msg.content = sys_prompt
break

View File

@@ -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

View File

@@ -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)

View File

@@ -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()):

View File

@@ -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)),
}

View File

@@ -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):

View File

@@ -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:

View File

@@ -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)

View File

@@ -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"],
}, },
) )

View File

@@ -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:

View File

@@ -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"""

View 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,
}

View File

@@ -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 ==========

View File

@@ -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",
] ]

View File

@@ -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]
prices, data_source = _router.get_prices(ticker, start_date, end_date) try:
prices, data_source = _router.get_prices(ticker, start_date, end_date)
except Exception as exc:
logger.info("Price lookup failed for %s: %s", ticker, exc)
return []
if not prices: 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]
financial_metrics, data_source = _router.get_financial_metrics( try:
ticker=ticker, financial_metrics, data_source = _router.get_financial_metrics(
end_date=end_date, ticker=ticker,
period=period, end_date=end_date,
limit=limit, period=period,
) 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]
all_trades, data_source = _router.get_insider_trades( try:
ticker=ticker, all_trades, data_source = _router.get_insider_trades(
end_date=end_date, ticker=ticker,
start_date=start_date, end_date=end_date,
limit=limit, start_date=start_date,
) limit=limit,
)
except Exception as exc:
logger.info("Insider trades lookup failed for %s: %s", ticker, exc)
return []
if not all_trades: 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]
all_news, data_source = _router.get_company_news( try:
ticker=ticker, all_news, data_source = _router.get_company_news(
end_date=end_date, ticker=ticker,
start_date=start_date, end_date=end_date,
limit=limit, start_date=start_date,
) limit=limit,
)
except Exception as exc:
logger.info("Company news lookup failed for %s: %s", ticker, exc)
return []
if not all_news: 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,
) )
market_cap, _ = _router.get_market_cap( try:
ticker=ticker, market_cap, _ = _router.get_market_cap(
end_date=end_date, ticker=ticker,
metrics_lookup=_metrics_lookup, end_date=end_date,
) metrics_lookup=_metrics_lookup,
)
except Exception as exc:
logger.info("Market cap lookup failed for %s: %s", ticker, exc)
return None
return market_cap return market_cap

View File

@@ -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" />

View File

@@ -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>
); );
} }

View File

@@ -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>
</>
);
}

View File

@@ -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>
</a> </span>
</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>

View File

@@ -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>
); );
} }

View File

@@ -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>
); );
} }

View File

@@ -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>
);
}

File diff suppressed because it is too large Load Diff

View 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>
);
}

View File

@@ -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;

View File

@@ -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>
); );
} }

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
[]

View 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"
}
]

View 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
}
}
}

View 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": []
}

View File

@@ -0,0 +1 @@
[]

1
reference/PokieTicker Submodule

Submodule reference/PokieTicker added at 4fed7755e5

View 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.

View 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.

View File

@@ -0,0 +1,11 @@
# Role
Optional run-scoped role override.
作为基本面分析师,你专注于:
- 公司财务健康状况和盈利能力
- 商业模式可持续性和竞争优势
- 管理层质量和公司治理
- 行业地位和市场份额
- 长期投资价值评估
你倾向于选择能够深入了解公司内在价值的工具,更偏好基本面和估值类工具。

View File

@@ -0,0 +1,9 @@
# Style
Optional run-scoped communication or reasoning style.
- 公司财务健康状况和盈利能力
- 商业模式可持续性和竞争优势
- 管理层质量和公司治理
- 行业地位和市场份额
- 长期投资价值评估

View File

@@ -0,0 +1,5 @@
# Policy
Optional run-scoped constraints, limits, or strategy policy.
Respect cash, margin, and portfolio concentration constraints before recording decisions.

View File

@@ -0,0 +1,5 @@
# Role
Optional run-scoped role override.
Synthesize analyst and risk inputs into explicit portfolio decisions.

View File

@@ -0,0 +1,5 @@
# Style
Optional run-scoped communication or reasoning style.
Be concise, capital-aware, and explicit about sizing rationale.

View File

@@ -0,0 +1,5 @@
# Policy
Optional run-scoped constraints, limits, or strategy policy.
Use available risk tools before issuing the final risk memo.

View File

@@ -0,0 +1,5 @@
# Role
Optional run-scoped role override.
Quantify concentration, leverage, liquidity, and volatility risk before trade execution.

View 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.

View 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.

View File

@@ -0,0 +1,11 @@
# Role
Optional run-scoped role override.
作为情绪分析师,你专注于:
- 市场参与者情绪变化
- 新闻舆情和媒体影响
- 内部人交易行为
- 投资者恐慌和贪婪情绪
- 市场预期和心理因素
你倾向于选择能够反映市场情绪和投资者行为的工具,更偏好情绪和行为类工具。

View File

@@ -0,0 +1,9 @@
# Style
Optional run-scoped communication or reasoning style.
- 市场参与者情绪变化
- 新闻舆情和媒体影响
- 内部人交易行为
- 投资者恐慌和贪婪情绪
- 市场预期和心理因素

View 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.

View File

@@ -0,0 +1,11 @@
# Role
Optional run-scoped role override.
作为技术分析师,你专注于:
- 价格趋势和图表形态
- 技术指标和交易信号
- 市场情绪和资金流向
- 支撑/阻力位和关键价格点
- 中短期交易机会
你倾向于选择能够捕捉价格动态和市场趋势的工具,更偏好技术分析类工具。

View File

@@ -0,0 +1,9 @@
# Style
Optional run-scoped communication or reasoning style.
- 价格趋势和图表形态
- 技术指标和交易信号
- 市场情绪和资金流向
- 支撑/阻力位和关键价格点
- 中短期交易机会

View 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.

View File

@@ -0,0 +1,11 @@
# Role
Optional run-scoped role override.
作为估值分析师,你专注于:
- 公司内在价值计算
- 不同估值方法的比较
- 估值模型假设和敏感性分析
- 相对估值和绝对估值
- 投资安全边际评估
你倾向于选择能够准确计算公司价值的工具,更偏好估值模型和基本面工具。

View File

@@ -0,0 +1,9 @@
# Style
Optional run-scoped communication or reasoning style.
- 公司内在价值计算
- 不同估值方法的比较
- 估值模型假设和敏感性分析
- 相对估值和绝对估值
- 投资安全边际评估

View 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: {}

View 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.

View File

@@ -0,0 +1,21 @@
---
name: portfolio_decisioning
description: Synthesize analyst inputs and risk feedback into explicit portfolio decisions.
---
# Portfolio Decisioning
Use this skill when you are responsible for converting team analysis into final trades.
## Workflow
1. Read analyst conclusions and risk warnings before acting.
2. Evaluate the current portfolio, cash, and margin constraints.
3. Record one explicit decision per ticker using the decision tool.
4. Summarize the portfolio-level rationale after all decisions are recorded.
## Guardrails
- Position sizing must respect capital and margin limits.
- Prefer smaller size when analyst conviction and risk signals disagree.
- Do not leave a ticker undecided when the task expects a full slate of decisions.

View 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.

View 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.

View 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.

View 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.

View 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"
}

View File

@@ -0,0 +1 @@
[]

View 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"
}
]

View 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
}
}
}

View 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": []
}

View File

@@ -0,0 +1 @@
[]

View 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.

View File

@@ -0,0 +1,2 @@
# POLICY

View File

@@ -0,0 +1,2 @@
# ROLE

View File

@@ -0,0 +1,2 @@
# STYLE

View File

@@ -0,0 +1,2 @@
# POLICY

View File

@@ -0,0 +1,2 @@
# ROLE

View File

@@ -0,0 +1,2 @@
# STYLE

View File

@@ -0,0 +1,2 @@
# POLICY

View File

@@ -0,0 +1,2 @@
# ROLE

View File

@@ -0,0 +1,3 @@
# Style
STYLE_MARKER_TWO

View File

@@ -0,0 +1,2 @@
# POLICY

View File

@@ -0,0 +1,2 @@
# ROLE

View File

@@ -0,0 +1,2 @@
# STYLE

View File

@@ -0,0 +1,2 @@
# POLICY

View File

@@ -0,0 +1,2 @@
# ROLE

View File

@@ -0,0 +1,2 @@
# STYLE

View File

@@ -0,0 +1,2 @@
# POLICY

View File

@@ -0,0 +1,2 @@
# ROLE

View File

@@ -0,0 +1,2 @@
# STYLE

View File

@@ -0,0 +1,4 @@
global_enabled_skills: []
global_disabled_skills: []
agent_enabled_skills: {}
agent_disabled_skills: {}

View 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.

View File

@@ -0,0 +1,21 @@
---
name: portfolio_decisioning
description: Synthesize analyst inputs and risk feedback into explicit portfolio decisions.
---
# Portfolio Decisioning
Use this skill when you are responsible for converting team analysis into final trades.
## Workflow
1. Read analyst conclusions and risk warnings before acting.
2. Evaluate the current portfolio, cash, and margin constraints.
3. Record one explicit decision per ticker using the decision tool.
4. Summarize the portfolio-level rationale after all decisions are recorded.
## Guardrails
- Position sizing must respect capital and margin limits.
- Prefer smaller size when analyst conviction and risk signals disagree.
- Do not leave a ticker undecided when the task expects a full slate of decisions.

View 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.

View 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.

View 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.

View 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.

View 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.

View File

@@ -0,0 +1,2 @@
# POLICY

View File

@@ -0,0 +1,2 @@
# ROLE

View File

@@ -0,0 +1,2 @@
# STYLE

View File

@@ -0,0 +1,2 @@
# POLICY

View File

@@ -0,0 +1,2 @@
# ROLE

View File

@@ -0,0 +1,2 @@
# STYLE

View File

@@ -0,0 +1,2 @@
# POLICY

Some files were not shown because too many files have changed in this diff Show More