Files
ljwx-stock/backend/app/routes/sentiment_analysis.py
T
2025-11-11 04:57:54 +01:00

308 lines
10 KiB
Python

"""
Sentiment Analysis Routes
Endpoints for market sentiment, announcements, and sentiment-based recommendations
"""
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from typing import Dict, List, Optional
import logging
from app.config import get_settings
from app.modules.sentiment_fetcher import SentimentFetcher
from app.modules.announcement_crawler import AnnouncementCrawler
from app.modules.cache_manager import CacheManager
router = APIRouter(prefix="/api/sentiment", tags=["Sentiment Analysis"])
settings = get_settings()
logger = logging.getLogger(__name__)
# Initialize modules
_sentiment_fetcher: Optional[SentimentFetcher] = None
_announcement_crawler: Optional[AnnouncementCrawler] = None
_cache_manager = CacheManager()
def get_sentiment_fetcher() -> Optional[SentimentFetcher]:
"""Get or initialize sentiment fetcher"""
global _sentiment_fetcher
if _sentiment_fetcher is None:
if not settings.tushare_token:
logger.warning("⚠️ TuShare token not configured")
return None
try:
_sentiment_fetcher = SentimentFetcher(
settings.tushare_token,
_cache_manager
)
except Exception as e:
logger.error(f"❌ Failed to initialize SentimentFetcher: {e}")
return None
return _sentiment_fetcher
def get_announcement_crawler() -> Optional[AnnouncementCrawler]:
"""Get or initialize announcement crawler"""
global _announcement_crawler
if _announcement_crawler is None:
if not settings.tushare_token:
logger.warning("⚠️ TuShare token not configured")
return None
try:
_announcement_crawler = AnnouncementCrawler(
settings.tushare_token,
_cache_manager
)
except Exception as e:
logger.error(f"❌ Failed to initialize AnnouncementCrawler: {e}")
return None
return _announcement_crawler
class SentimentRequest(BaseModel):
"""Sentiment request model"""
symbol: Optional[str] = None
sentiment_type: str = "aggregate" # aggregate, investor, news, breadth
@router.get("/market")
async def get_market_sentiment() -> Dict:
"""Get overall market sentiment"""
try:
fetcher = get_sentiment_fetcher()
if not fetcher:
raise HTTPException(status_code=503, detail="Sentiment fetcher not available")
sentiment = fetcher.get_aggregate_sentiment()
return {
"status": "success",
"data": sentiment,
"timestamp": __import__("datetime").datetime.now().isoformat()
}
except Exception as e:
logger.error(f"❌ Error fetching market sentiment: {e}")
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
@router.get("/stock/{symbol}")
async def get_stock_sentiment(symbol: str) -> Dict:
"""Get sentiment for a specific stock"""
try:
fetcher = get_sentiment_fetcher()
if not fetcher:
raise HTTPException(status_code=503, detail="Sentiment fetcher not available")
sentiment = fetcher.get_aggregate_sentiment(symbol)
news = fetcher.get_news_sentiment(symbol)
return {
"status": "success",
"symbol": symbol,
"overall_sentiment": sentiment,
"news_sentiment": news,
"timestamp": __import__("datetime").datetime.now().isoformat()
}
except Exception as e:
logger.error(f"❌ Error fetching stock sentiment: {e}")
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
@router.get("/sectors")
async def get_sector_sentiment() -> Dict:
"""Get sentiment by sector"""
try:
fetcher = get_sentiment_fetcher()
if not fetcher:
raise HTTPException(status_code=503, detail="Sentiment fetcher not available")
sectors = fetcher.get_sector_sentiment()
return {
"status": "success",
"data": sectors,
"timestamp": __import__("datetime").datetime.now().isoformat()
}
except Exception as e:
logger.error(f"❌ Error fetching sector sentiment: {e}")
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
@router.get("/breadth")
async def get_market_breadth() -> Dict:
"""Get market breadth indicators"""
try:
fetcher = get_sentiment_fetcher()
if not fetcher:
raise HTTPException(status_code=503, detail="Sentiment fetcher not available")
breadth = fetcher.get_market_breadth()
return {
"status": "success",
"data": breadth,
"timestamp": __import__("datetime").datetime.now().isoformat()
}
except Exception as e:
logger.error(f"❌ Error fetching market breadth: {e}")
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
@router.get("/announcements/{symbol}")
async def get_announcements(symbol: str, limit: int = 20) -> Dict:
"""Get recent announcements for a stock"""
try:
crawler = get_announcement_crawler()
if not crawler:
raise HTTPException(status_code=503, detail="Announcement crawler not available")
announcements = crawler.get_recent_announcements(symbol, limit)
return {
"status": "success",
"symbol": symbol,
"announcements": announcements,
"total": len(announcements),
"timestamp": __import__("datetime").datetime.now().isoformat()
}
except Exception as e:
logger.error(f"❌ Error fetching announcements: {e}")
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
@router.get("/announcements/{symbol}/material")
async def get_material_announcements(symbol: str) -> Dict:
"""Get material announcements for a stock"""
try:
crawler = get_announcement_crawler()
if not crawler:
raise HTTPException(status_code=503, detail="Announcement crawler not available")
announcements = crawler.get_material_announcements(symbol)
return {
"status": "success",
"symbol": symbol,
"material_announcements": announcements,
"total": len(announcements),
"timestamp": __import__("datetime").datetime.now().isoformat()
}
except Exception as e:
logger.error(f"❌ Error fetching material announcements: {e}")
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
@router.get("/announcements/{symbol}/impact")
async def get_announcement_impact(symbol: str) -> Dict:
"""Get impact analysis of recent announcements"""
try:
crawler = get_announcement_crawler()
if not crawler:
raise HTTPException(status_code=503, detail="Announcement crawler not available")
impact = crawler.get_announcement_impact_analysis(symbol)
return {
"status": "success",
"symbol": symbol,
"impact_analysis": impact,
"timestamp": __import__("datetime").datetime.now().isoformat()
}
except Exception as e:
logger.error(f"❌ Error analyzing announcement impact: {e}")
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
@router.post("/combine")
async def get_combined_analysis(request: SentimentRequest) -> Dict:
"""
Get combined sentiment and fundamental analysis
This endpoint combines:
- Sentiment analysis
- Announcement monitoring
- Recommendation signals
"""
try:
fetcher = get_sentiment_fetcher()
crawler = get_announcement_crawler()
if not fetcher or not crawler:
raise HTTPException(status_code=503, detail="Required modules not available")
# Get sentiment
sentiment = fetcher.get_aggregate_sentiment(request.symbol)
# Get announcements
announcements = crawler.get_recent_announcements(request.symbol, limit=5)
impact = crawler.get_announcement_impact_analysis(request.symbol)
# Combine for recommendation
recommendation = {
'symbol': request.symbol,
'sentiment_score': sentiment.get('aggregate_score', 0),
'sentiment_label': sentiment.get('overall_sentiment', 'unknown'),
'announcement_impact': impact.get('recent_impact', 'neutral'),
'announcement_count': len(announcements),
'material_count': sum(1 for ann in announcements if ann.get('importance_score', 0) >= 0.7),
'combined_signal': self._get_combined_signal(sentiment, impact),
'confidence': 0.75,
'recommendation': 'buy' if sentiment.get('aggregate_score', 0) > 0.3 else 'hold' if sentiment.get('aggregate_score', 0) > -0.3 else 'sell'
}
return {
"status": "success",
"analysis": recommendation,
"sentiment": sentiment,
"announcements": {
'total': len(announcements),
'recent': announcements[:3],
'impact': impact
},
"timestamp": __import__("datetime").datetime.now().isoformat()
}
except Exception as e:
logger.error(f"❌ Error in combined analysis: {e}")
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
@router.get("/health")
async def health_check() -> Dict:
"""Health check for sentiment analysis"""
fetcher = get_sentiment_fetcher()
crawler = get_announcement_crawler()
return {
"status": "healthy",
"components": {
"sentiment_fetcher": "initialized" if fetcher else "not available",
"announcement_crawler": "initialized" if crawler else "not available",
},
"features": [
"market_sentiment",
"stock_sentiment",
"sector_analysis",
"announcement_monitoring",
"impact_analysis"
],
"timestamp": __import__("datetime").datetime.now().isoformat()
}
def _get_combined_signal(sentiment: Dict, impact: Dict) -> str:
"""Get combined trading signal"""
sentiment_score = sentiment.get('aggregate_score', 0)
impact_score = impact.get('impact_score', 0)
combined = (sentiment_score * 0.6) + (impact_score * 0.4)
if combined > 0.5:
return "strong_buy"
elif combined > 0.2:
return "buy"
elif combined < -0.5:
return "strong_sell"
elif combined < -0.2:
return "sell"
else:
return "hold"