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