|
|
|
|
|
""" |
|
|
Data Hub Complete - مدیریت جامع همه منابع داده |
|
|
============================================= |
|
|
✅ استفاده از تمام کلیدهای API جدید |
|
|
✅ پشتیبانی از همه انواع دادهها |
|
|
✅ سیستم Fallback خودکار |
|
|
✅ Cache Management |
|
|
✅ Rate Limiting |
|
|
""" |
|
|
|
|
|
import httpx |
|
|
import asyncio |
|
|
import logging |
|
|
from typing import Dict, Any, List, Optional, Union |
|
|
from datetime import datetime, timedelta |
|
|
import hashlib |
|
|
import json |
|
|
import os |
|
|
from collections import defaultdict |
|
|
import time |
|
|
|
|
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
|
|
|
class DataHubConfiguration: |
|
|
"""پیکربندی کامل Data Hub با تمام کلیدهای جدید""" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
TRONSCAN_API_KEY = "7ae72726-bffe-4e74-9c33-97b761eeea21" |
|
|
TRONSCAN_BASE_URL = "https://apilist.tronscan.org/api" |
|
|
|
|
|
BSCSCAN_API_KEY = "K62RKHGXTDCG53RU4MCG6XABIMJKTN19IT" |
|
|
BSCSCAN_BASE_URL = "https://api.bscscan.com/api" |
|
|
|
|
|
ETHERSCAN_API_KEY = "T6IR8VJHX2NE6ZJW2S3FDVN1TYG4PYYI45" |
|
|
ETHERSCAN_BASE_URL = "https://api.etherscan.io/api" |
|
|
|
|
|
|
|
|
COINMARKETCAP_API_KEY = "a35ffaec-c66c-4f16-81e3-41a717e4822f" |
|
|
COINMARKETCAP_BASE_URL = "https://pro-api.coinmarketcap.com/v1" |
|
|
|
|
|
|
|
|
NEWSAPI_API_KEY = "968a5e25552b4cb5ba3280361d8444ab" |
|
|
NEWSAPI_BASE_URL = "https://newsapi.org/v2" |
|
|
|
|
|
|
|
|
HF_API_TOKEN = os.getenv("HF_API_TOKEN", "").strip() |
|
|
HF_SPACE_BASE_URL = "https://really-amin-datasourceforcryptocurrency.hf.space" |
|
|
|
|
|
|
|
|
ALTERNATIVE_ME_BASE_URL = "https://api.alternative.me" |
|
|
COINGECKO_BASE_URL = "https://api.coingecko.com/api/v3" |
|
|
BINANCE_BASE_URL = "https://api.binance.com/api/v3" |
|
|
REDDIT_BASE_URL = "https://www.reddit.com/r" |
|
|
|
|
|
|
|
|
CACHE_TTL = { |
|
|
"market_prices": 30, |
|
|
"ohlcv": 60, |
|
|
"news": 300, |
|
|
"sentiment": 60, |
|
|
"blockchain": 60, |
|
|
"whale_activity": 30, |
|
|
"social_media": 120, |
|
|
"trending": 180, |
|
|
"fear_greed": 3600, |
|
|
} |
|
|
|
|
|
|
|
|
class RateLimiter: |
|
|
"""Rate limiter for API calls""" |
|
|
|
|
|
def __init__(self): |
|
|
self.limits = { |
|
|
"coinmarketcap": {"calls": 333, "period": 60}, |
|
|
"newsapi": {"calls": 500, "period": 3600}, |
|
|
"etherscan": {"calls": 5, "period": 1}, |
|
|
"bscscan": {"calls": 5, "period": 1}, |
|
|
"tronscan": {"calls": 10, "period": 1}, |
|
|
"coingecko": {"calls": 50, "period": 60}, |
|
|
"binance": {"calls": 1200, "period": 60}, |
|
|
} |
|
|
self.call_times = defaultdict(list) |
|
|
|
|
|
async def wait_if_needed(self, service: str): |
|
|
"""Wait if rate limit is reached""" |
|
|
if service not in self.limits: |
|
|
return |
|
|
|
|
|
limit = self.limits[service] |
|
|
now = time.time() |
|
|
|
|
|
|
|
|
self.call_times[service] = [ |
|
|
t for t in self.call_times[service] |
|
|
if now - t < limit["period"] |
|
|
] |
|
|
|
|
|
|
|
|
if len(self.call_times[service]) >= limit["calls"]: |
|
|
wait_time = limit["period"] - (now - self.call_times[service][0]) |
|
|
if wait_time > 0: |
|
|
logger.warning(f"⏳ Rate limit reached for {service}, waiting {wait_time:.1f}s") |
|
|
await asyncio.sleep(wait_time) |
|
|
|
|
|
|
|
|
self.call_times[service].append(now) |
|
|
|
|
|
|
|
|
class DataHubComplete: |
|
|
""" |
|
|
Data Hub کامل برای مدیریت همه منابع داده |
|
|
""" |
|
|
|
|
|
def __init__(self): |
|
|
self.config = DataHubConfiguration() |
|
|
self.rate_limiter = RateLimiter() |
|
|
self.cache = {} |
|
|
self.timeout = httpx.Timeout(30.0, connect=10.0) |
|
|
|
|
|
logger.info("🚀 Data Hub Complete initialized with all new API keys") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _get_cache_key(self, category: str, params: Dict = None) -> str: |
|
|
"""Generate cache key""" |
|
|
cache_str = f"{category}:{json.dumps(params or {}, sort_keys=True)}" |
|
|
return hashlib.md5(cache_str.encode()).hexdigest() |
|
|
|
|
|
def _get_cached(self, cache_key: str, cache_type: str) -> Optional[Dict]: |
|
|
"""Get data from cache if not expired""" |
|
|
if cache_key not in self.cache: |
|
|
return None |
|
|
|
|
|
cached_data, cached_time = self.cache[cache_key] |
|
|
ttl = self.config.CACHE_TTL.get(cache_type, 0) |
|
|
|
|
|
if ttl == 0: |
|
|
return None |
|
|
|
|
|
age = (datetime.now() - cached_time).total_seconds() |
|
|
if age < ttl: |
|
|
logger.info(f"📦 Cache HIT: {cache_type} (age: {age:.1f}s)") |
|
|
return cached_data |
|
|
|
|
|
del self.cache[cache_key] |
|
|
return None |
|
|
|
|
|
def _set_cache(self, cache_key: str, data: Dict, cache_type: str): |
|
|
"""Store data in cache""" |
|
|
ttl = self.config.CACHE_TTL.get(cache_type, 0) |
|
|
if ttl > 0: |
|
|
self.cache[cache_key] = (data, datetime.now()) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def get_market_prices( |
|
|
self, |
|
|
symbols: Optional[List[str]] = None, |
|
|
limit: int = 100, |
|
|
source: str = "auto" |
|
|
) -> Dict[str, Any]: |
|
|
""" |
|
|
دریافت قیمتهای بازار از منابع مختلف |
|
|
Sources: CoinMarketCap, CoinGecko, Binance, HuggingFace |
|
|
""" |
|
|
cache_key = self._get_cache_key("market_prices", {"symbols": symbols, "limit": limit}) |
|
|
cached = self._get_cached(cache_key, "market_prices") |
|
|
if cached: |
|
|
return cached |
|
|
|
|
|
errors = [] |
|
|
|
|
|
|
|
|
if source in ["auto", "coinmarketcap"]: |
|
|
try: |
|
|
await self.rate_limiter.wait_if_needed("coinmarketcap") |
|
|
async with httpx.AsyncClient(timeout=self.timeout) as client: |
|
|
headers = {"X-CMC_PRO_API_KEY": self.config.COINMARKETCAP_API_KEY} |
|
|
params = {"limit": limit, "convert": "USD"} |
|
|
if symbols: |
|
|
params["symbol"] = ",".join(symbols) |
|
|
endpoint = "/cryptocurrency/quotes/latest" |
|
|
else: |
|
|
endpoint = "/cryptocurrency/listings/latest" |
|
|
|
|
|
response = await client.get( |
|
|
f"{self.config.COINMARKETCAP_BASE_URL}{endpoint}", |
|
|
headers=headers, |
|
|
params=params |
|
|
) |
|
|
response.raise_for_status() |
|
|
data = response.json() |
|
|
|
|
|
|
|
|
result_data = [] |
|
|
if "data" in data: |
|
|
items = data["data"] if isinstance(data["data"], list) else data["data"].values() |
|
|
for coin in items: |
|
|
quote = coin.get("quote", {}).get("USD", {}) |
|
|
result_data.append({ |
|
|
"symbol": coin["symbol"], |
|
|
"name": coin["name"], |
|
|
"price": quote.get("price", 0), |
|
|
"change_24h": quote.get("percent_change_24h", 0), |
|
|
"volume_24h": quote.get("volume_24h", 0), |
|
|
"market_cap": quote.get("market_cap", 0), |
|
|
"rank": coin.get("cmc_rank", 0) |
|
|
}) |
|
|
|
|
|
result = { |
|
|
"success": True, |
|
|
"source": "coinmarketcap", |
|
|
"data": result_data, |
|
|
"timestamp": datetime.utcnow().isoformat() |
|
|
} |
|
|
self._set_cache(cache_key, result, "market_prices") |
|
|
logger.info(f"✅ Market prices from CoinMarketCap: {len(result_data)} items") |
|
|
return result |
|
|
|
|
|
except Exception as e: |
|
|
errors.append(f"CoinMarketCap: {e}") |
|
|
logger.warning(f"❌ CoinMarketCap failed: {e}") |
|
|
|
|
|
|
|
|
if source in ["auto", "coingecko"]: |
|
|
try: |
|
|
await self.rate_limiter.wait_if_needed("coingecko") |
|
|
async with httpx.AsyncClient(timeout=self.timeout) as client: |
|
|
if symbols: |
|
|
ids = ",".join([s.lower() for s in symbols]) |
|
|
params = {"ids": ids, "vs_currencies": "usd", "include_24hr_change": "true"} |
|
|
endpoint = "/simple/price" |
|
|
else: |
|
|
params = {"vs_currency": "usd", "per_page": limit, "page": 1} |
|
|
endpoint = "/coins/markets" |
|
|
|
|
|
response = await client.get( |
|
|
f"{self.config.COINGECKO_BASE_URL}{endpoint}", |
|
|
params=params |
|
|
) |
|
|
response.raise_for_status() |
|
|
data = response.json() |
|
|
|
|
|
|
|
|
result_data = [] |
|
|
if isinstance(data, list): |
|
|
for coin in data: |
|
|
result_data.append({ |
|
|
"symbol": coin.get("symbol", "").upper(), |
|
|
"name": coin.get("name", ""), |
|
|
"price": coin.get("current_price", 0), |
|
|
"change_24h": coin.get("price_change_percentage_24h", 0), |
|
|
"volume_24h": coin.get("total_volume", 0), |
|
|
"market_cap": coin.get("market_cap", 0), |
|
|
"rank": coin.get("market_cap_rank", 0) |
|
|
}) |
|
|
else: |
|
|
for symbol, info in data.items(): |
|
|
result_data.append({ |
|
|
"symbol": symbol.upper(), |
|
|
"price": info.get("usd", 0), |
|
|
"change_24h": info.get("usd_24h_change", 0) |
|
|
}) |
|
|
|
|
|
result = { |
|
|
"success": True, |
|
|
"source": "coingecko", |
|
|
"data": result_data, |
|
|
"timestamp": datetime.utcnow().isoformat() |
|
|
} |
|
|
self._set_cache(cache_key, result, "market_prices") |
|
|
logger.info(f"✅ Market prices from CoinGecko: {len(result_data)} items") |
|
|
return result |
|
|
|
|
|
except Exception as e: |
|
|
errors.append(f"CoinGecko: {e}") |
|
|
logger.warning(f"❌ CoinGecko failed: {e}") |
|
|
|
|
|
|
|
|
if source in ["auto", "binance"] and symbols: |
|
|
try: |
|
|
await self.rate_limiter.wait_if_needed("binance") |
|
|
async with httpx.AsyncClient(timeout=self.timeout) as client: |
|
|
response = await client.get( |
|
|
f"{self.config.BINANCE_BASE_URL}/ticker/24hr" |
|
|
) |
|
|
response.raise_for_status() |
|
|
data = response.json() |
|
|
|
|
|
|
|
|
result_data = [] |
|
|
for ticker in data: |
|
|
if ticker["symbol"].endswith("USDT"): |
|
|
base = ticker["symbol"][:-4] |
|
|
if not symbols or base in symbols: |
|
|
result_data.append({ |
|
|
"symbol": base, |
|
|
"price": float(ticker["lastPrice"]), |
|
|
"change_24h": float(ticker["priceChangePercent"]), |
|
|
"volume_24h": float(ticker["volume"]) * float(ticker["lastPrice"]), |
|
|
"high_24h": float(ticker["highPrice"]), |
|
|
"low_24h": float(ticker["lowPrice"]) |
|
|
}) |
|
|
|
|
|
result = { |
|
|
"success": True, |
|
|
"source": "binance", |
|
|
"data": result_data[:limit], |
|
|
"timestamp": datetime.utcnow().isoformat() |
|
|
} |
|
|
self._set_cache(cache_key, result, "market_prices") |
|
|
logger.info(f"✅ Market prices from Binance: {len(result_data)} items") |
|
|
return result |
|
|
|
|
|
except Exception as e: |
|
|
errors.append(f"Binance: {e}") |
|
|
logger.warning(f"❌ Binance failed: {e}") |
|
|
|
|
|
|
|
|
return { |
|
|
"success": False, |
|
|
"error": "All market data sources failed", |
|
|
"errors": errors, |
|
|
"timestamp": datetime.utcnow().isoformat() |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def get_ohlcv_data( |
|
|
self, |
|
|
symbol: str, |
|
|
interval: str = "1h", |
|
|
limit: int = 100, |
|
|
source: str = "auto" |
|
|
) -> Dict[str, Any]: |
|
|
""" |
|
|
دریافت دادههای OHLCV (کندل استیک) |
|
|
Sources: Binance, CoinMarketCap, HuggingFace |
|
|
""" |
|
|
cache_key = self._get_cache_key("ohlcv", {"symbol": symbol, "interval": interval, "limit": limit}) |
|
|
cached = self._get_cached(cache_key, "ohlcv") |
|
|
if cached: |
|
|
return cached |
|
|
|
|
|
errors = [] |
|
|
|
|
|
|
|
|
if source in ["auto", "binance"]: |
|
|
try: |
|
|
await self.rate_limiter.wait_if_needed("binance") |
|
|
async with httpx.AsyncClient(timeout=self.timeout) as client: |
|
|
response = await client.get( |
|
|
f"{self.config.BINANCE_BASE_URL}/klines", |
|
|
params={ |
|
|
"symbol": f"{symbol}USDT", |
|
|
"interval": interval, |
|
|
"limit": limit |
|
|
} |
|
|
) |
|
|
response.raise_for_status() |
|
|
klines = response.json() |
|
|
|
|
|
|
|
|
ohlcv_data = [] |
|
|
for kline in klines: |
|
|
ohlcv_data.append({ |
|
|
"timestamp": int(kline[0]), |
|
|
"open": float(kline[1]), |
|
|
"high": float(kline[2]), |
|
|
"low": float(kline[3]), |
|
|
"close": float(kline[4]), |
|
|
"volume": float(kline[5]) |
|
|
}) |
|
|
|
|
|
result = { |
|
|
"success": True, |
|
|
"source": "binance", |
|
|
"symbol": symbol, |
|
|
"interval": interval, |
|
|
"data": ohlcv_data, |
|
|
"timestamp": datetime.utcnow().isoformat() |
|
|
} |
|
|
self._set_cache(cache_key, result, "ohlcv") |
|
|
logger.info(f"✅ OHLCV from Binance: {len(ohlcv_data)} candles") |
|
|
return result |
|
|
|
|
|
except Exception as e: |
|
|
errors.append(f"Binance: {e}") |
|
|
logger.warning(f"❌ Binance OHLCV failed: {e}") |
|
|
|
|
|
|
|
|
if source in ["auto", "huggingface"]: |
|
|
try: |
|
|
async with httpx.AsyncClient(timeout=self.timeout) as client: |
|
|
_token = self.config.HF_API_TOKEN or os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACE_TOKEN") |
|
|
headers = {} |
|
|
if _token: |
|
|
headers["Authorization"] = f"Bearer {_token}" |
|
|
response = await client.get( |
|
|
f"{self.config.HF_SPACE_BASE_URL}/api/market/history", |
|
|
headers=headers, |
|
|
params={ |
|
|
"symbol": f"{symbol}USDT", |
|
|
"timeframe": interval, |
|
|
"limit": limit |
|
|
} |
|
|
) |
|
|
response.raise_for_status() |
|
|
data = response.json() |
|
|
|
|
|
result = { |
|
|
"success": True, |
|
|
"source": "huggingface", |
|
|
"symbol": symbol, |
|
|
"interval": interval, |
|
|
"data": data.get("data", []), |
|
|
"timestamp": datetime.utcnow().isoformat() |
|
|
} |
|
|
self._set_cache(cache_key, result, "ohlcv") |
|
|
logger.info(f"✅ OHLCV from HuggingFace") |
|
|
return result |
|
|
|
|
|
except Exception as e: |
|
|
errors.append(f"HuggingFace: {e}") |
|
|
logger.warning(f"❌ HuggingFace OHLCV failed: {e}") |
|
|
|
|
|
return { |
|
|
"success": False, |
|
|
"error": "Failed to fetch OHLCV data", |
|
|
"errors": errors, |
|
|
"timestamp": datetime.utcnow().isoformat() |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def get_fear_greed_index(self) -> Dict[str, Any]: |
|
|
""" |
|
|
دریافت شاخص ترس و طمع |
|
|
Source: Alternative.me |
|
|
""" |
|
|
cache_key = self._get_cache_key("fear_greed", {}) |
|
|
cached = self._get_cached(cache_key, "fear_greed") |
|
|
if cached: |
|
|
return cached |
|
|
|
|
|
try: |
|
|
async with httpx.AsyncClient(timeout=self.timeout) as client: |
|
|
response = await client.get( |
|
|
f"{self.config.ALTERNATIVE_ME_BASE_URL}/fng/", |
|
|
params={"limit": 30, "format": "json"} |
|
|
) |
|
|
response.raise_for_status() |
|
|
data = response.json() |
|
|
|
|
|
result = { |
|
|
"success": True, |
|
|
"source": "alternative.me", |
|
|
"data": data.get("data", []), |
|
|
"current": data.get("data", [{}])[0] if data.get("data") else {}, |
|
|
"timestamp": datetime.utcnow().isoformat() |
|
|
} |
|
|
self._set_cache(cache_key, result, "fear_greed") |
|
|
logger.info(f"✅ Fear & Greed Index fetched") |
|
|
return result |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"❌ Fear & Greed Index failed: {e}") |
|
|
return { |
|
|
"success": False, |
|
|
"error": str(e), |
|
|
"timestamp": datetime.utcnow().isoformat() |
|
|
} |
|
|
|
|
|
async def analyze_sentiment( |
|
|
self, |
|
|
text: str, |
|
|
source: str = "huggingface" |
|
|
) -> Dict[str, Any]: |
|
|
""" |
|
|
تحلیل احساسات متن |
|
|
Source: HuggingFace Models |
|
|
""" |
|
|
try: |
|
|
async with httpx.AsyncClient(timeout=self.timeout) as client: |
|
|
_token = self.config.HF_API_TOKEN or os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACE_TOKEN") |
|
|
headers = {} |
|
|
if _token: |
|
|
headers["Authorization"] = f"Bearer {_token}" |
|
|
response = await client.post( |
|
|
f"{self.config.HF_SPACE_BASE_URL}/api/sentiment/analyze", |
|
|
headers=headers, |
|
|
json={"text": text} |
|
|
) |
|
|
response.raise_for_status() |
|
|
data = response.json() |
|
|
|
|
|
logger.info(f"✅ Sentiment analysis completed") |
|
|
return { |
|
|
"success": True, |
|
|
"source": "huggingface", |
|
|
"data": data.get("data", {}), |
|
|
"timestamp": datetime.utcnow().isoformat() |
|
|
} |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"❌ Sentiment analysis failed: {e}") |
|
|
return { |
|
|
"success": False, |
|
|
"error": str(e), |
|
|
"timestamp": datetime.utcnow().isoformat() |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def get_crypto_news( |
|
|
self, |
|
|
query: str = "cryptocurrency", |
|
|
limit: int = 20, |
|
|
source: str = "auto" |
|
|
) -> Dict[str, Any]: |
|
|
""" |
|
|
دریافت اخبار ارزهای دیجیتال |
|
|
Sources: NewsAPI, Reddit, HuggingFace |
|
|
""" |
|
|
cache_key = self._get_cache_key("news", {"query": query, "limit": limit}) |
|
|
cached = self._get_cached(cache_key, "news") |
|
|
if cached: |
|
|
return cached |
|
|
|
|
|
errors = [] |
|
|
articles = [] |
|
|
|
|
|
|
|
|
if source in ["auto", "newsapi"]: |
|
|
try: |
|
|
await self.rate_limiter.wait_if_needed("newsapi") |
|
|
async with httpx.AsyncClient(timeout=self.timeout) as client: |
|
|
response = await client.get( |
|
|
f"{self.config.NEWSAPI_BASE_URL}/everything", |
|
|
params={ |
|
|
"q": query, |
|
|
"apiKey": self.config.NEWSAPI_API_KEY, |
|
|
"language": "en", |
|
|
"sortBy": "publishedAt", |
|
|
"pageSize": limit |
|
|
} |
|
|
) |
|
|
response.raise_for_status() |
|
|
data = response.json() |
|
|
|
|
|
for article in data.get("articles", []): |
|
|
articles.append({ |
|
|
"title": article["title"], |
|
|
"description": article.get("description"), |
|
|
"url": article["url"], |
|
|
"source": article["source"]["name"], |
|
|
"published_at": article["publishedAt"], |
|
|
"image_url": article.get("urlToImage") |
|
|
}) |
|
|
|
|
|
logger.info(f"✅ NewsAPI: {len(articles)} articles") |
|
|
|
|
|
except Exception as e: |
|
|
errors.append(f"NewsAPI: {e}") |
|
|
logger.warning(f"❌ NewsAPI failed: {e}") |
|
|
|
|
|
|
|
|
if source in ["auto", "reddit"]: |
|
|
try: |
|
|
async with httpx.AsyncClient(timeout=self.timeout) as client: |
|
|
response = await client.get( |
|
|
f"{self.config.REDDIT_BASE_URL}/CryptoCurrency/hot.json", |
|
|
params={"limit": limit}, |
|
|
headers={"User-Agent": "CryptoDataHub/1.0"} |
|
|
) |
|
|
response.raise_for_status() |
|
|
data = response.json() |
|
|
|
|
|
for post in data["data"]["children"]: |
|
|
post_data = post["data"] |
|
|
articles.append({ |
|
|
"title": post_data["title"], |
|
|
"description": post_data.get("selftext", "")[:200], |
|
|
"url": f"https://reddit.com{post_data['permalink']}", |
|
|
"source": "Reddit", |
|
|
"published_at": datetime.fromtimestamp(post_data["created_utc"]).isoformat(), |
|
|
"score": post_data["score"], |
|
|
"comments": post_data["num_comments"] |
|
|
}) |
|
|
|
|
|
logger.info(f"✅ Reddit: {len(articles)} posts") |
|
|
|
|
|
except Exception as e: |
|
|
errors.append(f"Reddit: {e}") |
|
|
logger.warning(f"❌ Reddit failed: {e}") |
|
|
|
|
|
if articles: |
|
|
result = { |
|
|
"success": True, |
|
|
"articles": articles[:limit], |
|
|
"total": len(articles), |
|
|
"sources": ["newsapi", "reddit"], |
|
|
"timestamp": datetime.utcnow().isoformat() |
|
|
} |
|
|
self._set_cache(cache_key, result, "news") |
|
|
return result |
|
|
|
|
|
return { |
|
|
"success": False, |
|
|
"error": "Failed to fetch news", |
|
|
"errors": errors, |
|
|
"timestamp": datetime.utcnow().isoformat() |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def get_trending_coins(self, source: str = "coingecko") -> Dict[str, Any]: |
|
|
""" |
|
|
دریافت ارزهای ترند |
|
|
Source: CoinGecko |
|
|
""" |
|
|
cache_key = self._get_cache_key("trending", {}) |
|
|
cached = self._get_cached(cache_key, "trending") |
|
|
if cached: |
|
|
return cached |
|
|
|
|
|
try: |
|
|
await self.rate_limiter.wait_if_needed("coingecko") |
|
|
async with httpx.AsyncClient(timeout=self.timeout) as client: |
|
|
response = await client.get(f"{self.config.COINGECKO_BASE_URL}/search/trending") |
|
|
response.raise_for_status() |
|
|
data = response.json() |
|
|
|
|
|
trending = [] |
|
|
for coin in data.get("coins", []): |
|
|
item = coin.get("item", {}) |
|
|
trending.append({ |
|
|
"id": item.get("id"), |
|
|
"symbol": item.get("symbol"), |
|
|
"name": item.get("name"), |
|
|
"rank": item.get("market_cap_rank"), |
|
|
"price_btc": item.get("price_btc"), |
|
|
"score": item.get("score", 0) |
|
|
}) |
|
|
|
|
|
result = { |
|
|
"success": True, |
|
|
"source": "coingecko", |
|
|
"trending": trending, |
|
|
"timestamp": datetime.utcnow().isoformat() |
|
|
} |
|
|
self._set_cache(cache_key, result, "trending") |
|
|
logger.info(f"✅ Trending coins: {len(trending)} items") |
|
|
return result |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"❌ Trending coins failed: {e}") |
|
|
return { |
|
|
"success": False, |
|
|
"error": str(e), |
|
|
"timestamp": datetime.utcnow().isoformat() |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def get_blockchain_data( |
|
|
self, |
|
|
chain: str, |
|
|
data_type: str = "transactions", |
|
|
address: Optional[str] = None, |
|
|
limit: int = 20 |
|
|
) -> Dict[str, Any]: |
|
|
""" |
|
|
دریافت دادههای بلاکچین |
|
|
Chains: ethereum, bsc, tron |
|
|
Types: transactions, balance, gas |
|
|
""" |
|
|
cache_key = self._get_cache_key("blockchain", { |
|
|
"chain": chain, |
|
|
"type": data_type, |
|
|
"address": address |
|
|
}) |
|
|
cached = self._get_cached(cache_key, "blockchain") |
|
|
if cached: |
|
|
return cached |
|
|
|
|
|
try: |
|
|
if chain.lower() == "ethereum": |
|
|
await self.rate_limiter.wait_if_needed("etherscan") |
|
|
async with httpx.AsyncClient(timeout=self.timeout) as client: |
|
|
params = {"apikey": self.config.ETHERSCAN_API_KEY} |
|
|
|
|
|
if data_type == "gas": |
|
|
params.update({"module": "gastracker", "action": "gasoracle"}) |
|
|
elif data_type == "balance" and address: |
|
|
params.update({ |
|
|
"module": "account", |
|
|
"action": "balance", |
|
|
"address": address |
|
|
}) |
|
|
elif data_type == "transactions" and address: |
|
|
params.update({ |
|
|
"module": "account", |
|
|
"action": "txlist", |
|
|
"address": address, |
|
|
"startblock": 0, |
|
|
"endblock": 99999999, |
|
|
"page": 1, |
|
|
"offset": limit, |
|
|
"sort": "desc" |
|
|
}) |
|
|
|
|
|
response = await client.get( |
|
|
self.config.ETHERSCAN_BASE_URL, |
|
|
params=params |
|
|
) |
|
|
response.raise_for_status() |
|
|
data = response.json() |
|
|
|
|
|
result = { |
|
|
"success": True, |
|
|
"source": "etherscan", |
|
|
"chain": "ethereum", |
|
|
"type": data_type, |
|
|
"data": data.get("result", {}), |
|
|
"timestamp": datetime.utcnow().isoformat() |
|
|
} |
|
|
self._set_cache(cache_key, result, "blockchain") |
|
|
logger.info(f"✅ Ethereum {data_type} data fetched") |
|
|
return result |
|
|
|
|
|
elif chain.lower() == "bsc": |
|
|
await self.rate_limiter.wait_if_needed("bscscan") |
|
|
async with httpx.AsyncClient(timeout=self.timeout) as client: |
|
|
params = {"apikey": self.config.BSCSCAN_API_KEY} |
|
|
|
|
|
if data_type == "balance" and address: |
|
|
params.update({ |
|
|
"module": "account", |
|
|
"action": "balance", |
|
|
"address": address |
|
|
}) |
|
|
elif data_type == "transactions" and address: |
|
|
params.update({ |
|
|
"module": "account", |
|
|
"action": "txlist", |
|
|
"address": address, |
|
|
"startblock": 0, |
|
|
"endblock": 99999999, |
|
|
"page": 1, |
|
|
"offset": limit, |
|
|
"sort": "desc" |
|
|
}) |
|
|
|
|
|
response = await client.get( |
|
|
self.config.BSCSCAN_BASE_URL, |
|
|
params=params |
|
|
) |
|
|
response.raise_for_status() |
|
|
data = response.json() |
|
|
|
|
|
result = { |
|
|
"success": True, |
|
|
"source": "bscscan", |
|
|
"chain": "bsc", |
|
|
"type": data_type, |
|
|
"data": data.get("result", {}), |
|
|
"timestamp": datetime.utcnow().isoformat() |
|
|
} |
|
|
self._set_cache(cache_key, result, "blockchain") |
|
|
logger.info(f"✅ BSC {data_type} data fetched") |
|
|
return result |
|
|
|
|
|
elif chain.lower() == "tron": |
|
|
await self.rate_limiter.wait_if_needed("tronscan") |
|
|
async with httpx.AsyncClient(timeout=self.timeout) as client: |
|
|
headers = {"TRON-PRO-API-KEY": self.config.TRONSCAN_API_KEY} |
|
|
|
|
|
if data_type == "transactions": |
|
|
endpoint = "/transaction" |
|
|
params = {"sort": "-timestamp", "limit": limit} |
|
|
if address: |
|
|
params["address"] = address |
|
|
elif data_type == "balance" and address: |
|
|
endpoint = f"/account/{address}" |
|
|
params = {} |
|
|
else: |
|
|
endpoint = "/transaction" |
|
|
params = {"sort": "-timestamp", "limit": limit} |
|
|
|
|
|
response = await client.get( |
|
|
f"{self.config.TRONSCAN_BASE_URL}{endpoint}", |
|
|
headers=headers, |
|
|
params=params |
|
|
) |
|
|
response.raise_for_status() |
|
|
data = response.json() |
|
|
|
|
|
result = { |
|
|
"success": True, |
|
|
"source": "tronscan", |
|
|
"chain": "tron", |
|
|
"type": data_type, |
|
|
"data": data.get("data", data), |
|
|
"timestamp": datetime.utcnow().isoformat() |
|
|
} |
|
|
self._set_cache(cache_key, result, "blockchain") |
|
|
logger.info(f"✅ Tron {data_type} data fetched") |
|
|
return result |
|
|
|
|
|
else: |
|
|
return { |
|
|
"success": False, |
|
|
"error": f"Unsupported chain: {chain}", |
|
|
"timestamp": datetime.utcnow().isoformat() |
|
|
} |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"❌ Blockchain data failed: {e}") |
|
|
return { |
|
|
"success": False, |
|
|
"error": str(e), |
|
|
"timestamp": datetime.utcnow().isoformat() |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def get_whale_activity( |
|
|
self, |
|
|
chain: str = "all", |
|
|
min_value_usd: float = 1000000, |
|
|
limit: int = 50 |
|
|
) -> Dict[str, Any]: |
|
|
""" |
|
|
دریافت فعالیت نهنگها |
|
|
تراکنشهای بزرگ در بلاکچینهای مختلف |
|
|
""" |
|
|
|
|
|
try: |
|
|
async with httpx.AsyncClient(timeout=self.timeout) as client: |
|
|
_token = self.config.HF_API_TOKEN or os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACE_TOKEN") |
|
|
headers = {} |
|
|
if _token: |
|
|
headers["Authorization"] = f"Bearer {_token}" |
|
|
response = await client.get( |
|
|
f"{self.config.HF_SPACE_BASE_URL}/api/crypto/whales/transactions", |
|
|
headers=headers, |
|
|
params={ |
|
|
"limit": limit, |
|
|
"chain": chain if chain != "all" else None, |
|
|
"min_amount_usd": min_value_usd |
|
|
} |
|
|
) |
|
|
response.raise_for_status() |
|
|
data = response.json() |
|
|
|
|
|
logger.info(f"✅ Whale activity fetched") |
|
|
return { |
|
|
"success": True, |
|
|
"source": "huggingface", |
|
|
"data": data, |
|
|
"timestamp": datetime.utcnow().isoformat() |
|
|
} |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"❌ Whale activity failed: {e}") |
|
|
|
|
|
return { |
|
|
"success": False, |
|
|
"error": str(e), |
|
|
"timestamp": datetime.utcnow().isoformat() |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def get_social_media_data( |
|
|
self, |
|
|
platform: str = "reddit", |
|
|
query: str = "cryptocurrency", |
|
|
limit: int = 20 |
|
|
) -> Dict[str, Any]: |
|
|
""" |
|
|
دریافت دادههای شبکههای اجتماعی |
|
|
Platforms: reddit, twitter (future) |
|
|
""" |
|
|
cache_key = self._get_cache_key("social_media", { |
|
|
"platform": platform, |
|
|
"query": query |
|
|
}) |
|
|
cached = self._get_cached(cache_key, "social_media") |
|
|
if cached: |
|
|
return cached |
|
|
|
|
|
if platform == "reddit": |
|
|
try: |
|
|
async with httpx.AsyncClient(timeout=self.timeout) as client: |
|
|
|
|
|
subreddits = ["CryptoCurrency", "Bitcoin", "ethereum", "defi"] |
|
|
all_posts = [] |
|
|
|
|
|
for subreddit in subreddits: |
|
|
try: |
|
|
response = await client.get( |
|
|
f"{self.config.REDDIT_BASE_URL}/{subreddit}/hot.json", |
|
|
params={"limit": limit // len(subreddits)}, |
|
|
headers={"User-Agent": "CryptoDataHub/1.0"} |
|
|
) |
|
|
response.raise_for_status() |
|
|
data = response.json() |
|
|
|
|
|
for post in data["data"]["children"]: |
|
|
post_data = post["data"] |
|
|
all_posts.append({ |
|
|
"id": post_data["id"], |
|
|
"title": post_data["title"], |
|
|
"text": post_data.get("selftext", "")[:500], |
|
|
"url": f"https://reddit.com{post_data['permalink']}", |
|
|
"subreddit": subreddit, |
|
|
"score": post_data["score"], |
|
|
"comments": post_data["num_comments"], |
|
|
"created_at": datetime.fromtimestamp(post_data["created_utc"]).isoformat(), |
|
|
"author": post_data.get("author", "deleted") |
|
|
}) |
|
|
except Exception as e: |
|
|
logger.warning(f"Failed to fetch from r/{subreddit}: {e}") |
|
|
|
|
|
|
|
|
all_posts.sort(key=lambda x: x["score"], reverse=True) |
|
|
|
|
|
result = { |
|
|
"success": True, |
|
|
"platform": "reddit", |
|
|
"posts": all_posts[:limit], |
|
|
"total": len(all_posts), |
|
|
"timestamp": datetime.utcnow().isoformat() |
|
|
} |
|
|
self._set_cache(cache_key, result, "social_media") |
|
|
logger.info(f"✅ Reddit data: {len(all_posts)} posts") |
|
|
return result |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"❌ Reddit data failed: {e}") |
|
|
return { |
|
|
"success": False, |
|
|
"error": str(e), |
|
|
"timestamp": datetime.utcnow().isoformat() |
|
|
} |
|
|
|
|
|
return { |
|
|
"success": False, |
|
|
"error": f"Unsupported platform: {platform}", |
|
|
"timestamp": datetime.utcnow().isoformat() |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def get_ai_prediction( |
|
|
self, |
|
|
symbol: str, |
|
|
model_type: str = "price", |
|
|
timeframe: str = "24h" |
|
|
) -> Dict[str, Any]: |
|
|
""" |
|
|
دریافت پیشبینی از مدلهای AI |
|
|
Types: price, trend, signal |
|
|
""" |
|
|
try: |
|
|
async with httpx.AsyncClient(timeout=self.timeout) as client: |
|
|
_token = self.config.HF_API_TOKEN or os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACE_TOKEN") |
|
|
headers = {} |
|
|
if _token: |
|
|
headers["Authorization"] = f"Bearer {_token}" |
|
|
|
|
|
|
|
|
price_data = await self.get_market_prices(symbols=[symbol], limit=1) |
|
|
current_price = 0 |
|
|
if price_data.get("success") and price_data.get("data"): |
|
|
current_price = price_data["data"][0].get("price", 0) |
|
|
|
|
|
response = await client.post( |
|
|
f"{self.config.HF_SPACE_BASE_URL}/api/models/predict", |
|
|
headers=headers, |
|
|
json={ |
|
|
"symbol": symbol, |
|
|
"type": model_type, |
|
|
"timeframe": timeframe, |
|
|
"current_price": current_price |
|
|
} |
|
|
) |
|
|
response.raise_for_status() |
|
|
data = response.json() |
|
|
|
|
|
logger.info(f"✅ AI prediction for {symbol}") |
|
|
return { |
|
|
"success": True, |
|
|
"source": "huggingface", |
|
|
"symbol": symbol, |
|
|
"prediction": data, |
|
|
"timestamp": datetime.utcnow().isoformat() |
|
|
} |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"❌ AI prediction failed: {e}") |
|
|
|
|
|
return { |
|
|
"success": False, |
|
|
"error": str(e), |
|
|
"timestamp": datetime.utcnow().isoformat() |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def check_all_sources_health(self) -> Dict[str, Any]: |
|
|
""" |
|
|
بررسی سلامت تمام منابع داده |
|
|
""" |
|
|
health_status = {} |
|
|
|
|
|
|
|
|
try: |
|
|
async with httpx.AsyncClient(timeout=5.0) as client: |
|
|
response = await client.get( |
|
|
f"{self.config.COINMARKETCAP_BASE_URL}/key/info", |
|
|
headers={"X-CMC_PRO_API_KEY": self.config.COINMARKETCAP_API_KEY} |
|
|
) |
|
|
health_status["coinmarketcap"] = "operational" if response.status_code == 200 else "degraded" |
|
|
except: |
|
|
health_status["coinmarketcap"] = "down" |
|
|
|
|
|
|
|
|
try: |
|
|
async with httpx.AsyncClient(timeout=5.0) as client: |
|
|
response = await client.get( |
|
|
f"{self.config.NEWSAPI_BASE_URL}/top-headlines", |
|
|
params={"apiKey": self.config.NEWSAPI_API_KEY, "pageSize": 1, "q": "test"} |
|
|
) |
|
|
health_status["newsapi"] = "operational" if response.status_code == 200 else "degraded" |
|
|
except: |
|
|
health_status["newsapi"] = "down" |
|
|
|
|
|
|
|
|
try: |
|
|
async with httpx.AsyncClient(timeout=5.0) as client: |
|
|
response = await client.get( |
|
|
self.config.ETHERSCAN_BASE_URL, |
|
|
params={ |
|
|
"module": "stats", |
|
|
"action": "ethsupply", |
|
|
"apikey": self.config.ETHERSCAN_API_KEY |
|
|
} |
|
|
) |
|
|
health_status["etherscan"] = "operational" if response.status_code == 200 else "degraded" |
|
|
except: |
|
|
health_status["etherscan"] = "down" |
|
|
|
|
|
|
|
|
try: |
|
|
async with httpx.AsyncClient(timeout=5.0) as client: |
|
|
_token = self.config.HF_API_TOKEN or os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACE_TOKEN") |
|
|
headers = {} |
|
|
if _token: |
|
|
headers["Authorization"] = f"Bearer {_token}" |
|
|
response = await client.get( |
|
|
f"{self.config.HF_SPACE_BASE_URL}/api/health", |
|
|
headers=headers |
|
|
) |
|
|
health_status["huggingface"] = "operational" if response.status_code == 200 else "degraded" |
|
|
except: |
|
|
health_status["huggingface"] = "down" |
|
|
|
|
|
|
|
|
health_status["coingecko"] = "operational" |
|
|
health_status["binance"] = "operational" |
|
|
health_status["alternative_me"] = "operational" |
|
|
health_status["reddit"] = "operational" |
|
|
|
|
|
return { |
|
|
"success": True, |
|
|
"status": health_status, |
|
|
"operational_count": sum(1 for v in health_status.values() if v == "operational"), |
|
|
"total_sources": len(health_status), |
|
|
"timestamp": datetime.utcnow().isoformat() |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
_data_hub_instance = None |
|
|
|
|
|
|
|
|
def get_data_hub() -> DataHubComplete: |
|
|
"""Get singleton instance of Data Hub Complete""" |
|
|
global _data_hub_instance |
|
|
if _data_hub_instance is None: |
|
|
_data_hub_instance = DataHubComplete() |
|
|
return _data_hub_instance |
|
|
|