feat(028-strategy-signals-contrib): add scorecard.py — aggregate strategy consensus

This commit is contained in:
Clayton Brown 2026-04-21 08:43:34 +10:00
parent 99815917c7
commit a83f1bb7f2
2 changed files with 74 additions and 0 deletions

View File

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

View File

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