nimazasinich
Cursor Agent
bxsfy712
commited on
Commit
·
3fea549
1
Parent(s):
a24b1f8
News source and monitoring update (#113)
Browse files* Refactor: Restructure data sources and add real-time monitoring
Co-authored-by: bxsfy712 <bxsfy712@outlook.com>
* feat: Add free crypto data resource registry
This commit introduces a comprehensive registry for free cryptocurrency data sources. It includes API keys, resource definitions, and utility functions for accessing and managing these resources.
Co-authored-by: bxsfy712 <bxsfy712@outlook.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: bxsfy712 <bxsfy712@outlook.com>
- FREE_RESOURCES_UPDATE_SUMMARY.md +191 -0
- SOURCES_UPDATE_SUMMARY.md +311 -0
- api/realtime_monitoring.py +483 -0
- backend/providers/free_resources.py +1018 -0
- backend/providers/sentiment_news_providers.py +889 -0
- config/api_keys.json +91 -0
- database/data_sources_model.py +487 -0
- scripts/init_free_resources.py +164 -0
- static/data/services.json +579 -354
- static/js/free_resources.ts +978 -0
- static/shared/components/config-helper-modal.js +254 -79
- workers/data_collection_worker.py +643 -0
FREE_RESOURCES_UPDATE_SUMMARY.md
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Free Resources Update Summary
|
| 2 |
+
## بروزرسانی منابع رایگان - خلاصه
|
| 3 |
+
|
| 4 |
+
**تاریخ**: 2025-12-12
|
| 5 |
+
|
| 6 |
+
---
|
| 7 |
+
|
| 8 |
+
## 📋 تغییرات اعمال شده
|
| 9 |
+
|
| 10 |
+
### 1. کلیدهای API جدید اضافه شده
|
| 11 |
+
|
| 12 |
+
| سرویس | کلید API | وضعیت |
|
| 13 |
+
|-------|---------|--------|
|
| 14 |
+
| **Etherscan** | `SZHYFZK2RR8H9TIMJBVW54V4H81K2Z2KR2` | ✅ فعال |
|
| 15 |
+
| **Etherscan (Backup)** | `T6IR8VJHX2NE6ZJW2S3FDVN1TYG4PYYI45` | ✅ فعال |
|
| 16 |
+
| **BscScan** | `K62RKHGXTDCG53RU4MCG6XABIMJKTN19IT` | ✅ فعال |
|
| 17 |
+
| **TronScan** | `7ae72726-bffe-4e74-9c33-97b761eeea21` | ✅ فعال |
|
| 18 |
+
| **CoinMarketCap #1** | `a35ffaec-c66c-4f16-81e3-41a717e4822f` | ✅ فعال |
|
| 19 |
+
| **CoinMarketCap #2** | `04cf4b5b-9868-465c-8ba0-9f2e78c92eb1` | ✅ فعال |
|
| 20 |
+
| **NewsAPI** | `968a5e25552b4cb5ba3280361d8444ab` | ✅ فعال |
|
| 21 |
+
| **Sentiment API** | `vltdvdho63uqnjgf_fq75qbks72e3wfmx` | ✅ فعال |
|
| 22 |
+
| **HuggingFace** | `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` | ✅ فعال |
|
| 23 |
+
| **Telegram Bot** | `7437859619:AAGeGG3ZkLM0OVaw-Exx1uMRE55JtBCZZCY` | ✅ فعال |
|
| 24 |
+
|
| 25 |
+
---
|
| 26 |
+
|
| 27 |
+
### 2. فایلهای جدید ایجاد شده
|
| 28 |
+
|
| 29 |
+
| فایل | توضیحات |
|
| 30 |
+
|------|---------|
|
| 31 |
+
| `config/api_keys.json` | کانفیگ کلیدهای API |
|
| 32 |
+
| `backend/providers/free_resources.py` | رجیستری منابع رایگان (Python) |
|
| 33 |
+
| `static/js/free_resources.ts` | رجیستری منابع رایگان (TypeScript) |
|
| 34 |
+
| `scripts/init_free_resources.py` | اسکریپت مقداردهی دیتابیس |
|
| 35 |
+
|
| 36 |
+
---
|
| 37 |
+
|
| 38 |
+
### 3. منابع ثبت شده در دیتابیس
|
| 39 |
+
|
| 40 |
+
**تعداد کل: 34 منبع**
|
| 41 |
+
|
| 42 |
+
#### Block Explorers (5)
|
| 43 |
+
- ✅ Etherscan (Ethereum)
|
| 44 |
+
- ✅ BscScan (BSC)
|
| 45 |
+
- ✅ TronScan (Tron)
|
| 46 |
+
- ✅ Polygonscan (Polygon)
|
| 47 |
+
- ✅ Blockchair (Multi-chain)
|
| 48 |
+
|
| 49 |
+
#### Market Data (6)
|
| 50 |
+
- ✅ CoinMarketCap
|
| 51 |
+
- ✅ CoinGecko
|
| 52 |
+
- ✅ CoinCap
|
| 53 |
+
- ✅ Binance
|
| 54 |
+
- ✅ KuCoin
|
| 55 |
+
- ✅ Kraken
|
| 56 |
+
|
| 57 |
+
#### News (5)
|
| 58 |
+
- ✅ NewsAPI
|
| 59 |
+
- ✅ CryptoPanic
|
| 60 |
+
- ✅ CoinDesk RSS
|
| 61 |
+
- ✅ Cointelegraph RSS
|
| 62 |
+
- ✅ CryptoCompare News
|
| 63 |
+
|
| 64 |
+
#### Sentiment (4)
|
| 65 |
+
- ✅ Fear & Greed Index
|
| 66 |
+
- ✅ Custom Sentiment API
|
| 67 |
+
- ✅ LunarCrush
|
| 68 |
+
- ✅ Santiment
|
| 69 |
+
|
| 70 |
+
#### On-Chain (3)
|
| 71 |
+
- ✅ Glassnode
|
| 72 |
+
- ✅ Blockchain.com
|
| 73 |
+
- ✅ Mempool.space
|
| 74 |
+
|
| 75 |
+
#### DeFi (3)
|
| 76 |
+
- ✅ DefiLlama
|
| 77 |
+
- ✅ 1inch
|
| 78 |
+
- ✅ Uniswap Subgraph
|
| 79 |
+
|
| 80 |
+
#### Whale Tracking (2)
|
| 81 |
+
- ✅ Whale Alert
|
| 82 |
+
- ✅ Etherscan Whale Tracker
|
| 83 |
+
|
| 84 |
+
#### Technical (2)
|
| 85 |
+
- ✅ TAAPI.IO
|
| 86 |
+
- ✅ TradingView Ideas
|
| 87 |
+
|
| 88 |
+
#### Social (2)
|
| 89 |
+
- ✅ Reddit API
|
| 90 |
+
- ✅ Twitter/X API
|
| 91 |
+
|
| 92 |
+
#### Historical (2)
|
| 93 |
+
- ✅ CryptoCompare Historical
|
| 94 |
+
- ✅ Messari
|
| 95 |
+
|
| 96 |
+
---
|
| 97 |
+
|
| 98 |
+
### 4. مدلهای یادگیری ماشین (از Word Doc)
|
| 99 |
+
|
| 100 |
+
| نام مدل | نوع | کاربرد |
|
| 101 |
+
|--------|-----|--------|
|
| 102 |
+
| PricePredictionLSTM | LSTM | پیشبینی قیمت کوتاهمدت |
|
| 103 |
+
| SentimentAnalysisTransformer | Transformer | تحلیل احساسات اخبار و شبکههای اجتماعی |
|
| 104 |
+
| AnomalyDetectionIsolationForest | Isolation Forest | تشخیص ناهنجاریهای بازار |
|
| 105 |
+
| TrendClassificationRandomForest | Random Forest | طبقهبندی روند بازار |
|
| 106 |
+
|
| 107 |
+
---
|
| 108 |
+
|
| 109 |
+
### 5. Endpoints تحلیل (از Word Doc)
|
| 110 |
+
|
| 111 |
+
```
|
| 112 |
+
GET /track_position - Track position
|
| 113 |
+
GET /market_analysis - Market analysis
|
| 114 |
+
GET /technical_analysis - Technical analysis
|
| 115 |
+
GET /sentiment_analysis - Sentiment analysis
|
| 116 |
+
GET /whale_activity - Whale activity
|
| 117 |
+
GET /trading_strategies - Trading strategies
|
| 118 |
+
GET /ai_prediction - AI prediction
|
| 119 |
+
GET /risk_management - Risk management
|
| 120 |
+
POST /pdf_analysis - PDF analysis
|
| 121 |
+
GET /ai_enhanced_analysis - AI enhanced analysis
|
| 122 |
+
GET /multi_source_data - Multi source data
|
| 123 |
+
GET /news_analysis - News analysis
|
| 124 |
+
POST /exchange_integration - Exchange integration
|
| 125 |
+
GET /smart_alerts - Smart alerts
|
| 126 |
+
GET /greed_fear_index - Fear & Greed Index
|
| 127 |
+
GET /onchain_metrics - On-chain metrics
|
| 128 |
+
POST /custom_alerts - Custom alerts
|
| 129 |
+
GET /stakeholder_analysis - Stakeholder analysis
|
| 130 |
+
```
|
| 131 |
+
|
| 132 |
+
---
|
| 133 |
+
|
| 134 |
+
## 🔧 نحوه استفاده
|
| 135 |
+
|
| 136 |
+
### Python
|
| 137 |
+
```python
|
| 138 |
+
from backend.providers.free_resources import get_free_resources_registry
|
| 139 |
+
|
| 140 |
+
registry = get_free_resources_registry()
|
| 141 |
+
|
| 142 |
+
# Get all resources
|
| 143 |
+
all_resources = registry.get_all_resources()
|
| 144 |
+
|
| 145 |
+
# Get by type
|
| 146 |
+
market_sources = registry.get_by_type(ResourceType.MARKET_DATA)
|
| 147 |
+
|
| 148 |
+
# Get free (no auth) sources
|
| 149 |
+
free_sources = registry.get_no_auth_resources()
|
| 150 |
+
|
| 151 |
+
# Search
|
| 152 |
+
results = registry.search_resources("bitcoin")
|
| 153 |
+
```
|
| 154 |
+
|
| 155 |
+
### TypeScript
|
| 156 |
+
```typescript
|
| 157 |
+
import {
|
| 158 |
+
ALL_RESOURCES,
|
| 159 |
+
getResourcesByType,
|
| 160 |
+
ResourceType
|
| 161 |
+
} from './free_resources';
|
| 162 |
+
|
| 163 |
+
// Get all market data sources
|
| 164 |
+
const marketSources = getResourcesByType(ResourceType.MARKET_DATA);
|
| 165 |
+
|
| 166 |
+
// Get statistics
|
| 167 |
+
const stats = getStatistics();
|
| 168 |
+
```
|
| 169 |
+
|
| 170 |
+
---
|
| 171 |
+
|
| 172 |
+
## 📊 آمار کلی
|
| 173 |
+
|
| 174 |
+
| متریک | مقدار |
|
| 175 |
+
|-------|-------|
|
| 176 |
+
| کل منابع | 34 |
|
| 177 |
+
| منابع رایگان | 31 |
|
| 178 |
+
| بدون نیاز به کلید | 19 |
|
| 179 |
+
| منابع فعال | 34 |
|
| 180 |
+
|
| 181 |
+
---
|
| 182 |
+
|
| 183 |
+
## 🔗 فایلهای مرتبط
|
| 184 |
+
|
| 185 |
+
- `/workspace/config/api_keys.json` - کانفیگ کلیدها
|
| 186 |
+
- `/workspace/backend/providers/free_resources.py` - رجیستری Python
|
| 187 |
+
- `/workspace/backend/providers/sentiment_news_providers.py` - منابع سنتیمنت
|
| 188 |
+
- `/workspace/backend/providers/new_providers_registry.py` - منابع قبلی
|
| 189 |
+
- `/workspace/static/js/free_resources.ts` - رجیستری TypeScript
|
| 190 |
+
- `/workspace/database/data_sources_model.py` - مدل دیتابیس
|
| 191 |
+
- `/workspace/scripts/init_free_resources.py` - اسکریپت مقداردهی
|
SOURCES_UPDATE_SUMMARY.md
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Data Sources Update Summary
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
This update adds comprehensive sentiment/news sources, database management for data sources, configurable collection intervals, and real-time monitoring capabilities.
|
| 6 |
+
|
| 7 |
+
## Changes Made
|
| 8 |
+
|
| 9 |
+
### 1. New Sentiment & News Sources Registry
|
| 10 |
+
**File:** `backend/providers/sentiment_news_providers.py`
|
| 11 |
+
|
| 12 |
+
Added 25+ new data sources including:
|
| 13 |
+
|
| 14 |
+
#### Sentiment APIs
|
| 15 |
+
- Fear & Greed Index (free, no key)
|
| 16 |
+
- LunarCrush (social sentiment)
|
| 17 |
+
- Santiment (on-chain + social)
|
| 18 |
+
- Augmento (social media analysis)
|
| 19 |
+
- The TIE (enterprise sentiment)
|
| 20 |
+
- CryptoQuant Sentiment
|
| 21 |
+
- Glassnode Sentiment
|
| 22 |
+
|
| 23 |
+
#### News Sources
|
| 24 |
+
- CryptoPanic (aggregated news)
|
| 25 |
+
- NewsAPI
|
| 26 |
+
- CryptoCompare News
|
| 27 |
+
- Messari News
|
| 28 |
+
- RSS Feeds:
|
| 29 |
+
- Bitcoin Magazine
|
| 30 |
+
- Decrypt
|
| 31 |
+
- CryptoSlate
|
| 32 |
+
- The Block
|
| 33 |
+
- CoinTelegraph
|
| 34 |
+
- CoinDesk
|
| 35 |
+
|
| 36 |
+
#### Social Sources
|
| 37 |
+
- Reddit r/CryptoCurrency
|
| 38 |
+
- Reddit r/Bitcoin
|
| 39 |
+
|
| 40 |
+
#### Historical Data
|
| 41 |
+
- CoinGecko Historical
|
| 42 |
+
- Binance Historical
|
| 43 |
+
- CryptoCompare Historical
|
| 44 |
+
|
| 45 |
+
#### Aggregated Sources
|
| 46 |
+
- CoinCap Real-time
|
| 47 |
+
- CoinPaprika
|
| 48 |
+
- DefiLlama
|
| 49 |
+
|
| 50 |
+
---
|
| 51 |
+
|
| 52 |
+
### 2. Database Model for Data Sources
|
| 53 |
+
**File:** `database/data_sources_model.py`
|
| 54 |
+
|
| 55 |
+
New database tables:
|
| 56 |
+
|
| 57 |
+
#### DataSource Table
|
| 58 |
+
```python
|
| 59 |
+
class DataSource(Base):
|
| 60 |
+
__tablename__ = 'data_sources'
|
| 61 |
+
|
| 62 |
+
# Basic Info
|
| 63 |
+
source_id = Column(String(100), unique=True)
|
| 64 |
+
name = Column(String(255))
|
| 65 |
+
source_type = Column(String(50))
|
| 66 |
+
description = Column(Text)
|
| 67 |
+
|
| 68 |
+
# Connection Info
|
| 69 |
+
base_url = Column(String(500))
|
| 70 |
+
|
| 71 |
+
# Authentication
|
| 72 |
+
requires_api_key = Column(Boolean)
|
| 73 |
+
api_key_env_var = Column(String(100))
|
| 74 |
+
|
| 75 |
+
# Collection Settings
|
| 76 |
+
collection_interval = Column(String(20)) # "15m", "30m"
|
| 77 |
+
supports_realtime = Column(Boolean)
|
| 78 |
+
|
| 79 |
+
# Status
|
| 80 |
+
is_active = Column(Boolean, default=True)
|
| 81 |
+
status = Column(String(50)) # "active", "error", "rate_limited"
|
| 82 |
+
|
| 83 |
+
# Statistics
|
| 84 |
+
total_requests = Column(Integer)
|
| 85 |
+
successful_requests = Column(Integer)
|
| 86 |
+
avg_response_time_ms = Column(Float)
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
#### DataCollectionLog Table
|
| 90 |
+
Tracks collection history for each source.
|
| 91 |
+
|
| 92 |
+
#### CollectionSchedule Table
|
| 93 |
+
Manages scheduled collection times.
|
| 94 |
+
|
| 95 |
+
---
|
| 96 |
+
|
| 97 |
+
### 3. Updated Services Configuration
|
| 98 |
+
**File:** `static/data/services.json`
|
| 99 |
+
|
| 100 |
+
Updated to include all 40+ providers organized by category:
|
| 101 |
+
- Market Data (8 providers)
|
| 102 |
+
- News (9 sources)
|
| 103 |
+
- Sentiment (4 providers)
|
| 104 |
+
- Analytics (4 providers)
|
| 105 |
+
- DeFi (3 providers)
|
| 106 |
+
- Technical Analysis
|
| 107 |
+
- AI Models
|
| 108 |
+
- Block Explorers (4 providers)
|
| 109 |
+
|
| 110 |
+
---
|
| 111 |
+
|
| 112 |
+
### 4. Data Collection Worker
|
| 113 |
+
**File:** `workers/data_collection_worker.py`
|
| 114 |
+
|
| 115 |
+
Configurable collection intervals:
|
| 116 |
+
|
| 117 |
+
```python
|
| 118 |
+
COLLECTION_INTERVALS = {
|
| 119 |
+
"market": 15, # 15 minutes
|
| 120 |
+
"news": 15, # 15 minutes
|
| 121 |
+
"sentiment": 15, # 15 minutes
|
| 122 |
+
"social": 30, # 30 minutes
|
| 123 |
+
"onchain": 30, # 30 minutes
|
| 124 |
+
"historical": 30, # 30 minutes
|
| 125 |
+
"defi": 15, # 15 minutes
|
| 126 |
+
"technical": 15, # 15 minutes
|
| 127 |
+
}
|
| 128 |
+
```
|
| 129 |
+
|
| 130 |
+
Features:
|
| 131 |
+
- **Bulk Collection:** Every 15-30 minutes (configurable per data type)
|
| 132 |
+
- **Real-time Fetching:** On-demand when client requests data
|
| 133 |
+
- **Caching:** Smart caching with configurable TTL
|
| 134 |
+
- **Multi-Source Fallback:** Automatic fallback to backup providers
|
| 135 |
+
|
| 136 |
+
---
|
| 137 |
+
|
| 138 |
+
### 5. Real-Time Monitoring
|
| 139 |
+
**File:** `api/realtime_monitoring.py`
|
| 140 |
+
|
| 141 |
+
WebSocket channels for real-time updates:
|
| 142 |
+
|
| 143 |
+
```
|
| 144 |
+
Channels:
|
| 145 |
+
- market_data : Real-time market prices
|
| 146 |
+
- price_updates : Individual price changes
|
| 147 |
+
- news : Latest news articles
|
| 148 |
+
- sentiment : Sentiment changes
|
| 149 |
+
- whale_alerts : Large transaction alerts
|
| 150 |
+
- collection_status: Data collection progress
|
| 151 |
+
- system_health : System health monitoring
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
WebSocket endpoints:
|
| 155 |
+
- `/ws/realtime` - Main endpoint (subscribe to any channel)
|
| 156 |
+
- `/ws/prices` - Dedicated price updates
|
| 157 |
+
- `/ws/alerts` - Whale and sentiment alerts
|
| 158 |
+
|
| 159 |
+
---
|
| 160 |
+
|
| 161 |
+
### 6. Updated Help Modal
|
| 162 |
+
**File:** `static/shared/components/config-helper-modal.js`
|
| 163 |
+
|
| 164 |
+
Updated to show all available services with examples:
|
| 165 |
+
- Unified Service API
|
| 166 |
+
- Market Data API
|
| 167 |
+
- News Aggregator API
|
| 168 |
+
- Sentiment Analysis API
|
| 169 |
+
- On-Chain Analytics API
|
| 170 |
+
- Technical Analysis API
|
| 171 |
+
- AI Models API
|
| 172 |
+
- DeFi Data API
|
| 173 |
+
- Resources & Monitoring API
|
| 174 |
+
- WebSocket API
|
| 175 |
+
|
| 176 |
+
---
|
| 177 |
+
|
| 178 |
+
## Collection Strategy
|
| 179 |
+
|
| 180 |
+
### Bulk Data (15-30 minute intervals)
|
| 181 |
+
Used for data that doesn't change frequently:
|
| 182 |
+
- Market overview data
|
| 183 |
+
- News articles
|
| 184 |
+
- On-chain statistics
|
| 185 |
+
- DeFi TVL data
|
| 186 |
+
|
| 187 |
+
### Real-time Data (on-demand)
|
| 188 |
+
Fetched immediately when client requests:
|
| 189 |
+
- Current prices (Binance, CoinGecko)
|
| 190 |
+
- OHLCV candlestick data
|
| 191 |
+
- Fear & Greed Index
|
| 192 |
+
- Whale transactions
|
| 193 |
+
|
| 194 |
+
### Caching Strategy
|
| 195 |
+
```python
|
| 196 |
+
CACHE_TTL = {
|
| 197 |
+
"market": 60, # 1 minute
|
| 198 |
+
"news": 300, # 5 minutes
|
| 199 |
+
"sentiment": 300, # 5 minutes
|
| 200 |
+
"ohlcv": 60, # 1 minute
|
| 201 |
+
"fear_greed": 3600, # 1 hour
|
| 202 |
+
"whale": 300, # 5 minutes
|
| 203 |
+
}
|
| 204 |
+
```
|
| 205 |
+
|
| 206 |
+
---
|
| 207 |
+
|
| 208 |
+
## API Endpoints Reference
|
| 209 |
+
|
| 210 |
+
### Unified Service API
|
| 211 |
+
```
|
| 212 |
+
GET /api/service/rate?pair=BTC/USDT
|
| 213 |
+
GET /api/service/rate/batch?pairs=BTC/USDT,ETH/USDT
|
| 214 |
+
GET /api/service/market-status
|
| 215 |
+
GET /api/service/top?n=10
|
| 216 |
+
GET /api/service/sentiment?symbol=BTC
|
| 217 |
+
GET /api/service/whales?chain=ethereum&min_amount_usd=1000000
|
| 218 |
+
```
|
| 219 |
+
|
| 220 |
+
### Market Data
|
| 221 |
+
```
|
| 222 |
+
GET /api/market?limit=100
|
| 223 |
+
GET /api/ohlcv?symbol=BTC&timeframe=1h&limit=500
|
| 224 |
+
GET /api/coins/top?limit=50
|
| 225 |
+
```
|
| 226 |
+
|
| 227 |
+
### News
|
| 228 |
+
```
|
| 229 |
+
GET /api/news?limit=20
|
| 230 |
+
GET /api/news/latest?symbol=BTC&limit=10
|
| 231 |
+
```
|
| 232 |
+
|
| 233 |
+
### Sentiment
|
| 234 |
+
```
|
| 235 |
+
GET /api/sentiment/global
|
| 236 |
+
GET /api/fear-greed
|
| 237 |
+
POST /api/sentiment/analyze
|
| 238 |
+
```
|
| 239 |
+
|
| 240 |
+
### Real-Time WebSocket
|
| 241 |
+
```javascript
|
| 242 |
+
const ws = new WebSocket('wss://host/ws/realtime');
|
| 243 |
+
|
| 244 |
+
ws.onopen = () => {
|
| 245 |
+
ws.send(JSON.stringify({
|
| 246 |
+
action: 'subscribe',
|
| 247 |
+
channels: ['market_data', 'price_updates', 'whale_alerts']
|
| 248 |
+
}));
|
| 249 |
+
};
|
| 250 |
+
|
| 251 |
+
ws.onmessage = (event) => {
|
| 252 |
+
const data = JSON.parse(event.data);
|
| 253 |
+
console.log('Update:', data.channel, data.data);
|
| 254 |
+
};
|
| 255 |
+
```
|
| 256 |
+
|
| 257 |
+
---
|
| 258 |
+
|
| 259 |
+
## Usage Examples
|
| 260 |
+
|
| 261 |
+
### Python - Fetch Market Data
|
| 262 |
+
```python
|
| 263 |
+
import requests
|
| 264 |
+
|
| 265 |
+
# Get prices
|
| 266 |
+
response = requests.get('https://your-api/api/service/rate/batch?pairs=BTC/USDT,ETH/USDT')
|
| 267 |
+
data = response.json()
|
| 268 |
+
for rate in data['data']:
|
| 269 |
+
print(f"{rate['pair']}: ${rate['price']}")
|
| 270 |
+
```
|
| 271 |
+
|
| 272 |
+
### JavaScript - Real-time Updates
|
| 273 |
+
```javascript
|
| 274 |
+
const ws = new WebSocket('wss://your-api/ws/realtime');
|
| 275 |
+
|
| 276 |
+
ws.onopen = () => {
|
| 277 |
+
// Subscribe to price updates
|
| 278 |
+
ws.send(JSON.stringify({
|
| 279 |
+
action: 'subscribe',
|
| 280 |
+
channels: ['price_updates']
|
| 281 |
+
}));
|
| 282 |
+
};
|
| 283 |
+
|
| 284 |
+
ws.onmessage = (event) => {
|
| 285 |
+
const msg = JSON.parse(event.data);
|
| 286 |
+
if (msg.channel === 'price_updates') {
|
| 287 |
+
console.log(`${msg.data.symbol}: $${msg.data.price}`);
|
| 288 |
+
}
|
| 289 |
+
};
|
| 290 |
+
```
|
| 291 |
+
|
| 292 |
+
---
|
| 293 |
+
|
| 294 |
+
## Files Modified/Created
|
| 295 |
+
|
| 296 |
+
1. `backend/providers/sentiment_news_providers.py` - NEW
|
| 297 |
+
2. `database/data_sources_model.py` - NEW
|
| 298 |
+
3. `workers/data_collection_worker.py` - NEW
|
| 299 |
+
4. `api/realtime_monitoring.py` - NEW
|
| 300 |
+
5. `static/data/services.json` - UPDATED
|
| 301 |
+
6. `static/shared/components/config-helper-modal.js` - UPDATED
|
| 302 |
+
|
| 303 |
+
---
|
| 304 |
+
|
| 305 |
+
## Notes
|
| 306 |
+
|
| 307 |
+
- All new sources are configured with appropriate rate limits
|
| 308 |
+
- Database model supports tracking active/inactive status
|
| 309 |
+
- Collection intervals are configurable per data type
|
| 310 |
+
- Real-time WebSocket provides push updates, not just polling
|
| 311 |
+
- HTTP endpoints remain available as fallback
|
api/realtime_monitoring.py
ADDED
|
@@ -0,0 +1,483 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Real-Time Monitoring Service with WebSocket Push Updates
|
| 3 |
+
|
| 4 |
+
This module provides real-time monitoring capabilities:
|
| 5 |
+
- Push updates for market data
|
| 6 |
+
- Real-time news alerts
|
| 7 |
+
- Sentiment changes
|
| 8 |
+
- Data collection status
|
| 9 |
+
- System health monitoring
|
| 10 |
+
|
| 11 |
+
All data is pushed via WebSocket when changes occur,
|
| 12 |
+
not just on a fixed interval.
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
import asyncio
|
| 16 |
+
import logging
|
| 17 |
+
from datetime import datetime, timedelta
|
| 18 |
+
from typing import Dict, Any, List, Optional, Callable, Set
|
| 19 |
+
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
| 20 |
+
import json
|
| 21 |
+
|
| 22 |
+
logger = logging.getLogger(__name__)
|
| 23 |
+
|
| 24 |
+
router = APIRouter()
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
# ===== CONNECTION MANAGER =====
|
| 28 |
+
|
| 29 |
+
class RealTimeConnectionManager:
|
| 30 |
+
"""
|
| 31 |
+
Manages WebSocket connections for real-time updates
|
| 32 |
+
Supports multiple channels for different data types
|
| 33 |
+
"""
|
| 34 |
+
|
| 35 |
+
def __init__(self):
|
| 36 |
+
self.active_connections: Dict[str, WebSocket] = {}
|
| 37 |
+
self.subscriptions: Dict[str, Set[str]] = {} # client_id -> set of channels
|
| 38 |
+
self.channel_subscribers: Dict[str, Set[str]] = {} # channel -> set of client_ids
|
| 39 |
+
self._client_counter = 0
|
| 40 |
+
|
| 41 |
+
async def connect(self, websocket: WebSocket) -> str:
|
| 42 |
+
"""Accept connection and return client ID"""
|
| 43 |
+
await websocket.accept()
|
| 44 |
+
self._client_counter += 1
|
| 45 |
+
client_id = f"client_{self._client_counter}_{datetime.utcnow().timestamp()}"
|
| 46 |
+
self.active_connections[client_id] = websocket
|
| 47 |
+
self.subscriptions[client_id] = set()
|
| 48 |
+
logger.info(f"Real-time client connected: {client_id}")
|
| 49 |
+
return client_id
|
| 50 |
+
|
| 51 |
+
def disconnect(self, client_id: str):
|
| 52 |
+
"""Remove client and clean up subscriptions"""
|
| 53 |
+
if client_id in self.active_connections:
|
| 54 |
+
del self.active_connections[client_id]
|
| 55 |
+
|
| 56 |
+
if client_id in self.subscriptions:
|
| 57 |
+
# Remove from all channel subscriber lists
|
| 58 |
+
for channel in self.subscriptions[client_id]:
|
| 59 |
+
if channel in self.channel_subscribers:
|
| 60 |
+
self.channel_subscribers[channel].discard(client_id)
|
| 61 |
+
del self.subscriptions[client_id]
|
| 62 |
+
|
| 63 |
+
logger.info(f"Real-time client disconnected: {client_id}")
|
| 64 |
+
|
| 65 |
+
def subscribe(self, client_id: str, channel: str):
|
| 66 |
+
"""Subscribe client to a channel"""
|
| 67 |
+
if client_id not in self.subscriptions:
|
| 68 |
+
self.subscriptions[client_id] = set()
|
| 69 |
+
self.subscriptions[client_id].add(channel)
|
| 70 |
+
|
| 71 |
+
if channel not in self.channel_subscribers:
|
| 72 |
+
self.channel_subscribers[channel] = set()
|
| 73 |
+
self.channel_subscribers[channel].add(client_id)
|
| 74 |
+
|
| 75 |
+
logger.debug(f"Client {client_id} subscribed to {channel}")
|
| 76 |
+
|
| 77 |
+
def unsubscribe(self, client_id: str, channel: str):
|
| 78 |
+
"""Unsubscribe client from a channel"""
|
| 79 |
+
if client_id in self.subscriptions:
|
| 80 |
+
self.subscriptions[client_id].discard(channel)
|
| 81 |
+
if channel in self.channel_subscribers:
|
| 82 |
+
self.channel_subscribers[channel].discard(client_id)
|
| 83 |
+
|
| 84 |
+
async def broadcast_to_channel(self, channel: str, data: Dict[str, Any]):
|
| 85 |
+
"""Broadcast message to all subscribers of a channel"""
|
| 86 |
+
if channel not in self.channel_subscribers:
|
| 87 |
+
return
|
| 88 |
+
|
| 89 |
+
message = {
|
| 90 |
+
"channel": channel,
|
| 91 |
+
"data": data,
|
| 92 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
disconnected = []
|
| 96 |
+
for client_id in self.channel_subscribers[channel]:
|
| 97 |
+
try:
|
| 98 |
+
websocket = self.active_connections.get(client_id)
|
| 99 |
+
if websocket:
|
| 100 |
+
await websocket.send_json(message)
|
| 101 |
+
except Exception as e:
|
| 102 |
+
logger.warning(f"Failed to send to {client_id}: {e}")
|
| 103 |
+
disconnected.append(client_id)
|
| 104 |
+
|
| 105 |
+
# Clean up disconnected clients
|
| 106 |
+
for client_id in disconnected:
|
| 107 |
+
self.disconnect(client_id)
|
| 108 |
+
|
| 109 |
+
async def send_to_client(self, client_id: str, data: Dict[str, Any]):
|
| 110 |
+
"""Send message to specific client"""
|
| 111 |
+
websocket = self.active_connections.get(client_id)
|
| 112 |
+
if websocket:
|
| 113 |
+
try:
|
| 114 |
+
await websocket.send_json(data)
|
| 115 |
+
except Exception as e:
|
| 116 |
+
logger.warning(f"Failed to send to {client_id}: {e}")
|
| 117 |
+
self.disconnect(client_id)
|
| 118 |
+
|
| 119 |
+
def get_stats(self) -> Dict[str, Any]:
|
| 120 |
+
"""Get connection statistics"""
|
| 121 |
+
return {
|
| 122 |
+
"total_connections": len(self.active_connections),
|
| 123 |
+
"channels": {
|
| 124 |
+
channel: len(subscribers)
|
| 125 |
+
for channel, subscribers in self.channel_subscribers.items()
|
| 126 |
+
},
|
| 127 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
# Global connection manager
|
| 132 |
+
connection_manager = RealTimeConnectionManager()
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
# ===== AVAILABLE CHANNELS =====
|
| 136 |
+
|
| 137 |
+
class Channels:
|
| 138 |
+
"""Available WebSocket channels"""
|
| 139 |
+
MARKET_DATA = "market_data"
|
| 140 |
+
PRICE_UPDATES = "price_updates"
|
| 141 |
+
NEWS = "news"
|
| 142 |
+
SENTIMENT = "sentiment"
|
| 143 |
+
WHALE_ALERTS = "whale_alerts"
|
| 144 |
+
COLLECTION_STATUS = "collection_status"
|
| 145 |
+
SYSTEM_HEALTH = "system_health"
|
| 146 |
+
ALL = "all"
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
# ===== REAL-TIME PUBLISHER =====
|
| 150 |
+
|
| 151 |
+
class RealTimePublisher:
|
| 152 |
+
"""
|
| 153 |
+
Publishes data to WebSocket channels in real-time
|
| 154 |
+
Used by data collectors to push updates
|
| 155 |
+
"""
|
| 156 |
+
|
| 157 |
+
def __init__(self, manager: RealTimeConnectionManager):
|
| 158 |
+
self.manager = manager
|
| 159 |
+
self.last_data: Dict[str, Any] = {} # Cache last data per channel
|
| 160 |
+
|
| 161 |
+
async def publish_market_data(self, data: List[Dict[str, Any]]):
|
| 162 |
+
"""Publish market data update"""
|
| 163 |
+
# Only publish if data has changed significantly
|
| 164 |
+
if self._has_significant_change(Channels.MARKET_DATA, data):
|
| 165 |
+
await self.manager.broadcast_to_channel(Channels.MARKET_DATA, {
|
| 166 |
+
"type": "market_update",
|
| 167 |
+
"coins": data,
|
| 168 |
+
"count": len(data)
|
| 169 |
+
})
|
| 170 |
+
self.last_data[Channels.MARKET_DATA] = data
|
| 171 |
+
|
| 172 |
+
async def publish_price_update(self, symbol: str, price: float, change_24h: float = None):
|
| 173 |
+
"""Publish single price update"""
|
| 174 |
+
await self.manager.broadcast_to_channel(Channels.PRICE_UPDATES, {
|
| 175 |
+
"type": "price_update",
|
| 176 |
+
"symbol": symbol,
|
| 177 |
+
"price": price,
|
| 178 |
+
"change_24h": change_24h
|
| 179 |
+
})
|
| 180 |
+
|
| 181 |
+
async def publish_news(self, articles: List[Dict[str, Any]]):
|
| 182 |
+
"""Publish news articles"""
|
| 183 |
+
await self.manager.broadcast_to_channel(Channels.NEWS, {
|
| 184 |
+
"type": "news_update",
|
| 185 |
+
"articles": articles,
|
| 186 |
+
"count": len(articles)
|
| 187 |
+
})
|
| 188 |
+
|
| 189 |
+
async def publish_sentiment(self, sentiment_data: Dict[str, Any]):
|
| 190 |
+
"""Publish sentiment update"""
|
| 191 |
+
await self.manager.broadcast_to_channel(Channels.SENTIMENT, {
|
| 192 |
+
"type": "sentiment_update",
|
| 193 |
+
"data": sentiment_data
|
| 194 |
+
})
|
| 195 |
+
|
| 196 |
+
async def publish_whale_alert(self, transaction: Dict[str, Any]):
|
| 197 |
+
"""Publish whale transaction alert"""
|
| 198 |
+
await self.manager.broadcast_to_channel(Channels.WHALE_ALERTS, {
|
| 199 |
+
"type": "whale_alert",
|
| 200 |
+
"transaction": transaction
|
| 201 |
+
})
|
| 202 |
+
|
| 203 |
+
async def publish_collection_status(self, collector_name: str, status: Dict[str, Any]):
|
| 204 |
+
"""Publish data collection status"""
|
| 205 |
+
await self.manager.broadcast_to_channel(Channels.COLLECTION_STATUS, {
|
| 206 |
+
"type": "collection_status",
|
| 207 |
+
"collector": collector_name,
|
| 208 |
+
"status": status
|
| 209 |
+
})
|
| 210 |
+
|
| 211 |
+
async def publish_system_health(self, health_data: Dict[str, Any]):
|
| 212 |
+
"""Publish system health update"""
|
| 213 |
+
await self.manager.broadcast_to_channel(Channels.SYSTEM_HEALTH, {
|
| 214 |
+
"type": "health_update",
|
| 215 |
+
"data": health_data
|
| 216 |
+
})
|
| 217 |
+
|
| 218 |
+
def _has_significant_change(self, channel: str, new_data: Any) -> bool:
|
| 219 |
+
"""Check if data has changed significantly (to avoid spam)"""
|
| 220 |
+
if channel not in self.last_data:
|
| 221 |
+
return True
|
| 222 |
+
|
| 223 |
+
# For market data, check if any price changed more than 0.1%
|
| 224 |
+
if channel == Channels.MARKET_DATA:
|
| 225 |
+
old_prices = {d.get("symbol"): d.get("price", 0) for d in self.last_data.get(channel, [])}
|
| 226 |
+
for item in new_data:
|
| 227 |
+
symbol = item.get("symbol")
|
| 228 |
+
new_price = item.get("price", 0)
|
| 229 |
+
old_price = old_prices.get(symbol, 0)
|
| 230 |
+
if old_price > 0 and abs((new_price - old_price) / old_price) > 0.001:
|
| 231 |
+
return True
|
| 232 |
+
return False
|
| 233 |
+
|
| 234 |
+
return True
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
# Global publisher
|
| 238 |
+
publisher = RealTimePublisher(connection_manager)
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
def get_realtime_publisher() -> RealTimePublisher:
|
| 242 |
+
"""Get global publisher instance"""
|
| 243 |
+
return publisher
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
# ===== WEBSOCKET ENDPOINTS =====
|
| 247 |
+
|
| 248 |
+
@router.websocket("/ws/realtime")
|
| 249 |
+
async def websocket_realtime(websocket: WebSocket):
|
| 250 |
+
"""
|
| 251 |
+
Main real-time WebSocket endpoint
|
| 252 |
+
|
| 253 |
+
After connecting, send subscription messages:
|
| 254 |
+
{
|
| 255 |
+
"action": "subscribe",
|
| 256 |
+
"channels": ["market_data", "news", "sentiment"]
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
Or subscribe to all:
|
| 260 |
+
{
|
| 261 |
+
"action": "subscribe",
|
| 262 |
+
"channels": ["all"]
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
To unsubscribe:
|
| 266 |
+
{
|
| 267 |
+
"action": "unsubscribe",
|
| 268 |
+
"channels": ["news"]
|
| 269 |
+
}
|
| 270 |
+
"""
|
| 271 |
+
client_id = await connection_manager.connect(websocket)
|
| 272 |
+
|
| 273 |
+
try:
|
| 274 |
+
# Send welcome message with available channels
|
| 275 |
+
await websocket.send_json({
|
| 276 |
+
"type": "connected",
|
| 277 |
+
"client_id": client_id,
|
| 278 |
+
"available_channels": [
|
| 279 |
+
Channels.MARKET_DATA,
|
| 280 |
+
Channels.PRICE_UPDATES,
|
| 281 |
+
Channels.NEWS,
|
| 282 |
+
Channels.SENTIMENT,
|
| 283 |
+
Channels.WHALE_ALERTS,
|
| 284 |
+
Channels.COLLECTION_STATUS,
|
| 285 |
+
Channels.SYSTEM_HEALTH,
|
| 286 |
+
Channels.ALL
|
| 287 |
+
],
|
| 288 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 289 |
+
})
|
| 290 |
+
|
| 291 |
+
while True:
|
| 292 |
+
data = await websocket.receive_json()
|
| 293 |
+
action = data.get("action")
|
| 294 |
+
channels = data.get("channels", [])
|
| 295 |
+
|
| 296 |
+
if action == "subscribe":
|
| 297 |
+
if Channels.ALL in channels:
|
| 298 |
+
# Subscribe to all channels
|
| 299 |
+
for channel in [Channels.MARKET_DATA, Channels.PRICE_UPDATES,
|
| 300 |
+
Channels.NEWS, Channels.SENTIMENT,
|
| 301 |
+
Channels.WHALE_ALERTS, Channels.COLLECTION_STATUS,
|
| 302 |
+
Channels.SYSTEM_HEALTH]:
|
| 303 |
+
connection_manager.subscribe(client_id, channel)
|
| 304 |
+
else:
|
| 305 |
+
for channel in channels:
|
| 306 |
+
connection_manager.subscribe(client_id, channel)
|
| 307 |
+
|
| 308 |
+
await websocket.send_json({
|
| 309 |
+
"type": "subscribed",
|
| 310 |
+
"channels": list(connection_manager.subscriptions.get(client_id, set())),
|
| 311 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 312 |
+
})
|
| 313 |
+
|
| 314 |
+
elif action == "unsubscribe":
|
| 315 |
+
for channel in channels:
|
| 316 |
+
connection_manager.unsubscribe(client_id, channel)
|
| 317 |
+
|
| 318 |
+
await websocket.send_json({
|
| 319 |
+
"type": "unsubscribed",
|
| 320 |
+
"channels": channels,
|
| 321 |
+
"remaining": list(connection_manager.subscriptions.get(client_id, set())),
|
| 322 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 323 |
+
})
|
| 324 |
+
|
| 325 |
+
elif action == "get_stats":
|
| 326 |
+
await websocket.send_json({
|
| 327 |
+
"type": "stats",
|
| 328 |
+
"data": connection_manager.get_stats()
|
| 329 |
+
})
|
| 330 |
+
|
| 331 |
+
elif action == "ping":
|
| 332 |
+
await websocket.send_json({
|
| 333 |
+
"type": "pong",
|
| 334 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 335 |
+
})
|
| 336 |
+
|
| 337 |
+
except WebSocketDisconnect:
|
| 338 |
+
logger.info(f"Client {client_id} disconnected")
|
| 339 |
+
except Exception as e:
|
| 340 |
+
logger.error(f"WebSocket error for {client_id}: {e}")
|
| 341 |
+
finally:
|
| 342 |
+
connection_manager.disconnect(client_id)
|
| 343 |
+
|
| 344 |
+
|
| 345 |
+
@router.websocket("/ws/prices")
|
| 346 |
+
async def websocket_prices(websocket: WebSocket):
|
| 347 |
+
"""Dedicated WebSocket for price updates only"""
|
| 348 |
+
client_id = await connection_manager.connect(websocket)
|
| 349 |
+
connection_manager.subscribe(client_id, Channels.PRICE_UPDATES)
|
| 350 |
+
connection_manager.subscribe(client_id, Channels.MARKET_DATA)
|
| 351 |
+
|
| 352 |
+
try:
|
| 353 |
+
await websocket.send_json({
|
| 354 |
+
"type": "connected",
|
| 355 |
+
"channels": [Channels.PRICE_UPDATES, Channels.MARKET_DATA],
|
| 356 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 357 |
+
})
|
| 358 |
+
|
| 359 |
+
while True:
|
| 360 |
+
data = await websocket.receive_json()
|
| 361 |
+
if data.get("action") == "ping":
|
| 362 |
+
await websocket.send_json({"type": "pong"})
|
| 363 |
+
|
| 364 |
+
except WebSocketDisconnect:
|
| 365 |
+
pass
|
| 366 |
+
except Exception as e:
|
| 367 |
+
logger.error(f"Price WebSocket error: {e}")
|
| 368 |
+
finally:
|
| 369 |
+
connection_manager.disconnect(client_id)
|
| 370 |
+
|
| 371 |
+
|
| 372 |
+
@router.websocket("/ws/alerts")
|
| 373 |
+
async def websocket_alerts(websocket: WebSocket):
|
| 374 |
+
"""Dedicated WebSocket for alerts (whale, sentiment changes)"""
|
| 375 |
+
client_id = await connection_manager.connect(websocket)
|
| 376 |
+
connection_manager.subscribe(client_id, Channels.WHALE_ALERTS)
|
| 377 |
+
connection_manager.subscribe(client_id, Channels.SENTIMENT)
|
| 378 |
+
|
| 379 |
+
try:
|
| 380 |
+
await websocket.send_json({
|
| 381 |
+
"type": "connected",
|
| 382 |
+
"channels": [Channels.WHALE_ALERTS, Channels.SENTIMENT],
|
| 383 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 384 |
+
})
|
| 385 |
+
|
| 386 |
+
while True:
|
| 387 |
+
data = await websocket.receive_json()
|
| 388 |
+
if data.get("action") == "ping":
|
| 389 |
+
await websocket.send_json({"type": "pong"})
|
| 390 |
+
|
| 391 |
+
except WebSocketDisconnect:
|
| 392 |
+
pass
|
| 393 |
+
except Exception as e:
|
| 394 |
+
logger.error(f"Alerts WebSocket error: {e}")
|
| 395 |
+
finally:
|
| 396 |
+
connection_manager.disconnect(client_id)
|
| 397 |
+
|
| 398 |
+
|
| 399 |
+
# ===== BACKGROUND TASKS =====
|
| 400 |
+
|
| 401 |
+
async def start_realtime_monitoring():
|
| 402 |
+
"""Start real-time monitoring background tasks"""
|
| 403 |
+
logger.info("Starting real-time monitoring services...")
|
| 404 |
+
|
| 405 |
+
# Import data collection worker
|
| 406 |
+
try:
|
| 407 |
+
from workers.data_collection_worker import get_data_collection_worker, get_realtime_fetcher
|
| 408 |
+
worker = get_data_collection_worker()
|
| 409 |
+
fetcher = get_realtime_fetcher()
|
| 410 |
+
|
| 411 |
+
# Start periodic health check broadcasts
|
| 412 |
+
asyncio.create_task(_broadcast_health_status())
|
| 413 |
+
|
| 414 |
+
# Start periodic market data broadcasts
|
| 415 |
+
asyncio.create_task(_broadcast_market_updates(fetcher))
|
| 416 |
+
|
| 417 |
+
logger.info("Real-time monitoring services started")
|
| 418 |
+
except Exception as e:
|
| 419 |
+
logger.error(f"Failed to start real-time monitoring: {e}")
|
| 420 |
+
|
| 421 |
+
|
| 422 |
+
async def _broadcast_health_status():
|
| 423 |
+
"""Periodically broadcast system health"""
|
| 424 |
+
while True:
|
| 425 |
+
try:
|
| 426 |
+
health_data = {
|
| 427 |
+
"status": "healthy",
|
| 428 |
+
"connections": connection_manager.get_stats(),
|
| 429 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 430 |
+
}
|
| 431 |
+
await publisher.publish_system_health(health_data)
|
| 432 |
+
except Exception as e:
|
| 433 |
+
logger.error(f"Health broadcast error: {e}")
|
| 434 |
+
|
| 435 |
+
await asyncio.sleep(30) # Every 30 seconds
|
| 436 |
+
|
| 437 |
+
|
| 438 |
+
async def _broadcast_market_updates(fetcher):
|
| 439 |
+
"""Periodically broadcast market updates"""
|
| 440 |
+
while True:
|
| 441 |
+
try:
|
| 442 |
+
# Only broadcast if there are subscribers
|
| 443 |
+
if connection_manager.channel_subscribers.get(Channels.MARKET_DATA):
|
| 444 |
+
# Fetch latest data
|
| 445 |
+
price_result = await fetcher.fetch_price("BTC")
|
| 446 |
+
if price_result.get("success"):
|
| 447 |
+
await publisher.publish_price_update(
|
| 448 |
+
"BTC",
|
| 449 |
+
price_result.get("price"),
|
| 450 |
+
None
|
| 451 |
+
)
|
| 452 |
+
except Exception as e:
|
| 453 |
+
logger.error(f"Market broadcast error: {e}")
|
| 454 |
+
|
| 455 |
+
await asyncio.sleep(60) # Every minute
|
| 456 |
+
|
| 457 |
+
|
| 458 |
+
# ===== HTTP ENDPOINTS FOR STATS =====
|
| 459 |
+
|
| 460 |
+
@router.get("/api/realtime/stats")
|
| 461 |
+
async def get_realtime_stats():
|
| 462 |
+
"""Get real-time connection statistics"""
|
| 463 |
+
return {
|
| 464 |
+
"success": True,
|
| 465 |
+
"data": connection_manager.get_stats()
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
|
| 469 |
+
@router.get("/api/realtime/channels")
|
| 470 |
+
async def get_available_channels():
|
| 471 |
+
"""Get available real-time channels"""
|
| 472 |
+
return {
|
| 473 |
+
"success": True,
|
| 474 |
+
"channels": [
|
| 475 |
+
{"id": Channels.MARKET_DATA, "name": "Market Data", "description": "Real-time market prices and stats"},
|
| 476 |
+
{"id": Channels.PRICE_UPDATES, "name": "Price Updates", "description": "Individual price changes"},
|
| 477 |
+
{"id": Channels.NEWS, "name": "News", "description": "Latest crypto news articles"},
|
| 478 |
+
{"id": Channels.SENTIMENT, "name": "Sentiment", "description": "Market sentiment updates"},
|
| 479 |
+
{"id": Channels.WHALE_ALERTS, "name": "Whale Alerts", "description": "Large transaction alerts"},
|
| 480 |
+
{"id": Channels.COLLECTION_STATUS, "name": "Collection Status", "description": "Data collection progress"},
|
| 481 |
+
{"id": Channels.SYSTEM_HEALTH, "name": "System Health", "description": "System health monitoring"}
|
| 482 |
+
]
|
| 483 |
+
}
|
backend/providers/free_resources.py
ADDED
|
@@ -0,0 +1,1018 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Free Resources Provider - Comprehensive Collection of Crypto Data Sources
|
| 3 |
+
Based on NewResourceApi documentation and additional verified sources
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from dataclasses import dataclass, field
|
| 7 |
+
from typing import List, Dict, Any, Optional
|
| 8 |
+
from enum import Enum
|
| 9 |
+
import os
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class ResourceType(Enum):
|
| 13 |
+
MARKET_DATA = "market_data"
|
| 14 |
+
NEWS = "news"
|
| 15 |
+
SENTIMENT = "sentiment"
|
| 16 |
+
BLOCKCHAIN = "blockchain"
|
| 17 |
+
ONCHAIN = "onchain"
|
| 18 |
+
DEFI = "defi"
|
| 19 |
+
WHALE_TRACKING = "whale_tracking"
|
| 20 |
+
TECHNICAL = "technical"
|
| 21 |
+
AI_MODEL = "ai_model"
|
| 22 |
+
SOCIAL = "social"
|
| 23 |
+
HISTORICAL = "historical"
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class TimeFrame(Enum):
|
| 27 |
+
REALTIME = "realtime"
|
| 28 |
+
MINUTE_1 = "1m"
|
| 29 |
+
MINUTE_5 = "5m"
|
| 30 |
+
MINUTE_15 = "15m"
|
| 31 |
+
MINUTE_30 = "30m"
|
| 32 |
+
HOUR_1 = "1h"
|
| 33 |
+
HOUR_4 = "4h"
|
| 34 |
+
DAY_1 = "1d"
|
| 35 |
+
WEEK_1 = "1w"
|
| 36 |
+
MONTH_1 = "1M"
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
@dataclass
|
| 40 |
+
class APIResource:
|
| 41 |
+
"""Data class for API Resource configuration"""
|
| 42 |
+
id: str
|
| 43 |
+
name: str
|
| 44 |
+
resource_type: ResourceType
|
| 45 |
+
base_url: str
|
| 46 |
+
api_key_env: str = ""
|
| 47 |
+
api_key: str = ""
|
| 48 |
+
rate_limit: str = "unlimited"
|
| 49 |
+
is_free: bool = True
|
| 50 |
+
requires_auth: bool = False
|
| 51 |
+
is_active: bool = True
|
| 52 |
+
priority: int = 1
|
| 53 |
+
description: str = ""
|
| 54 |
+
endpoints: Dict[str, str] = field(default_factory=dict)
|
| 55 |
+
supported_timeframes: List[str] = field(default_factory=list)
|
| 56 |
+
features: List[str] = field(default_factory=list)
|
| 57 |
+
headers: Dict[str, str] = field(default_factory=dict)
|
| 58 |
+
documentation_url: str = ""
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
class FreeResourcesRegistry:
|
| 62 |
+
"""Registry of all available free and configured API resources"""
|
| 63 |
+
|
| 64 |
+
def __init__(self):
|
| 65 |
+
self.resources: Dict[str, APIResource] = {}
|
| 66 |
+
self._load_all_resources()
|
| 67 |
+
|
| 68 |
+
def _load_all_resources(self):
|
| 69 |
+
"""Load all available resources"""
|
| 70 |
+
self._load_block_explorers()
|
| 71 |
+
self._load_market_data_sources()
|
| 72 |
+
self._load_news_sources()
|
| 73 |
+
self._load_sentiment_sources()
|
| 74 |
+
self._load_onchain_analytics()
|
| 75 |
+
self._load_defi_sources()
|
| 76 |
+
self._load_whale_tracking()
|
| 77 |
+
self._load_technical_analysis()
|
| 78 |
+
self._load_social_sources()
|
| 79 |
+
self._load_historical_sources()
|
| 80 |
+
|
| 81 |
+
def _load_block_explorers(self):
|
| 82 |
+
"""Block explorer APIs - Etherscan, BscScan, TronScan, etc."""
|
| 83 |
+
|
| 84 |
+
# Etherscan - Ethereum
|
| 85 |
+
self.resources["etherscan"] = APIResource(
|
| 86 |
+
id="etherscan",
|
| 87 |
+
name="Etherscan",
|
| 88 |
+
resource_type=ResourceType.BLOCKCHAIN,
|
| 89 |
+
base_url="https://api.etherscan.io/api",
|
| 90 |
+
api_key_env="ETHERSCAN_KEY",
|
| 91 |
+
api_key=os.getenv("ETHERSCAN_KEY", "SZHYFZK2RR8H9TIMJBVW54V4H81K2Z2KR2"),
|
| 92 |
+
rate_limit="5 req/sec",
|
| 93 |
+
is_free=True,
|
| 94 |
+
requires_auth=True,
|
| 95 |
+
description="Ethereum blockchain explorer API",
|
| 96 |
+
endpoints={
|
| 97 |
+
"account_balance": "?module=account&action=balance",
|
| 98 |
+
"account_txlist": "?module=account&action=txlist",
|
| 99 |
+
"token_balance": "?module=account&action=tokenbalance",
|
| 100 |
+
"gas_price": "?module=gastracker&action=gasoracle",
|
| 101 |
+
"eth_price": "?module=stats&action=ethprice",
|
| 102 |
+
"block_by_time": "?module=block&action=getblocknobytime",
|
| 103 |
+
"contract_abi": "?module=contract&action=getabi",
|
| 104 |
+
"token_transfers": "?module=account&action=tokentx"
|
| 105 |
+
},
|
| 106 |
+
features=["transactions", "tokens", "gas", "prices", "contracts"],
|
| 107 |
+
documentation_url="https://docs.etherscan.io/"
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
# BscScan - Binance Smart Chain
|
| 111 |
+
self.resources["bscscan"] = APIResource(
|
| 112 |
+
id="bscscan",
|
| 113 |
+
name="BscScan",
|
| 114 |
+
resource_type=ResourceType.BLOCKCHAIN,
|
| 115 |
+
base_url="https://api.bscscan.com/api",
|
| 116 |
+
api_key_env="BSCSCAN_KEY",
|
| 117 |
+
api_key=os.getenv("BSCSCAN_KEY", "K62RKHGXTDCG53RU4MCG6XABIMJKTN19IT"),
|
| 118 |
+
rate_limit="5 req/sec",
|
| 119 |
+
is_free=True,
|
| 120 |
+
requires_auth=True,
|
| 121 |
+
description="BSC blockchain explorer API",
|
| 122 |
+
endpoints={
|
| 123 |
+
"account_balance": "?module=account&action=balance",
|
| 124 |
+
"account_txlist": "?module=account&action=txlist",
|
| 125 |
+
"token_balance": "?module=account&action=tokenbalance",
|
| 126 |
+
"gas_price": "?module=gastracker&action=gasoracle",
|
| 127 |
+
"bnb_price": "?module=stats&action=bnbprice",
|
| 128 |
+
"token_transfers": "?module=account&action=tokentx"
|
| 129 |
+
},
|
| 130 |
+
features=["transactions", "tokens", "gas", "prices", "contracts"],
|
| 131 |
+
documentation_url="https://docs.bscscan.com/"
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
# TronScan - Tron Network
|
| 135 |
+
self.resources["tronscan"] = APIResource(
|
| 136 |
+
id="tronscan",
|
| 137 |
+
name="TronScan",
|
| 138 |
+
resource_type=ResourceType.BLOCKCHAIN,
|
| 139 |
+
base_url="https://apilist.tronscanapi.com/api",
|
| 140 |
+
api_key_env="TRONSCAN_KEY",
|
| 141 |
+
api_key=os.getenv("TRONSCAN_KEY", "7ae72726-bffe-4e74-9c33-97b761eeea21"),
|
| 142 |
+
rate_limit="varies",
|
| 143 |
+
is_free=True,
|
| 144 |
+
requires_auth=True,
|
| 145 |
+
description="Tron blockchain explorer API",
|
| 146 |
+
endpoints={
|
| 147 |
+
"account": "/account",
|
| 148 |
+
"account_list": "/accountv2",
|
| 149 |
+
"transaction": "/transaction",
|
| 150 |
+
"transaction_info": "/transaction-info",
|
| 151 |
+
"token": "/token",
|
| 152 |
+
"token_trc10": "/token_trc10",
|
| 153 |
+
"token_trc20": "/token_trc20",
|
| 154 |
+
"contract": "/contract",
|
| 155 |
+
"node": "/node"
|
| 156 |
+
},
|
| 157 |
+
headers={"TRON-PRO-API-KEY": os.getenv("TRONSCAN_KEY", "7ae72726-bffe-4e74-9c33-97b761eeea21")},
|
| 158 |
+
features=["transactions", "tokens", "contracts", "trc10", "trc20"],
|
| 159 |
+
documentation_url="https://tronscan.org/#/doc"
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
# Polygonscan - Polygon Network
|
| 163 |
+
self.resources["polygonscan"] = APIResource(
|
| 164 |
+
id="polygonscan",
|
| 165 |
+
name="Polygonscan",
|
| 166 |
+
resource_type=ResourceType.BLOCKCHAIN,
|
| 167 |
+
base_url="https://api.polygonscan.com/api",
|
| 168 |
+
api_key_env="POLYGONSCAN_KEY",
|
| 169 |
+
rate_limit="5 req/sec",
|
| 170 |
+
is_free=True,
|
| 171 |
+
requires_auth=True,
|
| 172 |
+
description="Polygon blockchain explorer API",
|
| 173 |
+
endpoints={
|
| 174 |
+
"account_balance": "?module=account&action=balance",
|
| 175 |
+
"account_txlist": "?module=account&action=txlist",
|
| 176 |
+
"token_balance": "?module=account&action=tokenbalance",
|
| 177 |
+
"gas_price": "?module=gastracker&action=gasoracle",
|
| 178 |
+
"matic_price": "?module=stats&action=maticprice"
|
| 179 |
+
},
|
| 180 |
+
features=["transactions", "tokens", "gas", "prices"],
|
| 181 |
+
documentation_url="https://docs.polygonscan.com/"
|
| 182 |
+
)
|
| 183 |
+
|
| 184 |
+
# Blockchair - Multi-chain
|
| 185 |
+
self.resources["blockchair"] = APIResource(
|
| 186 |
+
id="blockchair",
|
| 187 |
+
name="Blockchair",
|
| 188 |
+
resource_type=ResourceType.BLOCKCHAIN,
|
| 189 |
+
base_url="https://api.blockchair.com",
|
| 190 |
+
rate_limit="30 req/min free",
|
| 191 |
+
is_free=True,
|
| 192 |
+
requires_auth=False,
|
| 193 |
+
description="Multi-chain blockchain explorer API",
|
| 194 |
+
endpoints={
|
| 195 |
+
"bitcoin_stats": "/bitcoin/stats",
|
| 196 |
+
"ethereum_stats": "/ethereum/stats",
|
| 197 |
+
"bitcoin_blocks": "/bitcoin/blocks",
|
| 198 |
+
"ethereum_blocks": "/ethereum/blocks",
|
| 199 |
+
"bitcoin_transactions": "/bitcoin/transactions",
|
| 200 |
+
"ethereum_transactions": "/ethereum/transactions"
|
| 201 |
+
},
|
| 202 |
+
features=["multi-chain", "transactions", "blocks", "stats"],
|
| 203 |
+
documentation_url="https://blockchair.com/api/docs"
|
| 204 |
+
)
|
| 205 |
+
|
| 206 |
+
def _load_market_data_sources(self):
|
| 207 |
+
"""Market data sources - CoinMarketCap, CoinGecko, etc."""
|
| 208 |
+
|
| 209 |
+
# CoinMarketCap
|
| 210 |
+
self.resources["coinmarketcap"] = APIResource(
|
| 211 |
+
id="coinmarketcap",
|
| 212 |
+
name="CoinMarketCap",
|
| 213 |
+
resource_type=ResourceType.MARKET_DATA,
|
| 214 |
+
base_url="https://pro-api.coinmarketcap.com/v1",
|
| 215 |
+
api_key_env="COINMARKETCAP_KEY",
|
| 216 |
+
api_key=os.getenv("COINMARKETCAP_KEY", "a35ffaec-c66c-4f16-81e3-41a717e4822f"),
|
| 217 |
+
rate_limit="333 req/day free",
|
| 218 |
+
is_free=True,
|
| 219 |
+
requires_auth=True,
|
| 220 |
+
description="Leading cryptocurrency market data API",
|
| 221 |
+
endpoints={
|
| 222 |
+
"listings_latest": "/cryptocurrency/listings/latest",
|
| 223 |
+
"quotes_latest": "/cryptocurrency/quotes/latest",
|
| 224 |
+
"info": "/cryptocurrency/info",
|
| 225 |
+
"map": "/cryptocurrency/map",
|
| 226 |
+
"categories": "/cryptocurrency/categories",
|
| 227 |
+
"global_metrics": "/global-metrics/quotes/latest",
|
| 228 |
+
"exchange_listings": "/exchange/listings/latest"
|
| 229 |
+
},
|
| 230 |
+
headers={"X-CMC_PRO_API_KEY": os.getenv("COINMARKETCAP_KEY", "a35ffaec-c66c-4f16-81e3-41a717e4822f")},
|
| 231 |
+
features=["prices", "market_cap", "volume", "rankings", "historical"],
|
| 232 |
+
supported_timeframes=["1h", "24h", "7d", "30d", "60d", "90d"],
|
| 233 |
+
documentation_url="https://coinmarketcap.com/api/documentation/v1/"
|
| 234 |
+
)
|
| 235 |
+
|
| 236 |
+
# CoinGecko
|
| 237 |
+
self.resources["coingecko"] = APIResource(
|
| 238 |
+
id="coingecko",
|
| 239 |
+
name="CoinGecko",
|
| 240 |
+
resource_type=ResourceType.MARKET_DATA,
|
| 241 |
+
base_url="https://api.coingecko.com/api/v3",
|
| 242 |
+
rate_limit="10-50 req/min free",
|
| 243 |
+
is_free=True,
|
| 244 |
+
requires_auth=False,
|
| 245 |
+
description="Comprehensive cryptocurrency data API",
|
| 246 |
+
endpoints={
|
| 247 |
+
"ping": "/ping",
|
| 248 |
+
"simple_price": "/simple/price",
|
| 249 |
+
"coins_list": "/coins/list",
|
| 250 |
+
"coins_markets": "/coins/markets",
|
| 251 |
+
"coin_detail": "/coins/{id}",
|
| 252 |
+
"coin_history": "/coins/{id}/history",
|
| 253 |
+
"coin_market_chart": "/coins/{id}/market_chart",
|
| 254 |
+
"coin_ohlc": "/coins/{id}/ohlc",
|
| 255 |
+
"trending": "/search/trending",
|
| 256 |
+
"global": "/global",
|
| 257 |
+
"exchanges": "/exchanges"
|
| 258 |
+
},
|
| 259 |
+
features=["prices", "market_cap", "volume", "historical", "trending", "defi"],
|
| 260 |
+
supported_timeframes=["1d", "7d", "14d", "30d", "90d", "180d", "365d", "max"],
|
| 261 |
+
documentation_url="https://www.coingecko.com/en/api/documentation"
|
| 262 |
+
)
|
| 263 |
+
|
| 264 |
+
# CoinCap
|
| 265 |
+
self.resources["coincap"] = APIResource(
|
| 266 |
+
id="coincap",
|
| 267 |
+
name="CoinCap",
|
| 268 |
+
resource_type=ResourceType.MARKET_DATA,
|
| 269 |
+
base_url="https://api.coincap.io/v2",
|
| 270 |
+
rate_limit="200 req/min free",
|
| 271 |
+
is_free=True,
|
| 272 |
+
requires_auth=False,
|
| 273 |
+
description="Real-time cryptocurrency market data",
|
| 274 |
+
endpoints={
|
| 275 |
+
"assets": "/assets",
|
| 276 |
+
"asset_detail": "/assets/{id}",
|
| 277 |
+
"asset_history": "/assets/{id}/history",
|
| 278 |
+
"markets": "/assets/{id}/markets",
|
| 279 |
+
"rates": "/rates",
|
| 280 |
+
"exchanges": "/exchanges",
|
| 281 |
+
"candles": "/candles"
|
| 282 |
+
},
|
| 283 |
+
features=["real-time", "prices", "volume", "market_cap", "historical"],
|
| 284 |
+
supported_timeframes=["m1", "m5", "m15", "m30", "h1", "h2", "h6", "h12", "d1"],
|
| 285 |
+
documentation_url="https://docs.coincap.io/"
|
| 286 |
+
)
|
| 287 |
+
|
| 288 |
+
# Binance
|
| 289 |
+
self.resources["binance"] = APIResource(
|
| 290 |
+
id="binance",
|
| 291 |
+
name="Binance",
|
| 292 |
+
resource_type=ResourceType.MARKET_DATA,
|
| 293 |
+
base_url="https://api.binance.com/api/v3",
|
| 294 |
+
rate_limit="1200 req/min",
|
| 295 |
+
is_free=True,
|
| 296 |
+
requires_auth=False,
|
| 297 |
+
description="Binance exchange public API",
|
| 298 |
+
endpoints={
|
| 299 |
+
"ping": "/ping",
|
| 300 |
+
"time": "/time",
|
| 301 |
+
"ticker_price": "/ticker/price",
|
| 302 |
+
"ticker_24hr": "/ticker/24hr",
|
| 303 |
+
"klines": "/klines",
|
| 304 |
+
"depth": "/depth",
|
| 305 |
+
"trades": "/trades",
|
| 306 |
+
"avg_price": "/avgPrice",
|
| 307 |
+
"exchange_info": "/exchangeInfo"
|
| 308 |
+
},
|
| 309 |
+
features=["real-time", "prices", "ohlcv", "order_book", "trades"],
|
| 310 |
+
supported_timeframes=["1m", "3m", "5m", "15m", "30m", "1h", "2h", "4h", "6h", "8h", "12h", "1d", "3d", "1w", "1M"],
|
| 311 |
+
documentation_url="https://binance-docs.github.io/apidocs/spot/en/"
|
| 312 |
+
)
|
| 313 |
+
|
| 314 |
+
# KuCoin
|
| 315 |
+
self.resources["kucoin"] = APIResource(
|
| 316 |
+
id="kucoin",
|
| 317 |
+
name="KuCoin",
|
| 318 |
+
resource_type=ResourceType.MARKET_DATA,
|
| 319 |
+
base_url="https://api.kucoin.com/api/v1",
|
| 320 |
+
rate_limit="varies",
|
| 321 |
+
is_free=True,
|
| 322 |
+
requires_auth=False,
|
| 323 |
+
description="KuCoin exchange public API",
|
| 324 |
+
endpoints={
|
| 325 |
+
"market_list": "/market/allTickers",
|
| 326 |
+
"ticker": "/market/orderbook/level1",
|
| 327 |
+
"market_stats": "/market/stats",
|
| 328 |
+
"currencies": "/currencies",
|
| 329 |
+
"symbols": "/symbols",
|
| 330 |
+
"klines": "/market/candles"
|
| 331 |
+
},
|
| 332 |
+
features=["prices", "ohlcv", "order_book", "trades"],
|
| 333 |
+
supported_timeframes=["1min", "3min", "5min", "15min", "30min", "1hour", "2hour", "4hour", "6hour", "8hour", "12hour", "1day", "1week"],
|
| 334 |
+
documentation_url="https://docs.kucoin.com/"
|
| 335 |
+
)
|
| 336 |
+
|
| 337 |
+
# Kraken
|
| 338 |
+
self.resources["kraken"] = APIResource(
|
| 339 |
+
id="kraken",
|
| 340 |
+
name="Kraken",
|
| 341 |
+
resource_type=ResourceType.MARKET_DATA,
|
| 342 |
+
base_url="https://api.kraken.com/0/public",
|
| 343 |
+
rate_limit="1 req/sec",
|
| 344 |
+
is_free=True,
|
| 345 |
+
requires_auth=False,
|
| 346 |
+
description="Kraken exchange public API",
|
| 347 |
+
endpoints={
|
| 348 |
+
"time": "/Time",
|
| 349 |
+
"assets": "/Assets",
|
| 350 |
+
"asset_pairs": "/AssetPairs",
|
| 351 |
+
"ticker": "/Ticker",
|
| 352 |
+
"ohlc": "/OHLC",
|
| 353 |
+
"depth": "/Depth",
|
| 354 |
+
"trades": "/Trades",
|
| 355 |
+
"spread": "/Spread"
|
| 356 |
+
},
|
| 357 |
+
features=["prices", "ohlcv", "order_book", "trades"],
|
| 358 |
+
supported_timeframes=["1", "5", "15", "30", "60", "240", "1440", "10080", "21600"],
|
| 359 |
+
documentation_url="https://docs.kraken.com/rest/"
|
| 360 |
+
)
|
| 361 |
+
|
| 362 |
+
def _load_news_sources(self):
|
| 363 |
+
"""News sources - NewsAPI, CryptoPanic, RSS feeds"""
|
| 364 |
+
|
| 365 |
+
# NewsAPI
|
| 366 |
+
self.resources["newsapi"] = APIResource(
|
| 367 |
+
id="newsapi",
|
| 368 |
+
name="NewsAPI",
|
| 369 |
+
resource_type=ResourceType.NEWS,
|
| 370 |
+
base_url="https://newsapi.org/v2",
|
| 371 |
+
api_key_env="NEWSAPI_KEY",
|
| 372 |
+
api_key=os.getenv("NEWSAPI_KEY", "968a5e25552b4cb5ba3280361d8444ab"),
|
| 373 |
+
rate_limit="100 req/day free",
|
| 374 |
+
is_free=True,
|
| 375 |
+
requires_auth=True,
|
| 376 |
+
description="News articles from thousands of sources",
|
| 377 |
+
endpoints={
|
| 378 |
+
"everything": "/everything",
|
| 379 |
+
"top_headlines": "/top-headlines",
|
| 380 |
+
"sources": "/sources"
|
| 381 |
+
},
|
| 382 |
+
features=["articles", "headlines", "sources", "search"],
|
| 383 |
+
documentation_url="https://newsapi.org/docs"
|
| 384 |
+
)
|
| 385 |
+
|
| 386 |
+
# CryptoPanic
|
| 387 |
+
self.resources["cryptopanic"] = APIResource(
|
| 388 |
+
id="cryptopanic",
|
| 389 |
+
name="CryptoPanic",
|
| 390 |
+
resource_type=ResourceType.NEWS,
|
| 391 |
+
base_url="https://cryptopanic.com/api/v1",
|
| 392 |
+
api_key_env="CRYPTOPANIC_KEY",
|
| 393 |
+
rate_limit="5 req/sec",
|
| 394 |
+
is_free=True,
|
| 395 |
+
requires_auth=True,
|
| 396 |
+
description="Cryptocurrency news aggregator",
|
| 397 |
+
endpoints={
|
| 398 |
+
"posts": "/posts/",
|
| 399 |
+
"currencies": "/currencies/"
|
| 400 |
+
},
|
| 401 |
+
features=["news", "sentiment", "trending"],
|
| 402 |
+
documentation_url="https://cryptopanic.com/developers/api/"
|
| 403 |
+
)
|
| 404 |
+
|
| 405 |
+
# CoinDesk RSS
|
| 406 |
+
self.resources["coindesk_rss"] = APIResource(
|
| 407 |
+
id="coindesk_rss",
|
| 408 |
+
name="CoinDesk RSS",
|
| 409 |
+
resource_type=ResourceType.NEWS,
|
| 410 |
+
base_url="https://www.coindesk.com",
|
| 411 |
+
rate_limit="unlimited",
|
| 412 |
+
is_free=True,
|
| 413 |
+
requires_auth=False,
|
| 414 |
+
description="CoinDesk crypto news RSS feed",
|
| 415 |
+
endpoints={
|
| 416 |
+
"rss": "/arc/outboundfeeds/rss/"
|
| 417 |
+
},
|
| 418 |
+
features=["news", "rss"],
|
| 419 |
+
documentation_url="https://www.coindesk.com/arc/outboundfeeds/rss/"
|
| 420 |
+
)
|
| 421 |
+
|
| 422 |
+
# Cointelegraph RSS
|
| 423 |
+
self.resources["cointelegraph_rss"] = APIResource(
|
| 424 |
+
id="cointelegraph_rss",
|
| 425 |
+
name="Cointelegraph RSS",
|
| 426 |
+
resource_type=ResourceType.NEWS,
|
| 427 |
+
base_url="https://cointelegraph.com",
|
| 428 |
+
rate_limit="unlimited",
|
| 429 |
+
is_free=True,
|
| 430 |
+
requires_auth=False,
|
| 431 |
+
description="Cointelegraph crypto news RSS feed",
|
| 432 |
+
endpoints={
|
| 433 |
+
"rss": "/rss"
|
| 434 |
+
},
|
| 435 |
+
features=["news", "rss"],
|
| 436 |
+
documentation_url="https://cointelegraph.com/rss"
|
| 437 |
+
)
|
| 438 |
+
|
| 439 |
+
# CryptoCompare News
|
| 440 |
+
self.resources["cryptocompare_news"] = APIResource(
|
| 441 |
+
id="cryptocompare_news",
|
| 442 |
+
name="CryptoCompare News",
|
| 443 |
+
resource_type=ResourceType.NEWS,
|
| 444 |
+
base_url="https://min-api.cryptocompare.com/data",
|
| 445 |
+
rate_limit="100,000 req/month free",
|
| 446 |
+
is_free=True,
|
| 447 |
+
requires_auth=False,
|
| 448 |
+
description="CryptoCompare news API",
|
| 449 |
+
endpoints={
|
| 450 |
+
"news_latest": "/v2/news/?lang=EN",
|
| 451 |
+
"news_feeds": "/news/feeds",
|
| 452 |
+
"news_categories": "/news/categories"
|
| 453 |
+
},
|
| 454 |
+
features=["news", "categories", "feeds"],
|
| 455 |
+
documentation_url="https://min-api.cryptocompare.com/documentation"
|
| 456 |
+
)
|
| 457 |
+
|
| 458 |
+
def _load_sentiment_sources(self):
|
| 459 |
+
"""Sentiment analysis sources"""
|
| 460 |
+
|
| 461 |
+
# Alternative.me Fear & Greed
|
| 462 |
+
self.resources["fear_greed_index"] = APIResource(
|
| 463 |
+
id="fear_greed_index",
|
| 464 |
+
name="Fear & Greed Index",
|
| 465 |
+
resource_type=ResourceType.SENTIMENT,
|
| 466 |
+
base_url="https://api.alternative.me",
|
| 467 |
+
rate_limit="unlimited",
|
| 468 |
+
is_free=True,
|
| 469 |
+
requires_auth=False,
|
| 470 |
+
description="Crypto Fear & Greed Index",
|
| 471 |
+
endpoints={
|
| 472 |
+
"fng": "/fng/",
|
| 473 |
+
"fng_history": "/fng/?limit=30"
|
| 474 |
+
},
|
| 475 |
+
features=["sentiment", "fear_greed", "historical"],
|
| 476 |
+
supported_timeframes=["daily"],
|
| 477 |
+
documentation_url="https://alternative.me/crypto/fear-and-greed-index/"
|
| 478 |
+
)
|
| 479 |
+
|
| 480 |
+
# Custom Sentiment API
|
| 481 |
+
self.resources["custom_sentiment"] = APIResource(
|
| 482 |
+
id="custom_sentiment",
|
| 483 |
+
name="Custom Sentiment API",
|
| 484 |
+
resource_type=ResourceType.SENTIMENT,
|
| 485 |
+
base_url="https://sentiment-api.example.com",
|
| 486 |
+
api_key_env="SENTIMENT_API_KEY",
|
| 487 |
+
api_key=os.getenv("SENTIMENT_API_KEY", "vltdvdho63uqnjgf_fq75qbks72e3wfmx"),
|
| 488 |
+
rate_limit="varies",
|
| 489 |
+
is_free=True,
|
| 490 |
+
requires_auth=True,
|
| 491 |
+
description="Custom sentiment analysis API",
|
| 492 |
+
endpoints={
|
| 493 |
+
"analyze": "/analyze",
|
| 494 |
+
"market_sentiment": "/market-sentiment",
|
| 495 |
+
"social_sentiment": "/social-sentiment"
|
| 496 |
+
},
|
| 497 |
+
features=["sentiment", "social", "market"]
|
| 498 |
+
)
|
| 499 |
+
|
| 500 |
+
# LunarCrush
|
| 501 |
+
self.resources["lunarcrush"] = APIResource(
|
| 502 |
+
id="lunarcrush",
|
| 503 |
+
name="LunarCrush",
|
| 504 |
+
resource_type=ResourceType.SENTIMENT,
|
| 505 |
+
base_url="https://lunarcrush.com/api/v2",
|
| 506 |
+
api_key_env="LUNARCRUSH_KEY",
|
| 507 |
+
rate_limit="varies",
|
| 508 |
+
is_free=True,
|
| 509 |
+
requires_auth=True,
|
| 510 |
+
description="Social sentiment analytics",
|
| 511 |
+
endpoints={
|
| 512 |
+
"assets": "/assets",
|
| 513 |
+
"market": "/market",
|
| 514 |
+
"global": "/global",
|
| 515 |
+
"influencers": "/influencers"
|
| 516 |
+
},
|
| 517 |
+
features=["social_sentiment", "influencers", "trending"],
|
| 518 |
+
documentation_url="https://lunarcrush.com/developers"
|
| 519 |
+
)
|
| 520 |
+
|
| 521 |
+
# Santiment
|
| 522 |
+
self.resources["santiment"] = APIResource(
|
| 523 |
+
id="santiment",
|
| 524 |
+
name="Santiment",
|
| 525 |
+
resource_type=ResourceType.SENTIMENT,
|
| 526 |
+
base_url="https://api.santiment.net/graphql",
|
| 527 |
+
api_key_env="SANTIMENT_KEY",
|
| 528 |
+
rate_limit="varies",
|
| 529 |
+
is_free=False,
|
| 530 |
+
requires_auth=True,
|
| 531 |
+
description="On-chain and social metrics",
|
| 532 |
+
endpoints={
|
| 533 |
+
"graphql": ""
|
| 534 |
+
},
|
| 535 |
+
features=["on-chain", "social", "development"],
|
| 536 |
+
documentation_url="https://academy.santiment.net/for-developers/"
|
| 537 |
+
)
|
| 538 |
+
|
| 539 |
+
def _load_onchain_analytics(self):
|
| 540 |
+
"""On-chain analytics sources"""
|
| 541 |
+
|
| 542 |
+
# Glassnode
|
| 543 |
+
self.resources["glassnode"] = APIResource(
|
| 544 |
+
id="glassnode",
|
| 545 |
+
name="Glassnode",
|
| 546 |
+
resource_type=ResourceType.ONCHAIN,
|
| 547 |
+
base_url="https://api.glassnode.com/v1/metrics",
|
| 548 |
+
api_key_env="GLASSNODE_KEY",
|
| 549 |
+
rate_limit="varies",
|
| 550 |
+
is_free=False,
|
| 551 |
+
requires_auth=True,
|
| 552 |
+
description="On-chain market intelligence",
|
| 553 |
+
endpoints={
|
| 554 |
+
"market": "/market",
|
| 555 |
+
"addresses": "/addresses",
|
| 556 |
+
"supply": "/supply",
|
| 557 |
+
"indicators": "/indicators"
|
| 558 |
+
},
|
| 559 |
+
features=["on-chain", "market_intelligence", "addresses"],
|
| 560 |
+
documentation_url="https://docs.glassnode.com/"
|
| 561 |
+
)
|
| 562 |
+
|
| 563 |
+
# Blockchain.com
|
| 564 |
+
self.resources["blockchain_com"] = APIResource(
|
| 565 |
+
id="blockchain_com",
|
| 566 |
+
name="Blockchain.com",
|
| 567 |
+
resource_type=ResourceType.ONCHAIN,
|
| 568 |
+
base_url="https://api.blockchain.info",
|
| 569 |
+
rate_limit="varies",
|
| 570 |
+
is_free=True,
|
| 571 |
+
requires_auth=False,
|
| 572 |
+
description="Bitcoin blockchain data",
|
| 573 |
+
endpoints={
|
| 574 |
+
"stats": "/stats",
|
| 575 |
+
"ticker": "/ticker",
|
| 576 |
+
"rawblock": "/rawblock/{hash}",
|
| 577 |
+
"rawtx": "/rawtx/{hash}",
|
| 578 |
+
"balance": "/balance"
|
| 579 |
+
},
|
| 580 |
+
features=["bitcoin", "transactions", "blocks", "addresses"],
|
| 581 |
+
documentation_url="https://www.blockchain.com/api"
|
| 582 |
+
)
|
| 583 |
+
|
| 584 |
+
# Mempool.space
|
| 585 |
+
self.resources["mempool_space"] = APIResource(
|
| 586 |
+
id="mempool_space",
|
| 587 |
+
name="Mempool.space",
|
| 588 |
+
resource_type=ResourceType.ONCHAIN,
|
| 589 |
+
base_url="https://mempool.space/api",
|
| 590 |
+
rate_limit="varies",
|
| 591 |
+
is_free=True,
|
| 592 |
+
requires_auth=False,
|
| 593 |
+
description="Bitcoin mempool and blockchain explorer",
|
| 594 |
+
endpoints={
|
| 595 |
+
"mempool": "/mempool",
|
| 596 |
+
"fees_recommended": "/v1/fees/recommended",
|
| 597 |
+
"blocks": "/blocks",
|
| 598 |
+
"block_height": "/block-height/{height}",
|
| 599 |
+
"tx": "/tx/{txid}"
|
| 600 |
+
},
|
| 601 |
+
features=["mempool", "fees", "blocks", "transactions"],
|
| 602 |
+
documentation_url="https://mempool.space/docs/api"
|
| 603 |
+
)
|
| 604 |
+
|
| 605 |
+
def _load_defi_sources(self):
|
| 606 |
+
"""DeFi data sources"""
|
| 607 |
+
|
| 608 |
+
# DefiLlama
|
| 609 |
+
self.resources["defillama"] = APIResource(
|
| 610 |
+
id="defillama",
|
| 611 |
+
name="DefiLlama",
|
| 612 |
+
resource_type=ResourceType.DEFI,
|
| 613 |
+
base_url="https://api.llama.fi",
|
| 614 |
+
rate_limit="unlimited",
|
| 615 |
+
is_free=True,
|
| 616 |
+
requires_auth=False,
|
| 617 |
+
description="DeFi TVL and protocol analytics",
|
| 618 |
+
endpoints={
|
| 619 |
+
"protocols": "/protocols",
|
| 620 |
+
"protocol_detail": "/protocol/{protocol}",
|
| 621 |
+
"tvl_all": "/tvl",
|
| 622 |
+
"chains": "/chains",
|
| 623 |
+
"stablecoins": "/stablecoins",
|
| 624 |
+
"yields": "/yields/pools",
|
| 625 |
+
"dexs": "/overview/dexs"
|
| 626 |
+
},
|
| 627 |
+
features=["tvl", "protocols", "chains", "yields", "dexs"],
|
| 628 |
+
documentation_url="https://defillama.com/docs/api"
|
| 629 |
+
)
|
| 630 |
+
|
| 631 |
+
# 1inch
|
| 632 |
+
self.resources["1inch"] = APIResource(
|
| 633 |
+
id="1inch",
|
| 634 |
+
name="1inch",
|
| 635 |
+
resource_type=ResourceType.DEFI,
|
| 636 |
+
base_url="https://api.1inch.io/v5.0/1",
|
| 637 |
+
rate_limit="varies",
|
| 638 |
+
is_free=True,
|
| 639 |
+
requires_auth=False,
|
| 640 |
+
description="DEX aggregator API",
|
| 641 |
+
endpoints={
|
| 642 |
+
"tokens": "/tokens",
|
| 643 |
+
"quote": "/quote",
|
| 644 |
+
"swap": "/swap",
|
| 645 |
+
"liquidity_sources": "/liquidity-sources"
|
| 646 |
+
},
|
| 647 |
+
features=["dex", "swap", "quotes", "aggregator"],
|
| 648 |
+
documentation_url="https://docs.1inch.io/"
|
| 649 |
+
)
|
| 650 |
+
|
| 651 |
+
# Uniswap Subgraph
|
| 652 |
+
self.resources["uniswap_subgraph"] = APIResource(
|
| 653 |
+
id="uniswap_subgraph",
|
| 654 |
+
name="Uniswap Subgraph",
|
| 655 |
+
resource_type=ResourceType.DEFI,
|
| 656 |
+
base_url="https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3",
|
| 657 |
+
rate_limit="varies",
|
| 658 |
+
is_free=True,
|
| 659 |
+
requires_auth=False,
|
| 660 |
+
description="Uniswap V3 subgraph data",
|
| 661 |
+
endpoints={
|
| 662 |
+
"graphql": ""
|
| 663 |
+
},
|
| 664 |
+
features=["liquidity", "pools", "swaps", "tokens"],
|
| 665 |
+
documentation_url="https://docs.uniswap.org/api/subgraph/overview"
|
| 666 |
+
)
|
| 667 |
+
|
| 668 |
+
def _load_whale_tracking(self):
|
| 669 |
+
"""Whale tracking and large transaction monitoring"""
|
| 670 |
+
|
| 671 |
+
# Whale Alert
|
| 672 |
+
self.resources["whale_alert"] = APIResource(
|
| 673 |
+
id="whale_alert",
|
| 674 |
+
name="Whale Alert",
|
| 675 |
+
resource_type=ResourceType.WHALE_TRACKING,
|
| 676 |
+
base_url="https://api.whale-alert.io/v1",
|
| 677 |
+
api_key_env="WHALE_ALERT_KEY",
|
| 678 |
+
rate_limit="10 req/min free",
|
| 679 |
+
is_free=True,
|
| 680 |
+
requires_auth=True,
|
| 681 |
+
description="Large crypto transaction tracking",
|
| 682 |
+
endpoints={
|
| 683 |
+
"status": "/status",
|
| 684 |
+
"transactions": "/transactions"
|
| 685 |
+
},
|
| 686 |
+
features=["whale_alerts", "large_transactions", "multi-chain"],
|
| 687 |
+
documentation_url="https://docs.whale-alert.io/"
|
| 688 |
+
)
|
| 689 |
+
|
| 690 |
+
# Etherscan Whale Tracker (using main Etherscan)
|
| 691 |
+
self.resources["etherscan_whales"] = APIResource(
|
| 692 |
+
id="etherscan_whales",
|
| 693 |
+
name="Etherscan Whale Tracker",
|
| 694 |
+
resource_type=ResourceType.WHALE_TRACKING,
|
| 695 |
+
base_url="https://api.etherscan.io/api",
|
| 696 |
+
api_key_env="ETHERSCAN_KEY",
|
| 697 |
+
api_key=os.getenv("ETHERSCAN_KEY", "SZHYFZK2RR8H9TIMJBVW54V4H81K2Z2KR2"),
|
| 698 |
+
rate_limit="5 req/sec",
|
| 699 |
+
is_free=True,
|
| 700 |
+
requires_auth=True,
|
| 701 |
+
description="Track large ETH/ERC20 transactions",
|
| 702 |
+
endpoints={
|
| 703 |
+
"large_txs": "?module=account&action=txlist&sort=desc",
|
| 704 |
+
"token_transfers": "?module=account&action=tokentx&sort=desc"
|
| 705 |
+
},
|
| 706 |
+
features=["large_transactions", "ethereum", "erc20"]
|
| 707 |
+
)
|
| 708 |
+
|
| 709 |
+
def _load_technical_analysis(self):
|
| 710 |
+
"""Technical analysis sources"""
|
| 711 |
+
|
| 712 |
+
# TAAPI
|
| 713 |
+
self.resources["taapi"] = APIResource(
|
| 714 |
+
id="taapi",
|
| 715 |
+
name="TAAPI.IO",
|
| 716 |
+
resource_type=ResourceType.TECHNICAL,
|
| 717 |
+
base_url="https://api.taapi.io",
|
| 718 |
+
api_key_env="TAAPI_KEY",
|
| 719 |
+
rate_limit="varies",
|
| 720 |
+
is_free=True,
|
| 721 |
+
requires_auth=True,
|
| 722 |
+
description="Technical analysis indicators API",
|
| 723 |
+
endpoints={
|
| 724 |
+
"rsi": "/rsi",
|
| 725 |
+
"macd": "/macd",
|
| 726 |
+
"ema": "/ema",
|
| 727 |
+
"sma": "/sma",
|
| 728 |
+
"bbands": "/bbands",
|
| 729 |
+
"stoch": "/stoch",
|
| 730 |
+
"atr": "/atr",
|
| 731 |
+
"adx": "/adx",
|
| 732 |
+
"dmi": "/dmi",
|
| 733 |
+
"sar": "/sar",
|
| 734 |
+
"ichimoku": "/ichimoku"
|
| 735 |
+
},
|
| 736 |
+
features=["indicators", "rsi", "macd", "bollinger", "ema", "sma"],
|
| 737 |
+
documentation_url="https://taapi.io/documentation/"
|
| 738 |
+
)
|
| 739 |
+
|
| 740 |
+
# TradingView (unofficial scraping - use with caution)
|
| 741 |
+
self.resources["tradingview_ideas"] = APIResource(
|
| 742 |
+
id="tradingview_ideas",
|
| 743 |
+
name="TradingView Ideas",
|
| 744 |
+
resource_type=ResourceType.TECHNICAL,
|
| 745 |
+
base_url="https://www.tradingview.com",
|
| 746 |
+
rate_limit="limited",
|
| 747 |
+
is_free=True,
|
| 748 |
+
requires_auth=False,
|
| 749 |
+
description="TradingView trading ideas",
|
| 750 |
+
endpoints={
|
| 751 |
+
"ideas": "/ideas/"
|
| 752 |
+
},
|
| 753 |
+
features=["ideas", "analysis", "charts"],
|
| 754 |
+
documentation_url="https://www.tradingview.com/"
|
| 755 |
+
)
|
| 756 |
+
|
| 757 |
+
def _load_social_sources(self):
|
| 758 |
+
"""Social media and community sources"""
|
| 759 |
+
|
| 760 |
+
# Reddit
|
| 761 |
+
self.resources["reddit"] = APIResource(
|
| 762 |
+
id="reddit",
|
| 763 |
+
name="Reddit API",
|
| 764 |
+
resource_type=ResourceType.SOCIAL,
|
| 765 |
+
base_url="https://www.reddit.com",
|
| 766 |
+
rate_limit="60 req/min",
|
| 767 |
+
is_free=True,
|
| 768 |
+
requires_auth=False,
|
| 769 |
+
description="Reddit cryptocurrency communities",
|
| 770 |
+
endpoints={
|
| 771 |
+
"r_crypto": "/r/CryptoCurrency/hot.json",
|
| 772 |
+
"r_bitcoin": "/r/Bitcoin/hot.json",
|
| 773 |
+
"r_ethereum": "/r/ethereum/hot.json",
|
| 774 |
+
"r_altcoin": "/r/altcoin/hot.json",
|
| 775 |
+
"r_defi": "/r/defi/hot.json"
|
| 776 |
+
},
|
| 777 |
+
features=["discussions", "sentiment", "trending"],
|
| 778 |
+
documentation_url="https://www.reddit.com/dev/api/"
|
| 779 |
+
)
|
| 780 |
+
|
| 781 |
+
# Twitter/X (requires API key)
|
| 782 |
+
self.resources["twitter"] = APIResource(
|
| 783 |
+
id="twitter",
|
| 784 |
+
name="Twitter/X API",
|
| 785 |
+
resource_type=ResourceType.SOCIAL,
|
| 786 |
+
base_url="https://api.twitter.com/2",
|
| 787 |
+
api_key_env="TWITTER_BEARER_TOKEN",
|
| 788 |
+
rate_limit="varies",
|
| 789 |
+
is_free=False,
|
| 790 |
+
requires_auth=True,
|
| 791 |
+
description="Twitter/X crypto discussions",
|
| 792 |
+
endpoints={
|
| 793 |
+
"search": "/tweets/search/recent",
|
| 794 |
+
"user": "/users/by/username/{username}",
|
| 795 |
+
"tweets": "/tweets"
|
| 796 |
+
},
|
| 797 |
+
features=["tweets", "sentiment", "influencers"],
|
| 798 |
+
documentation_url="https://developer.twitter.com/en/docs"
|
| 799 |
+
)
|
| 800 |
+
|
| 801 |
+
def _load_historical_sources(self):
|
| 802 |
+
"""Historical data sources"""
|
| 803 |
+
|
| 804 |
+
# CryptoCompare Historical
|
| 805 |
+
self.resources["cryptocompare_historical"] = APIResource(
|
| 806 |
+
id="cryptocompare_historical",
|
| 807 |
+
name="CryptoCompare Historical",
|
| 808 |
+
resource_type=ResourceType.HISTORICAL,
|
| 809 |
+
base_url="https://min-api.cryptocompare.com/data",
|
| 810 |
+
rate_limit="100,000 req/month free",
|
| 811 |
+
is_free=True,
|
| 812 |
+
requires_auth=False,
|
| 813 |
+
description="Historical crypto price data",
|
| 814 |
+
endpoints={
|
| 815 |
+
"histoday": "/v2/histoday",
|
| 816 |
+
"histohour": "/v2/histohour",
|
| 817 |
+
"histominute": "/histominute"
|
| 818 |
+
},
|
| 819 |
+
features=["ohlcv", "historical", "daily", "hourly", "minute"],
|
| 820 |
+
supported_timeframes=["1m", "1h", "1d"],
|
| 821 |
+
documentation_url="https://min-api.cryptocompare.com/documentation"
|
| 822 |
+
)
|
| 823 |
+
|
| 824 |
+
# Messari
|
| 825 |
+
self.resources["messari"] = APIResource(
|
| 826 |
+
id="messari",
|
| 827 |
+
name="Messari",
|
| 828 |
+
resource_type=ResourceType.HISTORICAL,
|
| 829 |
+
base_url="https://data.messari.io/api/v1",
|
| 830 |
+
api_key_env="MESSARI_KEY",
|
| 831 |
+
rate_limit="20 req/min free",
|
| 832 |
+
is_free=True,
|
| 833 |
+
requires_auth=False,
|
| 834 |
+
description="Crypto research and data",
|
| 835 |
+
endpoints={
|
| 836 |
+
"assets": "/assets",
|
| 837 |
+
"asset_detail": "/assets/{symbol}",
|
| 838 |
+
"asset_metrics": "/assets/{symbol}/metrics",
|
| 839 |
+
"asset_profile": "/assets/{symbol}/profile"
|
| 840 |
+
},
|
| 841 |
+
features=["metrics", "profiles", "research"],
|
| 842 |
+
documentation_url="https://messari.io/api"
|
| 843 |
+
)
|
| 844 |
+
|
| 845 |
+
# ============ Registry Access Methods ============
|
| 846 |
+
|
| 847 |
+
def get_resource(self, resource_id: str) -> Optional[APIResource]:
|
| 848 |
+
"""Get a specific resource by ID"""
|
| 849 |
+
return self.resources.get(resource_id)
|
| 850 |
+
|
| 851 |
+
def get_by_type(self, resource_type: ResourceType) -> List[APIResource]:
|
| 852 |
+
"""Get all resources of a specific type"""
|
| 853 |
+
return [r for r in self.resources.values() if r.resource_type == resource_type]
|
| 854 |
+
|
| 855 |
+
def get_free_resources(self) -> List[APIResource]:
|
| 856 |
+
"""Get all free resources"""
|
| 857 |
+
return [r for r in self.resources.values() if r.is_free]
|
| 858 |
+
|
| 859 |
+
def get_active_resources(self) -> List[APIResource]:
|
| 860 |
+
"""Get all active resources"""
|
| 861 |
+
return [r for r in self.resources.values() if r.is_active]
|
| 862 |
+
|
| 863 |
+
def get_no_auth_resources(self) -> List[APIResource]:
|
| 864 |
+
"""Get all resources that don't require authentication"""
|
| 865 |
+
return [r for r in self.resources.values() if not r.requires_auth]
|
| 866 |
+
|
| 867 |
+
def search_resources(self, query: str) -> List[APIResource]:
|
| 868 |
+
"""Search resources by name or description"""
|
| 869 |
+
query_lower = query.lower()
|
| 870 |
+
return [
|
| 871 |
+
r for r in self.resources.values()
|
| 872 |
+
if query_lower in r.name.lower() or query_lower in r.description.lower()
|
| 873 |
+
]
|
| 874 |
+
|
| 875 |
+
def get_all_resources(self) -> List[APIResource]:
|
| 876 |
+
"""Get all registered resources"""
|
| 877 |
+
return list(self.resources.values())
|
| 878 |
+
|
| 879 |
+
def get_statistics(self) -> Dict[str, Any]:
|
| 880 |
+
"""Get registry statistics"""
|
| 881 |
+
resources = list(self.resources.values())
|
| 882 |
+
return {
|
| 883 |
+
"total_resources": len(resources),
|
| 884 |
+
"free_resources": len([r for r in resources if r.is_free]),
|
| 885 |
+
"active_resources": len([r for r in resources if r.is_active]),
|
| 886 |
+
"no_auth_required": len([r for r in resources if not r.requires_auth]),
|
| 887 |
+
"by_type": {
|
| 888 |
+
rt.value: len([r for r in resources if r.resource_type == rt])
|
| 889 |
+
for rt in ResourceType
|
| 890 |
+
}
|
| 891 |
+
}
|
| 892 |
+
|
| 893 |
+
def export_to_dict(self) -> Dict[str, Any]:
|
| 894 |
+
"""Export all resources as dictionary"""
|
| 895 |
+
return {
|
| 896 |
+
rid: {
|
| 897 |
+
"id": r.id,
|
| 898 |
+
"name": r.name,
|
| 899 |
+
"type": r.resource_type.value,
|
| 900 |
+
"base_url": r.base_url,
|
| 901 |
+
"is_free": r.is_free,
|
| 902 |
+
"requires_auth": r.requires_auth,
|
| 903 |
+
"is_active": r.is_active,
|
| 904 |
+
"rate_limit": r.rate_limit,
|
| 905 |
+
"description": r.description,
|
| 906 |
+
"endpoints": r.endpoints,
|
| 907 |
+
"features": r.features
|
| 908 |
+
}
|
| 909 |
+
for rid, r in self.resources.items()
|
| 910 |
+
}
|
| 911 |
+
|
| 912 |
+
|
| 913 |
+
# ============ ML Models Configuration ============
|
| 914 |
+
|
| 915 |
+
ML_MODELS_CONFIG = {
|
| 916 |
+
"price_prediction_lstm": {
|
| 917 |
+
"name": "PricePredictionLSTM",
|
| 918 |
+
"type": "LSTM",
|
| 919 |
+
"purpose": "Short-term price prediction",
|
| 920 |
+
"input_features": ["open", "high", "low", "close", "volume"],
|
| 921 |
+
"timeframes": ["1m", "5m", "15m", "1h", "4h"],
|
| 922 |
+
"huggingface_model": None
|
| 923 |
+
},
|
| 924 |
+
"sentiment_analysis_transformer": {
|
| 925 |
+
"name": "SentimentAnalysisTransformer",
|
| 926 |
+
"type": "Transformer",
|
| 927 |
+
"purpose": "News and social media sentiment analysis",
|
| 928 |
+
"huggingface_model": "ProsusAI/finbert"
|
| 929 |
+
},
|
| 930 |
+
"anomaly_detection_isolation_forest": {
|
| 931 |
+
"name": "AnomalyDetectionIsolationForest",
|
| 932 |
+
"type": "Isolation Forest",
|
| 933 |
+
"purpose": "Detecting market anomalies"
|
| 934 |
+
},
|
| 935 |
+
"trend_classification_random_forest": {
|
| 936 |
+
"name": "TrendClassificationRandomForest",
|
| 937 |
+
"type": "Random Forest",
|
| 938 |
+
"purpose": "Market trend classification"
|
| 939 |
+
}
|
| 940 |
+
}
|
| 941 |
+
|
| 942 |
+
|
| 943 |
+
# ============ Analysis Endpoints Configuration ============
|
| 944 |
+
|
| 945 |
+
ANALYSIS_ENDPOINTS = {
|
| 946 |
+
"track_position": "/track_position",
|
| 947 |
+
"market_analysis": "/market_analysis",
|
| 948 |
+
"technical_analysis": "/technical_analysis",
|
| 949 |
+
"sentiment_analysis": "/sentiment_analysis",
|
| 950 |
+
"whale_activity": "/whale_activity",
|
| 951 |
+
"trading_strategies": "/trading_strategies",
|
| 952 |
+
"ai_prediction": "/ai_prediction",
|
| 953 |
+
"risk_management": "/risk_management",
|
| 954 |
+
"pdf_analysis": "/pdf_analysis",
|
| 955 |
+
"ai_enhanced_analysis": "/ai_enhanced_analysis",
|
| 956 |
+
"multi_source_data": "/multi_source_data",
|
| 957 |
+
"news_analysis": "/news_analysis",
|
| 958 |
+
"exchange_integration": "/exchange_integration",
|
| 959 |
+
"smart_alerts": "/smart_alerts",
|
| 960 |
+
"advanced_social_media_analysis": "/advanced_social_media_analysis",
|
| 961 |
+
"dynamic_modeling": "/dynamic_modeling",
|
| 962 |
+
"multi_currency_analysis": "/multi_currency_analysis",
|
| 963 |
+
"telegram_settings": "/telegram_settings",
|
| 964 |
+
"collect_data": "/collect-data",
|
| 965 |
+
"greed_fear_index": "/greed-fear-index",
|
| 966 |
+
"onchain_metrics": "/onchain-metrics",
|
| 967 |
+
"custom_alerts": "/custom-alerts",
|
| 968 |
+
"stakeholder_analysis": "/stakeholder-analysis"
|
| 969 |
+
}
|
| 970 |
+
|
| 971 |
+
|
| 972 |
+
# ============ Singleton Instance ============
|
| 973 |
+
|
| 974 |
+
_registry_instance: Optional[FreeResourcesRegistry] = None
|
| 975 |
+
|
| 976 |
+
|
| 977 |
+
def get_free_resources_registry() -> FreeResourcesRegistry:
|
| 978 |
+
"""Get or create the singleton registry instance"""
|
| 979 |
+
global _registry_instance
|
| 980 |
+
if _registry_instance is None:
|
| 981 |
+
_registry_instance = FreeResourcesRegistry()
|
| 982 |
+
return _registry_instance
|
| 983 |
+
|
| 984 |
+
|
| 985 |
+
# ============ Test Function ============
|
| 986 |
+
|
| 987 |
+
if __name__ == "__main__":
|
| 988 |
+
registry = get_free_resources_registry()
|
| 989 |
+
stats = registry.get_statistics()
|
| 990 |
+
|
| 991 |
+
print("=" * 60)
|
| 992 |
+
print("FREE RESOURCES REGISTRY - STATISTICS")
|
| 993 |
+
print("=" * 60)
|
| 994 |
+
print(f"Total Resources: {stats['total_resources']}")
|
| 995 |
+
print(f"Free Resources: {stats['free_resources']}")
|
| 996 |
+
print(f"Active Resources: {stats['active_resources']}")
|
| 997 |
+
print(f"No Auth Required: {stats['no_auth_required']}")
|
| 998 |
+
print()
|
| 999 |
+
print("By Type:")
|
| 1000 |
+
for rtype, count in stats['by_type'].items():
|
| 1001 |
+
print(f" - {rtype}: {count}")
|
| 1002 |
+
|
| 1003 |
+
print()
|
| 1004 |
+
print("=" * 60)
|
| 1005 |
+
print("BLOCK EXPLORERS (with API keys)")
|
| 1006 |
+
print("=" * 60)
|
| 1007 |
+
for r in registry.get_by_type(ResourceType.BLOCKCHAIN):
|
| 1008 |
+
print(f" - {r.name}: {r.base_url}")
|
| 1009 |
+
if r.api_key:
|
| 1010 |
+
print(f" API Key: {r.api_key[:10]}...")
|
| 1011 |
+
|
| 1012 |
+
print()
|
| 1013 |
+
print("=" * 60)
|
| 1014 |
+
print("MARKET DATA SOURCES")
|
| 1015 |
+
print("=" * 60)
|
| 1016 |
+
for r in registry.get_by_type(ResourceType.MARKET_DATA):
|
| 1017 |
+
print(f" - {r.name}: {r.base_url}")
|
| 1018 |
+
print(f" Features: {', '.join(r.features[:5])}")
|
backend/providers/sentiment_news_providers.py
ADDED
|
@@ -0,0 +1,889 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Sentiment & News Providers Registry - Extended Sources
|
| 4 |
+
منابع احساسات بازار و اخبار رمزارزها
|
| 5 |
+
|
| 6 |
+
این ماژول شامل منابع زیر است:
|
| 7 |
+
- Sentiment Analysis APIs
|
| 8 |
+
- News Aggregation APIs
|
| 9 |
+
- Social Media Sentiment
|
| 10 |
+
- Market Sentiment Indices
|
| 11 |
+
- Historical Data Sources
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
import aiohttp
|
| 15 |
+
import asyncio
|
| 16 |
+
import feedparser
|
| 17 |
+
from typing import Dict, List, Any, Optional
|
| 18 |
+
from dataclasses import dataclass, field
|
| 19 |
+
from enum import Enum
|
| 20 |
+
from datetime import datetime
|
| 21 |
+
import logging
|
| 22 |
+
|
| 23 |
+
logger = logging.getLogger(__name__)
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class SourceType(Enum):
|
| 27 |
+
"""نوع منبع داده"""
|
| 28 |
+
SENTIMENT = "sentiment"
|
| 29 |
+
NEWS = "news"
|
| 30 |
+
SOCIAL = "social"
|
| 31 |
+
MARKET_MOOD = "market_mood"
|
| 32 |
+
HISTORICAL = "historical"
|
| 33 |
+
AGGREGATED = "aggregated"
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class TimeFrame(Enum):
|
| 37 |
+
"""بازههای زمانی پشتیبانی شده"""
|
| 38 |
+
REALTIME = "realtime"
|
| 39 |
+
MINUTES_1 = "1m"
|
| 40 |
+
MINUTES_5 = "5m"
|
| 41 |
+
MINUTES_15 = "15m"
|
| 42 |
+
MINUTES_30 = "30m"
|
| 43 |
+
HOURLY = "1h"
|
| 44 |
+
HOURS_4 = "4h"
|
| 45 |
+
DAILY = "1d"
|
| 46 |
+
WEEKLY = "1w"
|
| 47 |
+
MONTHLY = "1M"
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
@dataclass
|
| 51 |
+
class SentimentNewsSource:
|
| 52 |
+
"""تعریف یک منبع سنتیمنت یا اخبار"""
|
| 53 |
+
id: str
|
| 54 |
+
name: str
|
| 55 |
+
source_type: str
|
| 56 |
+
url: str
|
| 57 |
+
description: str
|
| 58 |
+
requires_api_key: bool = False
|
| 59 |
+
api_key_env: str = ""
|
| 60 |
+
rate_limit: str = "unlimited"
|
| 61 |
+
supported_timeframes: List[str] = field(default_factory=list)
|
| 62 |
+
categories: List[str] = field(default_factory=list)
|
| 63 |
+
is_active: bool = True
|
| 64 |
+
priority: int = 1 # 1-10, lower is higher priority
|
| 65 |
+
verified: bool = False
|
| 66 |
+
free_tier: bool = True
|
| 67 |
+
features: List[str] = field(default_factory=list)
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
class SentimentNewsRegistry:
|
| 71 |
+
"""
|
| 72 |
+
رجیستری جامع منابع سنتیمنت و اخبار
|
| 73 |
+
Comprehensive Sentiment & News Sources Registry
|
| 74 |
+
"""
|
| 75 |
+
|
| 76 |
+
def __init__(self):
|
| 77 |
+
self.sources: Dict[str, SentimentNewsSource] = {}
|
| 78 |
+
self._load_all_sources()
|
| 79 |
+
|
| 80 |
+
def _load_all_sources(self):
|
| 81 |
+
"""بارگذاری تمام منابع"""
|
| 82 |
+
|
| 83 |
+
# ===== SENTIMENT APIS =====
|
| 84 |
+
self.sources["fear_greed_index"] = SentimentNewsSource(
|
| 85 |
+
id="fear_greed_index",
|
| 86 |
+
name="Fear & Greed Index",
|
| 87 |
+
source_type=SourceType.SENTIMENT.value,
|
| 88 |
+
url="https://api.alternative.me/fng/",
|
| 89 |
+
description="Crypto Fear & Greed Index - measure market sentiment",
|
| 90 |
+
requires_api_key=False,
|
| 91 |
+
rate_limit="unlimited",
|
| 92 |
+
supported_timeframes=["1d", "1w", "1M"],
|
| 93 |
+
categories=["sentiment", "market_mood"],
|
| 94 |
+
is_active=True,
|
| 95 |
+
priority=1,
|
| 96 |
+
verified=True,
|
| 97 |
+
free_tier=True,
|
| 98 |
+
features=["fear_index", "greed_index", "historical"]
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
self.sources["lunarcrush"] = SentimentNewsSource(
|
| 102 |
+
id="lunarcrush",
|
| 103 |
+
name="LunarCrush",
|
| 104 |
+
source_type=SourceType.SOCIAL.value,
|
| 105 |
+
url="https://lunarcrush.com/api",
|
| 106 |
+
description="Social metrics and sentiment for cryptocurrencies",
|
| 107 |
+
requires_api_key=True,
|
| 108 |
+
api_key_env="LUNARCRUSH_KEY",
|
| 109 |
+
rate_limit="50 req/day (free)",
|
| 110 |
+
supported_timeframes=["realtime", "1h", "1d", "1w"],
|
| 111 |
+
categories=["social", "sentiment", "influencers"],
|
| 112 |
+
is_active=True,
|
| 113 |
+
priority=2,
|
| 114 |
+
verified=False,
|
| 115 |
+
free_tier=True,
|
| 116 |
+
features=["social_volume", "sentiment_score", "influencers", "galaxy_score"]
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
self.sources["santiment"] = SentimentNewsSource(
|
| 120 |
+
id="santiment",
|
| 121 |
+
name="Santiment",
|
| 122 |
+
source_type=SourceType.SENTIMENT.value,
|
| 123 |
+
url="https://api.santiment.net/graphql",
|
| 124 |
+
description="On-chain, social, and development metrics",
|
| 125 |
+
requires_api_key=True,
|
| 126 |
+
api_key_env="SANTIMENT_KEY",
|
| 127 |
+
rate_limit="varies",
|
| 128 |
+
supported_timeframes=["1h", "1d", "1w"],
|
| 129 |
+
categories=["onchain", "social", "development"],
|
| 130 |
+
is_active=True,
|
| 131 |
+
priority=3,
|
| 132 |
+
verified=False,
|
| 133 |
+
free_tier=True,
|
| 134 |
+
features=["dev_activity", "social_volume", "whale_movements", "network_growth"]
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
self.sources["augmento"] = SentimentNewsSource(
|
| 138 |
+
id="augmento",
|
| 139 |
+
name="Augmento",
|
| 140 |
+
source_type=SourceType.SOCIAL.value,
|
| 141 |
+
url="https://api.augmento.ai/v0.1",
|
| 142 |
+
description="Social media sentiment analysis",
|
| 143 |
+
requires_api_key=False,
|
| 144 |
+
rate_limit="100 req/day",
|
| 145 |
+
supported_timeframes=["1h", "1d"],
|
| 146 |
+
categories=["social", "sentiment"],
|
| 147 |
+
is_active=True,
|
| 148 |
+
priority=4,
|
| 149 |
+
verified=False,
|
| 150 |
+
free_tier=True,
|
| 151 |
+
features=["sentiment_topics", "social_trends", "coin_sentiment"]
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
self.sources["the_tie"] = SentimentNewsSource(
|
| 155 |
+
id="the_tie",
|
| 156 |
+
name="The TIE",
|
| 157 |
+
source_type=SourceType.SENTIMENT.value,
|
| 158 |
+
url="https://api.thetie.io/v1",
|
| 159 |
+
description="Enterprise-grade sentiment data",
|
| 160 |
+
requires_api_key=True,
|
| 161 |
+
api_key_env="THE_TIE_KEY",
|
| 162 |
+
rate_limit="varies",
|
| 163 |
+
supported_timeframes=["realtime", "1h", "1d"],
|
| 164 |
+
categories=["sentiment", "analytics"],
|
| 165 |
+
is_active=True,
|
| 166 |
+
priority=5,
|
| 167 |
+
verified=False,
|
| 168 |
+
free_tier=False,
|
| 169 |
+
features=["sentiment_score", "volume_buzz", "tweet_sentiment"]
|
| 170 |
+
)
|
| 171 |
+
|
| 172 |
+
self.sources["cryptoquant_sentiment"] = SentimentNewsSource(
|
| 173 |
+
id="cryptoquant_sentiment",
|
| 174 |
+
name="CryptoQuant Sentiment",
|
| 175 |
+
source_type=SourceType.SENTIMENT.value,
|
| 176 |
+
url="https://api.cryptoquant.com/v1",
|
| 177 |
+
description="On-chain sentiment indicators",
|
| 178 |
+
requires_api_key=True,
|
| 179 |
+
api_key_env="CRYPTOQUANT_KEY",
|
| 180 |
+
rate_limit="100 req/day",
|
| 181 |
+
supported_timeframes=["1h", "1d"],
|
| 182 |
+
categories=["onchain", "sentiment"],
|
| 183 |
+
is_active=True,
|
| 184 |
+
priority=3,
|
| 185 |
+
verified=False,
|
| 186 |
+
free_tier=True,
|
| 187 |
+
features=["exchange_flows", "miner_flows", "market_indicators"]
|
| 188 |
+
)
|
| 189 |
+
|
| 190 |
+
self.sources["glassnode_sentiment"] = SentimentNewsSource(
|
| 191 |
+
id="glassnode_sentiment",
|
| 192 |
+
name="Glassnode Sentiment",
|
| 193 |
+
source_type=SourceType.SENTIMENT.value,
|
| 194 |
+
url="https://api.glassnode.com/v1/metrics",
|
| 195 |
+
description="Glassnode on-chain sentiment metrics",
|
| 196 |
+
requires_api_key=True,
|
| 197 |
+
api_key_env="GLASSNODE_KEY",
|
| 198 |
+
rate_limit="varies",
|
| 199 |
+
supported_timeframes=["1h", "1d", "1w"],
|
| 200 |
+
categories=["onchain", "sentiment"],
|
| 201 |
+
is_active=True,
|
| 202 |
+
priority=2,
|
| 203 |
+
verified=True,
|
| 204 |
+
free_tier=True,
|
| 205 |
+
features=["sopr", "nupl", "hodl_waves", "reserve_risk"]
|
| 206 |
+
)
|
| 207 |
+
|
| 208 |
+
# ===== NEWS APIS =====
|
| 209 |
+
self.sources["cryptopanic"] = SentimentNewsSource(
|
| 210 |
+
id="cryptopanic",
|
| 211 |
+
name="CryptoPanic",
|
| 212 |
+
source_type=SourceType.NEWS.value,
|
| 213 |
+
url="https://cryptopanic.com/api/v1/posts/",
|
| 214 |
+
description="Crypto news aggregator with sentiment",
|
| 215 |
+
requires_api_key=True,
|
| 216 |
+
api_key_env="CRYPTOPANIC_KEY",
|
| 217 |
+
rate_limit="500 req/day",
|
| 218 |
+
supported_timeframes=["realtime", "1h", "1d"],
|
| 219 |
+
categories=["news", "sentiment"],
|
| 220 |
+
is_active=True,
|
| 221 |
+
priority=1,
|
| 222 |
+
verified=True,
|
| 223 |
+
free_tier=True,
|
| 224 |
+
features=["news_feed", "sentiment_votes", "trending_news", "filter_by_coin"]
|
| 225 |
+
)
|
| 226 |
+
|
| 227 |
+
self.sources["newsapi"] = SentimentNewsSource(
|
| 228 |
+
id="newsapi",
|
| 229 |
+
name="NewsAPI",
|
| 230 |
+
source_type=SourceType.NEWS.value,
|
| 231 |
+
url="https://newsapi.org/v2/everything",
|
| 232 |
+
description="General news API with crypto filtering",
|
| 233 |
+
requires_api_key=True,
|
| 234 |
+
api_key_env="NEWSAPI_KEY",
|
| 235 |
+
rate_limit="100 req/day (free)",
|
| 236 |
+
supported_timeframes=["realtime", "1d"],
|
| 237 |
+
categories=["news"],
|
| 238 |
+
is_active=True,
|
| 239 |
+
priority=2,
|
| 240 |
+
verified=True,
|
| 241 |
+
free_tier=True,
|
| 242 |
+
features=["everything", "headlines", "sources"]
|
| 243 |
+
)
|
| 244 |
+
|
| 245 |
+
self.sources["cryptocompare_news"] = SentimentNewsSource(
|
| 246 |
+
id="cryptocompare_news",
|
| 247 |
+
name="CryptoCompare News",
|
| 248 |
+
source_type=SourceType.NEWS.value,
|
| 249 |
+
url="https://min-api.cryptocompare.com/data/v2/news/",
|
| 250 |
+
description="CryptoCompare news feed",
|
| 251 |
+
requires_api_key=False,
|
| 252 |
+
rate_limit="100K/month",
|
| 253 |
+
supported_timeframes=["realtime", "1h", "1d"],
|
| 254 |
+
categories=["news"],
|
| 255 |
+
is_active=True,
|
| 256 |
+
priority=1,
|
| 257 |
+
verified=True,
|
| 258 |
+
free_tier=True,
|
| 259 |
+
features=["latest_news", "news_by_categories", "news_by_coin"]
|
| 260 |
+
)
|
| 261 |
+
|
| 262 |
+
self.sources["messari_news"] = SentimentNewsSource(
|
| 263 |
+
id="messari_news",
|
| 264 |
+
name="Messari News",
|
| 265 |
+
source_type=SourceType.NEWS.value,
|
| 266 |
+
url="https://data.messari.io/api/v1/news",
|
| 267 |
+
description="Messari research and news",
|
| 268 |
+
requires_api_key=False,
|
| 269 |
+
rate_limit="20 req/min",
|
| 270 |
+
supported_timeframes=["realtime", "1d"],
|
| 271 |
+
categories=["news", "research"],
|
| 272 |
+
is_active=True,
|
| 273 |
+
priority=2,
|
| 274 |
+
verified=True,
|
| 275 |
+
free_tier=True,
|
| 276 |
+
features=["news", "research", "profiles"]
|
| 277 |
+
)
|
| 278 |
+
|
| 279 |
+
# ===== RSS NEWS FEEDS =====
|
| 280 |
+
self.sources["bitcoin_magazine_rss"] = SentimentNewsSource(
|
| 281 |
+
id="bitcoin_magazine_rss",
|
| 282 |
+
name="Bitcoin Magazine RSS",
|
| 283 |
+
source_type=SourceType.NEWS.value,
|
| 284 |
+
url="https://bitcoinmagazine.com/feed",
|
| 285 |
+
description="Bitcoin Magazine articles via RSS",
|
| 286 |
+
requires_api_key=False,
|
| 287 |
+
rate_limit="unlimited",
|
| 288 |
+
supported_timeframes=["realtime"],
|
| 289 |
+
categories=["news", "bitcoin"],
|
| 290 |
+
is_active=True,
|
| 291 |
+
priority=3,
|
| 292 |
+
verified=True,
|
| 293 |
+
free_tier=True,
|
| 294 |
+
features=["articles", "analysis"]
|
| 295 |
+
)
|
| 296 |
+
|
| 297 |
+
self.sources["decrypt_rss"] = SentimentNewsSource(
|
| 298 |
+
id="decrypt_rss",
|
| 299 |
+
name="Decrypt RSS",
|
| 300 |
+
source_type=SourceType.NEWS.value,
|
| 301 |
+
url="https://decrypt.co/feed",
|
| 302 |
+
description="Decrypt media RSS feed",
|
| 303 |
+
requires_api_key=False,
|
| 304 |
+
rate_limit="unlimited",
|
| 305 |
+
supported_timeframes=["realtime"],
|
| 306 |
+
categories=["news", "web3"],
|
| 307 |
+
is_active=True,
|
| 308 |
+
priority=3,
|
| 309 |
+
verified=True,
|
| 310 |
+
free_tier=True,
|
| 311 |
+
features=["articles", "web3_news"]
|
| 312 |
+
)
|
| 313 |
+
|
| 314 |
+
self.sources["cryptoslate_rss"] = SentimentNewsSource(
|
| 315 |
+
id="cryptoslate_rss",
|
| 316 |
+
name="CryptoSlate RSS",
|
| 317 |
+
source_type=SourceType.NEWS.value,
|
| 318 |
+
url="https://cryptoslate.com/feed/",
|
| 319 |
+
description="CryptoSlate news RSS",
|
| 320 |
+
requires_api_key=False,
|
| 321 |
+
rate_limit="unlimited",
|
| 322 |
+
supported_timeframes=["realtime"],
|
| 323 |
+
categories=["news", "analysis"],
|
| 324 |
+
is_active=True,
|
| 325 |
+
priority=3,
|
| 326 |
+
verified=True,
|
| 327 |
+
free_tier=True,
|
| 328 |
+
features=["articles", "analysis"]
|
| 329 |
+
)
|
| 330 |
+
|
| 331 |
+
self.sources["theblock_rss"] = SentimentNewsSource(
|
| 332 |
+
id="theblock_rss",
|
| 333 |
+
name="The Block RSS",
|
| 334 |
+
source_type=SourceType.NEWS.value,
|
| 335 |
+
url="https://www.theblock.co/rss.xml",
|
| 336 |
+
description="The Block crypto news RSS",
|
| 337 |
+
requires_api_key=False,
|
| 338 |
+
rate_limit="unlimited",
|
| 339 |
+
supported_timeframes=["realtime"],
|
| 340 |
+
categories=["news", "research"],
|
| 341 |
+
is_active=True,
|
| 342 |
+
priority=3,
|
| 343 |
+
verified=True,
|
| 344 |
+
free_tier=True,
|
| 345 |
+
features=["articles", "research"]
|
| 346 |
+
)
|
| 347 |
+
|
| 348 |
+
self.sources["cointelegraph_rss"] = SentimentNewsSource(
|
| 349 |
+
id="cointelegraph_rss",
|
| 350 |
+
name="CoinTelegraph RSS",
|
| 351 |
+
source_type=SourceType.NEWS.value,
|
| 352 |
+
url="https://cointelegraph.com/rss",
|
| 353 |
+
description="CoinTelegraph news feed",
|
| 354 |
+
requires_api_key=False,
|
| 355 |
+
rate_limit="unlimited",
|
| 356 |
+
supported_timeframes=["realtime"],
|
| 357 |
+
categories=["news"],
|
| 358 |
+
is_active=True,
|
| 359 |
+
priority=3,
|
| 360 |
+
verified=True,
|
| 361 |
+
free_tier=True,
|
| 362 |
+
features=["articles", "breaking_news"]
|
| 363 |
+
)
|
| 364 |
+
|
| 365 |
+
self.sources["coindesk_rss"] = SentimentNewsSource(
|
| 366 |
+
id="coindesk_rss",
|
| 367 |
+
name="CoinDesk RSS",
|
| 368 |
+
source_type=SourceType.NEWS.value,
|
| 369 |
+
url="https://www.coindesk.com/arc/outboundfeeds/rss/",
|
| 370 |
+
description="CoinDesk news feed",
|
| 371 |
+
requires_api_key=False,
|
| 372 |
+
rate_limit="unlimited",
|
| 373 |
+
supported_timeframes=["realtime"],
|
| 374 |
+
categories=["news"],
|
| 375 |
+
is_active=True,
|
| 376 |
+
priority=2,
|
| 377 |
+
verified=True,
|
| 378 |
+
free_tier=True,
|
| 379 |
+
features=["articles", "analysis"]
|
| 380 |
+
)
|
| 381 |
+
|
| 382 |
+
# ===== SOCIAL SENTIMENT =====
|
| 383 |
+
self.sources["reddit_crypto"] = SentimentNewsSource(
|
| 384 |
+
id="reddit_crypto",
|
| 385 |
+
name="Reddit r/CryptoCurrency",
|
| 386 |
+
source_type=SourceType.SOCIAL.value,
|
| 387 |
+
url="https://www.reddit.com/r/CryptoCurrency/new.json",
|
| 388 |
+
description="Reddit cryptocurrency subreddit",
|
| 389 |
+
requires_api_key=False,
|
| 390 |
+
rate_limit="60 req/min",
|
| 391 |
+
supported_timeframes=["realtime", "1h", "1d"],
|
| 392 |
+
categories=["social", "community"],
|
| 393 |
+
is_active=True,
|
| 394 |
+
priority=2,
|
| 395 |
+
verified=True,
|
| 396 |
+
free_tier=True,
|
| 397 |
+
features=["posts", "comments", "trending"]
|
| 398 |
+
)
|
| 399 |
+
|
| 400 |
+
self.sources["reddit_bitcoin"] = SentimentNewsSource(
|
| 401 |
+
id="reddit_bitcoin",
|
| 402 |
+
name="Reddit r/Bitcoin",
|
| 403 |
+
source_type=SourceType.SOCIAL.value,
|
| 404 |
+
url="https://www.reddit.com/r/Bitcoin/new.json",
|
| 405 |
+
description="Reddit Bitcoin subreddit",
|
| 406 |
+
requires_api_key=False,
|
| 407 |
+
rate_limit="60 req/min",
|
| 408 |
+
supported_timeframes=["realtime", "1h", "1d"],
|
| 409 |
+
categories=["social", "bitcoin"],
|
| 410 |
+
is_active=True,
|
| 411 |
+
priority=2,
|
| 412 |
+
verified=True,
|
| 413 |
+
free_tier=True,
|
| 414 |
+
features=["posts", "comments"]
|
| 415 |
+
)
|
| 416 |
+
|
| 417 |
+
# ===== HISTORICAL DATA =====
|
| 418 |
+
self.sources["coingecko_historical"] = SentimentNewsSource(
|
| 419 |
+
id="coingecko_historical",
|
| 420 |
+
name="CoinGecko Historical",
|
| 421 |
+
source_type=SourceType.HISTORICAL.value,
|
| 422 |
+
url="https://api.coingecko.com/api/v3",
|
| 423 |
+
description="Historical price and market data",
|
| 424 |
+
requires_api_key=False,
|
| 425 |
+
rate_limit="10-50 req/min",
|
| 426 |
+
supported_timeframes=["1m", "5m", "15m", "30m", "1h", "4h", "1d", "1w"],
|
| 427 |
+
categories=["market", "historical"],
|
| 428 |
+
is_active=True,
|
| 429 |
+
priority=1,
|
| 430 |
+
verified=True,
|
| 431 |
+
free_tier=True,
|
| 432 |
+
features=["ohlcv", "market_chart", "price_history"]
|
| 433 |
+
)
|
| 434 |
+
|
| 435 |
+
self.sources["binance_historical"] = SentimentNewsSource(
|
| 436 |
+
id="binance_historical",
|
| 437 |
+
name="Binance Historical",
|
| 438 |
+
source_type=SourceType.HISTORICAL.value,
|
| 439 |
+
url="https://api.binance.com/api/v3",
|
| 440 |
+
description="Binance historical OHLCV data",
|
| 441 |
+
requires_api_key=False,
|
| 442 |
+
rate_limit="1200 req/min",
|
| 443 |
+
supported_timeframes=["1m", "5m", "15m", "30m", "1h", "4h", "1d", "1w", "1M"],
|
| 444 |
+
categories=["market", "historical", "ohlcv"],
|
| 445 |
+
is_active=True,
|
| 446 |
+
priority=1,
|
| 447 |
+
verified=True,
|
| 448 |
+
free_tier=True,
|
| 449 |
+
features=["klines", "historical_trades", "agg_trades"]
|
| 450 |
+
)
|
| 451 |
+
|
| 452 |
+
self.sources["cryptocompare_historical"] = SentimentNewsSource(
|
| 453 |
+
id="cryptocompare_historical",
|
| 454 |
+
name="CryptoCompare Historical",
|
| 455 |
+
source_type=SourceType.HISTORICAL.value,
|
| 456 |
+
url="https://min-api.cryptocompare.com/data/v2",
|
| 457 |
+
description="Historical price data",
|
| 458 |
+
requires_api_key=False,
|
| 459 |
+
rate_limit="100K/month",
|
| 460 |
+
supported_timeframes=["1m", "1h", "1d"],
|
| 461 |
+
categories=["market", "historical"],
|
| 462 |
+
is_active=True,
|
| 463 |
+
priority=2,
|
| 464 |
+
verified=True,
|
| 465 |
+
free_tier=True,
|
| 466 |
+
features=["histominute", "histohour", "histoday"]
|
| 467 |
+
)
|
| 468 |
+
|
| 469 |
+
# ===== AGGREGATED SOURCES =====
|
| 470 |
+
self.sources["coincap_realtime"] = SentimentNewsSource(
|
| 471 |
+
id="coincap_realtime",
|
| 472 |
+
name="CoinCap Real-time",
|
| 473 |
+
source_type=SourceType.AGGREGATED.value,
|
| 474 |
+
url="https://api.coincap.io/v2",
|
| 475 |
+
description="Real-time aggregated market data",
|
| 476 |
+
requires_api_key=False,
|
| 477 |
+
rate_limit="200 req/min",
|
| 478 |
+
supported_timeframes=["realtime", "1m", "5m", "15m", "30m", "1h", "1d"],
|
| 479 |
+
categories=["market", "realtime"],
|
| 480 |
+
is_active=True,
|
| 481 |
+
priority=1,
|
| 482 |
+
verified=True,
|
| 483 |
+
free_tier=True,
|
| 484 |
+
features=["assets", "rates", "exchanges", "markets", "candles"]
|
| 485 |
+
)
|
| 486 |
+
|
| 487 |
+
self.sources["coinpaprika"] = SentimentNewsSource(
|
| 488 |
+
id="coinpaprika",
|
| 489 |
+
name="CoinPaprika",
|
| 490 |
+
source_type=SourceType.AGGREGATED.value,
|
| 491 |
+
url="https://api.coinpaprika.com/v1",
|
| 492 |
+
description="Crypto market data with OHLCV",
|
| 493 |
+
requires_api_key=False,
|
| 494 |
+
rate_limit="unlimited",
|
| 495 |
+
supported_timeframes=["5m", "15m", "30m", "1h", "4h", "1d"],
|
| 496 |
+
categories=["market", "ohlcv"],
|
| 497 |
+
is_active=True,
|
| 498 |
+
priority=2,
|
| 499 |
+
verified=True,
|
| 500 |
+
free_tier=True,
|
| 501 |
+
features=["tickers", "coins", "exchanges", "ohlcv"]
|
| 502 |
+
)
|
| 503 |
+
|
| 504 |
+
self.sources["defillama"] = SentimentNewsSource(
|
| 505 |
+
id="defillama",
|
| 506 |
+
name="DefiLlama",
|
| 507 |
+
source_type=SourceType.AGGREGATED.value,
|
| 508 |
+
url="https://api.llama.fi",
|
| 509 |
+
description="DeFi TVL and protocol data",
|
| 510 |
+
requires_api_key=False,
|
| 511 |
+
rate_limit="300 req/min",
|
| 512 |
+
supported_timeframes=["1h", "1d"],
|
| 513 |
+
categories=["defi", "tvl"],
|
| 514 |
+
is_active=True,
|
| 515 |
+
priority=1,
|
| 516 |
+
verified=True,
|
| 517 |
+
free_tier=True,
|
| 518 |
+
features=["protocols", "tvl", "chains", "yields", "stablecoins"]
|
| 519 |
+
)
|
| 520 |
+
|
| 521 |
+
# ===== MARKET INDICES =====
|
| 522 |
+
self.sources["tradingview_public"] = SentimentNewsSource(
|
| 523 |
+
id="tradingview_public",
|
| 524 |
+
name="TradingView Public",
|
| 525 |
+
source_type=SourceType.MARKET_MOOD.value,
|
| 526 |
+
url="https://www.tradingview.com",
|
| 527 |
+
description="Public technical indicators (scraping)",
|
| 528 |
+
requires_api_key=False,
|
| 529 |
+
rate_limit="varies",
|
| 530 |
+
supported_timeframes=["realtime", "1h", "1d"],
|
| 531 |
+
categories=["technical", "indicators"],
|
| 532 |
+
is_active=True,
|
| 533 |
+
priority=4,
|
| 534 |
+
verified=False,
|
| 535 |
+
free_tier=True,
|
| 536 |
+
features=["indicators", "signals", "screener"]
|
| 537 |
+
)
|
| 538 |
+
|
| 539 |
+
self.sources["taapi"] = SentimentNewsSource(
|
| 540 |
+
id="taapi",
|
| 541 |
+
name="TAAPI.IO",
|
| 542 |
+
source_type=SourceType.MARKET_MOOD.value,
|
| 543 |
+
url="https://api.taapi.io",
|
| 544 |
+
description="Technical Analysis API",
|
| 545 |
+
requires_api_key=True,
|
| 546 |
+
api_key_env="TAAPI_KEY",
|
| 547 |
+
rate_limit="50 req/day (free)",
|
| 548 |
+
supported_timeframes=["1m", "5m", "15m", "30m", "1h", "4h", "1d"],
|
| 549 |
+
categories=["technical", "indicators"],
|
| 550 |
+
is_active=True,
|
| 551 |
+
priority=3,
|
| 552 |
+
verified=False,
|
| 553 |
+
free_tier=True,
|
| 554 |
+
features=["rsi", "macd", "bollinger", "ema", "sma"]
|
| 555 |
+
)
|
| 556 |
+
|
| 557 |
+
# ===== QUERY METHODS =====
|
| 558 |
+
|
| 559 |
+
def get_all_sources(self) -> List[SentimentNewsSource]:
|
| 560 |
+
"""دریافت همه منابع"""
|
| 561 |
+
return list(self.sources.values())
|
| 562 |
+
|
| 563 |
+
def get_active_sources(self) -> List[SentimentNewsSource]:
|
| 564 |
+
"""دریافت منابع فعال"""
|
| 565 |
+
return [s for s in self.sources.values() if s.is_active]
|
| 566 |
+
|
| 567 |
+
def get_source_by_id(self, source_id: str) -> Optional[SentimentNewsSource]:
|
| 568 |
+
"""دریافت منبع با شناسه"""
|
| 569 |
+
return self.sources.get(source_id)
|
| 570 |
+
|
| 571 |
+
def get_sources_by_type(self, source_type: str) -> List[SentimentNewsSource]:
|
| 572 |
+
"""دریافت منابع بر اساس نوع"""
|
| 573 |
+
return [s for s in self.sources.values() if s.source_type == source_type]
|
| 574 |
+
|
| 575 |
+
def get_free_sources(self) -> List[SentimentNewsSource]:
|
| 576 |
+
"""دریافت منابع رایگان"""
|
| 577 |
+
return [s for s in self.sources.values() if s.free_tier and not s.requires_api_key]
|
| 578 |
+
|
| 579 |
+
def get_sources_by_timeframe(self, timeframe: str) -> List[SentimentNewsSource]:
|
| 580 |
+
"""دریافت منابع بر اساس بازه زمانی"""
|
| 581 |
+
return [s for s in self.sources.values() if timeframe in s.supported_timeframes]
|
| 582 |
+
|
| 583 |
+
def get_sources_by_category(self, category: str) -> List[SentimentNewsSource]:
|
| 584 |
+
"""دریافت منابع بر اساس دستهبندی"""
|
| 585 |
+
return [s for s in self.sources.values() if category in s.categories]
|
| 586 |
+
|
| 587 |
+
def search_sources(self, query: str) -> List[SentimentNewsSource]:
|
| 588 |
+
"""جستجوی منابع"""
|
| 589 |
+
query_lower = query.lower()
|
| 590 |
+
results = []
|
| 591 |
+
for source in self.sources.values():
|
| 592 |
+
if (query_lower in source.name.lower() or
|
| 593 |
+
query_lower in source.description.lower() or
|
| 594 |
+
any(query_lower in cat.lower() for cat in source.categories) or
|
| 595 |
+
any(query_lower in f.lower() for f in source.features)):
|
| 596 |
+
results.append(source)
|
| 597 |
+
return results
|
| 598 |
+
|
| 599 |
+
def get_statistics(self) -> Dict[str, Any]:
|
| 600 |
+
"""آمار منابع"""
|
| 601 |
+
all_sources = self.get_all_sources()
|
| 602 |
+
return {
|
| 603 |
+
"total_sources": len(all_sources),
|
| 604 |
+
"active_sources": len([s for s in all_sources if s.is_active]),
|
| 605 |
+
"free_sources": len([s for s in all_sources if s.free_tier]),
|
| 606 |
+
"no_key_required": len([s for s in all_sources if not s.requires_api_key]),
|
| 607 |
+
"verified_sources": len([s for s in all_sources if s.verified]),
|
| 608 |
+
"by_type": {
|
| 609 |
+
st.value: len([s for s in all_sources if s.source_type == st.value])
|
| 610 |
+
for st in SourceType
|
| 611 |
+
},
|
| 612 |
+
"categories": list(set(cat for s in all_sources for cat in s.categories))
|
| 613 |
+
}
|
| 614 |
+
|
| 615 |
+
def set_source_active(self, source_id: str, is_active: bool) -> bool:
|
| 616 |
+
"""تنظیم فعال/غیرفعال بودن منبع"""
|
| 617 |
+
if source_id in self.sources:
|
| 618 |
+
self.sources[source_id].is_active = is_active
|
| 619 |
+
return True
|
| 620 |
+
return False
|
| 621 |
+
|
| 622 |
+
def to_dict(self) -> Dict[str, Any]:
|
| 623 |
+
"""تبدیل به دیکشنری"""
|
| 624 |
+
return {
|
| 625 |
+
source_id: {
|
| 626 |
+
"id": source.id,
|
| 627 |
+
"name": source.name,
|
| 628 |
+
"source_type": source.source_type,
|
| 629 |
+
"url": source.url,
|
| 630 |
+
"description": source.description,
|
| 631 |
+
"requires_api_key": source.requires_api_key,
|
| 632 |
+
"api_key_env": source.api_key_env,
|
| 633 |
+
"rate_limit": source.rate_limit,
|
| 634 |
+
"supported_timeframes": source.supported_timeframes,
|
| 635 |
+
"categories": source.categories,
|
| 636 |
+
"is_active": source.is_active,
|
| 637 |
+
"priority": source.priority,
|
| 638 |
+
"verified": source.verified,
|
| 639 |
+
"free_tier": source.free_tier,
|
| 640 |
+
"features": source.features
|
| 641 |
+
}
|
| 642 |
+
for source_id, source in self.sources.items()
|
| 643 |
+
}
|
| 644 |
+
|
| 645 |
+
|
| 646 |
+
# ===== DATA FETCHERS =====
|
| 647 |
+
|
| 648 |
+
class SentimentNewsFetcher:
|
| 649 |
+
"""دریافت داده از منابع سنتیمنت و اخبار"""
|
| 650 |
+
|
| 651 |
+
def __init__(self):
|
| 652 |
+
self.registry = SentimentNewsRegistry()
|
| 653 |
+
self.timeout = aiohttp.ClientTimeout(total=15)
|
| 654 |
+
|
| 655 |
+
async def fetch_fear_greed_index(self, limit: int = 30) -> Dict[str, Any]:
|
| 656 |
+
"""دریافت شاخص ترس و طمع"""
|
| 657 |
+
source = self.registry.get_source_by_id("fear_greed_index")
|
| 658 |
+
if not source or not source.is_active:
|
| 659 |
+
return {"success": False, "error": "Source not available"}
|
| 660 |
+
|
| 661 |
+
try:
|
| 662 |
+
url = f"{source.url}?limit={limit}"
|
| 663 |
+
async with aiohttp.ClientSession(timeout=self.timeout) as session:
|
| 664 |
+
async with session.get(url) as response:
|
| 665 |
+
if response.status == 200:
|
| 666 |
+
data = await response.json()
|
| 667 |
+
return {
|
| 668 |
+
"success": True,
|
| 669 |
+
"data": data.get("data", []),
|
| 670 |
+
"source": "fear_greed_index"
|
| 671 |
+
}
|
| 672 |
+
return {"success": False, "error": f"HTTP {response.status}"}
|
| 673 |
+
except Exception as e:
|
| 674 |
+
logger.error(f"Fear & Greed fetch error: {e}")
|
| 675 |
+
return {"success": False, "error": str(e)}
|
| 676 |
+
|
| 677 |
+
async def fetch_rss_news(self, source_id: str, limit: int = 20) -> Dict[str, Any]:
|
| 678 |
+
"""دریافت اخبار از RSS"""
|
| 679 |
+
source = self.registry.get_source_by_id(source_id)
|
| 680 |
+
if not source or not source.is_active:
|
| 681 |
+
return {"success": False, "error": "Source not available"}
|
| 682 |
+
|
| 683 |
+
try:
|
| 684 |
+
loop = asyncio.get_event_loop()
|
| 685 |
+
feed = await loop.run_in_executor(None, feedparser.parse, source.url)
|
| 686 |
+
|
| 687 |
+
articles = []
|
| 688 |
+
for entry in feed.entries[:limit]:
|
| 689 |
+
articles.append({
|
| 690 |
+
"title": entry.get("title", ""),
|
| 691 |
+
"link": entry.get("link", ""),
|
| 692 |
+
"published": entry.get("published", ""),
|
| 693 |
+
"summary": entry.get("summary", "")[:500] if entry.get("summary") else "",
|
| 694 |
+
"author": entry.get("author", ""),
|
| 695 |
+
"source": source.name
|
| 696 |
+
})
|
| 697 |
+
|
| 698 |
+
return {
|
| 699 |
+
"success": True,
|
| 700 |
+
"data": articles,
|
| 701 |
+
"count": len(articles),
|
| 702 |
+
"source": source_id
|
| 703 |
+
}
|
| 704 |
+
except Exception as e:
|
| 705 |
+
logger.error(f"RSS fetch error for {source_id}: {e}")
|
| 706 |
+
return {"success": False, "error": str(e)}
|
| 707 |
+
|
| 708 |
+
async def fetch_all_rss_news(self, limit_per_source: int = 10) -> Dict[str, Any]:
|
| 709 |
+
"""دریافت اخبار از همه منابع RSS"""
|
| 710 |
+
rss_sources = [s for s in self.registry.get_active_sources()
|
| 711 |
+
if "_rss" in s.id or s.url.endswith("/feed")]
|
| 712 |
+
|
| 713 |
+
all_news = []
|
| 714 |
+
tasks = [self.fetch_rss_news(s.id, limit_per_source) for s in rss_sources]
|
| 715 |
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
| 716 |
+
|
| 717 |
+
for result in results:
|
| 718 |
+
if isinstance(result, dict) and result.get("success"):
|
| 719 |
+
all_news.extend(result.get("data", []))
|
| 720 |
+
|
| 721 |
+
# Sort by published date if available
|
| 722 |
+
all_news.sort(key=lambda x: x.get("published", ""), reverse=True)
|
| 723 |
+
|
| 724 |
+
return {
|
| 725 |
+
"success": True,
|
| 726 |
+
"data": all_news,
|
| 727 |
+
"count": len(all_news),
|
| 728 |
+
"sources": [s.id for s in rss_sources]
|
| 729 |
+
}
|
| 730 |
+
|
| 731 |
+
async def fetch_reddit_posts(self, subreddit: str = "cryptocurrency", limit: int = 25) -> Dict[str, Any]:
|
| 732 |
+
"""دریافت پستهای ردیت"""
|
| 733 |
+
source_id = f"reddit_{subreddit.lower()}"
|
| 734 |
+
source = self.registry.get_source_by_id(source_id)
|
| 735 |
+
|
| 736 |
+
if not source:
|
| 737 |
+
url = f"https://www.reddit.com/r/{subreddit}/new.json?limit={limit}"
|
| 738 |
+
else:
|
| 739 |
+
url = f"{source.url}?limit={limit}"
|
| 740 |
+
|
| 741 |
+
try:
|
| 742 |
+
headers = {"User-Agent": "CryptoMonitor/1.0"}
|
| 743 |
+
async with aiohttp.ClientSession(timeout=self.timeout) as session:
|
| 744 |
+
async with session.get(url, headers=headers) as response:
|
| 745 |
+
if response.status == 200:
|
| 746 |
+
data = await response.json()
|
| 747 |
+
posts = []
|
| 748 |
+
for post in data.get("data", {}).get("children", []):
|
| 749 |
+
post_data = post.get("data", {})
|
| 750 |
+
posts.append({
|
| 751 |
+
"title": post_data.get("title", ""),
|
| 752 |
+
"url": f"https://reddit.com{post_data.get('permalink', '')}",
|
| 753 |
+
"score": post_data.get("score", 0),
|
| 754 |
+
"num_comments": post_data.get("num_comments", 0),
|
| 755 |
+
"created_utc": post_data.get("created_utc", 0),
|
| 756 |
+
"author": post_data.get("author", ""),
|
| 757 |
+
"subreddit": subreddit
|
| 758 |
+
})
|
| 759 |
+
return {
|
| 760 |
+
"success": True,
|
| 761 |
+
"data": posts,
|
| 762 |
+
"count": len(posts),
|
| 763 |
+
"source": f"reddit_{subreddit}"
|
| 764 |
+
}
|
| 765 |
+
return {"success": False, "error": f"HTTP {response.status}"}
|
| 766 |
+
except Exception as e:
|
| 767 |
+
logger.error(f"Reddit fetch error: {e}")
|
| 768 |
+
return {"success": False, "error": str(e)}
|
| 769 |
+
|
| 770 |
+
async def fetch_cryptocompare_news(self, categories: str = "", limit: int = 50) -> Dict[str, Any]:
|
| 771 |
+
"""دریافت اخبار از CryptoCompare"""
|
| 772 |
+
source = self.registry.get_source_by_id("cryptocompare_news")
|
| 773 |
+
if not source or not source.is_active:
|
| 774 |
+
return {"success": False, "error": "Source not available"}
|
| 775 |
+
|
| 776 |
+
try:
|
| 777 |
+
params = {"lang": "EN"}
|
| 778 |
+
if categories:
|
| 779 |
+
params["categories"] = categories
|
| 780 |
+
|
| 781 |
+
url = source.url
|
| 782 |
+
async with aiohttp.ClientSession(timeout=self.timeout) as session:
|
| 783 |
+
async with session.get(url, params=params) as response:
|
| 784 |
+
if response.status == 200:
|
| 785 |
+
data = await response.json()
|
| 786 |
+
articles = []
|
| 787 |
+
for article in data.get("Data", [])[:limit]:
|
| 788 |
+
articles.append({
|
| 789 |
+
"id": article.get("id"),
|
| 790 |
+
"title": article.get("title", ""),
|
| 791 |
+
"body": article.get("body", "")[:500],
|
| 792 |
+
"url": article.get("url", ""),
|
| 793 |
+
"source": article.get("source", ""),
|
| 794 |
+
"published_on": article.get("published_on", 0),
|
| 795 |
+
"categories": article.get("categories", "")
|
| 796 |
+
})
|
| 797 |
+
return {
|
| 798 |
+
"success": True,
|
| 799 |
+
"data": articles,
|
| 800 |
+
"count": len(articles),
|
| 801 |
+
"source": "cryptocompare_news"
|
| 802 |
+
}
|
| 803 |
+
return {"success": False, "error": f"HTTP {response.status}"}
|
| 804 |
+
except Exception as e:
|
| 805 |
+
logger.error(f"CryptoCompare news fetch error: {e}")
|
| 806 |
+
return {"success": False, "error": str(e)}
|
| 807 |
+
|
| 808 |
+
|
| 809 |
+
# ===== SINGLETON =====
|
| 810 |
+
_registry = None
|
| 811 |
+
_fetcher = None
|
| 812 |
+
|
| 813 |
+
|
| 814 |
+
def get_sentiment_news_registry() -> SentimentNewsRegistry:
|
| 815 |
+
"""دریافت instance سراسری registry"""
|
| 816 |
+
global _registry
|
| 817 |
+
if _registry is None:
|
| 818 |
+
_registry = SentimentNewsRegistry()
|
| 819 |
+
return _registry
|
| 820 |
+
|
| 821 |
+
|
| 822 |
+
def get_sentiment_news_fetcher() -> SentimentNewsFetcher:
|
| 823 |
+
"""دریافت instance سراسری fetcher"""
|
| 824 |
+
global _fetcher
|
| 825 |
+
if _fetcher is None:
|
| 826 |
+
_fetcher = SentimentNewsFetcher()
|
| 827 |
+
return _fetcher
|
| 828 |
+
|
| 829 |
+
|
| 830 |
+
# ===== TEST =====
|
| 831 |
+
if __name__ == "__main__":
|
| 832 |
+
print("="*70)
|
| 833 |
+
print("🧪 Testing Sentiment & News Providers Registry")
|
| 834 |
+
print("="*70)
|
| 835 |
+
|
| 836 |
+
registry = SentimentNewsRegistry()
|
| 837 |
+
stats = registry.get_statistics()
|
| 838 |
+
|
| 839 |
+
print(f"\n📊 Statistics:")
|
| 840 |
+
print(f" Total Sources: {stats['total_sources']}")
|
| 841 |
+
print(f" Active: {stats['active_sources']}")
|
| 842 |
+
print(f" Free: {stats['free_sources']}")
|
| 843 |
+
print(f" No Key Required: {stats['no_key_required']}")
|
| 844 |
+
print(f" Verified: {stats['verified_sources']}")
|
| 845 |
+
|
| 846 |
+
print(f"\n By Type:")
|
| 847 |
+
for source_type, count in stats['by_type'].items():
|
| 848 |
+
print(f" • {source_type.upper()}: {count}")
|
| 849 |
+
|
| 850 |
+
print(f"\n⭐ Free Sources (No API Key):")
|
| 851 |
+
free_sources = registry.get_free_sources()
|
| 852 |
+
for i, s in enumerate(free_sources[:10], 1):
|
| 853 |
+
marker = "✅" if s.verified else "🟡"
|
| 854 |
+
print(f" {marker} {i}. {s.name} - {s.description[:50]}...")
|
| 855 |
+
|
| 856 |
+
print("\n" + "="*70)
|
| 857 |
+
|
| 858 |
+
# Test fetching
|
| 859 |
+
async def test_fetching():
|
| 860 |
+
fetcher = SentimentNewsFetcher()
|
| 861 |
+
|
| 862 |
+
print("\n🧪 Testing Fear & Greed Index...")
|
| 863 |
+
result = await fetcher.fetch_fear_greed_index(limit=5)
|
| 864 |
+
if result["success"]:
|
| 865 |
+
print(f" ✅ Got {len(result['data'])} entries")
|
| 866 |
+
else:
|
| 867 |
+
print(f" ❌ Error: {result.get('error')}")
|
| 868 |
+
|
| 869 |
+
print("\n🧪 Testing RSS News (Decrypt)...")
|
| 870 |
+
result = await fetcher.fetch_rss_news("decrypt_rss", limit=3)
|
| 871 |
+
if result["success"]:
|
| 872 |
+
print(f" ✅ Got {result['count']} articles")
|
| 873 |
+
for article in result['data'][:2]:
|
| 874 |
+
print(f" • {article['title'][:50]}...")
|
| 875 |
+
else:
|
| 876 |
+
print(f" ❌ Error: {result.get('error')}")
|
| 877 |
+
|
| 878 |
+
print("\n🧪 Testing Reddit Posts...")
|
| 879 |
+
result = await fetcher.fetch_reddit_posts("cryptocurrency", limit=3)
|
| 880 |
+
if result["success"]:
|
| 881 |
+
print(f" ✅ Got {result['count']} posts")
|
| 882 |
+
else:
|
| 883 |
+
print(f" ❌ Error: {result.get('error')}")
|
| 884 |
+
|
| 885 |
+
asyncio.run(test_fetching())
|
| 886 |
+
|
| 887 |
+
print("\n" + "="*70)
|
| 888 |
+
print("✅ Sentiment & News Providers Registry Complete!")
|
| 889 |
+
print("="*70)
|
config/api_keys.json
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"description": "API Keys Configuration for Crypto Intelligence Hub",
|
| 3 |
+
"last_updated": "2025-12-12",
|
| 4 |
+
|
| 5 |
+
"block_explorers": {
|
| 6 |
+
"etherscan": {
|
| 7 |
+
"key": "SZHYFZK2RR8H9TIMJBVW54V4H81K2Z2KR2",
|
| 8 |
+
"backup_key": "T6IR8VJHX2NE6ZJW2S3FDVN1TYG4PYYI45",
|
| 9 |
+
"url": "https://api.etherscan.io/api",
|
| 10 |
+
"rate_limit": "5 req/sec"
|
| 11 |
+
},
|
| 12 |
+
"bscscan": {
|
| 13 |
+
"key": "K62RKHGXTDCG53RU4MCG6XABIMJKTN19IT",
|
| 14 |
+
"url": "https://api.bscscan.com/api",
|
| 15 |
+
"rate_limit": "5 req/sec"
|
| 16 |
+
},
|
| 17 |
+
"tronscan": {
|
| 18 |
+
"key": "7ae72726-bffe-4e74-9c33-97b761eeea21",
|
| 19 |
+
"url": "https://apilist.tronscanapi.com/api",
|
| 20 |
+
"rate_limit": "varies"
|
| 21 |
+
}
|
| 22 |
+
},
|
| 23 |
+
|
| 24 |
+
"market_data": {
|
| 25 |
+
"coinmarketcap": {
|
| 26 |
+
"keys": [
|
| 27 |
+
"a35ffaec-c66c-4f16-81e3-41a717e4822f",
|
| 28 |
+
"04cf4b5b-9868-465c-8ba0-9f2e78c92eb1"
|
| 29 |
+
],
|
| 30 |
+
"url": "https://pro-api.coinmarketcap.com/v1",
|
| 31 |
+
"rate_limit": "333 req/day per key",
|
| 32 |
+
"endpoints": {
|
| 33 |
+
"listings": "/cryptocurrency/listings/latest",
|
| 34 |
+
"quotes": "/cryptocurrency/quotes/latest",
|
| 35 |
+
"info": "/cryptocurrency/info"
|
| 36 |
+
}
|
| 37 |
+
}
|
| 38 |
+
},
|
| 39 |
+
|
| 40 |
+
"news": {
|
| 41 |
+
"newsapi": {
|
| 42 |
+
"key": "968a5e25552b4cb5ba3280361d8444ab",
|
| 43 |
+
"url": "https://newsapi.org/v2",
|
| 44 |
+
"rate_limit": "100 req/day (free)",
|
| 45 |
+
"endpoints": {
|
| 46 |
+
"everything": "/everything",
|
| 47 |
+
"top_headlines": "/top-headlines"
|
| 48 |
+
}
|
| 49 |
+
}
|
| 50 |
+
},
|
| 51 |
+
|
| 52 |
+
"sentiment": {
|
| 53 |
+
"custom_sentiment_api": {
|
| 54 |
+
"key": "vltdvdho63uqnjgf_fq75qbks72e3wfmx",
|
| 55 |
+
"description": "Custom sentiment analysis API"
|
| 56 |
+
}
|
| 57 |
+
},
|
| 58 |
+
|
| 59 |
+
"ai_models": {
|
| 60 |
+
"huggingface": {
|
| 61 |
+
"key": "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",
|
| 62 |
+
"url": "https://api-inference.huggingface.co/models",
|
| 63 |
+
"rate_limit": "varies"
|
| 64 |
+
}
|
| 65 |
+
},
|
| 66 |
+
|
| 67 |
+
"notifications": {
|
| 68 |
+
"telegram": {
|
| 69 |
+
"enabled": true,
|
| 70 |
+
"bot_token": "7437859619:AAGeGG3ZkLM0OVaw-Exx1uMRE55JtBCZZCY",
|
| 71 |
+
"chat_id": "-1002228627548"
|
| 72 |
+
}
|
| 73 |
+
},
|
| 74 |
+
|
| 75 |
+
"environment_variables": {
|
| 76 |
+
"description": "Set these in your environment or .env file",
|
| 77 |
+
"variables": [
|
| 78 |
+
"ETHERSCAN_KEY=SZHYFZK2RR8H9TIMJBVW54V4H81K2Z2KR2",
|
| 79 |
+
"ETHERSCAN_BACKUP_KEY=T6IR8VJHX2NE6ZJW2S3FDVN1TYG4PYYI45",
|
| 80 |
+
"BSCSCAN_KEY=K62RKHGXTDCG53RU4MCG6XABIMJKTN19IT",
|
| 81 |
+
"TRONSCAN_KEY=7ae72726-bffe-4e74-9c33-97b761eeea21",
|
| 82 |
+
"COINMARKETCAP_KEY_1=a35ffaec-c66c-4f16-81e3-41a717e4822f",
|
| 83 |
+
"COINMARKETCAP_KEY_2=04cf4b5b-9868-465c-8ba0-9f2e78c92eb1",
|
| 84 |
+
"NEWSAPI_KEY=968a5e25552b4cb5ba3280361d8444ab",
|
| 85 |
+
"SENTIMENT_API_KEY=vltdvdho63uqnjgf_fq75qbks72e3wfmx",
|
| 86 |
+
"HF_TOKEN=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",
|
| 87 |
+
"TELEGRAM_BOT_TOKEN=7437859619:AAGeGG3ZkLM0OVaw-Exx1uMRE55JtBCZZCY",
|
| 88 |
+
"TELEGRAM_CHAT_ID=-1002228627548"
|
| 89 |
+
]
|
| 90 |
+
}
|
| 91 |
+
}
|
database/data_sources_model.py
ADDED
|
@@ -0,0 +1,487 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Data Sources Database Model
|
| 4 |
+
مدل دیتابیس برای مدیریت منابع داده
|
| 5 |
+
|
| 6 |
+
این مدل برای ذخیره و مدیریت منابع داده استفاده میشود.
|
| 7 |
+
شامل اطلاعات منبع، وضعیت فعال/غیرفعال، و آمار استفاده.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from sqlalchemy import Column, Integer, String, Float, Boolean, DateTime, Text, Enum, Index
|
| 11 |
+
from sqlalchemy.orm import relationship
|
| 12 |
+
from datetime import datetime
|
| 13 |
+
import enum
|
| 14 |
+
from typing import Dict, Any, List, Optional
|
| 15 |
+
import json
|
| 16 |
+
|
| 17 |
+
# Use the existing Base from models.py
|
| 18 |
+
try:
|
| 19 |
+
from database.models import Base
|
| 20 |
+
except ImportError:
|
| 21 |
+
from sqlalchemy.ext.declarative import declarative_base
|
| 22 |
+
Base = declarative_base()
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class DataSourceType(enum.Enum):
|
| 26 |
+
"""نوع منبع داده"""
|
| 27 |
+
MARKET = "market"
|
| 28 |
+
NEWS = "news"
|
| 29 |
+
SENTIMENT = "sentiment"
|
| 30 |
+
SOCIAL = "social"
|
| 31 |
+
ONCHAIN = "onchain"
|
| 32 |
+
DEFI = "defi"
|
| 33 |
+
HISTORICAL = "historical"
|
| 34 |
+
TECHNICAL = "technical"
|
| 35 |
+
AGGREGATED = "aggregated"
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class DataSourceStatus(enum.Enum):
|
| 39 |
+
"""وضعیت منبع داده"""
|
| 40 |
+
ACTIVE = "active"
|
| 41 |
+
INACTIVE = "inactive"
|
| 42 |
+
RATE_LIMITED = "rate_limited"
|
| 43 |
+
ERROR = "error"
|
| 44 |
+
MAINTENANCE = "maintenance"
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
class CollectionInterval(enum.Enum):
|
| 48 |
+
"""بازه جمعآوری داده"""
|
| 49 |
+
REALTIME = "realtime" # On-demand from client
|
| 50 |
+
MINUTES_1 = "1m"
|
| 51 |
+
MINUTES_5 = "5m"
|
| 52 |
+
MINUTES_15 = "15m"
|
| 53 |
+
MINUTES_30 = "30m"
|
| 54 |
+
HOURLY = "1h"
|
| 55 |
+
HOURS_4 = "4h"
|
| 56 |
+
DAILY = "1d"
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
class DataSource(Base):
|
| 60 |
+
"""
|
| 61 |
+
Data Source Model - منبع داده
|
| 62 |
+
ذخیره اطلاعات و وضعیت منابع داده در دیتابیس
|
| 63 |
+
"""
|
| 64 |
+
__tablename__ = 'data_sources'
|
| 65 |
+
|
| 66 |
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
| 67 |
+
|
| 68 |
+
# Basic Info
|
| 69 |
+
source_id = Column(String(100), nullable=False, unique=True, index=True)
|
| 70 |
+
name = Column(String(255), nullable=False)
|
| 71 |
+
source_type = Column(String(50), nullable=False, index=True)
|
| 72 |
+
description = Column(Text, nullable=True)
|
| 73 |
+
|
| 74 |
+
# Connection Info
|
| 75 |
+
base_url = Column(String(500), nullable=False)
|
| 76 |
+
api_version = Column(String(20), nullable=True)
|
| 77 |
+
|
| 78 |
+
# Authentication
|
| 79 |
+
requires_api_key = Column(Boolean, default=False)
|
| 80 |
+
api_key_env_var = Column(String(100), nullable=True)
|
| 81 |
+
has_api_key_configured = Column(Boolean, default=False)
|
| 82 |
+
|
| 83 |
+
# Rate Limiting
|
| 84 |
+
rate_limit_description = Column(String(100), nullable=True)
|
| 85 |
+
rate_limit_per_minute = Column(Integer, nullable=True)
|
| 86 |
+
rate_limit_per_hour = Column(Integer, nullable=True)
|
| 87 |
+
rate_limit_per_day = Column(Integer, nullable=True)
|
| 88 |
+
|
| 89 |
+
# Collection Settings
|
| 90 |
+
collection_interval = Column(String(20), default="30m") # Default: 30 minutes for bulk
|
| 91 |
+
supports_realtime = Column(Boolean, default=False) # Can fetch on-demand
|
| 92 |
+
|
| 93 |
+
# Supported Features
|
| 94 |
+
supported_timeframes = Column(Text, nullable=True) # JSON array
|
| 95 |
+
categories = Column(Text, nullable=True) # JSON array
|
| 96 |
+
features = Column(Text, nullable=True) # JSON array
|
| 97 |
+
|
| 98 |
+
# Status
|
| 99 |
+
is_active = Column(Boolean, default=True, index=True)
|
| 100 |
+
status = Column(String(50), default="active", index=True)
|
| 101 |
+
status_message = Column(Text, nullable=True)
|
| 102 |
+
|
| 103 |
+
# Priority & Weight
|
| 104 |
+
priority = Column(Integer, default=5) # 1-10, lower is higher priority
|
| 105 |
+
weight = Column(Integer, default=1) # For load balancing
|
| 106 |
+
|
| 107 |
+
# Verification
|
| 108 |
+
is_verified = Column(Boolean, default=False)
|
| 109 |
+
is_free_tier = Column(Boolean, default=True)
|
| 110 |
+
|
| 111 |
+
# Statistics
|
| 112 |
+
total_requests = Column(Integer, default=0)
|
| 113 |
+
successful_requests = Column(Integer, default=0)
|
| 114 |
+
failed_requests = Column(Integer, default=0)
|
| 115 |
+
avg_response_time_ms = Column(Float, default=0.0)
|
| 116 |
+
last_success_at = Column(DateTime, nullable=True)
|
| 117 |
+
last_failure_at = Column(DateTime, nullable=True)
|
| 118 |
+
last_error_message = Column(Text, nullable=True)
|
| 119 |
+
|
| 120 |
+
# Timestamps
|
| 121 |
+
created_at = Column(DateTime, default=datetime.utcnow)
|
| 122 |
+
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
| 123 |
+
last_checked_at = Column(DateTime, nullable=True)
|
| 124 |
+
|
| 125 |
+
# Indexes for common queries
|
| 126 |
+
__table_args__ = (
|
| 127 |
+
Index('idx_source_type_active', 'source_type', 'is_active'),
|
| 128 |
+
Index('idx_status_priority', 'status', 'priority'),
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
def __repr__(self):
|
| 132 |
+
return f"<DataSource(id={self.source_id}, name={self.name}, active={self.is_active})>"
|
| 133 |
+
|
| 134 |
+
def to_dict(self) -> Dict[str, Any]:
|
| 135 |
+
"""تبدیل به دیکشنری"""
|
| 136 |
+
return {
|
| 137 |
+
"id": self.id,
|
| 138 |
+
"source_id": self.source_id,
|
| 139 |
+
"name": self.name,
|
| 140 |
+
"source_type": self.source_type,
|
| 141 |
+
"description": self.description,
|
| 142 |
+
"base_url": self.base_url,
|
| 143 |
+
"api_version": self.api_version,
|
| 144 |
+
"requires_api_key": self.requires_api_key,
|
| 145 |
+
"api_key_env_var": self.api_key_env_var,
|
| 146 |
+
"has_api_key_configured": self.has_api_key_configured,
|
| 147 |
+
"rate_limit_description": self.rate_limit_description,
|
| 148 |
+
"collection_interval": self.collection_interval,
|
| 149 |
+
"supports_realtime": self.supports_realtime,
|
| 150 |
+
"supported_timeframes": json.loads(self.supported_timeframes) if self.supported_timeframes else [],
|
| 151 |
+
"categories": json.loads(self.categories) if self.categories else [],
|
| 152 |
+
"features": json.loads(self.features) if self.features else [],
|
| 153 |
+
"is_active": self.is_active,
|
| 154 |
+
"status": self.status,
|
| 155 |
+
"status_message": self.status_message,
|
| 156 |
+
"priority": self.priority,
|
| 157 |
+
"weight": self.weight,
|
| 158 |
+
"is_verified": self.is_verified,
|
| 159 |
+
"is_free_tier": self.is_free_tier,
|
| 160 |
+
"total_requests": self.total_requests,
|
| 161 |
+
"successful_requests": self.successful_requests,
|
| 162 |
+
"failed_requests": self.failed_requests,
|
| 163 |
+
"success_rate": (self.successful_requests / self.total_requests * 100) if self.total_requests > 0 else 0,
|
| 164 |
+
"avg_response_time_ms": self.avg_response_time_ms,
|
| 165 |
+
"last_success_at": self.last_success_at.isoformat() if self.last_success_at else None,
|
| 166 |
+
"last_failure_at": self.last_failure_at.isoformat() if self.last_failure_at else None,
|
| 167 |
+
"created_at": self.created_at.isoformat() if self.created_at else None,
|
| 168 |
+
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
| 169 |
+
"last_checked_at": self.last_checked_at.isoformat() if self.last_checked_at else None
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
class DataCollectionLog(Base):
|
| 174 |
+
"""
|
| 175 |
+
Data Collection Log - لاگ جمعآوری داده
|
| 176 |
+
ثبت تاریخچه جمعآوری داده از منابع
|
| 177 |
+
"""
|
| 178 |
+
__tablename__ = 'data_collection_logs'
|
| 179 |
+
|
| 180 |
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
| 181 |
+
source_id = Column(String(100), nullable=False, index=True)
|
| 182 |
+
|
| 183 |
+
# Collection Info
|
| 184 |
+
collection_type = Column(String(50), nullable=False) # scheduled, on_demand, bulk
|
| 185 |
+
interval_used = Column(String(20), nullable=True)
|
| 186 |
+
|
| 187 |
+
# Timing
|
| 188 |
+
started_at = Column(DateTime, nullable=False, default=datetime.utcnow)
|
| 189 |
+
completed_at = Column(DateTime, nullable=True)
|
| 190 |
+
duration_ms = Column(Integer, nullable=True)
|
| 191 |
+
|
| 192 |
+
# Results
|
| 193 |
+
success = Column(Boolean, default=False)
|
| 194 |
+
records_fetched = Column(Integer, default=0)
|
| 195 |
+
records_stored = Column(Integer, default=0)
|
| 196 |
+
|
| 197 |
+
# Error Info
|
| 198 |
+
error_type = Column(String(100), nullable=True)
|
| 199 |
+
error_message = Column(Text, nullable=True)
|
| 200 |
+
|
| 201 |
+
# HTTP Info
|
| 202 |
+
http_status_code = Column(Integer, nullable=True)
|
| 203 |
+
response_size_bytes = Column(Integer, nullable=True)
|
| 204 |
+
|
| 205 |
+
# Indexes
|
| 206 |
+
__table_args__ = (
|
| 207 |
+
Index('idx_collection_source_time', 'source_id', 'started_at'),
|
| 208 |
+
Index('idx_collection_success', 'success', 'started_at'),
|
| 209 |
+
)
|
| 210 |
+
|
| 211 |
+
def to_dict(self) -> Dict[str, Any]:
|
| 212 |
+
"""تبدیل به دیکشنری"""
|
| 213 |
+
return {
|
| 214 |
+
"id": self.id,
|
| 215 |
+
"source_id": self.source_id,
|
| 216 |
+
"collection_type": self.collection_type,
|
| 217 |
+
"interval_used": self.interval_used,
|
| 218 |
+
"started_at": self.started_at.isoformat() if self.started_at else None,
|
| 219 |
+
"completed_at": self.completed_at.isoformat() if self.completed_at else None,
|
| 220 |
+
"duration_ms": self.duration_ms,
|
| 221 |
+
"success": self.success,
|
| 222 |
+
"records_fetched": self.records_fetched,
|
| 223 |
+
"records_stored": self.records_stored,
|
| 224 |
+
"error_type": self.error_type,
|
| 225 |
+
"error_message": self.error_message,
|
| 226 |
+
"http_status_code": self.http_status_code,
|
| 227 |
+
"response_size_bytes": self.response_size_bytes
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
class CollectionSchedule(Base):
|
| 232 |
+
"""
|
| 233 |
+
Collection Schedule - زمانبندی جمعآوری
|
| 234 |
+
تنظیم بازههای جمعآوری داده برای هر منبع
|
| 235 |
+
"""
|
| 236 |
+
__tablename__ = 'collection_schedules'
|
| 237 |
+
|
| 238 |
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
| 239 |
+
source_id = Column(String(100), nullable=False, unique=True, index=True)
|
| 240 |
+
|
| 241 |
+
# Schedule Settings
|
| 242 |
+
collection_interval = Column(String(20), nullable=False, default="30m")
|
| 243 |
+
is_enabled = Column(Boolean, default=True)
|
| 244 |
+
|
| 245 |
+
# Execution Times
|
| 246 |
+
last_run_at = Column(DateTime, nullable=True)
|
| 247 |
+
next_run_at = Column(DateTime, nullable=True)
|
| 248 |
+
|
| 249 |
+
# Statistics
|
| 250 |
+
consecutive_failures = Column(Integer, default=0)
|
| 251 |
+
total_runs = Column(Integer, default=0)
|
| 252 |
+
successful_runs = Column(Integer, default=0)
|
| 253 |
+
|
| 254 |
+
# Backoff Settings
|
| 255 |
+
backoff_until = Column(DateTime, nullable=True) # If in backoff state
|
| 256 |
+
backoff_multiplier = Column(Float, default=1.0)
|
| 257 |
+
|
| 258 |
+
# Timestamps
|
| 259 |
+
created_at = Column(DateTime, default=datetime.utcnow)
|
| 260 |
+
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
| 261 |
+
|
| 262 |
+
def to_dict(self) -> Dict[str, Any]:
|
| 263 |
+
"""تبدیل به دیکشنری"""
|
| 264 |
+
return {
|
| 265 |
+
"id": self.id,
|
| 266 |
+
"source_id": self.source_id,
|
| 267 |
+
"collection_interval": self.collection_interval,
|
| 268 |
+
"is_enabled": self.is_enabled,
|
| 269 |
+
"last_run_at": self.last_run_at.isoformat() if self.last_run_at else None,
|
| 270 |
+
"next_run_at": self.next_run_at.isoformat() if self.next_run_at else None,
|
| 271 |
+
"consecutive_failures": self.consecutive_failures,
|
| 272 |
+
"total_runs": self.total_runs,
|
| 273 |
+
"successful_runs": self.successful_runs,
|
| 274 |
+
"success_rate": (self.successful_runs / self.total_runs * 100) if self.total_runs > 0 else 0,
|
| 275 |
+
"backoff_until": self.backoff_until.isoformat() if self.backoff_until else None,
|
| 276 |
+
"backoff_multiplier": self.backoff_multiplier,
|
| 277 |
+
"created_at": self.created_at.isoformat() if self.created_at else None,
|
| 278 |
+
"updated_at": self.updated_at.isoformat() if self.updated_at else None
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
# ===== DATA SOURCE MANAGER =====
|
| 283 |
+
|
| 284 |
+
class DataSourceManager:
|
| 285 |
+
"""
|
| 286 |
+
مدیریت منابع داده در دیتابیس
|
| 287 |
+
Data Source Manager for database operations
|
| 288 |
+
"""
|
| 289 |
+
|
| 290 |
+
def __init__(self, session):
|
| 291 |
+
self.session = session
|
| 292 |
+
|
| 293 |
+
def create_source(self, source_data: Dict[str, Any]) -> Optional[DataSource]:
|
| 294 |
+
"""ایجاد منبع جدید"""
|
| 295 |
+
try:
|
| 296 |
+
source = DataSource(
|
| 297 |
+
source_id=source_data["source_id"],
|
| 298 |
+
name=source_data["name"],
|
| 299 |
+
source_type=source_data.get("source_type", "market"),
|
| 300 |
+
description=source_data.get("description"),
|
| 301 |
+
base_url=source_data["base_url"],
|
| 302 |
+
api_version=source_data.get("api_version"),
|
| 303 |
+
requires_api_key=source_data.get("requires_api_key", False),
|
| 304 |
+
api_key_env_var=source_data.get("api_key_env_var"),
|
| 305 |
+
rate_limit_description=source_data.get("rate_limit_description"),
|
| 306 |
+
collection_interval=source_data.get("collection_interval", "30m"),
|
| 307 |
+
supports_realtime=source_data.get("supports_realtime", False),
|
| 308 |
+
supported_timeframes=json.dumps(source_data.get("supported_timeframes", [])),
|
| 309 |
+
categories=json.dumps(source_data.get("categories", [])),
|
| 310 |
+
features=json.dumps(source_data.get("features", [])),
|
| 311 |
+
is_active=source_data.get("is_active", True),
|
| 312 |
+
status=source_data.get("status", "active"),
|
| 313 |
+
priority=source_data.get("priority", 5),
|
| 314 |
+
weight=source_data.get("weight", 1),
|
| 315 |
+
is_verified=source_data.get("is_verified", False),
|
| 316 |
+
is_free_tier=source_data.get("is_free_tier", True)
|
| 317 |
+
)
|
| 318 |
+
self.session.add(source)
|
| 319 |
+
self.session.commit()
|
| 320 |
+
return source
|
| 321 |
+
except Exception as e:
|
| 322 |
+
self.session.rollback()
|
| 323 |
+
print(f"Error creating source: {e}")
|
| 324 |
+
return None
|
| 325 |
+
|
| 326 |
+
def get_source(self, source_id: str) -> Optional[DataSource]:
|
| 327 |
+
"""دریافت منبع با شناسه"""
|
| 328 |
+
return self.session.query(DataSource).filter_by(source_id=source_id).first()
|
| 329 |
+
|
| 330 |
+
def get_all_sources(self) -> List[DataSource]:
|
| 331 |
+
"""دریافت همه منابع"""
|
| 332 |
+
return self.session.query(DataSource).all()
|
| 333 |
+
|
| 334 |
+
def get_active_sources(self) -> List[DataSource]:
|
| 335 |
+
"""دریافت منابع فعال"""
|
| 336 |
+
return self.session.query(DataSource).filter_by(is_active=True).all()
|
| 337 |
+
|
| 338 |
+
def get_sources_by_type(self, source_type: str) -> List[DataSource]:
|
| 339 |
+
"""دریافت منابع بر اساس نوع"""
|
| 340 |
+
return self.session.query(DataSource).filter_by(source_type=source_type, is_active=True).all()
|
| 341 |
+
|
| 342 |
+
def update_source_status(self, source_id: str, is_active: bool, status: str = None, status_message: str = None) -> bool:
|
| 343 |
+
"""بهروزرسانی وضعیت منبع"""
|
| 344 |
+
try:
|
| 345 |
+
source = self.get_source(source_id)
|
| 346 |
+
if source:
|
| 347 |
+
source.is_active = is_active
|
| 348 |
+
if status:
|
| 349 |
+
source.status = status
|
| 350 |
+
if status_message:
|
| 351 |
+
source.status_message = status_message
|
| 352 |
+
source.updated_at = datetime.utcnow()
|
| 353 |
+
self.session.commit()
|
| 354 |
+
return True
|
| 355 |
+
return False
|
| 356 |
+
except Exception as e:
|
| 357 |
+
self.session.rollback()
|
| 358 |
+
print(f"Error updating source status: {e}")
|
| 359 |
+
return False
|
| 360 |
+
|
| 361 |
+
def record_request(self, source_id: str, success: bool, response_time_ms: float, error_message: str = None) -> bool:
|
| 362 |
+
"""ثبت درخواست"""
|
| 363 |
+
try:
|
| 364 |
+
source = self.get_source(source_id)
|
| 365 |
+
if source:
|
| 366 |
+
source.total_requests += 1
|
| 367 |
+
if success:
|
| 368 |
+
source.successful_requests += 1
|
| 369 |
+
source.last_success_at = datetime.utcnow()
|
| 370 |
+
else:
|
| 371 |
+
source.failed_requests += 1
|
| 372 |
+
source.last_failure_at = datetime.utcnow()
|
| 373 |
+
if error_message:
|
| 374 |
+
source.last_error_message = error_message
|
| 375 |
+
|
| 376 |
+
# Update average response time
|
| 377 |
+
if source.avg_response_time_ms > 0:
|
| 378 |
+
source.avg_response_time_ms = (source.avg_response_time_ms + response_time_ms) / 2
|
| 379 |
+
else:
|
| 380 |
+
source.avg_response_time_ms = response_time_ms
|
| 381 |
+
|
| 382 |
+
source.last_checked_at = datetime.utcnow()
|
| 383 |
+
self.session.commit()
|
| 384 |
+
return True
|
| 385 |
+
return False
|
| 386 |
+
except Exception as e:
|
| 387 |
+
self.session.rollback()
|
| 388 |
+
print(f"Error recording request: {e}")
|
| 389 |
+
return False
|
| 390 |
+
|
| 391 |
+
def get_sources_for_collection(self, interval: str) -> List[DataSource]:
|
| 392 |
+
"""دریافت منابع برای جمعآوری بر اساس بازه"""
|
| 393 |
+
return self.session.query(DataSource).filter(
|
| 394 |
+
DataSource.is_active == True,
|
| 395 |
+
DataSource.collection_interval == interval,
|
| 396 |
+
DataSource.status != "error"
|
| 397 |
+
).order_by(DataSource.priority).all()
|
| 398 |
+
|
| 399 |
+
def get_statistics(self) -> Dict[str, Any]:
|
| 400 |
+
"""آمار منابع"""
|
| 401 |
+
all_sources = self.get_all_sources()
|
| 402 |
+
active_sources = [s for s in all_sources if s.is_active]
|
| 403 |
+
|
| 404 |
+
total_requests = sum(s.total_requests for s in all_sources)
|
| 405 |
+
successful_requests = sum(s.successful_requests for s in all_sources)
|
| 406 |
+
|
| 407 |
+
return {
|
| 408 |
+
"total_sources": len(all_sources),
|
| 409 |
+
"active_sources": len(active_sources),
|
| 410 |
+
"by_type": {}, # Would need to count by type
|
| 411 |
+
"total_requests": total_requests,
|
| 412 |
+
"successful_requests": successful_requests,
|
| 413 |
+
"success_rate": (successful_requests / total_requests * 100) if total_requests > 0 else 0,
|
| 414 |
+
"sources_with_errors": len([s for s in all_sources if s.status == "error"])
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
|
| 418 |
+
# ===== INITIALIZATION HELPER =====
|
| 419 |
+
|
| 420 |
+
def init_data_sources_from_registry(session, registry):
|
| 421 |
+
"""
|
| 422 |
+
Initialize data sources in database from registry
|
| 423 |
+
پر کردن جدول منابع از رجیستری
|
| 424 |
+
"""
|
| 425 |
+
manager = DataSourceManager(session)
|
| 426 |
+
|
| 427 |
+
for source_id, source_info in registry.to_dict().items():
|
| 428 |
+
existing = manager.get_source(source_id)
|
| 429 |
+
if not existing:
|
| 430 |
+
source_data = {
|
| 431 |
+
"source_id": source_id,
|
| 432 |
+
"name": source_info["name"],
|
| 433 |
+
"source_type": source_info["source_type"],
|
| 434 |
+
"description": source_info.get("description"),
|
| 435 |
+
"base_url": source_info["url"],
|
| 436 |
+
"requires_api_key": source_info.get("requires_api_key", False),
|
| 437 |
+
"api_key_env_var": source_info.get("api_key_env"),
|
| 438 |
+
"rate_limit_description": source_info.get("rate_limit"),
|
| 439 |
+
"collection_interval": "30m", # Default to 30 minutes
|
| 440 |
+
"supports_realtime": "realtime" in source_info.get("supported_timeframes", []),
|
| 441 |
+
"supported_timeframes": source_info.get("supported_timeframes", []),
|
| 442 |
+
"categories": source_info.get("categories", []),
|
| 443 |
+
"features": source_info.get("features", []),
|
| 444 |
+
"is_active": source_info.get("is_active", True),
|
| 445 |
+
"priority": source_info.get("priority", 5),
|
| 446 |
+
"is_verified": source_info.get("verified", False),
|
| 447 |
+
"is_free_tier": source_info.get("free_tier", True)
|
| 448 |
+
}
|
| 449 |
+
manager.create_source(source_data)
|
| 450 |
+
print(f"Created data source: {source_id}")
|
| 451 |
+
else:
|
| 452 |
+
print(f"Data source already exists: {source_id}")
|
| 453 |
+
|
| 454 |
+
return manager
|
| 455 |
+
|
| 456 |
+
|
| 457 |
+
# ===== COLLECTION INTERVALS CONFIGURATION =====
|
| 458 |
+
|
| 459 |
+
# Recommended collection intervals for different data types
|
| 460 |
+
COLLECTION_INTERVALS = {
|
| 461 |
+
# Bulk data - collect every 15-30 minutes
|
| 462 |
+
"market": "15m",
|
| 463 |
+
"historical": "30m",
|
| 464 |
+
"onchain": "30m",
|
| 465 |
+
"defi": "15m",
|
| 466 |
+
|
| 467 |
+
# News - collect every 15-30 minutes
|
| 468 |
+
"news": "15m",
|
| 469 |
+
"social": "30m",
|
| 470 |
+
|
| 471 |
+
# Sentiment - collect every 15 minutes
|
| 472 |
+
"sentiment": "15m",
|
| 473 |
+
|
| 474 |
+
# Technical - collect every 15 minutes
|
| 475 |
+
"technical": "15m",
|
| 476 |
+
|
| 477 |
+
# Aggregated - collect every 15 minutes
|
| 478 |
+
"aggregated": "15m"
|
| 479 |
+
}
|
| 480 |
+
|
| 481 |
+
# Real-time sources - fetch on-demand from client
|
| 482 |
+
REALTIME_SOURCES = [
|
| 483 |
+
"binance_historical",
|
| 484 |
+
"coingecko_historical",
|
| 485 |
+
"coincap_realtime",
|
| 486 |
+
"fear_greed_index"
|
| 487 |
+
]
|
scripts/init_free_resources.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Initialize Free Resources in Database
|
| 4 |
+
این اسکریپت منابع رایگان را از رجیستری به دیتابیس منتقل میکند
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import sys
|
| 8 |
+
import os
|
| 9 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 10 |
+
|
| 11 |
+
from sqlalchemy import create_engine
|
| 12 |
+
from sqlalchemy.orm import sessionmaker
|
| 13 |
+
import json
|
| 14 |
+
from datetime import datetime
|
| 15 |
+
|
| 16 |
+
# Import models and registries
|
| 17 |
+
from database.data_sources_model import Base, DataSource, DataSourceManager, COLLECTION_INTERVALS
|
| 18 |
+
from backend.providers.free_resources import get_free_resources_registry, ResourceType
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def init_database(db_url: str = "sqlite:///data/crypto_data.db"):
|
| 22 |
+
"""Initialize database connection"""
|
| 23 |
+
engine = create_engine(db_url)
|
| 24 |
+
Base.metadata.create_all(engine)
|
| 25 |
+
Session = sessionmaker(bind=engine)
|
| 26 |
+
return Session()
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def populate_from_free_resources(session):
|
| 30 |
+
"""Populate database from FreeResourcesRegistry"""
|
| 31 |
+
registry = get_free_resources_registry()
|
| 32 |
+
manager = DataSourceManager(session)
|
| 33 |
+
|
| 34 |
+
created = 0
|
| 35 |
+
updated = 0
|
| 36 |
+
skipped = 0
|
| 37 |
+
|
| 38 |
+
for resource_id, resource in registry.resources.items():
|
| 39 |
+
existing = manager.get_source(resource_id)
|
| 40 |
+
|
| 41 |
+
# Map ResourceType to collection interval
|
| 42 |
+
type_to_interval = {
|
| 43 |
+
ResourceType.MARKET_DATA: "15m",
|
| 44 |
+
ResourceType.NEWS: "15m",
|
| 45 |
+
ResourceType.SENTIMENT: "15m",
|
| 46 |
+
ResourceType.BLOCKCHAIN: "30m",
|
| 47 |
+
ResourceType.ONCHAIN: "30m",
|
| 48 |
+
ResourceType.DEFI: "15m",
|
| 49 |
+
ResourceType.WHALE_TRACKING: "30m",
|
| 50 |
+
ResourceType.TECHNICAL: "15m",
|
| 51 |
+
ResourceType.AI_MODEL: "30m",
|
| 52 |
+
ResourceType.SOCIAL: "30m",
|
| 53 |
+
ResourceType.HISTORICAL: "30m",
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
source_type_str = resource.resource_type.value
|
| 57 |
+
collection_interval = type_to_interval.get(resource.resource_type, "30m")
|
| 58 |
+
|
| 59 |
+
# Check if it supports real-time
|
| 60 |
+
supports_realtime = "realtime" in resource.supported_timeframes or resource_id in [
|
| 61 |
+
"binance", "coincap", "coingecko", "fear_greed_index"
|
| 62 |
+
]
|
| 63 |
+
|
| 64 |
+
source_data = {
|
| 65 |
+
"source_id": resource.id,
|
| 66 |
+
"name": resource.name,
|
| 67 |
+
"source_type": source_type_str,
|
| 68 |
+
"description": resource.description,
|
| 69 |
+
"base_url": resource.base_url,
|
| 70 |
+
"requires_api_key": resource.requires_auth,
|
| 71 |
+
"api_key_env_var": resource.api_key_env if resource.api_key_env else None,
|
| 72 |
+
"rate_limit_description": resource.rate_limit,
|
| 73 |
+
"collection_interval": collection_interval,
|
| 74 |
+
"supports_realtime": supports_realtime,
|
| 75 |
+
"supported_timeframes": resource.supported_timeframes,
|
| 76 |
+
"categories": [],
|
| 77 |
+
"features": resource.features,
|
| 78 |
+
"is_active": resource.is_active,
|
| 79 |
+
"priority": resource.priority,
|
| 80 |
+
"is_verified": False,
|
| 81 |
+
"is_free_tier": resource.is_free,
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
if not existing:
|
| 85 |
+
result = manager.create_source(source_data)
|
| 86 |
+
if result:
|
| 87 |
+
created += 1
|
| 88 |
+
print(f"✅ Created: {resource.name}")
|
| 89 |
+
else:
|
| 90 |
+
print(f"❌ Failed to create: {resource.name}")
|
| 91 |
+
else:
|
| 92 |
+
skipped += 1
|
| 93 |
+
print(f"⏭️ Skipped (exists): {resource.name}")
|
| 94 |
+
|
| 95 |
+
return {
|
| 96 |
+
"created": created,
|
| 97 |
+
"updated": updated,
|
| 98 |
+
"skipped": skipped,
|
| 99 |
+
"total": created + updated + skipped
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def print_statistics(session):
|
| 104 |
+
"""Print database statistics"""
|
| 105 |
+
manager = DataSourceManager(session)
|
| 106 |
+
stats = manager.get_statistics()
|
| 107 |
+
|
| 108 |
+
print("\n" + "=" * 60)
|
| 109 |
+
print("📊 DATABASE STATISTICS")
|
| 110 |
+
print("=" * 60)
|
| 111 |
+
print(f"Total Sources: {stats['total_sources']}")
|
| 112 |
+
print(f"Active Sources: {stats['active_sources']}")
|
| 113 |
+
print(f"Total Requests: {stats['total_requests']}")
|
| 114 |
+
print(f"Success Rate: {stats['success_rate']:.2f}%")
|
| 115 |
+
print(f"Sources w/ Errors: {stats['sources_with_errors']}")
|
| 116 |
+
|
| 117 |
+
# Count by type
|
| 118 |
+
all_sources = manager.get_all_sources()
|
| 119 |
+
type_counts = {}
|
| 120 |
+
for source in all_sources:
|
| 121 |
+
stype = source.source_type
|
| 122 |
+
type_counts[stype] = type_counts.get(stype, 0) + 1
|
| 123 |
+
|
| 124 |
+
print("\nBy Type:")
|
| 125 |
+
for stype, count in sorted(type_counts.items()):
|
| 126 |
+
print(f" • {stype}: {count}")
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def main():
|
| 130 |
+
print("=" * 60)
|
| 131 |
+
print("🚀 INITIALIZING FREE RESOURCES IN DATABASE")
|
| 132 |
+
print("=" * 60)
|
| 133 |
+
|
| 134 |
+
# Ensure data directory exists
|
| 135 |
+
os.makedirs("data", exist_ok=True)
|
| 136 |
+
|
| 137 |
+
# Initialize database
|
| 138 |
+
db_path = "data/crypto_data.db"
|
| 139 |
+
db_url = f"sqlite:///{db_path}"
|
| 140 |
+
|
| 141 |
+
print(f"\n📁 Database: {db_path}")
|
| 142 |
+
|
| 143 |
+
session = init_database(db_url)
|
| 144 |
+
|
| 145 |
+
# Populate from free resources registry
|
| 146 |
+
print("\n📥 Populating from FreeResourcesRegistry...")
|
| 147 |
+
result = populate_from_free_resources(session)
|
| 148 |
+
|
| 149 |
+
print(f"\n✅ Complete!")
|
| 150 |
+
print(f" Created: {result['created']}")
|
| 151 |
+
print(f" Skipped: {result['skipped']}")
|
| 152 |
+
print(f" Total: {result['total']}")
|
| 153 |
+
|
| 154 |
+
# Print statistics
|
| 155 |
+
print_statistics(session)
|
| 156 |
+
|
| 157 |
+
session.close()
|
| 158 |
+
print("\n" + "=" * 60)
|
| 159 |
+
print("✅ Database initialization complete!")
|
| 160 |
+
print("=" * 60)
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
if __name__ == "__main__":
|
| 164 |
+
main()
|
static/data/services.json
CHANGED
|
@@ -1,360 +1,585 @@
|
|
| 1 |
{
|
| 2 |
-
"
|
| 3 |
-
{
|
| 4 |
-
"name": "
|
| 5 |
-
"
|
| 6 |
-
"
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
"
|
| 11 |
-
"
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
"
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
"
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
"
|
| 25 |
-
"
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
"
|
| 30 |
-
"
|
| 31 |
-
"
|
| 32 |
-
},
|
| 33 |
-
{
|
| 34 |
-
"name": "
|
| 35 |
-
"
|
| 36 |
-
"
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
"
|
| 41 |
-
"
|
| 42 |
-
"key": "",
|
| 43 |
-
"endpoints": ["/wallet/getaccount"]
|
| 44 |
-
},
|
| 45 |
-
{
|
| 46 |
-
"name": "Ankr",
|
| 47 |
-
"url": "https://rpc.ankr.com/multichain",
|
| 48 |
-
"key": "",
|
| 49 |
-
"endpoints": []
|
| 50 |
-
},
|
| 51 |
-
{
|
| 52 |
-
"name": "1inch BSC",
|
| 53 |
-
"url": "https://api.1inch.io/v5.0/56",
|
| 54 |
-
"key": "",
|
| 55 |
-
"endpoints": []
|
| 56 |
}
|
| 57 |
-
|
| 58 |
-
"
|
| 59 |
-
{
|
| 60 |
-
"
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
"
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
"
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
}
|
| 211 |
-
|
| 212 |
-
"
|
| 213 |
-
{
|
| 214 |
-
"
|
| 215 |
-
"
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
"
|
| 228 |
-
"
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
"
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
},
|
| 243 |
-
{
|
| 244 |
-
"
|
| 245 |
-
"
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
"
|
| 253 |
-
"endpoints": [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 254 |
}
|
| 255 |
-
|
| 256 |
-
"
|
| 257 |
-
{
|
| 258 |
-
"
|
| 259 |
-
"
|
| 260 |
-
"
|
| 261 |
-
"
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
"
|
| 267 |
-
"
|
| 268 |
-
|
| 269 |
-
{
|
| 270 |
-
"name": "DeBank",
|
| 271 |
-
"url": "https://api.debank.com",
|
| 272 |
-
"key": "",
|
| 273 |
-
"endpoints": []
|
| 274 |
-
},
|
| 275 |
-
{
|
| 276 |
-
"name": "Zerion",
|
| 277 |
-
"url": "https://api.zerion.io",
|
| 278 |
-
"key": "",
|
| 279 |
-
"endpoints": []
|
| 280 |
-
},
|
| 281 |
-
{
|
| 282 |
-
"name": "WhaleMap",
|
| 283 |
-
"url": "https://whalemap.io",
|
| 284 |
-
"key": "",
|
| 285 |
-
"endpoints": []
|
| 286 |
-
},
|
| 287 |
-
{
|
| 288 |
-
"name": "The Graph",
|
| 289 |
-
"url": "https://api.thegraph.com/subgraphs",
|
| 290 |
-
"key": "",
|
| 291 |
-
"endpoints": []
|
| 292 |
-
},
|
| 293 |
-
{
|
| 294 |
-
"name": "Glassnode",
|
| 295 |
-
"url": "https://api.glassnode.com/v1",
|
| 296 |
-
"key": "",
|
| 297 |
-
"endpoints": []
|
| 298 |
-
},
|
| 299 |
-
{
|
| 300 |
-
"name": "IntoTheBlock",
|
| 301 |
-
"url": "https://api.intotheblock.com/v1",
|
| 302 |
-
"key": "",
|
| 303 |
-
"endpoints": []
|
| 304 |
-
},
|
| 305 |
-
{
|
| 306 |
-
"name": "Dune",
|
| 307 |
-
"url": "https://api.dune.com/api/v1",
|
| 308 |
-
"key": "",
|
| 309 |
-
"endpoints": []
|
| 310 |
-
},
|
| 311 |
-
{
|
| 312 |
-
"name": "Covalent",
|
| 313 |
-
"url": "https://api.covalenthq.com/v1",
|
| 314 |
-
"key": "",
|
| 315 |
-
"endpoints": ["/1/address/{address}/balances_v2/"]
|
| 316 |
-
},
|
| 317 |
-
{
|
| 318 |
-
"name": "Moralis",
|
| 319 |
-
"url": "https://deep-index.moralis.io/api/v2",
|
| 320 |
-
"key": "",
|
| 321 |
-
"endpoints": []
|
| 322 |
-
},
|
| 323 |
-
{
|
| 324 |
-
"name": "Transpose",
|
| 325 |
-
"url": "https://api.transpose.io",
|
| 326 |
-
"key": "",
|
| 327 |
-
"endpoints": []
|
| 328 |
-
},
|
| 329 |
-
{
|
| 330 |
-
"name": "Footprint",
|
| 331 |
-
"url": "https://api.footprint.network",
|
| 332 |
-
"key": "",
|
| 333 |
-
"endpoints": []
|
| 334 |
-
},
|
| 335 |
-
{
|
| 336 |
-
"name": "Bitquery",
|
| 337 |
-
"url": "https://graphql.bitquery.io",
|
| 338 |
-
"key": "",
|
| 339 |
-
"endpoints": []
|
| 340 |
-
},
|
| 341 |
-
{
|
| 342 |
-
"name": "Arkham",
|
| 343 |
-
"url": "https://api.arkham.com",
|
| 344 |
-
"key": "",
|
| 345 |
-
"endpoints": []
|
| 346 |
-
},
|
| 347 |
-
{
|
| 348 |
-
"name": "Clank",
|
| 349 |
-
"url": "https://clankapp.com/api",
|
| 350 |
-
"key": "",
|
| 351 |
-
"endpoints": []
|
| 352 |
-
},
|
| 353 |
-
{
|
| 354 |
-
"name": "Hugging Face",
|
| 355 |
-
"url": "https://api-inference.huggingface.co/models",
|
| 356 |
-
"key": "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",
|
| 357 |
-
"endpoints": ["/ElKulako/cryptobert"]
|
| 358 |
}
|
| 359 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 360 |
}
|
|
|
|
| 1 |
{
|
| 2 |
+
"categories": {
|
| 3 |
+
"market_data": {
|
| 4 |
+
"name": "Market Data",
|
| 5 |
+
"description": "Real-time and historical cryptocurrency market data",
|
| 6 |
+
"icon": "📊"
|
| 7 |
+
},
|
| 8 |
+
"news": {
|
| 9 |
+
"name": "News & Media",
|
| 10 |
+
"description": "Crypto news from multiple sources",
|
| 11 |
+
"icon": "📰"
|
| 12 |
+
},
|
| 13 |
+
"sentiment": {
|
| 14 |
+
"name": "Sentiment Analysis",
|
| 15 |
+
"description": "Market sentiment and Fear & Greed Index",
|
| 16 |
+
"icon": "🎭"
|
| 17 |
+
},
|
| 18 |
+
"analytics": {
|
| 19 |
+
"name": "On-Chain Analytics",
|
| 20 |
+
"description": "Blockchain data and whale tracking",
|
| 21 |
+
"icon": "🔗"
|
| 22 |
+
},
|
| 23 |
+
"defi": {
|
| 24 |
+
"name": "DeFi Data",
|
| 25 |
+
"description": "DeFi protocols, TVL, and yields",
|
| 26 |
+
"icon": "🏦"
|
| 27 |
+
},
|
| 28 |
+
"technical": {
|
| 29 |
+
"name": "Technical Analysis",
|
| 30 |
+
"description": "Technical indicators and trading signals",
|
| 31 |
+
"icon": "📈"
|
| 32 |
+
},
|
| 33 |
+
"ai_models": {
|
| 34 |
+
"name": "AI & ML Models",
|
| 35 |
+
"description": "AI-powered analysis and predictions",
|
| 36 |
+
"icon": "🤖"
|
| 37 |
+
},
|
| 38 |
+
"explorers": {
|
| 39 |
+
"name": "Block Explorers",
|
| 40 |
+
"description": "Blockchain explorer APIs",
|
| 41 |
+
"icon": "🔍"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
}
|
| 43 |
+
},
|
| 44 |
+
"services": {
|
| 45 |
+
"market_data": {
|
| 46 |
+
"providers": [
|
| 47 |
+
{
|
| 48 |
+
"id": "coingecko",
|
| 49 |
+
"name": "CoinGecko",
|
| 50 |
+
"url": "https://api.coingecko.com/api/v3",
|
| 51 |
+
"free": true,
|
| 52 |
+
"requires_key": false,
|
| 53 |
+
"rate_limit": "10-50 req/min",
|
| 54 |
+
"endpoints": ["/simple/price", "/coins/markets", "/coins/{id}/market_chart"],
|
| 55 |
+
"features": ["prices", "market_cap", "volume", "historical", "ohlcv"],
|
| 56 |
+
"active": true
|
| 57 |
+
},
|
| 58 |
+
{
|
| 59 |
+
"id": "coinmarketcap",
|
| 60 |
+
"name": "CoinMarketCap",
|
| 61 |
+
"url": "https://pro-api.coinmarketcap.com/v1",
|
| 62 |
+
"free": true,
|
| 63 |
+
"requires_key": true,
|
| 64 |
+
"key_env": "COINMARKETCAP_KEY_1",
|
| 65 |
+
"rate_limit": "333 req/day",
|
| 66 |
+
"endpoints": ["/cryptocurrency/quotes/latest", "/cryptocurrency/listings/latest"],
|
| 67 |
+
"features": ["prices", "market_cap", "rankings"],
|
| 68 |
+
"active": true
|
| 69 |
+
},
|
| 70 |
+
{
|
| 71 |
+
"id": "binance",
|
| 72 |
+
"name": "Binance",
|
| 73 |
+
"url": "https://api.binance.com/api/v3",
|
| 74 |
+
"free": true,
|
| 75 |
+
"requires_key": false,
|
| 76 |
+
"rate_limit": "1200 req/min",
|
| 77 |
+
"endpoints": ["/ticker/price", "/klines", "/ticker/24hr"],
|
| 78 |
+
"features": ["prices", "ohlcv", "trades", "depth"],
|
| 79 |
+
"active": true
|
| 80 |
+
},
|
| 81 |
+
{
|
| 82 |
+
"id": "cryptocompare",
|
| 83 |
+
"name": "CryptoCompare",
|
| 84 |
+
"url": "https://min-api.cryptocompare.com/data",
|
| 85 |
+
"free": true,
|
| 86 |
+
"requires_key": false,
|
| 87 |
+
"rate_limit": "100K/month",
|
| 88 |
+
"endpoints": ["/pricemulti", "/histoday", "/histohour"],
|
| 89 |
+
"features": ["prices", "historical", "social"],
|
| 90 |
+
"active": true
|
| 91 |
+
},
|
| 92 |
+
{
|
| 93 |
+
"id": "coincap",
|
| 94 |
+
"name": "CoinCap",
|
| 95 |
+
"url": "https://api.coincap.io/v2",
|
| 96 |
+
"free": true,
|
| 97 |
+
"requires_key": false,
|
| 98 |
+
"rate_limit": "200 req/min",
|
| 99 |
+
"endpoints": ["/assets", "/rates", "/candles"],
|
| 100 |
+
"features": ["prices", "history", "exchanges"],
|
| 101 |
+
"active": true
|
| 102 |
+
},
|
| 103 |
+
{
|
| 104 |
+
"id": "coinpaprika",
|
| 105 |
+
"name": "CoinPaprika",
|
| 106 |
+
"url": "https://api.coinpaprika.com/v1",
|
| 107 |
+
"free": true,
|
| 108 |
+
"requires_key": false,
|
| 109 |
+
"rate_limit": "unlimited",
|
| 110 |
+
"endpoints": ["/tickers", "/coins", "/ohlcv"],
|
| 111 |
+
"features": ["prices", "ohlcv", "exchanges"],
|
| 112 |
+
"active": true
|
| 113 |
+
},
|
| 114 |
+
{
|
| 115 |
+
"id": "messari",
|
| 116 |
+
"name": "Messari",
|
| 117 |
+
"url": "https://data.messari.io/api/v1",
|
| 118 |
+
"free": true,
|
| 119 |
+
"requires_key": false,
|
| 120 |
+
"rate_limit": "20 req/min",
|
| 121 |
+
"endpoints": ["/assets", "/assets/{symbol}/metrics"],
|
| 122 |
+
"features": ["prices", "metrics", "profiles"],
|
| 123 |
+
"active": true
|
| 124 |
+
},
|
| 125 |
+
{
|
| 126 |
+
"id": "coinlore",
|
| 127 |
+
"name": "CoinLore",
|
| 128 |
+
"url": "https://api.coinlore.net/api",
|
| 129 |
+
"free": true,
|
| 130 |
+
"requires_key": false,
|
| 131 |
+
"rate_limit": "unlimited",
|
| 132 |
+
"endpoints": ["/tickers/", "/global/"],
|
| 133 |
+
"features": ["prices", "global_stats"],
|
| 134 |
+
"active": true
|
| 135 |
+
}
|
| 136 |
+
],
|
| 137 |
+
"collection_interval": "15m",
|
| 138 |
+
"realtime_supported": true
|
| 139 |
+
},
|
| 140 |
+
"news": {
|
| 141 |
+
"providers": [
|
| 142 |
+
{
|
| 143 |
+
"id": "cryptocompare_news",
|
| 144 |
+
"name": "CryptoCompare News",
|
| 145 |
+
"url": "https://min-api.cryptocompare.com/data/v2/news/",
|
| 146 |
+
"free": true,
|
| 147 |
+
"requires_key": false,
|
| 148 |
+
"rate_limit": "100K/month",
|
| 149 |
+
"endpoints": ["?lang=EN", "?categories=BTC"],
|
| 150 |
+
"features": ["news", "categories", "sources"],
|
| 151 |
+
"active": true
|
| 152 |
+
},
|
| 153 |
+
{
|
| 154 |
+
"id": "newsapi",
|
| 155 |
+
"name": "NewsAPI",
|
| 156 |
+
"url": "https://newsapi.org/v2",
|
| 157 |
+
"free": true,
|
| 158 |
+
"requires_key": true,
|
| 159 |
+
"key_env": "NEWSAPI_KEY",
|
| 160 |
+
"rate_limit": "100 req/day",
|
| 161 |
+
"endpoints": ["/everything", "/top-headlines"],
|
| 162 |
+
"features": ["news", "search", "sources"],
|
| 163 |
+
"active": true
|
| 164 |
+
},
|
| 165 |
+
{
|
| 166 |
+
"id": "cryptopanic",
|
| 167 |
+
"name": "CryptoPanic",
|
| 168 |
+
"url": "https://cryptopanic.com/api/v1/posts/",
|
| 169 |
+
"free": true,
|
| 170 |
+
"requires_key": true,
|
| 171 |
+
"key_env": "CRYPTOPANIC_KEY",
|
| 172 |
+
"rate_limit": "500 req/day",
|
| 173 |
+
"endpoints": ["?auth_token={key}", "?filter=hot"],
|
| 174 |
+
"features": ["news", "sentiment_votes", "trending"],
|
| 175 |
+
"active": true
|
| 176 |
+
},
|
| 177 |
+
{
|
| 178 |
+
"id": "bitcoin_magazine_rss",
|
| 179 |
+
"name": "Bitcoin Magazine",
|
| 180 |
+
"url": "https://bitcoinmagazine.com/feed",
|
| 181 |
+
"free": true,
|
| 182 |
+
"requires_key": false,
|
| 183 |
+
"rate_limit": "unlimited",
|
| 184 |
+
"endpoints": [],
|
| 185 |
+
"features": ["rss", "articles"],
|
| 186 |
+
"active": true
|
| 187 |
+
},
|
| 188 |
+
{
|
| 189 |
+
"id": "decrypt_rss",
|
| 190 |
+
"name": "Decrypt",
|
| 191 |
+
"url": "https://decrypt.co/feed",
|
| 192 |
+
"free": true,
|
| 193 |
+
"requires_key": false,
|
| 194 |
+
"rate_limit": "unlimited",
|
| 195 |
+
"endpoints": [],
|
| 196 |
+
"features": ["rss", "articles", "web3"],
|
| 197 |
+
"active": true
|
| 198 |
+
},
|
| 199 |
+
{
|
| 200 |
+
"id": "cryptoslate_rss",
|
| 201 |
+
"name": "CryptoSlate",
|
| 202 |
+
"url": "https://cryptoslate.com/feed/",
|
| 203 |
+
"free": true,
|
| 204 |
+
"requires_key": false,
|
| 205 |
+
"rate_limit": "unlimited",
|
| 206 |
+
"endpoints": [],
|
| 207 |
+
"features": ["rss", "articles", "analysis"],
|
| 208 |
+
"active": true
|
| 209 |
+
},
|
| 210 |
+
{
|
| 211 |
+
"id": "cointelegraph_rss",
|
| 212 |
+
"name": "CoinTelegraph",
|
| 213 |
+
"url": "https://cointelegraph.com/rss",
|
| 214 |
+
"free": true,
|
| 215 |
+
"requires_key": false,
|
| 216 |
+
"rate_limit": "unlimited",
|
| 217 |
+
"endpoints": [],
|
| 218 |
+
"features": ["rss", "articles"],
|
| 219 |
+
"active": true
|
| 220 |
+
},
|
| 221 |
+
{
|
| 222 |
+
"id": "coindesk_rss",
|
| 223 |
+
"name": "CoinDesk",
|
| 224 |
+
"url": "https://www.coindesk.com/arc/outboundfeeds/rss/",
|
| 225 |
+
"free": true,
|
| 226 |
+
"requires_key": false,
|
| 227 |
+
"rate_limit": "unlimited",
|
| 228 |
+
"endpoints": [],
|
| 229 |
+
"features": ["rss", "articles"],
|
| 230 |
+
"active": true
|
| 231 |
+
},
|
| 232 |
+
{
|
| 233 |
+
"id": "theblock_rss",
|
| 234 |
+
"name": "The Block",
|
| 235 |
+
"url": "https://www.theblock.co/rss.xml",
|
| 236 |
+
"free": true,
|
| 237 |
+
"requires_key": false,
|
| 238 |
+
"rate_limit": "unlimited",
|
| 239 |
+
"endpoints": [],
|
| 240 |
+
"features": ["rss", "research"],
|
| 241 |
+
"active": true
|
| 242 |
+
}
|
| 243 |
+
],
|
| 244 |
+
"collection_interval": "15m",
|
| 245 |
+
"realtime_supported": false
|
| 246 |
+
},
|
| 247 |
+
"sentiment": {
|
| 248 |
+
"providers": [
|
| 249 |
+
{
|
| 250 |
+
"id": "fear_greed_index",
|
| 251 |
+
"name": "Fear & Greed Index",
|
| 252 |
+
"url": "https://api.alternative.me/fng/",
|
| 253 |
+
"free": true,
|
| 254 |
+
"requires_key": false,
|
| 255 |
+
"rate_limit": "unlimited",
|
| 256 |
+
"endpoints": ["?limit=1", "?limit=30"],
|
| 257 |
+
"features": ["fear_greed", "historical"],
|
| 258 |
+
"active": true
|
| 259 |
+
},
|
| 260 |
+
{
|
| 261 |
+
"id": "lunarcrush",
|
| 262 |
+
"name": "LunarCrush",
|
| 263 |
+
"url": "https://lunarcrush.com/api",
|
| 264 |
+
"free": true,
|
| 265 |
+
"requires_key": true,
|
| 266 |
+
"key_env": "LUNARCRUSH_KEY",
|
| 267 |
+
"rate_limit": "50 req/day",
|
| 268 |
+
"endpoints": [],
|
| 269 |
+
"features": ["social_volume", "sentiment", "influencers"],
|
| 270 |
+
"active": true
|
| 271 |
+
},
|
| 272 |
+
{
|
| 273 |
+
"id": "santiment",
|
| 274 |
+
"name": "Santiment",
|
| 275 |
+
"url": "https://api.santiment.net/graphql",
|
| 276 |
+
"free": true,
|
| 277 |
+
"requires_key": true,
|
| 278 |
+
"key_env": "SANTIMENT_KEY",
|
| 279 |
+
"rate_limit": "varies",
|
| 280 |
+
"endpoints": [],
|
| 281 |
+
"features": ["onchain", "social", "development"],
|
| 282 |
+
"active": true
|
| 283 |
+
},
|
| 284 |
+
{
|
| 285 |
+
"id": "augmento",
|
| 286 |
+
"name": "Augmento",
|
| 287 |
+
"url": "https://api.augmento.ai/v0.1",
|
| 288 |
+
"free": true,
|
| 289 |
+
"requires_key": false,
|
| 290 |
+
"rate_limit": "100 req/day",
|
| 291 |
+
"endpoints": [],
|
| 292 |
+
"features": ["sentiment_topics", "social_trends"],
|
| 293 |
+
"active": true
|
| 294 |
+
}
|
| 295 |
+
],
|
| 296 |
+
"collection_interval": "15m",
|
| 297 |
+
"realtime_supported": true
|
| 298 |
+
},
|
| 299 |
+
"analytics": {
|
| 300 |
+
"providers": [
|
| 301 |
+
{
|
| 302 |
+
"id": "whale_alert",
|
| 303 |
+
"name": "Whale Alert",
|
| 304 |
+
"url": "https://api.whale-alert.io/v1",
|
| 305 |
+
"free": true,
|
| 306 |
+
"requires_key": true,
|
| 307 |
+
"key_env": "WHALE_ALERT_KEY",
|
| 308 |
+
"rate_limit": "10 req/min",
|
| 309 |
+
"endpoints": ["/transactions"],
|
| 310 |
+
"features": ["whale_transactions", "alerts"],
|
| 311 |
+
"active": true
|
| 312 |
+
},
|
| 313 |
+
{
|
| 314 |
+
"id": "blockchair",
|
| 315 |
+
"name": "Blockchair",
|
| 316 |
+
"url": "https://api.blockchair.com",
|
| 317 |
+
"free": true,
|
| 318 |
+
"requires_key": false,
|
| 319 |
+
"rate_limit": "30 req/min",
|
| 320 |
+
"endpoints": ["/bitcoin/stats", "/ethereum/stats"],
|
| 321 |
+
"features": ["blockchain_stats", "addresses"],
|
| 322 |
+
"active": true
|
| 323 |
+
},
|
| 324 |
+
{
|
| 325 |
+
"id": "glassnode",
|
| 326 |
+
"name": "Glassnode",
|
| 327 |
+
"url": "https://api.glassnode.com/v1/metrics",
|
| 328 |
+
"free": true,
|
| 329 |
+
"requires_key": true,
|
| 330 |
+
"key_env": "GLASSNODE_KEY",
|
| 331 |
+
"rate_limit": "varies",
|
| 332 |
+
"endpoints": [],
|
| 333 |
+
"features": ["onchain_metrics", "sopr", "nupl"],
|
| 334 |
+
"active": true
|
| 335 |
+
},
|
| 336 |
+
{
|
| 337 |
+
"id": "cryptoquant",
|
| 338 |
+
"name": "CryptoQuant",
|
| 339 |
+
"url": "https://api.cryptoquant.com/v1",
|
| 340 |
+
"free": true,
|
| 341 |
+
"requires_key": true,
|
| 342 |
+
"key_env": "CRYPTOQUANT_KEY",
|
| 343 |
+
"rate_limit": "100 req/day",
|
| 344 |
+
"endpoints": [],
|
| 345 |
+
"features": ["exchange_flows", "miner_data"],
|
| 346 |
+
"active": true
|
| 347 |
+
}
|
| 348 |
+
],
|
| 349 |
+
"collection_interval": "30m",
|
| 350 |
+
"realtime_supported": false
|
| 351 |
+
},
|
| 352 |
+
"defi": {
|
| 353 |
+
"providers": [
|
| 354 |
+
{
|
| 355 |
+
"id": "defillama",
|
| 356 |
+
"name": "DefiLlama",
|
| 357 |
+
"url": "https://api.llama.fi",
|
| 358 |
+
"free": true,
|
| 359 |
+
"requires_key": false,
|
| 360 |
+
"rate_limit": "300 req/min",
|
| 361 |
+
"endpoints": ["/protocols", "/tvl", "/chains", "/yields"],
|
| 362 |
+
"features": ["tvl", "protocols", "yields", "stablecoins"],
|
| 363 |
+
"active": true
|
| 364 |
+
},
|
| 365 |
+
{
|
| 366 |
+
"id": "1inch",
|
| 367 |
+
"name": "1inch",
|
| 368 |
+
"url": "https://api.1inch.io/v4.0",
|
| 369 |
+
"free": true,
|
| 370 |
+
"requires_key": false,
|
| 371 |
+
"rate_limit": "varies",
|
| 372 |
+
"endpoints": ["/1/quote", "/1/swap"],
|
| 373 |
+
"features": ["dex_aggregator", "quotes", "swap"],
|
| 374 |
+
"active": true
|
| 375 |
+
},
|
| 376 |
+
{
|
| 377 |
+
"id": "uniswap_subgraph",
|
| 378 |
+
"name": "Uniswap Subgraph",
|
| 379 |
+
"url": "https://api.thegraph.com/subgraphs/name/uniswap",
|
| 380 |
+
"free": true,
|
| 381 |
+
"requires_key": false,
|
| 382 |
+
"rate_limit": "varies",
|
| 383 |
+
"endpoints": [],
|
| 384 |
+
"features": ["pairs", "swaps", "liquidity"],
|
| 385 |
+
"active": true
|
| 386 |
+
}
|
| 387 |
+
],
|
| 388 |
+
"collection_interval": "15m",
|
| 389 |
+
"realtime_supported": false
|
| 390 |
+
},
|
| 391 |
+
"technical": {
|
| 392 |
+
"providers": [
|
| 393 |
+
{
|
| 394 |
+
"id": "taapi",
|
| 395 |
+
"name": "TAAPI.IO",
|
| 396 |
+
"url": "https://api.taapi.io",
|
| 397 |
+
"free": true,
|
| 398 |
+
"requires_key": true,
|
| 399 |
+
"key_env": "TAAPI_KEY",
|
| 400 |
+
"rate_limit": "50 req/day",
|
| 401 |
+
"endpoints": ["/rsi", "/macd", "/ema"],
|
| 402 |
+
"features": ["indicators", "rsi", "macd", "bollinger"],
|
| 403 |
+
"active": true
|
| 404 |
+
}
|
| 405 |
+
],
|
| 406 |
+
"collection_interval": "15m",
|
| 407 |
+
"realtime_supported": true
|
| 408 |
+
},
|
| 409 |
+
"ai_models": {
|
| 410 |
+
"providers": [
|
| 411 |
+
{
|
| 412 |
+
"id": "huggingface",
|
| 413 |
+
"name": "HuggingFace Inference",
|
| 414 |
+
"url": "https://api-inference.huggingface.co/models",
|
| 415 |
+
"free": true,
|
| 416 |
+
"requires_key": true,
|
| 417 |
+
"key_env": "HF_TOKEN",
|
| 418 |
+
"rate_limit": "varies",
|
| 419 |
+
"endpoints": ["/ElKulako/cryptobert", "/ProsusAI/finbert"],
|
| 420 |
+
"features": ["sentiment_analysis", "text_generation", "classification"],
|
| 421 |
+
"active": true
|
| 422 |
+
}
|
| 423 |
+
],
|
| 424 |
+
"collection_interval": "on_demand",
|
| 425 |
+
"realtime_supported": true
|
| 426 |
+
},
|
| 427 |
+
"explorers": {
|
| 428 |
+
"providers": [
|
| 429 |
+
{
|
| 430 |
+
"id": "etherscan",
|
| 431 |
+
"name": "Etherscan",
|
| 432 |
+
"url": "https://api.etherscan.io/api",
|
| 433 |
+
"free": true,
|
| 434 |
+
"requires_key": true,
|
| 435 |
+
"key_env": "ETHERSCAN_KEY",
|
| 436 |
+
"rate_limit": "5 req/sec",
|
| 437 |
+
"endpoints": ["?module=account&action=balance", "?module=gastracker"],
|
| 438 |
+
"features": ["balances", "transactions", "gas"],
|
| 439 |
+
"active": true
|
| 440 |
+
},
|
| 441 |
+
{
|
| 442 |
+
"id": "bscscan",
|
| 443 |
+
"name": "BscScan",
|
| 444 |
+
"url": "https://api.bscscan.com/api",
|
| 445 |
+
"free": true,
|
| 446 |
+
"requires_key": true,
|
| 447 |
+
"key_env": "BSCSCAN_KEY",
|
| 448 |
+
"rate_limit": "5 req/sec",
|
| 449 |
+
"endpoints": ["?module=account&action=balance"],
|
| 450 |
+
"features": ["balances", "transactions"],
|
| 451 |
+
"active": true
|
| 452 |
+
},
|
| 453 |
+
{
|
| 454 |
+
"id": "tronscan",
|
| 455 |
+
"name": "TronScan",
|
| 456 |
+
"url": "https://apilist.tronscanapi.com/api",
|
| 457 |
+
"free": true,
|
| 458 |
+
"requires_key": true,
|
| 459 |
+
"key_env": "TRONSCAN_KEY",
|
| 460 |
+
"rate_limit": "varies",
|
| 461 |
+
"endpoints": ["/account"],
|
| 462 |
+
"features": ["balances", "transactions"],
|
| 463 |
+
"active": true
|
| 464 |
+
},
|
| 465 |
+
{
|
| 466 |
+
"id": "ethplorer",
|
| 467 |
+
"name": "Ethplorer",
|
| 468 |
+
"url": "https://api.ethplorer.io",
|
| 469 |
+
"free": true,
|
| 470 |
+
"requires_key": false,
|
| 471 |
+
"rate_limit": "varies",
|
| 472 |
+
"endpoints": ["/getAddressInfo"],
|
| 473 |
+
"features": ["tokens", "balances"],
|
| 474 |
+
"active": true
|
| 475 |
+
}
|
| 476 |
+
],
|
| 477 |
+
"collection_interval": "on_demand",
|
| 478 |
+
"realtime_supported": true
|
| 479 |
}
|
| 480 |
+
},
|
| 481 |
+
"api_endpoints": {
|
| 482 |
+
"unified_service": {
|
| 483 |
+
"base": "/api/service",
|
| 484 |
+
"endpoints": [
|
| 485 |
+
{"method": "GET", "path": "/rate", "params": "?pair=BTC/USDT", "description": "Get exchange rate"},
|
| 486 |
+
{"method": "GET", "path": "/rate/batch", "params": "?pairs=BTC/USDT,ETH/USDT", "description": "Get multiple rates"},
|
| 487 |
+
{"method": "GET", "path": "/market-status", "params": "", "description": "Market overview"},
|
| 488 |
+
{"method": "GET", "path": "/top", "params": "?n=10", "description": "Top coins"},
|
| 489 |
+
{"method": "GET", "path": "/sentiment", "params": "?symbol=BTC", "description": "Get sentiment"},
|
| 490 |
+
{"method": "GET", "path": "/whales", "params": "?chain=ethereum&min_amount_usd=1000000", "description": "Whale transactions"},
|
| 491 |
+
{"method": "GET", "path": "/onchain", "params": "?address=0x...&chain=ethereum", "description": "On-chain data"},
|
| 492 |
+
{"method": "POST", "path": "/query", "params": "", "description": "Universal query endpoint"}
|
| 493 |
+
]
|
| 494 |
+
},
|
| 495 |
+
"market": {
|
| 496 |
+
"base": "/api",
|
| 497 |
+
"endpoints": [
|
| 498 |
+
{"method": "GET", "path": "/market", "params": "?limit=100", "description": "Market data"},
|
| 499 |
+
{"method": "GET", "path": "/ohlcv", "params": "?symbol=BTC&timeframe=1h&limit=500", "description": "OHLCV data"},
|
| 500 |
+
{"method": "GET", "path": "/klines", "params": "?symbol=BTCUSDT&interval=1h", "description": "Klines (alias)"},
|
| 501 |
+
{"method": "GET", "path": "/historical", "params": "?symbol=BTC&days=30", "description": "Historical data"},
|
| 502 |
+
{"method": "GET", "path": "/coins/top", "params": "?limit=50", "description": "Top coins"}
|
| 503 |
+
]
|
| 504 |
+
},
|
| 505 |
+
"news": {
|
| 506 |
+
"base": "/api",
|
| 507 |
+
"endpoints": [
|
| 508 |
+
{"method": "GET", "path": "/news", "params": "?limit=20", "description": "Latest news"},
|
| 509 |
+
{"method": "GET", "path": "/news/latest", "params": "?symbol=BTC&limit=10", "description": "Latest by symbol"}
|
| 510 |
+
]
|
| 511 |
+
},
|
| 512 |
+
"sentiment": {
|
| 513 |
+
"base": "/api",
|
| 514 |
+
"endpoints": [
|
| 515 |
+
{"method": "GET", "path": "/sentiment/global", "params": "", "description": "Global sentiment"},
|
| 516 |
+
{"method": "GET", "path": "/fear-greed", "params": "", "description": "Fear & Greed Index"},
|
| 517 |
+
{"method": "POST", "path": "/sentiment/analyze", "params": "", "description": "Analyze text sentiment"}
|
| 518 |
+
]
|
| 519 |
+
},
|
| 520 |
+
"technical": {
|
| 521 |
+
"base": "/api/technical",
|
| 522 |
+
"endpoints": [
|
| 523 |
+
{"method": "POST", "path": "/ta-quick", "params": "", "description": "Quick technical analysis"},
|
| 524 |
+
{"method": "POST", "path": "/fa-eval", "params": "", "description": "Fundamental evaluation"},
|
| 525 |
+
{"method": "POST", "path": "/onchain-health", "params": "", "description": "On-chain health"},
|
| 526 |
+
{"method": "POST", "path": "/risk-assessment", "params": "", "description": "Risk assessment"},
|
| 527 |
+
{"method": "POST", "path": "/comprehensive", "params": "", "description": "Comprehensive analysis"}
|
| 528 |
+
]
|
| 529 |
+
},
|
| 530 |
+
"ai_models": {
|
| 531 |
+
"base": "/api",
|
| 532 |
+
"endpoints": [
|
| 533 |
+
{"method": "GET", "path": "/models/list", "params": "", "description": "List all models"},
|
| 534 |
+
{"method": "GET", "path": "/models/status", "params": "", "description": "Models status"},
|
| 535 |
+
{"method": "GET", "path": "/models/health", "params": "", "description": "Models health"},
|
| 536 |
+
{"method": "POST", "path": "/models/reinit-all", "params": "", "description": "Reinitialize models"},
|
| 537 |
+
{"method": "POST", "path": "/ai/decision", "params": "", "description": "AI trading decision"}
|
| 538 |
+
]
|
| 539 |
+
},
|
| 540 |
+
"resources": {
|
| 541 |
+
"base": "/api",
|
| 542 |
+
"endpoints": [
|
| 543 |
+
{"method": "GET", "path": "/resources/stats", "params": "", "description": "Resources statistics"},
|
| 544 |
+
{"method": "GET", "path": "/resources/apis", "params": "", "description": "All APIs list"},
|
| 545 |
+
{"method": "GET", "path": "/resources/summary", "params": "", "description": "Resources summary"},
|
| 546 |
+
{"method": "GET", "path": "/providers", "params": "", "description": "Data providers"},
|
| 547 |
+
{"method": "GET", "path": "/status", "params": "", "description": "System status"},
|
| 548 |
+
{"method": "GET", "path": "/health", "params": "", "description": "Health check"}
|
| 549 |
+
]
|
| 550 |
+
},
|
| 551 |
+
"websocket": {
|
| 552 |
+
"base": "/ws",
|
| 553 |
+
"endpoints": [
|
| 554 |
+
{"method": "WS", "path": "/master", "params": "", "description": "Master WebSocket (all services)"},
|
| 555 |
+
{"method": "WS", "path": "/live", "params": "", "description": "Live market data"},
|
| 556 |
+
{"method": "WS", "path": "/ai/data", "params": "", "description": "AI model updates"},
|
| 557 |
+
{"method": "WS", "path": "/data", "params": "", "description": "Data collection stream"},
|
| 558 |
+
{"method": "WS", "path": "/monitoring", "params": "", "description": "System monitoring"}
|
| 559 |
+
]
|
| 560 |
}
|
| 561 |
+
},
|
| 562 |
+
"collection_config": {
|
| 563 |
+
"intervals": {
|
| 564 |
+
"market": {"minutes": 15, "description": "Market data collection every 15 minutes"},
|
| 565 |
+
"news": {"minutes": 15, "description": "News collection every 15 minutes"},
|
| 566 |
+
"sentiment": {"minutes": 15, "description": "Sentiment collection every 15 minutes"},
|
| 567 |
+
"onchain": {"minutes": 30, "description": "On-chain data every 30 minutes"},
|
| 568 |
+
"defi": {"minutes": 15, "description": "DeFi data every 15 minutes"},
|
| 569 |
+
"historical": {"minutes": 30, "description": "Historical data every 30 minutes"}
|
| 570 |
+
},
|
| 571 |
+
"realtime": {
|
| 572 |
+
"description": "For client-side requests, data is fetched immediately from source",
|
| 573 |
+
"sources": ["binance", "coingecko", "coincap", "fear_greed_index"],
|
| 574 |
+
"cache_ttl_seconds": 60
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 575 |
}
|
| 576 |
+
},
|
| 577 |
+
"statistics": {
|
| 578 |
+
"total_providers": 40,
|
| 579 |
+
"active_providers": 38,
|
| 580 |
+
"free_providers": 35,
|
| 581 |
+
"categories_count": 8,
|
| 582 |
+
"total_endpoints": 200,
|
| 583 |
+
"api_keys_configured": 11
|
| 584 |
+
}
|
| 585 |
}
|
static/js/free_resources.ts
ADDED
|
@@ -0,0 +1,978 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Free Resources - Comprehensive Collection of Crypto Data Sources
|
| 3 |
+
* Based on NewResourceApi documentation and additional verified sources
|
| 4 |
+
*
|
| 5 |
+
* این فایل شامل تمام منابع رایگان داده کریپتو است
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
// ============ Types & Enums ============
|
| 9 |
+
|
| 10 |
+
export enum ResourceType {
|
| 11 |
+
MARKET_DATA = "market_data",
|
| 12 |
+
NEWS = "news",
|
| 13 |
+
SENTIMENT = "sentiment",
|
| 14 |
+
BLOCKCHAIN = "blockchain",
|
| 15 |
+
ONCHAIN = "onchain",
|
| 16 |
+
DEFI = "defi",
|
| 17 |
+
WHALE_TRACKING = "whale_tracking",
|
| 18 |
+
TECHNICAL = "technical",
|
| 19 |
+
AI_MODEL = "ai_model",
|
| 20 |
+
SOCIAL = "social",
|
| 21 |
+
HISTORICAL = "historical"
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
export enum TimeFrame {
|
| 25 |
+
REALTIME = "realtime",
|
| 26 |
+
MINUTE_1 = "1m",
|
| 27 |
+
MINUTE_5 = "5m",
|
| 28 |
+
MINUTE_15 = "15m",
|
| 29 |
+
MINUTE_30 = "30m",
|
| 30 |
+
HOUR_1 = "1h",
|
| 31 |
+
HOUR_4 = "4h",
|
| 32 |
+
DAY_1 = "1d",
|
| 33 |
+
WEEK_1 = "1w",
|
| 34 |
+
MONTH_1 = "1M"
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
export interface APIEndpoint {
|
| 38 |
+
name: string;
|
| 39 |
+
path: string;
|
| 40 |
+
method?: "GET" | "POST" | "PUT" | "DELETE";
|
| 41 |
+
params?: Record<string, string>;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
export interface APIResource {
|
| 45 |
+
id: string;
|
| 46 |
+
name: string;
|
| 47 |
+
resourceType: ResourceType;
|
| 48 |
+
baseUrl: string;
|
| 49 |
+
apiKeyEnv?: string;
|
| 50 |
+
apiKey?: string;
|
| 51 |
+
rateLimit: string;
|
| 52 |
+
isFree: boolean;
|
| 53 |
+
requiresAuth: boolean;
|
| 54 |
+
isActive: boolean;
|
| 55 |
+
priority: number;
|
| 56 |
+
description: string;
|
| 57 |
+
endpoints: Record<string, string>;
|
| 58 |
+
supportedTimeframes: string[];
|
| 59 |
+
features: string[];
|
| 60 |
+
headers?: Record<string, string>;
|
| 61 |
+
documentationUrl?: string;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
// ============ API Keys Configuration ============
|
| 65 |
+
|
| 66 |
+
export const API_KEYS = {
|
| 67 |
+
// Block Explorers
|
| 68 |
+
etherscan: {
|
| 69 |
+
key: "SZHYFZK2RR8H9TIMJBVW54V4H81K2Z2KR2",
|
| 70 |
+
backupKey: "T6IR8VJHX2NE6ZJW2S3FDVN1TYG4PYYI45"
|
| 71 |
+
},
|
| 72 |
+
bscscan: {
|
| 73 |
+
key: "K62RKHGXTDCG53RU4MCG6XABIMJKTN19IT"
|
| 74 |
+
},
|
| 75 |
+
tronscan: {
|
| 76 |
+
key: "7ae72726-bffe-4e74-9c33-97b761eeea21"
|
| 77 |
+
},
|
| 78 |
+
|
| 79 |
+
// Market Data
|
| 80 |
+
coinmarketcap: {
|
| 81 |
+
keys: [
|
| 82 |
+
"a35ffaec-c66c-4f16-81e3-41a717e4822f",
|
| 83 |
+
"04cf4b5b-9868-465c-8ba0-9f2e78c92eb1"
|
| 84 |
+
]
|
| 85 |
+
},
|
| 86 |
+
|
| 87 |
+
// News
|
| 88 |
+
newsapi: {
|
| 89 |
+
key: "968a5e25552b4cb5ba3280361d8444ab"
|
| 90 |
+
},
|
| 91 |
+
|
| 92 |
+
// Sentiment
|
| 93 |
+
sentimentApi: {
|
| 94 |
+
key: "vltdvdho63uqnjgf_fq75qbks72e3wfmx"
|
| 95 |
+
},
|
| 96 |
+
|
| 97 |
+
// AI Models
|
| 98 |
+
huggingface: {
|
| 99 |
+
key: "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"
|
| 100 |
+
},
|
| 101 |
+
|
| 102 |
+
// Notifications
|
| 103 |
+
telegram: {
|
| 104 |
+
enabled: true,
|
| 105 |
+
botToken: "7437859619:AAGeGG3ZkLM0OVaw-Exx1uMRE55JtBCZZCY",
|
| 106 |
+
chatId: "-1002228627548"
|
| 107 |
+
}
|
| 108 |
+
};
|
| 109 |
+
|
| 110 |
+
// ============ Block Explorers ============
|
| 111 |
+
|
| 112 |
+
export const BLOCK_EXPLORERS: Record<string, APIResource> = {
|
| 113 |
+
etherscan: {
|
| 114 |
+
id: "etherscan",
|
| 115 |
+
name: "Etherscan",
|
| 116 |
+
resourceType: ResourceType.BLOCKCHAIN,
|
| 117 |
+
baseUrl: "https://api.etherscan.io/api",
|
| 118 |
+
apiKeyEnv: "ETHERSCAN_KEY",
|
| 119 |
+
apiKey: API_KEYS.etherscan.key,
|
| 120 |
+
rateLimit: "5 req/sec",
|
| 121 |
+
isFree: true,
|
| 122 |
+
requiresAuth: true,
|
| 123 |
+
isActive: true,
|
| 124 |
+
priority: 1,
|
| 125 |
+
description: "Ethereum blockchain explorer API",
|
| 126 |
+
endpoints: {
|
| 127 |
+
account_balance: "?module=account&action=balance",
|
| 128 |
+
account_txlist: "?module=account&action=txlist",
|
| 129 |
+
token_balance: "?module=account&action=tokenbalance",
|
| 130 |
+
gas_price: "?module=gastracker&action=gasoracle",
|
| 131 |
+
eth_price: "?module=stats&action=ethprice",
|
| 132 |
+
block_by_time: "?module=block&action=getblocknobytime",
|
| 133 |
+
contract_abi: "?module=contract&action=getabi",
|
| 134 |
+
token_transfers: "?module=account&action=tokentx"
|
| 135 |
+
},
|
| 136 |
+
supportedTimeframes: [],
|
| 137 |
+
features: ["transactions", "tokens", "gas", "prices", "contracts"],
|
| 138 |
+
documentationUrl: "https://docs.etherscan.io/"
|
| 139 |
+
},
|
| 140 |
+
|
| 141 |
+
bscscan: {
|
| 142 |
+
id: "bscscan",
|
| 143 |
+
name: "BscScan",
|
| 144 |
+
resourceType: ResourceType.BLOCKCHAIN,
|
| 145 |
+
baseUrl: "https://api.bscscan.com/api",
|
| 146 |
+
apiKeyEnv: "BSCSCAN_KEY",
|
| 147 |
+
apiKey: API_KEYS.bscscan.key,
|
| 148 |
+
rateLimit: "5 req/sec",
|
| 149 |
+
isFree: true,
|
| 150 |
+
requiresAuth: true,
|
| 151 |
+
isActive: true,
|
| 152 |
+
priority: 1,
|
| 153 |
+
description: "BSC blockchain explorer API",
|
| 154 |
+
endpoints: {
|
| 155 |
+
account_balance: "?module=account&action=balance",
|
| 156 |
+
account_txlist: "?module=account&action=txlist",
|
| 157 |
+
token_balance: "?module=account&action=tokenbalance",
|
| 158 |
+
gas_price: "?module=gastracker&action=gasoracle",
|
| 159 |
+
bnb_price: "?module=stats&action=bnbprice",
|
| 160 |
+
token_transfers: "?module=account&action=tokentx"
|
| 161 |
+
},
|
| 162 |
+
supportedTimeframes: [],
|
| 163 |
+
features: ["transactions", "tokens", "gas", "prices", "contracts"],
|
| 164 |
+
documentationUrl: "https://docs.bscscan.com/"
|
| 165 |
+
},
|
| 166 |
+
|
| 167 |
+
tronscan: {
|
| 168 |
+
id: "tronscan",
|
| 169 |
+
name: "TronScan",
|
| 170 |
+
resourceType: ResourceType.BLOCKCHAIN,
|
| 171 |
+
baseUrl: "https://apilist.tronscanapi.com/api",
|
| 172 |
+
apiKeyEnv: "TRONSCAN_KEY",
|
| 173 |
+
apiKey: API_KEYS.tronscan.key,
|
| 174 |
+
rateLimit: "varies",
|
| 175 |
+
isFree: true,
|
| 176 |
+
requiresAuth: true,
|
| 177 |
+
isActive: true,
|
| 178 |
+
priority: 1,
|
| 179 |
+
description: "Tron blockchain explorer API",
|
| 180 |
+
endpoints: {
|
| 181 |
+
account: "/account",
|
| 182 |
+
account_list: "/accountv2",
|
| 183 |
+
transaction: "/transaction",
|
| 184 |
+
transaction_info: "/transaction-info",
|
| 185 |
+
token: "/token",
|
| 186 |
+
token_trc10: "/token_trc10",
|
| 187 |
+
token_trc20: "/token_trc20",
|
| 188 |
+
contract: "/contract",
|
| 189 |
+
node: "/node"
|
| 190 |
+
},
|
| 191 |
+
supportedTimeframes: [],
|
| 192 |
+
features: ["transactions", "tokens", "contracts", "trc10", "trc20"],
|
| 193 |
+
headers: { "TRON-PRO-API-KEY": API_KEYS.tronscan.key },
|
| 194 |
+
documentationUrl: "https://tronscan.org/#/doc"
|
| 195 |
+
},
|
| 196 |
+
|
| 197 |
+
polygonscan: {
|
| 198 |
+
id: "polygonscan",
|
| 199 |
+
name: "Polygonscan",
|
| 200 |
+
resourceType: ResourceType.BLOCKCHAIN,
|
| 201 |
+
baseUrl: "https://api.polygonscan.com/api",
|
| 202 |
+
apiKeyEnv: "POLYGONSCAN_KEY",
|
| 203 |
+
rateLimit: "5 req/sec",
|
| 204 |
+
isFree: true,
|
| 205 |
+
requiresAuth: true,
|
| 206 |
+
isActive: true,
|
| 207 |
+
priority: 2,
|
| 208 |
+
description: "Polygon blockchain explorer API",
|
| 209 |
+
endpoints: {
|
| 210 |
+
account_balance: "?module=account&action=balance",
|
| 211 |
+
account_txlist: "?module=account&action=txlist",
|
| 212 |
+
token_balance: "?module=account&action=tokenbalance",
|
| 213 |
+
gas_price: "?module=gastracker&action=gasoracle",
|
| 214 |
+
matic_price: "?module=stats&action=maticprice"
|
| 215 |
+
},
|
| 216 |
+
supportedTimeframes: [],
|
| 217 |
+
features: ["transactions", "tokens", "gas", "prices"],
|
| 218 |
+
documentationUrl: "https://docs.polygonscan.com/"
|
| 219 |
+
},
|
| 220 |
+
|
| 221 |
+
blockchair: {
|
| 222 |
+
id: "blockchair",
|
| 223 |
+
name: "Blockchair",
|
| 224 |
+
resourceType: ResourceType.BLOCKCHAIN,
|
| 225 |
+
baseUrl: "https://api.blockchair.com",
|
| 226 |
+
rateLimit: "30 req/min free",
|
| 227 |
+
isFree: true,
|
| 228 |
+
requiresAuth: false,
|
| 229 |
+
isActive: true,
|
| 230 |
+
priority: 2,
|
| 231 |
+
description: "Multi-chain blockchain explorer API",
|
| 232 |
+
endpoints: {
|
| 233 |
+
bitcoin_stats: "/bitcoin/stats",
|
| 234 |
+
ethereum_stats: "/ethereum/stats",
|
| 235 |
+
bitcoin_blocks: "/bitcoin/blocks",
|
| 236 |
+
ethereum_blocks: "/ethereum/blocks",
|
| 237 |
+
bitcoin_transactions: "/bitcoin/transactions",
|
| 238 |
+
ethereum_transactions: "/ethereum/transactions"
|
| 239 |
+
},
|
| 240 |
+
supportedTimeframes: [],
|
| 241 |
+
features: ["multi-chain", "transactions", "blocks", "stats"],
|
| 242 |
+
documentationUrl: "https://blockchair.com/api/docs"
|
| 243 |
+
}
|
| 244 |
+
};
|
| 245 |
+
|
| 246 |
+
// ============ Market Data Sources ============
|
| 247 |
+
|
| 248 |
+
export const MARKET_DATA_SOURCES: Record<string, APIResource> = {
|
| 249 |
+
coinmarketcap: {
|
| 250 |
+
id: "coinmarketcap",
|
| 251 |
+
name: "CoinMarketCap",
|
| 252 |
+
resourceType: ResourceType.MARKET_DATA,
|
| 253 |
+
baseUrl: "https://pro-api.coinmarketcap.com/v1",
|
| 254 |
+
apiKeyEnv: "COINMARKETCAP_KEY",
|
| 255 |
+
apiKey: API_KEYS.coinmarketcap.keys[0],
|
| 256 |
+
rateLimit: "333 req/day free",
|
| 257 |
+
isFree: true,
|
| 258 |
+
requiresAuth: true,
|
| 259 |
+
isActive: true,
|
| 260 |
+
priority: 1,
|
| 261 |
+
description: "Leading cryptocurrency market data API",
|
| 262 |
+
endpoints: {
|
| 263 |
+
listings_latest: "/cryptocurrency/listings/latest",
|
| 264 |
+
quotes_latest: "/cryptocurrency/quotes/latest",
|
| 265 |
+
info: "/cryptocurrency/info",
|
| 266 |
+
map: "/cryptocurrency/map",
|
| 267 |
+
categories: "/cryptocurrency/categories",
|
| 268 |
+
global_metrics: "/global-metrics/quotes/latest",
|
| 269 |
+
exchange_listings: "/exchange/listings/latest"
|
| 270 |
+
},
|
| 271 |
+
supportedTimeframes: ["1h", "24h", "7d", "30d", "60d", "90d"],
|
| 272 |
+
features: ["prices", "market_cap", "volume", "rankings", "historical"],
|
| 273 |
+
headers: { "X-CMC_PRO_API_KEY": API_KEYS.coinmarketcap.keys[0] },
|
| 274 |
+
documentationUrl: "https://coinmarketcap.com/api/documentation/v1/"
|
| 275 |
+
},
|
| 276 |
+
|
| 277 |
+
coingecko: {
|
| 278 |
+
id: "coingecko",
|
| 279 |
+
name: "CoinGecko",
|
| 280 |
+
resourceType: ResourceType.MARKET_DATA,
|
| 281 |
+
baseUrl: "https://api.coingecko.com/api/v3",
|
| 282 |
+
rateLimit: "10-50 req/min free",
|
| 283 |
+
isFree: true,
|
| 284 |
+
requiresAuth: false,
|
| 285 |
+
isActive: true,
|
| 286 |
+
priority: 1,
|
| 287 |
+
description: "Comprehensive cryptocurrency data API",
|
| 288 |
+
endpoints: {
|
| 289 |
+
ping: "/ping",
|
| 290 |
+
simple_price: "/simple/price",
|
| 291 |
+
coins_list: "/coins/list",
|
| 292 |
+
coins_markets: "/coins/markets",
|
| 293 |
+
coin_detail: "/coins/{id}",
|
| 294 |
+
coin_history: "/coins/{id}/history",
|
| 295 |
+
coin_market_chart: "/coins/{id}/market_chart",
|
| 296 |
+
coin_ohlc: "/coins/{id}/ohlc",
|
| 297 |
+
trending: "/search/trending",
|
| 298 |
+
global: "/global",
|
| 299 |
+
exchanges: "/exchanges"
|
| 300 |
+
},
|
| 301 |
+
supportedTimeframes: ["1d", "7d", "14d", "30d", "90d", "180d", "365d", "max"],
|
| 302 |
+
features: ["prices", "market_cap", "volume", "historical", "trending", "defi"],
|
| 303 |
+
documentationUrl: "https://www.coingecko.com/en/api/documentation"
|
| 304 |
+
},
|
| 305 |
+
|
| 306 |
+
coincap: {
|
| 307 |
+
id: "coincap",
|
| 308 |
+
name: "CoinCap",
|
| 309 |
+
resourceType: ResourceType.MARKET_DATA,
|
| 310 |
+
baseUrl: "https://api.coincap.io/v2",
|
| 311 |
+
rateLimit: "200 req/min free",
|
| 312 |
+
isFree: true,
|
| 313 |
+
requiresAuth: false,
|
| 314 |
+
isActive: true,
|
| 315 |
+
priority: 1,
|
| 316 |
+
description: "Real-time cryptocurrency market data",
|
| 317 |
+
endpoints: {
|
| 318 |
+
assets: "/assets",
|
| 319 |
+
asset_detail: "/assets/{id}",
|
| 320 |
+
asset_history: "/assets/{id}/history",
|
| 321 |
+
markets: "/assets/{id}/markets",
|
| 322 |
+
rates: "/rates",
|
| 323 |
+
exchanges: "/exchanges",
|
| 324 |
+
candles: "/candles"
|
| 325 |
+
},
|
| 326 |
+
supportedTimeframes: ["m1", "m5", "m15", "m30", "h1", "h2", "h6", "h12", "d1"],
|
| 327 |
+
features: ["real-time", "prices", "volume", "market_cap", "historical"],
|
| 328 |
+
documentationUrl: "https://docs.coincap.io/"
|
| 329 |
+
},
|
| 330 |
+
|
| 331 |
+
binance: {
|
| 332 |
+
id: "binance",
|
| 333 |
+
name: "Binance",
|
| 334 |
+
resourceType: ResourceType.MARKET_DATA,
|
| 335 |
+
baseUrl: "https://api.binance.com/api/v3",
|
| 336 |
+
rateLimit: "1200 req/min",
|
| 337 |
+
isFree: true,
|
| 338 |
+
requiresAuth: false,
|
| 339 |
+
isActive: true,
|
| 340 |
+
priority: 1,
|
| 341 |
+
description: "Binance exchange public API",
|
| 342 |
+
endpoints: {
|
| 343 |
+
ping: "/ping",
|
| 344 |
+
time: "/time",
|
| 345 |
+
ticker_price: "/ticker/price",
|
| 346 |
+
ticker_24hr: "/ticker/24hr",
|
| 347 |
+
klines: "/klines",
|
| 348 |
+
depth: "/depth",
|
| 349 |
+
trades: "/trades",
|
| 350 |
+
avg_price: "/avgPrice",
|
| 351 |
+
exchange_info: "/exchangeInfo"
|
| 352 |
+
},
|
| 353 |
+
supportedTimeframes: ["1m", "3m", "5m", "15m", "30m", "1h", "2h", "4h", "6h", "8h", "12h", "1d", "3d", "1w", "1M"],
|
| 354 |
+
features: ["real-time", "prices", "ohlcv", "order_book", "trades"],
|
| 355 |
+
documentationUrl: "https://binance-docs.github.io/apidocs/spot/en/"
|
| 356 |
+
},
|
| 357 |
+
|
| 358 |
+
kucoin: {
|
| 359 |
+
id: "kucoin",
|
| 360 |
+
name: "KuCoin",
|
| 361 |
+
resourceType: ResourceType.MARKET_DATA,
|
| 362 |
+
baseUrl: "https://api.kucoin.com/api/v1",
|
| 363 |
+
rateLimit: "varies",
|
| 364 |
+
isFree: true,
|
| 365 |
+
requiresAuth: false,
|
| 366 |
+
isActive: true,
|
| 367 |
+
priority: 2,
|
| 368 |
+
description: "KuCoin exchange public API",
|
| 369 |
+
endpoints: {
|
| 370 |
+
market_list: "/market/allTickers",
|
| 371 |
+
ticker: "/market/orderbook/level1",
|
| 372 |
+
market_stats: "/market/stats",
|
| 373 |
+
currencies: "/currencies",
|
| 374 |
+
symbols: "/symbols",
|
| 375 |
+
klines: "/market/candles"
|
| 376 |
+
},
|
| 377 |
+
supportedTimeframes: ["1min", "3min", "5min", "15min", "30min", "1hour", "2hour", "4hour", "6hour", "8hour", "12hour", "1day", "1week"],
|
| 378 |
+
features: ["prices", "ohlcv", "order_book", "trades"],
|
| 379 |
+
documentationUrl: "https://docs.kucoin.com/"
|
| 380 |
+
},
|
| 381 |
+
|
| 382 |
+
kraken: {
|
| 383 |
+
id: "kraken",
|
| 384 |
+
name: "Kraken",
|
| 385 |
+
resourceType: ResourceType.MARKET_DATA,
|
| 386 |
+
baseUrl: "https://api.kraken.com/0/public",
|
| 387 |
+
rateLimit: "1 req/sec",
|
| 388 |
+
isFree: true,
|
| 389 |
+
requiresAuth: false,
|
| 390 |
+
isActive: true,
|
| 391 |
+
priority: 2,
|
| 392 |
+
description: "Kraken exchange public API",
|
| 393 |
+
endpoints: {
|
| 394 |
+
time: "/Time",
|
| 395 |
+
assets: "/Assets",
|
| 396 |
+
asset_pairs: "/AssetPairs",
|
| 397 |
+
ticker: "/Ticker",
|
| 398 |
+
ohlc: "/OHLC",
|
| 399 |
+
depth: "/Depth",
|
| 400 |
+
trades: "/Trades",
|
| 401 |
+
spread: "/Spread"
|
| 402 |
+
},
|
| 403 |
+
supportedTimeframes: ["1", "5", "15", "30", "60", "240", "1440", "10080", "21600"],
|
| 404 |
+
features: ["prices", "ohlcv", "order_book", "trades"],
|
| 405 |
+
documentationUrl: "https://docs.kraken.com/rest/"
|
| 406 |
+
}
|
| 407 |
+
};
|
| 408 |
+
|
| 409 |
+
// ============ News Sources ============
|
| 410 |
+
|
| 411 |
+
export const NEWS_SOURCES: Record<string, APIResource> = {
|
| 412 |
+
newsapi: {
|
| 413 |
+
id: "newsapi",
|
| 414 |
+
name: "NewsAPI",
|
| 415 |
+
resourceType: ResourceType.NEWS,
|
| 416 |
+
baseUrl: "https://newsapi.org/v2",
|
| 417 |
+
apiKeyEnv: "NEWSAPI_KEY",
|
| 418 |
+
apiKey: API_KEYS.newsapi.key,
|
| 419 |
+
rateLimit: "100 req/day free",
|
| 420 |
+
isFree: true,
|
| 421 |
+
requiresAuth: true,
|
| 422 |
+
isActive: true,
|
| 423 |
+
priority: 1,
|
| 424 |
+
description: "News articles from thousands of sources",
|
| 425 |
+
endpoints: {
|
| 426 |
+
everything: "/everything",
|
| 427 |
+
top_headlines: "/top-headlines",
|
| 428 |
+
sources: "/sources"
|
| 429 |
+
},
|
| 430 |
+
supportedTimeframes: [],
|
| 431 |
+
features: ["articles", "headlines", "sources", "search"],
|
| 432 |
+
documentationUrl: "https://newsapi.org/docs"
|
| 433 |
+
},
|
| 434 |
+
|
| 435 |
+
cryptopanic: {
|
| 436 |
+
id: "cryptopanic",
|
| 437 |
+
name: "CryptoPanic",
|
| 438 |
+
resourceType: ResourceType.NEWS,
|
| 439 |
+
baseUrl: "https://cryptopanic.com/api/v1",
|
| 440 |
+
apiKeyEnv: "CRYPTOPANIC_KEY",
|
| 441 |
+
rateLimit: "5 req/sec",
|
| 442 |
+
isFree: true,
|
| 443 |
+
requiresAuth: true,
|
| 444 |
+
isActive: true,
|
| 445 |
+
priority: 1,
|
| 446 |
+
description: "Cryptocurrency news aggregator",
|
| 447 |
+
endpoints: {
|
| 448 |
+
posts: "/posts/",
|
| 449 |
+
currencies: "/currencies/"
|
| 450 |
+
},
|
| 451 |
+
supportedTimeframes: [],
|
| 452 |
+
features: ["news", "sentiment", "trending"],
|
| 453 |
+
documentationUrl: "https://cryptopanic.com/developers/api/"
|
| 454 |
+
},
|
| 455 |
+
|
| 456 |
+
coindesk_rss: {
|
| 457 |
+
id: "coindesk_rss",
|
| 458 |
+
name: "CoinDesk RSS",
|
| 459 |
+
resourceType: ResourceType.NEWS,
|
| 460 |
+
baseUrl: "https://www.coindesk.com",
|
| 461 |
+
rateLimit: "unlimited",
|
| 462 |
+
isFree: true,
|
| 463 |
+
requiresAuth: false,
|
| 464 |
+
isActive: true,
|
| 465 |
+
priority: 2,
|
| 466 |
+
description: "CoinDesk crypto news RSS feed",
|
| 467 |
+
endpoints: {
|
| 468 |
+
rss: "/arc/outboundfeeds/rss/"
|
| 469 |
+
},
|
| 470 |
+
supportedTimeframes: [],
|
| 471 |
+
features: ["news", "rss"],
|
| 472 |
+
documentationUrl: "https://www.coindesk.com/arc/outboundfeeds/rss/"
|
| 473 |
+
},
|
| 474 |
+
|
| 475 |
+
cointelegraph_rss: {
|
| 476 |
+
id: "cointelegraph_rss",
|
| 477 |
+
name: "Cointelegraph RSS",
|
| 478 |
+
resourceType: ResourceType.NEWS,
|
| 479 |
+
baseUrl: "https://cointelegraph.com",
|
| 480 |
+
rateLimit: "unlimited",
|
| 481 |
+
isFree: true,
|
| 482 |
+
requiresAuth: false,
|
| 483 |
+
isActive: true,
|
| 484 |
+
priority: 2,
|
| 485 |
+
description: "Cointelegraph crypto news RSS feed",
|
| 486 |
+
endpoints: {
|
| 487 |
+
rss: "/rss"
|
| 488 |
+
},
|
| 489 |
+
supportedTimeframes: [],
|
| 490 |
+
features: ["news", "rss"],
|
| 491 |
+
documentationUrl: "https://cointelegraph.com/rss"
|
| 492 |
+
},
|
| 493 |
+
|
| 494 |
+
cryptocompare_news: {
|
| 495 |
+
id: "cryptocompare_news",
|
| 496 |
+
name: "CryptoCompare News",
|
| 497 |
+
resourceType: ResourceType.NEWS,
|
| 498 |
+
baseUrl: "https://min-api.cryptocompare.com/data",
|
| 499 |
+
rateLimit: "100,000 req/month free",
|
| 500 |
+
isFree: true,
|
| 501 |
+
requiresAuth: false,
|
| 502 |
+
isActive: true,
|
| 503 |
+
priority: 2,
|
| 504 |
+
description: "CryptoCompare news API",
|
| 505 |
+
endpoints: {
|
| 506 |
+
news_latest: "/v2/news/?lang=EN",
|
| 507 |
+
news_feeds: "/news/feeds",
|
| 508 |
+
news_categories: "/news/categories"
|
| 509 |
+
},
|
| 510 |
+
supportedTimeframes: [],
|
| 511 |
+
features: ["news", "categories", "feeds"],
|
| 512 |
+
documentationUrl: "https://min-api.cryptocompare.com/documentation"
|
| 513 |
+
}
|
| 514 |
+
};
|
| 515 |
+
|
| 516 |
+
// ============ Sentiment Sources ============
|
| 517 |
+
|
| 518 |
+
export const SENTIMENT_SOURCES: Record<string, APIResource> = {
|
| 519 |
+
fear_greed_index: {
|
| 520 |
+
id: "fear_greed_index",
|
| 521 |
+
name: "Fear & Greed Index",
|
| 522 |
+
resourceType: ResourceType.SENTIMENT,
|
| 523 |
+
baseUrl: "https://api.alternative.me",
|
| 524 |
+
rateLimit: "unlimited",
|
| 525 |
+
isFree: true,
|
| 526 |
+
requiresAuth: false,
|
| 527 |
+
isActive: true,
|
| 528 |
+
priority: 1,
|
| 529 |
+
description: "Crypto Fear & Greed Index",
|
| 530 |
+
endpoints: {
|
| 531 |
+
fng: "/fng/",
|
| 532 |
+
fng_history: "/fng/?limit=30"
|
| 533 |
+
},
|
| 534 |
+
supportedTimeframes: ["daily"],
|
| 535 |
+
features: ["sentiment", "fear_greed", "historical"],
|
| 536 |
+
documentationUrl: "https://alternative.me/crypto/fear-and-greed-index/"
|
| 537 |
+
},
|
| 538 |
+
|
| 539 |
+
custom_sentiment: {
|
| 540 |
+
id: "custom_sentiment",
|
| 541 |
+
name: "Custom Sentiment API",
|
| 542 |
+
resourceType: ResourceType.SENTIMENT,
|
| 543 |
+
baseUrl: "https://sentiment-api.example.com",
|
| 544 |
+
apiKeyEnv: "SENTIMENT_API_KEY",
|
| 545 |
+
apiKey: API_KEYS.sentimentApi.key,
|
| 546 |
+
rateLimit: "varies",
|
| 547 |
+
isFree: true,
|
| 548 |
+
requiresAuth: true,
|
| 549 |
+
isActive: true,
|
| 550 |
+
priority: 2,
|
| 551 |
+
description: "Custom sentiment analysis API",
|
| 552 |
+
endpoints: {
|
| 553 |
+
analyze: "/analyze",
|
| 554 |
+
market_sentiment: "/market-sentiment",
|
| 555 |
+
social_sentiment: "/social-sentiment"
|
| 556 |
+
},
|
| 557 |
+
supportedTimeframes: [],
|
| 558 |
+
features: ["sentiment", "social", "market"]
|
| 559 |
+
},
|
| 560 |
+
|
| 561 |
+
lunarcrush: {
|
| 562 |
+
id: "lunarcrush",
|
| 563 |
+
name: "LunarCrush",
|
| 564 |
+
resourceType: ResourceType.SENTIMENT,
|
| 565 |
+
baseUrl: "https://lunarcrush.com/api/v2",
|
| 566 |
+
apiKeyEnv: "LUNARCRUSH_KEY",
|
| 567 |
+
rateLimit: "varies",
|
| 568 |
+
isFree: true,
|
| 569 |
+
requiresAuth: true,
|
| 570 |
+
isActive: true,
|
| 571 |
+
priority: 2,
|
| 572 |
+
description: "Social sentiment analytics",
|
| 573 |
+
endpoints: {
|
| 574 |
+
assets: "/assets",
|
| 575 |
+
market: "/market",
|
| 576 |
+
global: "/global",
|
| 577 |
+
influencers: "/influencers"
|
| 578 |
+
},
|
| 579 |
+
supportedTimeframes: [],
|
| 580 |
+
features: ["social_sentiment", "influencers", "trending"],
|
| 581 |
+
documentationUrl: "https://lunarcrush.com/developers"
|
| 582 |
+
}
|
| 583 |
+
};
|
| 584 |
+
|
| 585 |
+
// ============ On-Chain Analytics ============
|
| 586 |
+
|
| 587 |
+
export const ONCHAIN_SOURCES: Record<string, APIResource> = {
|
| 588 |
+
blockchain_com: {
|
| 589 |
+
id: "blockchain_com",
|
| 590 |
+
name: "Blockchain.com",
|
| 591 |
+
resourceType: ResourceType.ONCHAIN,
|
| 592 |
+
baseUrl: "https://api.blockchain.info",
|
| 593 |
+
rateLimit: "varies",
|
| 594 |
+
isFree: true,
|
| 595 |
+
requiresAuth: false,
|
| 596 |
+
isActive: true,
|
| 597 |
+
priority: 1,
|
| 598 |
+
description: "Bitcoin blockchain data",
|
| 599 |
+
endpoints: {
|
| 600 |
+
stats: "/stats",
|
| 601 |
+
ticker: "/ticker",
|
| 602 |
+
rawblock: "/rawblock/{hash}",
|
| 603 |
+
rawtx: "/rawtx/{hash}",
|
| 604 |
+
balance: "/balance"
|
| 605 |
+
},
|
| 606 |
+
supportedTimeframes: [],
|
| 607 |
+
features: ["bitcoin", "transactions", "blocks", "addresses"],
|
| 608 |
+
documentationUrl: "https://www.blockchain.com/api"
|
| 609 |
+
},
|
| 610 |
+
|
| 611 |
+
mempool_space: {
|
| 612 |
+
id: "mempool_space",
|
| 613 |
+
name: "Mempool.space",
|
| 614 |
+
resourceType: ResourceType.ONCHAIN,
|
| 615 |
+
baseUrl: "https://mempool.space/api",
|
| 616 |
+
rateLimit: "varies",
|
| 617 |
+
isFree: true,
|
| 618 |
+
requiresAuth: false,
|
| 619 |
+
isActive: true,
|
| 620 |
+
priority: 1,
|
| 621 |
+
description: "Bitcoin mempool and blockchain explorer",
|
| 622 |
+
endpoints: {
|
| 623 |
+
mempool: "/mempool",
|
| 624 |
+
fees_recommended: "/v1/fees/recommended",
|
| 625 |
+
blocks: "/blocks",
|
| 626 |
+
block_height: "/block-height/{height}",
|
| 627 |
+
tx: "/tx/{txid}"
|
| 628 |
+
},
|
| 629 |
+
supportedTimeframes: [],
|
| 630 |
+
features: ["mempool", "fees", "blocks", "transactions"],
|
| 631 |
+
documentationUrl: "https://mempool.space/docs/api"
|
| 632 |
+
}
|
| 633 |
+
};
|
| 634 |
+
|
| 635 |
+
// ============ DeFi Sources ============
|
| 636 |
+
|
| 637 |
+
export const DEFI_SOURCES: Record<string, APIResource> = {
|
| 638 |
+
defillama: {
|
| 639 |
+
id: "defillama",
|
| 640 |
+
name: "DefiLlama",
|
| 641 |
+
resourceType: ResourceType.DEFI,
|
| 642 |
+
baseUrl: "https://api.llama.fi",
|
| 643 |
+
rateLimit: "unlimited",
|
| 644 |
+
isFree: true,
|
| 645 |
+
requiresAuth: false,
|
| 646 |
+
isActive: true,
|
| 647 |
+
priority: 1,
|
| 648 |
+
description: "DeFi TVL and protocol analytics",
|
| 649 |
+
endpoints: {
|
| 650 |
+
protocols: "/protocols",
|
| 651 |
+
protocol_detail: "/protocol/{protocol}",
|
| 652 |
+
tvl_all: "/tvl",
|
| 653 |
+
chains: "/chains",
|
| 654 |
+
stablecoins: "/stablecoins",
|
| 655 |
+
yields: "/yields/pools",
|
| 656 |
+
dexs: "/overview/dexs"
|
| 657 |
+
},
|
| 658 |
+
supportedTimeframes: [],
|
| 659 |
+
features: ["tvl", "protocols", "chains", "yields", "dexs"],
|
| 660 |
+
documentationUrl: "https://defillama.com/docs/api"
|
| 661 |
+
},
|
| 662 |
+
|
| 663 |
+
inch_1: {
|
| 664 |
+
id: "1inch",
|
| 665 |
+
name: "1inch",
|
| 666 |
+
resourceType: ResourceType.DEFI,
|
| 667 |
+
baseUrl: "https://api.1inch.io/v5.0/1",
|
| 668 |
+
rateLimit: "varies",
|
| 669 |
+
isFree: true,
|
| 670 |
+
requiresAuth: false,
|
| 671 |
+
isActive: true,
|
| 672 |
+
priority: 2,
|
| 673 |
+
description: "DEX aggregator API",
|
| 674 |
+
endpoints: {
|
| 675 |
+
tokens: "/tokens",
|
| 676 |
+
quote: "/quote",
|
| 677 |
+
swap: "/swap",
|
| 678 |
+
liquidity_sources: "/liquidity-sources"
|
| 679 |
+
},
|
| 680 |
+
supportedTimeframes: [],
|
| 681 |
+
features: ["dex", "swap", "quotes", "aggregator"],
|
| 682 |
+
documentationUrl: "https://docs.1inch.io/"
|
| 683 |
+
}
|
| 684 |
+
};
|
| 685 |
+
|
| 686 |
+
// ============ Whale Tracking ============
|
| 687 |
+
|
| 688 |
+
export const WHALE_SOURCES: Record<string, APIResource> = {
|
| 689 |
+
whale_alert: {
|
| 690 |
+
id: "whale_alert",
|
| 691 |
+
name: "Whale Alert",
|
| 692 |
+
resourceType: ResourceType.WHALE_TRACKING,
|
| 693 |
+
baseUrl: "https://api.whale-alert.io/v1",
|
| 694 |
+
apiKeyEnv: "WHALE_ALERT_KEY",
|
| 695 |
+
rateLimit: "10 req/min free",
|
| 696 |
+
isFree: true,
|
| 697 |
+
requiresAuth: true,
|
| 698 |
+
isActive: true,
|
| 699 |
+
priority: 1,
|
| 700 |
+
description: "Large crypto transaction tracking",
|
| 701 |
+
endpoints: {
|
| 702 |
+
status: "/status",
|
| 703 |
+
transactions: "/transactions"
|
| 704 |
+
},
|
| 705 |
+
supportedTimeframes: [],
|
| 706 |
+
features: ["whale_alerts", "large_transactions", "multi-chain"],
|
| 707 |
+
documentationUrl: "https://docs.whale-alert.io/"
|
| 708 |
+
}
|
| 709 |
+
};
|
| 710 |
+
|
| 711 |
+
// ============ Technical Analysis ============
|
| 712 |
+
|
| 713 |
+
export const TECHNICAL_SOURCES: Record<string, APIResource> = {
|
| 714 |
+
taapi: {
|
| 715 |
+
id: "taapi",
|
| 716 |
+
name: "TAAPI.IO",
|
| 717 |
+
resourceType: ResourceType.TECHNICAL,
|
| 718 |
+
baseUrl: "https://api.taapi.io",
|
| 719 |
+
apiKeyEnv: "TAAPI_KEY",
|
| 720 |
+
rateLimit: "varies",
|
| 721 |
+
isFree: true,
|
| 722 |
+
requiresAuth: true,
|
| 723 |
+
isActive: true,
|
| 724 |
+
priority: 1,
|
| 725 |
+
description: "Technical analysis indicators API",
|
| 726 |
+
endpoints: {
|
| 727 |
+
rsi: "/rsi",
|
| 728 |
+
macd: "/macd",
|
| 729 |
+
ema: "/ema",
|
| 730 |
+
sma: "/sma",
|
| 731 |
+
bbands: "/bbands",
|
| 732 |
+
stoch: "/stoch",
|
| 733 |
+
atr: "/atr",
|
| 734 |
+
adx: "/adx",
|
| 735 |
+
dmi: "/dmi",
|
| 736 |
+
sar: "/sar",
|
| 737 |
+
ichimoku: "/ichimoku"
|
| 738 |
+
},
|
| 739 |
+
supportedTimeframes: [],
|
| 740 |
+
features: ["indicators", "rsi", "macd", "bollinger", "ema", "sma"],
|
| 741 |
+
documentationUrl: "https://taapi.io/documentation/"
|
| 742 |
+
}
|
| 743 |
+
};
|
| 744 |
+
|
| 745 |
+
// ============ Social Sources ============
|
| 746 |
+
|
| 747 |
+
export const SOCIAL_SOURCES: Record<string, APIResource> = {
|
| 748 |
+
reddit: {
|
| 749 |
+
id: "reddit",
|
| 750 |
+
name: "Reddit API",
|
| 751 |
+
resourceType: ResourceType.SOCIAL,
|
| 752 |
+
baseUrl: "https://www.reddit.com",
|
| 753 |
+
rateLimit: "60 req/min",
|
| 754 |
+
isFree: true,
|
| 755 |
+
requiresAuth: false,
|
| 756 |
+
isActive: true,
|
| 757 |
+
priority: 1,
|
| 758 |
+
description: "Reddit cryptocurrency communities",
|
| 759 |
+
endpoints: {
|
| 760 |
+
r_crypto: "/r/CryptoCurrency/hot.json",
|
| 761 |
+
r_bitcoin: "/r/Bitcoin/hot.json",
|
| 762 |
+
r_ethereum: "/r/ethereum/hot.json",
|
| 763 |
+
r_altcoin: "/r/altcoin/hot.json",
|
| 764 |
+
r_defi: "/r/defi/hot.json"
|
| 765 |
+
},
|
| 766 |
+
supportedTimeframes: [],
|
| 767 |
+
features: ["discussions", "sentiment", "trending"],
|
| 768 |
+
documentationUrl: "https://www.reddit.com/dev/api/"
|
| 769 |
+
}
|
| 770 |
+
};
|
| 771 |
+
|
| 772 |
+
// ============ Historical Data Sources ============
|
| 773 |
+
|
| 774 |
+
export const HISTORICAL_SOURCES: Record<string, APIResource> = {
|
| 775 |
+
cryptocompare_historical: {
|
| 776 |
+
id: "cryptocompare_historical",
|
| 777 |
+
name: "CryptoCompare Historical",
|
| 778 |
+
resourceType: ResourceType.HISTORICAL,
|
| 779 |
+
baseUrl: "https://min-api.cryptocompare.com/data",
|
| 780 |
+
rateLimit: "100,000 req/month free",
|
| 781 |
+
isFree: true,
|
| 782 |
+
requiresAuth: false,
|
| 783 |
+
isActive: true,
|
| 784 |
+
priority: 1,
|
| 785 |
+
description: "Historical crypto price data",
|
| 786 |
+
endpoints: {
|
| 787 |
+
histoday: "/v2/histoday",
|
| 788 |
+
histohour: "/v2/histohour",
|
| 789 |
+
histominute: "/histominute"
|
| 790 |
+
},
|
| 791 |
+
supportedTimeframes: ["1m", "1h", "1d"],
|
| 792 |
+
features: ["ohlcv", "historical", "daily", "hourly", "minute"],
|
| 793 |
+
documentationUrl: "https://min-api.cryptocompare.com/documentation"
|
| 794 |
+
},
|
| 795 |
+
|
| 796 |
+
messari: {
|
| 797 |
+
id: "messari",
|
| 798 |
+
name: "Messari",
|
| 799 |
+
resourceType: ResourceType.HISTORICAL,
|
| 800 |
+
baseUrl: "https://data.messari.io/api/v1",
|
| 801 |
+
apiKeyEnv: "MESSARI_KEY",
|
| 802 |
+
rateLimit: "20 req/min free",
|
| 803 |
+
isFree: true,
|
| 804 |
+
requiresAuth: false,
|
| 805 |
+
isActive: true,
|
| 806 |
+
priority: 2,
|
| 807 |
+
description: "Crypto research and data",
|
| 808 |
+
endpoints: {
|
| 809 |
+
assets: "/assets",
|
| 810 |
+
asset_detail: "/assets/{symbol}",
|
| 811 |
+
asset_metrics: "/assets/{symbol}/metrics",
|
| 812 |
+
asset_profile: "/assets/{symbol}/profile"
|
| 813 |
+
},
|
| 814 |
+
supportedTimeframes: [],
|
| 815 |
+
features: ["metrics", "profiles", "research"],
|
| 816 |
+
documentationUrl: "https://messari.io/api"
|
| 817 |
+
}
|
| 818 |
+
};
|
| 819 |
+
|
| 820 |
+
// ============ ML Models Configuration ============
|
| 821 |
+
|
| 822 |
+
export interface MLModel {
|
| 823 |
+
name: string;
|
| 824 |
+
type: string;
|
| 825 |
+
purpose: string;
|
| 826 |
+
inputFeatures?: string[];
|
| 827 |
+
timeframes?: string[];
|
| 828 |
+
huggingfaceModel?: string;
|
| 829 |
+
}
|
| 830 |
+
|
| 831 |
+
export const ML_MODELS_CONFIG: Record<string, MLModel> = {
|
| 832 |
+
price_prediction_lstm: {
|
| 833 |
+
name: "PricePredictionLSTM",
|
| 834 |
+
type: "LSTM",
|
| 835 |
+
purpose: "Short-term price prediction",
|
| 836 |
+
inputFeatures: ["open", "high", "low", "close", "volume"],
|
| 837 |
+
timeframes: ["1m", "5m", "15m", "1h", "4h"]
|
| 838 |
+
},
|
| 839 |
+
sentiment_analysis_transformer: {
|
| 840 |
+
name: "SentimentAnalysisTransformer",
|
| 841 |
+
type: "Transformer",
|
| 842 |
+
purpose: "News and social media sentiment analysis",
|
| 843 |
+
huggingfaceModel: "ProsusAI/finbert"
|
| 844 |
+
},
|
| 845 |
+
anomaly_detection_isolation_forest: {
|
| 846 |
+
name: "AnomalyDetectionIsolationForest",
|
| 847 |
+
type: "Isolation Forest",
|
| 848 |
+
purpose: "Detecting market anomalies"
|
| 849 |
+
},
|
| 850 |
+
trend_classification_random_forest: {
|
| 851 |
+
name: "TrendClassificationRandomForest",
|
| 852 |
+
type: "Random Forest",
|
| 853 |
+
purpose: "Market trend classification"
|
| 854 |
+
}
|
| 855 |
+
};
|
| 856 |
+
|
| 857 |
+
// ============ Analysis Endpoints Configuration ============
|
| 858 |
+
|
| 859 |
+
export const ANALYSIS_ENDPOINTS: Record<string, string> = {
|
| 860 |
+
track_position: "/track_position",
|
| 861 |
+
market_analysis: "/market_analysis",
|
| 862 |
+
technical_analysis: "/technical_analysis",
|
| 863 |
+
sentiment_analysis: "/sentiment_analysis",
|
| 864 |
+
whale_activity: "/whale_activity",
|
| 865 |
+
trading_strategies: "/trading_strategies",
|
| 866 |
+
ai_prediction: "/ai_prediction",
|
| 867 |
+
risk_management: "/risk_management",
|
| 868 |
+
pdf_analysis: "/pdf_analysis",
|
| 869 |
+
ai_enhanced_analysis: "/ai_enhanced_analysis",
|
| 870 |
+
multi_source_data: "/multi_source_data",
|
| 871 |
+
news_analysis: "/news_analysis",
|
| 872 |
+
exchange_integration: "/exchange_integration",
|
| 873 |
+
smart_alerts: "/smart_alerts",
|
| 874 |
+
advanced_social_media_analysis: "/advanced_social_media_analysis",
|
| 875 |
+
dynamic_modeling: "/dynamic_modeling",
|
| 876 |
+
multi_currency_analysis: "/multi_currency_analysis",
|
| 877 |
+
telegram_settings: "/telegram_settings",
|
| 878 |
+
collect_data: "/collect-data",
|
| 879 |
+
greed_fear_index: "/greed-fear-index",
|
| 880 |
+
onchain_metrics: "/onchain-metrics",
|
| 881 |
+
custom_alerts: "/custom-alerts",
|
| 882 |
+
stakeholder_analysis: "/stakeholder-analysis"
|
| 883 |
+
};
|
| 884 |
+
|
| 885 |
+
// ============ Combined Registry ============
|
| 886 |
+
|
| 887 |
+
export const ALL_RESOURCES: Record<string, APIResource> = {
|
| 888 |
+
...BLOCK_EXPLORERS,
|
| 889 |
+
...MARKET_DATA_SOURCES,
|
| 890 |
+
...NEWS_SOURCES,
|
| 891 |
+
...SENTIMENT_SOURCES,
|
| 892 |
+
...ONCHAIN_SOURCES,
|
| 893 |
+
...DEFI_SOURCES,
|
| 894 |
+
...WHALE_SOURCES,
|
| 895 |
+
...TECHNICAL_SOURCES,
|
| 896 |
+
...SOCIAL_SOURCES,
|
| 897 |
+
...HISTORICAL_SOURCES
|
| 898 |
+
};
|
| 899 |
+
|
| 900 |
+
// ============ Utility Functions ============
|
| 901 |
+
|
| 902 |
+
export function getResourceById(id: string): APIResource | undefined {
|
| 903 |
+
return ALL_RESOURCES[id];
|
| 904 |
+
}
|
| 905 |
+
|
| 906 |
+
export function getResourcesByType(type: ResourceType): APIResource[] {
|
| 907 |
+
return Object.values(ALL_RESOURCES).filter(r => r.resourceType === type);
|
| 908 |
+
}
|
| 909 |
+
|
| 910 |
+
export function getFreeResources(): APIResource[] {
|
| 911 |
+
return Object.values(ALL_RESOURCES).filter(r => r.isFree);
|
| 912 |
+
}
|
| 913 |
+
|
| 914 |
+
export function getActiveResources(): APIResource[] {
|
| 915 |
+
return Object.values(ALL_RESOURCES).filter(r => r.isActive);
|
| 916 |
+
}
|
| 917 |
+
|
| 918 |
+
export function getNoAuthResources(): APIResource[] {
|
| 919 |
+
return Object.values(ALL_RESOURCES).filter(r => !r.requiresAuth);
|
| 920 |
+
}
|
| 921 |
+
|
| 922 |
+
export function searchResources(query: string): APIResource[] {
|
| 923 |
+
const q = query.toLowerCase();
|
| 924 |
+
return Object.values(ALL_RESOURCES).filter(
|
| 925 |
+
r => r.name.toLowerCase().includes(q) ||
|
| 926 |
+
r.description.toLowerCase().includes(q) ||
|
| 927 |
+
r.features.some(f => f.toLowerCase().includes(q))
|
| 928 |
+
);
|
| 929 |
+
}
|
| 930 |
+
|
| 931 |
+
export function getStatistics(): {
|
| 932 |
+
total: number;
|
| 933 |
+
free: number;
|
| 934 |
+
active: number;
|
| 935 |
+
noAuth: number;
|
| 936 |
+
byType: Record<string, number>;
|
| 937 |
+
} {
|
| 938 |
+
const resources = Object.values(ALL_RESOURCES);
|
| 939 |
+
const byType: Record<string, number> = {};
|
| 940 |
+
|
| 941 |
+
for (const r of resources) {
|
| 942 |
+
const type = r.resourceType;
|
| 943 |
+
byType[type] = (byType[type] || 0) + 1;
|
| 944 |
+
}
|
| 945 |
+
|
| 946 |
+
return {
|
| 947 |
+
total: resources.length,
|
| 948 |
+
free: resources.filter(r => r.isFree).length,
|
| 949 |
+
active: resources.filter(r => r.isActive).length,
|
| 950 |
+
noAuth: resources.filter(r => !r.requiresAuth).length,
|
| 951 |
+
byType
|
| 952 |
+
};
|
| 953 |
+
}
|
| 954 |
+
|
| 955 |
+
// Export default
|
| 956 |
+
export default {
|
| 957 |
+
API_KEYS,
|
| 958 |
+
ALL_RESOURCES,
|
| 959 |
+
BLOCK_EXPLORERS,
|
| 960 |
+
MARKET_DATA_SOURCES,
|
| 961 |
+
NEWS_SOURCES,
|
| 962 |
+
SENTIMENT_SOURCES,
|
| 963 |
+
ONCHAIN_SOURCES,
|
| 964 |
+
DEFI_SOURCES,
|
| 965 |
+
WHALE_SOURCES,
|
| 966 |
+
TECHNICAL_SOURCES,
|
| 967 |
+
SOCIAL_SOURCES,
|
| 968 |
+
HISTORICAL_SOURCES,
|
| 969 |
+
ML_MODELS_CONFIG,
|
| 970 |
+
ANALYSIS_ENDPOINTS,
|
| 971 |
+
getResourceById,
|
| 972 |
+
getResourcesByType,
|
| 973 |
+
getFreeResources,
|
| 974 |
+
getActiveResources,
|
| 975 |
+
getNoAuthResources,
|
| 976 |
+
searchResources,
|
| 977 |
+
getStatistics
|
| 978 |
+
};
|
static/shared/components/config-helper-modal.js
CHANGED
|
@@ -1,6 +1,16 @@
|
|
| 1 |
/**
|
| 2 |
-
* Configuration Helper Modal
|
| 3 |
* Shows users how to configure and use all backend services
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
*/
|
| 5 |
|
| 6 |
export class ConfigHelperModal {
|
|
@@ -13,135 +23,264 @@ export class ConfigHelperModal {
|
|
| 13 |
const baseUrl = window.location.origin;
|
| 14 |
|
| 15 |
return [
|
|
|
|
| 16 |
{
|
| 17 |
-
name: '
|
| 18 |
category: 'Core Services',
|
| 19 |
-
description: '
|
| 20 |
endpoints: [
|
| 21 |
-
{ method: 'GET', path: '/api/
|
| 22 |
-
{ method: 'GET', path: '/api/
|
| 23 |
-
{ method: 'GET', path: '/api/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
],
|
| 25 |
-
example:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
.then(res => res.json())
|
| 27 |
-
.then(data => console.log(
|
| 28 |
},
|
|
|
|
|
|
|
| 29 |
{
|
| 30 |
-
name: '
|
| 31 |
-
category: '
|
| 32 |
-
description: '
|
| 33 |
endpoints: [
|
| 34 |
-
{ method: 'GET', path: '/api/
|
| 35 |
-
{ method: 'GET', path: '/api/
|
| 36 |
-
{ method: '
|
|
|
|
|
|
|
|
|
|
| 37 |
],
|
| 38 |
-
example:
|
|
|
|
| 39 |
.then(res => res.json())
|
| 40 |
-
.then(data =>
|
|
|
|
|
|
|
|
|
|
| 41 |
},
|
|
|
|
|
|
|
| 42 |
{
|
| 43 |
name: 'News Aggregator API',
|
| 44 |
-
category: '
|
| 45 |
-
description: 'Crypto news from
|
| 46 |
endpoints: [
|
| 47 |
-
{ method: 'GET', path: '/api/news', desc: 'Latest crypto news' },
|
| 48 |
-
{ method: 'GET', path: '/api/news/latest?limit=10', desc: 'News
|
| 49 |
-
{ method: 'GET', path: '/api/news?source=
|
| 50 |
],
|
| 51 |
-
example:
|
|
|
|
| 52 |
.then(res => res.json())
|
| 53 |
-
.then(data =>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
},
|
|
|
|
|
|
|
| 55 |
{
|
| 56 |
-
name: '
|
| 57 |
-
category: '
|
| 58 |
-
description: '
|
| 59 |
endpoints: [
|
| 60 |
-
{ method: 'GET', path: '/api/
|
| 61 |
-
{ method: 'GET', path: '/api/
|
| 62 |
-
{ method: 'GET', path: '/api/
|
|
|
|
| 63 |
],
|
| 64 |
-
example:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
.then(res => res.json())
|
| 66 |
-
.then(data => console.log(data));`
|
| 67 |
},
|
|
|
|
|
|
|
| 68 |
{
|
| 69 |
-
name: '
|
| 70 |
-
category: '
|
| 71 |
-
description: '
|
| 72 |
endpoints: [
|
| 73 |
-
{ method: 'GET', path: '/api/
|
| 74 |
-
{ method: 'GET', path: '/api/
|
| 75 |
-
{ method: 'GET', path: '/api/
|
|
|
|
| 76 |
],
|
| 77 |
-
example:
|
|
|
|
| 78 |
.then(res => res.json())
|
| 79 |
-
.then(data =>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
},
|
|
|
|
|
|
|
| 81 |
{
|
| 82 |
-
name: '
|
| 83 |
-
category: '
|
| 84 |
-
description: '
|
| 85 |
endpoints: [
|
| 86 |
-
{ method: '
|
| 87 |
-
{ method: '
|
| 88 |
-
{ method: 'POST', path: '/api/
|
|
|
|
|
|
|
| 89 |
],
|
| 90 |
-
example:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
.then(res => res.json())
|
| 92 |
-
.then(data =>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
},
|
|
|
|
|
|
|
| 94 |
{
|
| 95 |
-
name: '
|
| 96 |
-
category: '
|
| 97 |
-
description: '
|
| 98 |
endpoints: [
|
| 99 |
-
{ method: 'GET', path: '/api/
|
| 100 |
-
{ method: 'GET', path: '/api/
|
| 101 |
-
{ method: 'GET', path: '/api/
|
|
|
|
|
|
|
| 102 |
],
|
| 103 |
-
example:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
.then(res => res.json())
|
| 105 |
-
.then(data =>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
},
|
|
|
|
|
|
|
| 107 |
{
|
| 108 |
-
name: '
|
| 109 |
-
category: '
|
| 110 |
-
description: '
|
| 111 |
endpoints: [
|
| 112 |
-
{ method: 'GET', path: '/api/
|
| 113 |
-
{ method: 'GET', path: '/api/
|
| 114 |
-
{ method: 'GET', path: '/api/
|
| 115 |
],
|
| 116 |
-
example:
|
|
|
|
| 117 |
.then(res => res.json())
|
| 118 |
-
.then(data =>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
},
|
|
|
|
|
|
|
| 120 |
{
|
| 121 |
-
name: 'Resources API',
|
| 122 |
category: 'System Services',
|
| 123 |
-
description: 'API resources and
|
| 124 |
endpoints: [
|
|
|
|
|
|
|
| 125 |
{ method: 'GET', path: '/api/resources/summary', desc: 'Resources summary' },
|
| 126 |
-
{ method: 'GET', path: '/api/
|
| 127 |
-
{ method: 'GET', path: '/api/
|
|
|
|
| 128 |
],
|
| 129 |
-
example:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
.then(res => res.json())
|
| 131 |
-
.then(data =>
|
|
|
|
|
|
|
|
|
|
| 132 |
},
|
|
|
|
|
|
|
| 133 |
{
|
| 134 |
-
name: '
|
| 135 |
-
category: '
|
| 136 |
-
description: '
|
| 137 |
endpoints: [
|
| 138 |
-
{ method: '
|
| 139 |
-
{ method: '
|
| 140 |
-
{ method: '
|
|
|
|
| 141 |
],
|
| 142 |
-
example:
|
| 143 |
-
|
| 144 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
}
|
| 146 |
];
|
| 147 |
}
|
|
@@ -185,7 +324,7 @@ export class ConfigHelperModal {
|
|
| 185 |
|
| 186 |
<div class="config-helper-body">
|
| 187 |
<div class="config-helper-intro">
|
| 188 |
-
<p>Copy and paste these
|
| 189 |
<div class="config-helper-base-url">
|
| 190 |
<strong>Base URL:</strong>
|
| 191 |
<code>${window.location.origin}</code>
|
|
@@ -196,6 +335,12 @@ export class ConfigHelperModal {
|
|
| 196 |
</svg>
|
| 197 |
</button>
|
| 198 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
</div>
|
| 200 |
|
| 201 |
<div class="config-helper-services">
|
|
@@ -435,6 +580,7 @@ style.textContent = `
|
|
| 435 |
background: var(--bg-secondary, #f3f4f6);
|
| 436 |
border-radius: 8px;
|
| 437 |
font-size: 14px;
|
|
|
|
| 438 |
}
|
| 439 |
|
| 440 |
.config-helper-base-url code {
|
|
@@ -446,6 +592,23 @@ style.textContent = `
|
|
| 446 |
font-size: 13px;
|
| 447 |
}
|
| 448 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 449 |
.service-category {
|
| 450 |
margin-bottom: 24px;
|
| 451 |
}
|
|
@@ -556,16 +719,23 @@ style.textContent = `
|
|
| 556 |
color: white;
|
| 557 |
}
|
| 558 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 559 |
.endpoint-path {
|
| 560 |
flex: 1;
|
| 561 |
font-family: 'Courier New', monospace;
|
| 562 |
font-size: 12px;
|
| 563 |
color: var(--text-primary, #0f2926);
|
|
|
|
| 564 |
}
|
| 565 |
|
| 566 |
.endpoint-desc {
|
| 567 |
color: var(--text-muted, #6b7280);
|
| 568 |
font-size: 12px;
|
|
|
|
| 569 |
}
|
| 570 |
|
| 571 |
.code-example {
|
|
@@ -630,6 +800,11 @@ style.textContent = `
|
|
| 630 |
.endpoint-desc {
|
| 631 |
width: 100%;
|
| 632 |
margin-top: 4px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 633 |
}
|
| 634 |
}
|
| 635 |
`;
|
|
|
|
| 1 |
/**
|
| 2 |
+
* Configuration Helper Modal - Updated with All Services
|
| 3 |
* Shows users how to configure and use all backend services
|
| 4 |
+
*
|
| 5 |
+
* Services Include:
|
| 6 |
+
* - Market Data (8+ providers)
|
| 7 |
+
* - News (9+ sources)
|
| 8 |
+
* - Sentiment Analysis (4+ providers)
|
| 9 |
+
* - On-Chain Analytics (4+ providers)
|
| 10 |
+
* - DeFi Data (3+ providers)
|
| 11 |
+
* - Technical Analysis
|
| 12 |
+
* - AI Models
|
| 13 |
+
* - Block Explorers
|
| 14 |
*/
|
| 15 |
|
| 16 |
export class ConfigHelperModal {
|
|
|
|
| 23 |
const baseUrl = window.location.origin;
|
| 24 |
|
| 25 |
return [
|
| 26 |
+
// ===== UNIFIED SERVICE API =====
|
| 27 |
{
|
| 28 |
+
name: 'Unified Service API',
|
| 29 |
category: 'Core Services',
|
| 30 |
+
description: 'Single entry point for all cryptocurrency data needs',
|
| 31 |
endpoints: [
|
| 32 |
+
{ method: 'GET', path: '/api/service/rate?pair=BTC/USDT', desc: 'Get exchange rate' },
|
| 33 |
+
{ method: 'GET', path: '/api/service/rate/batch?pairs=BTC/USDT,ETH/USDT', desc: 'Multiple rates' },
|
| 34 |
+
{ method: 'GET', path: '/api/service/market-status', desc: 'Market overview' },
|
| 35 |
+
{ method: 'GET', path: '/api/service/top?n=10', desc: 'Top cryptocurrencies' },
|
| 36 |
+
{ method: 'GET', path: '/api/service/sentiment?symbol=BTC', desc: 'Get sentiment' },
|
| 37 |
+
{ method: 'GET', path: '/api/service/whales?chain=ethereum&min_amount_usd=1000000', desc: 'Whale transactions' },
|
| 38 |
+
{ method: 'GET', path: '/api/service/onchain?address=0x...&chain=ethereum', desc: 'On-chain data' },
|
| 39 |
+
{ method: 'POST', path: '/api/service/query', desc: 'Universal query endpoint' }
|
| 40 |
],
|
| 41 |
+
example: `// Get BTC price
|
| 42 |
+
fetch('${baseUrl}/api/service/rate?pair=BTC/USDT')
|
| 43 |
+
.then(res => res.json())
|
| 44 |
+
.then(data => console.log('BTC Price:', data.data.price));
|
| 45 |
+
|
| 46 |
+
// Get multiple prices
|
| 47 |
+
fetch('${baseUrl}/api/service/rate/batch?pairs=BTC/USDT,ETH/USDT,BNB/USDT')
|
| 48 |
.then(res => res.json())
|
| 49 |
+
.then(data => data.data.forEach(r => console.log(r.pair + ': $' + r.price)));`
|
| 50 |
},
|
| 51 |
+
|
| 52 |
+
// ===== MARKET DATA =====
|
| 53 |
{
|
| 54 |
+
name: 'Market Data API',
|
| 55 |
+
category: 'Market Data',
|
| 56 |
+
description: 'Real-time prices, OHLCV, and market statistics from 8+ providers',
|
| 57 |
endpoints: [
|
| 58 |
+
{ method: 'GET', path: '/api/market?limit=100', desc: 'Market data with prices' },
|
| 59 |
+
{ method: 'GET', path: '/api/ohlcv?symbol=BTC&timeframe=1h&limit=500', desc: 'OHLCV candlestick data' },
|
| 60 |
+
{ method: 'GET', path: '/api/klines?symbol=BTCUSDT&interval=1h', desc: 'Klines (alias for OHLCV)' },
|
| 61 |
+
{ method: 'GET', path: '/api/historical?symbol=BTC&days=30', desc: 'Historical price data' },
|
| 62 |
+
{ method: 'GET', path: '/api/coins/top?limit=50', desc: 'Top coins by market cap' },
|
| 63 |
+
{ method: 'GET', path: '/api/trending', desc: 'Trending cryptocurrencies' }
|
| 64 |
],
|
| 65 |
+
example: `// Get OHLCV data for charting
|
| 66 |
+
fetch('${baseUrl}/api/ohlcv?symbol=BTC&timeframe=1h&limit=100')
|
| 67 |
.then(res => res.json())
|
| 68 |
+
.then(data => {
|
| 69 |
+
console.log('OHLCV data:', data.data);
|
| 70 |
+
// Each candle: { t, o, h, l, c, v }
|
| 71 |
+
});`
|
| 72 |
},
|
| 73 |
+
|
| 74 |
+
// ===== NEWS =====
|
| 75 |
{
|
| 76 |
name: 'News Aggregator API',
|
| 77 |
+
category: 'News & Media',
|
| 78 |
+
description: 'Crypto news from 9+ sources including RSS feeds',
|
| 79 |
endpoints: [
|
| 80 |
+
{ method: 'GET', path: '/api/news?limit=20', desc: 'Latest crypto news' },
|
| 81 |
+
{ method: 'GET', path: '/api/news/latest?symbol=BTC&limit=10', desc: 'News filtered by symbol' },
|
| 82 |
+
{ method: 'GET', path: '/api/news?source=decrypt', desc: 'News from specific source' }
|
| 83 |
],
|
| 84 |
+
example: `// Get latest news
|
| 85 |
+
fetch('${baseUrl}/api/news?limit=10')
|
| 86 |
.then(res => res.json())
|
| 87 |
+
.then(data => {
|
| 88 |
+
data.articles.forEach(article => {
|
| 89 |
+
console.log(article.title, '-', article.source);
|
| 90 |
+
});
|
| 91 |
+
});`
|
| 92 |
},
|
| 93 |
+
|
| 94 |
+
// ===== SENTIMENT =====
|
| 95 |
{
|
| 96 |
+
name: 'Sentiment Analysis API',
|
| 97 |
+
category: 'Sentiment',
|
| 98 |
+
description: 'Fear & Greed Index, social sentiment, and AI-powered analysis',
|
| 99 |
endpoints: [
|
| 100 |
+
{ method: 'GET', path: '/api/sentiment/global', desc: 'Global market sentiment' },
|
| 101 |
+
{ method: 'GET', path: '/api/fear-greed', desc: 'Fear & Greed Index' },
|
| 102 |
+
{ method: 'GET', path: '/api/sentiment/asset/{symbol}', desc: 'Asset-specific sentiment' },
|
| 103 |
+
{ method: 'POST', path: '/api/sentiment/analyze', desc: 'Analyze custom text' }
|
| 104 |
],
|
| 105 |
+
example: `// Get Fear & Greed Index
|
| 106 |
+
fetch('${baseUrl}/api/fear-greed')
|
| 107 |
+
.then(res => res.json())
|
| 108 |
+
.then(data => {
|
| 109 |
+
console.log('Fear & Greed:', data.value, '-', data.classification);
|
| 110 |
+
});
|
| 111 |
+
|
| 112 |
+
// Analyze text sentiment
|
| 113 |
+
fetch('${baseUrl}/api/sentiment/analyze', {
|
| 114 |
+
method: 'POST',
|
| 115 |
+
headers: { 'Content-Type': 'application/json' },
|
| 116 |
+
body: JSON.stringify({ text: 'Bitcoin is going to the moon!' })
|
| 117 |
+
})
|
| 118 |
.then(res => res.json())
|
| 119 |
+
.then(data => console.log('Sentiment:', data.label, data.score));`
|
| 120 |
},
|
| 121 |
+
|
| 122 |
+
// ===== ON-CHAIN ANALYTICS =====
|
| 123 |
{
|
| 124 |
+
name: 'On-Chain Analytics API',
|
| 125 |
+
category: 'Analytics',
|
| 126 |
+
description: 'Blockchain data, whale tracking, and network statistics',
|
| 127 |
endpoints: [
|
| 128 |
+
{ method: 'GET', path: '/api/whale', desc: 'Whale transactions' },
|
| 129 |
+
{ method: 'GET', path: '/api/whales/transactions?limit=50', desc: 'Recent whale moves' },
|
| 130 |
+
{ method: 'GET', path: '/api/whales/stats?hours=24', desc: 'Whale activity statistics' },
|
| 131 |
+
{ method: 'GET', path: '/api/blockchain/gas?chain=ethereum', desc: 'Gas prices' }
|
| 132 |
],
|
| 133 |
+
example: `// Get whale transactions
|
| 134 |
+
fetch('${baseUrl}/api/service/whales?chain=ethereum&min_amount_usd=1000000&limit=20')
|
| 135 |
.then(res => res.json())
|
| 136 |
+
.then(data => {
|
| 137 |
+
data.data.forEach(tx => {
|
| 138 |
+
console.log('Whale:', tx.amount_usd, 'USD', tx.chain);
|
| 139 |
+
});
|
| 140 |
+
});`
|
| 141 |
},
|
| 142 |
+
|
| 143 |
+
// ===== TECHNICAL ANALYSIS =====
|
| 144 |
{
|
| 145 |
+
name: 'Technical Analysis API',
|
| 146 |
+
category: 'Analysis Services',
|
| 147 |
+
description: '5 analysis modes: Quick TA, Fundamental, On-Chain, Risk, Comprehensive',
|
| 148 |
endpoints: [
|
| 149 |
+
{ method: 'POST', path: '/api/technical/ta-quick', desc: 'Quick technical analysis' },
|
| 150 |
+
{ method: 'POST', path: '/api/technical/fa-eval', desc: 'Fundamental evaluation' },
|
| 151 |
+
{ method: 'POST', path: '/api/technical/onchain-health', desc: 'On-chain network health' },
|
| 152 |
+
{ method: 'POST', path: '/api/technical/risk-assessment', desc: 'Risk & volatility assessment' },
|
| 153 |
+
{ method: 'POST', path: '/api/technical/comprehensive', desc: 'Comprehensive analysis' }
|
| 154 |
],
|
| 155 |
+
example: `// Quick Technical Analysis
|
| 156 |
+
const ohlcv = await fetch('${baseUrl}/api/ohlcv?symbol=BTC&timeframe=4h&limit=200')
|
| 157 |
+
.then(r => r.json()).then(d => d.data);
|
| 158 |
+
|
| 159 |
+
fetch('${baseUrl}/api/technical/ta-quick', {
|
| 160 |
+
method: 'POST',
|
| 161 |
+
headers: { 'Content-Type': 'application/json' },
|
| 162 |
+
body: JSON.stringify({
|
| 163 |
+
symbol: 'BTC',
|
| 164 |
+
timeframe: '4h',
|
| 165 |
+
ohlcv: ohlcv
|
| 166 |
+
})
|
| 167 |
+
})
|
| 168 |
.then(res => res.json())
|
| 169 |
+
.then(data => {
|
| 170 |
+
console.log('Trend:', data.trend);
|
| 171 |
+
console.log('RSI:', data.rsi);
|
| 172 |
+
console.log('Entry Range:', data.entry_range);
|
| 173 |
+
});`
|
| 174 |
},
|
| 175 |
+
|
| 176 |
+
// ===== AI MODELS =====
|
| 177 |
{
|
| 178 |
+
name: 'AI Models API',
|
| 179 |
+
category: 'AI Services',
|
| 180 |
+
description: 'HuggingFace AI models for sentiment, analysis, and predictions',
|
| 181 |
endpoints: [
|
| 182 |
+
{ method: 'GET', path: '/api/models/status', desc: 'Models status' },
|
| 183 |
+
{ method: 'GET', path: '/api/models/list', desc: 'List all models' },
|
| 184 |
+
{ method: 'GET', path: '/api/models/health', desc: 'Model health check' },
|
| 185 |
+
{ method: 'POST', path: '/api/models/reinit-all', desc: 'Reinitialize models' },
|
| 186 |
+
{ method: 'POST', path: '/api/ai/decision', desc: 'AI trading decision' }
|
| 187 |
],
|
| 188 |
+
example: `// Get AI trading decision
|
| 189 |
+
fetch('${baseUrl}/api/ai/decision', {
|
| 190 |
+
method: 'POST',
|
| 191 |
+
headers: { 'Content-Type': 'application/json' },
|
| 192 |
+
body: JSON.stringify({ symbol: 'BTC', timeframe: '1h' })
|
| 193 |
+
})
|
| 194 |
.then(res => res.json())
|
| 195 |
+
.then(data => {
|
| 196 |
+
console.log('Decision:', data.decision);
|
| 197 |
+
console.log('Confidence:', data.confidence);
|
| 198 |
+
console.log('Signals:', data.signals);
|
| 199 |
+
});`
|
| 200 |
},
|
| 201 |
+
|
| 202 |
+
// ===== DEFI DATA =====
|
| 203 |
{
|
| 204 |
+
name: 'DeFi Data API',
|
| 205 |
+
category: 'DeFi Services',
|
| 206 |
+
description: 'DefiLlama TVL, protocols, yields, and stablecoins',
|
| 207 |
endpoints: [
|
| 208 |
+
{ method: 'GET', path: '/api/defi/tvl', desc: 'Total Value Locked' },
|
| 209 |
+
{ method: 'GET', path: '/api/defi/protocols?limit=20', desc: 'Top DeFi protocols' },
|
| 210 |
+
{ method: 'GET', path: '/api/defi/yields', desc: 'DeFi yields' }
|
| 211 |
],
|
| 212 |
+
example: `// Get DeFi TVL data
|
| 213 |
+
fetch('${baseUrl}/api/defi/protocols?limit=10')
|
| 214 |
.then(res => res.json())
|
| 215 |
+
.then(data => {
|
| 216 |
+
data.protocols.forEach(p => {
|
| 217 |
+
console.log(p.name, '- TVL:', p.tvl);
|
| 218 |
+
});
|
| 219 |
+
});`
|
| 220 |
},
|
| 221 |
+
|
| 222 |
+
// ===== RESOURCES & MONITORING =====
|
| 223 |
{
|
| 224 |
+
name: 'Resources & Monitoring API',
|
| 225 |
category: 'System Services',
|
| 226 |
+
description: 'API resources, providers status, and system health',
|
| 227 |
endpoints: [
|
| 228 |
+
{ method: 'GET', path: '/api/resources/stats', desc: 'Resources statistics' },
|
| 229 |
+
{ method: 'GET', path: '/api/resources/apis', desc: 'All APIs list' },
|
| 230 |
{ method: 'GET', path: '/api/resources/summary', desc: 'Resources summary' },
|
| 231 |
+
{ method: 'GET', path: '/api/providers', desc: 'Data providers list' },
|
| 232 |
+
{ method: 'GET', path: '/api/status', desc: 'System status' },
|
| 233 |
+
{ method: 'GET', path: '/api/health', desc: 'Health check' }
|
| 234 |
],
|
| 235 |
+
example: `// Check system health
|
| 236 |
+
fetch('${baseUrl}/api/health')
|
| 237 |
+
.then(res => res.json())
|
| 238 |
+
.then(data => {
|
| 239 |
+
console.log('Status:', data.status);
|
| 240 |
+
console.log('Providers:', data.providers);
|
| 241 |
+
});
|
| 242 |
+
|
| 243 |
+
// Get resources stats
|
| 244 |
+
fetch('${baseUrl}/api/resources/stats')
|
| 245 |
.then(res => res.json())
|
| 246 |
+
.then(data => {
|
| 247 |
+
console.log('Total APIs:', data.total_functional);
|
| 248 |
+
console.log('Success Rate:', data.success_rate + '%');
|
| 249 |
+
});`
|
| 250 |
},
|
| 251 |
+
|
| 252 |
+
// ===== WEBSOCKET =====
|
| 253 |
{
|
| 254 |
+
name: 'WebSocket API (Optional)',
|
| 255 |
+
category: 'Real-time Services',
|
| 256 |
+
description: 'Optional real-time streaming via WebSocket (HTTP polling recommended)',
|
| 257 |
endpoints: [
|
| 258 |
+
{ method: 'WS', path: '/ws/master', desc: 'Master endpoint (all services)' },
|
| 259 |
+
{ method: 'WS', path: '/ws/live', desc: 'Live market data' },
|
| 260 |
+
{ method: 'WS', path: '/ws/ai/data', desc: 'AI model updates' },
|
| 261 |
+
{ method: 'WS', path: '/ws/monitoring', desc: 'System monitoring' }
|
| 262 |
],
|
| 263 |
+
example: `// WebSocket connection (optional - HTTP works fine)
|
| 264 |
+
const ws = new WebSocket('wss://${window.location.host}/ws/master');
|
| 265 |
+
|
| 266 |
+
ws.onopen = () => {
|
| 267 |
+
ws.send(JSON.stringify({
|
| 268 |
+
action: 'subscribe',
|
| 269 |
+
service: 'market_data'
|
| 270 |
+
}));
|
| 271 |
+
};
|
| 272 |
+
|
| 273 |
+
ws.onmessage = (event) => {
|
| 274 |
+
const data = JSON.parse(event.data);
|
| 275 |
+
console.log('Real-time update:', data);
|
| 276 |
+
};
|
| 277 |
+
|
| 278 |
+
// Alternative: HTTP polling (recommended)
|
| 279 |
+
setInterval(async () => {
|
| 280 |
+
const data = await fetch('${baseUrl}/api/market?limit=100')
|
| 281 |
+
.then(r => r.json());
|
| 282 |
+
console.log('Market data:', data);
|
| 283 |
+
}, 30000);`
|
| 284 |
}
|
| 285 |
];
|
| 286 |
}
|
|
|
|
| 324 |
|
| 325 |
<div class="config-helper-body">
|
| 326 |
<div class="config-helper-intro">
|
| 327 |
+
<p>Access <strong>40+ data providers</strong> through our unified API. Copy and paste these examples to get started.</p>
|
| 328 |
<div class="config-helper-base-url">
|
| 329 |
<strong>Base URL:</strong>
|
| 330 |
<code>${window.location.origin}</code>
|
|
|
|
| 335 |
</svg>
|
| 336 |
</button>
|
| 337 |
</div>
|
| 338 |
+
<div class="config-helper-stats">
|
| 339 |
+
<span>📊 8+ Market Providers</span>
|
| 340 |
+
<span>📰 9+ News Sources</span>
|
| 341 |
+
<span>🎭 4+ Sentiment APIs</span>
|
| 342 |
+
<span>🔗 4+ On-Chain APIs</span>
|
| 343 |
+
</div>
|
| 344 |
</div>
|
| 345 |
|
| 346 |
<div class="config-helper-services">
|
|
|
|
| 580 |
background: var(--bg-secondary, #f3f4f6);
|
| 581 |
border-radius: 8px;
|
| 582 |
font-size: 14px;
|
| 583 |
+
margin-bottom: 12px;
|
| 584 |
}
|
| 585 |
|
| 586 |
.config-helper-base-url code {
|
|
|
|
| 592 |
font-size: 13px;
|
| 593 |
}
|
| 594 |
|
| 595 |
+
.config-helper-stats {
|
| 596 |
+
display: flex;
|
| 597 |
+
flex-wrap: wrap;
|
| 598 |
+
gap: 12px;
|
| 599 |
+
padding: 12px;
|
| 600 |
+
background: linear-gradient(135deg, #e0f2f1 0%, #b2dfdb 100%);
|
| 601 |
+
border-radius: 8px;
|
| 602 |
+
font-size: 13px;
|
| 603 |
+
font-weight: 500;
|
| 604 |
+
}
|
| 605 |
+
|
| 606 |
+
.config-helper-stats span {
|
| 607 |
+
padding: 4px 8px;
|
| 608 |
+
background: rgba(255,255,255,0.7);
|
| 609 |
+
border-radius: 4px;
|
| 610 |
+
}
|
| 611 |
+
|
| 612 |
.service-category {
|
| 613 |
margin-bottom: 24px;
|
| 614 |
}
|
|
|
|
| 719 |
color: white;
|
| 720 |
}
|
| 721 |
|
| 722 |
+
.method-badge.ws {
|
| 723 |
+
background: #8b5cf6;
|
| 724 |
+
color: white;
|
| 725 |
+
}
|
| 726 |
+
|
| 727 |
.endpoint-path {
|
| 728 |
flex: 1;
|
| 729 |
font-family: 'Courier New', monospace;
|
| 730 |
font-size: 12px;
|
| 731 |
color: var(--text-primary, #0f2926);
|
| 732 |
+
word-break: break-all;
|
| 733 |
}
|
| 734 |
|
| 735 |
.endpoint-desc {
|
| 736 |
color: var(--text-muted, #6b7280);
|
| 737 |
font-size: 12px;
|
| 738 |
+
white-space: nowrap;
|
| 739 |
}
|
| 740 |
|
| 741 |
.code-example {
|
|
|
|
| 800 |
.endpoint-desc {
|
| 801 |
width: 100%;
|
| 802 |
margin-top: 4px;
|
| 803 |
+
white-space: normal;
|
| 804 |
+
}
|
| 805 |
+
|
| 806 |
+
.config-helper-stats {
|
| 807 |
+
flex-direction: column;
|
| 808 |
}
|
| 809 |
}
|
| 810 |
`;
|
workers/data_collection_worker.py
ADDED
|
@@ -0,0 +1,643 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Data Collection Background Worker - CONFIGURABLE INTERVALS
|
| 3 |
+
|
| 4 |
+
This worker manages data collection from all sources with:
|
| 5 |
+
- Bulk data collection: 15-30 minute intervals
|
| 6 |
+
- Real-time data: On-demand when client requests
|
| 7 |
+
- Smart scheduling based on source type
|
| 8 |
+
|
| 9 |
+
COLLECTION INTERVALS:
|
| 10 |
+
- Market data: 15 minutes
|
| 11 |
+
- News: 15 minutes
|
| 12 |
+
- Sentiment: 15 minutes
|
| 13 |
+
- On-chain: 30 minutes
|
| 14 |
+
- Historical: 30 minutes
|
| 15 |
+
- DeFi: 15 minutes
|
| 16 |
+
|
| 17 |
+
REAL-TIME DATA:
|
| 18 |
+
- When client requests data, fetch immediately from source
|
| 19 |
+
- Cache results for configured TTL
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
import asyncio
|
| 23 |
+
import time
|
| 24 |
+
import logging
|
| 25 |
+
import os
|
| 26 |
+
from datetime import datetime, timedelta
|
| 27 |
+
from typing import List, Dict, Any, Optional
|
| 28 |
+
import httpx
|
| 29 |
+
|
| 30 |
+
from utils.logger import setup_logger
|
| 31 |
+
|
| 32 |
+
logger = setup_logger("data_collection_worker")
|
| 33 |
+
|
| 34 |
+
# ===== COLLECTION CONFIGURATION =====
|
| 35 |
+
|
| 36 |
+
# Bulk collection intervals (in minutes)
|
| 37 |
+
COLLECTION_INTERVALS = {
|
| 38 |
+
"market": 15, # Market data every 15 minutes
|
| 39 |
+
"news": 15, # News every 15 minutes
|
| 40 |
+
"sentiment": 15, # Sentiment every 15 minutes
|
| 41 |
+
"social": 30, # Social data every 30 minutes
|
| 42 |
+
"onchain": 30, # On-chain every 30 minutes
|
| 43 |
+
"historical": 30, # Historical every 30 minutes
|
| 44 |
+
"defi": 15, # DeFi data every 15 minutes
|
| 45 |
+
"technical": 15, # Technical indicators every 15 minutes
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
# Cache TTL for different data types (in seconds)
|
| 49 |
+
CACHE_TTL = {
|
| 50 |
+
"market": 60, # 1 minute cache for prices
|
| 51 |
+
"news": 300, # 5 minutes cache for news
|
| 52 |
+
"sentiment": 300, # 5 minutes cache for sentiment
|
| 53 |
+
"ohlcv": 60, # 1 minute cache for OHLCV
|
| 54 |
+
"fear_greed": 3600, # 1 hour cache for Fear & Greed
|
| 55 |
+
"whale": 300, # 5 minutes cache for whale alerts
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
# Sources that support real-time fetching (on-demand)
|
| 59 |
+
REALTIME_SOURCES = {
|
| 60 |
+
"binance": ["price", "ohlcv", "trades"],
|
| 61 |
+
"coingecko": ["price", "market"],
|
| 62 |
+
"coincap": ["price", "assets"],
|
| 63 |
+
"cryptocompare": ["price", "ohlcv"],
|
| 64 |
+
"fear_greed": ["index"],
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
# ===== DATA COLLECTORS =====
|
| 69 |
+
|
| 70 |
+
class BaseDataCollector:
|
| 71 |
+
"""Base class for data collectors"""
|
| 72 |
+
|
| 73 |
+
def __init__(self, name: str, interval_minutes: int):
|
| 74 |
+
self.name = name
|
| 75 |
+
self.interval_minutes = interval_minutes
|
| 76 |
+
self.last_run = None
|
| 77 |
+
self.is_running = False
|
| 78 |
+
self.error_count = 0
|
| 79 |
+
self.success_count = 0
|
| 80 |
+
self.timeout = httpx.Timeout(15.0)
|
| 81 |
+
|
| 82 |
+
async def collect(self) -> Dict[str, Any]:
|
| 83 |
+
"""Override in subclass"""
|
| 84 |
+
raise NotImplementedError
|
| 85 |
+
|
| 86 |
+
async def should_run(self) -> bool:
|
| 87 |
+
"""Check if collector should run based on interval"""
|
| 88 |
+
if self.is_running:
|
| 89 |
+
return False
|
| 90 |
+
if self.last_run is None:
|
| 91 |
+
return True
|
| 92 |
+
elapsed = datetime.utcnow() - self.last_run
|
| 93 |
+
return elapsed >= timedelta(minutes=self.interval_minutes)
|
| 94 |
+
|
| 95 |
+
async def run(self) -> Optional[Dict[str, Any]]:
|
| 96 |
+
"""Run collection with error handling"""
|
| 97 |
+
if not await self.should_run():
|
| 98 |
+
return None
|
| 99 |
+
|
| 100 |
+
self.is_running = True
|
| 101 |
+
start_time = time.time()
|
| 102 |
+
|
| 103 |
+
try:
|
| 104 |
+
logger.info(f"[{self.name}] Starting collection...")
|
| 105 |
+
result = await self.collect()
|
| 106 |
+
|
| 107 |
+
elapsed = time.time() - start_time
|
| 108 |
+
self.last_run = datetime.utcnow()
|
| 109 |
+
self.success_count += 1
|
| 110 |
+
self.error_count = 0 # Reset error count on success
|
| 111 |
+
|
| 112 |
+
logger.info(f"[{self.name}] Collection completed in {elapsed:.2f}s")
|
| 113 |
+
return result
|
| 114 |
+
|
| 115 |
+
except Exception as e:
|
| 116 |
+
self.error_count += 1
|
| 117 |
+
logger.error(f"[{self.name}] Collection error: {e}")
|
| 118 |
+
return {"success": False, "error": str(e)}
|
| 119 |
+
|
| 120 |
+
finally:
|
| 121 |
+
self.is_running = False
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
class MarketDataCollector(BaseDataCollector):
|
| 125 |
+
"""Collect market data (prices, market cap, volume)"""
|
| 126 |
+
|
| 127 |
+
COINGECKO_URL = "https://api.coingecko.com/api/v3"
|
| 128 |
+
COINCAP_URL = "https://api.coincap.io/v2"
|
| 129 |
+
|
| 130 |
+
def __init__(self):
|
| 131 |
+
super().__init__("market_data", COLLECTION_INTERVALS["market"])
|
| 132 |
+
self.top_coins = [
|
| 133 |
+
"bitcoin", "ethereum", "binancecoin", "ripple", "cardano",
|
| 134 |
+
"solana", "polkadot", "dogecoin", "polygon", "avalanche"
|
| 135 |
+
]
|
| 136 |
+
|
| 137 |
+
async def collect(self) -> Dict[str, Any]:
|
| 138 |
+
"""Collect market data from multiple sources"""
|
| 139 |
+
results = {"success": True, "data": [], "source": "multi"}
|
| 140 |
+
|
| 141 |
+
# Try CoinGecko first
|
| 142 |
+
try:
|
| 143 |
+
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
| 144 |
+
ids = ",".join(self.top_coins)
|
| 145 |
+
url = f"{self.COINGECKO_URL}/coins/markets"
|
| 146 |
+
params = {
|
| 147 |
+
"vs_currency": "usd",
|
| 148 |
+
"ids": ids,
|
| 149 |
+
"order": "market_cap_desc",
|
| 150 |
+
"per_page": 50,
|
| 151 |
+
"sparkline": False
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
response = await client.get(url, params=params)
|
| 155 |
+
if response.status_code == 200:
|
| 156 |
+
coins = response.json()
|
| 157 |
+
for coin in coins:
|
| 158 |
+
results["data"].append({
|
| 159 |
+
"symbol": coin.get("symbol", "").upper(),
|
| 160 |
+
"name": coin.get("name"),
|
| 161 |
+
"price": coin.get("current_price"),
|
| 162 |
+
"market_cap": coin.get("market_cap"),
|
| 163 |
+
"volume_24h": coin.get("total_volume"),
|
| 164 |
+
"change_24h": coin.get("price_change_percentage_24h"),
|
| 165 |
+
"high_24h": coin.get("high_24h"),
|
| 166 |
+
"low_24h": coin.get("low_24h"),
|
| 167 |
+
"source": "coingecko",
|
| 168 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 169 |
+
})
|
| 170 |
+
results["source"] = "coingecko"
|
| 171 |
+
return results
|
| 172 |
+
except Exception as e:
|
| 173 |
+
logger.warning(f"CoinGecko failed, trying CoinCap: {e}")
|
| 174 |
+
|
| 175 |
+
# Fallback to CoinCap
|
| 176 |
+
try:
|
| 177 |
+
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
| 178 |
+
response = await client.get(f"{self.COINCAP_URL}/assets?limit=50")
|
| 179 |
+
if response.status_code == 200:
|
| 180 |
+
data = response.json()
|
| 181 |
+
for asset in data.get("data", []):
|
| 182 |
+
results["data"].append({
|
| 183 |
+
"symbol": asset.get("symbol", "").upper(),
|
| 184 |
+
"name": asset.get("name"),
|
| 185 |
+
"price": float(asset.get("priceUsd", 0)),
|
| 186 |
+
"market_cap": float(asset.get("marketCapUsd", 0)) if asset.get("marketCapUsd") else None,
|
| 187 |
+
"volume_24h": float(asset.get("volumeUsd24Hr", 0)) if asset.get("volumeUsd24Hr") else None,
|
| 188 |
+
"change_24h": float(asset.get("changePercent24Hr", 0)) if asset.get("changePercent24Hr") else None,
|
| 189 |
+
"source": "coincap",
|
| 190 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 191 |
+
})
|
| 192 |
+
results["source"] = "coincap"
|
| 193 |
+
except Exception as e:
|
| 194 |
+
logger.error(f"CoinCap also failed: {e}")
|
| 195 |
+
results["success"] = False
|
| 196 |
+
results["error"] = str(e)
|
| 197 |
+
|
| 198 |
+
return results
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
class NewsDataCollector(BaseDataCollector):
|
| 202 |
+
"""Collect news from multiple sources"""
|
| 203 |
+
|
| 204 |
+
RSS_FEEDS = {
|
| 205 |
+
"decrypt": "https://decrypt.co/feed",
|
| 206 |
+
"cryptoslate": "https://cryptoslate.com/feed/",
|
| 207 |
+
"bitcoinmagazine": "https://bitcoinmagazine.com/feed",
|
| 208 |
+
"coindesk": "https://www.coindesk.com/arc/outboundfeeds/rss/",
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
CRYPTOCOMPARE_URL = "https://min-api.cryptocompare.com/data/v2/news/"
|
| 212 |
+
|
| 213 |
+
def __init__(self):
|
| 214 |
+
super().__init__("news_data", COLLECTION_INTERVALS["news"])
|
| 215 |
+
|
| 216 |
+
async def collect(self) -> Dict[str, Any]:
|
| 217 |
+
"""Collect news from multiple sources"""
|
| 218 |
+
import feedparser
|
| 219 |
+
|
| 220 |
+
results = {"success": True, "data": [], "sources": []}
|
| 221 |
+
|
| 222 |
+
# Collect from RSS feeds
|
| 223 |
+
for source_name, feed_url in self.RSS_FEEDS.items():
|
| 224 |
+
try:
|
| 225 |
+
loop = asyncio.get_event_loop()
|
| 226 |
+
feed = await loop.run_in_executor(None, feedparser.parse, feed_url)
|
| 227 |
+
|
| 228 |
+
for entry in feed.entries[:10]:
|
| 229 |
+
results["data"].append({
|
| 230 |
+
"title": entry.get("title", ""),
|
| 231 |
+
"link": entry.get("link", ""),
|
| 232 |
+
"published": entry.get("published", ""),
|
| 233 |
+
"summary": entry.get("summary", "")[:300] if entry.get("summary") else "",
|
| 234 |
+
"source": source_name,
|
| 235 |
+
"fetched_at": datetime.utcnow().isoformat()
|
| 236 |
+
})
|
| 237 |
+
results["sources"].append(source_name)
|
| 238 |
+
except Exception as e:
|
| 239 |
+
logger.warning(f"RSS feed {source_name} failed: {e}")
|
| 240 |
+
|
| 241 |
+
# Collect from CryptoCompare
|
| 242 |
+
try:
|
| 243 |
+
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
| 244 |
+
response = await client.get(self.CRYPTOCOMPARE_URL, params={"lang": "EN"})
|
| 245 |
+
if response.status_code == 200:
|
| 246 |
+
data = response.json()
|
| 247 |
+
for article in data.get("Data", [])[:20]:
|
| 248 |
+
results["data"].append({
|
| 249 |
+
"title": article.get("title", ""),
|
| 250 |
+
"link": article.get("url", ""),
|
| 251 |
+
"published": datetime.fromtimestamp(article.get("published_on", 0)).isoformat(),
|
| 252 |
+
"summary": article.get("body", "")[:300] if article.get("body") else "",
|
| 253 |
+
"source": "cryptocompare",
|
| 254 |
+
"fetched_at": datetime.utcnow().isoformat()
|
| 255 |
+
})
|
| 256 |
+
results["sources"].append("cryptocompare")
|
| 257 |
+
except Exception as e:
|
| 258 |
+
logger.warning(f"CryptoCompare news failed: {e}")
|
| 259 |
+
|
| 260 |
+
return results
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
class SentimentDataCollector(BaseDataCollector):
|
| 264 |
+
"""Collect sentiment data"""
|
| 265 |
+
|
| 266 |
+
FEAR_GREED_URL = "https://api.alternative.me/fng/"
|
| 267 |
+
|
| 268 |
+
def __init__(self):
|
| 269 |
+
super().__init__("sentiment_data", COLLECTION_INTERVALS["sentiment"])
|
| 270 |
+
|
| 271 |
+
async def collect(self) -> Dict[str, Any]:
|
| 272 |
+
"""Collect Fear & Greed Index and other sentiment"""
|
| 273 |
+
results = {"success": True, "data": {}, "source": "fear_greed"}
|
| 274 |
+
|
| 275 |
+
try:
|
| 276 |
+
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
| 277 |
+
response = await client.get(f"{self.FEAR_GREED_URL}?limit=30")
|
| 278 |
+
if response.status_code == 200:
|
| 279 |
+
data = response.json()
|
| 280 |
+
fng_data = data.get("data", [])
|
| 281 |
+
|
| 282 |
+
if fng_data:
|
| 283 |
+
latest = fng_data[0]
|
| 284 |
+
results["data"] = {
|
| 285 |
+
"value": int(latest.get("value", 50)),
|
| 286 |
+
"classification": latest.get("value_classification", "Neutral"),
|
| 287 |
+
"timestamp": latest.get("timestamp"),
|
| 288 |
+
"history": [
|
| 289 |
+
{
|
| 290 |
+
"value": int(d.get("value", 50)),
|
| 291 |
+
"classification": d.get("value_classification"),
|
| 292 |
+
"timestamp": d.get("timestamp")
|
| 293 |
+
}
|
| 294 |
+
for d in fng_data[:30]
|
| 295 |
+
]
|
| 296 |
+
}
|
| 297 |
+
except Exception as e:
|
| 298 |
+
logger.error(f"Fear & Greed fetch failed: {e}")
|
| 299 |
+
results["success"] = False
|
| 300 |
+
results["error"] = str(e)
|
| 301 |
+
|
| 302 |
+
return results
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
class OnChainDataCollector(BaseDataCollector):
|
| 306 |
+
"""Collect on-chain data"""
|
| 307 |
+
|
| 308 |
+
BLOCKCHAIR_URL = "https://api.blockchair.com"
|
| 309 |
+
|
| 310 |
+
def __init__(self):
|
| 311 |
+
super().__init__("onchain_data", COLLECTION_INTERVALS["onchain"])
|
| 312 |
+
|
| 313 |
+
async def collect(self) -> Dict[str, Any]:
|
| 314 |
+
"""Collect on-chain statistics"""
|
| 315 |
+
results = {"success": True, "data": {}, "source": "blockchair"}
|
| 316 |
+
|
| 317 |
+
try:
|
| 318 |
+
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
| 319 |
+
# Bitcoin stats
|
| 320 |
+
response = await client.get(f"{self.BLOCKCHAIR_URL}/bitcoin/stats")
|
| 321 |
+
if response.status_code == 200:
|
| 322 |
+
data = response.json()
|
| 323 |
+
results["data"]["bitcoin"] = data.get("data", {})
|
| 324 |
+
|
| 325 |
+
# Ethereum stats
|
| 326 |
+
response = await client.get(f"{self.BLOCKCHAIR_URL}/ethereum/stats")
|
| 327 |
+
if response.status_code == 200:
|
| 328 |
+
data = response.json()
|
| 329 |
+
results["data"]["ethereum"] = data.get("data", {})
|
| 330 |
+
except Exception as e:
|
| 331 |
+
logger.error(f"On-chain data fetch failed: {e}")
|
| 332 |
+
results["success"] = False
|
| 333 |
+
results["error"] = str(e)
|
| 334 |
+
|
| 335 |
+
return results
|
| 336 |
+
|
| 337 |
+
|
| 338 |
+
class DeFiDataCollector(BaseDataCollector):
|
| 339 |
+
"""Collect DeFi data from DefiLlama"""
|
| 340 |
+
|
| 341 |
+
DEFILLAMA_URL = "https://api.llama.fi"
|
| 342 |
+
|
| 343 |
+
def __init__(self):
|
| 344 |
+
super().__init__("defi_data", COLLECTION_INTERVALS["defi"])
|
| 345 |
+
|
| 346 |
+
async def collect(self) -> Dict[str, Any]:
|
| 347 |
+
"""Collect DeFi TVL and protocol data"""
|
| 348 |
+
results = {"success": True, "data": {}, "source": "defillama"}
|
| 349 |
+
|
| 350 |
+
try:
|
| 351 |
+
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
| 352 |
+
# Total TVL
|
| 353 |
+
response = await client.get(f"{self.DEFILLAMA_URL}/tvl")
|
| 354 |
+
if response.status_code == 200:
|
| 355 |
+
results["data"]["total_tvl"] = response.json()
|
| 356 |
+
|
| 357 |
+
# Top protocols
|
| 358 |
+
response = await client.get(f"{self.DEFILLAMA_URL}/protocols")
|
| 359 |
+
if response.status_code == 200:
|
| 360 |
+
protocols = response.json()
|
| 361 |
+
results["data"]["top_protocols"] = protocols[:20] if isinstance(protocols, list) else []
|
| 362 |
+
except Exception as e:
|
| 363 |
+
logger.error(f"DeFi data fetch failed: {e}")
|
| 364 |
+
results["success"] = False
|
| 365 |
+
results["error"] = str(e)
|
| 366 |
+
|
| 367 |
+
return results
|
| 368 |
+
|
| 369 |
+
|
| 370 |
+
# ===== REAL-TIME DATA FETCHER =====
|
| 371 |
+
|
| 372 |
+
class RealTimeDataFetcher:
|
| 373 |
+
"""
|
| 374 |
+
Fetch data in real-time when client requests
|
| 375 |
+
For instant data that shouldn't wait for scheduled collection
|
| 376 |
+
"""
|
| 377 |
+
|
| 378 |
+
def __init__(self):
|
| 379 |
+
self.cache = {} # Simple in-memory cache
|
| 380 |
+
self.timeout = httpx.Timeout(10.0)
|
| 381 |
+
|
| 382 |
+
def _get_cache_key(self, source: str, data_type: str, params: Dict) -> str:
|
| 383 |
+
"""Generate cache key"""
|
| 384 |
+
params_str = "_".join(f"{k}={v}" for k, v in sorted(params.items()))
|
| 385 |
+
return f"{source}_{data_type}_{params_str}"
|
| 386 |
+
|
| 387 |
+
def _is_cache_valid(self, cache_key: str, ttl_seconds: int) -> bool:
|
| 388 |
+
"""Check if cached data is still valid"""
|
| 389 |
+
if cache_key not in self.cache:
|
| 390 |
+
return False
|
| 391 |
+
cached_at = self.cache[cache_key].get("cached_at")
|
| 392 |
+
if not cached_at:
|
| 393 |
+
return False
|
| 394 |
+
return (datetime.utcnow() - cached_at).total_seconds() < ttl_seconds
|
| 395 |
+
|
| 396 |
+
async def fetch_price(self, symbol: str, source: str = "binance") -> Dict[str, Any]:
|
| 397 |
+
"""Fetch real-time price"""
|
| 398 |
+
cache_key = self._get_cache_key(source, "price", {"symbol": symbol})
|
| 399 |
+
ttl = CACHE_TTL.get("market", 60)
|
| 400 |
+
|
| 401 |
+
if self._is_cache_valid(cache_key, ttl):
|
| 402 |
+
return self.cache[cache_key]["data"]
|
| 403 |
+
|
| 404 |
+
try:
|
| 405 |
+
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
| 406 |
+
if source == "binance":
|
| 407 |
+
url = f"https://api.binance.com/api/v3/ticker/price?symbol={symbol}USDT"
|
| 408 |
+
response = await client.get(url)
|
| 409 |
+
if response.status_code == 200:
|
| 410 |
+
data = response.json()
|
| 411 |
+
result = {
|
| 412 |
+
"success": True,
|
| 413 |
+
"symbol": symbol,
|
| 414 |
+
"price": float(data.get("price", 0)),
|
| 415 |
+
"source": "binance",
|
| 416 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 417 |
+
}
|
| 418 |
+
self.cache[cache_key] = {"data": result, "cached_at": datetime.utcnow()}
|
| 419 |
+
return result
|
| 420 |
+
|
| 421 |
+
elif source == "coingecko":
|
| 422 |
+
url = f"https://api.coingecko.com/api/v3/simple/price?ids={symbol.lower()}&vs_currencies=usd"
|
| 423 |
+
response = await client.get(url)
|
| 424 |
+
if response.status_code == 200:
|
| 425 |
+
data = response.json()
|
| 426 |
+
price = data.get(symbol.lower(), {}).get("usd", 0)
|
| 427 |
+
result = {
|
| 428 |
+
"success": True,
|
| 429 |
+
"symbol": symbol,
|
| 430 |
+
"price": price,
|
| 431 |
+
"source": "coingecko",
|
| 432 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 433 |
+
}
|
| 434 |
+
self.cache[cache_key] = {"data": result, "cached_at": datetime.utcnow()}
|
| 435 |
+
return result
|
| 436 |
+
except Exception as e:
|
| 437 |
+
logger.error(f"Real-time price fetch error: {e}")
|
| 438 |
+
|
| 439 |
+
return {"success": False, "error": "Failed to fetch price"}
|
| 440 |
+
|
| 441 |
+
async def fetch_ohlcv(self, symbol: str, interval: str = "1h", limit: int = 100) -> Dict[str, Any]:
|
| 442 |
+
"""Fetch real-time OHLCV data"""
|
| 443 |
+
cache_key = self._get_cache_key("binance", "ohlcv", {"symbol": symbol, "interval": interval})
|
| 444 |
+
ttl = CACHE_TTL.get("ohlcv", 60)
|
| 445 |
+
|
| 446 |
+
if self._is_cache_valid(cache_key, ttl):
|
| 447 |
+
return self.cache[cache_key]["data"]
|
| 448 |
+
|
| 449 |
+
try:
|
| 450 |
+
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
| 451 |
+
url = "https://api.binance.com/api/v3/klines"
|
| 452 |
+
params = {
|
| 453 |
+
"symbol": f"{symbol}USDT",
|
| 454 |
+
"interval": interval,
|
| 455 |
+
"limit": limit
|
| 456 |
+
}
|
| 457 |
+
response = await client.get(url, params=params)
|
| 458 |
+
|
| 459 |
+
if response.status_code == 200:
|
| 460 |
+
klines = response.json()
|
| 461 |
+
ohlcv = []
|
| 462 |
+
for k in klines:
|
| 463 |
+
ohlcv.append({
|
| 464 |
+
"t": k[0], # Open time
|
| 465 |
+
"o": float(k[1]), # Open
|
| 466 |
+
"h": float(k[2]), # High
|
| 467 |
+
"l": float(k[3]), # Low
|
| 468 |
+
"c": float(k[4]), # Close
|
| 469 |
+
"v": float(k[5]), # Volume
|
| 470 |
+
})
|
| 471 |
+
|
| 472 |
+
result = {
|
| 473 |
+
"success": True,
|
| 474 |
+
"symbol": symbol,
|
| 475 |
+
"interval": interval,
|
| 476 |
+
"data": ohlcv,
|
| 477 |
+
"source": "binance",
|
| 478 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 479 |
+
}
|
| 480 |
+
self.cache[cache_key] = {"data": result, "cached_at": datetime.utcnow()}
|
| 481 |
+
return result
|
| 482 |
+
except Exception as e:
|
| 483 |
+
logger.error(f"OHLCV fetch error: {e}")
|
| 484 |
+
|
| 485 |
+
return {"success": False, "error": "Failed to fetch OHLCV"}
|
| 486 |
+
|
| 487 |
+
|
| 488 |
+
# ===== MAIN WORKER =====
|
| 489 |
+
|
| 490 |
+
class DataCollectionWorker:
|
| 491 |
+
"""Main data collection worker managing all collectors"""
|
| 492 |
+
|
| 493 |
+
def __init__(self):
|
| 494 |
+
self.collectors = {
|
| 495 |
+
"market": MarketDataCollector(),
|
| 496 |
+
"news": NewsDataCollector(),
|
| 497 |
+
"sentiment": SentimentDataCollector(),
|
| 498 |
+
"onchain": OnChainDataCollector(),
|
| 499 |
+
"defi": DeFiDataCollector(),
|
| 500 |
+
}
|
| 501 |
+
self.realtime_fetcher = RealTimeDataFetcher()
|
| 502 |
+
self.is_running = False
|
| 503 |
+
self.last_results = {}
|
| 504 |
+
|
| 505 |
+
async def run_all_collectors(self) -> Dict[str, Any]:
|
| 506 |
+
"""Run all collectors that are due"""
|
| 507 |
+
results = {}
|
| 508 |
+
for name, collector in self.collectors.items():
|
| 509 |
+
result = await collector.run()
|
| 510 |
+
if result:
|
| 511 |
+
results[name] = result
|
| 512 |
+
self.last_results[name] = {
|
| 513 |
+
"data": result,
|
| 514 |
+
"collected_at": datetime.utcnow().isoformat()
|
| 515 |
+
}
|
| 516 |
+
return results
|
| 517 |
+
|
| 518 |
+
async def worker_loop(self):
|
| 519 |
+
"""Main worker loop"""
|
| 520 |
+
self.is_running = True
|
| 521 |
+
logger.info("Starting data collection worker...")
|
| 522 |
+
logger.info(f"Collection intervals: {COLLECTION_INTERVALS}")
|
| 523 |
+
|
| 524 |
+
while self.is_running:
|
| 525 |
+
try:
|
| 526 |
+
# Check and run each collector
|
| 527 |
+
for name, collector in self.collectors.items():
|
| 528 |
+
if await collector.should_run():
|
| 529 |
+
result = await collector.run()
|
| 530 |
+
if result:
|
| 531 |
+
self.last_results[name] = {
|
| 532 |
+
"data": result,
|
| 533 |
+
"collected_at": datetime.utcnow().isoformat()
|
| 534 |
+
}
|
| 535 |
+
|
| 536 |
+
# Sleep for 1 minute before checking again
|
| 537 |
+
await asyncio.sleep(60)
|
| 538 |
+
|
| 539 |
+
except Exception as e:
|
| 540 |
+
logger.error(f"Worker loop error: {e}")
|
| 541 |
+
await asyncio.sleep(60)
|
| 542 |
+
|
| 543 |
+
def stop(self):
|
| 544 |
+
"""Stop the worker"""
|
| 545 |
+
self.is_running = False
|
| 546 |
+
logger.info("Stopping data collection worker...")
|
| 547 |
+
|
| 548 |
+
def get_collector_status(self) -> Dict[str, Any]:
|
| 549 |
+
"""Get status of all collectors"""
|
| 550 |
+
return {
|
| 551 |
+
name: {
|
| 552 |
+
"last_run": collector.last_run.isoformat() if collector.last_run else None,
|
| 553 |
+
"interval_minutes": collector.interval_minutes,
|
| 554 |
+
"is_running": collector.is_running,
|
| 555 |
+
"success_count": collector.success_count,
|
| 556 |
+
"error_count": collector.error_count,
|
| 557 |
+
"next_run_in": max(0, collector.interval_minutes * 60 -
|
| 558 |
+
(datetime.utcnow() - collector.last_run).total_seconds())
|
| 559 |
+
if collector.last_run else 0
|
| 560 |
+
}
|
| 561 |
+
for name, collector in self.collectors.items()
|
| 562 |
+
}
|
| 563 |
+
|
| 564 |
+
|
| 565 |
+
# ===== GLOBAL INSTANCES =====
|
| 566 |
+
|
| 567 |
+
_worker = None
|
| 568 |
+
_realtime_fetcher = None
|
| 569 |
+
|
| 570 |
+
|
| 571 |
+
def get_data_collection_worker() -> DataCollectionWorker:
|
| 572 |
+
"""Get global worker instance"""
|
| 573 |
+
global _worker
|
| 574 |
+
if _worker is None:
|
| 575 |
+
_worker = DataCollectionWorker()
|
| 576 |
+
return _worker
|
| 577 |
+
|
| 578 |
+
|
| 579 |
+
def get_realtime_fetcher() -> RealTimeDataFetcher:
|
| 580 |
+
"""Get global real-time fetcher instance"""
|
| 581 |
+
global _realtime_fetcher
|
| 582 |
+
if _realtime_fetcher is None:
|
| 583 |
+
_realtime_fetcher = RealTimeDataFetcher()
|
| 584 |
+
return _realtime_fetcher
|
| 585 |
+
|
| 586 |
+
|
| 587 |
+
async def start_data_collection_worker():
|
| 588 |
+
"""Start the data collection worker"""
|
| 589 |
+
worker = get_data_collection_worker()
|
| 590 |
+
|
| 591 |
+
# Run initial collection
|
| 592 |
+
logger.info("Running initial data collection...")
|
| 593 |
+
await worker.run_all_collectors()
|
| 594 |
+
|
| 595 |
+
# Start background loop
|
| 596 |
+
asyncio.create_task(worker.worker_loop())
|
| 597 |
+
logger.info("Data collection worker started")
|
| 598 |
+
|
| 599 |
+
|
| 600 |
+
# ===== TEST =====
|
| 601 |
+
if __name__ == "__main__":
|
| 602 |
+
async def test():
|
| 603 |
+
print("="*70)
|
| 604 |
+
print("🧪 Testing Data Collection Worker")
|
| 605 |
+
print("="*70)
|
| 606 |
+
|
| 607 |
+
worker = DataCollectionWorker()
|
| 608 |
+
|
| 609 |
+
print("\n📊 Collection Intervals:")
|
| 610 |
+
for data_type, interval in COLLECTION_INTERVALS.items():
|
| 611 |
+
print(f" • {data_type}: {interval} minutes")
|
| 612 |
+
|
| 613 |
+
print("\n🔄 Running all collectors...")
|
| 614 |
+
results = await worker.run_all_collectors()
|
| 615 |
+
|
| 616 |
+
for name, result in results.items():
|
| 617 |
+
if result.get("success"):
|
| 618 |
+
data = result.get("data", {})
|
| 619 |
+
count = len(data) if isinstance(data, list) else "object"
|
| 620 |
+
print(f" ✅ {name}: {count} items")
|
| 621 |
+
else:
|
| 622 |
+
print(f" ❌ {name}: {result.get('error')}")
|
| 623 |
+
|
| 624 |
+
print("\n⚡ Testing Real-time Fetcher...")
|
| 625 |
+
fetcher = RealTimeDataFetcher()
|
| 626 |
+
|
| 627 |
+
price = await fetcher.fetch_price("BTC")
|
| 628 |
+
if price.get("success"):
|
| 629 |
+
print(f" ✅ BTC Price: ${price.get('price')}")
|
| 630 |
+
else:
|
| 631 |
+
print(f" ❌ Price fetch failed: {price.get('error')}")
|
| 632 |
+
|
| 633 |
+
ohlcv = await fetcher.fetch_ohlcv("BTC", "1h", 10)
|
| 634 |
+
if ohlcv.get("success"):
|
| 635 |
+
print(f" ✅ OHLCV: {len(ohlcv.get('data', []))} candles")
|
| 636 |
+
else:
|
| 637 |
+
print(f" ❌ OHLCV fetch failed: {ohlcv.get('error')}")
|
| 638 |
+
|
| 639 |
+
print("\n" + "="*70)
|
| 640 |
+
print("✅ Data Collection Worker Test Complete!")
|
| 641 |
+
print("="*70)
|
| 642 |
+
|
| 643 |
+
asyncio.run(test())
|