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