Files
sentiment-engine/sentiment_engine/tests/unit/test_signal_processing_comprehensive.py

351 lines
12 KiB
Python
Raw Normal View History

"""
Comprehensive tests for Signal Processing components.
"""
import pytest
import numpy as np
from unittest.mock import AsyncMock, MagicMock, patch
from sentiment_engine.signal.processor import FearGreedProcessor
from sentiment_engine.signal.velocity import VelocityCalculator
from sentiment_engine.signal.decay import DecayEngine
from sentiment_engine.signal.fusion import MultiSourceFusion
from sentiment_engine.schemas.processed import ProcessedItem, SentimentScores, EmotionScores
class TestFearGreedProcessor:
"""Tests for FearGreedProcessor"""
@pytest.fixture
def processor(self):
return FearGreedProcessor()
def test_compute_fear_greed_basic(self, processor):
"""Should compute basic fear/greed index"""
items = [
ProcessedItem(
payload_id="test1",
source_id="source1",
source_type="news",
ingest_ts=1700000000.0,
publish_ts=1700000000.0,
entities=[],
sentiment_per_asset={"BTC": SentimentScores(polarity=0.8, confidence=0.9, positive_prob=0.9, negative_prob=0.05, neutral_prob=0.05)},
emotions_per_asset={"BTC": EmotionScores(joy=0.8, fear=0.1, anger=0.0, greed=0.7, sadness=0.0, intensity=0.8)},
events=[],
temporal=None,
credibility=None,
processed_ts=1700000000.0,
processing_latency_ms=100,
model_versions={}
)
]
result = processor.compute(items)
assert 0 <= result <= 100
# High positive sentiment should give high (greed) index
assert result > 50
def test_compute_fear_greed_negative(self, processor):
"""Negative sentiment should give low (fear) index"""
items = [
ProcessedItem(
payload_id="test1",
source_id="source1",
source_type="news",
ingest_ts=1700000000.0,
publish_ts=1700000000.0,
entities=[],
sentiment_per_asset={"BTC": SentimentScores(polarity=-0.8, confidence=0.9, positive_prob=0.05, negative_prob=0.9, neutral_prob=0.05)},
emotions_per_asset={"BTC": EmotionScores(joy=0.1, fear=0.8, anger=0.3, greed=0.0, sadness=0.4, intensity=0.8)},
events=[],
temporal=None,
credibility=None,
processed_ts=1700000000.0,
processing_latency_ms=100,
model_versions={}
)
]
result = processor.compute(items)
assert 0 <= result <= 100
assert result < 50
def test_compute_empty(self, processor):
"""Empty items should return neutral"""
result = processor.compute([])
assert result == 50
def test_compute_multiple_assets(self, processor):
"""Should aggregate across multiple assets"""
items = [
ProcessedItem(
payload_id="test1",
source_id="source1",
source_type="news",
ingest_ts=1700000000.0,
publish_ts=1700000000.0,
entities=[],
sentiment_per_asset={
"BTC": SentimentScores(polarity=0.5, confidence=0.8, positive_prob=0.7, negative_prob=0.1, neutral_prob=0.2),
"ETH": SentimentScores(polarity=-0.3, confidence=0.7, positive_prob=0.2, negative_prob=0.6, neutral_prob=0.2)
},
emotions_per_asset={},
events=[],
temporal=None,
credibility=None,
processed_ts=1700000000.0,
processing_latency_ms=100,
model_versions={}
)
]
result = processor.compute(items)
assert 0 <= result <= 100
class TestVelocityCalculator:
"""Tests for VelocityCalculator"""
@pytest.fixture
def calculator(self):
return VelocityCalculator()
def test_compute_velocity_basic(self, calculator):
"""Should compute velocity from recent items"""
now = 1700000000.0
items = [
{"asset_id": "BTC", "publish_ts": now - 3600, "sentiment_polarity": 0.5},
{"asset_id": "BTC", "publish_ts": now - 1800, "sentiment_polarity": 0.7},
]
velocity = calculator.compute_velocity("BTC", items)
assert isinstance(velocity, float)
def test_velocity_positive_trend(self, calculator):
"""Positive trend should give positive velocity"""
now = 1700000000.0
items = [
{"asset_id": "BTC", "publish_ts": now - 7200, "sentiment_polarity": 0.2},
{"asset_id": "BTC", "publish_ts": now - 3600, "sentiment_polarity": 0.5},
{"asset_id": "BTC", "publish_ts": now - 1800, "sentiment_polarity": 0.8},
]
velocity = calculator.compute_velocity("BTC", items)
assert velocity > 0
def test_velocity_negative_trend(self, calculator):
"""Negative trend should give negative velocity"""
now = 1700000000.0
items = [
{"asset_id": "BTC", "publish_ts": now - 7200, "sentiment_polarity": 0.8},
{"asset_id": "BTC", "publish_ts": now - 3600, "sentiment_polarity": 0.5},
{"asset_id": "BTC", "publish_ts": now - 1800, "sentiment_polarity": 0.2},
]
velocity = calculator.compute_velocity("BTC", items)
assert velocity < 0
def test_velocity_flat(self, calculator):
"""Flat sentiment should give near-zero velocity"""
now = 1700000000.0
items = [
{"asset_id": "BTC", "publish_ts": now - 7200, "sentiment_polarity": 0.5},
{"asset_id": "BTC", "publish_ts": now - 3600, "sentiment_polarity": 0.5},
{"asset_id": "BTC", "publish_ts": now - 1800, "sentiment_polarity": 0.5},
]
velocity = calculator.compute_velocity("BTC", items)
assert abs(velocity) < 0.1
def test_velocity_empty(self, calculator):
"""Empty items should return 0"""
velocity = calculator.compute_velocity("BTC", [])
assert velocity == 0
def test_velocity_single_point(self, calculator):
"""Single point should return 0"""
now = 1700000000.0
items = [{"asset_id": "BTC", "publish_ts": now - 3600, "sentiment_polarity": 0.5}]
velocity = calculator.compute_velocity("BTC", items)
assert velocity == 0
class TestDecayEngine:
"""Tests for DecayEngine"""
@pytest.fixture
def engine(self):
return DecayEngine()
def test_compute_decay_exponential(self, engine):
"""Exponential decay should decrease over time"""
now = 1700000000.0
weight_recent = engine.compute_decay(now - 60, now, halflife_minutes=60) # 1 min ago
weight_old = engine.compute_decay(now - 3600, now, halflife_minutes=60) # 1 hour ago
assert weight_recent > weight_old
def test_compute_decay_half_life(self, engine):
"""At half-life, weight should be 0.5"""
now = 1700000000.0
halflife = 60 # minutes
# At exactly one half-life
weight = engine.compute_decay(now - halflife * 60, now, halflife_minutes=halflife)
assert abs(weight - 0.5) < 0.01
def test_compute_decay_now(self, engine):
"""Weight at now should be 1"""
now = 1700000000.0
weight = engine.compute_decay(now, now, halflife_minutes=60)
assert weight == 1.0
def test_compute_decay_future(self, engine):
"""Future timestamps should return 1"""
now = 1700000000.0
weight = engine.compute_decay(now + 3600, now, halflife_minutes=60)
assert weight == 1.0
def test_compute_decay_custom_halflife(self, engine):
"""Custom half-life should work"""
now = 1700000000.0
weight_short = engine.compute_decay(now - 3600, now, halflife_minutes=30) # 1 hour ago, 30min halflife
weight_long = engine.compute_decay(now - 3600, now, halflife_minutes=120) # 1 hour ago, 2hr halflife
# Shorter half-life = more decay
assert weight_short < weight_long
class TestMultiSourceFusion:
"""Tests for MultiSourceFusion"""
@pytest.fixture
def fusion(self):
return MultiSourceFusion()
def test_fuse_equal_weights(self, fusion):
"""Equal weights should produce average"""
scores = {
"source1": 0.8,
"source2": 0.2,
}
result = fusion.fuse(scores, weights={"source1": 0.5, "source2": 0.5})
assert abs(result - 0.5) < 0.01
def test_fuse_weighted(self, fusion):
"""Weighted fusion should respect weights"""
scores = {
"source1": 1.0,
"source2": 0.0,
}
result = fusion.fuse(scores, weights={"source1": 0.8, "source2": 0.2})
assert result > 0.7 # Closer to source1
def test_fuse_normalizes_weights(self, fusion):
"""Should normalize weights that don't sum to 1"""
scores = {
"source1": 1.0,
"source2": 0.0,
}
result = fusion.fuse(scores, weights={"source1": 2.0, "source2": 1.0})
# Weights normalized to 2/3 and 1/3
assert result > 0.6
def test_fuse_missing_weight(self, fusion):
"""Missing weights should default to equal"""
scores = {
"source1": 1.0,
"source2": 0.0,
"source3": 0.5,
}
result = fusion.fuse(scores, weights={"source1": 0.5, "source2": 0.5})
# source3 gets default weight
assert 0 <= result <= 1
def test_fuse_empty(self, fusion):
"""Empty scores should return neutral"""
result = fusion.fuse({})
assert result == 0.5
class TestSignalProcessingIntegration:
"""Integration tests for signal processing"""
def test_fear_greed_velocity_correlation(self):
"""High fear/greed with positive velocity should align"""
from sentiment_engine.signal.processor import FearGreedProcessor
from sentiment_engine.signal.velocity import VelocityCalculator
processor = FearGreedProcessor()
calculator = VelocityCalculator()
now = 1700000000.0
# Create items with positive sentiment trend
items = [
ProcessedItem(
payload_id="test1",
source_id="source1",
source_type="news",
ingest_ts=now,
publish_ts=now - 3600,
entities=[],
sentiment_per_asset={"BTC": SentimentScores(polarity=0.8, confidence=0.9, positive_prob=0.9, negative_prob=0.05, neutral_prob=0.05)},
emotions_per_asset={"BTC": EmotionScores(joy=0.8, fear=0.1, anger=0.0, greed=0.7, sadness=0.0, intensity=0.8)},
events=[],
temporal=None,
credibility=None,
processed_ts=now,
processing_latency_ms=100,
model_versions={}
),
ProcessedItem(
payload_id="test2",
source_id="source1",
source_type="news",
ingest_ts=now,
publish_ts=now - 1800,
entities=[],
sentiment_per_asset={"BTC": SentimentScores(polarity=0.9, confidence=0.95, positive_prob=0.95, negative_prob=0.02, neutral_prob=0.03)},
emotions_per_asset={"BTC": EmotionScores(joy=0.9, fear=0.05, anger=0.0, greed=0.8, sadness=0.0, intensity=0.9)},
events=[],
temporal=None,
credibility=None,
processed_ts=now,
processing_latency_ms=100,
model_versions={}
)
]
fg = processor.compute(items)
velocity = calculator.compute_velocity("BTC", [
{"asset_id": "BTC", "publish_ts": 1700000000.0 - 3600, "sentiment_polarity": 0.8},
{"asset_id": "BTC", "publish_ts": 1700000000.0 - 1800, "sentiment_polarity": 0.9},
])
# Both should be positive
assert fg > 50
assert velocity > 0
if __name__ == "__main__":
pytest.main([__file__, "-v"])