308 lines
10 KiB
Python
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"
|