|
|
|
|
|
""" |
|
|
Sentiment & News Providers Registry - Extended Sources |
|
|
منابع احساسات بازار و اخبار رمزارزها |
|
|
|
|
|
این ماژول شامل منابع زیر است: |
|
|
- Sentiment Analysis APIs |
|
|
- News Aggregation APIs |
|
|
- Social Media Sentiment |
|
|
- Market Sentiment Indices |
|
|
- Historical Data Sources |
|
|
""" |
|
|
|
|
|
import aiohttp |
|
|
import asyncio |
|
|
import feedparser |
|
|
from typing import Dict, List, Any, Optional |
|
|
from dataclasses import dataclass, field |
|
|
from enum import Enum |
|
|
from datetime import datetime |
|
|
import logging |
|
|
|
|
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
|
|
|
class SourceType(Enum): |
|
|
"""نوع منبع داده""" |
|
|
SENTIMENT = "sentiment" |
|
|
NEWS = "news" |
|
|
SOCIAL = "social" |
|
|
MARKET_MOOD = "market_mood" |
|
|
HISTORICAL = "historical" |
|
|
AGGREGATED = "aggregated" |
|
|
|
|
|
|
|
|
class TimeFrame(Enum): |
|
|
"""بازههای زمانی پشتیبانی شده""" |
|
|
REALTIME = "realtime" |
|
|
MINUTES_1 = "1m" |
|
|
MINUTES_5 = "5m" |
|
|
MINUTES_15 = "15m" |
|
|
MINUTES_30 = "30m" |
|
|
HOURLY = "1h" |
|
|
HOURS_4 = "4h" |
|
|
DAILY = "1d" |
|
|
WEEKLY = "1w" |
|
|
MONTHLY = "1M" |
|
|
|
|
|
|
|
|
@dataclass |
|
|
class SentimentNewsSource: |
|
|
"""تعریف یک منبع سنتیمنت یا اخبار""" |
|
|
id: str |
|
|
name: str |
|
|
source_type: str |
|
|
url: str |
|
|
description: str |
|
|
requires_api_key: bool = False |
|
|
api_key_env: str = "" |
|
|
rate_limit: str = "unlimited" |
|
|
supported_timeframes: List[str] = field(default_factory=list) |
|
|
categories: List[str] = field(default_factory=list) |
|
|
is_active: bool = True |
|
|
priority: int = 1 |
|
|
verified: bool = False |
|
|
free_tier: bool = True |
|
|
features: List[str] = field(default_factory=list) |
|
|
|
|
|
|
|
|
class SentimentNewsRegistry: |
|
|
""" |
|
|
رجیستری جامع منابع سنتیمنت و اخبار |
|
|
Comprehensive Sentiment & News Sources Registry |
|
|
""" |
|
|
|
|
|
def __init__(self): |
|
|
self.sources: Dict[str, SentimentNewsSource] = {} |
|
|
self._load_all_sources() |
|
|
|
|
|
def _load_all_sources(self): |
|
|
"""بارگذاری تمام منابع""" |
|
|
|
|
|
|
|
|
self.sources["fear_greed_index"] = SentimentNewsSource( |
|
|
id="fear_greed_index", |
|
|
name="Fear & Greed Index", |
|
|
source_type=SourceType.SENTIMENT.value, |
|
|
url="https://api.alternative.me/fng/", |
|
|
description="Crypto Fear & Greed Index - measure market sentiment", |
|
|
requires_api_key=False, |
|
|
rate_limit="unlimited", |
|
|
supported_timeframes=["1d", "1w", "1M"], |
|
|
categories=["sentiment", "market_mood"], |
|
|
is_active=True, |
|
|
priority=1, |
|
|
verified=True, |
|
|
free_tier=True, |
|
|
features=["fear_index", "greed_index", "historical"] |
|
|
) |
|
|
|
|
|
self.sources["lunarcrush"] = SentimentNewsSource( |
|
|
id="lunarcrush", |
|
|
name="LunarCrush", |
|
|
source_type=SourceType.SOCIAL.value, |
|
|
url="https://lunarcrush.com/api", |
|
|
description="Social metrics and sentiment for cryptocurrencies", |
|
|
requires_api_key=True, |
|
|
api_key_env="LUNARCRUSH_KEY", |
|
|
rate_limit="50 req/day (free)", |
|
|
supported_timeframes=["realtime", "1h", "1d", "1w"], |
|
|
categories=["social", "sentiment", "influencers"], |
|
|
is_active=True, |
|
|
priority=2, |
|
|
verified=False, |
|
|
free_tier=True, |
|
|
features=["social_volume", "sentiment_score", "influencers", "galaxy_score"] |
|
|
) |
|
|
|
|
|
self.sources["santiment"] = SentimentNewsSource( |
|
|
id="santiment", |
|
|
name="Santiment", |
|
|
source_type=SourceType.SENTIMENT.value, |
|
|
url="https://api.santiment.net/graphql", |
|
|
description="On-chain, social, and development metrics", |
|
|
requires_api_key=True, |
|
|
api_key_env="SANTIMENT_KEY", |
|
|
rate_limit="varies", |
|
|
supported_timeframes=["1h", "1d", "1w"], |
|
|
categories=["onchain", "social", "development"], |
|
|
is_active=True, |
|
|
priority=3, |
|
|
verified=False, |
|
|
free_tier=True, |
|
|
features=["dev_activity", "social_volume", "whale_movements", "network_growth"] |
|
|
) |
|
|
|
|
|
self.sources["augmento"] = SentimentNewsSource( |
|
|
id="augmento", |
|
|
name="Augmento", |
|
|
source_type=SourceType.SOCIAL.value, |
|
|
url="https://api.augmento.ai/v0.1", |
|
|
description="Social media sentiment analysis", |
|
|
requires_api_key=False, |
|
|
rate_limit="100 req/day", |
|
|
supported_timeframes=["1h", "1d"], |
|
|
categories=["social", "sentiment"], |
|
|
is_active=True, |
|
|
priority=4, |
|
|
verified=False, |
|
|
free_tier=True, |
|
|
features=["sentiment_topics", "social_trends", "coin_sentiment"] |
|
|
) |
|
|
|
|
|
self.sources["the_tie"] = SentimentNewsSource( |
|
|
id="the_tie", |
|
|
name="The TIE", |
|
|
source_type=SourceType.SENTIMENT.value, |
|
|
url="https://api.thetie.io/v1", |
|
|
description="Enterprise-grade sentiment data", |
|
|
requires_api_key=True, |
|
|
api_key_env="THE_TIE_KEY", |
|
|
rate_limit="varies", |
|
|
supported_timeframes=["realtime", "1h", "1d"], |
|
|
categories=["sentiment", "analytics"], |
|
|
is_active=True, |
|
|
priority=5, |
|
|
verified=False, |
|
|
free_tier=False, |
|
|
features=["sentiment_score", "volume_buzz", "tweet_sentiment"] |
|
|
) |
|
|
|
|
|
self.sources["cryptoquant_sentiment"] = SentimentNewsSource( |
|
|
id="cryptoquant_sentiment", |
|
|
name="CryptoQuant Sentiment", |
|
|
source_type=SourceType.SENTIMENT.value, |
|
|
url="https://api.cryptoquant.com/v1", |
|
|
description="On-chain sentiment indicators", |
|
|
requires_api_key=True, |
|
|
api_key_env="CRYPTOQUANT_KEY", |
|
|
rate_limit="100 req/day", |
|
|
supported_timeframes=["1h", "1d"], |
|
|
categories=["onchain", "sentiment"], |
|
|
is_active=True, |
|
|
priority=3, |
|
|
verified=False, |
|
|
free_tier=True, |
|
|
features=["exchange_flows", "miner_flows", "market_indicators"] |
|
|
) |
|
|
|
|
|
self.sources["glassnode_sentiment"] = SentimentNewsSource( |
|
|
id="glassnode_sentiment", |
|
|
name="Glassnode Sentiment", |
|
|
source_type=SourceType.SENTIMENT.value, |
|
|
url="https://api.glassnode.com/v1/metrics", |
|
|
description="Glassnode on-chain sentiment metrics", |
|
|
requires_api_key=True, |
|
|
api_key_env="GLASSNODE_KEY", |
|
|
rate_limit="varies", |
|
|
supported_timeframes=["1h", "1d", "1w"], |
|
|
categories=["onchain", "sentiment"], |
|
|
is_active=True, |
|
|
priority=2, |
|
|
verified=True, |
|
|
free_tier=True, |
|
|
features=["sopr", "nupl", "hodl_waves", "reserve_risk"] |
|
|
) |
|
|
|
|
|
|
|
|
self.sources["cryptopanic"] = SentimentNewsSource( |
|
|
id="cryptopanic", |
|
|
name="CryptoPanic", |
|
|
source_type=SourceType.NEWS.value, |
|
|
url="https://cryptopanic.com/api/v1/posts/", |
|
|
description="Crypto news aggregator with sentiment", |
|
|
requires_api_key=True, |
|
|
api_key_env="CRYPTOPANIC_KEY", |
|
|
rate_limit="500 req/day", |
|
|
supported_timeframes=["realtime", "1h", "1d"], |
|
|
categories=["news", "sentiment"], |
|
|
is_active=True, |
|
|
priority=1, |
|
|
verified=True, |
|
|
free_tier=True, |
|
|
features=["news_feed", "sentiment_votes", "trending_news", "filter_by_coin"] |
|
|
) |
|
|
|
|
|
self.sources["newsapi"] = SentimentNewsSource( |
|
|
id="newsapi", |
|
|
name="NewsAPI", |
|
|
source_type=SourceType.NEWS.value, |
|
|
url="https://newsapi.org/v2/everything", |
|
|
description="General news API with crypto filtering", |
|
|
requires_api_key=True, |
|
|
api_key_env="NEWSAPI_KEY", |
|
|
rate_limit="100 req/day (free)", |
|
|
supported_timeframes=["realtime", "1d"], |
|
|
categories=["news"], |
|
|
is_active=True, |
|
|
priority=2, |
|
|
verified=True, |
|
|
free_tier=True, |
|
|
features=["everything", "headlines", "sources"] |
|
|
) |
|
|
|
|
|
self.sources["cryptocompare_news"] = SentimentNewsSource( |
|
|
id="cryptocompare_news", |
|
|
name="CryptoCompare News", |
|
|
source_type=SourceType.NEWS.value, |
|
|
url="https://min-api.cryptocompare.com/data/v2/news/", |
|
|
description="CryptoCompare news feed", |
|
|
requires_api_key=False, |
|
|
rate_limit="100K/month", |
|
|
supported_timeframes=["realtime", "1h", "1d"], |
|
|
categories=["news"], |
|
|
is_active=True, |
|
|
priority=1, |
|
|
verified=True, |
|
|
free_tier=True, |
|
|
features=["latest_news", "news_by_categories", "news_by_coin"] |
|
|
) |
|
|
|
|
|
self.sources["messari_news"] = SentimentNewsSource( |
|
|
id="messari_news", |
|
|
name="Messari News", |
|
|
source_type=SourceType.NEWS.value, |
|
|
url="https://data.messari.io/api/v1/news", |
|
|
description="Messari research and news", |
|
|
requires_api_key=False, |
|
|
rate_limit="20 req/min", |
|
|
supported_timeframes=["realtime", "1d"], |
|
|
categories=["news", "research"], |
|
|
is_active=True, |
|
|
priority=2, |
|
|
verified=True, |
|
|
free_tier=True, |
|
|
features=["news", "research", "profiles"] |
|
|
) |
|
|
|
|
|
|
|
|
self.sources["bitcoin_magazine_rss"] = SentimentNewsSource( |
|
|
id="bitcoin_magazine_rss", |
|
|
name="Bitcoin Magazine RSS", |
|
|
source_type=SourceType.NEWS.value, |
|
|
url="https://bitcoinmagazine.com/feed", |
|
|
description="Bitcoin Magazine articles via RSS", |
|
|
requires_api_key=False, |
|
|
rate_limit="unlimited", |
|
|
supported_timeframes=["realtime"], |
|
|
categories=["news", "bitcoin"], |
|
|
is_active=True, |
|
|
priority=3, |
|
|
verified=True, |
|
|
free_tier=True, |
|
|
features=["articles", "analysis"] |
|
|
) |
|
|
|
|
|
self.sources["decrypt_rss"] = SentimentNewsSource( |
|
|
id="decrypt_rss", |
|
|
name="Decrypt RSS", |
|
|
source_type=SourceType.NEWS.value, |
|
|
url="https://decrypt.co/feed", |
|
|
description="Decrypt media RSS feed", |
|
|
requires_api_key=False, |
|
|
rate_limit="unlimited", |
|
|
supported_timeframes=["realtime"], |
|
|
categories=["news", "web3"], |
|
|
is_active=True, |
|
|
priority=3, |
|
|
verified=True, |
|
|
free_tier=True, |
|
|
features=["articles", "web3_news"] |
|
|
) |
|
|
|
|
|
self.sources["cryptoslate_rss"] = SentimentNewsSource( |
|
|
id="cryptoslate_rss", |
|
|
name="CryptoSlate RSS", |
|
|
source_type=SourceType.NEWS.value, |
|
|
url="https://cryptoslate.com/feed/", |
|
|
description="CryptoSlate news RSS", |
|
|
requires_api_key=False, |
|
|
rate_limit="unlimited", |
|
|
supported_timeframes=["realtime"], |
|
|
categories=["news", "analysis"], |
|
|
is_active=True, |
|
|
priority=3, |
|
|
verified=True, |
|
|
free_tier=True, |
|
|
features=["articles", "analysis"] |
|
|
) |
|
|
|
|
|
self.sources["theblock_rss"] = SentimentNewsSource( |
|
|
id="theblock_rss", |
|
|
name="The Block RSS", |
|
|
source_type=SourceType.NEWS.value, |
|
|
url="https://www.theblock.co/rss.xml", |
|
|
description="The Block crypto news RSS", |
|
|
requires_api_key=False, |
|
|
rate_limit="unlimited", |
|
|
supported_timeframes=["realtime"], |
|
|
categories=["news", "research"], |
|
|
is_active=True, |
|
|
priority=3, |
|
|
verified=True, |
|
|
free_tier=True, |
|
|
features=["articles", "research"] |
|
|
) |
|
|
|
|
|
self.sources["cointelegraph_rss"] = SentimentNewsSource( |
|
|
id="cointelegraph_rss", |
|
|
name="CoinTelegraph RSS", |
|
|
source_type=SourceType.NEWS.value, |
|
|
url="https://cointelegraph.com/rss", |
|
|
description="CoinTelegraph news feed", |
|
|
requires_api_key=False, |
|
|
rate_limit="unlimited", |
|
|
supported_timeframes=["realtime"], |
|
|
categories=["news"], |
|
|
is_active=True, |
|
|
priority=3, |
|
|
verified=True, |
|
|
free_tier=True, |
|
|
features=["articles", "breaking_news"] |
|
|
) |
|
|
|
|
|
self.sources["coindesk_rss"] = SentimentNewsSource( |
|
|
id="coindesk_rss", |
|
|
name="CoinDesk RSS", |
|
|
source_type=SourceType.NEWS.value, |
|
|
url="https://www.coindesk.com/arc/outboundfeeds/rss/", |
|
|
description="CoinDesk news feed", |
|
|
requires_api_key=False, |
|
|
rate_limit="unlimited", |
|
|
supported_timeframes=["realtime"], |
|
|
categories=["news"], |
|
|
is_active=True, |
|
|
priority=2, |
|
|
verified=True, |
|
|
free_tier=True, |
|
|
features=["articles", "analysis"] |
|
|
) |
|
|
|
|
|
|
|
|
self.sources["reddit_crypto"] = SentimentNewsSource( |
|
|
id="reddit_crypto", |
|
|
name="Reddit r/CryptoCurrency", |
|
|
source_type=SourceType.SOCIAL.value, |
|
|
url="https://www.reddit.com/r/CryptoCurrency/new.json", |
|
|
description="Reddit cryptocurrency subreddit", |
|
|
requires_api_key=False, |
|
|
rate_limit="60 req/min", |
|
|
supported_timeframes=["realtime", "1h", "1d"], |
|
|
categories=["social", "community"], |
|
|
is_active=True, |
|
|
priority=2, |
|
|
verified=True, |
|
|
free_tier=True, |
|
|
features=["posts", "comments", "trending"] |
|
|
) |
|
|
|
|
|
self.sources["reddit_bitcoin"] = SentimentNewsSource( |
|
|
id="reddit_bitcoin", |
|
|
name="Reddit r/Bitcoin", |
|
|
source_type=SourceType.SOCIAL.value, |
|
|
url="https://www.reddit.com/r/Bitcoin/new.json", |
|
|
description="Reddit Bitcoin subreddit", |
|
|
requires_api_key=False, |
|
|
rate_limit="60 req/min", |
|
|
supported_timeframes=["realtime", "1h", "1d"], |
|
|
categories=["social", "bitcoin"], |
|
|
is_active=True, |
|
|
priority=2, |
|
|
verified=True, |
|
|
free_tier=True, |
|
|
features=["posts", "comments"] |
|
|
) |
|
|
|
|
|
|
|
|
self.sources["coingecko_historical"] = SentimentNewsSource( |
|
|
id="coingecko_historical", |
|
|
name="CoinGecko Historical", |
|
|
source_type=SourceType.HISTORICAL.value, |
|
|
url="https://api.coingecko.com/api/v3", |
|
|
description="Historical price and market data", |
|
|
requires_api_key=False, |
|
|
rate_limit="10-50 req/min", |
|
|
supported_timeframes=["1m", "5m", "15m", "30m", "1h", "4h", "1d", "1w"], |
|
|
categories=["market", "historical"], |
|
|
is_active=True, |
|
|
priority=1, |
|
|
verified=True, |
|
|
free_tier=True, |
|
|
features=["ohlcv", "market_chart", "price_history"] |
|
|
) |
|
|
|
|
|
self.sources["binance_historical"] = SentimentNewsSource( |
|
|
id="binance_historical", |
|
|
name="Binance Historical", |
|
|
source_type=SourceType.HISTORICAL.value, |
|
|
url="https://api.binance.com/api/v3", |
|
|
description="Binance historical OHLCV data", |
|
|
requires_api_key=False, |
|
|
rate_limit="1200 req/min", |
|
|
supported_timeframes=["1m", "5m", "15m", "30m", "1h", "4h", "1d", "1w", "1M"], |
|
|
categories=["market", "historical", "ohlcv"], |
|
|
is_active=True, |
|
|
priority=1, |
|
|
verified=True, |
|
|
free_tier=True, |
|
|
features=["klines", "historical_trades", "agg_trades"] |
|
|
) |
|
|
|
|
|
self.sources["cryptocompare_historical"] = SentimentNewsSource( |
|
|
id="cryptocompare_historical", |
|
|
name="CryptoCompare Historical", |
|
|
source_type=SourceType.HISTORICAL.value, |
|
|
url="https://min-api.cryptocompare.com/data/v2", |
|
|
description="Historical price data", |
|
|
requires_api_key=False, |
|
|
rate_limit="100K/month", |
|
|
supported_timeframes=["1m", "1h", "1d"], |
|
|
categories=["market", "historical"], |
|
|
is_active=True, |
|
|
priority=2, |
|
|
verified=True, |
|
|
free_tier=True, |
|
|
features=["histominute", "histohour", "histoday"] |
|
|
) |
|
|
|
|
|
|
|
|
self.sources["coincap_realtime"] = SentimentNewsSource( |
|
|
id="coincap_realtime", |
|
|
name="CoinCap Real-time", |
|
|
source_type=SourceType.AGGREGATED.value, |
|
|
url="https://api.coincap.io/v2", |
|
|
description="Real-time aggregated market data", |
|
|
requires_api_key=False, |
|
|
rate_limit="200 req/min", |
|
|
supported_timeframes=["realtime", "1m", "5m", "15m", "30m", "1h", "1d"], |
|
|
categories=["market", "realtime"], |
|
|
is_active=True, |
|
|
priority=1, |
|
|
verified=True, |
|
|
free_tier=True, |
|
|
features=["assets", "rates", "exchanges", "markets", "candles"] |
|
|
) |
|
|
|
|
|
self.sources["coinpaprika"] = SentimentNewsSource( |
|
|
id="coinpaprika", |
|
|
name="CoinPaprika", |
|
|
source_type=SourceType.AGGREGATED.value, |
|
|
url="https://api.coinpaprika.com/v1", |
|
|
description="Crypto market data with OHLCV", |
|
|
requires_api_key=False, |
|
|
rate_limit="unlimited", |
|
|
supported_timeframes=["5m", "15m", "30m", "1h", "4h", "1d"], |
|
|
categories=["market", "ohlcv"], |
|
|
is_active=True, |
|
|
priority=2, |
|
|
verified=True, |
|
|
free_tier=True, |
|
|
features=["tickers", "coins", "exchanges", "ohlcv"] |
|
|
) |
|
|
|
|
|
self.sources["defillama"] = SentimentNewsSource( |
|
|
id="defillama", |
|
|
name="DefiLlama", |
|
|
source_type=SourceType.AGGREGATED.value, |
|
|
url="https://api.llama.fi", |
|
|
description="DeFi TVL and protocol data", |
|
|
requires_api_key=False, |
|
|
rate_limit="300 req/min", |
|
|
supported_timeframes=["1h", "1d"], |
|
|
categories=["defi", "tvl"], |
|
|
is_active=True, |
|
|
priority=1, |
|
|
verified=True, |
|
|
free_tier=True, |
|
|
features=["protocols", "tvl", "chains", "yields", "stablecoins"] |
|
|
) |
|
|
|
|
|
|
|
|
self.sources["tradingview_public"] = SentimentNewsSource( |
|
|
id="tradingview_public", |
|
|
name="TradingView Public", |
|
|
source_type=SourceType.MARKET_MOOD.value, |
|
|
url="https://www.tradingview.com", |
|
|
description="Public technical indicators (scraping)", |
|
|
requires_api_key=False, |
|
|
rate_limit="varies", |
|
|
supported_timeframes=["realtime", "1h", "1d"], |
|
|
categories=["technical", "indicators"], |
|
|
is_active=True, |
|
|
priority=4, |
|
|
verified=False, |
|
|
free_tier=True, |
|
|
features=["indicators", "signals", "screener"] |
|
|
) |
|
|
|
|
|
self.sources["taapi"] = SentimentNewsSource( |
|
|
id="taapi", |
|
|
name="TAAPI.IO", |
|
|
source_type=SourceType.MARKET_MOOD.value, |
|
|
url="https://api.taapi.io", |
|
|
description="Technical Analysis API", |
|
|
requires_api_key=True, |
|
|
api_key_env="TAAPI_KEY", |
|
|
rate_limit="50 req/day (free)", |
|
|
supported_timeframes=["1m", "5m", "15m", "30m", "1h", "4h", "1d"], |
|
|
categories=["technical", "indicators"], |
|
|
is_active=True, |
|
|
priority=3, |
|
|
verified=False, |
|
|
free_tier=True, |
|
|
features=["rsi", "macd", "bollinger", "ema", "sma"] |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
def get_all_sources(self) -> List[SentimentNewsSource]: |
|
|
"""دریافت همه منابع""" |
|
|
return list(self.sources.values()) |
|
|
|
|
|
def get_active_sources(self) -> List[SentimentNewsSource]: |
|
|
"""دریافت منابع فعال""" |
|
|
return [s for s in self.sources.values() if s.is_active] |
|
|
|
|
|
def get_source_by_id(self, source_id: str) -> Optional[SentimentNewsSource]: |
|
|
"""دریافت منبع با شناسه""" |
|
|
return self.sources.get(source_id) |
|
|
|
|
|
def get_sources_by_type(self, source_type: str) -> List[SentimentNewsSource]: |
|
|
"""دریافت منابع بر اساس نوع""" |
|
|
return [s for s in self.sources.values() if s.source_type == source_type] |
|
|
|
|
|
def get_free_sources(self) -> List[SentimentNewsSource]: |
|
|
"""دریافت منابع رایگان""" |
|
|
return [s for s in self.sources.values() if s.free_tier and not s.requires_api_key] |
|
|
|
|
|
def get_sources_by_timeframe(self, timeframe: str) -> List[SentimentNewsSource]: |
|
|
"""دریافت منابع بر اساس بازه زمانی""" |
|
|
return [s for s in self.sources.values() if timeframe in s.supported_timeframes] |
|
|
|
|
|
def get_sources_by_category(self, category: str) -> List[SentimentNewsSource]: |
|
|
"""دریافت منابع بر اساس دستهبندی""" |
|
|
return [s for s in self.sources.values() if category in s.categories] |
|
|
|
|
|
def search_sources(self, query: str) -> List[SentimentNewsSource]: |
|
|
"""جستجوی منابع""" |
|
|
query_lower = query.lower() |
|
|
results = [] |
|
|
for source in self.sources.values(): |
|
|
if (query_lower in source.name.lower() or |
|
|
query_lower in source.description.lower() or |
|
|
any(query_lower in cat.lower() for cat in source.categories) or |
|
|
any(query_lower in f.lower() for f in source.features)): |
|
|
results.append(source) |
|
|
return results |
|
|
|
|
|
def get_statistics(self) -> Dict[str, Any]: |
|
|
"""آمار منابع""" |
|
|
all_sources = self.get_all_sources() |
|
|
return { |
|
|
"total_sources": len(all_sources), |
|
|
"active_sources": len([s for s in all_sources if s.is_active]), |
|
|
"free_sources": len([s for s in all_sources if s.free_tier]), |
|
|
"no_key_required": len([s for s in all_sources if not s.requires_api_key]), |
|
|
"verified_sources": len([s for s in all_sources if s.verified]), |
|
|
"by_type": { |
|
|
st.value: len([s for s in all_sources if s.source_type == st.value]) |
|
|
for st in SourceType |
|
|
}, |
|
|
"categories": list(set(cat for s in all_sources for cat in s.categories)) |
|
|
} |
|
|
|
|
|
def set_source_active(self, source_id: str, is_active: bool) -> bool: |
|
|
"""تنظیم فعال/غیرفعال بودن منبع""" |
|
|
if source_id in self.sources: |
|
|
self.sources[source_id].is_active = is_active |
|
|
return True |
|
|
return False |
|
|
|
|
|
def to_dict(self) -> Dict[str, Any]: |
|
|
"""تبدیل به دیکشنری""" |
|
|
return { |
|
|
source_id: { |
|
|
"id": source.id, |
|
|
"name": source.name, |
|
|
"source_type": source.source_type, |
|
|
"url": source.url, |
|
|
"description": source.description, |
|
|
"requires_api_key": source.requires_api_key, |
|
|
"api_key_env": source.api_key_env, |
|
|
"rate_limit": source.rate_limit, |
|
|
"supported_timeframes": source.supported_timeframes, |
|
|
"categories": source.categories, |
|
|
"is_active": source.is_active, |
|
|
"priority": source.priority, |
|
|
"verified": source.verified, |
|
|
"free_tier": source.free_tier, |
|
|
"features": source.features |
|
|
} |
|
|
for source_id, source in self.sources.items() |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SentimentNewsFetcher: |
|
|
"""دریافت داده از منابع سنتیمنت و اخبار""" |
|
|
|
|
|
def __init__(self): |
|
|
self.registry = SentimentNewsRegistry() |
|
|
self.timeout = aiohttp.ClientTimeout(total=15) |
|
|
|
|
|
async def fetch_fear_greed_index(self, limit: int = 30) -> Dict[str, Any]: |
|
|
"""دریافت شاخص ترس و طمع""" |
|
|
source = self.registry.get_source_by_id("fear_greed_index") |
|
|
if not source or not source.is_active: |
|
|
return {"success": False, "error": "Source not available"} |
|
|
|
|
|
try: |
|
|
url = f"{source.url}?limit={limit}" |
|
|
async with aiohttp.ClientSession(timeout=self.timeout) as session: |
|
|
async with session.get(url) as response: |
|
|
if response.status == 200: |
|
|
data = await response.json() |
|
|
return { |
|
|
"success": True, |
|
|
"data": data.get("data", []), |
|
|
"source": "fear_greed_index" |
|
|
} |
|
|
return {"success": False, "error": f"HTTP {response.status}"} |
|
|
except Exception as e: |
|
|
logger.error(f"Fear & Greed fetch error: {e}") |
|
|
return {"success": False, "error": str(e)} |
|
|
|
|
|
async def fetch_rss_news(self, source_id: str, limit: int = 20) -> Dict[str, Any]: |
|
|
"""دریافت اخبار از RSS""" |
|
|
source = self.registry.get_source_by_id(source_id) |
|
|
if not source or not source.is_active: |
|
|
return {"success": False, "error": "Source not available"} |
|
|
|
|
|
try: |
|
|
loop = asyncio.get_event_loop() |
|
|
feed = await loop.run_in_executor(None, feedparser.parse, source.url) |
|
|
|
|
|
articles = [] |
|
|
for entry in feed.entries[:limit]: |
|
|
articles.append({ |
|
|
"title": entry.get("title", ""), |
|
|
"link": entry.get("link", ""), |
|
|
"published": entry.get("published", ""), |
|
|
"summary": entry.get("summary", "")[:500] if entry.get("summary") else "", |
|
|
"author": entry.get("author", ""), |
|
|
"source": source.name |
|
|
}) |
|
|
|
|
|
return { |
|
|
"success": True, |
|
|
"data": articles, |
|
|
"count": len(articles), |
|
|
"source": source_id |
|
|
} |
|
|
except Exception as e: |
|
|
logger.error(f"RSS fetch error for {source_id}: {e}") |
|
|
return {"success": False, "error": str(e)} |
|
|
|
|
|
async def fetch_all_rss_news(self, limit_per_source: int = 10) -> Dict[str, Any]: |
|
|
"""دریافت اخبار از همه منابع RSS""" |
|
|
rss_sources = [s for s in self.registry.get_active_sources() |
|
|
if "_rss" in s.id or s.url.endswith("/feed")] |
|
|
|
|
|
all_news = [] |
|
|
tasks = [self.fetch_rss_news(s.id, limit_per_source) for s in rss_sources] |
|
|
results = await asyncio.gather(*tasks, return_exceptions=True) |
|
|
|
|
|
for result in results: |
|
|
if isinstance(result, dict) and result.get("success"): |
|
|
all_news.extend(result.get("data", [])) |
|
|
|
|
|
|
|
|
all_news.sort(key=lambda x: x.get("published", ""), reverse=True) |
|
|
|
|
|
return { |
|
|
"success": True, |
|
|
"data": all_news, |
|
|
"count": len(all_news), |
|
|
"sources": [s.id for s in rss_sources] |
|
|
} |
|
|
|
|
|
async def fetch_reddit_posts(self, subreddit: str = "cryptocurrency", limit: int = 25) -> Dict[str, Any]: |
|
|
"""دریافت پستهای ردیت""" |
|
|
source_id = f"reddit_{subreddit.lower()}" |
|
|
source = self.registry.get_source_by_id(source_id) |
|
|
|
|
|
if not source: |
|
|
url = f"https://www.reddit.com/r/{subreddit}/new.json?limit={limit}" |
|
|
else: |
|
|
url = f"{source.url}?limit={limit}" |
|
|
|
|
|
try: |
|
|
headers = {"User-Agent": "CryptoMonitor/1.0"} |
|
|
async with aiohttp.ClientSession(timeout=self.timeout) as session: |
|
|
async with session.get(url, headers=headers) as response: |
|
|
if response.status == 200: |
|
|
data = await response.json() |
|
|
posts = [] |
|
|
for post in data.get("data", {}).get("children", []): |
|
|
post_data = post.get("data", {}) |
|
|
posts.append({ |
|
|
"title": post_data.get("title", ""), |
|
|
"url": f"https://reddit.com{post_data.get('permalink', '')}", |
|
|
"score": post_data.get("score", 0), |
|
|
"num_comments": post_data.get("num_comments", 0), |
|
|
"created_utc": post_data.get("created_utc", 0), |
|
|
"author": post_data.get("author", ""), |
|
|
"subreddit": subreddit |
|
|
}) |
|
|
return { |
|
|
"success": True, |
|
|
"data": posts, |
|
|
"count": len(posts), |
|
|
"source": f"reddit_{subreddit}" |
|
|
} |
|
|
return {"success": False, "error": f"HTTP {response.status}"} |
|
|
except Exception as e: |
|
|
logger.error(f"Reddit fetch error: {e}") |
|
|
return {"success": False, "error": str(e)} |
|
|
|
|
|
async def fetch_cryptocompare_news(self, categories: str = "", limit: int = 50) -> Dict[str, Any]: |
|
|
"""دریافت اخبار از CryptoCompare""" |
|
|
source = self.registry.get_source_by_id("cryptocompare_news") |
|
|
if not source or not source.is_active: |
|
|
return {"success": False, "error": "Source not available"} |
|
|
|
|
|
try: |
|
|
params = {"lang": "EN"} |
|
|
if categories: |
|
|
params["categories"] = categories |
|
|
|
|
|
url = source.url |
|
|
async with aiohttp.ClientSession(timeout=self.timeout) as session: |
|
|
async with session.get(url, params=params) as response: |
|
|
if response.status == 200: |
|
|
data = await response.json() |
|
|
articles = [] |
|
|
for article in data.get("Data", [])[:limit]: |
|
|
articles.append({ |
|
|
"id": article.get("id"), |
|
|
"title": article.get("title", ""), |
|
|
"body": article.get("body", "")[:500], |
|
|
"url": article.get("url", ""), |
|
|
"source": article.get("source", ""), |
|
|
"published_on": article.get("published_on", 0), |
|
|
"categories": article.get("categories", "") |
|
|
}) |
|
|
return { |
|
|
"success": True, |
|
|
"data": articles, |
|
|
"count": len(articles), |
|
|
"source": "cryptocompare_news" |
|
|
} |
|
|
return {"success": False, "error": f"HTTP {response.status}"} |
|
|
except Exception as e: |
|
|
logger.error(f"CryptoCompare news fetch error: {e}") |
|
|
return {"success": False, "error": str(e)} |
|
|
|
|
|
|
|
|
|
|
|
_registry = None |
|
|
_fetcher = None |
|
|
|
|
|
|
|
|
def get_sentiment_news_registry() -> SentimentNewsRegistry: |
|
|
"""دریافت instance سراسری registry""" |
|
|
global _registry |
|
|
if _registry is None: |
|
|
_registry = SentimentNewsRegistry() |
|
|
return _registry |
|
|
|
|
|
|
|
|
def get_sentiment_news_fetcher() -> SentimentNewsFetcher: |
|
|
"""دریافت instance سراسری fetcher""" |
|
|
global _fetcher |
|
|
if _fetcher is None: |
|
|
_fetcher = SentimentNewsFetcher() |
|
|
return _fetcher |
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
print("="*70) |
|
|
print("🧪 Testing Sentiment & News Providers Registry") |
|
|
print("="*70) |
|
|
|
|
|
registry = SentimentNewsRegistry() |
|
|
stats = registry.get_statistics() |
|
|
|
|
|
print(f"\n📊 Statistics:") |
|
|
print(f" Total Sources: {stats['total_sources']}") |
|
|
print(f" Active: {stats['active_sources']}") |
|
|
print(f" Free: {stats['free_sources']}") |
|
|
print(f" No Key Required: {stats['no_key_required']}") |
|
|
print(f" Verified: {stats['verified_sources']}") |
|
|
|
|
|
print(f"\n By Type:") |
|
|
for source_type, count in stats['by_type'].items(): |
|
|
print(f" • {source_type.upper()}: {count}") |
|
|
|
|
|
print(f"\n⭐ Free Sources (No API Key):") |
|
|
free_sources = registry.get_free_sources() |
|
|
for i, s in enumerate(free_sources[:10], 1): |
|
|
marker = "✅" if s.verified else "🟡" |
|
|
print(f" {marker} {i}. {s.name} - {s.description[:50]}...") |
|
|
|
|
|
print("\n" + "="*70) |
|
|
|
|
|
|
|
|
async def test_fetching(): |
|
|
fetcher = SentimentNewsFetcher() |
|
|
|
|
|
print("\n🧪 Testing Fear & Greed Index...") |
|
|
result = await fetcher.fetch_fear_greed_index(limit=5) |
|
|
if result["success"]: |
|
|
print(f" ✅ Got {len(result['data'])} entries") |
|
|
else: |
|
|
print(f" ❌ Error: {result.get('error')}") |
|
|
|
|
|
print("\n🧪 Testing RSS News (Decrypt)...") |
|
|
result = await fetcher.fetch_rss_news("decrypt_rss", limit=3) |
|
|
if result["success"]: |
|
|
print(f" ✅ Got {result['count']} articles") |
|
|
for article in result['data'][:2]: |
|
|
print(f" • {article['title'][:50]}...") |
|
|
else: |
|
|
print(f" ❌ Error: {result.get('error')}") |
|
|
|
|
|
print("\n🧪 Testing Reddit Posts...") |
|
|
result = await fetcher.fetch_reddit_posts("cryptocurrency", limit=3) |
|
|
if result["success"]: |
|
|
print(f" ✅ Got {result['count']} posts") |
|
|
else: |
|
|
print(f" ❌ Error: {result.get('error')}") |
|
|
|
|
|
asyncio.run(test_fetching()) |
|
|
|
|
|
print("\n" + "="*70) |
|
|
print("✅ Sentiment & News Providers Registry Complete!") |
|
|
print("="*70) |
|
|
|