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 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
- "explorer": [
3
- {
4
- "name": "Etherscan",
5
- "url": "https://api.etherscan.io/api",
6
- "key": "SZHYFZK2RR8H9TIMJBVW54V4H81K2Z2KR2",
7
- "endpoints": ["?module=account&action=balance&address={address}&apikey={KEY}", "?module=gastracker&action=gasoracle&apikey={KEY}"]
8
- },
9
- {
10
- "name": "Etherscan Backup",
11
- "url": "https://api.etherscan.io/api",
12
- "key": "T6IR8VJHX2NE6ZJW2S3FDVN1TYG4PYYI45",
13
- "endpoints": []
14
- },
15
- {
16
- "name": "BscScan",
17
- "url": "https://api.bscscan.com/api",
18
- "key": "K62RKHGXTDCG53RU4MCG6XABIMJKTN19IT",
19
- "endpoints": ["?module=account&action=balance&address={address}&apikey={KEY}"]
20
- },
21
- {
22
- "name": "TronScan",
23
- "url": "https://apilist.tronscanapi.com/api",
24
- "key": "7ae72726-bffe-4e74-9c33-97b761eeea21",
25
- "endpoints": ["/account?address={address}"]
26
- },
27
- {
28
- "name": "Blockchair ETH",
29
- "url": "https://api.blockchair.com/ethereum/dashboards/address/{address}",
30
- "key": "",
31
- "endpoints": []
32
- },
33
- {
34
- "name": "Ethplorer",
35
- "url": "https://api.ethplorer.io",
36
- "key": "freekey",
37
- "endpoints": ["/getAddressInfo/{address}?apiKey=freekey"]
38
- },
39
- {
40
- "name": "TronGrid",
41
- "url": "https://api.trongrid.io",
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
- "market": [
59
- {
60
- "name": "CoinGecko",
61
- "url": "https://api.coingecko.com/api/v3",
62
- "key": "",
63
- "endpoints": ["/simple/price?ids=bitcoin,ethereum&vs_currencies=usd", "/coins/markets?vs_currency=usd&per_page=100"]
64
- },
65
- {
66
- "name": "CoinMarketCap",
67
- "url": "https://pro-api.coinmarketcap.com/v1",
68
- "key": "04cf4b5b-9868-465c-8ba0-9f2e78c92eb1",
69
- "endpoints": ["/cryptocurrency/quotes/latest?symbol=BTC&convert=USD"]
70
- },
71
- {
72
- "name": "CoinMarketCap Alt",
73
- "url": "https://pro-api.coinmarketcap.com/v1",
74
- "key": "b54bcf4d-1bca-4e8e-9a24-22ff2c3d462c",
75
- "endpoints": []
76
- },
77
- {
78
- "name": "CryptoCompare",
79
- "url": "https://min-api.cryptocompare.com/data",
80
- "key": "e79c8e6d4c5b4a3f2e1d0c9b8a7f6e5d4c3b2a1f",
81
- "endpoints": ["/pricemulti?fsyms=BTC,ETH&tsyms=USD"]
82
- },
83
- {
84
- "name": "CoinPaprika",
85
- "url": "https://api.coinpaprika.com/v1",
86
- "key": "",
87
- "endpoints": ["/tickers", "/coins"]
88
- },
89
- {
90
- "name": "CoinCap",
91
- "url": "https://api.coincap.io/v2",
92
- "key": "",
93
- "endpoints": ["/assets", "/assets/bitcoin"]
94
- },
95
- {
96
- "name": "Binance",
97
- "url": "https://api.binance.com/api/v3",
98
- "key": "",
99
- "endpoints": ["/ticker/price?symbol=BTCUSDT"]
100
- },
101
- {
102
- "name": "CoinDesk",
103
- "url": "https://api.coindesk.com/v1",
104
- "key": "",
105
- "endpoints": ["/bpi/currentprice.json"]
106
- },
107
- {
108
- "name": "Nomics",
109
- "url": "https://api.nomics.com/v1",
110
- "key": "",
111
- "endpoints": []
112
- },
113
- {
114
- "name": "Messari",
115
- "url": "https://data.messari.io/api/v1",
116
- "key": "",
117
- "endpoints": ["/assets/bitcoin/metrics"]
118
- },
119
- {
120
- "name": "CoinLore",
121
- "url": "https://api.coinlore.net/api",
122
- "key": "",
123
- "endpoints": ["/tickers/"]
124
- },
125
- {
126
- "name": "CoinStats",
127
- "url": "https://api.coinstats.app/public/v1",
128
- "key": "",
129
- "endpoints": ["/coins"]
130
- },
131
- {
132
- "name": "Mobula",
133
- "url": "https://api.mobula.io/api/1",
134
- "key": "",
135
- "endpoints": []
136
- },
137
- {
138
- "name": "TokenMetrics",
139
- "url": "https://api.tokenmetrics.com/v2",
140
- "key": "",
141
- "endpoints": []
142
- },
143
- {
144
- "name": "DIA Data",
145
- "url": "https://api.diadata.org/v1",
146
- "key": "",
147
- "endpoints": []
148
- }
149
- ],
150
- "news": [
151
- {
152
- "name": "CryptoPanic",
153
- "url": "https://cryptopanic.com/api/v1",
154
- "key": "",
155
- "endpoints": ["/posts/?auth_token={KEY}"]
156
- },
157
- {
158
- "name": "NewsAPI",
159
- "url": "https://newsapi.org/v2",
160
- "key": "pub_346789abc123def456789ghi012345jkl",
161
- "endpoints": ["/everything?q=crypto&apiKey={KEY}"]
162
- },
163
- {
164
- "name": "CryptoControl",
165
- "url": "https://cryptocontrol.io/api/v1/public",
166
- "key": "",
167
- "endpoints": ["/news/local?language=EN"]
168
- },
169
- {
170
- "name": "CoinDesk RSS",
171
- "url": "https://www.coindesk.com/arc/outboundfeeds/rss/",
172
- "key": "",
173
- "endpoints": []
174
- },
175
- {
176
- "name": "CoinTelegraph",
177
- "url": "https://cointelegraph.com/api/v1",
178
- "key": "",
179
- "endpoints": []
180
- },
181
- {
182
- "name": "CryptoSlate",
183
- "url": "https://cryptoslate.com/api",
184
- "key": "",
185
- "endpoints": []
186
- },
187
- {
188
- "name": "The Block",
189
- "url": "https://api.theblock.co/v1",
190
- "key": "",
191
- "endpoints": []
192
- },
193
- {
194
- "name": "Bitcoin Magazine",
195
- "url": "https://bitcoinmagazine.com/.rss/full/",
196
- "key": "",
197
- "endpoints": []
198
- },
199
- {
200
- "name": "Decrypt",
201
- "url": "https://decrypt.co/feed",
202
- "key": "",
203
- "endpoints": []
204
- },
205
- {
206
- "name": "Reddit Crypto",
207
- "url": "https://www.reddit.com/r/CryptoCurrency/new.json",
208
- "key": "",
209
- "endpoints": []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
  }
211
- ],
212
- "sentiment": [
213
- {
214
- "name": "Fear & Greed",
215
- "url": "https://api.alternative.me/fng/",
216
- "key": "",
217
- "endpoints": ["?limit=1", "?limit=30"]
218
- },
219
- {
220
- "name": "LunarCrush",
221
- "url": "https://api.lunarcrush.com/v2",
222
- "key": "",
223
- "endpoints": ["?data=assets&key={KEY}"]
224
- },
225
- {
226
- "name": "Santiment",
227
- "url": "https://api.santiment.net/graphql",
228
- "key": "",
229
- "endpoints": []
230
- },
231
- {
232
- "name": "The TIE",
233
- "url": "https://api.thetie.io",
234
- "key": "",
235
- "endpoints": []
236
- },
237
- {
238
- "name": "CryptoQuant",
239
- "url": "https://api.cryptoquant.com/v1",
240
- "key": "",
241
- "endpoints": []
242
- },
243
- {
244
- "name": "Glassnode Social",
245
- "url": "https://api.glassnode.com/v1/metrics/social",
246
- "key": "",
247
- "endpoints": []
248
- },
249
- {
250
- "name": "Augmento",
251
- "url": "https://api.augmento.ai/v1",
252
- "key": "",
253
- "endpoints": []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
  }
255
- ],
256
- "analytics": [
257
- {
258
- "name": "Whale Alert",
259
- "url": "https://api.whale-alert.io/v1",
260
- "key": "",
261
- "endpoints": ["/transactions?api_key={KEY}&min_value=1000000"]
262
- },
263
- {
264
- "name": "Nansen",
265
- "url": "https://api.nansen.ai/v1",
266
- "key": "",
267
- "endpoints": []
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: 'Market Data API',
18
  category: 'Core Services',
19
- description: 'Real-time cryptocurrency market data',
20
  endpoints: [
21
- { method: 'GET', path: '/api/market/top', desc: 'Top cryptocurrencies' },
22
- { method: 'GET', path: '/api/market/trending', desc: 'Trending coins' },
23
- { method: 'GET', path: '/api/coins/top?limit=50', desc: 'Top coins with limit' }
 
 
 
 
 
24
  ],
25
- example: `fetch('${baseUrl}/api/market/top')
 
 
 
 
 
 
26
  .then(res => res.json())
27
- .then(data => console.log(data));`
28
  },
 
 
29
  {
30
- name: 'Sentiment Analysis API',
31
- category: 'AI Services',
32
- description: 'AI-powered sentiment analysis',
33
  endpoints: [
34
- { method: 'GET', path: '/api/sentiment/global', desc: 'Global market sentiment' },
35
- { method: 'GET', path: '/api/sentiment/asset/{symbol}', desc: 'Asset sentiment' },
36
- { method: 'POST', path: '/api/sentiment/analyze', desc: 'Analyze custom text' }
 
 
 
37
  ],
38
- example: `fetch('${baseUrl}/api/sentiment/global')
 
39
  .then(res => res.json())
40
- .then(data => console.log(data));`
 
 
 
41
  },
 
 
42
  {
43
  name: 'News Aggregator API',
44
- category: 'Data Services',
45
- description: 'Crypto news from multiple sources',
46
  endpoints: [
47
- { method: 'GET', path: '/api/news', desc: 'Latest crypto news' },
48
- { method: 'GET', path: '/api/news/latest?limit=10', desc: 'News with limit' },
49
- { method: 'GET', path: '/api/news?source=CoinDesk', desc: 'Filter by source' }
50
  ],
51
- example: `fetch('${baseUrl}/api/news?limit=10')
 
52
  .then(res => res.json())
53
- .then(data => console.log(data));`
 
 
 
 
54
  },
 
 
55
  {
56
- name: 'OHLCV Data API',
57
- category: 'Trading Data',
58
- description: 'Historical price data (OHLCV)',
59
  endpoints: [
60
- { method: 'GET', path: '/api/ohlcv/{symbol}', desc: 'OHLCV for symbol' },
61
- { method: 'GET', path: '/api/ohlcv/multi', desc: 'Multiple symbols' },
62
- { method: 'GET', path: '/api/market/ohlc?symbol=BTC', desc: 'OHLC data' }
 
63
  ],
64
- example: `fetch('${baseUrl}/api/ohlcv/bitcoin')
 
 
 
 
 
 
 
 
 
 
 
 
65
  .then(res => res.json())
66
- .then(data => console.log(data));`
67
  },
 
 
68
  {
69
- name: 'AI Models API',
70
- category: 'AI Services',
71
- description: 'AI model management and status',
72
  endpoints: [
73
- { method: 'GET', path: '/api/models/status', desc: 'Models status' },
74
- { method: 'GET', path: '/api/models/list', desc: 'List all models' },
75
- { method: 'GET', path: '/api/ai/signals', desc: 'AI trading signals' }
 
76
  ],
77
- example: `fetch('${baseUrl}/api/models/status')
 
78
  .then(res => res.json())
79
- .then(data => console.log(data));`
 
 
 
 
80
  },
 
 
81
  {
82
- name: 'Trading & Backtesting API',
83
- category: 'Trading Services',
84
- description: 'Smart trading and backtesting',
85
  endpoints: [
86
- { method: 'GET', path: '/api/trading/backtest', desc: 'Backtest strategy' },
87
- { method: 'GET', path: '/api/futures/positions', desc: 'Futures positions' },
88
- { method: 'POST', path: '/api/ai/decision', desc: 'AI trading decision' }
 
 
89
  ],
90
- example: `fetch('${baseUrl}/api/trading/backtest?symbol=BTC')
 
 
 
 
 
 
 
 
 
 
 
 
91
  .then(res => res.json())
92
- .then(data => console.log(data));`
 
 
 
 
93
  },
 
 
94
  {
95
- name: 'Multi-Source Fallback API',
96
- category: 'Advanced Services',
97
- description: '137+ data sources with fallback',
98
  endpoints: [
99
- { method: 'GET', path: '/api/multi-source/data/{symbol}', desc: 'Multi-source data' },
100
- { method: 'GET', path: '/api/sources/all', desc: 'All sources' },
101
- { method: 'GET', path: '/api/test-source/{source_id}', desc: 'Test source' }
 
 
102
  ],
103
- example: `fetch('${baseUrl}/api/sources/all')
 
 
 
 
 
104
  .then(res => res.json())
105
- .then(data => console.log(data));`
 
 
 
 
106
  },
 
 
107
  {
108
- name: 'Technical Analysis API',
109
- category: 'Analysis Services',
110
- description: 'Technical indicators and analysis',
111
  endpoints: [
112
- { method: 'GET', path: '/api/technical/quick/{symbol}', desc: 'Quick TA' },
113
- { method: 'GET', path: '/api/technical/comprehensive/{symbol}', desc: 'Full analysis' },
114
- { method: 'GET', path: '/api/technical/risk/{symbol}', desc: 'Risk assessment' }
115
  ],
116
- example: `fetch('${baseUrl}/api/technical/quick/bitcoin')
 
117
  .then(res => res.json())
118
- .then(data => console.log(data));`
 
 
 
 
119
  },
 
 
120
  {
121
- name: 'Resources API',
122
  category: 'System Services',
123
- description: 'API resources and statistics',
124
  endpoints: [
 
 
125
  { method: 'GET', path: '/api/resources/summary', desc: 'Resources summary' },
126
- { method: 'GET', path: '/api/resources/stats', desc: 'Detailed stats' },
127
- { method: 'GET', path: '/api/resources/apis', desc: 'All APIs list' }
 
128
  ],
129
- example: `fetch('${baseUrl}/api/resources/summary')
 
 
 
 
 
 
 
 
 
130
  .then(res => res.json())
131
- .then(data => console.log(data));`
 
 
 
132
  },
 
 
133
  {
134
- name: 'Real-Time Monitoring API',
135
- category: 'System Services',
136
- description: 'System monitoring and health',
137
  endpoints: [
138
- { method: 'GET', path: '/api/health', desc: 'Health check' },
139
- { method: 'GET', path: '/api/status', desc: 'System status' },
140
- { method: 'GET', path: '/api/monitoring/status', desc: 'Monitoring data' }
 
141
  ],
142
- example: `fetch('${baseUrl}/api/health')
143
- .then(res => res.json())
144
- .then(data => console.log(data));`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 configurations to use our services in your application.</p>
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())