diff --git a/tradingagents/strategies/__init__.py b/tradingagents/strategies/__init__.py index 6bfe891b..db448cc8 100644 --- a/tradingagents/strategies/__init__.py +++ b/tradingagents/strategies/__init__.py @@ -10,12 +10,16 @@ Based on: from .base import BaseStrategy, Role, StrategySignal from .registry import compute_signals, format_signals_for_role, get_registry, reset_registry +from .scorecard import Scorecard, build_scorecard, format_scorecard __all__ = [ "BaseStrategy", "Role", + "Scorecard", "StrategySignal", + "build_scorecard", "compute_signals", + "format_scorecard", "format_signals_for_role", "get_registry", "reset_registry", diff --git a/tradingagents/strategies/scorecard.py b/tradingagents/strategies/scorecard.py new file mode 100644 index 00000000..5fde2031 --- /dev/null +++ b/tradingagents/strategies/scorecard.py @@ -0,0 +1,70 @@ +"""Scorecard — aggregate strategy consensus from computed signals.""" + +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypedDict + +from .base import StrategySignal + + +class Scorecard(TypedDict): + """Aggregated consensus across all strategy signals.""" + + ticker: str + date: str + bullish: int + bearish: int + neutral: int + total: int + overall: Literal["bullish", "bearish", "neutral"] + avg_strength: float # mean signal_strength across all signals + + +def build_scorecard(signals: list[StrategySignal]) -> Scorecard | None: + """Build a consensus scorecard from a list of signals. + + Returns ``None`` when *signals* is empty. + """ + if not signals: + return None + + counts = {"bullish": 0, "bearish": 0, "neutral": 0} + for s in signals: + counts[s["direction"]] += 1 + + total = len(signals) + avg = sum(s["signal_strength"] for s in signals) / total + + # Overall direction: majority wins; tie-break by avg_strength sign + if counts["bullish"] > counts["bearish"]: + overall: Literal["bullish", "bearish", "neutral"] = "bullish" + elif counts["bearish"] > counts["bullish"]: + overall = "bearish" + else: + overall = "bullish" if avg > 0 else "bearish" if avg < 0 else "neutral" + + return Scorecard( + ticker=signals[0]["ticker"], + date=signals[0]["date"], + bullish=counts["bullish"], + bearish=counts["bearish"], + neutral=counts["neutral"], + total=total, + overall=overall, + avg_strength=round(avg, 4), + ) + + +def format_scorecard(sc: Scorecard | None) -> str: + """Format a scorecard as a prompt-ready string. Empty string if None.""" + if sc is None: + return "" + return ( + f"## Strategy Consensus Scorecard\n" + f"- Ticker: {sc['ticker']} | Date: {sc['date']}\n" + f"- Bullish: {sc['bullish']} | Bearish: {sc['bearish']} | Neutral: {sc['neutral']} (total: {sc['total']})\n" + f"- Avg signal strength: {sc['avg_strength']:+.4f}\n" + f"- Overall direction: **{sc['overall']}**" + )