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

251 lines
11 KiB
Python
Raw Normal View History

"""Tests for signal processing module"""
import pytest
import time
from sentiment_engine.signal.velocity import VelocityComputer
from sentiment_engine.signal.decay import TemporalDecay
from sentiment_engine.signal.fusion import MultiSourceFusion
from sentiment_engine.schemas.output import AssetSentiment, VelocityMetrics, PumpDumpScore, EventFlag
class TestVelocityComputer:
"""Tests for VelocityComputer"""
@pytest.fixture
def computer(self):
return VelocityComputer()
def test_insufficient_data(self, computer):
"""Test with insufficient history"""
velocity = computer.get_asset_velocity("BTC")
assert velocity is None
def test_velocity_direction_accelerating(self, computer):
"""Test accelerating direction detection"""
now = time.time()
# Use compute method to properly initialize the window
from sentiment_engine.schemas.processed import ProcessedItem, EntityExtraction, SentimentScores, EmotionScores, EventClassification, TemporalAnchor, CredibilityScore, EventType
for i in range(10):
item = ProcessedItem(
payload_id="test",
source_id="test",
source_type="news",
ingest_ts=time.time() - 600 + i * 60,
publish_ts=time.time() - 600 + i * 60,
entities=[EntityExtraction(asset_id="BTC", mention_span=(0,3), confidence=0.9, entity_type="ticker", canonical_name="BTC")],
sentiment_per_asset={"BTC": SentimentScores(polarity=0.7, confidence=0.85, positive_prob=0.8, negative_prob=0.1, neutral_prob=0.1)},
emotions_per_asset={"BTC": EmotionScores(joy=0.8, fear=0.1, anger=0.05, greed=0.7, sadness=0.05, intensity=0.1 + i * 0.08)},
events=[],
temporal=TemporalAnchor(time_horizon="immediate", is_breaking=False, is_scheduled=False),
credibility=CredibilityScore(source_base=0.8, content_quality=0.8, engagement_authenticity=0.7, cross_source_corroboration=0.6, historical_accuracy=0.8, composite=0.78),
processed_ts=time.time() - 600 + i * 60,
processing_latency_ms=45.2,
model_versions={}
)
computer.compute("BTC", item, 0.2, 0.8)
velocity = computer.get_asset_velocity("BTC")
assert velocity is not None
assert velocity.velocity_direction == "accelerating"
def test_velocity_direction_decelerating(self, computer):
"""Test decelerating direction detection"""
now = time.time()
from sentiment_engine.schemas.processed import ProcessedItem, EntityExtraction, SentimentScores, EmotionScores, EventClassification, TemporalAnchor, CredibilityScore, EventType
for i in range(10):
item = ProcessedItem(
payload_id="test",
source_id="test",
source_type="news",
ingest_ts=time.time() - 600 + i * 60,
publish_ts=time.time() - 600 + i * 60,
entities=[EntityExtraction(asset_id="BTC", mention_span=(0,3), confidence=0.9, entity_type="ticker", canonical_name="BTC")],
sentiment_per_asset={"BTC": SentimentScores(polarity=0.7, confidence=0.85, positive_prob=0.8, negative_prob=0.1, neutral_prob=0.1)},
emotions_per_asset={"BTC": EmotionScores(joy=0.8, fear=0.1, anger=0.05, greed=0.7, sadness=0.05, intensity=0.9 - i * 0.08)},
events=[],
temporal=TemporalAnchor(time_horizon="immediate", is_breaking=False, is_scheduled=False),
credibility=CredibilityScore(source_base=0.8, content_quality=0.8, engagement_authenticity=0.7, cross_source_corroboration=0.6, historical_accuracy=0.8, composite=0.78),
processed_ts=time.time() - 600 + i * 60,
processing_latency_ms=45.2,
model_versions={}
)
computer.compute("BTC", item, 0.2, 0.8)
velocity = computer.get_asset_velocity("BTC")
assert velocity is not None
assert velocity.velocity_direction == "decelerating"
def test_pub_velocity(self, computer):
"""Test publication velocity calculation"""
now = time.time()
from sentiment_engine.schemas.processed import ProcessedItem, EntityExtraction, SentimentScores, EmotionScores, EventClassification, TemporalAnchor, CredibilityScore, EventType
for i in range(5):
item = ProcessedItem(
payload_id="test",
source_id=f"source_{i}",
source_type="news",
ingest_ts=time.time() - 300 + i * 60,
publish_ts=time.time() - 300 + i * 60,
entities=[EntityExtraction(asset_id="BTC", mention_span=(0,3), confidence=0.9, entity_type="ticker", canonical_name="BTC")],
sentiment_per_asset={"BTC": SentimentScores(polarity=0.7, confidence=0.85, positive_prob=0.8, negative_prob=0.1, neutral_prob=0.1)},
emotions_per_asset={"BTC": EmotionScores(joy=0.8, fear=0.1, anger=0.05, greed=0.7, sadness=0.05, intensity=0.5)},
events=[],
temporal=TemporalAnchor(time_horizon="immediate", is_breaking=False, is_scheduled=False),
credibility=CredibilityScore(source_base=0.8, content_quality=0.8, engagement_authenticity=0.7, cross_source_corroboration=0.6, historical_accuracy=0.8, composite=0.78),
processed_ts=time.time() - 300 + i * 60,
processing_latency_ms=45.2,
model_versions={}
)
computer.compute("BTC", item, 0.2, 0.8)
velocity = computer.get_asset_velocity("BTC")
assert velocity is not None
assert velocity.pub_velocity > 0
assert velocity.source_count == 5
class TestTemporalDecay:
"""Tests for TemporalDecay"""
@pytest.fixture
def decay(self):
return TemporalDecay()
def test_decay_recent(self, decay):
"""Test decay for recent timestamp"""
now = time.time()
factor = decay.compute(now)
assert abs(factor - 1.0) < 1e-8
def test_decay_half_life(self, decay):
"""Test decay at half-life"""
half_life = 180 * 60 # 180 minutes in seconds
past = time.time() - half_life
factor = decay.compute(past, halflife_minutes=180)
assert 0.45 < factor < 0.55
def test_decay_old(self, decay):
"""Test decay for old timestamp"""
past = time.time() - 24 * 3600 # 24 hours ago
factor = decay.compute(past, halflife_minutes=180)
assert factor < 0.01
def test_apply_to_signal(self, decay):
"""Test applying decay to a signal"""
signal = 100.0
past = time.time() - 180 * 60 # 1 half-life ago
decayed = decay.apply_to_signal(signal, past, 180)
assert 45 < decayed < 55
def test_apply_to_asset_sentiment(self, decay):
"""Test applying decay to AssetSentiment"""
from sentiment_engine.schemas.output import AssetSentiment, PumpDumpScore, VelocityMetrics
asset = AssetSentiment(
asset_id="BTC",
fear_state=50.0,
greed_state=50.0,
sentiment_polarity=0.0,
pump_dump=PumpDumpScore(asset_id="BTC", pump_score=50.0, dump_score=50.0, pump_confidence=0.8, dump_confidence=0.7, last_update_ts=time.time() - 180 * 60),
velocity=VelocityMetrics(hype_velocity=0.5, pub_velocity=0.5, velocity_direction="neutral", window_minutes=15, source_count=1, unique_assets=1),
last_update_ts=time.time() - 180 * 60, # 1 half-life ago
contributing_sources=1,
decay_factor=1.0
)
decay.apply_to_asset_sentiment(asset, {
"fear_state": 180,
"greed_state": 180,
"pump_score": 180,
"dump_score": 180,
"hype_velocity": 60,
"pub_velocity": 120,
"event_flags": 480
})
assert abs(asset.fear_state - 25.0) < 0.001 # 50 * 0.5
assert abs(asset.greed_state - 25.0) < 0.001
assert abs(asset.pump_dump.pump_score - 25.0) < 0.02
assert abs(asset.pump_dump.dump_score - 25.0) < 0.02
class TestMultiSourceFusion:
"""Tests for MultiSourceFusion"""
@pytest.fixture
def fusion(self):
return MultiSourceFusion()
def test_single_signal_no_fusion(self, fusion):
"""Test single signal returns as-is"""
now = time.time()
signal = AssetSentiment(
asset_id="BTC",
fear_state=20.0,
greed_state=80.0,
sentiment_polarity=60.0,
pump_dump=PumpDumpScore(asset_id="BTC", pump_score=75.0, dump_score=15.0, pump_confidence=0.8, dump_confidence=0.7, last_update_ts=now),
last_update_ts=now,
decay_factor=1.0
)
result = fusion.add_signal(signal)
assert result is signal # Same object returned
def test_two_signals_fusion(self, fusion):
"""Test fusion of two signals"""
now = time.time()
signal1 = AssetSentiment(
asset_id="BTC",
fear_state=30.0,
greed_state=70.0,
sentiment_polarity=40.0,
pump_dump=PumpDumpScore(asset_id="BTC", pump_score=60.0, dump_score=20.0, pump_confidence=0.8, dump_confidence=0.7, last_update_ts=now),
last_update_ts=now,
decay_factor=1.0,
contributing_sources=1
)
signal2 = AssetSentiment(
asset_id="BTC",
fear_state=10.0,
greed_state=90.0,
sentiment_polarity=80.0,
pump_dump=PumpDumpScore(asset_id="BTC", pump_score=90.0, dump_score=10.0, pump_confidence=0.9, dump_confidence=0.6, last_update_ts=now),
last_update_ts=now,
decay_factor=1.0,
contributing_sources=1
)
# Add first signal
fusion.add_signal(signal1)
# Add second signal - should trigger fusion
result = fusion.add_signal(signal2)
assert result is not signal1 and result is not signal2
assert result.contributing_sources == 2
# Fused values should be weighted average
assert 10.0 < result.fear_state < 30.0
assert 70.0 < result.greed_state < 90.0
assert 60.0 < result.pump_dump.pump_score < 90.0
def test_force_fuse_all(self, fusion):
"""Test force fusion of all pending signals"""
now = time.time()
for i in range(3):
signal = AssetSentiment(
asset_id=f"ASSET{i}",
fear_state=20.0 + i * 10,
greed_state=80.0 - i * 10,
sentiment_polarity=60.0 - i * 20,
pump_dump=PumpDumpScore(asset_id=f"ASSET{i}", pump_score=50.0 + i * 10, dump_score=20.0, pump_confidence=0.8, dump_confidence=0.7, last_update_ts=now),
last_update_ts=now,
decay_factor=1.0,
contributing_sources=1
)
fusion.add_signal(signal)
results = fusion.force_fuse_all()
assert len(results) == 3
for asset_id, signal in results.items():
assert signal.contributing_sources == 1 # Each was single source