| | """
|
| | Data Access API Endpoints
|
| | Provides user-facing endpoints to access collected cryptocurrency data
|
| | """
|
| |
|
| | from datetime import datetime, timedelta
|
| | from typing import Optional, List
|
| | from fastapi import APIRouter, HTTPException, Query
|
| | from pydantic import BaseModel
|
| |
|
| | from database.db_manager import db_manager
|
| | from utils.logger import setup_logger
|
| |
|
| | logger = setup_logger("data_endpoints")
|
| |
|
| | router = APIRouter(prefix="/api/crypto", tags=["data"])
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | class PriceData(BaseModel):
|
| | """Price data model"""
|
| | symbol: str
|
| | price_usd: float
|
| | market_cap: Optional[float] = None
|
| | volume_24h: Optional[float] = None
|
| | price_change_24h: Optional[float] = None
|
| | timestamp: datetime
|
| | source: str
|
| |
|
| |
|
| | class NewsArticle(BaseModel):
|
| | """News article model"""
|
| | id: int
|
| | title: str
|
| | content: Optional[str] = None
|
| | source: str
|
| | url: Optional[str] = None
|
| | published_at: datetime
|
| | sentiment: Optional[str] = None
|
| | tags: Optional[List[str]] = None
|
| |
|
| |
|
| | class WhaleTransaction(BaseModel):
|
| | """Whale transaction model"""
|
| | id: int
|
| | blockchain: str
|
| | transaction_hash: str
|
| | from_address: str
|
| | to_address: str
|
| | amount: float
|
| | amount_usd: float
|
| | timestamp: datetime
|
| | source: str
|
| |
|
| |
|
| | class SentimentMetric(BaseModel):
|
| | """Sentiment metric model"""
|
| | metric_name: str
|
| | value: float
|
| | classification: str
|
| | timestamp: datetime
|
| | source: str
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | @router.get("/prices", response_model=List[PriceData])
|
| | async def get_all_prices(
|
| | limit: int = Query(default=100, ge=1, le=1000, description="Number of records to return")
|
| | ):
|
| | """
|
| | Get latest prices for all cryptocurrencies
|
| |
|
| | Returns the most recent price data for all tracked cryptocurrencies
|
| | """
|
| | try:
|
| | prices = db_manager.get_latest_prices(limit=limit)
|
| |
|
| | if not prices:
|
| | return []
|
| |
|
| | return [
|
| | PriceData(
|
| | symbol=p.symbol,
|
| | price_usd=p.price_usd,
|
| | market_cap=p.market_cap,
|
| | volume_24h=p.volume_24h,
|
| | price_change_24h=p.price_change_24h,
|
| | timestamp=p.timestamp,
|
| | source=p.source
|
| | )
|
| | for p in prices
|
| | ]
|
| |
|
| | except Exception as e:
|
| | logger.error(f"Error getting prices: {e}", exc_info=True)
|
| | raise HTTPException(status_code=500, detail=f"Failed to get prices: {str(e)}")
|
| |
|
| |
|
| | @router.get("/prices/{symbol}", response_model=PriceData)
|
| | async def get_price_by_symbol(symbol: str):
|
| | """
|
| | Get latest price for a specific cryptocurrency
|
| |
|
| | Args:
|
| | symbol: Cryptocurrency symbol (e.g., BTC, ETH, BNB)
|
| | """
|
| | try:
|
| | symbol = symbol.upper()
|
| | price = db_manager.get_latest_price_by_symbol(symbol)
|
| |
|
| | if not price:
|
| | raise HTTPException(status_code=404, detail=f"Price data not found for {symbol}")
|
| |
|
| | return PriceData(
|
| | symbol=price.symbol,
|
| | price_usd=price.price_usd,
|
| | market_cap=price.market_cap,
|
| | volume_24h=price.volume_24h,
|
| | price_change_24h=price.price_change_24h,
|
| | timestamp=price.timestamp,
|
| | source=price.source
|
| | )
|
| |
|
| | except HTTPException:
|
| | raise
|
| | except Exception as e:
|
| | logger.error(f"Error getting price for {symbol}: {e}", exc_info=True)
|
| | raise HTTPException(status_code=500, detail=f"Failed to get price: {str(e)}")
|
| |
|
| |
|
| | @router.get("/history/{symbol}")
|
| | async def get_price_history(
|
| | symbol: str,
|
| | hours: int = Query(default=24, ge=1, le=720, description="Number of hours of history"),
|
| | interval: int = Query(default=60, ge=1, le=1440, description="Interval in minutes")
|
| | ):
|
| | """
|
| | Get price history for a cryptocurrency
|
| |
|
| | Args:
|
| | symbol: Cryptocurrency symbol
|
| | hours: Number of hours of history to return
|
| | interval: Data point interval in minutes
|
| | """
|
| | try:
|
| | symbol = symbol.upper()
|
| | history = db_manager.get_price_history(symbol, hours=hours)
|
| |
|
| | if not history:
|
| | raise HTTPException(status_code=404, detail=f"No history found for {symbol}")
|
| |
|
| |
|
| | sampled = []
|
| | last_time = None
|
| |
|
| | for record in history:
|
| | if last_time is None or (record.timestamp - last_time).total_seconds() >= interval * 60:
|
| | sampled.append({
|
| | "timestamp": record.timestamp.isoformat(),
|
| | "price_usd": record.price_usd,
|
| | "volume_24h": record.volume_24h,
|
| | "market_cap": record.market_cap
|
| | })
|
| | last_time = record.timestamp
|
| |
|
| | return {
|
| | "symbol": symbol,
|
| | "data_points": len(sampled),
|
| | "interval_minutes": interval,
|
| | "history": sampled
|
| | }
|
| |
|
| | except HTTPException:
|
| | raise
|
| | except Exception as e:
|
| | logger.error(f"Error getting history for {symbol}: {e}", exc_info=True)
|
| | raise HTTPException(status_code=500, detail=f"Failed to get history: {str(e)}")
|
| |
|
| |
|
| | @router.get("/market-overview")
|
| | async def get_market_overview():
|
| | """
|
| | Get market overview with top cryptocurrencies
|
| | """
|
| | try:
|
| | prices = db_manager.get_latest_prices(limit=20)
|
| |
|
| | if not prices:
|
| | return {
|
| | "total_market_cap": 0,
|
| | "total_volume_24h": 0,
|
| | "top_gainers": [],
|
| | "top_losers": [],
|
| | "top_by_market_cap": []
|
| | }
|
| |
|
| |
|
| | total_market_cap = sum(p.market_cap for p in prices if p.market_cap)
|
| | total_volume_24h = sum(p.volume_24h for p in prices if p.volume_24h)
|
| |
|
| |
|
| | sorted_by_change = sorted(
|
| | [p for p in prices if p.price_change_24h is not None],
|
| | key=lambda x: x.price_change_24h,
|
| | reverse=True
|
| | )
|
| |
|
| |
|
| | sorted_by_mcap = sorted(
|
| | [p for p in prices if p.market_cap is not None],
|
| | key=lambda x: x.market_cap,
|
| | reverse=True
|
| | )
|
| |
|
| | return {
|
| | "total_market_cap": total_market_cap,
|
| | "total_volume_24h": total_volume_24h,
|
| | "top_gainers": [
|
| | {
|
| | "symbol": p.symbol,
|
| | "price_usd": p.price_usd,
|
| | "price_change_24h": p.price_change_24h
|
| | }
|
| | for p in sorted_by_change[:5]
|
| | ],
|
| | "top_losers": [
|
| | {
|
| | "symbol": p.symbol,
|
| | "price_usd": p.price_usd,
|
| | "price_change_24h": p.price_change_24h
|
| | }
|
| | for p in sorted_by_change[-5:]
|
| | ],
|
| | "top_by_market_cap": [
|
| | {
|
| | "symbol": p.symbol,
|
| | "price_usd": p.price_usd,
|
| | "market_cap": p.market_cap,
|
| | "volume_24h": p.volume_24h
|
| | }
|
| | for p in sorted_by_mcap[:10]
|
| | ],
|
| | "timestamp": datetime.utcnow().isoformat()
|
| | }
|
| |
|
| | except Exception as e:
|
| | logger.error(f"Error getting market overview: {e}", exc_info=True)
|
| | raise HTTPException(status_code=500, detail=f"Failed to get market overview: {str(e)}")
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | @router.get("/news", response_model=List[NewsArticle])
|
| | async def get_latest_news(
|
| | limit: int = Query(default=50, ge=1, le=200, description="Number of articles"),
|
| | source: Optional[str] = Query(default=None, description="Filter by source"),
|
| | sentiment: Optional[str] = Query(default=None, description="Filter by sentiment")
|
| | ):
|
| | """
|
| | Get latest cryptocurrency news
|
| |
|
| | Args:
|
| | limit: Maximum number of articles to return
|
| | source: Filter by news source
|
| | sentiment: Filter by sentiment (positive, negative, neutral)
|
| | """
|
| | try:
|
| | news = db_manager.get_latest_news(
|
| | limit=limit,
|
| | source=source,
|
| | sentiment=sentiment
|
| | )
|
| |
|
| | if not news:
|
| | return []
|
| |
|
| | return [
|
| | NewsArticle(
|
| | id=article.id,
|
| | title=article.title,
|
| | content=article.content,
|
| | source=article.source,
|
| | url=article.url,
|
| | published_at=article.published_at,
|
| | sentiment=article.sentiment,
|
| | tags=article.tags.split(',') if article.tags else None
|
| | )
|
| | for article in news
|
| | ]
|
| |
|
| | except Exception as e:
|
| | logger.error(f"Error getting news: {e}", exc_info=True)
|
| | raise HTTPException(status_code=500, detail=f"Failed to get news: {str(e)}")
|
| |
|
| |
|
| | @router.get("/news/{news_id}", response_model=NewsArticle)
|
| | async def get_news_by_id(news_id: int):
|
| | """
|
| | Get a specific news article by ID
|
| | """
|
| | try:
|
| | article = db_manager.get_news_by_id(news_id)
|
| |
|
| | if not article:
|
| | raise HTTPException(status_code=404, detail=f"News article {news_id} not found")
|
| |
|
| | return NewsArticle(
|
| | id=article.id,
|
| | title=article.title,
|
| | content=article.content,
|
| | source=article.source,
|
| | url=article.url,
|
| | published_at=article.published_at,
|
| | sentiment=article.sentiment,
|
| | tags=article.tags.split(',') if article.tags else None
|
| | )
|
| |
|
| | except HTTPException:
|
| | raise
|
| | except Exception as e:
|
| | logger.error(f"Error getting news {news_id}: {e}", exc_info=True)
|
| | raise HTTPException(status_code=500, detail=f"Failed to get news: {str(e)}")
|
| |
|
| |
|
| | @router.get("/news/search")
|
| | async def search_news(
|
| | q: str = Query(..., min_length=2, description="Search query"),
|
| | limit: int = Query(default=50, ge=1, le=200)
|
| | ):
|
| | """
|
| | Search news articles by keyword
|
| |
|
| | Args:
|
| | q: Search query
|
| | limit: Maximum number of results
|
| | """
|
| | try:
|
| | results = db_manager.search_news(query=q, limit=limit)
|
| |
|
| | return {
|
| | "query": q,
|
| | "count": len(results),
|
| | "results": [
|
| | {
|
| | "id": article.id,
|
| | "title": article.title,
|
| | "source": article.source,
|
| | "url": article.url,
|
| | "published_at": article.published_at.isoformat(),
|
| | "sentiment": article.sentiment
|
| | }
|
| | for article in results
|
| | ]
|
| | }
|
| |
|
| | except Exception as e:
|
| | logger.error(f"Error searching news: {e}", exc_info=True)
|
| | raise HTTPException(status_code=500, detail=f"Failed to search news: {str(e)}")
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | @router.get("/sentiment/current")
|
| | async def get_current_sentiment():
|
| | """
|
| | Get current market sentiment metrics
|
| | """
|
| | try:
|
| | sentiment = db_manager.get_latest_sentiment()
|
| |
|
| | if not sentiment:
|
| | return {
|
| | "fear_greed_index": None,
|
| | "classification": "unknown",
|
| | "timestamp": None,
|
| | "message": "No sentiment data available"
|
| | }
|
| |
|
| | return {
|
| | "fear_greed_index": sentiment.value,
|
| | "classification": sentiment.classification,
|
| | "timestamp": sentiment.timestamp.isoformat(),
|
| | "source": sentiment.source,
|
| | "description": _get_sentiment_description(sentiment.classification)
|
| | }
|
| |
|
| | except Exception as e:
|
| | logger.error(f"Error getting sentiment: {e}", exc_info=True)
|
| | raise HTTPException(status_code=500, detail=f"Failed to get sentiment: {str(e)}")
|
| |
|
| |
|
| | @router.get("/sentiment/history")
|
| | async def get_sentiment_history(
|
| | hours: int = Query(default=168, ge=1, le=720, description="Hours of history (default: 7 days)")
|
| | ):
|
| | """
|
| | Get sentiment history
|
| | """
|
| | try:
|
| | history = db_manager.get_sentiment_history(hours=hours)
|
| |
|
| | return {
|
| | "data_points": len(history),
|
| | "history": [
|
| | {
|
| | "timestamp": record.timestamp.isoformat(),
|
| | "value": record.value,
|
| | "classification": record.classification
|
| | }
|
| | for record in history
|
| | ]
|
| | }
|
| |
|
| | except Exception as e:
|
| | logger.error(f"Error getting sentiment history: {e}", exc_info=True)
|
| | raise HTTPException(status_code=500, detail=f"Failed to get sentiment history: {str(e)}")
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | @router.get("/whales/transactions", response_model=List[WhaleTransaction])
|
| | async def get_whale_transactions(
|
| | limit: int = Query(default=50, ge=1, le=200),
|
| | blockchain: Optional[str] = Query(default=None, description="Filter by blockchain"),
|
| | min_amount_usd: Optional[float] = Query(default=None, ge=0, description="Minimum transaction amount in USD")
|
| | ):
|
| | """
|
| | Get recent large cryptocurrency transactions (whale movements)
|
| |
|
| | Args:
|
| | limit: Maximum number of transactions
|
| | blockchain: Filter by blockchain (ethereum, bitcoin, etc.)
|
| | min_amount_usd: Minimum transaction amount in USD
|
| | """
|
| | try:
|
| | transactions = db_manager.get_whale_transactions(
|
| | limit=limit,
|
| | blockchain=blockchain,
|
| | min_amount_usd=min_amount_usd
|
| | )
|
| |
|
| | if not transactions:
|
| | return []
|
| |
|
| | return [
|
| | WhaleTransaction(
|
| | id=tx.id,
|
| | blockchain=tx.blockchain,
|
| | transaction_hash=tx.transaction_hash,
|
| | from_address=tx.from_address,
|
| | to_address=tx.to_address,
|
| | amount=tx.amount,
|
| | amount_usd=tx.amount_usd,
|
| | timestamp=tx.timestamp,
|
| | source=tx.source
|
| | )
|
| | for tx in transactions
|
| | ]
|
| |
|
| | except Exception as e:
|
| | logger.error(f"Error getting whale transactions: {e}", exc_info=True)
|
| | raise HTTPException(status_code=500, detail=f"Failed to get whale transactions: {str(e)}")
|
| |
|
| |
|
| | @router.get("/whales/stats")
|
| | async def get_whale_stats(
|
| | hours: int = Query(default=24, ge=1, le=168, description="Time period in hours")
|
| | ):
|
| | """
|
| | Get whale activity statistics
|
| | """
|
| | try:
|
| | stats = db_manager.get_whale_stats(hours=hours)
|
| |
|
| | return {
|
| | "period_hours": hours,
|
| | "total_transactions": stats.get('total_transactions', 0),
|
| | "total_volume_usd": stats.get('total_volume_usd', 0),
|
| | "avg_transaction_usd": stats.get('avg_transaction_usd', 0),
|
| | "largest_transaction_usd": stats.get('largest_transaction_usd', 0),
|
| | "by_blockchain": stats.get('by_blockchain', {}),
|
| | "timestamp": datetime.utcnow().isoformat()
|
| | }
|
| |
|
| | except Exception as e:
|
| | logger.error(f"Error getting whale stats: {e}", exc_info=True)
|
| | raise HTTPException(status_code=500, detail=f"Failed to get whale stats: {str(e)}")
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | @router.get("/blockchain/gas")
|
| | async def get_gas_prices():
|
| | """
|
| | Get current gas prices for various blockchains
|
| | """
|
| | try:
|
| | gas_prices = db_manager.get_latest_gas_prices()
|
| |
|
| | return {
|
| | "ethereum": gas_prices.get('ethereum', {}),
|
| | "bsc": gas_prices.get('bsc', {}),
|
| | "polygon": gas_prices.get('polygon', {}),
|
| | "timestamp": datetime.utcnow().isoformat()
|
| | }
|
| |
|
| | except Exception as e:
|
| | logger.error(f"Error getting gas prices: {e}", exc_info=True)
|
| | raise HTTPException(status_code=500, detail=f"Failed to get gas prices: {str(e)}")
|
| |
|
| |
|
| | @router.get("/blockchain/stats")
|
| | async def get_blockchain_stats():
|
| | """
|
| | Get blockchain statistics
|
| | """
|
| | try:
|
| | stats = db_manager.get_blockchain_stats()
|
| |
|
| | return {
|
| | "ethereum": stats.get('ethereum', {}),
|
| | "bitcoin": stats.get('bitcoin', {}),
|
| | "bsc": stats.get('bsc', {}),
|
| | "timestamp": datetime.utcnow().isoformat()
|
| | }
|
| |
|
| | except Exception as e:
|
| | logger.error(f"Error getting blockchain stats: {e}", exc_info=True)
|
| | raise HTTPException(status_code=500, detail=f"Failed to get blockchain stats: {str(e)}")
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | def _get_sentiment_description(classification: str) -> str:
|
| | """Get human-readable description for sentiment classification"""
|
| | descriptions = {
|
| | "extreme_fear": "Extreme Fear - Investors are very worried",
|
| | "fear": "Fear - Investors are concerned",
|
| | "neutral": "Neutral - Market is balanced",
|
| | "greed": "Greed - Investors are getting greedy",
|
| | "extreme_greed": "Extreme Greed - Market may be overheated"
|
| | }
|
| | return descriptions.get(classification, "Unknown sentiment")
|
| |
|
| |
|