Merge 9db0a0a040 into fa4d01c23a
This commit is contained in:
commit
9e48545aa9
119
cli/main.py
119
cli/main.py
|
|
@ -26,6 +26,7 @@ from rich.rule import Rule
|
|||
|
||||
from tradingagents.graph.trading_graph import TradingAgentsGraph
|
||||
from tradingagents.default_config import DEFAULT_CONFIG
|
||||
from tradingagents.metrics_config import is_metric_enabled, MetricsConfig, DEFAULT_METRICS_CONFIG, parse_metrics_flag, list_all_metrics
|
||||
from cli.models import AnalystType
|
||||
from cli.utils import *
|
||||
from cli.announcements import fetch_announcements, display_announcements
|
||||
|
|
@ -81,14 +82,17 @@ class MessageBuffer:
|
|||
self.report_sections = {}
|
||||
self.selected_analysts = []
|
||||
self._processed_message_ids = set()
|
||||
self.metrics_config: MetricsConfig = DEFAULT_METRICS_CONFIG
|
||||
|
||||
def init_for_analysis(self, selected_analysts):
|
||||
def init_for_analysis(self, selected_analysts, metrics_config: MetricsConfig | None = None):
|
||||
"""Initialize agent status and report sections based on selected analysts.
|
||||
|
||||
Args:
|
||||
selected_analysts: List of analyst type strings (e.g., ["market", "news"])
|
||||
metrics_config: Optional metrics configuration to filter report sections
|
||||
"""
|
||||
self.selected_analysts = [a.lower() for a in selected_analysts]
|
||||
self.metrics_config = metrics_config or DEFAULT_METRICS_CONFIG
|
||||
|
||||
# Build agent_status dynamically
|
||||
self.agent_status = {}
|
||||
|
|
@ -187,24 +191,26 @@ class MessageBuffer:
|
|||
|
||||
def _update_final_report(self):
|
||||
report_parts = []
|
||||
mc = self.metrics_config
|
||||
|
||||
# Analyst Team Reports - use .get() to handle missing sections
|
||||
analyst_sections = ["market_report", "sentiment_report", "news_report", "fundamentals_report"]
|
||||
if any(self.report_sections.get(section) for section in analyst_sections):
|
||||
visible_analysts = [s for s in analyst_sections if self.report_sections.get(s) and is_metric_enabled(mc, "analysts", s)]
|
||||
if visible_analysts:
|
||||
report_parts.append("## Analyst Team Reports")
|
||||
if self.report_sections.get("market_report"):
|
||||
if self.report_sections.get("market_report") and is_metric_enabled(mc, "analysts", "market_report"):
|
||||
report_parts.append(
|
||||
f"### Market Analysis\n{self.report_sections['market_report']}"
|
||||
)
|
||||
if self.report_sections.get("sentiment_report"):
|
||||
if self.report_sections.get("sentiment_report") and is_metric_enabled(mc, "analysts", "sentiment_report"):
|
||||
report_parts.append(
|
||||
f"### Social Sentiment\n{self.report_sections['sentiment_report']}"
|
||||
)
|
||||
if self.report_sections.get("news_report"):
|
||||
if self.report_sections.get("news_report") and is_metric_enabled(mc, "analysts", "news_report"):
|
||||
report_parts.append(
|
||||
f"### News Analysis\n{self.report_sections['news_report']}"
|
||||
)
|
||||
if self.report_sections.get("fundamentals_report"):
|
||||
if self.report_sections.get("fundamentals_report") and is_metric_enabled(mc, "analysts", "fundamentals_report"):
|
||||
report_parts.append(
|
||||
f"### Fundamentals Analysis\n{self.report_sections['fundamentals_report']}"
|
||||
)
|
||||
|
|
@ -215,12 +221,12 @@ class MessageBuffer:
|
|||
report_parts.append(f"{self.report_sections['investment_plan']}")
|
||||
|
||||
# Trading Team Reports
|
||||
if self.report_sections.get("trader_investment_plan"):
|
||||
if self.report_sections.get("trader_investment_plan") and is_metric_enabled(mc, "trading", "trader_investment_plan"):
|
||||
report_parts.append("## Trading Team Plan")
|
||||
report_parts.append(f"{self.report_sections['trader_investment_plan']}")
|
||||
|
||||
# Portfolio Management Decision
|
||||
if self.report_sections.get("final_trade_decision"):
|
||||
if self.report_sections.get("final_trade_decision") and is_metric_enabled(mc, "portfolio", "final_trade_decision"):
|
||||
report_parts.append("## Portfolio Management Decision")
|
||||
report_parts.append(f"{self.report_sections['final_trade_decision']}")
|
||||
|
||||
|
|
@ -636,27 +642,28 @@ def get_analysis_date():
|
|||
)
|
||||
|
||||
|
||||
def save_report_to_disk(final_state, ticker: str, save_path: Path):
|
||||
def save_report_to_disk(final_state, ticker: str, save_path: Path, metrics_config: MetricsConfig | None = None):
|
||||
"""Save complete analysis report to disk with organized subfolders."""
|
||||
mc = metrics_config or DEFAULT_METRICS_CONFIG
|
||||
save_path.mkdir(parents=True, exist_ok=True)
|
||||
sections = []
|
||||
|
||||
# 1. Analysts
|
||||
analysts_dir = save_path / "1_analysts"
|
||||
analyst_parts = []
|
||||
if final_state.get("market_report"):
|
||||
if final_state.get("market_report") and is_metric_enabled(mc, "analysts", "market_report"):
|
||||
analysts_dir.mkdir(exist_ok=True)
|
||||
(analysts_dir / "market.md").write_text(final_state["market_report"])
|
||||
analyst_parts.append(("Market Analyst", final_state["market_report"]))
|
||||
if final_state.get("sentiment_report"):
|
||||
if final_state.get("sentiment_report") and is_metric_enabled(mc, "analysts", "sentiment_report"):
|
||||
analysts_dir.mkdir(exist_ok=True)
|
||||
(analysts_dir / "sentiment.md").write_text(final_state["sentiment_report"])
|
||||
analyst_parts.append(("Social Analyst", final_state["sentiment_report"]))
|
||||
if final_state.get("news_report"):
|
||||
if final_state.get("news_report") and is_metric_enabled(mc, "analysts", "news_report"):
|
||||
analysts_dir.mkdir(exist_ok=True)
|
||||
(analysts_dir / "news.md").write_text(final_state["news_report"])
|
||||
analyst_parts.append(("News Analyst", final_state["news_report"]))
|
||||
if final_state.get("fundamentals_report"):
|
||||
if final_state.get("fundamentals_report") and is_metric_enabled(mc, "analysts", "fundamentals_report"):
|
||||
analysts_dir.mkdir(exist_ok=True)
|
||||
(analysts_dir / "fundamentals.md").write_text(final_state["fundamentals_report"])
|
||||
analyst_parts.append(("Fundamentals Analyst", final_state["fundamentals_report"]))
|
||||
|
|
@ -669,15 +676,15 @@ def save_report_to_disk(final_state, ticker: str, save_path: Path):
|
|||
research_dir = save_path / "2_research"
|
||||
debate = final_state["investment_debate_state"]
|
||||
research_parts = []
|
||||
if debate.get("bull_history"):
|
||||
if debate.get("bull_history") and is_metric_enabled(mc, "research", "bull_history"):
|
||||
research_dir.mkdir(exist_ok=True)
|
||||
(research_dir / "bull.md").write_text(debate["bull_history"])
|
||||
research_parts.append(("Bull Researcher", debate["bull_history"]))
|
||||
if debate.get("bear_history"):
|
||||
if debate.get("bear_history") and is_metric_enabled(mc, "research", "bear_history"):
|
||||
research_dir.mkdir(exist_ok=True)
|
||||
(research_dir / "bear.md").write_text(debate["bear_history"])
|
||||
research_parts.append(("Bear Researcher", debate["bear_history"]))
|
||||
if debate.get("judge_decision"):
|
||||
if debate.get("judge_decision") and is_metric_enabled(mc, "research", "judge_decision"):
|
||||
research_dir.mkdir(exist_ok=True)
|
||||
(research_dir / "manager.md").write_text(debate["judge_decision"])
|
||||
research_parts.append(("Research Manager", debate["judge_decision"]))
|
||||
|
|
@ -686,7 +693,7 @@ def save_report_to_disk(final_state, ticker: str, save_path: Path):
|
|||
sections.append(f"## II. Research Team Decision\n\n{content}")
|
||||
|
||||
# 3. Trading
|
||||
if final_state.get("trader_investment_plan"):
|
||||
if final_state.get("trader_investment_plan") and is_metric_enabled(mc, "trading", "trader_investment_plan"):
|
||||
trading_dir = save_path / "3_trading"
|
||||
trading_dir.mkdir(exist_ok=True)
|
||||
(trading_dir / "trader.md").write_text(final_state["trader_investment_plan"])
|
||||
|
|
@ -697,15 +704,15 @@ def save_report_to_disk(final_state, ticker: str, save_path: Path):
|
|||
risk_dir = save_path / "4_risk"
|
||||
risk = final_state["risk_debate_state"]
|
||||
risk_parts = []
|
||||
if risk.get("aggressive_history"):
|
||||
if risk.get("aggressive_history") and is_metric_enabled(mc, "risk", "aggressive_history"):
|
||||
risk_dir.mkdir(exist_ok=True)
|
||||
(risk_dir / "aggressive.md").write_text(risk["aggressive_history"])
|
||||
risk_parts.append(("Aggressive Analyst", risk["aggressive_history"]))
|
||||
if risk.get("conservative_history"):
|
||||
if risk.get("conservative_history") and is_metric_enabled(mc, "risk", "conservative_history"):
|
||||
risk_dir.mkdir(exist_ok=True)
|
||||
(risk_dir / "conservative.md").write_text(risk["conservative_history"])
|
||||
risk_parts.append(("Conservative Analyst", risk["conservative_history"]))
|
||||
if risk.get("neutral_history"):
|
||||
if risk.get("neutral_history") and is_metric_enabled(mc, "risk", "neutral_history"):
|
||||
risk_dir.mkdir(exist_ok=True)
|
||||
(risk_dir / "neutral.md").write_text(risk["neutral_history"])
|
||||
risk_parts.append(("Neutral Analyst", risk["neutral_history"]))
|
||||
|
|
@ -714,7 +721,7 @@ def save_report_to_disk(final_state, ticker: str, save_path: Path):
|
|||
sections.append(f"## IV. Risk Management Team Decision\n\n{content}")
|
||||
|
||||
# 5. Portfolio Manager
|
||||
if risk.get("judge_decision"):
|
||||
if risk.get("judge_decision") and is_metric_enabled(mc, "portfolio", "final_trade_decision"):
|
||||
portfolio_dir = save_path / "5_portfolio"
|
||||
portfolio_dir.mkdir(exist_ok=True)
|
||||
(portfolio_dir / "decision.md").write_text(risk["judge_decision"])
|
||||
|
|
@ -726,20 +733,21 @@ def save_report_to_disk(final_state, ticker: str, save_path: Path):
|
|||
return save_path / "complete_report.md"
|
||||
|
||||
|
||||
def display_complete_report(final_state):
|
||||
def display_complete_report(final_state, metrics_config: MetricsConfig | None = None):
|
||||
"""Display the complete analysis report sequentially (avoids truncation)."""
|
||||
mc = metrics_config or DEFAULT_METRICS_CONFIG
|
||||
console.print()
|
||||
console.print(Rule("Complete Analysis Report", style="bold green"))
|
||||
|
||||
# I. Analyst Team Reports
|
||||
analysts = []
|
||||
if final_state.get("market_report"):
|
||||
if final_state.get("market_report") and is_metric_enabled(mc, "analysts", "market_report"):
|
||||
analysts.append(("Market Analyst", final_state["market_report"]))
|
||||
if final_state.get("sentiment_report"):
|
||||
if final_state.get("sentiment_report") and is_metric_enabled(mc, "analysts", "sentiment_report"):
|
||||
analysts.append(("Social Analyst", final_state["sentiment_report"]))
|
||||
if final_state.get("news_report"):
|
||||
if final_state.get("news_report") and is_metric_enabled(mc, "analysts", "news_report"):
|
||||
analysts.append(("News Analyst", final_state["news_report"]))
|
||||
if final_state.get("fundamentals_report"):
|
||||
if final_state.get("fundamentals_report") and is_metric_enabled(mc, "analysts", "fundamentals_report"):
|
||||
analysts.append(("Fundamentals Analyst", final_state["fundamentals_report"]))
|
||||
if analysts:
|
||||
console.print(Panel("[bold]I. Analyst Team Reports[/bold]", border_style="cyan"))
|
||||
|
|
@ -750,11 +758,11 @@ def display_complete_report(final_state):
|
|||
if final_state.get("investment_debate_state"):
|
||||
debate = final_state["investment_debate_state"]
|
||||
research = []
|
||||
if debate.get("bull_history"):
|
||||
if debate.get("bull_history") and is_metric_enabled(mc, "research", "bull_history"):
|
||||
research.append(("Bull Researcher", debate["bull_history"]))
|
||||
if debate.get("bear_history"):
|
||||
if debate.get("bear_history") and is_metric_enabled(mc, "research", "bear_history"):
|
||||
research.append(("Bear Researcher", debate["bear_history"]))
|
||||
if debate.get("judge_decision"):
|
||||
if debate.get("judge_decision") and is_metric_enabled(mc, "research", "judge_decision"):
|
||||
research.append(("Research Manager", debate["judge_decision"]))
|
||||
if research:
|
||||
console.print(Panel("[bold]II. Research Team Decision[/bold]", border_style="magenta"))
|
||||
|
|
@ -762,7 +770,7 @@ def display_complete_report(final_state):
|
|||
console.print(Panel(Markdown(content), title=title, border_style="blue", padding=(1, 2)))
|
||||
|
||||
# III. Trading Team
|
||||
if final_state.get("trader_investment_plan"):
|
||||
if final_state.get("trader_investment_plan") and is_metric_enabled(mc, "trading", "trader_investment_plan"):
|
||||
console.print(Panel("[bold]III. Trading Team Plan[/bold]", border_style="yellow"))
|
||||
console.print(Panel(Markdown(final_state["trader_investment_plan"]), title="Trader", border_style="blue", padding=(1, 2)))
|
||||
|
||||
|
|
@ -770,11 +778,11 @@ def display_complete_report(final_state):
|
|||
if final_state.get("risk_debate_state"):
|
||||
risk = final_state["risk_debate_state"]
|
||||
risk_reports = []
|
||||
if risk.get("aggressive_history"):
|
||||
if risk.get("aggressive_history") and is_metric_enabled(mc, "risk", "aggressive_history"):
|
||||
risk_reports.append(("Aggressive Analyst", risk["aggressive_history"]))
|
||||
if risk.get("conservative_history"):
|
||||
if risk.get("conservative_history") and is_metric_enabled(mc, "risk", "conservative_history"):
|
||||
risk_reports.append(("Conservative Analyst", risk["conservative_history"]))
|
||||
if risk.get("neutral_history"):
|
||||
if risk.get("neutral_history") and is_metric_enabled(mc, "risk", "neutral_history"):
|
||||
risk_reports.append(("Neutral Analyst", risk["neutral_history"]))
|
||||
if risk_reports:
|
||||
console.print(Panel("[bold]IV. Risk Management Team Decision[/bold]", border_style="red"))
|
||||
|
|
@ -782,7 +790,7 @@ def display_complete_report(final_state):
|
|||
console.print(Panel(Markdown(content), title=title, border_style="blue", padding=(1, 2)))
|
||||
|
||||
# V. Portfolio Manager Decision
|
||||
if risk.get("judge_decision"):
|
||||
if risk.get("judge_decision") and is_metric_enabled(mc, "portfolio", "final_trade_decision"):
|
||||
console.print(Panel("[bold]V. Portfolio Manager Decision[/bold]", border_style="green"))
|
||||
console.print(Panel(Markdown(risk["judge_decision"]), title="Portfolio Manager", border_style="blue", padding=(1, 2)))
|
||||
|
||||
|
|
@ -926,7 +934,7 @@ def format_tool_args(args, max_length=80) -> str:
|
|||
return result[:max_length - 3] + "..."
|
||||
return result
|
||||
|
||||
def run_analysis():
|
||||
def run_analysis(metrics_config: MetricsConfig | None = None):
|
||||
# First get all user selections
|
||||
selections = get_user_selections()
|
||||
|
||||
|
|
@ -944,6 +952,10 @@ def run_analysis():
|
|||
config["anthropic_effort"] = selections.get("anthropic_effort")
|
||||
config["output_language"] = selections.get("output_language", "English")
|
||||
|
||||
# Apply --metrics override if provided
|
||||
if metrics_config is not None:
|
||||
config["metrics_config"] = metrics_config
|
||||
|
||||
# Create stats callback handler for tracking LLM/tool calls
|
||||
stats_handler = StatsCallbackHandler()
|
||||
|
||||
|
|
@ -960,7 +972,7 @@ def run_analysis():
|
|||
)
|
||||
|
||||
# Initialize message buffer with selected analysts
|
||||
message_buffer.init_for_analysis(selected_analyst_keys)
|
||||
message_buffer.init_for_analysis(selected_analyst_keys, config.get("metrics_config"))
|
||||
|
||||
# Track start time for elapsed display
|
||||
start_time = time.time()
|
||||
|
|
@ -1184,7 +1196,7 @@ def run_analysis():
|
|||
).strip()
|
||||
save_path = Path(save_path_str)
|
||||
try:
|
||||
report_file = save_report_to_disk(final_state, selections["ticker"], save_path)
|
||||
report_file = save_report_to_disk(final_state, selections["ticker"], save_path, config.get("metrics_config"))
|
||||
console.print(f"\n[green]✓ Report saved to:[/green] {save_path.resolve()}")
|
||||
console.print(f" [dim]Complete report:[/dim] {report_file.name}")
|
||||
except Exception as e:
|
||||
|
|
@ -1193,12 +1205,39 @@ def run_analysis():
|
|||
# Prompt to display full report
|
||||
display_choice = typer.prompt("\nDisplay full report on screen?", default="Y").strip().upper()
|
||||
if display_choice in ("Y", "YES", ""):
|
||||
display_complete_report(final_state)
|
||||
display_complete_report(final_state, config.get("metrics_config"))
|
||||
|
||||
|
||||
@app.command()
|
||||
def analyze():
|
||||
run_analysis()
|
||||
def analyze(
|
||||
metrics: Optional[str] = typer.Option(
|
||||
None,
|
||||
"--metrics",
|
||||
help="Comma-separated list of section.metric keys to include in reports. "
|
||||
"Omitted metrics are hidden. Example: 'analysts.market_report,risk.aggressive_history'. "
|
||||
"Use --list-metrics to see all available keys.",
|
||||
),
|
||||
list_metrics: bool = typer.Option(
|
||||
False,
|
||||
"--list-metrics",
|
||||
help="List all available metric keys and exit.",
|
||||
),
|
||||
):
|
||||
if list_metrics:
|
||||
console.print("[bold]Available metrics:[/bold]")
|
||||
for key in list_all_metrics():
|
||||
console.print(f" {key}")
|
||||
raise typer.Exit()
|
||||
|
||||
metrics_config = None
|
||||
if metrics:
|
||||
try:
|
||||
metrics_config = parse_metrics_flag(metrics)
|
||||
except ValueError as e:
|
||||
console.print(f"[red]Error: {e}[/red]")
|
||||
raise typer.Exit(code=1)
|
||||
|
||||
run_analysis(metrics_config=metrics_config)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import os
|
||||
|
||||
from tradingagents.metrics_config import DEFAULT_METRICS_CONFIG
|
||||
|
||||
_TRADINGAGENTS_HOME = os.path.join(os.path.expanduser("~"), ".tradingagents")
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
|
|
@ -34,4 +36,6 @@ DEFAULT_CONFIG = {
|
|||
"tool_vendors": {
|
||||
# Example: "get_stock_data": "alpha_vantage", # Override category default
|
||||
},
|
||||
# Metrics configuration — controls which metrics appear in reports
|
||||
"metrics_config": DEFAULT_METRICS_CONFIG,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,118 @@
|
|||
"""Schema for configurable report metrics.
|
||||
|
||||
Each key maps to a report section/metric that can be toggled on or off.
|
||||
When a metric is False, it is omitted from the generated report output.
|
||||
"""
|
||||
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
|
||||
class AnalystMetrics(TypedDict, total=False):
|
||||
market_report: bool
|
||||
sentiment_report: bool
|
||||
news_report: bool
|
||||
fundamentals_report: bool
|
||||
|
||||
|
||||
class ResearchMetrics(TypedDict, total=False):
|
||||
bull_history: bool
|
||||
bear_history: bool
|
||||
judge_decision: bool
|
||||
|
||||
|
||||
class TradingMetrics(TypedDict, total=False):
|
||||
trader_investment_plan: bool
|
||||
|
||||
|
||||
class RiskMetrics(TypedDict, total=False):
|
||||
aggressive_history: bool
|
||||
conservative_history: bool
|
||||
neutral_history: bool
|
||||
|
||||
|
||||
class PortfolioMetrics(TypedDict, total=False):
|
||||
final_trade_decision: bool
|
||||
|
||||
|
||||
class MetricsConfig(TypedDict, total=False):
|
||||
analysts: AnalystMetrics
|
||||
research: ResearchMetrics
|
||||
trading: TradingMetrics
|
||||
risk: RiskMetrics
|
||||
portfolio: PortfolioMetrics
|
||||
|
||||
|
||||
# All metrics enabled by default
|
||||
DEFAULT_METRICS_CONFIG: MetricsConfig = {
|
||||
"analysts": {
|
||||
"market_report": True,
|
||||
"sentiment_report": True,
|
||||
"news_report": True,
|
||||
"fundamentals_report": True,
|
||||
},
|
||||
"research": {
|
||||
"bull_history": True,
|
||||
"bear_history": True,
|
||||
"judge_decision": True,
|
||||
},
|
||||
"trading": {
|
||||
"trader_investment_plan": True,
|
||||
},
|
||||
"risk": {
|
||||
"aggressive_history": True,
|
||||
"conservative_history": True,
|
||||
"neutral_history": True,
|
||||
},
|
||||
"portfolio": {
|
||||
"final_trade_decision": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def is_metric_enabled(config: MetricsConfig, section: str, metric: str) -> bool:
|
||||
"""Check if a specific metric is enabled in the config.
|
||||
|
||||
Missing sections or metrics default to True (enabled).
|
||||
"""
|
||||
section_cfg = config.get(section, {})
|
||||
return section_cfg.get(metric, True)
|
||||
|
||||
|
||||
def list_all_metrics() -> list[str]:
|
||||
"""Return all available metric keys as 'section.metric' strings."""
|
||||
return [
|
||||
f"{section}.{metric}"
|
||||
for section, metrics in DEFAULT_METRICS_CONFIG.items()
|
||||
for metric in metrics
|
||||
]
|
||||
|
||||
|
||||
def parse_metrics_flag(flag: str) -> MetricsConfig:
|
||||
"""Parse a --metrics flag value into a MetricsConfig.
|
||||
|
||||
Format: comma-separated 'section.metric' items.
|
||||
Only the listed metrics are enabled; all others are disabled.
|
||||
|
||||
Example: "analysts.market_report,risk.aggressive_history"
|
||||
|
||||
Raises ValueError for unknown metric keys.
|
||||
"""
|
||||
import copy
|
||||
|
||||
valid = set(list_all_metrics())
|
||||
enabled = set()
|
||||
for item in flag.split(","):
|
||||
item = item.strip()
|
||||
if not item:
|
||||
continue
|
||||
if item not in valid:
|
||||
raise ValueError(
|
||||
f"Unknown metric '{item}'. Available: {', '.join(sorted(valid))}"
|
||||
)
|
||||
enabled.add(item)
|
||||
|
||||
config: MetricsConfig = copy.deepcopy(DEFAULT_METRICS_CONFIG)
|
||||
for section, metrics in config.items():
|
||||
for metric in metrics:
|
||||
metrics[metric] = f"{section}.{metric}" in enabled
|
||||
return config
|
||||
Loading…
Reference in New Issue