feat: competitive intelligence & historical pattern matching layer

This commit is contained in:
Celes Renata
2026-04-14 19:42:48 +00:00
parent b478022ba3
commit f7a11d14ea
203 changed files with 20155 additions and 97 deletions
+177
View File
@@ -171,3 +171,180 @@ def test_disagreement_with_conflict():
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 (
SectorMacroImpact,
compute_sector_macro_concentration,
SECTOR_CONCENTRATION_THRESHOLD,
)
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