← Files Stock & ETF Research PanelARCHIVED FILE

skills/screen-stocks-etfs/scripts/aggregate_scores.py

6.62 KB · Oct 2, 2026 · 00:30 UTC

↓ Download file

#!/usr/bin/env python3
"""Aggregate blind panel scores, debate revisions, and scenario sensitivity."""

import json
import math
import sys
from pathlib import Path

DEFAULT_WEIGHTS = {
    "portfolio_manager": 20,
    "global_macro_strategist": 15,
    "geopolitical_risk_analyst": 10,
    "market_strategist": 10,
    "fundamental_research_analyst": 25,
    "portfolio_construction_analyst": 20,
}

def load_payload(path: str) -> dict:
    if path == "-":
        return json.load(sys.stdin)
    with Path(path).open(encoding="utf-8") as handle:
        return json.load(handle)

def validate_weights(weights: dict) -> None:
    if abs(sum(float(value) for value in weights.values()) - 100) > 0.001:
        raise ValueError("Committee weights must total 100")

def merged_scores(candidate: dict, include_revisions: bool) -> dict:
    scores = dict(candidate.get("judge_scores", {}))
    if include_revisions:
        scores.update(candidate.get("post_debate_judge_scores", {}))
    return scores

def score_snapshot(candidate: dict, weights: dict, include_revisions: bool, require_complete_panel: bool) -> dict:
    observations = []
    scores = merged_scores(candidate, include_revisions)
    missing_members = [member for member in weights if member not in scores]
    if require_complete_panel and missing_members:
        names = ", ".join(missing_members)
        raise ValueError(f"Incomplete Full Panel for {candidate['ticker']}; missing: {names}")
    for member, weight in weights.items():
        result = scores.get(member)
        if not result or result.get("abstain"):
            continue
        score = float(result["score"])
        confidence = float(result.get("confidence", 0.5))
        if not 0 <= score <= 10 or not 0 <= confidence <= 1:
            raise ValueError(f"Invalid score/confidence for {candidate['ticker']}:{member}")
        effective_weight = float(weight) * confidence
        observations.append({"committee_member": member, "score": score, "effective_weight": effective_weight})
    if not observations:
        return {"status": "insufficient_data"}
    weight_sum = sum(item["effective_weight"] for item in observations)
    mean = sum(item["score"] * item["effective_weight"] for item in observations) / weight_sum
    variance = sum(item["effective_weight"] * (item["score"] - mean) ** 2 for item in observations) / weight_sum
    disagreement = math.sqrt(variance)
    missing = int(candidate.get("missing_critical_data", 0))
    hard_gate = candidate.get("hard_gate", "pass")
    disagreement_penalty = 0.35 * disagreement
    missing_data_penalty = 0.50 * missing
    gate_penalty = 0.0
    final = max(0.0, min(10.0, mean - disagreement_penalty - missing_data_penalty))
    if hard_gate == "fail":
        final = 0.0
    elif hard_gate == "caution":
        gate_penalty = 0.5
        final = max(0.0, final - gate_penalty)
    high = max(observations, key=lambda item: item["score"])
    low = min(observations, key=lambda item: item["score"])
    return {
        "status": "scored",
        "final_score": round(final, 2),
        "confidence_weighted_mean": round(mean, 2),
        "committee_disagreement": round(disagreement, 2),
        "disagreement_penalty": round(disagreement_penalty, 2),
        "missing_data_penalty": round(missing_data_penalty, 2),
        "gate_penalty": round(gate_penalty, 2),
        "score_equation": (
            f"{mean:.2f} - {disagreement_penalty:.2f} - {missing_data_penalty:.2f} - {gate_penalty:.2f} = {final:.2f}"
            if hard_gate != "fail" else f"hard gate failed; final score = {final:.2f}"
        ),
        "highest_member": high["committee_member"],
        "highest_score": round(high["score"], 2),
        "lowest_member": low["committee_member"],
        "lowest_score": round(low["score"], 2),
        "hard_gate": hard_gate,
        "missing_critical_data": missing,
        "members_scored": len(observations),
        "members_abstained": len(weights) - len(observations),
    }

def scenario_summary(candidate: dict) -> dict:
    scenarios = candidate.get("scenario_scores", {})
    if not scenarios:
        return {}
    values = []
    weighted_total = 0.0
    probability_total = 0.0
    for name, entry in scenarios.items():
        if isinstance(entry, dict):
            score = float(entry["score"])
            probability = entry.get("probability")
        else:
            score = float(entry)
            probability = None
        if not 0 <= score <= 10:
            raise ValueError(f"Invalid scenario score for {candidate['ticker']}:{name}")
        values.append(score)
        if probability is not None:
            probability = float(probability)
            if not 0 <= probability <= 1:
                raise ValueError(f"Invalid scenario probability for {candidate['ticker']}:{name}")
            weighted_total += score * probability
            probability_total += probability
    average = weighted_total / probability_total if probability_total > 0 else sum(values) / len(values)
    return {
        "scenario_average": round(average, 2),
        "scenario_low": round(min(values), 2),
        "scenario_high": round(max(values), 2),
        "scenario_sensitivity": round(max(values) - min(values), 2),
    }

def aggregate(candidate: dict, weights: dict, require_complete_panel: bool) -> dict:
    baseline = score_snapshot(candidate, weights, include_revisions=False, require_complete_panel=require_complete_panel)
    if baseline["status"] != "scored":
        return {"ticker": candidate["ticker"], "status": "insufficient_data"}
    has_revisions = bool(candidate.get("post_debate_judge_scores"))
    revised = score_snapshot(candidate, weights, include_revisions=True, require_complete_panel=require_complete_panel) if has_revisions else baseline
    result = {
        "ticker": candidate["ticker"],
        "status": "scored",
        **revised,
        "baseline_score": baseline["final_score"],
        "debate_delta": round(revised["final_score"] - baseline["final_score"], 2),
    }
    result.update(scenario_summary(candidate))
    return result

def main() -> None:
    if len(sys.argv) != 2:
        raise SystemExit("Usage: aggregate_scores.py <scores.json|->")
    payload = load_payload(sys.argv[1])
    weights = payload.get("weights", DEFAULT_WEIGHTS)
    validate_weights(weights)
    mode = payload.get("mode", "quick_scan")
    require_complete_panel = mode in {"full_panel", "panel_debate"}
    ranked = [aggregate(candidate, weights, require_complete_panel) for candidate in payload["candidates"]]
    ranked.sort(key=lambda item: item.get("final_score", -1), reverse=True)
    json.dump({"ranking": ranked}, sys.stdout, indent=2)
    sys.stdout.write("\n")

if __name__ == "__main__":
    main()

SHA-256: fd096fc828c67aa8a76df2917c6c6dd067be23efec397f5236aa630f775a1620