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