Files
stonks-oracle/tests/test_v3_layers.py
T

360 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Unit tests for v3 macro and competitive layers.
Tests the noisy-OR normalized macro exposure, resilience dampener,
macro LLR computation, shrunk correlation convergence, competitive LLR
clamping, and graph-distance attenuation.
Requirements validated: 9.19.5, 10.110.5
"""
from __future__ import annotations
import math
import pytest
from services.aggregation.interpolation import (
compute_macro_llr,
compute_normalized_macro_exposure,
)
from services.aggregation.signal_propagation import (
compute_competitive_llr,
compute_shrunk_correlation,
)
# ---------------------------------------------------------------------------
# Noisy-OR: compute_normalized_macro_exposure
# ---------------------------------------------------------------------------
class TestNoisyORExposure:
"""Tests for noisy-OR normalized macro exposure (Req 9.1, 9.2, 9.3)."""
def test_all_overlaps_max_regional(self):
"""All O_k = 1.0 with regional tier → E_macro = 1.0."""
overlaps = {"geo": 1.0, "supply": 1.0, "commodity": 1.0, "sector": 1.0}
result = compute_normalized_macro_exposure(overlaps, tier="regional")
assert result == pytest.approx(1.0, abs=1e-9)
def test_all_overlaps_zero(self):
"""All O_k = 0 → E_macro = 0.0."""
overlaps = {"geo": 0.0, "supply": 0.0, "commodity": 0.0, "sector": 0.0}
result = compute_normalized_macro_exposure(overlaps, tier="regional")
assert result == pytest.approx(0.0, abs=1e-9)
def test_empty_overlaps(self):
"""Empty overlaps dict → E_macro = 0.0."""
result = compute_normalized_macro_exposure({}, tier="regional")
assert result == pytest.approx(0.0, abs=1e-9)
def test_single_dimension_geo(self):
"""Only geo overlap → partial exposure."""
overlaps = {"geo": 1.0}
result = compute_normalized_macro_exposure(overlaps, tier="regional")
# E_raw = 1 - (1-0.35*1)(1-0.25*0)(1-0.25*0)(1-0.15*0) = 1 - 0.65 = 0.35
# E_max = 1 - (0.65)(0.75)(0.75)(0.85) = 1 - 0.311484375 ≈ 0.688515625
# E_macro = 0.35 / 0.688515625 ≈ 0.508
expected_e_raw = 0.35
e_max = 1.0 - (0.65 * 0.75 * 0.75 * 0.85)
expected = expected_e_raw / e_max
assert result == pytest.approx(expected, rel=1e-6)
def test_partial_overlaps(self):
"""Partial overlaps produce intermediate exposure."""
overlaps = {"geo": 0.5, "supply": 0.3, "commodity": 0.0, "sector": 0.8}
result = compute_normalized_macro_exposure(overlaps, tier="regional")
# Should be between 0 and 1
assert 0.0 < result < 1.0
# ---------------------------------------------------------------------------
# Resilience dampener per tier
# ---------------------------------------------------------------------------
class TestResilienceDampener:
"""Tests for resilience dampener application (Req 9.3)."""
def test_global_leader_dampener(self):
"""Global leader tier dampens exposure by 0.70."""
overlaps = {"geo": 1.0, "supply": 1.0, "commodity": 1.0, "sector": 1.0}
result = compute_normalized_macro_exposure(overlaps, tier="global_leader")
# E_macro = 1.0 * 0.70 = 0.70
assert result == pytest.approx(0.70, abs=1e-9)
def test_multinational_dampener(self):
"""Multinational tier dampens exposure by 0.85."""
overlaps = {"geo": 1.0, "supply": 1.0, "commodity": 1.0, "sector": 1.0}
result = compute_normalized_macro_exposure(overlaps, tier="multinational")
assert result == pytest.approx(0.85, abs=1e-9)
def test_regional_dampener(self):
"""Regional tier has no dampening (1.00)."""
overlaps = {"geo": 1.0, "supply": 1.0, "commodity": 1.0, "sector": 1.0}
result = compute_normalized_macro_exposure(overlaps, tier="regional")
assert result == pytest.approx(1.00, abs=1e-9)
def test_domestic_amplifier(self):
"""Domestic tier amplifies exposure by 1.20."""
overlaps = {"geo": 1.0, "supply": 1.0, "commodity": 1.0, "sector": 1.0}
result = compute_normalized_macro_exposure(overlaps, tier="domestic")
assert result == pytest.approx(1.20, abs=1e-9)
def test_unknown_tier_no_dampening(self):
"""Unknown tier defaults to 1.0 dampener."""
overlaps = {"geo": 1.0, "supply": 1.0, "commodity": 1.0, "sector": 1.0}
result = compute_normalized_macro_exposure(overlaps, tier="unknown_tier")
assert result == pytest.approx(1.00, abs=1e-9)
# ---------------------------------------------------------------------------
# Macro LLR at boundary values
# ---------------------------------------------------------------------------
class TestMacroLLR:
"""Tests for macro LLR computation (Req 9.4, 9.5)."""
def test_max_positive_inputs(self):
"""macro_impact=1, event_conf=1, q_recency=1, direction=+1 → p_macro=0.80."""
llr = compute_macro_llr(
macro_impact=1.0,
event_confidence=1.0,
q_recency=1.0,
macro_direction=1,
)
# p_macro = 0.50 + 0.30*1*1*1 = 0.80
expected = math.log(0.80 / 0.20) # ≈ 1.386
assert llr == pytest.approx(expected, rel=1e-6)
def test_max_negative_inputs(self):
"""All max with direction=-1 → negative LLR."""
llr = compute_macro_llr(
macro_impact=1.0,
event_confidence=1.0,
q_recency=1.0,
macro_direction=-1,
)
expected = -math.log(0.80 / 0.20)
assert llr == pytest.approx(expected, rel=1e-6)
def test_neutral_direction_zero_llr(self):
"""direction=0 → LLR=0.0 regardless of other inputs."""
llr = compute_macro_llr(
macro_impact=1.0,
event_confidence=1.0,
q_recency=1.0,
macro_direction=0,
)
assert llr == 0.0
def test_minimum_p_macro_clamp(self):
"""All impact factors zero → p_macro clamped to 0.501."""
llr = compute_macro_llr(
macro_impact=0.0,
event_confidence=0.0,
q_recency=0.0,
macro_direction=1,
)
# p_macro = 0.50 + 0 = 0.50, clamped up to 0.501
expected = math.log(0.501 / (1.0 - 0.501))
assert llr == pytest.approx(expected, rel=1e-6)
def test_mid_range_inputs(self):
"""Intermediate inputs produce reasonable LLR."""
llr = compute_macro_llr(
macro_impact=0.5,
event_confidence=0.7,
q_recency=0.8,
macro_direction=1,
)
# p_macro = 0.50 + 0.30 * 0.5 * 0.7 * 0.8 = 0.50 + 0.084 = 0.584
p_macro = 0.584
expected = math.log(p_macro / (1.0 - p_macro))
assert llr == pytest.approx(expected, rel=1e-6)
# ---------------------------------------------------------------------------
# Shrunk correlation convergence
# ---------------------------------------------------------------------------
class TestShrunkCorrelation:
"""Tests for shrinkage-adjusted correlation (Req 10.1, 10.2)."""
def test_large_n_approaches_rho_rolling(self):
"""n=1000 with same_sector → result ≈ rho_rolling."""
rho_rolling = 0.65
result = compute_shrunk_correlation(
rho_rolling=rho_rolling,
n_observations=1000,
same_sector=True,
)
# (1000/1030) × 0.65 + (30/1030) × 0.30 ≈ 0.6311 + 0.00874 ≈ 0.6398
weight_data = 1000 / 1030
weight_prior = 30 / 1030
expected = weight_data * rho_rolling + weight_prior * 0.30
assert result == pytest.approx(expected, rel=1e-6)
# Should be close to rho_rolling
assert abs(result - rho_rolling) < 0.02
def test_zero_observations_returns_prior(self):
"""n=0 → result = prior (same_sector: 0.30, cross_sector: 0.10)."""
# same sector
result_same = compute_shrunk_correlation(
rho_rolling=0.9,
n_observations=0,
same_sector=True,
)
# (0/30) × 0.9 + (30/30) × 0.30 = 0.30
assert result_same == pytest.approx(0.30, abs=1e-9)
# cross sector
result_cross = compute_shrunk_correlation(
rho_rolling=0.9,
n_observations=0,
same_sector=False,
)
# (0/30) × 0.9 + (30/30) × 0.10 = 0.10
assert result_cross == pytest.approx(0.10, abs=1e-9)
def test_cross_sector_prior(self):
"""Cross-sector uses prior = 0.10."""
result = compute_shrunk_correlation(
rho_rolling=0.50,
n_observations=30,
same_sector=False,
)
# (30/60) × 0.50 + (30/60) × 0.10 = 0.25 + 0.05 = 0.30
assert result == pytest.approx(0.30, abs=1e-9)
def test_negative_rolling_floored_at_zero(self):
"""Negative rolling correlation → rho_effective floored at 0."""
result = compute_shrunk_correlation(
rho_rolling=-0.50,
n_observations=100,
same_sector=False,
)
# (100/130)×(-0.50) + (30/130)×0.10 = -0.3846 + 0.0231 ≈ -0.3615
# Floored at 0
assert result == 0.0
def test_n_30_equal_weight(self):
"""n=30 → data and prior have equal weight."""
rho_rolling = 0.80
result = compute_shrunk_correlation(
rho_rolling=rho_rolling,
n_observations=30,
same_sector=True,
)
# (30/60)×0.80 + (30/60)×0.30 = 0.40 + 0.15 = 0.55
assert result == pytest.approx(0.55, abs=1e-9)
# ---------------------------------------------------------------------------
# Competitive LLR clamp at ±1.25
# ---------------------------------------------------------------------------
class TestCompetitiveLLRClamp:
"""Tests for competitive LLR clamping (Req 10.3, 10.4, 10.5)."""
def test_large_positive_clamped(self):
"""Large positive inputs → clamped to +1.25."""
result = compute_competitive_llr(
llr_source=10.0,
rho_effective=0.9,
d_network=1,
pattern_confidence=1.0,
)
assert result == pytest.approx(1.25, abs=1e-9)
def test_large_negative_clamped(self):
"""Large negative inputs → clamped to -1.25."""
result = compute_competitive_llr(
llr_source=-10.0,
rho_effective=0.9,
d_network=1,
pattern_confidence=1.0,
)
assert result == pytest.approx(-1.25, abs=1e-9)
def test_within_bounds_not_clamped(self):
"""Small inputs produce unclamped result."""
# attenuation = 0.5 × exp(-0.85 × 1) ≈ 0.5 × 0.4274 ≈ 0.2137
# LLR_competitive = 1.0 × 0.2137 × 0.8 ≈ 0.1710
result = compute_competitive_llr(
llr_source=1.0,
rho_effective=0.5,
d_network=1,
pattern_confidence=0.8,
)
expected = 1.0 * 0.5 * math.exp(-0.85 * 1) * 0.8
assert result == pytest.approx(expected, rel=1e-6)
assert abs(result) < 1.25
def test_zero_rho_gives_zero(self):
"""Zero correlation → zero competitive LLR."""
result = compute_competitive_llr(
llr_source=5.0,
rho_effective=0.0,
d_network=1,
pattern_confidence=1.0,
)
assert result == pytest.approx(0.0, abs=1e-9)
# ---------------------------------------------------------------------------
# Distance > 3 → zero attenuation
# ---------------------------------------------------------------------------
class TestDistanceAttenuation:
"""Tests for graph distance cutoff (Req 10.5)."""
def test_distance_4_returns_zero(self):
"""d_network=4 → LLR_competitive = 0.0."""
result = compute_competitive_llr(
llr_source=5.0,
rho_effective=0.9,
d_network=4,
pattern_confidence=1.0,
)
assert result == 0.0
def test_distance_10_returns_zero(self):
"""Very large distance → LLR_competitive = 0.0."""
result = compute_competitive_llr(
llr_source=5.0,
rho_effective=0.9,
d_network=10,
pattern_confidence=1.0,
)
assert result == 0.0
def test_distance_3_still_active(self):
"""d_network=3 (max allowed) → non-zero result."""
result = compute_competitive_llr(
llr_source=2.0,
rho_effective=0.8,
d_network=3,
pattern_confidence=0.9,
)
# attenuation = 0.8 × exp(-0.85 × 3) ≈ 0.8 × 0.0776 ≈ 0.0621
# LLR_competitive = 2.0 × 0.0621 × 0.9 ≈ 0.1118
expected = 2.0 * 0.8 * math.exp(-0.85 * 3) * 0.9
assert result == pytest.approx(expected, rel=1e-6)
assert result != 0.0
def test_distance_1_strongest(self):
"""d_network=1 gives strongest attenuation (least decay)."""
result_d1 = compute_competitive_llr(
llr_source=2.0, rho_effective=0.8, d_network=1, pattern_confidence=0.9,
)
result_d2 = compute_competitive_llr(
llr_source=2.0, rho_effective=0.8, d_network=2, pattern_confidence=0.9,
)
result_d3 = compute_competitive_llr(
llr_source=2.0, rho_effective=0.8, d_network=3, pattern_confidence=0.9,
)
assert result_d1 > result_d2 > result_d3 > 0.0