feat: configurable metrics in report output (#545)

Add MetricsConfig schema for customizing which columns appear in report tables.
Users can enable/disable metrics per section via config or --metrics CLI flag.

Closes #545
This commit is contained in:
Clayton Brown 2026-04-20 19:01:34 +10:00
parent fa4d01c23a
commit 9db0a0a040
3 changed files with 201 additions and 40 deletions

View File

@ -26,6 +26,7 @@ from rich.rule import Rule
from tradingagents.graph.trading_graph import TradingAgentsGraph from tradingagents.graph.trading_graph import TradingAgentsGraph
from tradingagents.default_config import DEFAULT_CONFIG 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.models import AnalystType
from cli.utils import * from cli.utils import *
from cli.announcements import fetch_announcements, display_announcements from cli.announcements import fetch_announcements, display_announcements
@ -81,14 +82,17 @@ class MessageBuffer:
self.report_sections = {} self.report_sections = {}
self.selected_analysts = [] self.selected_analysts = []
self._processed_message_ids = set() 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. """Initialize agent status and report sections based on selected analysts.
Args: Args:
selected_analysts: List of analyst type strings (e.g., ["market", "news"]) 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.selected_analysts = [a.lower() for a in selected_analysts]
self.metrics_config = metrics_config or DEFAULT_METRICS_CONFIG
# Build agent_status dynamically # Build agent_status dynamically
self.agent_status = {} self.agent_status = {}
@ -187,24 +191,26 @@ class MessageBuffer:
def _update_final_report(self): def _update_final_report(self):
report_parts = [] report_parts = []
mc = self.metrics_config
# Analyst Team Reports - use .get() to handle missing sections # Analyst Team Reports - use .get() to handle missing sections
analyst_sections = ["market_report", "sentiment_report", "news_report", "fundamentals_report"] 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") 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( report_parts.append(
f"### Market Analysis\n{self.report_sections['market_report']}" 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( report_parts.append(
f"### Social Sentiment\n{self.report_sections['sentiment_report']}" 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( report_parts.append(
f"### News Analysis\n{self.report_sections['news_report']}" 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( report_parts.append(
f"### Fundamentals Analysis\n{self.report_sections['fundamentals_report']}" f"### Fundamentals Analysis\n{self.report_sections['fundamentals_report']}"
) )
@ -215,12 +221,12 @@ class MessageBuffer:
report_parts.append(f"{self.report_sections['investment_plan']}") report_parts.append(f"{self.report_sections['investment_plan']}")
# Trading Team Reports # 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("## Trading Team Plan")
report_parts.append(f"{self.report_sections['trader_investment_plan']}") report_parts.append(f"{self.report_sections['trader_investment_plan']}")
# Portfolio Management Decision # 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("## Portfolio Management Decision")
report_parts.append(f"{self.report_sections['final_trade_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.""" """Save complete analysis report to disk with organized subfolders."""
mc = metrics_config or DEFAULT_METRICS_CONFIG
save_path.mkdir(parents=True, exist_ok=True) save_path.mkdir(parents=True, exist_ok=True)
sections = [] sections = []
# 1. Analysts # 1. Analysts
analysts_dir = save_path / "1_analysts" analysts_dir = save_path / "1_analysts"
analyst_parts = [] 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.mkdir(exist_ok=True)
(analysts_dir / "market.md").write_text(final_state["market_report"]) (analysts_dir / "market.md").write_text(final_state["market_report"])
analyst_parts.append(("Market Analyst", 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.mkdir(exist_ok=True)
(analysts_dir / "sentiment.md").write_text(final_state["sentiment_report"]) (analysts_dir / "sentiment.md").write_text(final_state["sentiment_report"])
analyst_parts.append(("Social Analyst", 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.mkdir(exist_ok=True)
(analysts_dir / "news.md").write_text(final_state["news_report"]) (analysts_dir / "news.md").write_text(final_state["news_report"])
analyst_parts.append(("News Analyst", 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.mkdir(exist_ok=True)
(analysts_dir / "fundamentals.md").write_text(final_state["fundamentals_report"]) (analysts_dir / "fundamentals.md").write_text(final_state["fundamentals_report"])
analyst_parts.append(("Fundamentals Analyst", 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" research_dir = save_path / "2_research"
debate = final_state["investment_debate_state"] debate = final_state["investment_debate_state"]
research_parts = [] 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.mkdir(exist_ok=True)
(research_dir / "bull.md").write_text(debate["bull_history"]) (research_dir / "bull.md").write_text(debate["bull_history"])
research_parts.append(("Bull Researcher", 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.mkdir(exist_ok=True)
(research_dir / "bear.md").write_text(debate["bear_history"]) (research_dir / "bear.md").write_text(debate["bear_history"])
research_parts.append(("Bear Researcher", 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.mkdir(exist_ok=True)
(research_dir / "manager.md").write_text(debate["judge_decision"]) (research_dir / "manager.md").write_text(debate["judge_decision"])
research_parts.append(("Research Manager", 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}") sections.append(f"## II. Research Team Decision\n\n{content}")
# 3. Trading # 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 = save_path / "3_trading"
trading_dir.mkdir(exist_ok=True) trading_dir.mkdir(exist_ok=True)
(trading_dir / "trader.md").write_text(final_state["trader_investment_plan"]) (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_dir = save_path / "4_risk"
risk = final_state["risk_debate_state"] risk = final_state["risk_debate_state"]
risk_parts = [] 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.mkdir(exist_ok=True)
(risk_dir / "aggressive.md").write_text(risk["aggressive_history"]) (risk_dir / "aggressive.md").write_text(risk["aggressive_history"])
risk_parts.append(("Aggressive Analyst", 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.mkdir(exist_ok=True)
(risk_dir / "conservative.md").write_text(risk["conservative_history"]) (risk_dir / "conservative.md").write_text(risk["conservative_history"])
risk_parts.append(("Conservative Analyst", 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.mkdir(exist_ok=True)
(risk_dir / "neutral.md").write_text(risk["neutral_history"]) (risk_dir / "neutral.md").write_text(risk["neutral_history"])
risk_parts.append(("Neutral Analyst", 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}") sections.append(f"## IV. Risk Management Team Decision\n\n{content}")
# 5. Portfolio Manager # 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 = save_path / "5_portfolio"
portfolio_dir.mkdir(exist_ok=True) portfolio_dir.mkdir(exist_ok=True)
(portfolio_dir / "decision.md").write_text(risk["judge_decision"]) (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" 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).""" """Display the complete analysis report sequentially (avoids truncation)."""
mc = metrics_config or DEFAULT_METRICS_CONFIG
console.print() console.print()
console.print(Rule("Complete Analysis Report", style="bold green")) console.print(Rule("Complete Analysis Report", style="bold green"))
# I. Analyst Team Reports # I. Analyst Team Reports
analysts = [] 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"])) 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"])) 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"])) 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"])) analysts.append(("Fundamentals Analyst", final_state["fundamentals_report"]))
if analysts: if analysts:
console.print(Panel("[bold]I. Analyst Team Reports[/bold]", border_style="cyan")) 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"): if final_state.get("investment_debate_state"):
debate = final_state["investment_debate_state"] debate = final_state["investment_debate_state"]
research = [] 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"])) 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"])) 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"])) research.append(("Research Manager", debate["judge_decision"]))
if research: if research:
console.print(Panel("[bold]II. Research Team Decision[/bold]", border_style="magenta")) 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))) console.print(Panel(Markdown(content), title=title, border_style="blue", padding=(1, 2)))
# III. Trading Team # 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("[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))) 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"): if final_state.get("risk_debate_state"):
risk = final_state["risk_debate_state"] risk = final_state["risk_debate_state"]
risk_reports = [] 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"])) 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"])) 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"])) risk_reports.append(("Neutral Analyst", risk["neutral_history"]))
if risk_reports: if risk_reports:
console.print(Panel("[bold]IV. Risk Management Team Decision[/bold]", border_style="red")) 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))) console.print(Panel(Markdown(content), title=title, border_style="blue", padding=(1, 2)))
# V. Portfolio Manager Decision # 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("[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))) 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[:max_length - 3] + "..."
return result return result
def run_analysis(): def run_analysis(metrics_config: MetricsConfig | None = None):
# First get all user selections # First get all user selections
selections = get_user_selections() selections = get_user_selections()
@ -944,6 +952,10 @@ def run_analysis():
config["anthropic_effort"] = selections.get("anthropic_effort") config["anthropic_effort"] = selections.get("anthropic_effort")
config["output_language"] = selections.get("output_language", "English") 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 # Create stats callback handler for tracking LLM/tool calls
stats_handler = StatsCallbackHandler() stats_handler = StatsCallbackHandler()
@ -960,7 +972,7 @@ def run_analysis():
) )
# Initialize message buffer with selected analysts # 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 # Track start time for elapsed display
start_time = time.time() start_time = time.time()
@ -1184,7 +1196,7 @@ def run_analysis():
).strip() ).strip()
save_path = Path(save_path_str) save_path = Path(save_path_str)
try: 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"\n[green]✓ Report saved to:[/green] {save_path.resolve()}")
console.print(f" [dim]Complete report:[/dim] {report_file.name}") console.print(f" [dim]Complete report:[/dim] {report_file.name}")
except Exception as e: except Exception as e:
@ -1193,12 +1205,39 @@ def run_analysis():
# Prompt to display full report # Prompt to display full report
display_choice = typer.prompt("\nDisplay full report on screen?", default="Y").strip().upper() display_choice = typer.prompt("\nDisplay full report on screen?", default="Y").strip().upper()
if display_choice in ("Y", "YES", ""): if display_choice in ("Y", "YES", ""):
display_complete_report(final_state) display_complete_report(final_state, config.get("metrics_config"))
@app.command() @app.command()
def analyze(): def analyze(
run_analysis() 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__": if __name__ == "__main__":

View File

@ -1,5 +1,7 @@
import os import os
from tradingagents.metrics_config import DEFAULT_METRICS_CONFIG
_TRADINGAGENTS_HOME = os.path.join(os.path.expanduser("~"), ".tradingagents") _TRADINGAGENTS_HOME = os.path.join(os.path.expanduser("~"), ".tradingagents")
DEFAULT_CONFIG = { DEFAULT_CONFIG = {
@ -34,4 +36,6 @@ DEFAULT_CONFIG = {
"tool_vendors": { "tool_vendors": {
# Example: "get_stock_data": "alpha_vantage", # Override category default # Example: "get_stock_data": "alpha_vantage", # Override category default
}, },
# Metrics configuration — controls which metrics appear in reports
"metrics_config": DEFAULT_METRICS_CONFIG,
} }

View File

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