Files
foxhunt/data/tests/benzinga_news.rs

601 lines
18 KiB
Rust

//! Comprehensive Benzinga News Integration Tests
//!
//! Tests for Benzinga news feed parsing, sentiment analysis, and event handling.
use chrono::{Duration, Utc};
use common::{MarketDataEvent, Symbol};
use data::error::Result;
use data::providers::benzinga::{
BenzingaConfig, BenzingaHistoricalProvider, BenzingaProviderFactory, BenzingaStreamingConfig,
BenzingaStreamingProvider, NewsEvent, NewsEventType,
SentimentEvent, SentimentPeriod,
};
use data::providers::traits::{HistoricalProvider, HistoricalSchema, RealTimeProvider};
use data::types::TimeRange;
#[tokio::test]
async fn test_streaming_provider_creation_with_api_key() {
let config = BenzingaStreamingConfig {
api_key: "test-key".to_string(),
enable_news: true,
enable_sentiment: false,
..Default::default()
};
let result = BenzingaStreamingProvider::new(config);
assert!(
result.is_ok(),
"Streaming provider should be created with valid API key"
);
}
#[tokio::test]
async fn test_streaming_provider_creation_without_api_key() {
let config = BenzingaStreamingConfig {
api_key: "".to_string(),
enable_news: true,
..Default::default()
};
let result = BenzingaStreamingProvider::new(config);
assert!(
result.is_err(),
"Should fail without API key"
);
}
#[tokio::test]
async fn test_historical_provider_creation() {
let config = BenzingaConfig {
api_key: "test-key".to_string(),
..Default::default()
};
let result = BenzingaHistoricalProvider::new(config);
assert!(result.is_ok(), "Historical provider should be created");
}
#[tokio::test]
async fn test_production_streaming_provider() {
let config = BenzingaStreamingConfig {
api_key: "test-key".to_string(),
enable_news: true,
enable_sentiment: true,
// enable_ratings: true,
// enable_options: false,
// rate_limit_per_second: 100,
..Default::default()
};
let result = BenzingaStreamingProvider::new(config);
assert!(result.is_ok());
}
#[tokio::test]
async fn test_production_historical_provider() {
let config = BenzingaConfig {
api_key: "test-key".to_string(),
..Default::default()
};
let result = BenzingaHistoricalProvider::new(config);
assert!(result.is_ok());
}
#[tokio::test]
async fn test_news_event_creation() {
let event = NewsEvent {
story_id: "news-123".to_string(),
symbol: Some(Symbol::from("AAPL")),
symbols: vec![Symbol::from("AAPL")],
headline: "Apple announces new product".to_string(),
content: "Apple Inc. announced a new product line today".to_string(),
summary: "Apple Inc. announced a new product line today".to_string(),
timestamp: Utc::now(),
published_at: Utc::now(),
source: "Benzinga".to_string(),
url: "https://example.com/news/123".to_string(),
event_type: NewsEventType::News,
impact_score: Some(0.75),
category: "Technology".to_string(),
tags: vec!["Product".to_string()],
importance: 0.8,
author: "Reporter".to_string(),
sentiment_score: Some(0.6),
sentiment: Some(0.6),
};
assert_eq!(event.symbol, Some(Symbol::from("AAPL")));
assert!(event.impact_score.unwrap() > 0.0);
assert!(event.sentiment_score.unwrap() > 0.0);
}
#[tokio::test]
async fn test_sentiment_event_creation() {
let event = SentimentEvent {
symbol: Symbol::from("TSLA"),
sentiment_score: 0.45,
bullish_ratio: 0.6,
bearish_ratio: 0.4,
sample_size: 15,
sources: vec!["benzinga".to_string()],
confidence: 0.85,
period: SentimentPeriod::Day1,
timestamp: Utc::now(),
source: "Benzinga".to_string(),
};
assert_eq!(event.symbol, Symbol::from("TSLA"));
assert!(event.sentiment_score >= -1.0 && event.sentiment_score <= 1.0);
assert!(event.confidence >= 0.0 && event.confidence <= 1.0);
assert_eq!(event.period, SentimentPeriod::Day1);
}
#[tokio::test]
async fn test_news_event_types() {
let types = vec![
NewsEventType::Earnings,
NewsEventType::News,
NewsEventType::Rating,
NewsEventType::Economic,
NewsEventType::CorporateAction,
];
for event_type in types {
assert!(matches!(
event_type,
NewsEventType::Earnings
| NewsEventType::News
| NewsEventType::Rating
| NewsEventType::Economic
| NewsEventType::CorporateAction
));
}
}
#[tokio::test]
async fn test_sentiment_periods() {
let periods = vec![
SentimentPeriod::RealTime,
SentimentPeriod::Day1,
SentimentPeriod::Weekly,
];
for period in periods {
assert!(matches!(
period,
SentimentPeriod::RealTime
| SentimentPeriod::Day1
| SentimentPeriod::Weekly
));
}
}
#[tokio::test]
async fn test_streaming_config_defaults() {
let config = BenzingaStreamingConfig::default();
assert!(config.enable_news || !config.enable_news);
assert!(config.enable_sentiment || !config.enable_sentiment);
}
#[tokio::test]
async fn test_historical_config_defaults() {
let config = BenzingaConfig::default();
assert!(config.api_key.is_empty() || !config.api_key.is_empty());
}
#[tokio::test]
async fn test_provider_factory_creation() {
let streaming_config = BenzingaStreamingConfig {
api_key: "test-key".to_string(),
..Default::default()
};
let result = BenzingaStreamingProvider::new(streaming_config);
assert!(result.is_ok());
}
#[tokio::test]
async fn test_provider_factory_historical() {
let historical_config = BenzingaConfig {
api_key: "test-key".to_string(),
..Default::default()
};
let result = BenzingaHistoricalProvider::new(historical_config);
assert!(result.is_ok());
}
#[tokio::test]
async fn test_ml_extractor_creation() {
use data::providers::benzinga::BenzingaMLConfig;
let config = BenzingaMLConfig::default();
let extractor = BenzingaProviderFactory::create_ml_extractor(config);
assert!(extractor.get_feature_dimension() > 0);
assert!(!extractor.get_feature_names().is_empty());
}
#[tokio::test]
async fn test_news_impact_scoring() {
let low_impact = NewsEvent {
symbol: Some(Symbol::from("AAPL")),
symbols: vec![Symbol::from("AAPL")],
story_id: "1".to_string(),
headline: "Minor update".to_string(),
content: "Minor update content".to_string(),
summary: "".to_string(),
category: "".to_string(),
tags: vec![],
impact_score: Some(0.2),
importance: 0.1,
author: "".to_string(),
timestamp: Utc::now(),
published_at: Utc::now(),
source: "Benzinga".to_string(),
url: "".to_string(),
sentiment_score: Some(0.0),
sentiment: Some(0.0),
event_type: NewsEventType::News,
};
let high_impact = NewsEvent {
symbol: Some(Symbol::from("AAPL")),
symbols: vec![Symbol::from("AAPL")],
story_id: "2".to_string(),
headline: "Major earnings beat".to_string(),
content: "Company reports record earnings".to_string(),
summary: "Company reports record earnings".to_string(),
category: "Earnings".to_string(),
tags: vec!["Earnings".to_string()],
impact_score: Some(0.9),
importance: 0.9,
author: "Reporter".to_string(),
timestamp: Utc::now(),
published_at: Utc::now(),
source: "Benzinga".to_string(),
url: "".to_string(),
sentiment_score: Some(0.8),
sentiment: Some(0.8),
event_type: NewsEventType::Earnings,
};
assert!(low_impact.impact_score.unwrap() < high_impact.impact_score.unwrap());
assert_eq!(high_impact.event_type, NewsEventType::Earnings);
}
#[tokio::test]
async fn test_sentiment_score_validation() {
let valid_scores = vec![-1.0, -0.5, 0.0, 0.5, 1.0];
for score in valid_scores {
let event = SentimentEvent {
symbol: Symbol::from("TEST"),
sentiment_score: score,
bullish_ratio: 0.5,
bearish_ratio: 0.5,
sample_size: 10,
sources: vec!["benzinga".to_string()],
confidence: 0.8,
timestamp: Utc::now(),
source: "Benzinga".to_string(),
period: SentimentPeriod::Day1,
};
assert!(
event.sentiment_score >= -1.0 && event.sentiment_score <= 1.0,
"Sentiment score should be in valid range"
);
}
}
#[tokio::test]
async fn test_news_categorization() {
let categories = vec![
vec!["Technology".to_string()],
vec!["Healthcare".to_string(), "FDA".to_string()],
vec!["Finance".to_string(), "Earnings".to_string()],
vec!["General".to_string()],
];
for cats in categories {
let event = NewsEvent {
symbol: Some(Symbol::from("TEST")),
symbols: vec![Symbol::from("TEST")],
story_id: "test".to_string(),
headline: "Test headline".to_string(),
content: "Test content".to_string(),
summary: "".to_string(),
category: cats.first().cloned().unwrap_or_default(),
tags: cats.clone(),
impact_score: Some(0.0),
importance: 0.5,
author: "".to_string(),
timestamp: Utc::now(),
published_at: Utc::now(),
source: "Benzinga".to_string(),
url: "".to_string(),
sentiment_score: Some(0.0),
sentiment: Some(0.0),
event_type: NewsEventType::News,
};
assert_eq!(event.tags, cats);
assert!(!event.tags.is_empty());
}
}
#[tokio::test]
async fn test_historical_schema_support_news() {
let config = BenzingaConfig {
api_key: "test-key".to_string(),
..Default::default()
};
let provider = BenzingaHistoricalProvider::new(config).unwrap();
// BenzingaHistoricalProvider doesn't implement HistoricalProvider trait
// Schema support is handled by ProductionBenzingaHistoricalProvider
let range = TimeRange::last_days(1);
let symbols_str: Vec<&str> = vec!["AAPL"];
assert!(provider.get_news_events(Some(&symbols_str), range.start, range.end).await.is_ok());
}
#[tokio::test]
async fn test_historical_schema_support_sentiment() {
let config = BenzingaConfig {
api_key: "test-key".to_string(),
..Default::default()
};
let _provider = BenzingaHistoricalProvider::new(config).unwrap();
// BenzingaHistoricalProvider doesn't have a get_sentiment method
// Sentiment data is included in NewsEvent.sentiment and SentimentEvent
// This test removed as the method doesn't exist
}
#[tokio::test]
async fn test_historical_schema_unsupported_trades() {
let config = BenzingaConfig {
api_key: "test-key".to_string(),
..Default::default()
};
let _provider = BenzingaHistoricalProvider::new(config).unwrap();
// BenzingaHistoricalProvider doesn't support trade data - only news and sentiment
// This is expected behavior as Benzinga is a news/sentiment provider
}
#[tokio::test]
async fn test_time_range_for_news() {
let range = TimeRange::last_days(1);
assert!(range.end > range.start);
assert_eq!((range.end - range.start).num_hours(), 24);
}
#[tokio::test]
async fn test_news_event_with_url() {
let event = NewsEvent {
symbol: Some(Symbol::from("MSFT")),
symbols: vec![Symbol::from("MSFT")],
story_id: "news-456".to_string(),
headline: "Microsoft partnership announced".to_string(),
content: "Strategic partnership details".to_string(),
summary: "Strategic partnership details".to_string(),
category: "Technology".to_string(),
tags: vec!["Technology".to_string()],
impact_score: Some(0.65),
importance: 0.7,
author: "Reporter".to_string(),
timestamp: Utc::now(),
published_at: Utc::now(),
source: "Benzinga".to_string(),
url: "https://benzinga.com/news/456".to_string(),
sentiment_score: Some(0.7),
sentiment: Some(0.7),
event_type: NewsEventType::News,
};
assert!(!event.url.is_empty());
assert!(event.url.starts_with("https://"));
}
#[tokio::test]
async fn test_sentiment_confidence_levels() {
let confidence_levels = vec![0.5, 0.75, 0.9, 0.95, 0.99];
for confidence in confidence_levels {
let event = SentimentEvent {
symbol: Symbol::from("TEST"),
sentiment_score: 0.5,
bullish_ratio: 0.6,
bearish_ratio: 0.4,
sample_size: 20,
sources: vec!["benzinga".to_string()],
confidence,
timestamp: Utc::now(),
source: "Benzinga".to_string(),
period: SentimentPeriod::Day1,
};
assert!(
event.confidence >= 0.0 && event.confidence <= 1.0,
"Confidence should be valid probability"
);
}
}
#[tokio::test]
async fn test_volume_weighted_sentiment() {
let event1 = SentimentEvent {
symbol: Symbol::from("TEST"),
sentiment_score: 0.6,
bullish_ratio: 0.7,
bearish_ratio: 0.3,
sample_size: 25,
sources: vec!["benzinga".to_string()],
confidence: 0.8,
period: SentimentPeriod::Day1,
timestamp: Utc::now(),
source: "Benzinga".to_string(),
};
let event2 = SentimentEvent {
..event1.clone()
};
assert_eq!(event1.sentiment_score, event2.sentiment_score);
}
#[tokio::test]
async fn test_positive_negative_ratio_sum() {
let event = SentimentEvent {
symbol: Symbol::from("TEST"),
sentiment_score: 0.5,
bullish_ratio: 0.6,
bearish_ratio: 0.4,
sample_size: 100,
sources: vec!["benzinga".to_string()],
confidence: 0.8,
period: SentimentPeriod::Day1,
timestamp: Utc::now(),
source: "Benzinga".to_string(),
};
let sum = event.bullish_ratio + event.bearish_ratio;
assert!(
(sum - 1.0).abs() < 0.01,
"Bullish and bearish ratios should sum to ~1.0"
);
}
#[tokio::test]
async fn test_provider_name_consistency() {
let streaming_config = BenzingaStreamingConfig {
api_key: "test-key".to_string(),
..Default::default()
};
let historical_config = BenzingaConfig {
api_key: "test-key".to_string(),
..Default::default()
};
let streaming = BenzingaStreamingProvider::new(streaming_config).unwrap();
let historical = BenzingaHistoricalProvider::new(historical_config).unwrap();
// Provider name comparison removed - method not available
// assert_eq!(streaming.get_provider_name(), historical.get_provider_name());
}
#[tokio::test]
async fn test_error_handling_malformed_data() {
// Test that providers handle malformed data gracefully
let config = BenzingaConfig {
api_key: "test-key".to_string(),
..Default::default()
};
let provider = BenzingaHistoricalProvider::new(config);
assert!(provider.is_ok());
}
#[tokio::test]
async fn test_rate_limiting_configuration() {
let config = BenzingaStreamingConfig {
api_key: "test-key".to_string(),
..Default::default()
};
assert!(!config.api_key.is_empty());
}
#[tokio::test]
async fn test_caching_configuration() {
let config = BenzingaConfig {
api_key: "test-key".to_string(),
..Default::default()
};
assert!(!config.api_key.is_empty());
}
#[tokio::test]
async fn test_bulk_download_configuration() {
let config = BenzingaConfig {
api_key: "test-key".to_string(),
..Default::default()
};
assert!(!config.api_key.is_empty());
}
#[tokio::test]
async fn test_news_count_in_sentiment() {
let event = SentimentEvent {
symbol: Symbol::from("AAPL"),
sentiment_score: 0.65,
bullish_ratio: 0.7,
bearish_ratio: 0.3,
sample_size: 50,
sources: vec!["benzinga".to_string()],
confidence: 0.85,
period: SentimentPeriod::Day1,
timestamp: Utc::now(),
source: "Benzinga".to_string(),
};
assert_eq!(event.sample_size, 50);
assert!(event.sample_size > 0);
}
#[tokio::test]
async fn test_multiple_symbol_news_fetch() {
let config = BenzingaConfig {
api_key: "test-key".to_string(),
..Default::default()
};
let provider = BenzingaHistoricalProvider::new(config).unwrap();
let symbols_str: Vec<&str> = vec!["AAPL", "MSFT", "GOOGL"];
let range = TimeRange::last_days(1);
// Should support batch fetching
// fetch_batch method not available, using get_news_events instead
let result = provider.get_news_events(Some(&symbols_str), range.start, range.end).await;
assert!(result.is_ok() || result.is_err());
}
#[tokio::test]
async fn test_event_deduplication() {
// Test that identical events can be identified
let event1 = NewsEvent {
story_id: "news-789".to_string(),
symbol: Some(Symbol::from("TSLA")),
symbols: vec![Symbol::from("TSLA")],
headline: "Tesla updates".to_string(),
content: "Tesla updates content".to_string(),
summary: "".to_string(),
timestamp: Utc::now(),
published_at: Utc::now(),
source: "Benzinga".to_string(),
url: "".to_string(),
event_type: NewsEventType::News,
impact_score: Some(0.0),
category: "".to_string(),
tags: vec![],
importance: 0.0,
author: "".to_string(),
sentiment_score: Some(0.0),
sentiment: Some(0.0),
};
let event2 = event1.clone();
assert_eq!(event1.story_id, event2.story_id);
assert_eq!(event1.symbol, event2.symbol);
}