| |
| """ |
| Direct API Router - Complete REST Endpoints |
| All external API integrations exposed through REST endpoints |
| NO PIPELINES - Direct model loading and inference |
| """ |
|
|
| from fastapi import APIRouter, HTTPException, Query, Body |
| from fastapi.responses import JSONResponse |
| from typing import Optional, List, Dict, Any |
| from pydantic import BaseModel |
| from datetime import datetime |
| import logging |
|
|
| |
| from backend.services.direct_model_loader import direct_model_loader |
| from backend.services.dataset_loader import crypto_dataset_loader |
| from backend.services.external_api_clients import ( |
| alternative_me_client, |
| reddit_client, |
| rss_feed_client |
| ) |
| from backend.services.coingecko_client import coingecko_client |
| from backend.services.binance_client import binance_client |
| from backend.services.crypto_news_client import crypto_news_client |
|
|
| logger = logging.getLogger(__name__) |
|
|
| router = APIRouter( |
| prefix="/api/v1", |
| tags=["Direct API - External Services"] |
| ) |
|
|
|
|
| |
| |
| |
|
|
| class SentimentRequest(BaseModel): |
| """Sentiment analysis request""" |
| text: str |
| model_key: Optional[str] = "cryptobert_elkulako" |
|
|
|
|
| class BatchSentimentRequest(BaseModel): |
| """Batch sentiment analysis request""" |
| texts: List[str] |
| model_key: Optional[str] = "cryptobert_elkulako" |
|
|
|
|
| class DatasetQueryRequest(BaseModel): |
| """Dataset query request""" |
| dataset_key: str |
| filters: Optional[Dict[str, Any]] = None |
| limit: int = 100 |
|
|
|
|
| |
| |
| |
|
|
| @router.get("/coingecko/price") |
| async def get_coingecko_prices( |
| symbols: Optional[str] = Query(None, description="Comma-separated symbols (e.g., BTC,ETH)"), |
| limit: int = Query(100, description="Maximum number of coins") |
| ): |
| """ |
| Get real-time cryptocurrency prices from CoinGecko |
| |
| Examples: |
| - `/api/v1/coingecko/price?symbols=BTC,ETH` |
| - `/api/v1/coingecko/price?limit=50` |
| """ |
| try: |
| symbol_list = symbols.split(",") if symbols else None |
| result = await coingecko_client.get_market_prices( |
| symbols=symbol_list, |
| limit=limit |
| ) |
| |
| return { |
| "success": True, |
| "data": result, |
| "source": "coingecko", |
| "timestamp": datetime.utcnow().isoformat() |
| } |
| |
| except Exception as e: |
| logger.error(f"❌ CoinGecko price endpoint failed: {e}") |
| raise HTTPException(status_code=503, detail=str(e)) |
|
|
|
|
| @router.get("/coingecko/trending") |
| async def get_coingecko_trending( |
| limit: int = Query(10, description="Number of trending coins") |
| ): |
| """ |
| Get trending cryptocurrencies from CoinGecko |
| """ |
| try: |
| result = await coingecko_client.get_trending_coins(limit=limit) |
| |
| return { |
| "success": True, |
| "data": result, |
| "source": "coingecko", |
| "timestamp": datetime.utcnow().isoformat() |
| } |
| |
| except Exception as e: |
| logger.error(f"❌ CoinGecko trending endpoint failed: {e}") |
| raise HTTPException(status_code=503, detail=str(e)) |
|
|
|
|
| |
| |
| |
|
|
| @router.get("/binance/klines") |
| async def get_binance_klines( |
| symbol: str = Query(..., description="Symbol (e.g., BTC, BTCUSDT)"), |
| timeframe: str = Query("1h", description="Timeframe (1m, 5m, 15m, 1h, 4h, 1d)"), |
| limit: int = Query(1000, description="Number of candles (max 1000)") |
| ): |
| """ |
| Get OHLCV candlestick data from Binance |
| |
| Examples: |
| - `/api/v1/binance/klines?symbol=BTC&timeframe=1h&limit=100` |
| - `/api/v1/binance/klines?symbol=ETHUSDT&timeframe=4h&limit=500` |
| """ |
| try: |
| result = await binance_client.get_ohlcv( |
| symbol=symbol, |
| timeframe=timeframe, |
| limit=limit |
| ) |
| |
| return { |
| "success": True, |
| "data": result, |
| "source": "binance", |
| "symbol": symbol, |
| "timeframe": timeframe, |
| "count": len(result), |
| "timestamp": datetime.utcnow().isoformat() |
| } |
| |
| except Exception as e: |
| logger.error(f"❌ Binance klines endpoint failed: {e}") |
| raise HTTPException(status_code=503, detail=str(e)) |
|
|
|
|
| @router.get("/ohlcv/{symbol}") |
| async def get_ohlcv( |
| symbol: str, |
| interval: str = Query("1d", description="Interval: 1m, 5m, 15m, 1h, 4h, 1d"), |
| limit: int = Query(30, description="Number of candles") |
| ): |
| """ |
| Get OHLCV data for a cryptocurrency symbol |
| |
| This endpoint provides a unified interface for OHLCV data with automatic fallback. |
| Tries Binance first, then CoinGecko as fallback. |
| |
| Examples: |
| - `/api/v1/ohlcv/BTC?interval=1d&limit=30` |
| - `/api/v1/ohlcv/ETH?interval=1h&limit=100` |
| """ |
| try: |
| |
| try: |
| binance_symbol = f"{symbol.upper()}USDT" |
| result = await binance_client.get_ohlcv( |
| symbol=binance_symbol, |
| timeframe=interval, |
| limit=limit |
| ) |
| |
| return { |
| "success": True, |
| "symbol": symbol.upper(), |
| "interval": interval, |
| "data": result, |
| "source": "binance", |
| "count": len(result), |
| "timestamp": datetime.utcnow().isoformat() |
| } |
| except Exception as binance_error: |
| logger.warning(f"⚠ Binance failed for {symbol}: {binance_error}") |
| |
| |
| try: |
| coin_id = symbol.lower() |
| result = await coingecko_client.get_ohlc( |
| coin_id=coin_id, |
| days=30 if interval == "1d" else 7 |
| ) |
| |
| return { |
| "success": True, |
| "symbol": symbol.upper(), |
| "interval": interval, |
| "data": result, |
| "source": "coingecko", |
| "count": len(result), |
| "timestamp": datetime.utcnow().isoformat(), |
| "fallback_used": True |
| } |
| except Exception as coingecko_error: |
| logger.error(f"❌ Both Binance and CoinGecko failed for {symbol}") |
| raise HTTPException( |
| status_code=503, |
| detail=f"Failed to fetch OHLCV data: Binance error: {str(binance_error)}, CoinGecko error: {str(coingecko_error)}" |
| ) |
| |
| except HTTPException: |
| raise |
| except Exception as e: |
| logger.error(f"❌ OHLCV endpoint failed: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
| @router.get("/binance/ticker") |
| async def get_binance_ticker( |
| symbol: str = Query(..., description="Symbol (e.g., BTC)") |
| ): |
| """ |
| Get 24-hour ticker data from Binance |
| """ |
| try: |
| result = await binance_client.get_24h_ticker(symbol=symbol) |
| |
| return { |
| "success": True, |
| "data": result, |
| "source": "binance", |
| "timestamp": datetime.utcnow().isoformat() |
| } |
| |
| except Exception as e: |
| logger.error(f"❌ Binance ticker endpoint failed: {e}") |
| raise HTTPException(status_code=503, detail=str(e)) |
|
|
|
|
| |
| |
| |
|
|
| @router.get("/alternative/fng") |
| async def get_fear_greed_index( |
| limit: int = Query(1, description="Number of historical data points") |
| ): |
| """ |
| Get Fear & Greed Index from Alternative.me |
| |
| Examples: |
| - `/api/v1/alternative/fng` - Current index |
| - `/api/v1/alternative/fng?limit=30` - Last 30 days |
| """ |
| try: |
| result = await alternative_me_client.get_fear_greed_index(limit=limit) |
| |
| return result |
| |
| except Exception as e: |
| logger.error(f"❌ Alternative.me endpoint failed: {e}") |
| raise HTTPException(status_code=503, detail=str(e)) |
|
|
|
|
| |
| |
| |
|
|
| @router.get("/reddit/top") |
| async def get_reddit_top_posts( |
| subreddit: str = Query("cryptocurrency", description="Subreddit name"), |
| time_filter: str = Query("day", description="Time filter (hour, day, week, month)"), |
| limit: int = Query(25, description="Number of posts") |
| ): |
| """ |
| Get top posts from Reddit cryptocurrency subreddits |
| |
| Examples: |
| - `/api/v1/reddit/top?subreddit=cryptocurrency&time_filter=day&limit=25` |
| - `/api/v1/reddit/top?subreddit=bitcoin&time_filter=week&limit=50` |
| """ |
| try: |
| result = await reddit_client.get_top_posts( |
| subreddit=subreddit, |
| time_filter=time_filter, |
| limit=limit |
| ) |
| |
| return result |
| |
| except Exception as e: |
| logger.error(f"❌ Reddit endpoint failed: {e}") |
| raise HTTPException(status_code=503, detail=str(e)) |
|
|
|
|
| @router.get("/reddit/new") |
| async def get_reddit_new_posts( |
| subreddit: str = Query("cryptocurrency", description="Subreddit name"), |
| limit: int = Query(25, description="Number of posts") |
| ): |
| """ |
| Get new posts from Reddit cryptocurrency subreddits |
| """ |
| try: |
| result = await reddit_client.get_new_posts( |
| subreddit=subreddit, |
| limit=limit |
| ) |
| |
| return result |
| |
| except Exception as e: |
| logger.error(f"❌ Reddit endpoint failed: {e}") |
| raise HTTPException(status_code=503, detail=str(e)) |
|
|
|
|
| |
| |
| |
|
|
| @router.get("/rss/feed") |
| async def get_rss_feed( |
| feed_name: str = Query(..., description="Feed name (coindesk, cointelegraph, bitcoinmagazine, decrypt, theblock)"), |
| limit: int = Query(20, description="Number of articles") |
| ): |
| """ |
| Get news articles from RSS feeds |
| |
| Available feeds: coindesk, cointelegraph, bitcoinmagazine, decrypt, theblock |
| |
| Examples: |
| - `/api/v1/rss/feed?feed_name=coindesk&limit=20` |
| - `/api/v1/rss/feed?feed_name=cointelegraph&limit=10` |
| """ |
| try: |
| result = await rss_feed_client.fetch_feed( |
| feed_name=feed_name, |
| limit=limit |
| ) |
| |
| return result |
| |
| except Exception as e: |
| logger.error(f"❌ RSS feed endpoint failed: {e}") |
| raise HTTPException(status_code=503, detail=str(e)) |
|
|
|
|
| @router.get("/rss/all") |
| async def get_all_rss_feeds( |
| limit_per_feed: int = Query(10, description="Articles per feed") |
| ): |
| """ |
| Get news articles from all RSS feeds |
| """ |
| try: |
| result = await rss_feed_client.fetch_all_feeds( |
| limit_per_feed=limit_per_feed |
| ) |
| |
| return result |
| |
| except Exception as e: |
| logger.error(f"❌ RSS all feeds endpoint failed: {e}") |
| raise HTTPException(status_code=503, detail=str(e)) |
|
|
|
|
| @router.get("/coindesk/rss") |
| async def get_coindesk_rss( |
| limit: int = Query(20, description="Number of articles") |
| ): |
| """ |
| Get CoinDesk RSS feed |
| |
| Direct endpoint: https://www.coindesk.com/arc/outboundfeeds/rss/ |
| """ |
| try: |
| result = await rss_feed_client.fetch_feed("coindesk", limit) |
| return result |
| except Exception as e: |
| logger.error(f"❌ CoinDesk RSS failed: {e}") |
| raise HTTPException(status_code=503, detail=str(e)) |
|
|
|
|
| @router.get("/cointelegraph/rss") |
| async def get_cointelegraph_rss( |
| limit: int = Query(20, description="Number of articles") |
| ): |
| """ |
| Get CoinTelegraph RSS feed |
| |
| Direct endpoint: https://cointelegraph.com/rss |
| """ |
| try: |
| result = await rss_feed_client.fetch_feed("cointelegraph", limit) |
| return result |
| except Exception as e: |
| logger.error(f"❌ CoinTelegraph RSS failed: {e}") |
| raise HTTPException(status_code=503, detail=str(e)) |
|
|
|
|
| |
| |
| |
|
|
| @router.get("/news/latest") |
| async def get_latest_crypto_news( |
| limit: int = Query(20, description="Number of articles") |
| ): |
| """ |
| Get latest cryptocurrency news from multiple sources |
| (Aggregates NewsAPI, CryptoPanic, and RSS feeds) |
| """ |
| try: |
| result = await crypto_news_client.get_latest_news(limit=limit) |
| |
| return { |
| "success": True, |
| "data": result, |
| "count": len(result), |
| "source": "aggregated", |
| "timestamp": datetime.utcnow().isoformat() |
| } |
| |
| except Exception as e: |
| logger.error(f"❌ Crypto news endpoint failed: {e}") |
| raise HTTPException(status_code=503, detail=str(e)) |
|
|
|
|
| |
| |
| |
|
|
| @router.post("/hf/sentiment") |
| async def analyze_sentiment(request: SentimentRequest): |
| """ |
| Analyze sentiment using HuggingFace models with automatic fallback |
| |
| Available models (in fallback order): |
| - cryptobert_elkulako (default): ElKulako/cryptobert |
| - cryptobert_kk08: kk08/CryptoBERT |
| - finbert: ProsusAI/finbert |
| - twitter_sentiment: cardiffnlp/twitter-roberta-base-sentiment |
| |
| Example: |
| ```json |
| { |
| "text": "Bitcoin price is surging to new heights!", |
| "model_key": "cryptobert_elkulako" |
| } |
| ``` |
| """ |
| |
| fallback_models = [ |
| request.model_key, |
| "cryptobert_kk08", |
| "finbert", |
| "twitter_sentiment" |
| ] |
| |
| last_error = None |
| |
| for model_key in fallback_models: |
| try: |
| result = await direct_model_loader.predict_sentiment( |
| text=request.text, |
| model_key=model_key |
| ) |
| |
| |
| if model_key != request.model_key: |
| result["fallback_used"] = True |
| result["primary_model"] = request.model_key |
| result["actual_model"] = model_key |
| |
| return result |
| |
| except Exception as e: |
| logger.warning(f"⚠ Model {model_key} failed: {e}") |
| last_error = e |
| continue |
| |
| |
| logger.error(f"❌ All sentiment models failed. Last error: {last_error}") |
| raise HTTPException( |
| status_code=503, |
| detail={ |
| "error": "All sentiment models unavailable", |
| "message": "Sentiment analysis service is temporarily unavailable", |
| "tried_models": fallback_models, |
| "last_error": str(last_error), |
| "degraded_response": { |
| "sentiment": "neutral", |
| "score": 0.5, |
| "confidence": 0.0, |
| "method": "fallback", |
| "warning": "Using degraded mode - all models unavailable" |
| } |
| } |
| ) |
|
|
|
|
| @router.post("/hf/sentiment/batch") |
| async def analyze_sentiment_batch(request: BatchSentimentRequest): |
| """ |
| Batch sentiment analysis (NO PIPELINE) |
| |
| Example: |
| ```json |
| { |
| "texts": [ |
| "Bitcoin is mooning!", |
| "Ethereum looks bearish today", |
| "Market is neutral" |
| ], |
| "model_key": "cryptobert_elkulako" |
| } |
| ``` |
| """ |
| try: |
| result = await direct_model_loader.batch_predict_sentiment( |
| texts=request.texts, |
| model_key=request.model_key |
| ) |
| |
| return result |
| |
| except Exception as e: |
| logger.error(f"❌ Batch sentiment analysis failed: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
| @router.get("/hf/models") |
| async def get_loaded_models(): |
| """ |
| Get list of loaded HuggingFace models |
| """ |
| try: |
| result = direct_model_loader.get_loaded_models() |
| return result |
| |
| except Exception as e: |
| logger.error(f"❌ Get models failed: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
| @router.post("/hf/models/load") |
| async def load_model( |
| model_key: str = Query(..., description="Model key to load") |
| ): |
| """ |
| Load a specific HuggingFace model |
| |
| Available models: |
| - cryptobert_elkulako |
| - cryptobert_kk08 |
| - finbert |
| - twitter_sentiment |
| """ |
| try: |
| result = await direct_model_loader.load_model(model_key) |
| return result |
| |
| except Exception as e: |
| logger.error(f"❌ Load model failed: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
| @router.post("/hf/models/load-all") |
| async def load_all_models(): |
| """ |
| Load all configured HuggingFace models |
| """ |
| try: |
| result = await direct_model_loader.load_all_models() |
| return result |
| |
| except Exception as e: |
| logger.error(f"❌ Load all models failed: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
| |
| |
| |
|
|
| @router.get("/hf/datasets") |
| async def get_loaded_datasets(): |
| """ |
| Get list of loaded HuggingFace datasets |
| """ |
| try: |
| result = crypto_dataset_loader.get_loaded_datasets() |
| return result |
| |
| except Exception as e: |
| logger.error(f"❌ Get datasets failed: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
| @router.post("/hf/datasets/load") |
| async def load_dataset( |
| dataset_key: str = Query(..., description="Dataset key to load"), |
| split: Optional[str] = Query(None, description="Dataset split"), |
| streaming: bool = Query(False, description="Enable streaming") |
| ): |
| """ |
| Load a specific HuggingFace dataset |
| |
| Available datasets: |
| - cryptocoin: linxy/CryptoCoin |
| - bitcoin_btc_usdt: WinkingFace/CryptoLM-Bitcoin-BTC-USDT |
| - ethereum_eth_usdt: WinkingFace/CryptoLM-Ethereum-ETH-USDT |
| - solana_sol_usdt: WinkingFace/CryptoLM-Solana-SOL-USDT |
| - ripple_xrp_usdt: WinkingFace/CryptoLM-Ripple-XRP-USDT |
| """ |
| try: |
| result = await crypto_dataset_loader.load_dataset( |
| dataset_key=dataset_key, |
| split=split, |
| streaming=streaming |
| ) |
| return result |
| |
| except Exception as e: |
| logger.error(f"❌ Load dataset failed: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
| @router.post("/hf/datasets/load-all") |
| async def load_all_datasets( |
| streaming: bool = Query(False, description="Enable streaming") |
| ): |
| """ |
| Load all configured HuggingFace datasets |
| """ |
| try: |
| result = await crypto_dataset_loader.load_all_datasets(streaming=streaming) |
| return result |
| |
| except Exception as e: |
| logger.error(f"❌ Load all datasets failed: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
| @router.get("/hf/datasets/sample") |
| async def get_dataset_sample( |
| dataset_key: str = Query(..., description="Dataset key"), |
| num_samples: int = Query(10, description="Number of samples"), |
| split: Optional[str] = Query(None, description="Dataset split") |
| ): |
| """ |
| Get sample rows from a dataset |
| """ |
| try: |
| result = await crypto_dataset_loader.get_dataset_sample( |
| dataset_key=dataset_key, |
| num_samples=num_samples, |
| split=split |
| ) |
| return result |
| |
| except Exception as e: |
| logger.error(f"❌ Get dataset sample failed: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
| @router.post("/hf/datasets/query") |
| async def query_dataset(request: DatasetQueryRequest): |
| """ |
| Query dataset with filters |
| |
| Example: |
| ```json |
| { |
| "dataset_key": "bitcoin_btc_usdt", |
| "filters": {"price": 50000}, |
| "limit": 100 |
| } |
| ``` |
| """ |
| try: |
| result = await crypto_dataset_loader.query_dataset( |
| dataset_key=request.dataset_key, |
| filters=request.filters, |
| limit=request.limit |
| ) |
| return result |
| |
| except Exception as e: |
| logger.error(f"❌ Query dataset failed: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
| @router.get("/hf/datasets/stats") |
| async def get_dataset_stats( |
| dataset_key: str = Query(..., description="Dataset key") |
| ): |
| """ |
| Get statistics about a dataset |
| """ |
| try: |
| result = await crypto_dataset_loader.get_dataset_stats(dataset_key=dataset_key) |
| return result |
| |
| except Exception as e: |
| logger.error(f"❌ Get dataset stats failed: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
| |
| |
| |
|
|
| @router.get("/status") |
| async def get_system_status(): |
| """ |
| Get overall system status |
| """ |
| try: |
| models_info = direct_model_loader.get_loaded_models() |
| datasets_info = crypto_dataset_loader.get_loaded_datasets() |
| |
| return { |
| "success": True, |
| "status": "operational", |
| "models": { |
| "total_configured": models_info["total_configured"], |
| "total_loaded": models_info["total_loaded"], |
| "device": models_info["device"] |
| }, |
| "datasets": { |
| "total_configured": datasets_info["total_configured"], |
| "total_loaded": datasets_info["total_loaded"] |
| }, |
| "external_apis": { |
| "coingecko": "available", |
| "binance": "available", |
| "alternative_me": "available", |
| "reddit": "available", |
| "rss_feeds": "available" |
| }, |
| "timestamp": datetime.utcnow().isoformat() |
| } |
| |
| except Exception as e: |
| logger.error(f"❌ System status failed: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
| |
| __all__ = ["router"] |
|
|