feat: math core v3 engine upgrade

This commit is contained in:
Celes Renata
2026-06-27 12:21:41 +00:00
parent 365bc5d4b7
commit b4bf0f2361
34 changed files with 11693 additions and 3 deletions
+349
View File
@@ -0,0 +1,349 @@
"""Unit tests for v3 multiplicative confidence and data quality.
Tests for compute_v3_confidence, compute_v3_data_quality, and
should_force_informational_v3 functions.
Requirements validated: 8.18.5, 17.117.8
"""
from __future__ import annotations
import math
from datetime import datetime, timezone
import pytest
from services.aggregation.scoring import EvidenceUnit
from services.aggregation.worker import (
compute_v3_confidence,
compute_v3_data_quality,
should_force_informational_v3,
)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
_NOW = datetime(2025, 1, 15, 12, 0, 0, tzinfo=timezone.utc)
def _make_unit(
layer: str = "company",
extraction_conf: float = 0.8,
impact: float = 0.7,
) -> EvidenceUnit:
"""Create a minimal EvidenceUnit for testing."""
return EvidenceUnit(
symbol="AAPL",
layer=layer,
event_type="earnings",
source_id="doc-1",
source_group="company",
timestamp=_NOW,
horizon="7d",
direction=1,
sentiment_strength=0.8,
impact=impact,
extraction_conf=extraction_conf,
source_cred=0.85,
novelty=0.9,
event_base_rate=0.25,
cluster_id="test-cluster",
)
# ---------------------------------------------------------------------------
# Test: Zero data quality → zero confidence
# Requirement: 8.1, 8.5
# ---------------------------------------------------------------------------
class TestZeroDataQualityConfidence:
"""When data_quality = 0.0, confidence must be 0.0."""
def test_zero_data_quality_produces_zero_confidence(self):
confidence = compute_v3_confidence(
n_eff_total=5.0,
q_values=[0.8, 0.7],
llrs=[1.0, 0.5],
strength=0.6,
regime_confidence_mult=1.0,
contradiction=0.0,
data_quality=0.0,
)
assert confidence == 0.0
def test_near_zero_data_quality_suppresses_confidence(self):
"""Very low data_quality → near-zero confidence."""
confidence = compute_v3_confidence(
n_eff_total=10.0,
q_values=[0.9, 0.9],
llrs=[1.5, 1.5],
strength=0.8,
regime_confidence_mult=1.0,
contradiction=0.0,
data_quality=0.01,
)
assert confidence < 0.05
# ---------------------------------------------------------------------------
# Test: Full contradiction (1.0) → zero confidence
# Requirement: 8.4
# ---------------------------------------------------------------------------
class TestFullContradictionConfidence:
"""When contradiction = 1.0, confidence must be 0.0."""
def test_full_contradiction_produces_zero_confidence(self):
confidence = compute_v3_confidence(
n_eff_total=10.0,
q_values=[0.9, 0.8],
llrs=[1.0, 1.2],
strength=0.7,
regime_confidence_mult=1.0,
contradiction=1.0,
data_quality=0.9,
)
assert confidence == 0.0
def test_high_contradiction_suppresses_confidence(self):
"""Contradiction = 0.9 → confidence heavily suppressed."""
conf_no_contra = compute_v3_confidence(
n_eff_total=5.0,
q_values=[0.8],
llrs=[1.0],
strength=0.6,
regime_confidence_mult=1.0,
contradiction=0.0,
data_quality=0.8,
)
conf_high_contra = compute_v3_confidence(
n_eff_total=5.0,
q_values=[0.8],
llrs=[1.0],
strength=0.6,
regime_confidence_mult=1.0,
contradiction=0.9,
data_quality=0.8,
)
assert conf_high_contra < conf_no_contra * 0.15
# ---------------------------------------------------------------------------
# Test: Low n_eff → suppressed C_evidence
# Requirement: 8.2
# ---------------------------------------------------------------------------
class TestLowNEffConfidence:
"""Low n_eff_total → C_evidence is suppressed."""
def test_very_low_n_eff_suppresses_c_evidence(self):
"""n_eff_total=0.5 → C_evidence = 1 - exp(-0.1) ≈ 0.095."""
# C_evidence = 1 - exp(-0.5 / 5.0) = 1 - exp(-0.1) ≈ 0.0952
expected_c_evidence = 1.0 - math.exp(-0.1)
assert expected_c_evidence == pytest.approx(0.0952, rel=1e-2)
confidence = compute_v3_confidence(
n_eff_total=0.5,
q_values=[0.8],
llrs=[1.0],
strength=0.8,
regime_confidence_mult=1.0,
contradiction=0.0,
data_quality=0.9,
)
# Confidence is bounded by C_evidence ≈ 0.095
assert confidence < 0.15
def test_high_n_eff_yields_higher_confidence(self):
"""Higher n_eff → higher C_evidence → higher overall confidence."""
conf_low = compute_v3_confidence(
n_eff_total=1.0,
q_values=[0.8],
llrs=[1.0],
strength=0.6,
regime_confidence_mult=1.0,
contradiction=0.0,
data_quality=0.8,
)
conf_high = compute_v3_confidence(
n_eff_total=10.0,
q_values=[0.8],
llrs=[1.0],
strength=0.6,
regime_confidence_mult=1.0,
contradiction=0.0,
data_quality=0.8,
)
assert conf_high > conf_low
# ---------------------------------------------------------------------------
# Test: Data quality computed from known inputs
# Requirement: 17.1, 17.2, 17.3, 17.4, 17.5, 17.6
# ---------------------------------------------------------------------------
class TestDataQualityComputation:
"""Verify data quality formula with known inputs."""
def test_high_quality_inputs(self):
"""Zero failure, fresh signal, many sources → high quality."""
units = [_make_unit() for _ in range(10)]
dq = compute_v3_data_quality(
units=units,
extraction_failure_rate=0.0,
age_newest_hours=1.0,
n_source_types=4,
)
# Q_parse=1.0, Q_fresh=exp(-1/168)≈0.994, Q_coverage=1-exp(-2)≈0.865,
# Q_diversity=min(1, log2(5)/log2(4))=1.0
assert dq > 0.60
def test_high_extraction_failure_rate(self):
"""extraction_failure_rate=0.8 → Q_parse=0.2 → low quality."""
units = [_make_unit() for _ in range(5)]
dq = compute_v3_data_quality(
units=units,
extraction_failure_rate=0.8,
age_newest_hours=1.0,
n_source_types=3,
)
# Q_parse = 0.2 → heavily suppresses data_quality
assert dq < 0.30
def test_zero_sources_zero_diversity(self):
"""No source types → Q_diversity = 0 → data_quality = 0."""
units = [_make_unit() for _ in range(5)]
dq = compute_v3_data_quality(
units=units,
extraction_failure_rate=0.0,
age_newest_hours=1.0,
n_source_types=0,
)
assert dq == 0.0
def test_empty_units_low_coverage(self):
"""No valid units → Q_coverage = 1 - exp(0) = 0 → data_quality = 0."""
dq = compute_v3_data_quality(
units=[],
extraction_failure_rate=0.0,
age_newest_hours=1.0,
n_source_types=3,
)
assert dq == 0.0
def test_stale_signal_decays_quality(self):
"""Very old signal → Q_fresh low → suppresses data_quality."""
units = [_make_unit() for _ in range(5)]
dq_fresh = compute_v3_data_quality(
units=units,
extraction_failure_rate=0.0,
age_newest_hours=1.0,
n_source_types=3,
)
dq_stale = compute_v3_data_quality(
units=units,
extraction_failure_rate=0.0,
age_newest_hours=500.0,
n_source_types=3,
)
assert dq_stale < dq_fresh
# ---------------------------------------------------------------------------
# Test: Force informational mode
# Requirement: 17.7, 17.8
# ---------------------------------------------------------------------------
class TestForceInformational:
"""Test should_force_informational_v3 forcing conditions."""
def test_data_quality_below_threshold_forces(self):
"""data_quality < 0.50 → forces informational."""
units = [_make_unit() for _ in range(5)]
forced, reason = should_force_informational_v3(
data_quality=0.49,
units=units,
extraction_failure_rate=0.0,
)
assert forced is True
assert reason == "data_quality_below_threshold"
def test_data_quality_at_threshold_does_not_force(self):
"""data_quality = 0.50 → does NOT force informational."""
units = [_make_unit() for _ in range(5)]
forced, reason = should_force_informational_v3(
data_quality=0.50,
units=units,
extraction_failure_rate=0.0,
)
assert forced is False
def test_insufficient_evidence_forces(self):
"""N_valid < 2 → forces informational."""
units = [_make_unit()]
forced, reason = should_force_informational_v3(
data_quality=0.80,
units=units,
extraction_failure_rate=0.0,
)
assert forced is True
assert reason == "insufficient_evidence_count"
def test_high_extraction_failure_forces(self):
"""extraction_failure_rate > 0.50 → Q_parse < 0.50 → forces."""
units = [_make_unit() for _ in range(5)]
forced, reason = should_force_informational_v3(
data_quality=0.80,
units=units,
extraction_failure_rate=0.51,
)
assert forced is True
assert reason == "extraction_parse_rate_below_threshold"
def test_only_macro_signals_forces(self):
"""Only macro/competitive evidence (no company) → forces."""
units = [_make_unit(layer="macro"), _make_unit(layer="competitive")]
forced, reason = should_force_informational_v3(
data_quality=0.80,
units=units,
extraction_failure_rate=0.0,
)
assert forced is True
assert reason == "macro_competitive_only_evidence"
def test_only_macro_with_macro_only_enabled_does_not_force(self):
"""Only macro evidence WITH macro_only_enabled → does NOT force."""
units = [_make_unit(layer="macro"), _make_unit(layer="macro")]
forced, reason = should_force_informational_v3(
data_quality=0.80,
units=units,
extraction_failure_rate=0.0,
macro_only_enabled=True,
)
assert forced is False
def test_mixed_signals_does_not_force(self):
"""Company + macro evidence → does NOT force."""
units = [_make_unit(layer="company"), _make_unit(layer="macro")]
forced, reason = should_force_informational_v3(
data_quality=0.80,
units=units,
extraction_failure_rate=0.0,
)
assert forced is False
def test_good_inputs_do_not_force(self):
"""All good → no forcing."""
units = [_make_unit() for _ in range(5)]
forced, reason = should_force_informational_v3(
data_quality=0.80,
units=units,
extraction_failure_rate=0.1,
)
assert forced is False
assert reason == ""