"""Tests for sector and market rollup aggregation. Tests the pure rollup logic (no DB required). Requirements: 6.3, 6.4, 6.5 """ from datetime import datetime, timezone from services.aggregation.rollups import ( CompanyTrendRow, rollup_trends, _build_rollup_disagreement, _derive_rollup_direction, ) from services.shared.schemas import TrendDirection, TrendWindow NOW = datetime(2026, 4, 11, 12, 0, 0, tzinfo=timezone.utc) def _make_trend( ticker: str = "AAPL", sector: str = "Technology", window: str = "7d", direction: str = "bullish", strength: float = 0.6, confidence: float = 0.8, contradiction: float = 0.1, catalysts: list[str] | None = None, risks: list[str] | None = None, supporting: list[str] | None = None, opposing: list[str] | None = None, ) -> CompanyTrendRow: return CompanyTrendRow( entity_id=ticker, sector=sector, window=window, trend_direction=direction, trend_strength=strength, confidence=confidence, contradiction_score=contradiction, dominant_catalysts=catalysts or ["earnings"], material_risks=risks or [], top_supporting_evidence=supporting or ["doc-1"], top_opposing_evidence=opposing or [], ) # --------------------------------------------------------------------------- # rollup_trends # --------------------------------------------------------------------------- def test_rollup_empty(): summary = rollup_trends([], "sector", "Technology", "7d", NOW) assert summary.entity_type == "sector" assert summary.entity_id == "Technology" assert summary.trend_direction == TrendDirection.NEUTRAL assert summary.trend_strength == 0.0 assert summary.confidence == 0.0 def test_rollup_single_bullish(): trends = [_make_trend("AAPL", direction="bullish", strength=0.7, confidence=0.9)] summary = rollup_trends(trends, "sector", "Technology", "7d", NOW) assert summary.trend_direction == TrendDirection.BULLISH assert summary.trend_strength > 0 assert summary.confidence > 0 assert summary.window == TrendWindow.SEVEN_DAY def test_rollup_mixed_directions(): trends = [ _make_trend("AAPL", direction="bullish", strength=0.6, confidence=0.8), _make_trend("MSFT", direction="bearish", strength=0.6, confidence=0.8), ] summary = rollup_trends(trends, "sector", "Technology", "7d", NOW) # Equal and opposite → neutral or mixed assert summary.trend_direction in (TrendDirection.NEUTRAL, TrendDirection.MIXED) def test_rollup_confidence_weighted(): """Higher-confidence company should dominate the rollup direction.""" trends = [ _make_trend("AAPL", direction="bullish", strength=0.8, confidence=0.95), _make_trend("MSFT", direction="bearish", strength=0.3, confidence=0.2), ] summary = rollup_trends(trends, "sector", "Technology", "7d", NOW) assert summary.trend_direction == TrendDirection.BULLISH def test_rollup_catalysts_aggregated(): trends = [ _make_trend("AAPL", catalysts=["earnings", "product"], confidence=0.8), _make_trend("MSFT", catalysts=["product", "macro"], confidence=0.6), ] summary = rollup_trends(trends, "sector", "Technology", "7d", NOW) # "product" appears in both → should be top catalyst assert "product" in summary.dominant_catalysts def test_rollup_risks_deduplicated(): trends = [ _make_trend("AAPL", risks=["regulatory risk", "supply chain"], confidence=0.8), _make_trend("MSFT", risks=["Regulatory Risk", "tariffs"], confidence=0.6), ] summary = rollup_trends(trends, "sector", "Technology", "7d", NOW) risk_lower = [r.lower() for r in summary.material_risks] assert risk_lower.count("regulatory risk") == 1 def test_rollup_evidence_collected(): trends = [ _make_trend("AAPL", supporting=["doc-1", "doc-2"], opposing=["doc-3"]), _make_trend("MSFT", supporting=["doc-4"], opposing=["doc-5"]), ] summary = rollup_trends(trends, "market", "all", "7d", NOW) assert "doc-1" in summary.top_supporting_evidence assert "doc-4" in summary.top_supporting_evidence assert "doc-3" in summary.top_opposing_evidence def test_rollup_market_entity_type(): trends = [_make_trend("AAPL"), _make_trend("JPM", sector="Financials")] summary = rollup_trends(trends, "market", "all", "7d", NOW) assert summary.entity_type == "market" assert summary.entity_id == "all" # --------------------------------------------------------------------------- # _derive_rollup_direction # --------------------------------------------------------------------------- def test_derive_direction_bullish(): assert _derive_rollup_direction(0.5, 0.0) == TrendDirection.BULLISH def test_derive_direction_bearish(): assert _derive_rollup_direction(-0.5, 0.0) == TrendDirection.BEARISH def test_derive_direction_neutral(): assert _derive_rollup_direction(0.05, 0.0) == TrendDirection.NEUTRAL def test_derive_direction_mixed_high_contradiction(): assert _derive_rollup_direction(0.1, 0.2) == TrendDirection.MIXED # --------------------------------------------------------------------------- # _build_rollup_disagreement # --------------------------------------------------------------------------- def test_disagreement_no_conflict(): trends = [ _make_trend("AAPL", direction="bullish"), _make_trend("MSFT", direction="bullish"), ] details = _build_rollup_disagreement(trends, "Technology") assert details == [] def test_disagreement_with_conflict(): trends = [ _make_trend("AAPL", direction="bullish", confidence=0.8), _make_trend("MSFT", direction="bearish", confidence=0.7), ] details = _build_rollup_disagreement(trends, "Technology") assert len(details) == 1 assert details[0].dimension == "company_direction" assert "AAPL" in details[0].positive_doc_ids assert "MSFT" in details[0].negative_doc_ids