qchen34--tradingagents
243 行
8.5 KiB
Python
243 行
8.5 KiB
Python
from pathlib import Path
|
|
from typing import Callable, Dict, Optional, Tuple
|
|
|
|
from cli.main import classify_message_type, save_report_to_disk
|
|
from cli.stats_handler import StatsCallbackHandler
|
|
from tradingagents.default_config import DEFAULT_CONFIG
|
|
from tradingagents.graph.trading_graph import TradingAgentsGraph
|
|
|
|
|
|
def _infer_stage(s: dict) -> str:
|
|
"""Map accumulated graph state to a coarse I–V stage label for the Web UI."""
|
|
if (s.get("final_trade_decision") or "").strip():
|
|
return "V — Final trade decision"
|
|
if (s.get("investment_plan") or "").strip():
|
|
return "IV — Risk / portfolio plan"
|
|
if (s.get("trader_investment_plan") or "").strip():
|
|
return "III — Trader investment plan"
|
|
inv = s.get("investment_debate_state") or {}
|
|
if isinstance(inv, dict) and (inv.get("judge_decision") or "").strip():
|
|
return "II — Investment debate (judge)"
|
|
names = []
|
|
for key, short in (
|
|
("market_report", "market"),
|
|
("sentiment_report", "social"),
|
|
("news_report", "news"),
|
|
("fundamentals_report", "fundamentals"),
|
|
):
|
|
if (s.get(key) or "").strip():
|
|
names.append(short)
|
|
if names:
|
|
return f"I — Analysts ({', '.join(names)})"
|
|
return "I — Pipeline starting"
|
|
|
|
|
|
def _web_event_role(classify_role: str) -> str:
|
|
"""Map CLI classify roles to plan buckets: Agent / Tool / System."""
|
|
if classify_role == "Data":
|
|
return "Tool"
|
|
if classify_role == "Agent":
|
|
return "Agent"
|
|
return "System"
|
|
|
|
|
|
STEP_LABELS = [
|
|
"Market analysis",
|
|
"Social sentiment analysis",
|
|
"News analysis",
|
|
"Fundamental analysis",
|
|
"Bull researcher",
|
|
"Bear researcher",
|
|
"Research manager",
|
|
"Trader",
|
|
"Aggressive risk analyst",
|
|
"Conservative risk analyst",
|
|
"Neutral risk analyst",
|
|
"Portfolio manager",
|
|
]
|
|
|
|
# Web UI 步骤列表(与 STEP_LABELS 一一对应)
|
|
STEP_LABELS_ZH = [
|
|
"大盘分析",
|
|
"社交媒体情绪",
|
|
"新闻分析",
|
|
"基本面分析",
|
|
"多头研究员",
|
|
"空头研究员",
|
|
"研究经理",
|
|
"交易员",
|
|
"激进风控分析师",
|
|
"保守风控分析师",
|
|
"中性风控分析师",
|
|
"组合经理",
|
|
]
|
|
|
|
|
|
def _infer_step_index(s: dict) -> int:
|
|
"""Infer current CLI-like step index (0-based, total 12)."""
|
|
if (s.get("final_trade_decision") or "").strip():
|
|
return 11
|
|
risk = s.get("risk_debate_state") or {}
|
|
if isinstance(risk, dict):
|
|
if (risk.get("neutral_history") or "").strip():
|
|
return 10
|
|
if (risk.get("conservative_history") or "").strip():
|
|
return 9
|
|
if (risk.get("aggressive_history") or "").strip():
|
|
return 8
|
|
if (s.get("trader_investment_plan") or "").strip():
|
|
return 7
|
|
inv = s.get("investment_debate_state") or {}
|
|
if isinstance(inv, dict):
|
|
if (inv.get("judge_decision") or "").strip():
|
|
return 6
|
|
if (inv.get("bear_history") or "").strip():
|
|
return 5
|
|
if (inv.get("bull_history") or "").strip():
|
|
return 4
|
|
if (s.get("fundamentals_report") or "").strip():
|
|
return 3
|
|
if (s.get("news_report") or "").strip():
|
|
return 2
|
|
if (s.get("sentiment_report") or "").strip():
|
|
return 1
|
|
return 0
|
|
|
|
|
|
def _preview(text: Optional[str], limit: int = 400) -> str:
|
|
if not text:
|
|
return ""
|
|
one = text.replace("\n", " ").strip()
|
|
if len(one) > limit:
|
|
return one[: limit - 3] + "..."
|
|
return one
|
|
|
|
|
|
def run_analysis(
|
|
params: Dict,
|
|
progress_cb: Callable[[str], None] | None = None,
|
|
event_cb: Callable[[str], None] | None = None,
|
|
stage_cb: Callable[[str], None] | None = None,
|
|
step_cb: Callable[[dict], None] | None = None,
|
|
stats_cb: Callable[[dict], None] | None = None,
|
|
stream_events: bool = True,
|
|
) -> Tuple[dict, str]:
|
|
"""Run analysis with web params and return final state and output dir.
|
|
|
|
When ``stream_events`` is True, uses LangGraph ``stream`` (CLI-style) and pushes
|
|
classified message lines through ``event_cb``. On failure, falls back to
|
|
``propagate`` (stage-only via ``progress_cb``).
|
|
"""
|
|
if progress_cb:
|
|
progress_cb("Preparing configuration...")
|
|
|
|
config = DEFAULT_CONFIG.copy()
|
|
config["max_debate_rounds"] = params["research_depth"]
|
|
config["max_risk_discuss_rounds"] = params["research_depth"]
|
|
config["quick_think_llm"] = params["quick_model"]
|
|
config["deep_think_llm"] = params["deep_model"]
|
|
config["backend_url"] = params["backend_url"]
|
|
config["llm_provider"] = params["llm_provider"]
|
|
config["output_language"] = params["output_language"]
|
|
config["google_thinking_level"] = params.get("google_thinking_level")
|
|
config["openai_reasoning_effort"] = params.get("openai_reasoning_effort")
|
|
config["anthropic_effort"] = params.get("anthropic_effort")
|
|
|
|
if progress_cb:
|
|
progress_cb("Initializing TradingAgents graph...")
|
|
stats_handler = StatsCallbackHandler()
|
|
graph = TradingAgentsGraph(
|
|
params["selected_analysts"],
|
|
config=config,
|
|
debug=False,
|
|
callbacks=[stats_handler],
|
|
)
|
|
graph.ticker = params["ticker"]
|
|
|
|
out_dir = (
|
|
Path("reports") / f"{params['ticker']}_{params['analysis_date'].replace('-', '')}"
|
|
)
|
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
init_agent_state = graph.propagator.create_initial_state(
|
|
params["ticker"], params["analysis_date"]
|
|
)
|
|
args = graph.propagator.get_graph_args(callbacks=[stats_handler])
|
|
invoke_config = args.get("config") or {}
|
|
|
|
final_state: dict
|
|
used_stream = False
|
|
|
|
if stream_events and (event_cb is not None or stage_cb is not None):
|
|
if progress_cb:
|
|
progress_cb("Running I→V pipeline (streaming events)...")
|
|
trace: list = []
|
|
last_msg_id = None
|
|
try:
|
|
for chunk in graph.graph.stream(init_agent_state, **args):
|
|
trace.append(chunk)
|
|
if not isinstance(chunk, dict):
|
|
continue
|
|
if stage_cb:
|
|
stage_cb(_infer_stage(chunk))
|
|
if step_cb:
|
|
step_i = _infer_step_index(chunk)
|
|
step_cb(
|
|
{
|
|
"index": step_i,
|
|
"total": len(STEP_LABELS),
|
|
"label": STEP_LABELS[step_i],
|
|
"steps": STEP_LABELS,
|
|
}
|
|
)
|
|
msgs = chunk.get("messages") or []
|
|
if event_cb is not None and len(msgs) > 0:
|
|
last_message = msgs[-1]
|
|
msg_id = getattr(last_message, "id", None)
|
|
if msg_id != last_msg_id:
|
|
last_msg_id = msg_id
|
|
role, content = classify_message_type(last_message)
|
|
web_role = _web_event_role(role)
|
|
line = f"[{web_role}] {_preview(content)}"
|
|
event_cb(line)
|
|
if stats_cb:
|
|
stats_cb(stats_handler.get_stats())
|
|
if trace:
|
|
final_state = trace[-1]
|
|
used_stream = True
|
|
if event_cb is not None:
|
|
st = stats_handler.get_stats()
|
|
event_cb(
|
|
f"[System] final stats: llm_calls={st['llm_calls']} "
|
|
f"tool_calls={st['tool_calls']} "
|
|
f"tokens_in={st['tokens_in']} tokens_out={st['tokens_out']}"
|
|
)
|
|
else:
|
|
raise RuntimeError("empty stream trace")
|
|
except Exception as exc:
|
|
if progress_cb:
|
|
progress_cb(f"Streaming failed ({exc!r}); falling back to invoke...")
|
|
used_stream = False
|
|
|
|
if not used_stream:
|
|
if progress_cb:
|
|
progress_cb("Running I→V analysis pipeline...")
|
|
if step_cb:
|
|
step_cb({"index": 0, "total": len(STEP_LABELS), "label": STEP_LABELS[0], "steps": STEP_LABELS})
|
|
final_state = graph.graph.invoke(init_agent_state, config=invoke_config)
|
|
if step_cb:
|
|
step_cb({"index": len(STEP_LABELS) - 1, "total": len(STEP_LABELS), "label": STEP_LABELS[-1], "steps": STEP_LABELS})
|
|
if stats_cb:
|
|
stats_cb(stats_handler.get_stats())
|
|
|
|
graph.curr_state = final_state
|
|
graph._log_state(params["analysis_date"], final_state)
|
|
|
|
if progress_cb:
|
|
progress_cb("Saving report files...")
|
|
save_report_to_disk(final_state, params["ticker"], out_dir)
|
|
|
|
if progress_cb:
|
|
progress_cb("Done.")
|
|
return final_state, str(out_dir)
|