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

351 lines
12 KiB
Python

"""
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"])