251 lines
11 KiB
Python
251 lines
11 KiB
Python
"""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
|