You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

278 lines
9.9 KiB
Python

"""
News and Social Media API endpoints for ticker-based sentiment analysis
"""
from datetime import datetime
from typing import Optional, Dict, Any, List
import logging
from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel, Field
from app.services.news_social_service import news_social_service
logger = logging.getLogger(__name__)
router = APIRouter()
class NewsArticleSchema(BaseModel):
"""News article schema for API response"""
title: str
summary: Optional[str] = None
content: Optional[str] = None
url: str
source: str
published_at: Optional[str] = None
author: Optional[str] = None
relevance_score: Optional[float] = None
image_url: Optional[str] = None
tags: List[str] = []
class SocialPostSchema(BaseModel):
"""Social media post schema for API response"""
title: str
content: str
url: str
platform: str
author: str
published_at: Optional[str] = None
score: Optional[int] = None
comments_count: Optional[int] = None
upvotes: Optional[int] = None
downvotes: Optional[int] = None
subreddit: Optional[str] = None
class NewsSourcesSchema(BaseModel):
"""News sources breakdown"""
yahoo_finance: int = 0
newsapi: int = 0
class SocialPlatformsSchema(BaseModel):
"""Social media platforms breakdown"""
reddit: int = 0
class NewsSocialSummarySchema(BaseModel):
"""Summary of news and social data"""
total_items: int
time_range_days: int
oldest_item: Optional[str] = None
newest_item: Optional[str] = None
class NewsSocialResponse(BaseModel):
"""Complete response for ticker news and social data"""
ticker: str
retrieved_at: str
news: Dict[str, Any] = Field(description="News articles and sources breakdown")
social_media: Dict[str, Any] = Field(description="Social media posts and platforms breakdown")
summary: NewsSocialSummarySchema
@router.get("/{ticker}", response_model=NewsSocialResponse)
async def get_ticker_news_and_social(
ticker: str,
days_back: int = Query(7, ge=1, le=30, description="Number of days to look back for articles (1-30)"),
max_articles: int = Query(20, ge=1, le=100, description="Maximum number of news articles to return (1-100)"),
max_social_posts: int = Query(15, ge=0, le=50, description="Maximum number of social media posts to return (0-50)"),
include_social: bool = Query(True, description="Whether to include social media data")
):
"""
Get comprehensive news and social media data for a ticker
- **ticker**: Stock ticker symbol (e.g., AAPL, TSLA, QQQ)
- **days_back**: Number of days to look back for articles (default: 7, max: 30)
- **max_articles**: Maximum number of news articles to return (default: 20, min: 1, max: 100)
- **max_social_posts**: Maximum number of social media posts to return (default: 15, max: 50)
- **include_social**: Whether to include social media data (default: true)
## Data Sources
- **News**: Yahoo Finance, NewsAPI
- **Social Media**: Reddit (multiple investing subreddits)
## Features
- ✅ Parallel data fetching from multiple sources
- ✅ Automatic deduplication and relevance ranking
- ✅ Rate limiting and error handling
- ✅ Comprehensive metadata and source attribution
## Use Cases
- Sentiment analysis and market research
- News aggregation for trading decisions
- Social media monitoring for retail sentiment
- Research and fundamental analysis support
"""
try:
# Validate ticker format
ticker_upper = ticker.upper().strip()
if not ticker_upper or len(ticker_upper) > 10:
raise HTTPException(
status_code=400,
detail=f"Invalid ticker format: {ticker}. Must be 1-10 characters."
)
logger.info(f"Fetching news and social data for {ticker_upper}")
# Get data from service
result = await news_social_service.get_ticker_news_and_social(
ticker=ticker_upper,
days_back=days_back,
max_articles=max_articles,
max_social_posts=max_social_posts,
include_social=include_social
)
logger.info(f"Successfully retrieved {result['news']['total_articles']} articles and {result['social_media']['total_posts']} social posts for {ticker_upper}")
return result
except ValueError as e:
logger.error(f"Invalid input for {ticker}: {e}")
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"Error fetching news and social data for {ticker}: {e}")
raise HTTPException(
status_code=500,
detail=f"Failed to retrieve news and social data for {ticker}. Please try again later."
)
@router.get("/{ticker}/news-only", response_model=Dict[str, Any])
async def get_ticker_news_only(
ticker: str,
days_back: int = Query(7, ge=1, le=30, description="Number of days to look back for articles (1-30)"),
max_articles: int = Query(30, ge=1, le=100, description="Maximum number of news articles to return (1-100)")
):
"""
Get only news articles for a ticker (faster endpoint without social media data)
- **ticker**: Stock ticker symbol (e.g., AAPL, TSLA, QQQ)
- **days_back**: Number of days to look back for articles (default: 7, max: 30)
- **max_articles**: Maximum number of news articles to return (default: 30, min: 1, max: 100)
## Performance
- ⚡ Faster response time (no social media API calls)
- ⚡ Optimized for high-frequency news monitoring
- ⚡ Ideal for news-only sentiment analysis
"""
try:
ticker_upper = ticker.upper().strip()
if not ticker_upper or len(ticker_upper) > 10:
raise HTTPException(
status_code=400,
detail=f"Invalid ticker format: {ticker}. Must be 1-10 characters."
)
logger.info(f"Fetching news-only data for {ticker_upper}")
# Get data with social media disabled
result = await news_social_service.get_ticker_news_and_social(
ticker=ticker_upper,
days_back=days_back,
max_articles=max_articles,
max_social_posts=0,
include_social=False
)
# Return only news portion
news_only_result = {
"ticker": result["ticker"],
"retrieved_at": result["retrieved_at"],
"news": result["news"],
"summary": {
"total_articles": result["news"]["total_articles"],
"time_range_days": days_back,
"sources": result["news"]["sources"]
}
}
logger.info(f"Successfully retrieved {result['news']['total_articles']} articles for {ticker_upper}")
return news_only_result
except ValueError as e:
logger.error(f"Invalid input for {ticker}: {e}")
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"Error fetching news for {ticker}: {e}")
raise HTTPException(
status_code=500,
detail=f"Failed to retrieve news for {ticker}. Please try again later."
)
@router.get("/{ticker}/social-only", response_model=Dict[str, Any])
async def get_ticker_social_only(
ticker: str,
days_back: int = Query(7, ge=1, le=30, description="Number of days to look back for posts (1-30)"),
max_social_posts: int = Query(20, ge=1, le=50, description="Maximum number of social media posts to return (1-50)")
):
"""
Get only social media posts for a ticker
- **ticker**: Stock ticker symbol (e.g., AAPL, TSLA, QQQ)
- **days_back**: Number of days to look back for posts (default: 7, max: 30)
- **max_social_posts**: Maximum number of social media posts to return (default: 20, min: 1, max: 50)
## Social Media Sources
- Reddit: r/stocks, r/investing, r/SecurityAnalysis, r/StockMarket, r/ValueInvesting, r/financialindependence, r/wallstreetbets
## Use Cases
- Retail investor sentiment monitoring
- Social media trend analysis
- Community discussion tracking
"""
try:
ticker_upper = ticker.upper().strip()
if not ticker_upper or len(ticker_upper) > 10:
raise HTTPException(
status_code=400,
detail=f"Invalid ticker format: {ticker}. Must be 1-10 characters."
)
logger.info(f"Fetching social-only data for {ticker_upper}")
# Get data with minimal news articles
result = await news_social_service.get_ticker_news_and_social(
ticker=ticker_upper,
days_back=days_back,
max_articles=0, # Minimal news data
max_social_posts=max_social_posts,
include_social=True
)
# Return only social media portion
social_only_result = {
"ticker": result["ticker"],
"retrieved_at": result["retrieved_at"],
"social_media": result["social_media"],
"summary": {
"total_posts": result["social_media"]["total_posts"],
"time_range_days": days_back,
"platforms": result["social_media"]["platforms"]
}
}
logger.info(f"Successfully retrieved {result['social_media']['total_posts']} social posts for {ticker_upper}")
return social_only_result
except ValueError as e:
logger.error(f"Invalid input for {ticker}: {e}")
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"Error fetching social media data for {ticker}: {e}")
raise HTTPException(
status_code=500,
detail=f"Failed to retrieve social media data for {ticker}. Please try again later."
)