c85c0068a2
- Replace all datetime.utcnow() with datetime.now(tz=timezone.utc) across 8 files - Fix 12 failing tests to match current implementation behavior - Fix pytest_plugins in non-top-level conftest (moved to root conftest.py) - Auto-fix 189 lint issues (import sorting, unused imports) - Add CI/CD pipeline infrastructure (ARC, ArgoCD, Kargo manifests) - Add values-beta.yaml and values-paper.yaml for staged deployments - Update GitHub Actions workflow to use self-hosted-gremlin runners - Add integration-test job to CI pipeline Result: 1596 passed, 0 failed, 0 warnings
351 lines
13 KiB
Python
351 lines
13 KiB
Python
"""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,
|
|
_build_rollup_disagreement,
|
|
_derive_rollup_direction,
|
|
rollup_trends,
|
|
)
|
|
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
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Macro rollup integration (Requirements 6.1, 6.2, 6.3)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
from services.aggregation.rollups import (
|
|
SECTOR_CONCENTRATION_THRESHOLD,
|
|
SectorMacroImpact,
|
|
compute_sector_macro_concentration,
|
|
)
|
|
|
|
|
|
def _make_sector_macro(
|
|
sector: str = "Technology",
|
|
total_impact: float = 1.0,
|
|
avg_impact: float = 0.5,
|
|
company_count: int = 2,
|
|
net_direction: float = -1.0,
|
|
event_ids: list[str] | None = None,
|
|
) -> SectorMacroImpact:
|
|
return SectorMacroImpact(
|
|
sector=sector,
|
|
total_impact=total_impact,
|
|
avg_impact=avg_impact,
|
|
company_count=company_count,
|
|
net_direction=net_direction,
|
|
event_ids=event_ids or ["evt-1"],
|
|
)
|
|
|
|
|
|
def test_rollup_no_macro_unchanged():
|
|
"""Without macro data, rollup output is identical to original behavior."""
|
|
trends = [_make_trend("AAPL", direction="bullish", strength=0.7, confidence=0.9)]
|
|
without_macro = rollup_trends(trends, "sector", "Technology", "7d", NOW)
|
|
with_none = rollup_trends(trends, "sector", "Technology", "7d", NOW, macro_impacts=None)
|
|
with_empty = rollup_trends(trends, "sector", "Technology", "7d", NOW, macro_impacts={})
|
|
assert without_macro.trend_strength == with_none.trend_strength
|
|
assert without_macro.trend_strength == with_empty.trend_strength
|
|
assert without_macro.confidence == with_none.confidence
|
|
assert without_macro.confidence == with_empty.confidence
|
|
|
|
|
|
def test_sector_rollup_with_macro_adjusts_strength():
|
|
"""Sector rollup with macro data should adjust strength."""
|
|
trends = [
|
|
_make_trend("AAPL", sector="Technology", direction="bullish", strength=0.5, confidence=0.8),
|
|
_make_trend("MSFT", sector="Technology", direction="bullish", strength=0.4, confidence=0.7),
|
|
]
|
|
macro = {"Technology": _make_sector_macro("Technology", total_impact=2.0, avg_impact=0.6, company_count=2)}
|
|
|
|
without = rollup_trends(trends, "sector", "Technology", "7d", NOW)
|
|
with_macro = rollup_trends(trends, "sector", "Technology", "7d", NOW, macro_impacts=macro)
|
|
|
|
# Macro should increase strength
|
|
assert with_macro.trend_strength >= without.trend_strength
|
|
|
|
|
|
def test_sector_rollup_macro_no_match_unchanged():
|
|
"""Sector rollup with macro data for a different sector is unchanged."""
|
|
trends = [_make_trend("AAPL", sector="Technology", direction="bullish", strength=0.5, confidence=0.8)]
|
|
macro = {"Financials": _make_sector_macro("Financials")}
|
|
|
|
without = rollup_trends(trends, "sector", "Technology", "7d", NOW)
|
|
with_macro = rollup_trends(trends, "sector", "Technology", "7d", NOW, macro_impacts=macro)
|
|
|
|
assert without.trend_strength == with_macro.trend_strength
|
|
assert without.confidence == with_macro.confidence
|
|
|
|
|
|
def test_market_rollup_with_macro_adjusts():
|
|
"""Market rollup with macro data should adjust strength and confidence."""
|
|
trends = [
|
|
_make_trend("AAPL", sector="Technology", direction="bullish", strength=0.5, confidence=0.8),
|
|
_make_trend("JPM", sector="Financials", direction="bearish", strength=0.4, confidence=0.7),
|
|
]
|
|
macro = {
|
|
"Technology": _make_sector_macro("Technology", total_impact=1.5, avg_impact=0.5, company_count=1),
|
|
"Financials": _make_sector_macro("Financials", total_impact=0.5, avg_impact=0.3, company_count=1),
|
|
}
|
|
|
|
without = rollup_trends(trends, "market", "all", "7d", NOW)
|
|
with_macro = rollup_trends(trends, "market", "all", "7d", NOW, macro_impacts=macro)
|
|
|
|
# With macro data, at least one of strength or confidence should differ
|
|
differs = (
|
|
with_macro.trend_strength != without.trend_strength
|
|
or with_macro.confidence != without.confidence
|
|
)
|
|
assert differs
|
|
|
|
|
|
def test_market_rollup_disproportionate_sector_surfaced():
|
|
"""When one sector has >60% of macro impact, it appears in risks or catalysts."""
|
|
trends = [
|
|
_make_trend("AAPL", sector="Technology", direction="bullish", strength=0.5, confidence=0.8),
|
|
_make_trend("JPM", sector="Financials", direction="bullish", strength=0.4, confidence=0.7),
|
|
]
|
|
# Technology has 90% of total macro impact
|
|
macro = {
|
|
"Technology": _make_sector_macro("Technology", total_impact=9.0, avg_impact=0.9, company_count=1, net_direction=-1.0),
|
|
"Financials": _make_sector_macro("Financials", total_impact=1.0, avg_impact=0.1, company_count=1, net_direction=0.5),
|
|
}
|
|
|
|
summary = rollup_trends(trends, "market", "all", "7d", NOW, macro_impacts=macro)
|
|
|
|
# Technology should appear in material_risks (negative direction) or dominant_catalysts
|
|
all_labels = summary.material_risks + summary.dominant_catalysts
|
|
tech_found = any("Technology" in label for label in all_labels)
|
|
assert tech_found, f"Expected Technology in risks/catalysts, got: {all_labels}"
|
|
|
|
|
|
def test_market_rollup_no_disproportionate_sector():
|
|
"""When no sector has >60% of macro impact, no macro labels are surfaced."""
|
|
trends = [
|
|
_make_trend("AAPL", sector="Technology", direction="bullish", strength=0.5, confidence=0.8),
|
|
_make_trend("JPM", sector="Financials", direction="bullish", strength=0.4, confidence=0.7),
|
|
]
|
|
# Even split: 50/50
|
|
macro = {
|
|
"Technology": _make_sector_macro("Technology", total_impact=5.0, avg_impact=0.5, company_count=1),
|
|
"Financials": _make_sector_macro("Financials", total_impact=5.0, avg_impact=0.5, company_count=1),
|
|
}
|
|
|
|
summary = rollup_trends(trends, "market", "all", "7d", NOW, macro_impacts=macro)
|
|
|
|
all_labels = summary.material_risks + summary.dominant_catalysts
|
|
macro_labels = [l for l in all_labels if l.startswith("Macro:")]
|
|
assert len(macro_labels) == 0
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# compute_sector_macro_concentration
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_concentration_empty():
|
|
assert compute_sector_macro_concentration({}) == []
|
|
|
|
|
|
def test_concentration_single_sector():
|
|
impacts = {"Technology": _make_sector_macro("Technology", total_impact=5.0)}
|
|
result = compute_sector_macro_concentration(impacts)
|
|
assert len(result) == 1
|
|
assert result[0] == ("Technology", 1.0)
|
|
|
|
|
|
def test_concentration_multiple_sectors():
|
|
impacts = {
|
|
"Technology": _make_sector_macro("Technology", total_impact=7.0),
|
|
"Financials": _make_sector_macro("Financials", total_impact=3.0),
|
|
}
|
|
result = compute_sector_macro_concentration(impacts)
|
|
assert result[0][0] == "Technology"
|
|
assert abs(result[0][1] - 0.7) < 0.01
|
|
assert result[1][0] == "Financials"
|
|
assert abs(result[1][1] - 0.3) < 0.01
|
|
|
|
|
|
def test_concentration_threshold_boundary():
|
|
"""Exactly at 60% should not be considered disproportionate (>60% required)."""
|
|
impacts = {
|
|
"Technology": _make_sector_macro("Technology", total_impact=6.0),
|
|
"Financials": _make_sector_macro("Financials", total_impact=4.0),
|
|
}
|
|
result = compute_sector_macro_concentration(impacts)
|
|
# 60% is exactly at threshold, not above it
|
|
assert result[0][1] <= SECTOR_CONCENTRATION_THRESHOLD
|
|
|
|
|
|
def test_concentration_above_threshold():
|
|
impacts = {
|
|
"Technology": _make_sector_macro("Technology", total_impact=7.0),
|
|
"Financials": _make_sector_macro("Financials", total_impact=3.0),
|
|
}
|
|
result = compute_sector_macro_concentration(impacts)
|
|
assert result[0][1] > SECTOR_CONCENTRATION_THRESHOLD
|