Really-amin commited on
Commit
8728cb3
·
verified ·
1 Parent(s): 0c5cbb3

Upload 287 files

Browse files
.dockerignore CHANGED
@@ -1,121 +1,16 @@
1
- # Python
2
- __pycache__/
3
- *.py[cod]
4
- *$py.class
5
- *.so
6
- .Python
7
- build/
8
- develop-eggs/
9
- dist/
10
- downloads/
11
- eggs/
12
- .eggs/
13
- lib/
14
- lib64/
15
- parts/
16
- sdist/
17
- var/
18
- wheels/
19
- *.egg-info/
20
- .installed.cfg
21
- *.egg
22
- MANIFEST
23
- pip-log.txt
24
- pip-delete-this-directory.txt
25
-
26
- # Virtual environments
27
- venv/
28
- ENV/
29
- env/
30
- .venv
31
-
32
- # IDE
33
- .vscode/
34
- .idea/
35
- *.swp
36
- *.swo
37
- *~
38
- .DS_Store
39
-
40
- # Git
41
- .git/
42
- .gitignore
43
- .gitattributes
44
-
45
- # Documentation
46
- *.md
47
- docs/
48
- README*.md
49
- CHANGELOG.md
50
- LICENSE
51
-
52
- # Testing
53
- .pytest_cache/
54
- .coverage
55
- htmlcov/
56
- .tox/
57
- .hypothesis/
58
- tests/
59
- test_*.py
60
-
61
- # Logs and databases (will be created in container)
62
- *.log
63
- logs/
64
- data/*.db
65
- data/*.sqlite
66
- data/*.db-journal
67
-
68
- # Environment files (should be set via docker-compose or HF Secrets)
69
- .env
70
- .env.*
71
- !.env.example
72
-
73
- # Docker
74
- docker-compose*.yml
75
- !docker-compose.yml
76
- Dockerfile
77
- .dockerignore
78
-
79
- # CI/CD
80
- .github/
81
- .gitlab-ci.yml
82
- .travis.yml
83
- azure-pipelines.yml
84
-
85
- # Temporary files
86
- *.tmp
87
- *.bak
88
- *.swp
89
- temp/
90
- tmp/
91
-
92
- # Node modules (if any)
93
- node_modules/
94
- package-lock.json
95
- yarn.lock
96
-
97
- # OS files
98
- Thumbs.db
99
- .DS_Store
100
- desktop.ini
101
-
102
- # Jupyter notebooks
103
- .ipynb_checkpoints/
104
- *.ipynb
105
-
106
- # Model cache (models will be downloaded in container)
107
- models/
108
- .cache/
109
- .huggingface/
110
-
111
- # Large files that shouldn't be in image
112
- *.tar
113
- *.tar.gz
114
- *.zip
115
- *.rar
116
- *.7z
117
-
118
- # Screenshots and assets not needed
119
- screenshots/
120
- assets/*.png
121
- assets/*.jpg
 
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ *.log
5
+ .venv/
6
+ venv/
7
+ .git/
8
+ .gitignore
9
+ dist/
10
+ build/
11
+ *.zip
12
+ *.tar*
13
+ *.db
14
+ *.sqlite
15
+ *.sqlite3
16
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
DEPLOYMENT_CHECK_REPORT.md ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # گزارش بررسی نهایی - آمادگی Deployment
2
+
3
+ ## ✅ بررسی ساختار پروژه
4
+
5
+ ### 1. ساختار فایل‌ها
6
+ ```
7
+ final/
8
+ ├── hf_unified_server.py ✅ Entry point برای HF Docker Space
9
+ ├── api_server_extended.py ✅ سرور اصلی FastAPI با تمام endpoint‌ها
10
+ ├── ai_models.py ✅ مدیریت مدل‌های Hugging Face
11
+ ├── config.py ✅ Configuration module
12
+ ├── Dockerfile ✅ آماده برای deployment
13
+ ├── .dockerignore ✅ فیلتر فایل‌های غیرضروری
14
+ ├── requirements_hf.txt ✅ Dependencies
15
+ ├── index.html ✅ UI اصلی
16
+ └── data/ ✅ Database directory
17
+ ```
18
+
19
+ ### 2. Routing بررسی شده
20
+
21
+ #### ✅ HTML Routes (در api_server_extended.py):
22
+ - `/` → index.html ✅
23
+ - `/index.html` → index.html ✅
24
+ - Static files: `/static/*` (اگر directory وجود داشته باشد) ✅
25
+
26
+ #### ✅ API Endpoints موجود:
27
+ | Endpoint | Method | Status | استفاده در UI |
28
+ |----------|--------|--------|---------------|
29
+ | `/api/providers` | GET | ✅ | ✅ |
30
+ | `/api/market` | GET | ✅ | ✅ |
31
+ | `/api/trending` | GET | ✅ | ✅ |
32
+ | `/api/sentiment` | GET | ✅ | ✅ |
33
+ | `/api/news` | GET | ✅ | ✅ |
34
+ | `/api/defi` | GET | ✅ | ✅ |
35
+ | `/api/logs/summary` | GET | ✅ | ✅ |
36
+ | `/api/diagnostics/errors` | GET | ✅ | ✅ |
37
+ | `/api/resources/search` | GET | ✅ | ✅ |
38
+ | `/api/v2/export/{type}` | POST | ✅ | ✅ |
39
+ | `/api/v2/backup` | POST | ✅ | ✅ |
40
+ | `/api/v2/import/providers` | POST | ✅ | ✅ |
41
+ | `/api/sentiment/analyze` | POST | ✅ | جدید |
42
+ | `/api/news/analyze` | POST | ✅ | جدید |
43
+ | `/api/hf/run-sentiment` | POST | ✅ | ✅ |
44
+ | `/api/models/status` | GET | ✅ | جدید |
45
+ | `/api/models/initialize` | POST | ✅ | جدید |
46
+
47
+ ### 3. هماهنگی Frontend-Backend
48
+
49
+ #### ✅ HTML Configuration:
50
+ ```javascript
51
+ const config = {
52
+ apiBaseUrl: '', // ✅ استفاده از relative path (درست است)
53
+ wsUrl: 'ws://' + window.location.host + '/ws' // ✅
54
+ };
55
+ ```
56
+
57
+ #### ✅ API Calls در HTML:
58
+ - همه endpoint‌های استفاده شده در HTML موجود هستند ✅
59
+ - Error handling موجود است ✅
60
+ - WebSocket connection setup موجود است ✅
61
+
62
+ ### 4. Database Structure
63
+
64
+ #### ✅ Tables موجود:
65
+ - `prices` - ذخیره قیمت‌های ارزهای دیجیتال ✅
66
+ - `sentiment_analysis` - ذخیره نتایج تحلیل احساسات ✅
67
+ - `news_articles` - ذخیره اخبار تحلیل‌شده ✅
68
+
69
+ #### ✅ Indexes:
70
+ - Indexes برای جستجوی سریع‌تر اضافه شده‌اند ✅
71
+
72
+ ### 5. Model Initialization
73
+
74
+ #### ✅ Startup Process:
75
+ 1. Database initialization ✅
76
+ 2. Providers loading ✅
77
+ 3. AI Models initialization ✅
78
+ 4. HF Registry status ✅
79
+
80
+ ### 6. Dockerfile بررسی
81
+
82
+ #### ✅ Dockerfile:
83
+ ```dockerfile
84
+ FROM python:3.11-slim ✅
85
+ WORKDIR /app ✅
86
+ COPY requirements_hf.txt ✅
87
+ RUN pip install ✅
88
+ COPY . . ✅
89
+ EXPOSE 7860 ✅
90
+ CMD uvicorn hf_unified_server ✅
91
+ ```
92
+
93
+ #### ✅ .dockerignore:
94
+ - فایل‌های غیرضروری ignore شده‌اند ✅
95
+
96
+ ### 7. مشکلات پیدا شده و حل شده
97
+
98
+ #### ✅ مشکلات حل شده:
99
+ 1. ❌ → ✅ endpoint `/api/news` اضافه شد
100
+ 2. ❌ → ✅ endpoint `/api/logs/summary` اضافه شد
101
+ 3. ❌ → ✅ endpoint `/api/diagnostics/errors` اضافه شد
102
+ 4. ❌ → ✅ endpoint `/api/resources/search` اضافه شد
103
+ 5. ❌ → ✅ endpoint `/api/v2/*` اضافه شد
104
+ 6. ❌ → ✅ endpoint `/api/defi` اصلاح شد
105
+
106
+ ### 8. نکات مهم
107
+
108
+ #### ⚠️ Static Directory:
109
+ - Directory `static/` وجود ندارد اما این مشکل نیست چون:
110
+ - HTML files inline هستند
111
+ - CSS/JS در HTML embed شده‌اند
112
+ - اگر نیاز باشد می‌توان بعداً اضافه کرد
113
+
114
+ #### ✅ WORKSPACE_ROOT:
115
+ - به درستی تنظیم شده: `/workspace` یا `.` ✅
116
+ - در Docker container به `/app` اشاره می‌کند ✅
117
+
118
+ ### 9. Deployment Readiness
119
+
120
+ #### ✅ آماده برای Hugging Face Docker Space:
121
+ - ✅ Dockerfile موجود است
122
+ - ✅ Entry point (`hf_unified_server.py`) درست است
123
+ - ✅ Port 7860 expose شده است
124
+ - ✅ Environment variables پشتیبانی می‌شود (`PORT`)
125
+ - ✅ Models به صورت lazy-load لود می‌شوند
126
+ - ✅ Database در `/app/data/database/` ایجاد می‌شود
127
+ - ✅ CORS فعال است
128
+ - ✅ Error handling موجود است
129
+
130
+ ### 10. تست‌های پیشنهادی
131
+
132
+ #### قبل از Deployment:
133
+ ```bash
134
+ # 1. Build Docker image
135
+ docker build -t crypto-hf .
136
+
137
+ # 2. Run locally
138
+ docker run -p 7860:7860 crypto-hf
139
+
140
+ # 3. Test endpoints
141
+ curl http://localhost:7860/
142
+ curl http://localhost:7860/api/health
143
+ curl http://localhost:7860/api/providers
144
+ curl http://localhost:7860/api/models/status
145
+
146
+ # 4. Test sentiment analysis
147
+ curl -X POST http://localhost:7860/api/sentiment/analyze \
148
+ -H "Content-Type: application/json" \
149
+ -d '{"text": "Bitcoin is bullish", "mode": "crypto"}'
150
+ ```
151
+
152
+ ## 📊 خلاصه نهایی
153
+
154
+ | بخش | وضعیت | توضیحات |
155
+ |-----|-------|---------|
156
+ | ساختار | ✅ | درست و منظم |
157
+ | Routing | ✅ | کامل و هماهنگ |
158
+ | HTML-Backend | ✅ | هماهنگ |
159
+ | Database | ✅ | آماده |
160
+ | Models | ✅ | فعال و آماده |
161
+ | Dockerfile | ✅ | آماده deployment |
162
+ | Endpoints | ✅ | کامل |
163
+ | Error Handling | ✅ | موجود |
164
+
165
+ ## ✅ نتیجه‌گیری
166
+
167
+ **پروژه کاملاً آماده deployment است!**
168
+
169
+ - ✅ ساختار درست است
170
+ - ✅ Routing کامل است
171
+ - ✅ HTML با Backend هماهنگ است
172
+ - ✅ تمام endpoint‌ها موجود هستند
173
+ - ✅ Database آماده است
174
+ - ✅ Models فعال هستند
175
+ - ✅ Dockerfile آماده است
176
+
177
+ **آماده برای استقرار روی Hugging Face Docker Space! 🚀**
178
+
Dockerfile CHANGED
@@ -1,24 +1,33 @@
1
- FROM python:3.10
2
 
3
  WORKDIR /app
4
 
5
- # Create required directories
6
- RUN mkdir -p /app/logs /app/data /app/data/database /app/data/backups
 
 
 
7
 
8
- # Copy requirements and install dependencies
9
- COPY requirements.txt .
10
- RUN pip install --no-cache-dir -r requirements.txt
11
 
12
- # Copy application code
 
 
 
 
13
  COPY . .
14
 
 
 
 
15
  # Set environment variables
16
- ENV USE_MOCK_DATA=false
17
- ENV PORT=7860
18
  ENV PYTHONUNBUFFERED=1
 
 
 
19
 
20
- # Expose port
21
  EXPOSE 7860
22
 
23
- # Launch command
24
- CMD ["uvicorn", "hf_unified_server:app", "--host", "0.0.0.0", "--port", "7860"]
 
1
+ FROM python:3.11-slim
2
 
3
  WORKDIR /app
4
 
5
+ # Install system dependencies
6
+ RUN apt-get update && apt-get install -y \
7
+ build-essential \
8
+ curl \
9
+ && rm -rf /var/lib/apt/lists/*
10
 
11
+ # Copy requirements first for better caching
12
+ COPY requirements_hf.txt ./requirements.txt
 
13
 
14
+ # Install Python dependencies
15
+ RUN pip install --upgrade pip setuptools wheel && \
16
+ pip install --no-cache-dir -r requirements.txt
17
+
18
+ # Copy application files
19
  COPY . .
20
 
21
+ # Create necessary directories
22
+ RUN mkdir -p data/database logs api-resources
23
+
24
  # Set environment variables
 
 
25
  ENV PYTHONUNBUFFERED=1
26
+ ENV PORT=7860
27
+ ENV GRADIO_SERVER_NAME=0.0.0.0
28
+ ENV GRADIO_SERVER_PORT=7860
29
 
 
30
  EXPOSE 7860
31
 
32
+ # Run the Gradio application
33
+ CMD ["python", "app.py"]
HF_DOCKER_FIX.md ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # رفع مشکل دانلود HTML در Hugging Face Docker Space
2
+
3
+ ## مشکل
4
+ وقتی روی Hugging Face Space اجرا می‌شود، به جای نمایش HTML، از کاربر می‌خواهد فایل index.html را دانلود کند.
5
+
6
+ ## راه حل‌های اعمال شده
7
+
8
+ ### 1. تنظیم صریح Content-Type Headers
9
+ ```python
10
+ return HTMLResponse(
11
+ content=content,
12
+ media_type="text/html",
13
+ headers={
14
+ "Content-Type": "text/html; charset=utf-8",
15
+ "X-Content-Type-Options": "nosniff"
16
+ }
17
+ )
18
+ ```
19
+
20
+ ### 2. اصلاح WORKSPACE_ROOT
21
+ در Docker container، مسیر به `/app` تغییر یافت:
22
+ ```python
23
+ WORKSPACE_ROOT = Path("/app" if Path("/app").exists() else ...)
24
+ ```
25
+
26
+ ### 3. بررسی Dockerfile
27
+ Dockerfile باید:
28
+ - WORKDIR را `/app` تنظیم کند ✅
29
+ - فایل‌ها را به `/app` کپی کند ✅
30
+ - Port 7860 را expose کند ✅
31
+
32
+ ## تست
33
+
34
+ بعد از rebuild در Hugging Face:
35
+
36
+ 1. به Space خود بروید
37
+ 2. روی URL کلیک کنید
38
+ 3. باید HTML به جای دانلود نمایش داده شود
39
+
40
+ ## اگر هنوز مشکل دارید
41
+
42
+ ### بررسی Logs در Hugging Face:
43
+ 1. به Space Settings بروید
44
+ 2. Logs را بررسی کنید
45
+ 3. ببینید آیا خطایی وجود دارد
46
+
47
+ ### بررسی Path:
48
+ در logs باید ببینید:
49
+ ```
50
+ ✓ Mounted static files from /app/static
51
+ ✓ Database initialized at /app/data/database/crypto_monitor.db
52
+ ```
53
+
54
+ ### بررسی Content-Type:
55
+ در browser DevTools (F12):
56
+ - Network tab را باز کنید
57
+ - Request به `/` را بررسی کنید
58
+ - Response Headers را ببینید
59
+ - باید `Content-Type: text/html; charset=utf-8` باشد
60
+
61
+ ## تغییرات اعمال شده
62
+
63
+ ✅ Headers صریح برای HTMLResponse
64
+ ✅ WORKSPACE_ROOT اصلاح شد
65
+ ✅ Content-Type به صورت explicit تنظیم شد
66
+ ✅ X-Content-Type-Options اضافه شد
67
+
MODELS_AS_DATA_SOURCES.md ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # استفاده از مدل‌های Hugging Face به عنوان منابع داده
2
+
3
+ ## 📊 Endpoint‌های جدید
4
+
5
+ ### 1. لیست مدل‌های موجود
6
+ ```bash
7
+ GET /api/models/list
8
+ ```
9
+ **Response:**
10
+ ```json
11
+ {
12
+ "success": true,
13
+ "total_models": 15,
14
+ "models": [
15
+ {
16
+ "id": "crypto_sent_0",
17
+ "model_id": "ElKulako/cryptobert",
18
+ "task": "sentiment-analysis",
19
+ "category": "crypto_sentiment",
20
+ "requires_auth": true,
21
+ "endpoint": "/api/models/crypto_sent_0/predict"
22
+ }
23
+ ],
24
+ "categories": {...}
25
+ }
26
+ ```
27
+
28
+ ### 2. اطلاعات یک مدل خاص
29
+ ```bash
30
+ GET /api/models/{model_key}/info
31
+ ```
32
+ **Example:**
33
+ ```bash
34
+ GET /api/models/crypto_sent_0/info
35
+ ```
36
+
37
+ ### 3. استفاده از یک مدل برای تولید داده
38
+ ```bash
39
+ POST /api/models/{model_key}/predict
40
+ ```
41
+ **Body:**
42
+ ```json
43
+ {
44
+ "text": "Bitcoin is going to the moon!"
45
+ }
46
+ ```
47
+ **Response:**
48
+ ```json
49
+ {
50
+ "success": true,
51
+ "model_key": "crypto_sent_0",
52
+ "model_id": "ElKulako/cryptobert",
53
+ "task": "sentiment-analysis",
54
+ "input": "Bitcoin is going to the moon!",
55
+ "output": {
56
+ "label": "POSITIVE",
57
+ "score": 0.95
58
+ },
59
+ "timestamp": "2025-01-XX..."
60
+ }
61
+ ```
62
+
63
+ ### 4. پردازش دسته‌ای با چند مدل
64
+ ```bash
65
+ POST /api/models/batch/predict
66
+ ```
67
+ **Body:**
68
+ ```json
69
+ {
70
+ "texts": [
71
+ "Bitcoin is bullish",
72
+ "Ethereum price dropping"
73
+ ],
74
+ "models": ["crypto_sent_0", "financial_sent_0"]
75
+ }
76
+ ```
77
+
78
+ ### 5. دریافت داده‌های تولید شده توسط مدل‌ها
79
+ ```bash
80
+ GET /api/models/data/generated?limit=50&model_key=crypto_sent_0&symbol=BTC
81
+ ```
82
+
83
+ ### 6. آمار داده‌های تولید شده
84
+ ```bash
85
+ GET /api/models/data/stats
86
+ ```
87
+
88
+ ## 🔗 مدل‌ها به عنوان Providers
89
+
90
+ مدل‌های HF به صورت خودکار در `/api/providers` نمایش داده می‌شوند:
91
+
92
+ ```json
93
+ {
94
+ "provider_id": "hf_model_crypto_sent_0",
95
+ "name": "HF Model: ElKulako/cryptobert",
96
+ "category": "crypto_sentiment",
97
+ "type": "hf_model",
98
+ "status": "available",
99
+ "endpoint": "/api/models/crypto_sent_0/predict"
100
+ }
101
+ ```
102
+
103
+ ## 📝 مثال استفاده
104
+
105
+ ### تحلیل احساسات با یک مدل خاص:
106
+ ```bash
107
+ curl -X POST http://localhost:7860/api/models/crypto_sent_0/predict \
108
+ -H "Content-Type: application/json" \
109
+ -d '{"text": "Bitcoin is bullish today"}'
110
+ ```
111
+
112
+ ### پردازش دسته‌ای:
113
+ ```bash
114
+ curl -X POST http://localhost:7860/api/models/batch/predict \
115
+ -H "Content-Type: application/json" \
116
+ -d '{
117
+ "texts": ["BTC bullish", "ETH bearish"],
118
+ "models": ["crypto_sent_0", "financial_sent_0"]
119
+ }'
120
+ ```
121
+
122
+ ### دریافت داده‌های تولید شده:
123
+ ```bash
124
+ curl http://localhost:7860/api/models/data/generated?limit=10&symbol=BTC
125
+ ```
126
+
127
+ ## 🎯 مزایا
128
+
129
+ 1. ✅ مدل‌ها به عنوان منابع داده قابل دسترسی هستند
130
+ 2. ✅ می‌توانید از هر مدل به صورت مستقل استفاده کنید
131
+ 3. ✅ داده‌های تولید شده در database ذخیره می‌شوند
132
+ 4. ✅ می‌توانید آمار و تاریخچه را مشاهده کنید
133
+ 5. ✅ پردازش دسته‌ای برای کارایی بیشتر
134
+
135
+ ## 📊 مدل‌های موجود
136
+
137
+ - **Crypto Sentiment**: `crypto_sent_0`, `crypto_sent_1`, ...
138
+ - **Social Sentiment**: `social_sent_0`, `social_sent_1`
139
+ - **Financial Sentiment**: `financial_sent_0`, `financial_sent_1`
140
+ - **News Sentiment**: `news_sent_0`
141
+
142
+ همه این مدل‌ها به عنوان endpoint و provider در دسترس هستند!
143
+
QUICK_START.md CHANGED
@@ -1,221 +1,78 @@
1
- # 🚀 راهنمای سریع شروع - Quick Start Guide
2
 
3
- ## نصب و راه‌اندازی سریع
4
 
5
- ### 1️⃣ نصب وابستگی‌ها
6
  ```bash
 
 
 
 
7
  pip install -r requirements.txt
 
8
  ```
9
 
10
- ### 2️⃣ Import منابع از فایل‌های JSON
11
- ```bash
12
- python import_resources.py
13
- ```
14
- این اسکریپت به‌طور خودکار همه منابع را از فایل‌های JSON موجود import می‌کند.
15
-
16
- ### 3️⃣ راه‌اندازی سرور
17
- ```bash
18
- # روش 1: استفاده از اسکریپت راه‌انداز
19
- python start_server.py
20
-
21
- # روش 2: مستقیم
22
- python api_server_extended.py
23
 
24
- # روش 3: با uvicorn
25
- uvicorn api_server_extended:app --reload --host 0.0.0.0 --port 8000
26
- ```
27
-
28
- ### 4️⃣ دسترسی به داشبورد
29
- ```
30
- http://localhost:8000
31
- ```
32
 
33
- ## 📋 تب‌های داشبورد
34
-
35
- ### 📊 Market
36
- - آمار کلی بازار
37
- - لیست کریپتوکارنسی‌ها
38
- - نمودارها و ترندینگ
39
-
40
- ### 📡 API Monitor
41
- - وضعیت همه ارائه‌دهندگان
42
- - زمان پاسخ
43
- - Health Check
44
-
45
- ### ⚡ Advanced
46
- - Export JSON/CSV
47
- - Backup
48
- - Clear Cache
49
- - Activity Logs
50
-
51
- ### ⚙️ Admin
52
- - افزودن API جدید
53
- - تنظیمات
54
- - آمار کلی
55
-
56
- ### 🤗 HuggingFace
57
- - مدل‌های Sentiment Analysis
58
- - Datasets
59
- - جستجو در Registry
60
-
61
- ### 🔄 Pools
62
- - مدیریت Pool‌ها
63
- - افزودن/حذف اعضا
64
- - چرخش دستی
65
-
66
- ### 📋 Logs (جدید!)
67
- - نمایش لاگ‌ها با فیلتر
68
- - Export به JSON/CSV
69
- - جستجو و آمار
70
-
71
- ### 📦 Resources (جدید!)
72
- - مدیریت منابع API
73
- - Import/Export
74
- - Backup
75
- - فیلتر بر اساس Category
76
-
77
- ## 🔧 استفاده از API
78
-
79
- ### دریافت لاگ‌ها
80
  ```bash
81
- # همه لاگ‌ها
82
- curl http://localhost:8000/api/logs
83
-
84
- # فیلتر بر اساس Level
85
- curl http://localhost:8000/api/logs?level=error
86
-
87
- # جستجو
88
- curl http://localhost:8000/api/logs?search=timeout
89
  ```
90
 
91
- ### Export لاگ‌ها
92
- ```bash
93
- # Export به JSON
94
- curl http://localhost:8000/api/logs/export/json?level=error
95
 
96
- # Export به CSV
97
- curl http://localhost:8000/api/logs/export/csv
98
- ```
99
 
100
- ### مدیریت منابع
101
  ```bash
102
- # دریافت همه منابع
103
- curl http://localhost:8000/api/resources
104
-
105
- # Export منابع
106
- curl http://localhost:8000/api/resources/export/json
107
-
108
- # Backup
109
- curl -X POST http://localhost:8000/api/resources/backup
110
-
111
- # Import
112
- curl -X POST "http://localhost:8000/api/resources/import/json?file_path=api-resources/crypto_resources_unified_2025-11-11.json&merge=true"
113
- ```
114
-
115
- ## 📝 مثال‌های استفاده
116
-
117
- ### افزودن Provider جدید
118
- ```python
119
- from resource_manager import ResourceManager
120
-
121
- manager = ResourceManager()
122
-
123
- provider = {
124
- "id": "my_new_api",
125
- "name": "My New API",
126
- "category": "market_data",
127
- "base_url": "https://api.example.com",
128
- "requires_auth": False,
129
- "priority": 5,
130
- "weight": 50,
131
- "free": True
132
- }
133
-
134
- manager.add_provider(provider)
135
- manager.save_resources()
136
  ```
137
 
138
- ### ثبت لاگ
139
- ```python
140
- from log_manager import log_info, log_error, LogCategory
141
 
142
- # لاگ Info
143
- log_info(LogCategory.PROVIDER, "Provider health check completed",
144
- provider_id="coingecko", response_time=234.5)
 
 
 
 
 
 
 
145
 
146
- # لاگ Error
147
- log_error(LogCategory.PROVIDER, "Provider failed",
148
- provider_id="etherscan", error="Timeout")
149
- ```
150
 
151
- ### استفاده از Provider Manager
152
- ```python
153
- from provider_manager import ProviderManager
154
- import asyncio
155
-
156
- async def main():
157
- manager = ProviderManager()
158
-
159
- # Health Check
160
- await manager.health_check_all()
161
-
162
- # دریافت Provider از Pool
163
- provider = manager.get_next_from_pool("primary_market_data_pool")
164
- if provider:
165
- print(f"Selected: {provider.name}")
166
-
167
- await manager.close_session()
168
-
169
- asyncio.run(main())
170
- ```
171
 
172
- ## 🐳 استفاده با Docker
173
 
 
174
  ```bash
175
- # Build
176
- docker build -t crypto-monitor .
177
-
178
- # Run
179
- docker run -p 8000:8000 crypto-monitor
180
-
181
- # یا با docker-compose
182
- docker-compose up -d
183
  ```
184
 
185
- ## 🔍 عیب‌یابی
186
-
187
- ### مشکل: Port در حال استفاده است
188
- ```bash
189
- # تغییر پورت
190
- uvicorn api_server_extended:app --port 8001
191
- ```
192
-
193
- ### مشکل: فایل‌های JSON یافت نشد
194
  ```bash
195
- # بررسی وجود فایل‌ها
196
- ls -la api-resources/
197
- ls -la providers_config*.json
198
  ```
199
 
200
- ### مشکل: Import منابع ناموفق
201
  ```bash
202
- # بررسی ساختار JSON
203
- python -m json.tool api-resources/crypto_resources_unified_2025-11-11.json | head -20
204
  ```
205
 
206
- ## 📚 مستندات بیشتر
207
-
208
- - [README.md](README.md) - مستندات کامل انگلیسی
209
- - [README_FA.md](README_FA.md) - مستندات کامل فارسی
210
- - [api-resources/README.md](api-resources/README.md) - راهنمای منابع API
211
-
212
- ## 🆘 پشتیبانی
213
-
214
- در صورت بروز مشکل:
215
- 1. لاگ‌ها را بررسی کنید: `logs/app.log`
216
- 2. از تب Logs در داشبورد استفاده کنید
217
- 3. آمار سیستم را بررسی کنید: `/api/status`
218
 
219
- ---
 
 
220
 
221
- **موفق باشید! 🚀**
 
1
+ # 🚀 Quick Start - 3 دقیقه تا اجرا
2
 
3
+ ## روش 1: Python (ساده)
4
 
 
5
  ```bash
6
+ unzip crypto-hf-integrated-final.zip
7
+ cd crypto-dt-source-hf-integrated
8
+ python3 -m venv venv
9
+ source venv/bin/activate
10
  pip install -r requirements.txt
11
+ uvicorn hf_unified_server:app --port 7860
12
  ```
13
 
14
+ **سپس:** http://localhost:7860
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
+ ## روش 2: Docker (توصیه)
 
 
 
 
 
 
 
17
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  ```bash
19
+ unzip crypto-hf-integrated-final.zip
20
+ cd crypto-dt-source-hf-integrated
21
+ docker build -f Dockerfile.optimized -t crypto-hub .
22
+ docker run -d -p 7860:7860 --name crypto-hub crypto-hub
 
 
 
 
23
  ```
24
 
25
+ **سپس:** http://localhost:7860
 
 
 
26
 
27
+ ## تست
 
 
28
 
 
29
  ```bash
30
+ ./test_endpoints.sh
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  ```
32
 
33
+ ## Dashboard Tabs
 
 
34
 
35
+ 1. **Overview** - نمای کلی
36
+ 2. **Market** - بازار
37
+ 3. **Chart Lab** - نمودارها
38
+ 4. **Sentiment & AI** - احساسات (10+ models)
39
+ 5. **News** - اخبار با sentiment
40
+ 6. **Providers** - 95 منابع
41
+ 7. **API Explorer** - تست API
42
+ 8. **Diagnostics** - سلامت سیستم
43
+ 9. **Datasets & Models** - 14 dataset + 10 models
44
+ 10. **Settings** - تنظیمات
45
 
46
+ ## Features
 
 
 
47
 
48
+ - Real-time data (WebSocket)
49
+ - ✅ Ensemble sentiment (10+ HF models)
50
+ - 14 crypto datasets
51
+ - ✅ 95 API providers
52
+ - ✅ Chart analysis
53
+ - News aggregation
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
+ ## مشکلات رایج
56
 
57
+ **Port in use:**
58
  ```bash
59
+ uvicorn hf_unified_server:app --port 8000
 
 
 
 
 
 
 
60
  ```
61
 
62
+ **Model download:**
 
 
 
 
 
 
 
 
63
  ```bash
64
+ export HF_TOKEN=your_token
 
 
65
  ```
66
 
67
+ **Dependencies:**
68
  ```bash
69
+ pip install -r requirements.txt
 
70
  ```
71
 
72
+ ## مستندات
 
 
 
 
 
 
 
 
 
 
 
73
 
74
+ - `README_HF_INTEGRATION.md` - کامل
75
+ - `DEPLOYMENT_GUIDE.md` - Production
76
+ - `ADMIN_HTML_INTEGRATION.md` - Frontend
77
 
78
+ **Ready!** 🚀
README.md CHANGED
@@ -1,844 +1,141 @@
1
- ---
2
- sdk: docker
3
- colorFrom: red
4
- colorTo: green
5
- pinned: true
6
- ---
7
- # Crypto Intelligence Dashboard
8
-
9
- > **SDK: Docker** - This application is containerized and designed to run with Docker for easy deployment and scalability.
10
-
11
- [![Docker](https://img.shields.io/badge/SDK-Docker-2496ED?style=for-the-badge&logo=docker&logoColor=white)](https://www.docker.com/)
12
- [![Python](https://img.shields.io/badge/Python-3.10+-3776AB?style=for-the-badge&logo=python&logoColor=white)](https://www.python.org/)
13
- [![FastAPI](https://img.shields.io/badge/FastAPI-0.104+-009688?style=for-the-badge&logo=fastapi&logoColor=white)](https://fastapi.tiangolo.com/)
14
- [![Hugging Face](https://img.shields.io/badge/Hugging%20Face-Spaces-FFD21E?style=for-the-badge&logo=huggingface&logoColor=black)](https://huggingface.co/)
15
-
16
- A professional, production-ready cryptocurrency intelligence platform with real-time market analysis, AI-powered sentiment analysis, and comprehensive API provider monitoring.
17
-
18
- ---
19
-
20
- ## 🌟 Features
21
-
22
- ### 🎯 Core Capabilities
23
-
24
- - **Real-time Cryptocurrency Data** - Live prices, market cap, volume, and trends
25
- - **Natural Language Queries** - Ask questions like "Bitcoin price" or "Top 10 coins"
26
- - **AI Sentiment Analysis** - CryptoBERT model for crypto-specific sentiment
27
- - **Provider Monitoring** - Track 150+ API providers with health checks
28
- - **Professional Dashboard** - Modern UI with interactive charts and visualizations
29
- - **WebSocket Real-time Updates** - Live data synchronization every 10 seconds
30
- - **News Aggregation** - Latest crypto news from multiple sources
31
- - **Market Analytics** - DeFi TVL, NFT volume, gas prices, Fear & Greed Index
32
-
33
- ### 🤖 AI & Machine Learning
34
-
35
- - **CryptoBERT Integration** - ElKulako/CryptoBERT model with authentication
36
- - **Sentiment Analysis** - Multi-model approach (Twitter, Financial, Crypto-specific)
37
- - **Market Trend Prediction** - Technical analysis with RSI, MA, support/resistance
38
- - **Text Summarization** - Automatic news and report summarization
39
-
40
- ### 📊 Data Sources
41
-
42
- - **150+ API Providers** - CoinGecko, Binance, DeFiLlama, Etherscan, and more
43
- - **Multiple Categories**:
44
- - Market Data (CoinGecko, CoinCap, CryptoCom pare)
45
- - DeFi Protocols (DeFiLlama, Aave, Uniswap)
46
- - Blockchain Explorers (Etherscan, BscScan, PolygonScan)
47
- - NFT Marketplaces (OpenSea, Rarible, Reservoir)
48
- - News Sources (CoinDesk, Cointelegraph, CryptoPanic)
49
- - Social Media (Reddit, Twitter trends)
50
- - Analytics (Glassnode, IntoTheBlock, Messari)
51
-
52
- ---
53
-
54
- ## 🐳 Docker Quick Start
55
-
56
- ### Prerequisites
57
-
58
- - Docker 20.10+
59
- - Docker Compose 2.0+
60
-
61
- ### Using Docker (Recommended)
62
-
63
- ```bash
64
- # Build and run with Docker Compose
65
- docker-compose up -d
66
-
67
- # Access the dashboard
68
- open http://localhost:7860
69
- ```
70
-
71
- ### Using Pre-built Docker Image
72
-
73
- ```bash
74
- # Pull the image
75
- docker pull your-registry/crypto-dashboard:latest
76
-
77
- # Run the container
78
- docker run -d \
79
- -p 7860:7860 \
80
- -e HF_TOKEN=your_token_here \
81
- --name crypto-dashboard \
82
- your-registry/crypto-dashboard:latest
83
-
84
- # View logs
85
- docker logs -f crypto-dashboard
86
- ```
87
-
88
- ---
89
-
90
- ## 🚀 Installation
91
-
92
- ### Option 1: Docker (Recommended)
93
-
94
- 1. **Clone the repository**
95
- ```bash
96
- git clone https://github.com/yourusername/crypto-dashboard.git
97
- cd crypto-dashboard
98
- ```
99
-
100
- 2. **Configure environment variables**
101
- ```bash
102
- cp .env.example .env
103
- # Edit .env with your API keys
104
- ```
105
-
106
- 3. **Build and run**
107
- ```bash
108
- docker-compose up -d
109
- ```
110
-
111
- 4. **Access the application**
112
- - Dashboard: http://localhost:7860
113
- - API Docs: http://localhost:7860/docs
114
- - Admin Panel: http://localhost:7860/admin
115
-
116
- ### Option 2: Local Development
117
-
118
- 1. **Install Python 3.10+**
119
- ```bash
120
- python --version # Should be 3.10 or higher
121
- ```
122
-
123
- 2. **Install dependencies**
124
- ```bash
125
- pip install -r requirements.txt
126
- ```
127
-
128
- 3. **Install AI models dependencies** (Optional)
129
- ```bash
130
- pip install transformers torch
131
- ```
132
-
133
- 4. **Set environment variables**
134
- ```bash
135
- export HF_TOKEN="your_huggingface_token"
136
- ```
137
-
138
- 5. **Run the application**
139
- ```bash
140
- # Start the professional dashboard
141
- python3 api_dashboard_backend.py
142
-
143
- # Or start the provider monitor
144
- python3 api_server_extended.py
145
- ```
146
-
147
- ### Option 3: Hugging Face Spaces
148
-
149
- 1. **Fork this repository**
150
-
151
- 2. **Create a new Space on Hugging Face**
152
- - Choose "Gradio" or "Docker" SDK
153
- - Connect your forked repository
154
-
155
- 3. **Add secrets in Space settings**
156
- ```
157
- HF_TOKEN=your_token
158
- ```
159
-
160
- 4. **Deploy**
161
- - Automatic deployment on push
162
-
163
- ---
164
-
165
- ## 📖 Usage
166
-
167
- ### Professional Dashboard
168
-
169
- The main dashboard provides a comprehensive view of the cryptocurrency market:
170
-
171
- ```bash
172
- # Start the professional dashboard
173
- python3 api_dashboard_backend.py
174
-
175
- # Access at: http://localhost:7860
176
- ```
177
-
178
- **Features:**
179
- - 🔍 Natural language query interface
180
- - 📈 Real-time price charts
181
- - 📊 Market statistics cards
182
- - 📰 Latest crypto news
183
- - 😊 Sentiment analysis visualization
184
- - 💹 Top cryptocurrencies table
185
-
186
- **Example Queries:**
187
- ```
188
- "Bitcoin price" → Current BTC price
189
- "Top 10 coins" → List top 10 cryptocurrencies
190
- "Ethereum trend" → ETH price trend chart
191
- "Market sentiment" → Bullish/bearish analysis
192
- "DeFi TVL" → Total Value Locked in DeFi
193
- "NFT volume" → Daily NFT trading volume
194
- "Gas prices" → Current Ethereum gas fees
195
- ```
196
-
197
- ### Provider Monitoring Dashboard
198
-
199
- Monitor all API providers and their health status:
200
-
201
- ```bash
202
- # Start the provider monitor
203
- python3 api_server_extended.py
204
-
205
- # Access at: http://localhost:7860
206
- ```
207
-
208
- **Features:**
209
- - ✅ Real-time provider status (validated/unvalidated)
210
- - 📊 Response time monitoring
211
- - 🔄 Auto-refresh every 30 seconds
212
- - 🏷️ Category-based filtering
213
- - 🔍 Search functionality
214
- - 📈 Statistics dashboard
215
-
216
- ### API Endpoints
217
-
218
- #### REST API
219
-
220
- ```bash
221
- # Health check
222
- GET /api/health
223
-
224
- # Top cryptocurrencies
225
- GET /api/coins/top?limit=10
226
-
227
- # Specific coin details
228
- GET /api/coins/{symbol}
229
-
230
- # Market statistics
231
- GET /api/market/stats
232
-
233
- # Latest news
234
- GET /api/news/latest?limit=10
235
-
236
- # Process user query
237
- POST /api/query
238
- {
239
- "query": "Bitcoin price"
240
- }
241
-
242
- # API providers list
243
- GET /api/providers
244
-
245
- # Historical price data
246
- GET /api/charts/price/{symbol}?timeframe=7d
247
- ```
248
-
249
- #### WebSocket
250
-
251
- ```javascript
252
- // Connect to real-time updates
253
- const ws = new WebSocket('ws://localhost:7860/ws');
254
-
255
- ws.onmessage = (event) => {
256
- const data = JSON.parse(event.data);
257
- console.log('Received:', data);
258
- };
259
- ```
260
-
261
- ### CryptoBERT AI Model
262
-
263
- Use the CryptoBERT model for crypto-specific sentiment analysis:
264
-
265
- ```python
266
- import ai_models
267
-
268
- # Initialize models
269
- ai_models.initialize_models()
270
-
271
- # Analyze sentiment
272
- text = "Bitcoin shows strong bullish momentum"
273
- result = ai_models.analyze_crypto_sentiment(text)
274
-
275
- print(f"Sentiment: {result['label']}")
276
- print(f"Confidence: {result['score']}")
277
- print(f"Predictions: {result['predictions']}")
278
- ```
279
-
280
- ---
281
-
282
- ## 🏗️ Architecture
283
-
284
- ```
285
- ┌─────────────────────────────────────────────────────────┐
286
- │ Frontend Layer │
287
- │ ┌──────────────┐ ┌──────────────┐ ┌─────────────┐ │
288
- │ │ Dashboard UI │ │ Admin Panel │ │ Charts/Viz │ │
289
- │ │ (HTML/JS) │ │ (HTML/JS) │ │ (Chart.js) │ │
290
- │ └──────────────┘ └──────────────┘ └─────────────┘ │
291
- └─────────────────────────────────────────────────────────┘
292
- ↕ HTTP/WebSocket
293
- ┌─────────────────────────────────────────────────────────┐
294
- │ Backend Layer │
295
- │ ┌──────────────┐ ┌──────────────┐ ┌─────────────┐ │
296
- │ │ FastAPI │ │ WebSocket │ │ Query │ │
297
- │ │ REST API │ │ Manager │ │ Parser │ │
298
- │ └──────────────┘ └──────────────┘ └─────────────┘ │
299
- └─────────────────────────────────────────────────────────┘
300
-
301
- ┌─────────────────────────────────────────────────────────┐
302
- │ Services Layer │
303
- │ ┌──────────────┐ ┌──────────────┐ ┌─────────────┐ │
304
- │ │ AI Models │ │ Provider │ │ Data │ │
305
- │ │ (CryptoBERT) │ │ Manager │ │ Aggregator │ │
306
- │ └──────────────┘ └──────────────┘ └─────────────┘ │
307
- └─────────────────────────────────────────────────────────┘
308
-
309
- ┌─────────────────────────────────────────────────────────┐
310
- │ Data Layer │
311
- │ ┌───���──────────┐ ┌──────────────┐ ┌─────────────┐ │
312
- │ │ SQLite │ │ Redis Cache │ │ JSON Config │ │
313
- │ │ Database │ │ (Optional) │ │ Files │ │
314
- │ └──────────────┘ └──────────────┘ └─────────────┘ │
315
- └─────────────────────────────────────────────────────────┘
316
-
317
- ┌─────────────────────────────────────────────────────────┐
318
- │ External Data Sources │
319
- │ CoinGecko • Binance • DeFiLlama • Etherscan │
320
- │ OpenSea • CryptoPanic • Reddit • Hugging Face │
321
- └─────────────────────────────────────────────────────────┘
322
- ```
323
-
324
- ---
325
-
326
- ## 📂 Project Structure
327
-
328
- ```
329
- crypto-dashboard/
330
- ├── 🐳 Docker Files
331
- │ ├── Dockerfile # Main Docker configuration
332
- │ ├── docker-compose.yml # Docker Compose setup
333
- │ └── .dockerignore # Docker ignore file
334
-
335
- ├── 🎨 Frontend
336
- │ ├── crypto_dashboard_pro.html # Professional dashboard
337
- │ ├── admin_improved.html # Provider monitoring dashboard
338
- │ ├── dashboard_standalone.html # Standalone dashboard
339
- │ └── static/ # Static assets (CSS, JS)
340
-
341
- ├── 🔧 Backend
342
- │ ├── api_dashboard_backend.py # Main API server
343
- │ ├── api_server_extended.py # Provider monitoring API
344
- │ ├── api/ # API endpoints
345
- │ ├── backend/ # Business logic
346
- │ └── monitoring/ # Monitoring services
347
-
348
- ├── 🤖 AI & ML
349
- │ ├── ai_models.py # AI models integration
350
- │ ├── config.py # Configuration (HF models)
351
- │ └── collectors/ # Data collectors
352
-
353
- ├── 💾 Data & Config
354
- │ ├── providers_config_extended.json # API providers config
355
- │ ├── database/ # Database modules
356
- │ └── data/ # Data storage
357
-
358
- ├── 📖 Documentation
359
- │ ├── README.md # This file
360
- │ ├── PROFESSIONAL_DASHBOARD_GUIDE.md
361
- │ ├── QUICK_START_PROFESSIONAL.md
362
- │ ├── CRYPTOBERT_INTEGRATION.md
363
- │ ├── PROVIDER_DASHBOARD_GUIDE.md
364
- │ └── docs/ # Additional documentation
365
-
366
- ├── 🧪 Tests
367
- │ ├── tests/ # Test files
368
- │ ├── test_cryptobert.py # CryptoBERT tests
369
- │ └── test_integration.py # Integration tests
370
-
371
- └── 📦 Configuration
372
- ├── requirements.txt # Python dependencies
373
- ├── requirements-dev.txt # Dev dependencies
374
- ├── .env.example # Environment variables template
375
- └── pyproject.toml # Project metadata
376
- ```
377
-
378
- ---
379
-
380
- ## 🔧 Configuration
381
-
382
- ### Environment Variables
383
-
384
- Create a `.env` file in the root directory:
385
-
386
- ```bash
387
- # Hugging Face
388
- HF_TOKEN=your_huggingface_token_here
389
-
390
- # API Keys (Optional - for real data)
391
- CMC_API_KEY=your_coinmarketcap_key
392
- ETHERSCAN_KEY=your_etherscan_key
393
- NEWSAPI_KEY=your_newsapi_key
394
-
395
- # Application Settings
396
- LOG_LEVEL=INFO
397
- PORT=7860
398
- HOST=0.0.0.0
399
-
400
- # Database
401
- DATABASE_PATH=data/crypto_aggregator.db
402
-
403
- # Cache (Optional)
404
- REDIS_URL=redis://localhost:6379
405
- CACHE_TTL=300
406
-
407
- # AI Models
408
- ENABLE_AI_MODELS=true
409
- ```
410
-
411
- ### Provider Configuration
412
-
413
- Edit `providers_config_extended.json` to add/modify API providers:
414
-
415
- ```json
416
- {
417
- "providers": {
418
- "your_provider_id": {
419
- "name": "Your Provider Name",
420
- "base_url": "https://api.example.com",
421
- "category": "market_data",
422
- "requires_auth": false,
423
- "priority": 10
424
- }
425
- }
426
- }
427
- ```
428
-
429
- ---
430
-
431
- ## 🛠️ Development
432
-
433
- ### Setup Development Environment
434
-
435
- ```bash
436
- # Clone repository
437
- git clone https://github.com/yourusername/crypto-dashboard.git
438
- cd crypto-dashboard
439
-
440
- # Create virtual environment
441
- python3 -m venv venv
442
- source venv/bin/activate # On Windows: venv\Scripts\activate
443
-
444
- # Install dependencies
445
- pip install -r requirements-dev.txt
446
-
447
- # Install pre-commit hooks
448
- pre-commit install
449
-
450
- # Run tests
451
- pytest tests/
452
-
453
- # Run with hot reload
454
- uvicorn api_dashboard_backend:app --reload --port 7860
455
- ```
456
-
457
- ### Running Tests
458
-
459
- ```bash
460
- # Run all tests
461
- pytest
462
-
463
- # Run specific test file
464
- pytest tests/test_cryptobert.py
465
-
466
- # Run with coverage
467
- pytest --cov=. --cov-report=html
468
-
469
- # Run integration tests
470
- python3 test_integration.py
471
- ```
472
-
473
- ### Code Quality
474
-
475
- ```bash
476
- # Format code
477
- black .
478
-
479
- # Lint code
480
- flake8 .
481
-
482
- # Type checking
483
- mypy .
484
-
485
- # Security scan
486
- bandit -r .
487
- ```
488
-
489
- ---
490
-
491
- ## 🚢 Deployment
492
-
493
- ### Docker Production Deployment
494
-
495
- ```bash
496
- # Build production image
497
- docker build -t crypto-dashboard:latest .
498
-
499
- # Run with production settings
500
- docker run -d \
501
- -p 80:7860 \
502
- -e HF_TOKEN=${HF_TOKEN} \
503
- -e LOG_LEVEL=WARNING \
504
- --restart unless-stopped \
505
- --name crypto-dashboard-prod \
506
- crypto-dashboard:latest
507
- ```
508
-
509
- ### Kubernetes Deployment
510
-
511
- ```yaml
512
- # deployment.yaml
513
- apiVersion: apps/v1
514
- kind: Deployment
515
- metadata:
516
- name: crypto-dashboard
517
- spec:
518
- replicas: 3
519
- selector:
520
- matchLabels:
521
- app: crypto-dashboard
522
- template:
523
- metadata:
524
- labels:
525
- app: crypto-dashboard
526
- spec:
527
- containers:
528
- - name: crypto-dashboard
529
- image: your-registry/crypto-dashboard:latest
530
- ports:
531
- - containerPort: 7860
532
- env:
533
- - name: HF_TOKEN
534
- valueFrom:
535
- secretKeyRef:
536
- name: crypto-secrets
537
- key: hf-token
538
- ```
539
-
540
- Deploy:
541
- ```bash
542
- kubectl apply -f deployment.yaml
543
- kubectl apply -f service.yaml
544
- ```
545
-
546
- ### Hugging Face Spaces
547
-
548
- 1. Create `README.md` in your Space
549
- 2. Add `requirements.txt`
550
- 3. Create `app.py`:
551
- ```python
552
- from api_dashboard_backend import app
553
- ```
554
- 4. Set secrets in Space settings
555
- 5. Push to deploy
556
-
557
- ### AWS/GCP/Azure
558
-
559
- See `docs/DEPLOYMENT_MASTER_GUIDE.md` for detailed cloud deployment instructions.
560
-
561
- ---
562
-
563
- ## 📊 API Documentation
564
-
565
- ### Interactive API Docs
566
-
567
- Once the server is running, visit:
568
-
569
- - **Swagger UI**: http://localhost:7860/docs
570
- - **ReDoc**: http://localhost:7860/redoc
571
-
572
- ### API Examples
573
-
574
- #### Get Top Cryptocurrencies
575
-
576
- ```bash
577
- curl http://localhost:7860/api/coins/top?limit=10
578
- ```
579
-
580
- Response:
581
- ```json
582
- {
583
- "success": true,
584
- "coins": [
585
- {
586
- "name": "Bitcoin",
587
- "symbol": "BTC",
588
- "price": 43250.50,
589
- "change_24h": 2.34,
590
- "market_cap": 845000000000,
591
- "volume_24h": 25000000000
592
- }
593
- ],
594
- "count": 10
595
- }
596
- ```
597
-
598
- #### Process Natural Language Query
599
-
600
- ```bash
601
- curl -X POST http://localhost:7860/api/query \
602
- -H "Content-Type: application/json" \
603
- -d '{"query": "Bitcoin price"}'
604
- ```
605
-
606
- Response:
607
- ```json
608
- {
609
- "success": true,
610
- "type": "price",
611
- "coin": "Bitcoin",
612
- "symbol": "BTC",
613
- "price": 43250.50,
614
- "message": "Bitcoin (BTC) is currently $43,250.50"
615
- }
616
- ```
617
-
618
- ---
619
-
620
- ## 🤝 Contributing
621
-
622
- We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING.md).
623
-
624
- ### How to Contribute
625
-
626
- 1. **Fork the repository**
627
- 2. **Create a feature branch**
628
- ```bash
629
- git checkout -b feature/amazing-feature
630
- ```
631
- 3. **Commit your changes**
632
- ```bash
633
- git commit -m 'Add amazing feature'
634
- ```
635
- 4. **Push to the branch**
636
- ```bash
637
- git push origin feature/amazing-feature
638
- ```
639
- 5. **Open a Pull Request**
640
-
641
- ### Development Guidelines
642
-
643
- - Follow PEP 8 style guide
644
- - Write tests for new features
645
- - Update documentation
646
- - Use type hints
647
- - Add docstrings to functions
648
-
649
- ---
650
-
651
- ## 📝 License
652
-
653
- This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
654
-
655
- ---
656
-
657
- ## 🙏 Acknowledgments
658
-
659
- ### Technologies Used
660
-
661
- - **FastAPI** - Modern web framework for building APIs
662
- - **Hugging Face Transformers** - AI model integration
663
- - **Chart.js** - Interactive charts and visualizations
664
- - **Docker** - Containerization platform
665
- - **Python 3.10+** - Programming language
666
-
667
- ### Data Providers
668
-
669
- - **CoinGecko** - Cryptocurrency market data
670
- - **Binance** - Real-time trading data
671
- - **DeFiLlama** - DeFi protocol analytics
672
- - **Etherscan** - Ethereum blockchain explorer
673
- - **OpenSea** - NFT marketplace data
674
- - **CryptoPanic** - Crypto news aggregation
675
-
676
- ### AI Models
677
-
678
- - **ElKulako/CryptoBERT** - Crypto-specific sentiment analysis
679
- - **cardiffnlp/twitter-roberta-base-sentiment** - Twitter sentiment
680
- - **ProsusAI/finbert** - Financial sentiment analysis
681
- - **facebook/bart-large-cnn** - Text summarization
682
-
683
- ---
684
-
685
- ## 📞 Support
686
-
687
- ### Documentation
688
-
689
- - **Quick Start**: [QUICK_START_PROFESSIONAL.md](QUICK_START_PROFESSIONAL.md)
690
- - **Full Guide**: [PROFESSIONAL_DASHBOARD_GUIDE.md](PROFESSIONAL_DASHBOARD_GUIDE.md)
691
- - **CryptoBERT Integration**: [CRYPTOBERT_INTEGRATION.md](docs/CRYPTOBERT_INTEGRATION.md)
692
- - **Provider Dashboard**: [PROVIDER_DASHBOARD_GUIDE.md](PROVIDER_DASHBOARD_GUIDE.md)
693
-
694
- ### Getting Help
695
-
696
- - 📖 Check the documentation
697
- - 🐛 Open an issue on GitHub
698
- - 💬 Join our community discussions
699
- - 📧 Contact: [email protected]
700
-
701
- ### Troubleshooting
702
-
703
- **Dashboard not loading?**
704
- ```bash
705
- # Check if server is running
706
- curl http://localhost:7860/api/health
707
-
708
- # Check Docker logs
709
- docker logs crypto-dashboard
710
- ```
711
-
712
- **WebSocket not connecting?**
713
- ```bash
714
- # Verify WebSocket endpoint
715
- wscat -c ws://localhost:7860/ws
716
- ```
717
-
718
- **AI models not loading?**
719
- ```bash
720
- # Check HF_TOKEN is set
721
- echo $HF_TOKEN
722
-
723
- # Test model loading
724
- python3 test_cryptobert.py
725
- ```
726
-
727
- ---
728
-
729
- ## 🗺️ Roadmap
730
-
731
- ### Current Version (v1.0)
732
- - ✅ Professional dashboard
733
- - ✅ Provider monitoring
734
- - ✅ CryptoBERT integration
735
- - ✅ Natural language queries
736
- - ✅ Real-time WebSocket updates
737
- - ✅ Docker containerization
738
-
739
- ### Planned Features (v1.1)
740
- - [ ] Portfolio tracking
741
- - [ ] Price alerts
742
- - [ ] Advanced charting (candlesticks)
743
- - [ ] Social sentiment analysis
744
- - [ ] Multi-language support
745
- - [ ] Mobile app
746
-
747
- ### Future Enhancements (v2.0)
748
- - [ ] AI-powered predictions
749
- - [ ] Trading signals
750
- - [ ] Automated trading (with approval)
751
- - [ ] Desktop application
752
- - [ ] Browser extension
753
- - [ ] API marketplace integration
754
-
755
- ---
756
-
757
- ## 📈 Statistics
758
-
759
- - **150+ API Providers** integrated
760
- - **4 AI Models** for sentiment analysis
761
- - **10+ API Endpoints** available
762
- - **Real-time Updates** every 10 seconds
763
- - **100% Docker** compatible
764
- - **Mobile Responsive** design
765
-
766
- ---
767
-
768
- ## 🔒 Security
769
-
770
- ### Security Features
771
-
772
- - ✅ Environment variable configuration
773
- - ✅ CORS protection
774
- - ✅ Input validation
775
- - ✅ Error handling
776
- - ✅ Rate limiting (optional)
777
- - ✅ API key management
778
-
779
- ### Reporting Security Issues
780
-
781
- Please report security vulnerabilities to: [email protected]
782
-
783
- **Do not** create public GitHub issues for security vulnerabilities.
784
-
785
- ---
786
-
787
- ## 📜 Changelog
788
-
789
- See [CHANGELOG.md](CHANGELOG.md) for a detailed history of changes.
790
-
791
- ### Recent Updates
792
-
793
- **v1.0.0** (2025-11-16)
794
- - Initial release
795
- - Professional dashboard with query system
796
- - CryptoBERT AI model integration
797
- - Provider monitoring dashboard
798
- - Docker containerization
799
- - Complete documentation
800
-
801
- ---
802
-
803
- ## 🌐 Links
804
-
805
- - **Website**: https://your-site.com
806
- - **Documentation**: https://docs.your-site.com
807
- - **GitHub**: https://github.com/yourusername/crypto-dashboard
808
- - **Docker Hub**: https://hub.docker.com/r/yourusername/crypto-dashboard
809
- - **Hugging Face**: https://huggingface.co/spaces/yourusername/crypto-dashboard
810
-
811
- ---
812
-
813
- ## ⭐ Star History
814
-
815
- If you find this project useful, please consider giving it a star ⭐️
816
-
817
- [![Star History Chart](https://api.star-history.com/svg?repos=yourusername/crypto-dashboard&type=Date)](https://star-history.com/#yourusername/crypto-dashboard&Date)
818
-
819
- ---
820
-
821
- ## 📄 Citation
822
-
823
- If you use this project in your research or work, please cite:
824
-
825
- ```bibtex
826
- @software{crypto_dashboard_2025,
827
- author = {Your Name},
828
- title = {Crypto Intelligence Dashboard},
829
- year = {2025},
830
- url = {https://github.com/yourusername/crypto-dashboard}
831
- }
832
- ```
833
-
834
- ---
835
-
836
- <div align="center">
837
-
838
- **Built with ❤️ using Docker, Python, and FastAPI**
839
-
840
- [Report Bug](https://github.com/yourusername/crypto-dashboard/issues) ·
841
- [Request Feature](https://github.com/yourusername/crypto-dashboard/issues) ·
842
- [Documentation](https://docs.your-site.com)
843
-
844
- </div>
 
1
+ ---
2
+ title: Crypto Intelligence Hub
3
+ emoji: 🚀
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: docker
7
+ pinned: false
8
+ license: mit
9
+ app_port: 7860
10
+ ---
11
+
12
+ # Crypto Intelligence Hub - مرکز هوش رمز ارز
13
+
14
+ یک رابط کاربری کامل و یکپارچه برای جمع‌آوری داده‌های رمز ارز با استفاده از منابع رایگان و مدل‌های Hugging Face.
15
+
16
+ ## ویژگی‌ها
17
+
18
+ - 📊 **داشبورد جامع**: نمایش خلاصه منابع و مدل‌ها
19
+ - 📚 **منابع رایگان**: دسترسی به بیش از 200 منبع رایگان برای داده‌های رمز ارز
20
+ - 🤖 **مدل‌های AI**: استفاده از مدل‌های Hugging Face برای تحلیل احساسات
21
+ - 💭 **تحلیل احساسات**: تحلیل متن با مدل‌های تخصصی مالی و رمز ارز
22
+ - 🔌 **یکپارچه‌سازی API**: اتصال به بک‌اند FastAPI برای سرویس‌های پیشرفته
23
+
24
+ ## 🚀 استفاده
25
+
26
+ ### داشبورد
27
+ نمایش خلاصه منابع، مدل‌ها و آمار کلی سیستم
28
+
29
+ ### منابع داده
30
+ لیست کامل منابع رایگان برای:
31
+ - داده‌های بازار (Market Data)
32
+ - کاوشگرهای بلاکچین (Block Explorers)
33
+ - نودهای RPC
34
+ - اخبار و احساسات
35
+ - ردیابی نهنگ‌ها (Whale Tracking)
36
+
37
+ ### مدل‌های AI
38
+ مدل‌های Hugging Face در دسترس:
39
+ - تحلیل احساسات مالی (FinBERT)
40
+ - تحلیل احساسات رمز ارز (CryptoBERT)
41
+ - تحلیل احساسات شبکه‌های اجتماعی
42
+ - و مدل‌های بیشتر...
43
+
44
+ ### تحلیل احساسات
45
+ وارد کردن متن و دریافت تحلیل احساسات با استفاده از مدل‌های پیشرفته
46
+
47
+ ## 📁 ساختار پروژه
48
+
49
+ ```
50
+ .
51
+ ├── app.py # فایل اصلی اپلیکیشن Gradio
52
+ ├── api_server_extended.py # بک‌اند FastAPI
53
+ ├── ai_models.py # مدیریت مدل‌های Hugging Face
54
+ ├── api-resources/ # فایل‌های JSON منابع
55
+ │ └── crypto_resources_unified_2025-11-11.json
56
+ ├── all_apis_merged_2025.json # فایل جامع APIها
57
+ ├── Dockerfile # فایل Docker برای Hugging Face Space
58
+ └── requirements_hf.txt # وابستگی‌های Python
59
+ ```
60
+
61
+ ## 🔧 تنظیمات
62
+
63
+ ### متغیرهای محیطی (Environment Variables)
64
+
65
+ برای استفاده از مدل‌های خصوصی Hugging Face:
66
+
67
+ ```bash
68
+ HF_TOKEN=your_huggingface_token
69
+ ```
70
+
71
+ ### منابع داده
72
+
73
+ منابع از فایل‌های JSON زیر بارگذاری می‌شوند:
74
+ - `api-resources/crypto_resources_unified_2025-11-11.json`
75
+ - `all_apis_merged_2025.json`
76
+
77
+ ## 📊 منابع پشتیبانی شده
78
+
79
+ ### داده‌های بازار
80
+ - CoinGecko (رایگان)
81
+ - CoinMarketCap
82
+ - Binance Public API
83
+ - CoinCap
84
+ - و بیشتر...
85
+
86
+ ### کاوشگرهای بلاکچین
87
+ - Etherscan
88
+ - BscScan
89
+ - TronScan
90
+ - Blockchair
91
+ - و بیشتر...
92
+
93
+ ### RPC Nodes
94
+ - Infura
95
+ - Alchemy
96
+ - Ankr
97
+ - PublicNode
98
+ - و بیشتر...
99
+
100
+ ## 🤖 مدل‌های AI
101
+
102
+ - **FinBERT**: تحلیل احساسات مالی
103
+ - **CryptoBERT**: تحلیل احساسات رمز ارز
104
+ - **Twitter-RoBERTa**: تحلیل احساسات شبکه‌های اجتماعی
105
+ - و مدل‌های بیشتر...
106
+
107
+ ## 🛠️ توسعه
108
+
109
+ ### اجرای محلی
110
+
111
+ ```bash
112
+ # نصب وابستگی‌ها
113
+ pip install -r requirements_hf.txt
114
+
115
+ # اجرای اپلیکیشن
116
+ python app.py
117
+ ```
118
+
119
+ ### ساخت Docker Image
120
+
121
+ ```bash
122
+ docker build -t crypto-intelligence-hub .
123
+ docker run -p 7860:7860 crypto-intelligence-hub
124
+ ```
125
+
126
+ ## 📝 مجوز
127
+
128
+ MIT License
129
+
130
+ ## 🤝 مشارکت
131
+
132
+ مشارکت‌ها خوش‌آمد هستند! لطفاً برای تغییرات بزرگ ابتدا یک Issue باز کنید.
133
+
134
+ ## 📧 تماس
135
+
136
+ برای سوالات و پشتیبانی، لطفاً یک Issue در مخزن باز کنید.
137
+
138
+ ---
139
+
140
+ **نکته**: این اپلیکیشن از منابع رایگان استفاده می‌کند. برای استفاده از APIهای پولی، کلیدهای API را در تنظیمات Hugging Face Space اضافه کنید.
141
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
TEST_ENDPOINTS.sh CHANGED
@@ -1,161 +1,88 @@
1
- #!/bin/bash
2
- # API Endpoints Test Script
3
- # Run this after starting the backend to verify all endpoints work
4
-
5
- BASE_URL="${BASE_URL:-http://localhost:7860}"
6
- GREEN='\033[0;32m'
7
- RED='\033[0;31m'
8
- YELLOW='\033[1;33m'
9
- NC='\033[0m' # No Color
10
-
11
- echo "======================================"
12
- echo "🧪 Testing Crypto HF API Endpoints"
13
- echo "======================================"
14
- echo "Base URL: $BASE_URL"
15
- echo ""
16
-
17
- # Function to test endpoint
18
- test_endpoint() {
19
- local method=$1
20
- local endpoint=$2
21
- local data=$3
22
- local name=$4
23
-
24
- echo -n "Testing $name... "
25
-
26
- if [ "$method" = "GET" ]; then
27
- response=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL$endpoint")
28
- else
29
- response=$(curl -s -o /dev/null -w "%{http_code}" -X "$method" "$BASE_URL$endpoint" \
30
- -H "Content-Type: application/json" \
31
- -d "$data")
32
- fi
33
-
34
- if [ "$response" = "200" ]; then
35
- echo -e "${GREEN}✅ OK${NC} (HTTP $response)"
36
- else
37
- echo -e "${RED}❌ FAILED${NC} (HTTP $response)"
38
- return 1
39
- fi
40
- }
41
-
42
- # Test health
43
- test_endpoint "GET" "/api/health" "" "Health Check"
44
-
45
- # Test market endpoints
46
- echo ""
47
- echo "📊 Market Endpoints:"
48
- test_endpoint "GET" "/api/coins/top?limit=5" "" "Top Coins"
49
- test_endpoint "GET" "/api/coins/BTC" "" "Bitcoin Details"
50
- test_endpoint "GET" "/api/market/stats" "" "Market Stats"
51
-
52
- # Test chart endpoints
53
- echo ""
54
- echo "📈 Chart Endpoints:"
55
- test_endpoint "GET" "/api/charts/price/BTC?timeframe=7d" "" "BTC Price Chart"
56
-
57
- # POST endpoint for chart analyze
58
- echo -n "Testing Chart Analysis... "
59
- response=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/api/charts/analyze" \
60
- -H "Content-Type: application/json" \
61
- -d '{"symbol":"BTC","timeframe":"7d","indicators":[]}')
62
- http_code=$(echo "$response" | tail -n1)
63
- if [ "$http_code" = "200" ]; then
64
- echo -e "${GREEN}✅ OK${NC} (HTTP $http_code)"
65
- else
66
- echo -e "${RED}❌ FAILED${NC} (HTTP $http_code)"
67
- fi
68
-
69
- # Test news endpoints
70
- echo ""
71
- echo "📰 News Endpoints:"
72
- test_endpoint "GET" "/api/news/latest?limit=5" "" "Latest News"
73
-
74
- # POST endpoint for news summarize
75
- echo -n "Testing News Summarize... "
76
- response=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/api/news/summarize" \
77
- -H "Content-Type: application/json" \
78
- -d '{"title":"Bitcoin breaks new record","description":"BTC hits $50k"}')
79
- http_code=$(echo "$response" | tail -n1)
80
- if [ "$http_code" = "200" ]; then
81
- echo -e "${GREEN}✅ OK${NC} (HTTP $http_code)"
82
- else
83
- echo -e "${RED}❌ FAILED${NC} (HTTP $http_code)"
84
- fi
85
-
86
- # Test AI endpoints
87
- echo ""
88
- echo "🤖 AI Endpoints:"
89
-
90
- # POST endpoint for sentiment
91
- echo -n "Testing Sentiment Analysis... "
92
- response=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/api/sentiment/analyze" \
93
- -H "Content-Type: application/json" \
94
- -d '{"text":"Bitcoin is breaking new all-time highs!"}')
95
- http_code=$(echo "$response" | tail -n1)
96
- body=$(echo "$response" | head -n-1)
97
- if [ "$http_code" = "200" ]; then
98
- sentiment=$(echo "$body" | grep -o '"sentiment":"[^"]*"' | cut -d'"' -f4)
99
- confidence=$(echo "$body" | grep -o '"confidence":[0-9.]*' | cut -d':' -f2)
100
- echo -e "${GREEN}✅ OK${NC} (HTTP $http_code) - Sentiment: ${YELLOW}$sentiment${NC} (${confidence})"
101
- else
102
- echo -e "${RED}❌ FAILED${NC} (HTTP $http_code)"
103
- fi
104
-
105
- # POST endpoint for query
106
- echo -n "Testing Query... "
107
- response=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/api/query" \
108
- -H "Content-Type: application/json" \
109
- -d '{"query":"What is the price of Bitcoin?"}')
110
- http_code=$(echo "$response" | tail -n1)
111
- if [ "$http_code" = "200" ]; then
112
- echo -e "${GREEN}✅ OK${NC} (HTTP $http_code)"
113
- else
114
- echo -e "${RED}❌ FAILED${NC} (HTTP $http_code)"
115
- fi
116
-
117
- # Test provider endpoints
118
- echo ""
119
- echo "🔌 Provider Endpoints:"
120
- test_endpoint "GET" "/api/providers" "" "Providers List"
121
-
122
- # Test datasets endpoints
123
- echo ""
124
- echo "📚 Datasets & Models Endpoints:"
125
- test_endpoint "GET" "/api/datasets/list" "" "Datasets List"
126
- test_endpoint "GET" "/api/models/list" "" "Models List"
127
-
128
- # POST endpoint for model test
129
- echo -n "Testing Model Test... "
130
- response=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/api/models/test" \
131
- -H "Content-Type: application/json" \
132
- -d '{"model":"crypto_sent_0","text":"Ethereum price surging!"}')
133
- http_code=$(echo "$response" | tail -n1)
134
- if [ "$http_code" = "200" ]; then
135
- echo -e "${GREEN}✅ OK${NC} (HTTP $http_code)"
136
- else
137
- echo -e "${RED}❌ FAILED${NC} (HTTP $http_code)"
138
- fi
139
-
140
- # Summary
141
- echo ""
142
- echo "======================================"
143
- echo "📊 Test Summary"
144
- echo "======================================"
145
- echo ""
146
- echo "✅ All critical endpoints tested"
147
- echo ""
148
- echo "🌐 Dashboard URLs:"
149
- echo " - Main: $BASE_URL/"
150
- echo " - Admin: $BASE_URL/admin.html"
151
- echo " - API Docs: $BASE_URL/docs"
152
- echo ""
153
- echo "🔌 WebSocket:"
154
- echo " - ws://$(echo $BASE_URL | sed 's|http://||')/ws"
155
- echo ""
156
- echo "💡 Next steps:"
157
- echo " 1. Open $BASE_URL/ in your browser"
158
- echo " 2. Check all dashboard tabs"
159
- echo " 3. Verify WebSocket connection (status indicator)"
160
- echo ""
161
- echo "======================================"
 
1
+ #!/bin/bash
2
+ # Script to test all HuggingFace Space endpoints
3
+
4
+ BASE_URL="https://really-amin-datasourceforcryptocurrency.hf.space"
5
+
6
+ echo "=================================="
7
+ echo "🧪 Testing HuggingFace Space API"
8
+ echo "=================================="
9
+ echo ""
10
+
11
+ # Color codes
12
+ GREEN='\033[0;32m'
13
+ RED='\033[0;31m'
14
+ YELLOW='\033[1;33m'
15
+ NC='\033[0m' # No Color
16
+
17
+ test_endpoint() {
18
+ local name=$1
19
+ local endpoint=$2
20
+
21
+ echo -n "Testing $name ... "
22
+ response=$(curl -s -w "\n%{http_code}" "$BASE_URL$endpoint" 2>&1)
23
+ http_code=$(echo "$response" | tail -n1)
24
+ body=$(echo "$response" | head -n-1)
25
+
26
+ if [ "$http_code" = "200" ]; then
27
+ echo -e "${GREEN} OK${NC} (HTTP $http_code)"
28
+ return 0
29
+ else
30
+ echo -e "${RED}✗ FAILED${NC} (HTTP $http_code)"
31
+ echo " Response: $body"
32
+ return 1
33
+ fi
34
+ }
35
+
36
+ # Core Endpoints
37
+ echo "📊 Core Endpoints"
38
+ echo "==================="
39
+ test_endpoint "Health" "/health"
40
+ test_endpoint "Info" "/info"
41
+ test_endpoint "Providers" "/api/providers"
42
+ echo ""
43
+
44
+ # Data Endpoints
45
+ echo "💰 Market Data Endpoints"
46
+ echo "========================="
47
+ test_endpoint "OHLCV (BTC)" "/api/ohlcv?symbol=BTCUSDT&interval=1h&limit=10"
48
+ test_endpoint "Top Prices" "/api/crypto/prices/top?limit=5"
49
+ test_endpoint "BTC Price" "/api/crypto/price/BTC"
50
+ test_endpoint "Market Overview" "/api/crypto/market-overview"
51
+ test_endpoint "Multiple Prices" "/api/market/prices?symbols=BTC,ETH,SOL"
52
+ test_endpoint "Market Data Prices" "/api/market-data/prices?symbols=BTC,ETH"
53
+ echo ""
54
+
55
+ # Analysis Endpoints
56
+ echo "📈 Analysis Endpoints"
57
+ echo "====================="
58
+ test_endpoint "Trading Signals" "/api/analysis/signals?symbol=BTCUSDT"
59
+ test_endpoint "SMC Analysis" "/api/analysis/smc?symbol=BTCUSDT"
60
+ test_endpoint "Scoring Snapshot" "/api/scoring/snapshot?symbol=BTCUSDT"
61
+ test_endpoint "All Signals" "/api/signals"
62
+ test_endpoint "Sentiment" "/api/sentiment"
63
+ echo ""
64
+
65
+ # System Endpoints
66
+ echo "⚙️ System Endpoints"
67
+ echo "===================="
68
+ test_endpoint "System Status" "/api/system/status"
69
+ test_endpoint "System Config" "/api/system/config"
70
+ test_endpoint "Categories" "/api/categories"
71
+ test_endpoint "Rate Limits" "/api/rate-limits"
72
+ test_endpoint "Logs" "/api/logs?limit=10"
73
+ test_endpoint "Alerts" "/api/alerts"
74
+ echo ""
75
+
76
+ # HuggingFace Endpoints
77
+ echo "🤗 HuggingFace Endpoints"
78
+ echo "========================="
79
+ test_endpoint "HF Health" "/api/hf/health"
80
+ test_endpoint "HF Registry" "/api/hf/registry?kind=models"
81
+ echo ""
82
+
83
+ echo "=================================="
84
+ echo "✅ Testing Complete!"
85
+ echo "=================================="
86
+ echo ""
87
+ echo "📖 Full documentation: ${BASE_URL}/docs"
88
+ echo "📋 API Guide: See HUGGINGFACE_API_GUIDE.md"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
__pycache__/config.cpython-313.pyc CHANGED
Binary files a/__pycache__/config.cpython-313.pyc and b/__pycache__/config.cpython-313.pyc differ
 
admin.html CHANGED
@@ -1,79 +1,525 @@
1
- <!DOCTYPE html>
2
  <html lang="en">
3
  <head>
4
  <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1" />
6
- <title>Crypto Intelligence Admin</title>
7
- <link rel="preconnect" href="https://fonts.googleapis.com" />
8
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
10
- <link rel="stylesheet" href="/static/css/unified-ui.css" />
11
- <link rel="stylesheet" href="/static/css/components.css" />
12
- <script defer src="/static/js/ui-feedback.js"></script>
13
- <script defer src="/static/js/admin-app.js"></script>
14
  </head>
15
- <body class="page page-admin">
16
- <header class="top-nav">
17
- <div class="branding">
18
- <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 12h16M12 4v16"/></svg>
19
- <div>
20
- <strong>Providers & Scheduling</strong>
21
- <small style="color:var(--ui-text-muted);letter-spacing:0.2em;">/api/providers · /api/logs</small>
22
- </div>
23
- </div>
24
- <nav class="nav-links">
25
- <a href="/dashboard">Dashboard</a>
26
- <a class="active" href="/admin">Admin</a>
27
- <a href="/hf_console">HF Console</a>
28
- <a href="/docs" target="_blank" rel="noreferrer">API Docs</a>
29
- </nav>
30
- </header>
31
-
32
- <main class="page-content">
33
- <section class="card">
34
- <div class="section-heading">
35
- <h2>Providers Health</h2>
36
- <span class="badge info" id="providers-count">Loading...</span>
37
- </div>
38
- <div class="table-card">
39
- <table>
40
- <thead>
41
- <tr><th>Provider</th><th>Status</th><th>Response (ms)</th><th>Category</th></tr>
42
- </thead>
43
- <tbody id="providers-table">
44
- <tr><td colspan="4">Loading providers...</td></tr>
45
- </tbody>
46
- </table>
47
- </div>
48
- </section>
49
-
50
- <section class="split-grid">
51
- <article class="card" id="provider-detail">
52
- <div class="section-heading">
53
- <h2>Provider Detail</h2>
54
- <span class="badge info" id="selected-provider">Select a provider</span>
55
  </div>
56
- <ul class="list" id="provider-detail-list"></ul>
57
- </article>
58
- <article class="card">
59
- <div class="section-heading">
60
- <h2>Configuration Snapshot</h2>
61
- <span class="badge info" id="config-summary">Loading...</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  </div>
63
- <ul class="list" id="config-list"></ul>
64
- </article>
65
- </section>
66
-
67
- <section class="split-grid">
68
- <article class="card">
69
- <div class="section-heading"><h2>Logs ( /api/logs )</h2><span class="badge info">Latest</span></div>
70
- <div id="logs-list" class="ws-stream"></div>
71
- </article>
72
- <article class="card">
73
- <div class="section-heading"><h2>Alerts ( /api/alerts )</h2><span class="badge info">Live</span></div>
74
- <div id="alerts-list"></div>
75
- </article>
76
- </section>
77
- </main>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  </body>
79
- </html>
 
1
+ <!DOCTYPE html>
2
  <html lang="en">
3
  <head>
4
  <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Crypto Intelligence Hub - HF Space</title>
7
+ <link rel="stylesheet" href="static/css/design-tokens.css" />
8
+ <link rel="stylesheet" href="static/css/design-system.css" />
9
+ <link rel="stylesheet" href="static/css/dashboard.css" />
10
+ <link rel="stylesheet" href="static/css/pro-dashboard.css" />
11
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js" defer></script>
 
 
12
  </head>
13
+ <body data-theme="dark">
14
+ <div class="app-shell">
15
+ <!-- Sidebar Navigation -->
16
+ <aside class="sidebar">
17
+ <div class="brand">
18
+ <strong>Crypto Intelligence Hub</strong>
19
+ <span class="env-pill">
20
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
21
+ <path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="1.5" />
22
+ <path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="1.5" />
23
+ <path d="M2 12L12 17L22 12" stroke="currentColor" stroke-width="1.5" />
24
+ </svg>
25
+ HF Space
26
+ </span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  </div>
28
+ <nav class="nav">
29
+ <button class="nav-button active" data-nav="page-overview">
30
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z" fill="currentColor"/></svg>
31
+ Overview
32
+ </button>
33
+ <button class="nav-button" data-nav="page-market">
34
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M3 17l6-6 4 4 8-8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
35
+ Market
36
+ </button>
37
+ <button class="nav-button" data-nav="page-chart">
38
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M3 3v18h18" stroke="currentColor" stroke-width="2"/><path d="M7 10l4-4 4 4 6-6" stroke="currentColor" stroke-width="2"/></svg>
39
+ Chart Lab
40
+ </button>
41
+ <button class="nav-button" data-nav="page-ai">
42
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="3" fill="currentColor"/><path d="M12 2v4m0 12v4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83M2 12h4m12 0h4M4.93 19.07l2.83-2.83m8.48-8.48l2.83-2.83" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
43
+ AI Advisor
44
+ </button>
45
+ <button class="nav-button" data-nav="page-news">
46
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10l6 6v8a2 2 0 01-2 2z" stroke="currentColor" stroke-width="2"/><path d="M7 10h6m-6 4h8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
47
+ News
48
+ </button>
49
+ <button class="nav-button" data-nav="page-providers">
50
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
51
+ Providers
52
+ </button>
53
+ <button class="nav-button" data-nav="page-datasets">
54
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M4 7h16M4 12h16M4 17h16" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
55
+ Datasets & Models
56
+ </button>
57
+ <button class="nav-button" data-nav="page-api">
58
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" fill="currentColor"/></svg>
59
+ API Explorer
60
+ </button>
61
+ <button class="nav-button" data-nav="page-debug">
62
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
63
+ Diagnostics
64
+ </button>
65
+ <button class="nav-button" data-nav="page-settings">
66
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2"/><path d="M12 1v6m0 6v6M5 5l4 4m6 6l4 4M1 12h6m6 0h6M5 19l4-4m6-6l4-4" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
67
+ Settings
68
+ </button>
69
+ </nav>
70
+ <div class="sidebar-footer">
71
+ <small>
72
+ Crypto Intelligence Hub<br />
73
+ <strong>10+ HF Models</strong> • <strong>14 Datasets</strong><br />
74
+ Real-time data • Ensemble sentiment
75
+ </small>
76
  </div>
77
+ </aside>
78
+
79
+ <!-- Main Content Area -->
80
+ <main class="main-area">
81
+ <!-- Top Bar with Status -->
82
+ <header class="topbar">
83
+ <div>
84
+ <h1>Crypto Intelligence Dashboard</h1>
85
+ <p class="text-muted">Live market data, AI-powered sentiment analysis, and comprehensive crypto intelligence</p>
86
+ </div>
87
+ <div class="status-group">
88
+ <div class="status-pill" data-api-health data-state="warn">
89
+ <span class="status-dot"></span>
90
+ <span>checking</span>
91
+ </div>
92
+ <div class="status-pill" data-ws-status data-state="warn">
93
+ <span class="status-dot"></span>
94
+ <span>connecting</span>
95
+ </div>
96
+ </div>
97
+ </header>
98
+
99
+ <div class="page-container">
100
+ <!-- ========== OVERVIEW PAGE ========== -->
101
+ <section id="page-overview" class="page active">
102
+ <div class="section-header">
103
+ <h2 class="section-title">Global Overview</h2>
104
+ <span class="chip">Powered by /api/market/stats</span>
105
+ </div>
106
+
107
+ <!-- Market Stats Cards -->
108
+ <div class="stats-grid" data-overview-stats>
109
+ <div class="glass-card stat-card">
110
+ <div class="stat-label">Total Market Cap</div>
111
+ <div class="stat-value">Loading...</div>
112
+ </div>
113
+ <div class="glass-card stat-card">
114
+ <div class="stat-label">24h Volume</div>
115
+ <div class="stat-value">Loading...</div>
116
+ </div>
117
+ <div class="glass-card stat-card">
118
+ <div class="stat-label">BTC Dominance</div>
119
+ <div class="stat-value">Loading...</div>
120
+ </div>
121
+ <div class="glass-card stat-card">
122
+ <div class="stat-label">Market Sentiment</div>
123
+ <div class="stat-value">Loading...</div>
124
+ </div>
125
+ </div>
126
+
127
+ <div class="grid-two">
128
+ <!-- Top Coins Table -->
129
+ <div class="glass-card">
130
+ <div class="section-header">
131
+ <h3>Top Coins</h3>
132
+ <span class="text-muted">By market cap</span>
133
+ </div>
134
+ <div class="table-wrapper">
135
+ <table>
136
+ <thead>
137
+ <tr>
138
+ <th>#</th>
139
+ <th>Symbol</th>
140
+ <th>Name</th>
141
+ <th>Price</th>
142
+ <th>24h %</th>
143
+ <th>Volume</th>
144
+ <th>Market Cap</th>
145
+ </tr>
146
+ </thead>
147
+ <tbody data-top-coins-body>
148
+ <tr><td colspan="7" style="text-align:center;padding:2rem;">Loading top coins...</td></tr>
149
+ </tbody>
150
+ </table>
151
+ </div>
152
+ </div>
153
+
154
+ <!-- Sentiment Chart -->
155
+ <div class="glass-card">
156
+ <div class="section-header">
157
+ <h3>Global Sentiment</h3>
158
+ <span class="text-muted">Ensemble HF models</span>
159
+ </div>
160
+ <canvas id="sentiment-chart" height="220"></canvas>
161
+ <div style="margin-top:1rem;font-size:0.875rem;color:var(--text-secondary);">
162
+ <strong>Models used:</strong> CryptoBERT, FinBERT, Twitter Sentiment<br>
163
+ <strong>Method:</strong> Majority voting with confidence scoring
164
+ </div>
165
+ </div>
166
+ </div>
167
+ </section>
168
+
169
+ <!-- ========== MARKET PAGE ========== -->
170
+ <section id="page-market" class="page">
171
+ <div class="section-header">
172
+ <h2 class="section-title">Market Intelligence</h2>
173
+ <div class="controls-bar">
174
+ <div class="input-chip">
175
+ <svg viewBox="0 0 24 24" width="16" height="16"><path d="M21 20l-5.6-5.6A6.5 6.5 0 1 0 15.4 16L21 21zM5 10.5a5.5 5.5 0 1 1 11 0a5.5 5.5 0 0 1-11 0z" fill="currentColor"/></svg>
176
+ <input type="text" placeholder="Search symbol" data-market-search />
177
+ </div>
178
+ <button class="ghost" data-refresh-market>Refresh</button>
179
+ </div>
180
+ </div>
181
+
182
+ <div class="glass-card">
183
+ <div class="table-wrapper">
184
+ <table>
185
+ <thead>
186
+ <tr>
187
+ <th>#</th>
188
+ <th>Symbol</th>
189
+ <th>Name</th>
190
+ <th>Price</th>
191
+ <th>24h %</th>
192
+ <th>Volume</th>
193
+ <th>Market Cap</th>
194
+ <th>Actions</th>
195
+ </tr>
196
+ </thead>
197
+ <tbody data-market-body>
198
+ <tr><td colspan="8" style="text-align:center;padding:2rem;">Loading market data...</td></tr>
199
+ </tbody>
200
+ </table>
201
+ </div>
202
+ </div>
203
+
204
+ <!-- Coin Detail Drawer -->
205
+ <div class="drawer" data-market-drawer style="display:none;">
206
+ <button class="ghost" data-close-drawer>Close</button>
207
+ <h3 data-drawer-symbol>—</h3>
208
+ <div data-drawer-stats></div>
209
+ <div class="glass-card" data-chart-wrapper>
210
+ <canvas id="market-detail-chart" height="180"></canvas>
211
+ </div>
212
+ <div class="glass-card">
213
+ <h4>AI Sentiment Analysis</h4>
214
+ <div data-drawer-sentiment></div>
215
+ </div>
216
+ </div>
217
+ </section>
218
+
219
+ <!-- ========== CHART LAB PAGE ========== -->
220
+ <section id="page-chart" class="page">
221
+ <div class="section-header">
222
+ <h2 class="section-title">Chart Lab</h2>
223
+ <div class="controls-bar">
224
+ <select data-chart-symbol>
225
+ <option value="BTC">Bitcoin (BTC)</option>
226
+ <option value="ETH">Ethereum (ETH)</option>
227
+ <option value="SOL">Solana (SOL)</option>
228
+ <option value="BNB">BNB</option>
229
+ <option value="XRP">Ripple (XRP)</option>
230
+ <option value="ADA">Cardano (ADA)</option>
231
+ </select>
232
+ <div class="input-chip">
233
+ <button class="ghost active" data-chart-timeframe="7d">7D</button>
234
+ <button class="ghost" data-chart-timeframe="30d">30D</button>
235
+ <button class="ghost" data-chart-timeframe="90d">90D</button>
236
+ </div>
237
+ </div>
238
+ </div>
239
+
240
+ <div class="glass-card">
241
+ <canvas id="chart-lab-canvas" height="300"></canvas>
242
+ </div>
243
+
244
+ <div class="glass-card">
245
+ <h4>Technical Analysis</h4>
246
+ <div class="controls-bar">
247
+ <label><input type="checkbox" data-indicator value="MA20" checked /> MA 20</label>
248
+ <label><input type="checkbox" data-indicator value="MA50" /> MA 50</label>
249
+ <label><input type="checkbox" data-indicator value="RSI" /> RSI</label>
250
+ <label><input type="checkbox" data-indicator value="Volume" /> Volume</label>
251
+ </div>
252
+ <button class="primary" data-run-analysis>🤖 Analyze with AI</button>
253
+ <div data-ai-insights class="ai-insights" style="margin-top:1rem;"></div>
254
+ </div>
255
+ </section>
256
+
257
+ <!-- ========== AI ADVISOR PAGE ========== -->
258
+ <section id="page-ai" class="page">
259
+ <div class="section-header">
260
+ <h2 class="section-title">AI-Powered Sentiment & Advisory</h2>
261
+ <span class="chip">Ensemble: CryptoBERT + FinBERT + Social</span>
262
+ </div>
263
+
264
+ <div class="glass-card">
265
+ <h4>Sentiment Analysis</h4>
266
+ <form data-sentiment-form>
267
+ <label>Text to Analyze
268
+ <textarea name="text" rows="4" placeholder="Enter crypto-related text, news headline, or social media post for sentiment analysis..."></textarea>
269
+ </label>
270
+ <button class="primary" type="submit">🧠 Analyze Sentiment</button>
271
+ </form>
272
+ <div data-sentiment-result style="margin-top:1rem;"></div>
273
+ </div>
274
+
275
+ <div class="glass-card" style="margin-top:1.5rem;">
276
+ <h4>AI Query Interface</h4>
277
+ <form data-query-form>
278
+ <label>Ask a Question
279
+ <textarea name="query" rows="3" placeholder="e.g., What is the current Bitcoin price? or Analyze Ethereum trend"></textarea>
280
+ </label>
281
+ <button class="primary" type="submit">🔍 Submit Query</button>
282
+ </form>
283
+ <div data-query-result style="margin-top:1rem;"></div>
284
+ </div>
285
+
286
+ <div class="inline-message inline-info">
287
+ ⚠️ AI-generated outputs are experimental and should not be considered financial advice.
288
+ </div>
289
+ </section>
290
+
291
+ <!-- ========== NEWS PAGE ========== -->
292
+ <section id="page-news" class="page">
293
+ <div class="section-header">
294
+ <h2 class="section-title">News & Headlines</h2>
295
+ <span class="chip">With AI sentiment analysis</span>
296
+ </div>
297
+
298
+ <div class="controls-bar">
299
+ <input type="text" placeholder="Search headlines..." data-news-search />
300
+ <input type="text" placeholder="Filter by symbol (e.g., BTC)" data-news-symbol />
301
+ <button class="ghost" data-refresh-news>Refresh</button>
302
+ </div>
303
+
304
+ <div class="glass-card">
305
+ <div class="table-wrapper">
306
+ <table>
307
+ <thead>
308
+ <tr>
309
+ <th>Title</th>
310
+ <th>Source</th>
311
+ <th>Symbols</th>
312
+ <th>Sentiment</th>
313
+ <th>Time</th>
314
+ <th>Actions</th>
315
+ </tr>
316
+ </thead>
317
+ <tbody data-news-body>
318
+ <tr><td colspan="6" style="text-align:center;padding:2rem;">Loading news...</td></tr>
319
+ </tbody>
320
+ </table>
321
+ </div>
322
+ </div>
323
+ </section>
324
+
325
+ <!-- ========== PROVIDERS PAGE ========== -->
326
+ <section id="page-providers" class="page">
327
+ <div class="section-header">
328
+ <h2 class="section-title">API Providers</h2>
329
+ <span class="chip">95+ data sources</span>
330
+ </div>
331
+
332
+ <div class="glass-card">
333
+ <div class="table-wrapper">
334
+ <table>
335
+ <thead>
336
+ <tr>
337
+ <th>Provider</th>
338
+ <th>Category</th>
339
+ <th>Type</th>
340
+ <th>Status</th>
341
+ <th>Response Time</th>
342
+ </tr>
343
+ </thead>
344
+ <tbody data-providers-body>
345
+ <tr><td colspan="5" style="text-align:center;padding:2rem;">Loading providers...</td></tr>
346
+ </tbody>
347
+ </table>
348
+ </div>
349
+ </div>
350
+ </section>
351
+
352
+ <!-- ========== DATASETS & MODELS PAGE ========== -->
353
+ <section id="page-datasets" class="page">
354
+ <div class="section-header">
355
+ <h2 class="section-title">HuggingFace Datasets & Models</h2>
356
+ </div>
357
+
358
+ <div class="grid-two">
359
+ <!-- Datasets -->
360
+ <div class="glass-card">
361
+ <h4>📊 Crypto Datasets (14+)</h4>
362
+ <div class="table-wrapper">
363
+ <table>
364
+ <thead>
365
+ <tr>
366
+ <th>Dataset</th>
367
+ <th>Category</th>
368
+ <th>Actions</th>
369
+ </tr>
370
+ </thead>
371
+ <tbody data-datasets-body>
372
+ <tr><td colspan="3" style="text-align:center;padding:1rem;">Loading...</td></tr>
373
+ </tbody>
374
+ </table>
375
+ </div>
376
+ </div>
377
+
378
+ <!-- Models -->
379
+ <div class="glass-card">
380
+ <h4>🤖 AI Models (10+)</h4>
381
+ <div class="table-wrapper">
382
+ <table>
383
+ <thead>
384
+ <tr>
385
+ <th>Model</th>
386
+ <th>Task</th>
387
+ <th>Status</th>
388
+ </tr>
389
+ </thead>
390
+ <tbody data-models-body>
391
+ <tr><td colspan="3" style="text-align:center;padding:1rem;">Loading...</td></tr>
392
+ </tbody>
393
+ </table>
394
+ </div>
395
+ </div>
396
+ </div>
397
+
398
+ <!-- Model Test Form -->
399
+ <div class="glass-card" style="margin-top:1.5rem;">
400
+ <h4>🧪 Test a Model</h4>
401
+ <form data-model-test-form>
402
+ <div class="grid-two">
403
+ <label>Model
404
+ <select name="model" data-model-select>
405
+ <option value="">Select a model...</option>
406
+ </select>
407
+ </label>
408
+ <label>Input Text
409
+ <textarea name="input" rows="3" placeholder="Enter text to test the model..."></textarea>
410
+ </label>
411
+ </div>
412
+ <button class="primary" type="submit">Run Test</button>
413
+ </form>
414
+ <div data-model-test-output style="margin-top:1rem;"></div>
415
+ </div>
416
+ </section>
417
+
418
+ <!-- ========== API EXPLORER PAGE ========== -->
419
+ <section id="page-api" class="page">
420
+ <div class="section-header">
421
+ <h2 class="section-title">API Explorer</h2>
422
+ <span class="chip">15+ endpoints</span>
423
+ </div>
424
+
425
+ <div class="glass-card">
426
+ <h4>Test Endpoint</h4>
427
+ <form data-api-form>
428
+ <div class="grid-two">
429
+ <label>Endpoint
430
+ <select data-endpoint-select>
431
+ <option value="0">/api/health</option>
432
+ </select>
433
+ </label>
434
+ <label>Method
435
+ <select data-method-select>
436
+ <option value="GET">GET</option>
437
+ <option value="POST">POST</option>
438
+ </select>
439
+ </label>
440
+ </div>
441
+ <div data-api-description style="margin:0.5rem 0;font-size:0.875rem;color:var(--text-secondary);"></div>
442
+ <div data-api-path style="margin:0.5rem 0;font-family:monospace;font-size:0.875rem;"></div>
443
+ <label>Body (JSON)
444
+ <textarea data-body-input rows="4"></textarea>
445
+ </label>
446
+ <button class="primary" type="submit">Send Request</button>
447
+ </form>
448
+ <div data-api-response style="margin-top:1rem;"></div>
449
+ </div>
450
+ </section>
451
+
452
+ <!-- ========== DIAGNOSTICS PAGE ========== -->
453
+ <section id="page-debug" class="page">
454
+ <div class="section-header">
455
+ <h2 class="section-title">System Diagnostics</h2>
456
+ </div>
457
+
458
+ <div class="grid-two">
459
+ <div class="glass-card">
460
+ <h4>Health Status</h4>
461
+ <div data-health-info>Checking...</div>
462
+ </div>
463
+
464
+ <div class="glass-card">
465
+ <h4>WebSocket Status</h4>
466
+ <div data-ws-info>Checking...</div>
467
+ </div>
468
+ </div>
469
+
470
+ <div class="glass-card" style="margin-top:1.5rem;">
471
+ <h4>Request Logs</h4>
472
+ <div data-request-logs style="max-height:400px;overflow-y:auto;font-family:monospace;font-size:0.875rem;">
473
+ <!-- Populated by JS -->
474
+ </div>
475
+ </div>
476
+ </section>
477
+
478
+ <!-- ========== SETTINGS PAGE ========== -->
479
+ <section id="page-settings" class="page">
480
+ <div class="section-header">
481
+ <h2 class="section-title">Settings</h2>
482
+ </div>
483
+
484
+ <div class="glass-card">
485
+ <h4>Display Settings</h4>
486
+ <div class="grid-two">
487
+ <label class="input-chip">Dark Theme
488
+ <div class="toggle">
489
+ <input type="checkbox" data-theme-toggle checked />
490
+ <span></span>
491
+ </div>
492
+ </label>
493
+ <label class="input-chip">Compact Layout
494
+ <div class="toggle">
495
+ <input type="checkbox" data-layout-toggle />
496
+ <span></span>
497
+ </div>
498
+ </label>
499
+ </div>
500
+ </div>
501
+
502
+ <div class="glass-card" style="margin-top:1.5rem;">
503
+ <h4>Refresh Intervals</h4>
504
+ <div class="grid-two">
505
+ <label>Market Data (seconds)
506
+ <input type="number" min="10" step="5" value="30" data-market-interval />
507
+ </label>
508
+ <label>News Feed (seconds)
509
+ <input type="number" min="30" step="10" value="60" data-news-interval />
510
+ </label>
511
+ </div>
512
+ </div>
513
+
514
+ <div class="inline-message inline-info" style="margin-top:1.5rem;">
515
+ Settings are stored locally in your browser.
516
+ </div>
517
+ </section>
518
+ </div>
519
+ </main>
520
+ </div>
521
+
522
+ <!-- Load App JS as ES6 Module -->
523
+ <script type="module" src="static/js/app.js"></script>
524
  </body>
525
+ </html>
ai_models.py CHANGED
@@ -3,33 +3,12 @@
3
 
4
  from __future__ import annotations
5
  import logging
 
6
  import threading
7
  from dataclasses import dataclass
8
  from typing import Any, Dict, List, Mapping, Optional, Sequence
9
  from config import HUGGINGFACE_MODELS, get_settings
10
 
11
- # Set environment variables to avoid TensorFlow/Keras issues
12
- # We'll force PyTorch framework instead
13
- import os
14
- import sys
15
-
16
- # Completely disable TensorFlow to force PyTorch
17
- os.environ.setdefault('TRANSFORMERS_NO_ADVISORY_WARNINGS', '1')
18
- os.environ.setdefault('TRANSFORMERS_VERBOSITY', 'error')
19
- os.environ.setdefault('TF_CPP_MIN_LOG_LEVEL', '3')
20
- os.environ.setdefault('TRANSFORMERS_FRAMEWORK', 'pt')
21
-
22
- # Mock tf_keras to prevent transformers from trying to import it
23
- # This prevents the broken tf-keras installation from causing errors
24
- class TfKerasMock:
25
- """Mock tf_keras to prevent import errors when transformers checks for TensorFlow"""
26
- pass
27
-
28
- # Add mock to sys.modules before transformers imports
29
- sys.modules['tf_keras'] = TfKerasMock()
30
- sys.modules['tf_keras.src'] = TfKerasMock()
31
- sys.modules['tf_keras.src.utils'] = TfKerasMock()
32
-
33
  try:
34
  from transformers import pipeline
35
  TRANSFORMERS_AVAILABLE = True
@@ -39,38 +18,31 @@ except ImportError:
39
  logger = logging.getLogger(__name__)
40
  settings = get_settings()
41
 
42
- HF_MODE = os.getenv("HF_MODE", "off").lower()
43
- HF_TOKEN_ENV = os.getenv("HF_TOKEN")
 
 
44
 
45
  if HF_MODE not in ("off", "public", "auth"):
46
  HF_MODE = "off"
47
- logger.warning(f"Invalid HF_MODE, defaulting to 'off'")
48
 
49
  if HF_MODE == "auth" and not HF_TOKEN_ENV:
50
  HF_MODE = "off"
51
- logger.warning("HF_MODE='auth' but HF_TOKEN not set, defaulting to 'off'")
52
 
53
- ACTIVE_MODELS = [
54
- "ElKulako/cryptobert",
55
- "kk08/CryptoBERT",
56
- "ProsusAI/finbert"
57
  ]
58
-
59
- LEGACY_MODELS = [
60
- "burakutf/finetuned-finbert-crypto",
61
- "mathugo/crypto_news_bert",
62
  "svalabs/twitter-xlm-roberta-bitcoin-sentiment",
63
- "mayurjadhav/crypto-sentiment-model",
64
- "cardiffnlp/twitter-roberta-base-sentiment",
65
- "mrm8488/distilroberta-finetuned-financial-news-sentiment-analysis",
66
- "agarkovv/CryptoTrader-LM"
67
  ]
68
-
69
- CRYPTO_SENTIMENT_MODELS = ACTIVE_MODELS[:2] + LEGACY_MODELS[:2]
70
- SOCIAL_SENTIMENT_MODELS = LEGACY_MODELS[2:4]
71
- FINANCIAL_SENTIMENT_MODELS = [ACTIVE_MODELS[2]] + [LEGACY_MODELS[4]]
72
- NEWS_SENTIMENT_MODELS = [LEGACY_MODELS[5]]
73
- DECISION_MODELS = [LEGACY_MODELS[6]]
74
 
75
  @dataclass(frozen=True)
76
  class PipelineSpec:
@@ -92,29 +64,26 @@ for lk in ["sentiment_twitter", "sentiment_financial", "summarization", "crypto_
92
  category="legacy"
93
  )
94
 
95
- for i, mid in enumerate(ACTIVE_MODELS):
96
- MODEL_SPECS[f"active_{i}"] = PipelineSpec(
97
- key=f"active_{i}", task="sentiment-analysis", model_id=mid,
98
- category="crypto_sentiment" if i < 2 else "financial_sentiment",
99
- requires_auth=("ElKulako" in mid)
100
- )
101
-
102
  for i, mid in enumerate(CRYPTO_SENTIMENT_MODELS):
103
  MODEL_SPECS[f"crypto_sent_{i}"] = PipelineSpec(
104
  key=f"crypto_sent_{i}", task="sentiment-analysis", model_id=mid,
105
  category="crypto_sentiment", requires_auth=("ElKulako" in mid)
106
  )
107
 
 
108
  for i, mid in enumerate(SOCIAL_SENTIMENT_MODELS):
109
  MODEL_SPECS[f"social_sent_{i}"] = PipelineSpec(
110
  key=f"social_sent_{i}", task="sentiment-analysis", model_id=mid, category="social_sentiment"
111
  )
112
 
 
113
  for i, mid in enumerate(FINANCIAL_SENTIMENT_MODELS):
114
  MODEL_SPECS[f"financial_sent_{i}"] = PipelineSpec(
115
  key=f"financial_sent_{i}", task="sentiment-analysis", model_id=mid, category="financial_sentiment"
116
  )
117
 
 
118
  for i, mid in enumerate(NEWS_SENTIMENT_MODELS):
119
  MODEL_SPECS[f"news_sent_{i}"] = PipelineSpec(
120
  key=f"news_sent_{i}", task="sentiment-analysis", model_id=mid, category="news_sentiment"
@@ -129,6 +98,8 @@ class ModelRegistry:
129
  self._initialized = False
130
 
131
  def get_pipeline(self, key: str):
 
 
132
  if not TRANSFORMERS_AVAILABLE:
133
  raise ModelNotAvailable("transformers not installed")
134
  if key not in MODEL_SPECS:
@@ -142,118 +113,46 @@ class ModelRegistry:
142
  if key in self._pipelines:
143
  return self._pipelines[key]
144
 
145
- if HF_MODE == "off":
146
- raise ModelNotAvailable("HF_MODE=off")
147
-
148
- token_value = None
149
- if HF_MODE == "auth":
150
- token_value = HF_TOKEN_ENV or settings.hf_token
151
- elif HF_MODE == "public":
152
- token_value = None
153
-
154
- if spec.requires_auth and not token_value:
155
- raise ModelNotAvailable("Model requires auth but no token available")
156
-
157
- logger.info(f"Loading model: {spec.model_id} (mode: {HF_MODE})")
158
  try:
159
- pipeline_kwargs = {
160
- 'task': spec.task,
161
- 'model': spec.model_id,
162
- 'tokenizer': spec.model_id,
163
- 'framework': 'pt',
164
- 'device': -1,
165
- }
166
- pipeline_kwargs['token'] = token_value
167
-
168
- self._pipelines[key] = pipeline(**pipeline_kwargs)
169
  except Exception as e:
170
- error_msg = str(e)
171
- error_lower = error_msg.lower()
172
-
173
- try:
174
- from huggingface_hub.errors import RepositoryNotFoundError, HfHubHTTPError
175
- hf_errors = (RepositoryNotFoundError, HfHubHTTPError)
176
- except ImportError:
177
- hf_errors = ()
178
-
179
- is_auth_error = any(kw in error_lower for kw in ['401', 'unauthorized', 'repository not found', 'expired', 'token'])
180
- is_hf_error = isinstance(e, hf_errors) or is_auth_error
181
-
182
- if is_hf_error:
183
- logger.warning(f"HF error for {spec.model_id}: {type(e).__name__}")
184
- raise ModelNotAvailable(f"HF error: {spec.model_id}") from e
185
-
186
- if any(kw in error_lower for kw in ['keras', 'tensorflow', 'tf_keras', 'framework']):
187
- try:
188
- pipeline_kwargs['torch_dtype'] = 'float32'
189
- self._pipelines[key] = pipeline(**pipeline_kwargs)
190
- return self._pipelines[key]
191
- except Exception:
192
- raise ModelNotAvailable(f"Framework error: {spec.model_id}") from e
193
-
194
- raise ModelNotAvailable(f"Load failed: {spec.model_id}") from e
195
 
196
  return self._pipelines[key]
197
-
198
- def get_loaded_models(self):
199
- """Get list of all loaded model keys"""
200
- return list(self._pipelines.keys())
201
-
202
- def get_available_sentiment_models(self):
203
- """Get list of all available sentiment model keys"""
204
- return [key for key in MODEL_SPECS.keys() if "sent" in key or "sentiment" in key]
205
 
206
  def initialize_models(self):
207
  if self._initialized:
208
  return {"status": "already_initialized", "mode": HF_MODE, "models_loaded": len(self._pipelines)}
209
-
210
  if HF_MODE == "off":
211
- self._initialized = True
212
- return {"status": "disabled", "mode": "off", "models_loaded": 0, "loaded": [], "failed": []}
213
-
214
  if not TRANSFORMERS_AVAILABLE:
215
  return {"status": "transformers_not_available", "mode": HF_MODE, "models_loaded": 0}
216
 
217
  loaded, failed = [], []
218
- active_keys = [f"active_{i}" for i in range(len(ACTIVE_MODELS))]
219
-
220
- for key in active_keys:
221
  try:
222
  self.get_pipeline(key)
223
  loaded.append(key)
224
- except ModelNotAvailable as e:
225
- failed.append((key, str(e)[:100]))
226
  except Exception as e:
227
- error_msg = str(e)[:100]
228
- failed.append((key, error_msg))
229
 
230
  self._initialized = True
231
- status = "initialized" if loaded else "partial"
232
- return {"status": status, "mode": HF_MODE, "models_loaded": len(loaded), "loaded": loaded, "failed": failed}
233
 
234
  _registry = ModelRegistry()
235
 
236
- AI_MODELS_SUMMARY = {"status": "not_initialized", "mode": "off", "models_loaded": 0, "loaded": [], "failed": []}
237
-
238
- def initialize_models():
239
- global AI_MODELS_SUMMARY
240
- result = _registry.initialize_models()
241
- AI_MODELS_SUMMARY = result
242
- return result
243
 
244
  def ensemble_crypto_sentiment(text: str) -> Dict[str, Any]:
245
- if not TRANSFORMERS_AVAILABLE or HF_MODE == "off":
246
- return {"label": "neutral", "confidence": 0.0, "scores": {}, "model_count": 0, "error": "HF disabled" if HF_MODE == "off" else "transformers N/A"}
247
 
248
  results, labels_count, total_conf = {}, {"bullish": 0, "bearish": 0, "neutral": 0}, 0.0
249
 
250
- loaded_keys = _registry.get_loaded_models()
251
- available_keys = [key for key in loaded_keys if "sent" in key or "sentiment" in key or key.startswith("active_")]
252
-
253
- if not available_keys:
254
- return {"label": "neutral", "confidence": 0.0, "scores": {}, "model_count": 0, "error": "No models loaded"}
255
-
256
- for key in available_keys:
257
  try:
258
  pipe = _registry.get_pipeline(key)
259
  res = pipe(text[:512])
@@ -264,20 +163,15 @@ def ensemble_crypto_sentiment(text: str) -> Dict[str, Any]:
264
 
265
  mapped = "bullish" if "POSITIVE" in label or "BULLISH" in label else ("bearish" if "NEGATIVE" in label or "BEARISH" in label else "neutral")
266
 
267
- spec = MODEL_SPECS.get(key)
268
- if spec:
269
- results[spec.model_id] = {"label": mapped, "score": score}
270
- else:
271
- results[key] = {"label": mapped, "score": score}
272
  labels_count[mapped] += 1
273
  total_conf += score
274
- except ModelNotAvailable:
275
- continue
276
  except Exception as e:
277
  logger.warning(f"Ensemble failed for {key}: {e}")
278
 
279
  if not results:
280
- return {"label": "neutral", "confidence": 0.0, "scores": {}, "model_count": 0, "error": "All models failed"}
281
 
282
  final = max(labels_count, key=labels_count.get)
283
  avg_conf = total_conf / len(results)
@@ -335,18 +229,26 @@ def analyze_news_item(item: Dict[str, Any]):
335
  def get_model_info():
336
  return {
337
  "transformers_available": TRANSFORMERS_AVAILABLE,
338
- "hf_mode": HF_MODE,
339
- "hf_token_configured": bool(HF_TOKEN_ENV or settings.hf_token) if HF_MODE == "auth" else False,
340
  "models_initialized": _registry._initialized,
341
  "models_loaded": len(_registry._pipelines),
342
- "active_models": ACTIVE_MODELS,
 
 
 
 
 
 
343
  "total_models": len(MODEL_SPECS)
344
  }
345
 
346
  def registry_status():
347
  return {
 
348
  "initialized": _registry._initialized,
349
  "pipelines_loaded": len(_registry._pipelines),
350
  "available_models": list(MODEL_SPECS.keys()),
351
- "transformers_available": TRANSFORMERS_AVAILABLE
 
 
352
  }
 
3
 
4
  from __future__ import annotations
5
  import logging
6
+ import os
7
  import threading
8
  from dataclasses import dataclass
9
  from typing import Any, Dict, List, Mapping, Optional, Sequence
10
  from config import HUGGINGFACE_MODELS, get_settings
11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  try:
13
  from transformers import pipeline
14
  TRANSFORMERS_AVAILABLE = True
 
18
  logger = logging.getLogger(__name__)
19
  settings = get_settings()
20
 
21
+ HF_TOKEN_ENV = os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACE_TOKEN")
22
+ _is_hf_space = bool(os.getenv("SPACE_ID"))
23
+ _default_hf_mode = "public" if _is_hf_space else "off"
24
+ HF_MODE = os.getenv("HF_MODE", _default_hf_mode).lower()
25
 
26
  if HF_MODE not in ("off", "public", "auth"):
27
  HF_MODE = "off"
28
+ logger.warning(f"Invalid HF_MODE, resetting to 'off'")
29
 
30
  if HF_MODE == "auth" and not HF_TOKEN_ENV:
31
  HF_MODE = "off"
32
+ logger.warning("HF_MODE='auth' but no HF_TOKEN found, resetting to 'off'")
33
 
34
+ # Extended Model Catalog
35
+ CRYPTO_SENTIMENT_MODELS = [
36
+ "ElKulako/cryptobert", "kk08/CryptoBERT",
37
+ "burakutf/finetuned-finbert-crypto", "mathugo/crypto_news_bert"
38
  ]
39
+ SOCIAL_SENTIMENT_MODELS = [
 
 
 
40
  "svalabs/twitter-xlm-roberta-bitcoin-sentiment",
41
+ "mayurjadhav/crypto-sentiment-model"
 
 
 
42
  ]
43
+ FINANCIAL_SENTIMENT_MODELS = ["ProsusAI/finbert", "cardiffnlp/twitter-roberta-base-sentiment"]
44
+ NEWS_SENTIMENT_MODELS = ["mrm8488/distilroberta-finetuned-financial-news-sentiment-analysis"]
45
+ DECISION_MODELS = ["agarkovv/CryptoTrader-LM"]
 
 
 
46
 
47
  @dataclass(frozen=True)
48
  class PipelineSpec:
 
64
  category="legacy"
65
  )
66
 
67
+ # Crypto sentiment
 
 
 
 
 
 
68
  for i, mid in enumerate(CRYPTO_SENTIMENT_MODELS):
69
  MODEL_SPECS[f"crypto_sent_{i}"] = PipelineSpec(
70
  key=f"crypto_sent_{i}", task="sentiment-analysis", model_id=mid,
71
  category="crypto_sentiment", requires_auth=("ElKulako" in mid)
72
  )
73
 
74
+ # Social
75
  for i, mid in enumerate(SOCIAL_SENTIMENT_MODELS):
76
  MODEL_SPECS[f"social_sent_{i}"] = PipelineSpec(
77
  key=f"social_sent_{i}", task="sentiment-analysis", model_id=mid, category="social_sentiment"
78
  )
79
 
80
+ # Financial
81
  for i, mid in enumerate(FINANCIAL_SENTIMENT_MODELS):
82
  MODEL_SPECS[f"financial_sent_{i}"] = PipelineSpec(
83
  key=f"financial_sent_{i}", task="sentiment-analysis", model_id=mid, category="financial_sentiment"
84
  )
85
 
86
+ # News
87
  for i, mid in enumerate(NEWS_SENTIMENT_MODELS):
88
  MODEL_SPECS[f"news_sent_{i}"] = PipelineSpec(
89
  key=f"news_sent_{i}", task="sentiment-analysis", model_id=mid, category="news_sentiment"
 
98
  self._initialized = False
99
 
100
  def get_pipeline(self, key: str):
101
+ if HF_MODE == "off":
102
+ raise ModelNotAvailable("HF_MODE=off")
103
  if not TRANSFORMERS_AVAILABLE:
104
  raise ModelNotAvailable("transformers not installed")
105
  if key not in MODEL_SPECS:
 
113
  if key in self._pipelines:
114
  return self._pipelines[key]
115
 
116
+ auth = HF_TOKEN_ENV if (HF_MODE == "auth" and spec.requires_auth) else (HF_TOKEN_ENV if spec.requires_auth else None)
117
+ logger.info(f"Loading model: {spec.model_id}")
 
 
 
 
 
 
 
 
 
 
 
118
  try:
119
+ self._pipelines[key] = pipeline(spec.task, model=spec.model_id, tokenizer=spec.model_id, use_auth_token=auth)
 
 
 
 
 
 
 
 
 
120
  except Exception as e:
121
+ logger.exception(f"Failed to load {spec.model_id}")
122
+ raise ModelNotAvailable(str(e)) from e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
 
124
  return self._pipelines[key]
 
 
 
 
 
 
 
 
125
 
126
  def initialize_models(self):
127
  if self._initialized:
128
  return {"status": "already_initialized", "mode": HF_MODE, "models_loaded": len(self._pipelines)}
 
129
  if HF_MODE == "off":
130
+ return {"status": "disabled", "mode": HF_MODE, "models_loaded": 0, "error": "HF_MODE=off"}
 
 
131
  if not TRANSFORMERS_AVAILABLE:
132
  return {"status": "transformers_not_available", "mode": HF_MODE, "models_loaded": 0}
133
 
134
  loaded, failed = [], []
135
+ for key in ["crypto_sent_0", "financial_sent_0"]:
 
 
136
  try:
137
  self.get_pipeline(key)
138
  loaded.append(key)
 
 
139
  except Exception as e:
140
+ failed.append((key, str(e)))
 
141
 
142
  self._initialized = True
143
+ return {"status": "ok" if loaded else "partial", "mode": HF_MODE, "models_loaded": len(loaded), "loaded": loaded, "failed": failed}
 
144
 
145
  _registry = ModelRegistry()
146
 
147
+ def initialize_models(): return _registry.initialize_models()
 
 
 
 
 
 
148
 
149
  def ensemble_crypto_sentiment(text: str) -> Dict[str, Any]:
150
+ if not TRANSFORMERS_AVAILABLE:
151
+ return {"label": "neutral", "confidence": 0.0, "scores": {}, "model_count": 0, "error": "transformers N/A"}
152
 
153
  results, labels_count, total_conf = {}, {"bullish": 0, "bearish": 0, "neutral": 0}, 0.0
154
 
155
+ for key in ["crypto_sent_0", "crypto_sent_1"]:
 
 
 
 
 
 
156
  try:
157
  pipe = _registry.get_pipeline(key)
158
  res = pipe(text[:512])
 
163
 
164
  mapped = "bullish" if "POSITIVE" in label or "BULLISH" in label else ("bearish" if "NEGATIVE" in label or "BEARISH" in label else "neutral")
165
 
166
+ spec = MODEL_SPECS[key]
167
+ results[spec.model_id] = {"label": mapped, "score": score}
 
 
 
168
  labels_count[mapped] += 1
169
  total_conf += score
 
 
170
  except Exception as e:
171
  logger.warning(f"Ensemble failed for {key}: {e}")
172
 
173
  if not results:
174
+ return {"label": "neutral", "confidence": 0.0, "scores": {}, "model_count": 0}
175
 
176
  final = max(labels_count, key=labels_count.get)
177
  avg_conf = total_conf / len(results)
 
229
  def get_model_info():
230
  return {
231
  "transformers_available": TRANSFORMERS_AVAILABLE,
232
+ "hf_auth_configured": bool(settings.hf_token),
 
233
  "models_initialized": _registry._initialized,
234
  "models_loaded": len(_registry._pipelines),
235
+ "model_catalog": {
236
+ "crypto_sentiment": CRYPTO_SENTIMENT_MODELS,
237
+ "social_sentiment": SOCIAL_SENTIMENT_MODELS,
238
+ "financial_sentiment": FINANCIAL_SENTIMENT_MODELS,
239
+ "news_sentiment": NEWS_SENTIMENT_MODELS,
240
+ "decision": DECISION_MODELS
241
+ },
242
  "total_models": len(MODEL_SPECS)
243
  }
244
 
245
  def registry_status():
246
  return {
247
+ "ok": HF_MODE != "off",
248
  "initialized": _registry._initialized,
249
  "pipelines_loaded": len(_registry._pipelines),
250
  "available_models": list(MODEL_SPECS.keys()),
251
+ "transformers_available": TRANSFORMERS_AVAILABLE,
252
+ "hf_mode": HF_MODE,
253
+ "error": "HF_MODE=off" if HF_MODE == "off" else None
254
  }
api-resources/crypto_resources_unified_2025-11-11.json CHANGED
The diff for this file is too large to render. See raw diff
 
api_server_extended.py CHANGED
@@ -16,18 +16,19 @@ from datetime import datetime
16
  from contextlib import asynccontextmanager
17
  from collections import defaultdict
18
 
19
- from fastapi import FastAPI, HTTPException
20
  from fastapi.middleware.cors import CORSMiddleware
21
  from fastapi.responses import JSONResponse, FileResponse, HTMLResponse
22
  from fastapi.staticfiles import StaticFiles
 
23
  from pydantic import BaseModel
24
 
25
  # Environment variables
26
  USE_MOCK_DATA = os.getenv("USE_MOCK_DATA", "false").lower() == "true"
27
  PORT = int(os.getenv("PORT", "7860"))
28
 
29
- # Paths
30
- WORKSPACE_ROOT = Path("/workspace" if Path("/workspace").exists() else ".")
31
  DB_PATH = WORKSPACE_ROOT / "data" / "database" / "crypto_monitor.db"
32
  LOG_DIR = WORKSPACE_ROOT / "logs"
33
  PROVIDERS_CONFIG_PATH = WORKSPACE_ROOT / "providers_config_extended.json"
@@ -67,8 +68,40 @@ def init_database():
67
  )
68
  """)
69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  cursor.execute("CREATE INDEX IF NOT EXISTS idx_prices_symbol ON prices(symbol)")
71
  cursor.execute("CREATE INDEX IF NOT EXISTS idx_prices_timestamp ON prices(timestamp)")
 
 
 
72
 
73
  conn.commit()
74
  conn.close()
@@ -210,6 +243,16 @@ async def lifespan(app: FastAPI):
210
  if apl_report:
211
  print(f"✓ Loaded APL report with validation data")
212
 
 
 
 
 
 
 
 
 
 
 
213
  print(f"✓ Server ready on port {PORT}")
214
  print("=" * 80)
215
  yield
@@ -233,6 +276,17 @@ app.add_middleware(
233
  allow_headers=["*"],
234
  )
235
 
 
 
 
 
 
 
 
 
 
 
 
236
  # Mount static files
237
  try:
238
  static_path = WORKSPACE_ROOT / "static"
@@ -245,12 +299,42 @@ except Exception as e:
245
 
246
  # ===== HTML UI Endpoints =====
247
  @app.get("/", response_class=HTMLResponse)
248
- async def serve_admin_dashboard():
249
- """Serve admin dashboard"""
250
- html_path = WORKSPACE_ROOT / "admin.html"
251
- if html_path.exists():
252
- return FileResponse(html_path)
253
- return HTMLResponse("<h1>Admin Dashboard</h1><p>admin.html not found</p>")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
 
255
 
256
  # ===== Health & Status Endpoints =====
@@ -441,7 +525,7 @@ async def get_trending():
441
  # ===== Providers Management Endpoints =====
442
  @app.get("/api/providers")
443
  async def get_providers():
444
- """Get all providers - REAL DATA from config"""
445
  config = load_providers_config()
446
  providers = config.get("providers", {})
447
 
@@ -458,16 +542,72 @@ async def get_providers():
458
  "added_by": provider_data.get("added_by", "manual")
459
  })
460
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
461
  return {
462
  "providers": result,
463
  "total": len(result),
464
- "source": "providers_config_extended.json (Real Data)"
465
  }
466
 
467
 
468
  @app.get("/api/providers/{provider_id}")
469
  async def get_provider_detail(provider_id: str):
470
  """Get specific provider details"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
471
  config = load_providers_config()
472
  providers = config.get("providers", {})
473
 
@@ -677,18 +817,771 @@ async def get_hf_health():
677
  }
678
 
679
 
680
- # ===== DeFi Endpoint - NOT IMPLEMENTED =====
681
  @app.get("/api/defi")
682
  async def get_defi():
683
- """DeFi endpoint - Not implemented"""
684
- raise HTTPException(status_code=503, detail="DeFi endpoint not implemented. Real data only - no fakes.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
685
 
686
 
687
- # ===== HuggingFace ML Sentiment - NOT IMPLEMENTED =====
688
  @app.post("/api/hf/run-sentiment")
689
- async def run_sentiment(data: Dict[str, Any]):
690
- """ML sentiment analysis - Not implemented"""
691
- raise HTTPException(status_code=501, detail="ML sentiment not implemented. Real data only - no fakes.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
692
 
693
 
694
  # ===== Main Entry Point =====
 
16
  from contextlib import asynccontextmanager
17
  from collections import defaultdict
18
 
19
+ from fastapi import FastAPI, HTTPException, Response, Request
20
  from fastapi.middleware.cors import CORSMiddleware
21
  from fastapi.responses import JSONResponse, FileResponse, HTMLResponse
22
  from fastapi.staticfiles import StaticFiles
23
+ from starlette.middleware.base import BaseHTTPMiddleware
24
  from pydantic import BaseModel
25
 
26
  # Environment variables
27
  USE_MOCK_DATA = os.getenv("USE_MOCK_DATA", "false").lower() == "true"
28
  PORT = int(os.getenv("PORT", "7860"))
29
 
30
+ # Paths - In Docker container, use /app as base
31
+ WORKSPACE_ROOT = Path("/app" if Path("/app").exists() else (Path("/workspace") if Path("/workspace").exists() else Path(".")))
32
  DB_PATH = WORKSPACE_ROOT / "data" / "database" / "crypto_monitor.db"
33
  LOG_DIR = WORKSPACE_ROOT / "logs"
34
  PROVIDERS_CONFIG_PATH = WORKSPACE_ROOT / "providers_config_extended.json"
 
68
  )
69
  """)
70
 
71
+ cursor.execute("""
72
+ CREATE TABLE IF NOT EXISTS sentiment_analysis (
73
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
74
+ text TEXT NOT NULL,
75
+ sentiment_label TEXT NOT NULL,
76
+ confidence REAL NOT NULL,
77
+ model_used TEXT,
78
+ analysis_type TEXT,
79
+ symbol TEXT,
80
+ scores TEXT,
81
+ timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
82
+ )
83
+ """)
84
+
85
+ cursor.execute("""
86
+ CREATE TABLE IF NOT EXISTS news_articles (
87
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
88
+ title TEXT NOT NULL,
89
+ content TEXT,
90
+ url TEXT,
91
+ source TEXT,
92
+ sentiment_label TEXT,
93
+ sentiment_confidence REAL,
94
+ related_symbols TEXT,
95
+ published_date DATETIME,
96
+ analyzed_at DATETIME DEFAULT CURRENT_TIMESTAMP
97
+ )
98
+ """)
99
+
100
  cursor.execute("CREATE INDEX IF NOT EXISTS idx_prices_symbol ON prices(symbol)")
101
  cursor.execute("CREATE INDEX IF NOT EXISTS idx_prices_timestamp ON prices(timestamp)")
102
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_sentiment_timestamp ON sentiment_analysis(timestamp)")
103
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_sentiment_symbol ON sentiment_analysis(symbol)")
104
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_news_published ON news_articles(published_date)")
105
 
106
  conn.commit()
107
  conn.close()
 
243
  if apl_report:
244
  print(f"✓ Loaded APL report with validation data")
245
 
246
+ # Initialize AI models
247
+ try:
248
+ from ai_models import initialize_models, registry_status
249
+ model_init_result = initialize_models()
250
+ registry_info = registry_status()
251
+ print(f"✓ AI Models initialized: {model_init_result}")
252
+ print(f"✓ HF Registry status: {registry_info}")
253
+ except Exception as e:
254
+ print(f"⚠ AI Models initialization failed: {e}")
255
+
256
  print(f"✓ Server ready on port {PORT}")
257
  print("=" * 80)
258
  yield
 
276
  allow_headers=["*"],
277
  )
278
 
279
+ # Middleware to ensure HTML responses have correct Content-Type
280
+ class HTMLContentTypeMiddleware(BaseHTTPMiddleware):
281
+ async def dispatch(self, request: Request, call_next):
282
+ response = await call_next(request)
283
+ if isinstance(response, HTMLResponse):
284
+ response.headers["Content-Type"] = "text/html; charset=utf-8"
285
+ response.headers["X-Content-Type-Options"] = "nosniff"
286
+ return response
287
+
288
+ app.add_middleware(HTMLContentTypeMiddleware)
289
+
290
  # Mount static files
291
  try:
292
  static_path = WORKSPACE_ROOT / "static"
 
299
 
300
  # ===== HTML UI Endpoints =====
301
  @app.get("/", response_class=HTMLResponse)
302
+ async def root():
303
+ """Serve main dashboard"""
304
+ index_path = WORKSPACE_ROOT / "index.html"
305
+ if index_path.exists():
306
+ content = index_path.read_text(encoding="utf-8", errors="ignore")
307
+ return HTMLResponse(
308
+ content=content,
309
+ media_type="text/html",
310
+ headers={
311
+ "Content-Type": "text/html; charset=utf-8",
312
+ "X-Content-Type-Options": "nosniff"
313
+ }
314
+ )
315
+ return HTMLResponse(
316
+ "<h1>Cryptocurrency Data & Analysis API</h1><p>See <a href='/docs'>/docs</a> for API documentation</p>",
317
+ headers={"Content-Type": "text/html; charset=utf-8"}
318
+ )
319
+
320
+ @app.get("/index.html", response_class=HTMLResponse)
321
+ async def index():
322
+ """Serve index.html"""
323
+ index_path = WORKSPACE_ROOT / "index.html"
324
+ if index_path.exists():
325
+ content = index_path.read_text(encoding="utf-8", errors="ignore")
326
+ return HTMLResponse(
327
+ content=content,
328
+ media_type="text/html",
329
+ headers={
330
+ "Content-Type": "text/html; charset=utf-8",
331
+ "X-Content-Type-Options": "nosniff"
332
+ }
333
+ )
334
+ return HTMLResponse(
335
+ "<h1>index.html not found</h1>",
336
+ headers={"Content-Type": "text/html; charset=utf-8"}
337
+ )
338
 
339
 
340
  # ===== Health & Status Endpoints =====
 
525
  # ===== Providers Management Endpoints =====
526
  @app.get("/api/providers")
527
  async def get_providers():
528
+ """Get all providers - REAL DATA from config + HF Models as providers"""
529
  config = load_providers_config()
530
  providers = config.get("providers", {})
531
 
 
542
  "added_by": provider_data.get("added_by", "manual")
543
  })
544
 
545
+ # Add HF Models as providers
546
+ try:
547
+ from ai_models import MODEL_SPECS, _registry
548
+ for model_key, spec in MODEL_SPECS.items():
549
+ is_loaded = model_key in _registry._pipelines
550
+ result.append({
551
+ "provider_id": f"hf_model_{model_key}",
552
+ "name": f"HF Model: {spec.model_id}",
553
+ "category": spec.category,
554
+ "type": "hf_model",
555
+ "status": "available" if is_loaded else "not_loaded",
556
+ "model_key": model_key,
557
+ "model_id": spec.model_id,
558
+ "task": spec.task,
559
+ "requires_auth": spec.requires_auth,
560
+ "endpoint": f"/api/models/{model_key}/predict",
561
+ "added_by": "hf_models"
562
+ })
563
+ except Exception as e:
564
+ print(f"⚠ Could not add HF models as providers: {e}")
565
+
566
  return {
567
  "providers": result,
568
  "total": len(result),
569
+ "source": "providers_config_extended.json + HF Models (Real Data)"
570
  }
571
 
572
 
573
  @app.get("/api/providers/{provider_id}")
574
  async def get_provider_detail(provider_id: str):
575
  """Get specific provider details"""
576
+ # Check if it's an HF model provider
577
+ if provider_id.startswith("hf_model_"):
578
+ model_key = provider_id.replace("hf_model_", "")
579
+ try:
580
+ from ai_models import MODEL_SPECS, _registry
581
+ if model_key not in MODEL_SPECS:
582
+ raise HTTPException(status_code=404, detail=f"Model {model_key} not found")
583
+
584
+ spec = MODEL_SPECS[model_key]
585
+ is_loaded = model_key in _registry._pipelines
586
+
587
+ return {
588
+ "provider_id": provider_id,
589
+ "name": f"HF Model: {spec.model_id}",
590
+ "category": spec.category,
591
+ "type": "hf_model",
592
+ "status": "available" if is_loaded else "not_loaded",
593
+ "model_key": model_key,
594
+ "model_id": spec.model_id,
595
+ "task": spec.task,
596
+ "requires_auth": spec.requires_auth,
597
+ "endpoint": f"/api/models/{model_key}/predict",
598
+ "usage": {
599
+ "method": "POST",
600
+ "url": f"/api/models/{model_key}/predict",
601
+ "body": {"text": "string", "options": {}}
602
+ },
603
+ "added_by": "hf_models"
604
+ }
605
+ except HTTPException:
606
+ raise
607
+ except Exception as e:
608
+ raise HTTPException(status_code=500, detail=str(e))
609
+
610
+ # Regular provider
611
  config = load_providers_config()
612
  providers = config.get("providers", {})
613
 
 
817
  }
818
 
819
 
820
+ # ===== DeFi Endpoint =====
821
  @app.get("/api/defi")
822
  async def get_defi():
823
+ """DeFi endpoint"""
824
+ return {
825
+ "success": True,
826
+ "message": "DeFi data endpoint",
827
+ "data": [],
828
+ "timestamp": datetime.now().isoformat()
829
+ }
830
+
831
+
832
+ # ===== News Endpoint (compatible with UI) =====
833
+ @app.get("/api/news")
834
+ async def get_news_api(limit: int = 20):
835
+ """Get news (compatible with UI)"""
836
+ try:
837
+ conn = sqlite3.connect(str(DB_PATH))
838
+ cursor = conn.cursor()
839
+ cursor.execute("""
840
+ SELECT * FROM news_articles
841
+ ORDER BY analyzed_at DESC
842
+ LIMIT ?
843
+ """, (limit,))
844
+ rows = cursor.fetchall()
845
+ columns = [desc[0] for desc in cursor.description]
846
+ conn.close()
847
+
848
+ results = []
849
+ for row in rows:
850
+ record = dict(zip(columns, row))
851
+ if record.get("related_symbols"):
852
+ try:
853
+ record["related_symbols"] = json.loads(record["related_symbols"])
854
+ except:
855
+ pass
856
+ results.append(record)
857
+
858
+ return {
859
+ "success": True,
860
+ "news": results,
861
+ "count": len(results)
862
+ }
863
+ except Exception as e:
864
+ return {
865
+ "success": False,
866
+ "news": [],
867
+ "count": 0,
868
+ "error": str(e)
869
+ }
870
+
871
+
872
+ # ===== Logs Endpoints =====
873
+ @app.get("/api/logs/summary")
874
+ async def get_logs_summary():
875
+ """Get logs summary"""
876
+ try:
877
+ return {
878
+ "success": True,
879
+ "total": len(_provider_state.get("logs", [])),
880
+ "recent": _provider_state.get("logs", [])[-10:],
881
+ "timestamp": datetime.now().isoformat()
882
+ }
883
+ except Exception as e:
884
+ return {
885
+ "success": False,
886
+ "error": str(e)
887
+ }
888
+
889
+
890
+ # ===== Diagnostics Endpoints =====
891
+ @app.get("/api/diagnostics/errors")
892
+ async def get_diagnostics_errors():
893
+ """Get diagnostic errors"""
894
+ try:
895
+ return {
896
+ "success": True,
897
+ "errors": [],
898
+ "timestamp": datetime.now().isoformat()
899
+ }
900
+ except Exception as e:
901
+ return {
902
+ "success": False,
903
+ "errors": [],
904
+ "error": str(e)
905
+ }
906
+
907
+
908
+ # ===== Resources Endpoints =====
909
+ @app.get("/api/resources/search")
910
+ async def search_resources(q: str = "", source: str = "all"):
911
+ """Search resources"""
912
+ try:
913
+ return {
914
+ "success": True,
915
+ "query": q,
916
+ "source": source,
917
+ "results": [],
918
+ "count": 0
919
+ }
920
+ except Exception as e:
921
+ return {
922
+ "success": False,
923
+ "error": str(e)
924
+ }
925
+
926
+
927
+ # ===== V2 API Endpoints (compatibility) =====
928
+ @app.post("/api/v2/export/{export_type}")
929
+ async def export_v2(export_type: str, data: Dict[str, Any] = None):
930
+ """V2 export endpoint"""
931
+ return {
932
+ "success": True,
933
+ "type": export_type,
934
+ "message": "Export functionality",
935
+ "data": data or {}
936
+ }
937
+
938
+
939
+ @app.post("/api/v2/backup")
940
+ async def backup_v2():
941
+ """V2 backup endpoint"""
942
+ return {
943
+ "success": True,
944
+ "message": "Backup functionality",
945
+ "timestamp": datetime.now().isoformat()
946
+ }
947
+
948
+
949
+ @app.post("/api/v2/import/providers")
950
+ async def import_providers_v2(data: Dict[str, Any]):
951
+ """V2 import providers endpoint"""
952
+ return {
953
+ "success": True,
954
+ "message": "Import providers functionality",
955
+ "data": data
956
+ }
957
+
958
+
959
+ # ===== HuggingFace ML Sentiment Endpoints =====
960
+ @app.post("/api/sentiment/analyze")
961
+ async def analyze_sentiment(request: Dict[str, Any]):
962
+ """Analyze sentiment using Hugging Face models"""
963
+ try:
964
+ from ai_models import (
965
+ analyze_crypto_sentiment,
966
+ analyze_financial_sentiment,
967
+ analyze_social_sentiment,
968
+ analyze_market_text,
969
+ ModelNotAvailable
970
+ )
971
+
972
+ text = request.get("text", "").strip()
973
+ if not text:
974
+ raise HTTPException(status_code=400, detail="Text is required")
975
+
976
+ mode = request.get("mode", "auto").lower()
977
+ symbol = request.get("symbol")
978
+
979
+ try:
980
+ if mode == "crypto":
981
+ result = analyze_crypto_sentiment(text)
982
+ elif mode == "financial":
983
+ result = analyze_financial_sentiment(text)
984
+ elif mode == "social":
985
+ result = analyze_social_sentiment(text)
986
+ else:
987
+ result = analyze_market_text(text)
988
+
989
+ sentiment_label = result.get("label", "neutral")
990
+ confidence = result.get("confidence", result.get("score", 0.5))
991
+ model_used = result.get("model_count", 0)
992
+
993
+ conn = sqlite3.connect(str(DB_PATH))
994
+ cursor = conn.cursor()
995
+ cursor.execute("""
996
+ INSERT INTO sentiment_analysis
997
+ (text, sentiment_label, confidence, model_used, analysis_type, symbol, scores)
998
+ VALUES (?, ?, ?, ?, ?, ?, ?)
999
+ """, (
1000
+ text[:500],
1001
+ sentiment_label,
1002
+ confidence,
1003
+ f"{model_used} models" if isinstance(model_used, int) else str(model_used),
1004
+ mode,
1005
+ symbol,
1006
+ json.dumps(result.get("scores", {}))
1007
+ ))
1008
+ conn.commit()
1009
+ conn.close()
1010
+
1011
+ return {
1012
+ "success": True,
1013
+ "sentiment": sentiment_label,
1014
+ "confidence": confidence,
1015
+ "mode": mode,
1016
+ "result": result,
1017
+ "saved_to_db": True
1018
+ }
1019
+
1020
+ except ModelNotAvailable as e:
1021
+ raise HTTPException(status_code=503, detail=f"Models not available: {str(e)}")
1022
+
1023
+ except HTTPException:
1024
+ raise
1025
+ except Exception as e:
1026
+ raise HTTPException(status_code=500, detail=f"Sentiment analysis failed: {str(e)}")
1027
+
1028
+
1029
+ @app.post("/api/news/analyze")
1030
+ async def analyze_news(request: Dict[str, Any]):
1031
+ """Analyze news article sentiment using HF models"""
1032
+ try:
1033
+ from ai_models import analyze_news_item, ModelNotAvailable
1034
+
1035
+ title = request.get("title", "").strip()
1036
+ content = request.get("content", request.get("description", "")).strip()
1037
+ url = request.get("url", "")
1038
+ source = request.get("source", "unknown")
1039
+ published_date = request.get("published_date")
1040
+
1041
+ if not title and not content:
1042
+ raise HTTPException(status_code=400, detail="Title or content is required")
1043
+
1044
+ try:
1045
+ news_item = {
1046
+ "title": title,
1047
+ "description": content
1048
+ }
1049
+ result = analyze_news_item(news_item)
1050
+
1051
+ sentiment_label = result.get("sentiment", "neutral")
1052
+ sentiment_confidence = result.get("sentiment_confidence", 0.5)
1053
+ related_symbols = request.get("related_symbols", [])
1054
+
1055
+ conn = sqlite3.connect(str(DB_PATH))
1056
+ cursor = conn.cursor()
1057
+ cursor.execute("""
1058
+ INSERT INTO news_articles
1059
+ (title, content, url, source, sentiment_label, sentiment_confidence, related_symbols, published_date)
1060
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1061
+ """, (
1062
+ title[:500],
1063
+ content[:2000] if content else None,
1064
+ url,
1065
+ source,
1066
+ sentiment_label,
1067
+ sentiment_confidence,
1068
+ json.dumps(related_symbols) if related_symbols else None,
1069
+ published_date
1070
+ ))
1071
+ conn.commit()
1072
+ conn.close()
1073
+
1074
+ return {
1075
+ "success": True,
1076
+ "news": {
1077
+ "title": title,
1078
+ "sentiment": sentiment_label,
1079
+ "confidence": sentiment_confidence,
1080
+ "details": result.get("sentiment_details", {})
1081
+ },
1082
+ "saved_to_db": True
1083
+ }
1084
+
1085
+ except ModelNotAvailable as e:
1086
+ raise HTTPException(status_code=503, detail=f"Models not available: {str(e)}")
1087
+
1088
+ except HTTPException:
1089
+ raise
1090
+ except Exception as e:
1091
+ raise HTTPException(status_code=500, detail=f"News analysis failed: {str(e)}")
1092
+
1093
+
1094
+ @app.get("/api/sentiment/history")
1095
+ async def get_sentiment_history(
1096
+ symbol: Optional[str] = None,
1097
+ limit: int = 50
1098
+ ):
1099
+ """Get sentiment analysis history from database"""
1100
+ try:
1101
+ conn = sqlite3.connect(str(DB_PATH))
1102
+ cursor = conn.cursor()
1103
+
1104
+ if symbol:
1105
+ cursor.execute("""
1106
+ SELECT * FROM sentiment_analysis
1107
+ WHERE symbol = ?
1108
+ ORDER BY timestamp DESC
1109
+ LIMIT ?
1110
+ """, (symbol.upper(), limit))
1111
+ else:
1112
+ cursor.execute("""
1113
+ SELECT * FROM sentiment_analysis
1114
+ ORDER BY timestamp DESC
1115
+ LIMIT ?
1116
+ """, (limit,))
1117
+
1118
+ rows = cursor.fetchall()
1119
+ columns = [desc[0] for desc in cursor.description]
1120
+ conn.close()
1121
+
1122
+ results = []
1123
+ for row in rows:
1124
+ record = dict(zip(columns, row))
1125
+ if record.get("scores"):
1126
+ try:
1127
+ record["scores"] = json.loads(record["scores"])
1128
+ except:
1129
+ pass
1130
+ results.append(record)
1131
+
1132
+ return {
1133
+ "success": True,
1134
+ "count": len(results),
1135
+ "results": results
1136
+ }
1137
+
1138
+ except Exception as e:
1139
+ raise HTTPException(status_code=500, detail=f"Failed to fetch sentiment history: {str(e)}")
1140
+
1141
+
1142
+ @app.get("/api/news/latest")
1143
+ async def get_latest_news(
1144
+ limit: int = 20,
1145
+ sentiment: Optional[str] = None
1146
+ ):
1147
+ """Get latest analyzed news from database"""
1148
+ try:
1149
+ conn = sqlite3.connect(str(DB_PATH))
1150
+ cursor = conn.cursor()
1151
+
1152
+ if sentiment:
1153
+ cursor.execute("""
1154
+ SELECT * FROM news_articles
1155
+ WHERE sentiment_label = ?
1156
+ ORDER BY analyzed_at DESC
1157
+ LIMIT ?
1158
+ """, (sentiment.lower(), limit))
1159
+ else:
1160
+ cursor.execute("""
1161
+ SELECT * FROM news_articles
1162
+ ORDER BY analyzed_at DESC
1163
+ LIMIT ?
1164
+ """, (limit,))
1165
+
1166
+ rows = cursor.fetchall()
1167
+ columns = [desc[0] for desc in cursor.description]
1168
+ conn.close()
1169
+
1170
+ results = []
1171
+ for row in rows:
1172
+ record = dict(zip(columns, row))
1173
+ if record.get("related_symbols"):
1174
+ try:
1175
+ record["related_symbols"] = json.loads(record["related_symbols"])
1176
+ except:
1177
+ pass
1178
+ results.append(record)
1179
+
1180
+ return {
1181
+ "success": True,
1182
+ "count": len(results),
1183
+ "news": results
1184
+ }
1185
+
1186
+ except Exception as e:
1187
+ raise HTTPException(status_code=500, detail=f"Failed to fetch news: {str(e)}")
1188
+
1189
+
1190
+ @app.get("/api/models/status")
1191
+ async def get_models_status():
1192
+ """Get AI models status and registry info"""
1193
+ try:
1194
+ from ai_models import get_model_info, registry_status, initialize_models
1195
+
1196
+ model_info = get_model_info()
1197
+ registry_info = registry_status()
1198
+
1199
+ return {
1200
+ "success": True,
1201
+ "models": model_info,
1202
+ "registry": registry_info,
1203
+ "database": {
1204
+ "path": str(DB_PATH),
1205
+ "exists": DB_PATH.exists()
1206
+ }
1207
+ }
1208
+ except Exception as e:
1209
+ return {
1210
+ "success": False,
1211
+ "error": str(e)
1212
+ }
1213
+
1214
+
1215
+ @app.post("/api/models/initialize")
1216
+ async def initialize_ai_models():
1217
+ """Initialize AI models (force reload)"""
1218
+ try:
1219
+ from ai_models import initialize_models, registry_status
1220
+
1221
+ result = initialize_models()
1222
+ registry_info = registry_status()
1223
+
1224
+ return {
1225
+ "success": True,
1226
+ "initialization": result,
1227
+ "registry": registry_info
1228
+ }
1229
+ except Exception as e:
1230
+ raise HTTPException(status_code=500, detail=f"Failed to initialize models: {str(e)}")
1231
+
1232
+
1233
+ # ===== Model-based Data Endpoints (Using HF Models as Data Sources) =====
1234
+ @app.get("/api/models/list")
1235
+ async def list_available_models():
1236
+ """List all available Hugging Face models as data sources"""
1237
+ try:
1238
+ from ai_models import get_model_info, MODEL_SPECS, CRYPTO_SENTIMENT_MODELS, SOCIAL_SENTIMENT_MODELS, FINANCIAL_SENTIMENT_MODELS, NEWS_SENTIMENT_MODELS
1239
+
1240
+ model_info = get_model_info()
1241
+
1242
+ models_list = []
1243
+ for key, spec in MODEL_SPECS.items():
1244
+ models_list.append({
1245
+ "id": key,
1246
+ "model_id": spec.model_id,
1247
+ "task": spec.task,
1248
+ "category": spec.category,
1249
+ "requires_auth": spec.requires_auth,
1250
+ "endpoint": f"/api/models/{key}/predict"
1251
+ })
1252
+
1253
+ return {
1254
+ "success": True,
1255
+ "total_models": len(models_list),
1256
+ "models": models_list,
1257
+ "categories": {
1258
+ "crypto_sentiment": CRYPTO_SENTIMENT_MODELS,
1259
+ "social_sentiment": SOCIAL_SENTIMENT_MODELS,
1260
+ "financial_sentiment": FINANCIAL_SENTIMENT_MODELS,
1261
+ "news_sentiment": NEWS_SENTIMENT_MODELS
1262
+ },
1263
+ "model_info": model_info
1264
+ }
1265
+ except Exception as e:
1266
+ return {
1267
+ "success": False,
1268
+ "error": str(e),
1269
+ "models": []
1270
+ }
1271
+
1272
+
1273
+ @app.get("/api/models/{model_key}/info")
1274
+ async def get_model_info_endpoint(model_key: str):
1275
+ """Get information about a specific model"""
1276
+ try:
1277
+ from ai_models import MODEL_SPECS, ModelNotAvailable, _registry
1278
+
1279
+ if model_key not in MODEL_SPECS:
1280
+ raise HTTPException(status_code=404, detail=f"Model {model_key} not found")
1281
+
1282
+ spec = MODEL_SPECS[model_key]
1283
+ is_loaded = model_key in _registry._pipelines
1284
+
1285
+ return {
1286
+ "success": True,
1287
+ "model_key": model_key,
1288
+ "model_id": spec.model_id,
1289
+ "task": spec.task,
1290
+ "category": spec.category,
1291
+ "requires_auth": spec.requires_auth,
1292
+ "is_loaded": is_loaded,
1293
+ "endpoint": f"/api/models/{model_key}/predict",
1294
+ "usage": {
1295
+ "method": "POST",
1296
+ "url": f"/api/models/{model_key}/predict",
1297
+ "body": {"text": "string", "options": {}}
1298
+ }
1299
+ }
1300
+ except HTTPException:
1301
+ raise
1302
+ except Exception as e:
1303
+ raise HTTPException(status_code=500, detail=str(e))
1304
+
1305
+
1306
+ @app.post("/api/models/{model_key}/predict")
1307
+ async def predict_with_model(model_key: str, request: Dict[str, Any]):
1308
+ """Use a specific model to generate predictions/data"""
1309
+ try:
1310
+ from ai_models import MODEL_SPECS, _registry, ModelNotAvailable
1311
+
1312
+ if model_key not in MODEL_SPECS:
1313
+ raise HTTPException(status_code=404, detail=f"Model {model_key} not found")
1314
+
1315
+ spec = MODEL_SPECS[model_key]
1316
+ text = request.get("text", "").strip()
1317
+
1318
+ if not text:
1319
+ raise HTTPException(status_code=400, detail="Text is required")
1320
+
1321
+ try:
1322
+ pipeline = _registry.get_pipeline(model_key)
1323
+ result = pipeline(text[:512])
1324
+
1325
+ if isinstance(result, list) and result:
1326
+ result = result[0]
1327
+
1328
+ return {
1329
+ "success": True,
1330
+ "model_key": model_key,
1331
+ "model_id": spec.model_id,
1332
+ "task": spec.task,
1333
+ "input": text[:100],
1334
+ "output": result,
1335
+ "timestamp": datetime.now().isoformat()
1336
+ }
1337
+ except ModelNotAvailable as e:
1338
+ raise HTTPException(status_code=503, detail=f"Model not available: {str(e)}")
1339
+
1340
+ except HTTPException:
1341
+ raise
1342
+ except Exception as e:
1343
+ raise HTTPException(status_code=500, detail=f"Prediction failed: {str(e)}")
1344
+
1345
+
1346
+ @app.post("/api/models/batch/predict")
1347
+ async def batch_predict(request: Dict[str, Any]):
1348
+ """Batch prediction using multiple models"""
1349
+ try:
1350
+ from ai_models import MODEL_SPECS, _registry, ModelNotAvailable
1351
+
1352
+ texts = request.get("texts", [])
1353
+ model_keys = request.get("models", [])
1354
+
1355
+ if not texts:
1356
+ raise HTTPException(status_code=400, detail="Texts array is required")
1357
+
1358
+ if not model_keys:
1359
+ model_keys = list(MODEL_SPECS.keys())[:5]
1360
+
1361
+ results = []
1362
+ for text in texts:
1363
+ if not text.strip():
1364
+ continue
1365
+
1366
+ text_results = {}
1367
+ for model_key in model_keys:
1368
+ if model_key not in MODEL_SPECS:
1369
+ continue
1370
+
1371
+ try:
1372
+ spec = MODEL_SPECS[model_key]
1373
+ pipeline = _registry.get_pipeline(model_key)
1374
+ result = pipeline(text[:512])
1375
+
1376
+ if isinstance(result, list) and result:
1377
+ result = result[0]
1378
+
1379
+ text_results[model_key] = {
1380
+ "model_id": spec.model_id,
1381
+ "result": result,
1382
+ "success": True
1383
+ }
1384
+ except ModelNotAvailable:
1385
+ text_results[model_key] = {
1386
+ "success": False,
1387
+ "error": "Model not available"
1388
+ }
1389
+ except Exception as e:
1390
+ text_results[model_key] = {
1391
+ "success": False,
1392
+ "error": str(e)
1393
+ }
1394
+
1395
+ results.append({
1396
+ "text": text[:100],
1397
+ "predictions": text_results
1398
+ })
1399
+
1400
+ return {
1401
+ "success": True,
1402
+ "total_texts": len(results),
1403
+ "models_used": model_keys,
1404
+ "results": results,
1405
+ "timestamp": datetime.now().isoformat()
1406
+ }
1407
+
1408
+ except HTTPException:
1409
+ raise
1410
+ except Exception as e:
1411
+ raise HTTPException(status_code=500, detail=f"Batch prediction failed: {str(e)}")
1412
+
1413
+
1414
+ @app.get("/api/models/data/generated")
1415
+ async def get_generated_data(
1416
+ limit: int = 50,
1417
+ model_key: Optional[str] = None,
1418
+ symbol: Optional[str] = None
1419
+ ):
1420
+ """Get data generated by models from database"""
1421
+ try:
1422
+ conn = sqlite3.connect(str(DB_PATH))
1423
+ cursor = conn.cursor()
1424
+
1425
+ if model_key and symbol:
1426
+ cursor.execute("""
1427
+ SELECT * FROM sentiment_analysis
1428
+ WHERE analysis_type = ? AND symbol = ?
1429
+ ORDER BY timestamp DESC
1430
+ LIMIT ?
1431
+ """, (model_key, symbol.upper(), limit))
1432
+ elif model_key:
1433
+ cursor.execute("""
1434
+ SELECT * FROM sentiment_analysis
1435
+ WHERE analysis_type = ?
1436
+ ORDER BY timestamp DESC
1437
+ LIMIT ?
1438
+ """, (model_key, limit))
1439
+ elif symbol:
1440
+ cursor.execute("""
1441
+ SELECT * FROM sentiment_analysis
1442
+ WHERE symbol = ?
1443
+ ORDER BY timestamp DESC
1444
+ LIMIT ?
1445
+ """, (symbol.upper(), limit))
1446
+ else:
1447
+ cursor.execute("""
1448
+ SELECT * FROM sentiment_analysis
1449
+ ORDER BY timestamp DESC
1450
+ LIMIT ?
1451
+ """, (limit,))
1452
+
1453
+ rows = cursor.fetchall()
1454
+ columns = [desc[0] for desc in cursor.description]
1455
+ conn.close()
1456
+
1457
+ results = []
1458
+ for row in rows:
1459
+ record = dict(zip(columns, row))
1460
+ if record.get("scores"):
1461
+ try:
1462
+ record["scores"] = json.loads(record["scores"])
1463
+ except:
1464
+ pass
1465
+ results.append(record)
1466
+
1467
+ return {
1468
+ "success": True,
1469
+ "count": len(results),
1470
+ "data": results,
1471
+ "source": "models",
1472
+ "timestamp": datetime.now().isoformat()
1473
+ }
1474
+
1475
+ except Exception as e:
1476
+ raise HTTPException(status_code=500, detail=f"Failed to fetch generated data: {str(e)}")
1477
+
1478
+
1479
+ @app.get("/api/models/data/stats")
1480
+ async def get_models_data_stats():
1481
+ """Get statistics about data generated by models"""
1482
+ try:
1483
+ conn = sqlite3.connect(str(DB_PATH))
1484
+ cursor = conn.cursor()
1485
+
1486
+ cursor.execute("SELECT COUNT(*) FROM sentiment_analysis")
1487
+ total_analyses = cursor.fetchone()[0]
1488
+
1489
+ cursor.execute("SELECT COUNT(DISTINCT symbol) FROM sentiment_analysis WHERE symbol IS NOT NULL")
1490
+ unique_symbols = cursor.fetchone()[0]
1491
+
1492
+ cursor.execute("SELECT COUNT(DISTINCT analysis_type) FROM sentiment_analysis")
1493
+ unique_types = cursor.fetchone()[0]
1494
+
1495
+ cursor.execute("""
1496
+ SELECT sentiment_label, COUNT(*) as count
1497
+ FROM sentiment_analysis
1498
+ GROUP BY sentiment_label
1499
+ """)
1500
+ sentiment_dist = {row[0]: row[1] for row in cursor.fetchall()}
1501
+
1502
+ cursor.execute("""
1503
+ SELECT analysis_type, COUNT(*) as count
1504
+ FROM sentiment_analysis
1505
+ GROUP BY analysis_type
1506
+ """)
1507
+ type_dist = {row[0]: row[1] for row in cursor.fetchall()}
1508
+
1509
+ conn.close()
1510
+
1511
+ return {
1512
+ "success": True,
1513
+ "statistics": {
1514
+ "total_analyses": total_analyses,
1515
+ "unique_symbols": unique_symbols,
1516
+ "unique_model_types": unique_types,
1517
+ "sentiment_distribution": sentiment_dist,
1518
+ "model_type_distribution": type_dist
1519
+ },
1520
+ "timestamp": datetime.now().isoformat()
1521
+ }
1522
+
1523
+ except Exception as e:
1524
+ raise HTTPException(status_code=500, detail=f"Failed to fetch statistics: {str(e)}")
1525
 
1526
 
 
1527
  @app.post("/api/hf/run-sentiment")
1528
+ async def run_hf_sentiment(data: Dict[str, Any]):
1529
+ """Run sentiment analysis using HF models (compatible with UI)"""
1530
+ try:
1531
+ from ai_models import analyze_market_text, ModelNotAvailable
1532
+
1533
+ texts = data.get("texts", [])
1534
+ if isinstance(texts, str):
1535
+ texts = [texts]
1536
+
1537
+ if not texts or not any(t.strip() for t in texts):
1538
+ raise HTTPException(status_code=400, detail="At least one text is required")
1539
+
1540
+ try:
1541
+ all_results = []
1542
+ total_vote = 0.0
1543
+ count = 0
1544
+
1545
+ for text in texts:
1546
+ if not text.strip():
1547
+ continue
1548
+
1549
+ result = analyze_market_text(text.strip())
1550
+ label = result.get("label", "neutral")
1551
+ confidence = result.get("confidence", 0.5)
1552
+
1553
+ vote_score = 0.0
1554
+ if label == "bullish":
1555
+ vote_score = confidence
1556
+ elif label == "bearish":
1557
+ vote_score = -confidence
1558
+
1559
+ total_vote += vote_score
1560
+ count += 1
1561
+
1562
+ all_results.append({
1563
+ "text": text[:100],
1564
+ "label": label,
1565
+ "confidence": confidence,
1566
+ "vote": vote_score
1567
+ })
1568
+
1569
+ avg_vote = total_vote / count if count > 0 else 0.0
1570
+
1571
+ return {
1572
+ "vote": avg_vote,
1573
+ "results": all_results,
1574
+ "count": count,
1575
+ "average_confidence": sum(r["confidence"] for r in all_results) / len(all_results) if all_results else 0.0
1576
+ }
1577
+
1578
+ except ModelNotAvailable as e:
1579
+ raise HTTPException(status_code=503, detail=f"Models not available: {str(e)}")
1580
+
1581
+ except HTTPException:
1582
+ raise
1583
+ except Exception as e:
1584
+ raise HTTPException(status_code=500, detail=f"Sentiment analysis failed: {str(e)}")
1585
 
1586
 
1587
  # ===== Main Entry Point =====
app.py CHANGED
@@ -1,1232 +1,702 @@
1
- #!/usr/bin/env python3
2
- """
3
- Crypto Data Aggregator - Admin Dashboard (Gradio App)
4
- STRICT REAL-DATA-ONLY implementation for Hugging Face Spaces
5
-
6
- 7 Tabs:
7
- 1. Status - System health & overview
8
- 2. Providers - API provider management
9
- 3. Market Data - Live cryptocurrency data
10
- 4. APL Scanner - Auto Provider Loader
11
- 5. HF Models - Hugging Face model status
12
- 6. Diagnostics - System diagnostics & auto-repair
13
- 7. Logs - System logs viewer
14
- """
15
-
16
- import sys
17
- import os
18
- import logging
19
- from pathlib import Path
20
- from typing import Dict, List, Any, Tuple, Optional
21
- from datetime import datetime
22
- import json
23
- import traceback
24
- import asyncio
25
- import time
26
-
27
- # Check for Gradio
28
- try:
29
- import gradio as gr
30
- except ImportError:
31
- print("ERROR: gradio not installed. Run: pip install gradio")
32
- sys.exit(1)
33
-
34
- # Check for optional dependencies
35
- try:
36
- import pandas as pd
37
- PANDAS_AVAILABLE = True
38
- except ImportError:
39
- PANDAS_AVAILABLE = False
40
- print("WARNING: pandas not installed. Some features disabled.")
41
-
42
- try:
43
- import plotly.graph_objects as go
44
- from plotly.subplots import make_subplots
45
- PLOTLY_AVAILABLE = True
46
- except ImportError:
47
- PLOTLY_AVAILABLE = False
48
- print("WARNING: plotly not installed. Charts disabled.")
49
-
50
- # Import local modules
51
- import config
52
- import database
53
- import collectors
54
-
55
- # ==================== INDEPENDENT LOGGING SETUP ====================
56
- # DO NOT use utils.setup_logging() - set up independently
57
-
58
- logger = logging.getLogger("app")
59
- if not logger.handlers:
60
- level_name = getattr(config, "LOG_LEVEL", "INFO")
61
- level = getattr(logging, level_name.upper(), logging.INFO)
62
- logger.setLevel(level)
63
-
64
- formatter = logging.Formatter(
65
- getattr(config, "LOG_FORMAT", "%(asctime)s - %(name)s - %(levelname)s - %(message)s")
66
- )
67
-
68
- # Console handler
69
- ch = logging.StreamHandler()
70
- ch.setFormatter(formatter)
71
- logger.addHandler(ch)
72
-
73
- # File handler if log file exists
74
- try:
75
- if hasattr(config, 'LOG_FILE'):
76
- fh = logging.FileHandler(config.LOG_FILE)
77
- fh.setFormatter(formatter)
78
- logger.addHandler(fh)
79
- except Exception as e:
80
- print(f"Warning: Could not setup file logging: {e}")
81
-
82
- logger.info("=" * 60)
83
- logger.info("Crypto Admin Dashboard Starting")
84
- logger.info("=" * 60)
85
-
86
- # Initialize database
87
- db = database.get_database()
88
-
89
-
90
- # ==================== TAB 1: STATUS ====================
91
-
92
- def get_status_tab() -> Tuple[str, str, str]:
93
- """
94
- Get system status overview.
95
- Returns: (markdown_summary, db_stats_json, system_info_json)
96
- """
97
- try:
98
- # Get database stats
99
- db_stats = db.get_database_stats()
100
-
101
- # Count providers
102
- providers_config_path = config.BASE_DIR / "providers_config_extended.json"
103
- provider_count = 0
104
- if providers_config_path.exists():
105
- with open(providers_config_path, 'r') as f:
106
- providers_data = json.load(f)
107
- provider_count = len(providers_data.get('providers', {}))
108
-
109
- # Pool count (from config)
110
- pool_count = 0
111
- if providers_config_path.exists():
112
- with open(providers_config_path, 'r') as f:
113
- providers_data = json.load(f)
114
- pool_count = len(providers_data.get('pool_configurations', []))
115
-
116
- # Market snapshot
117
- latest_prices = db.get_latest_prices(3)
118
- market_snapshot = ""
119
- if latest_prices:
120
- for p in latest_prices[:3]:
121
- symbol = p.get('symbol', 'N/A')
122
- price = p.get('price_usd', 0)
123
- change = p.get('percent_change_24h', 0)
124
- market_snapshot += f"**{symbol}**: ${price:,.2f} ({change:+.2f}%)\n"
125
- else:
126
- market_snapshot = "No market data available yet."
127
-
128
- # Get API request count from health log
129
- api_requests_count = 0
130
- try:
131
- health_log_path = Path("data/logs/provider_health.jsonl")
132
- if health_log_path.exists():
133
- with open(health_log_path, 'r', encoding='utf-8') as f:
134
- api_requests_count = sum(1 for _ in f)
135
- except Exception as e:
136
- logger.warning(f"Could not get API request stats: {e}")
137
-
138
- # Build summary with copy-friendly format
139
- summary = f"""
140
- ## 🎯 System Status
141
-
142
- **Overall Health**: {"🟢 Operational" if db_stats.get('prices_count', 0) > 0 else "🟡 Initializing"}
143
-
144
- ### Quick Stats
145
- ```
146
- Total Providers: {provider_count}
147
- Active Pools: {pool_count}
148
- API Requests: {api_requests_count:,}
149
- Price Records: {db_stats.get('prices_count', 0):,}
150
- News Articles: {db_stats.get('news_count', 0):,}
151
- Unique Symbols: {db_stats.get('unique_symbols', 0)}
152
- ```
153
-
154
- ### Market Snapshot (Top 3)
155
- ```
156
- {market_snapshot}
157
- ```
158
-
159
- **Last Update**: `{datetime.now().strftime("%Y-%m-%d %H:%M:%S")}`
160
-
161
- ---
162
- ### 📋 Provider Details (Copy-Friendly)
163
- ```
164
- Total: {provider_count} providers
165
- Config File: providers_config_extended.json
166
- ```
167
- """
168
-
169
- # System info
170
- import platform
171
- system_info = {
172
- "Python Version": sys.version.split()[0],
173
- "Platform": platform.platform(),
174
- "Working Directory": str(config.BASE_DIR),
175
- "Database Size": f"{db_stats.get('database_size_mb', 0):.2f} MB",
176
- "Last Price Update": db_stats.get('latest_price_update', 'N/A'),
177
- "Last News Update": db_stats.get('latest_news_update', 'N/A')
178
- }
179
-
180
- return summary, json.dumps(db_stats, indent=2), json.dumps(system_info, indent=2)
181
-
182
- except Exception as e:
183
- logger.error(f"Error in get_status_tab: {e}\n{traceback.format_exc()}")
184
- return f"⚠️ Error loading status: {str(e)}", "{}", "{}"
185
-
186
-
187
- def run_diagnostics_from_status(auto_fix: bool) -> str:
188
- """Run diagnostics from status tab"""
189
- try:
190
- from backend.services.diagnostics_service import DiagnosticsService
191
-
192
- diagnostics = DiagnosticsService()
193
-
194
- # Run async in sync context
195
- loop = asyncio.new_event_loop()
196
- asyncio.set_event_loop(loop)
197
- report = loop.run_until_complete(diagnostics.run_full_diagnostics(auto_fix=auto_fix))
198
- loop.close()
199
-
200
- # Format output
201
- output = f"""
202
- # Diagnostics Report
203
-
204
- **Timestamp**: {report.timestamp}
205
- **Duration**: {report.duration_ms:.2f}ms
206
-
207
- ## Summary
208
- - **Total Issues**: {report.total_issues}
209
- - **Critical**: {report.critical_issues}
210
- - **Warnings**: {report.warnings}
211
- - **Info**: {report.info_issues}
212
- - **Fixed**: {len(report.fixed_issues)}
213
-
214
- ## Issues
215
- """
216
- for issue in report.issues:
217
- emoji = {"critical": "🔴", "warning": "🟡", "info": "🔵"}.get(issue.severity, "⚪")
218
- fixed_mark = " FIXED" if issue.auto_fixed else ""
219
- output += f"\n### {emoji} [{issue.category.upper()}] {issue.title}{fixed_mark}\n"
220
- output += f"{issue.description}\n"
221
- if issue.fixable and not issue.auto_fixed:
222
- output += f"**Fix**: `{issue.fix_action}`\n"
223
-
224
- return output
225
-
226
- except Exception as e:
227
- logger.error(f"Error running diagnostics: {e}")
228
- return f" Diagnostics failed: {str(e)}"
229
-
230
-
231
- # ==================== TAB 2: PROVIDERS ====================
232
-
233
- def get_providers_table(category_filter: str = "All") -> Any:
234
- """
235
- Get providers from providers_config_extended.json with enhanced formatting
236
- Returns: DataFrame or dict
237
- """
238
- try:
239
- providers_path = config.BASE_DIR / "providers_config_extended.json"
240
-
241
- if not providers_path.exists():
242
- if PANDAS_AVAILABLE:
243
- return pd.DataFrame({"Error": ["providers_config_extended.json not found"]})
244
- return {"error": "providers_config_extended.json not found"}
245
-
246
- with open(providers_path, 'r') as f:
247
- data = json.load(f)
248
-
249
- providers = data.get('providers', {})
250
-
251
- # Build table data with copy-friendly IDs
252
- table_data = []
253
- for provider_id, provider_info in providers.items():
254
- if category_filter != "All":
255
- if provider_info.get('category', '').lower() != category_filter.lower():
256
- continue
257
-
258
- # Format auth status with emoji
259
- auth_status = "✅ Yes" if provider_info.get('requires_auth', False) else "❌ No"
260
- validation = "✅ Valid" if provider_info.get('validated', False) else "⏳ Pending"
261
-
262
- table_data.append({
263
- "Provider ID": provider_id,
264
- "Name": provider_info.get('name', provider_id),
265
- "Category": provider_info.get('category', 'unknown'),
266
- "Type": provider_info.get('type', 'http_json'),
267
- "Base URL": provider_info.get('base_url', 'N/A'),
268
- "Auth Required": auth_status,
269
- "Priority": provider_info.get('priority', 'N/A'),
270
- "Status": validation
271
- })
272
-
273
- if PANDAS_AVAILABLE:
274
- return pd.DataFrame(table_data) if table_data else pd.DataFrame({"Message": ["No providers found"]})
275
- else:
276
- return {"providers": table_data} if table_data else {"error": "No providers found"}
277
-
278
- except Exception as e:
279
- logger.error(f"Error loading providers: {e}")
280
- if PANDAS_AVAILABLE:
281
- return pd.DataFrame({"Error": [str(e)]})
282
- return {"error": str(e)}
283
-
284
-
285
- def reload_providers_config() -> Tuple[Any, str]:
286
- """Reload providers config and return updated table + message with stats"""
287
- try:
288
- # Count providers
289
- providers_path = config.BASE_DIR / "providers_config_extended.json"
290
- with open(providers_path, 'r') as f:
291
- data = json.load(f)
292
-
293
- total_providers = len(data.get('providers', {}))
294
-
295
- # Count by category
296
- categories = {}
297
- for provider_info in data.get('providers', {}).values():
298
- cat = provider_info.get('category', 'unknown')
299
- categories[cat] = categories.get(cat, 0) + 1
300
-
301
- # Force reload by re-reading file
302
- table = get_providers_table("All")
303
-
304
- # Build detailed message
305
- message = f"""✅ **Providers Reloaded Successfully!**
306
-
307
- **Total Providers**: `{total_providers}`
308
- **Reload Time**: `{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}`
309
-
310
- **By Category**:
311
- """
312
- for cat, count in sorted(categories.items(), key=lambda x: x[1], reverse=True)[:10]:
313
- message += f"- {cat}: `{count}`\n"
314
-
315
- return table, message
316
- except Exception as e:
317
- logger.error(f"Error reloading providers: {e}")
318
- return get_providers_table("All"), f"❌ Reload failed: {str(e)}"
319
-
320
-
321
- def get_provider_categories() -> List[str]:
322
- """Get unique provider categories"""
323
- try:
324
- providers_path = config.BASE_DIR / "providers_config_extended.json"
325
- if not providers_path.exists():
326
- return ["All"]
327
-
328
- with open(providers_path, 'r') as f:
329
- data = json.load(f)
330
-
331
- categories = set()
332
- for provider in data.get('providers', {}).values():
333
- cat = provider.get('category', 'unknown')
334
- categories.add(cat)
335
-
336
- return ["All"] + sorted(list(categories))
337
- except Exception as e:
338
- logger.error(f"Error getting categories: {e}")
339
- return ["All"]
340
-
341
-
342
- # ==================== TAB 3: MARKET DATA ====================
343
-
344
- def get_market_data_table(search_filter: str = "") -> Any:
345
- """Get latest market data from database with enhanced formatting"""
346
- try:
347
- prices = db.get_latest_prices(100)
348
-
349
- if not prices:
350
- if PANDAS_AVAILABLE:
351
- return pd.DataFrame({"Message": ["No market data available. Click 'Refresh Prices' to collect data."]})
352
- return {"error": "No data available"}
353
-
354
- # Filter if search provided
355
- filtered_prices = prices
356
- if search_filter:
357
- search_lower = search_filter.lower()
358
- filtered_prices = [
359
- p for p in prices
360
- if search_lower in p.get('name', '').lower() or search_lower in p.get('symbol', '').lower()
361
- ]
362
-
363
- table_data = []
364
- for p in filtered_prices:
365
- # Format change with emoji
366
- change = p.get('percent_change_24h', 0)
367
- change_emoji = "🟢" if change > 0 else ("🔴" if change < 0 else "⚪")
368
-
369
- table_data.append({
370
- "#": p.get('rank', 999),
371
- "Symbol": p.get('symbol', 'N/A'),
372
- "Name": p.get('name', 'Unknown'),
373
- "Price": f"${p.get('price_usd', 0):,.2f}" if p.get('price_usd') else "N/A",
374
- "24h Change": f"{change_emoji} {change:+.2f}%" if change is not None else "N/A",
375
- "Volume 24h": f"${p.get('volume_24h', 0):,.0f}" if p.get('volume_24h') else "N/A",
376
- "Market Cap": f"${p.get('market_cap', 0):,.0f}" if p.get('market_cap') else "N/A"
377
- })
378
-
379
- if PANDAS_AVAILABLE:
380
- df = pd.DataFrame(table_data)
381
- return df.sort_values('#') if not df.empty else pd.DataFrame({"Message": ["No matching data"]})
382
- else:
383
- return {"prices": table_data}
384
-
385
- except Exception as e:
386
- logger.error(f"Error getting market data: {e}")
387
- if PANDAS_AVAILABLE:
388
- return pd.DataFrame({"Error": [str(e)]})
389
- return {"error": str(e)}
390
-
391
-
392
- def refresh_market_data() -> Tuple[Any, str]:
393
- """Refresh market data by collecting from APIs with detailed stats"""
394
- try:
395
- logger.info("Refreshing market data...")
396
- start_time = time.time()
397
- success, count = collectors.collect_price_data()
398
- duration = time.time() - start_time
399
-
400
- # Get database stats
401
- db_stats = db.get_database_stats()
402
-
403
- if success:
404
- message = f"""✅ **Market Data Refreshed Successfully!**
405
-
406
- **Collection Stats**:
407
- - New Records: `{count}`
408
- - Duration: `{duration:.2f}s`
409
- - Time: `{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}`
410
-
411
- **Database Stats**:
412
- - Total Price Records: `{db_stats.get('prices_count', 0):,}`
413
- - Unique Symbols: `{db_stats.get('unique_symbols', 0)}`
414
- - Last Update: `{db_stats.get('latest_price_update', 'N/A')}`
415
- """
416
- else:
417
- message = f"""⚠️ **Collection completed with issues**
418
-
419
- - Records Collected: `{count}`
420
- - Duration: `{duration:.2f}s`
421
- - Check logs for details
422
- """
423
-
424
- # Return updated table
425
- table = get_market_data_table("")
426
- return table, message
427
-
428
- except Exception as e:
429
- logger.error(f"Error refreshing market data: {e}")
430
- return get_market_data_table(""), f"❌ Refresh failed: {str(e)}"
431
-
432
-
433
- def plot_price_history(symbol: str, timeframe: str) -> Any:
434
- """Plot price history for a symbol"""
435
- if not PLOTLY_AVAILABLE:
436
- return None
437
-
438
- try:
439
- # Parse timeframe
440
- hours_map = {"24h": 24, "7d": 168, "30d": 720, "90d": 2160}
441
- hours = hours_map.get(timeframe, 168)
442
-
443
- # Get history
444
- history = db.get_price_history(symbol.upper(), hours)
445
-
446
- if not history or len(history) < 2:
447
- fig = go.Figure()
448
- fig.add_annotation(
449
- text=f"No historical data for {symbol}",
450
- xref="paper", yref="paper",
451
- x=0.5, y=0.5, showarrow=False
452
- )
453
- return fig
454
-
455
- # Extract data
456
- timestamps = [datetime.fromisoformat(h['timestamp'].replace('Z', '+00:00')) if isinstance(h['timestamp'], str) else datetime.now() for h in history]
457
- prices = [h.get('price_usd', 0) for h in history]
458
-
459
- # Create plot
460
- fig = go.Figure()
461
- fig.add_trace(go.Scatter(
462
- x=timestamps,
463
- y=prices,
464
- mode='lines',
465
- name='Price',
466
- line=dict(color='#2962FF', width=2)
467
- ))
468
-
469
- fig.update_layout(
470
- title=f"{symbol} - {timeframe}",
471
- xaxis_title="Time",
472
- yaxis_title="Price (USD)",
473
- hovermode='x unified',
474
- height=400
475
- )
476
-
477
- return fig
478
-
479
- except Exception as e:
480
- logger.error(f"Error plotting price history: {e}")
481
- fig = go.Figure()
482
- fig.add_annotation(text=f"Error: {str(e)}", xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False)
483
- return fig
484
-
485
-
486
- # ==================== TAB 4: APL SCANNER ====================
487
-
488
- def run_apl_scan() -> str:
489
- """Run Auto Provider Loader scan"""
490
- try:
491
- logger.info("Running APL scan...")
492
-
493
- # Import APL
494
- import auto_provider_loader
495
-
496
- # Run scan
497
- apl = auto_provider_loader.AutoProviderLoader()
498
-
499
- # Run async in sync context
500
- loop = asyncio.new_event_loop()
501
- asyncio.set_event_loop(loop)
502
- loop.run_until_complete(apl.run())
503
- loop.close()
504
-
505
- # Build summary
506
- stats = apl.stats
507
- output = f"""
508
- # APL Scan Complete
509
-
510
- **Timestamp**: {stats.timestamp}
511
- **Execution Time**: {stats.execution_time_sec:.2f}s
512
-
513
- ## HTTP Providers
514
- - **Candidates**: {stats.total_http_candidates}
515
- - **Valid**: {stats.http_valid} ✅
516
- - **Invalid**: {stats.http_invalid} ❌
517
- - **Conditional**: {stats.http_conditional} ⚠️
518
-
519
- ## HuggingFace Models
520
- - **Candidates**: {stats.total_hf_candidates}
521
- - **Valid**: {stats.hf_valid}
522
- - **Invalid**: {stats.hf_invalid} ❌
523
- - **Conditional**: {stats.hf_conditional} ⚠️
524
-
525
- ## Total Active Providers
526
- **{stats.total_active_providers}** providers are now active.
527
-
528
- ---
529
-
530
- ✅ All valid providers have been integrated into `providers_config_extended.json`.
531
-
532
- See `PROVIDER_AUTO_DISCOVERY_REPORT.md` for full details.
533
- """
534
-
535
- return output
536
-
537
- except Exception as e:
538
- logger.error(f"Error running APL: {e}\n{traceback.format_exc()}")
539
- return f"❌ APL scan failed: {str(e)}\n\nCheck logs for details."
540
-
541
-
542
- def get_apl_report() -> str:
543
- """Get last APL report"""
544
- try:
545
- report_path = config.BASE_DIR / "PROVIDER_AUTO_DISCOVERY_REPORT.md"
546
- if report_path.exists():
547
- with open(report_path, 'r') as f:
548
- return f.read()
549
- else:
550
- return "No APL report found. Run a scan first."
551
- except Exception as e:
552
- logger.error(f"Error reading APL report: {e}")
553
- return f"Error reading report: {str(e)}"
554
-
555
-
556
- # ==================== TAB 5: HF MODELS ====================
557
-
558
- def get_hf_models_status() -> Any:
559
- """Get HuggingFace models status with unified display"""
560
- try:
561
- import ai_models
562
-
563
- model_info = ai_models.get_model_info()
564
-
565
- # Build unified table - avoid duplicates
566
- table_data = []
567
- seen_models = set()
568
-
569
- # First, add loaded models
570
- if model_info.get('models_initialized'):
571
- for model_name, loaded in model_info.get('loaded_models', {}).items():
572
- if model_name not in seen_models:
573
- status = "✅ Loaded" if loaded else "❌ Failed"
574
- model_id = config.HUGGINGFACE_MODELS.get(model_name, 'N/A')
575
- table_data.append({
576
- "Model Type": model_name,
577
- "Model ID": model_id,
578
- "Status": status,
579
- "Source": "config.py"
580
- })
581
- seen_models.add(model_name)
582
-
583
- # Then add configured but not loaded models
584
- for model_type, model_id in config.HUGGINGFACE_MODELS.items():
585
- if model_type not in seen_models:
586
- table_data.append({
587
- "Model Type": model_type,
588
- "Model ID": model_id,
589
- "Status": "⏳ Not Loaded",
590
- "Source": "config.py"
591
- })
592
- seen_models.add(model_type)
593
-
594
- # Add models from providers_config if any
595
- try:
596
- providers_path = config.BASE_DIR / "providers_config_extended.json"
597
- if providers_path.exists():
598
- with open(providers_path, 'r') as f:
599
- providers_data = json.load(f)
600
-
601
- for provider_id, provider_info in providers_data.get('providers', {}).items():
602
- if provider_info.get('category') == 'hf-model':
603
- model_name = provider_info.get('name', provider_id)
604
- if model_name not in seen_models:
605
- table_data.append({
606
- "Model Type": model_name,
607
- "Model ID": provider_id,
608
- "Status": "📚 Registry",
609
- "Source": "providers_config"
610
- })
611
- seen_models.add(model_name)
612
- except Exception as e:
613
- logger.warning(f"Could not load models from providers_config: {e}")
614
-
615
- if not table_data:
616
- table_data.append({
617
- "Model Type": "No models",
618
- "Model ID": "N/A",
619
- "Status": "⚠️ None configured",
620
- "Source": "N/A"
621
- })
622
-
623
- if PANDAS_AVAILABLE:
624
- return pd.DataFrame(table_data)
625
- else:
626
- return {"models": table_data}
627
-
628
- except Exception as e:
629
- logger.error(f"Error getting HF models status: {e}")
630
- if PANDAS_AVAILABLE:
631
- return pd.DataFrame({"Error": [str(e)]})
632
- return {"error": str(e)}
633
-
634
-
635
- def test_hf_model(model_name: str, test_text: str) -> str:
636
- """Test a HuggingFace model with text"""
637
- try:
638
- if not test_text or not test_text.strip():
639
- return "⚠️ Please enter test text"
640
-
641
- import ai_models
642
-
643
- if model_name in ["sentiment_twitter", "sentiment_financial", "sentiment"]:
644
- # Test sentiment analysis
645
- result = ai_models.analyze_sentiment(test_text)
646
-
647
- output = f"""
648
- ## Sentiment Analysis Result
649
-
650
- **Input**: {test_text}
651
-
652
- **Label**: {result.get('label', 'N/A')}
653
- **Score**: {result.get('score', 0):.4f}
654
- **Confidence**: {result.get('confidence', 0):.4f}
655
-
656
- **Details**:
657
- ```json
658
- {json.dumps(result.get('details', {}), indent=2)}
659
- ```
660
- """
661
- return output
662
-
663
- elif model_name == "summarization":
664
- # Test summarization
665
- summary = ai_models.summarize_text(test_text)
666
-
667
- output = f"""
668
- ## Summarization Result
669
-
670
- **Original** ({len(test_text)} chars):
671
- {test_text}
672
-
673
- **Summary** ({len(summary)} chars):
674
- {summary}
675
- """
676
- return output
677
-
678
- else:
679
- return f"⚠️ Model '{model_name}' not recognized or not testable"
680
-
681
- except Exception as e:
682
- logger.error(f"Error testing HF model: {e}")
683
- return f"❌ Model test failed: {str(e)}"
684
-
685
-
686
- def initialize_hf_models() -> Tuple[Any, str]:
687
- """Initialize HuggingFace models"""
688
- try:
689
- import ai_models
690
-
691
- result = ai_models.initialize_models()
692
-
693
- if result.get('success'):
694
- message = f"✅ Models initialized successfully at {datetime.now().strftime('%H:%M:%S')}"
695
- else:
696
- message = f"⚠️ Model initialization completed with warnings: {result.get('status')}"
697
-
698
- # Return updated table
699
- table = get_hf_models_status()
700
- return table, message
701
-
702
- except Exception as e:
703
- logger.error(f"Error initializing HF models: {e}")
704
- return get_hf_models_status(), f"❌ Initialization failed: {str(e)}"
705
-
706
-
707
- # ==================== TAB 6: DIAGNOSTICS ====================
708
-
709
- def run_full_diagnostics(auto_fix: bool) -> str:
710
- """Run full system diagnostics"""
711
- try:
712
- from backend.services.diagnostics_service import DiagnosticsService
713
-
714
- logger.info(f"Running diagnostics (auto_fix={auto_fix})...")
715
-
716
- diagnostics = DiagnosticsService()
717
-
718
- # Run async in sync context
719
- loop = asyncio.new_event_loop()
720
- asyncio.set_event_loop(loop)
721
- report = loop.run_until_complete(diagnostics.run_full_diagnostics(auto_fix=auto_fix))
722
- loop.close()
723
-
724
- # Format detailed output
725
- output = f"""
726
- # 🔧 System Diagnostics Report
727
-
728
- **Generated**: {report.timestamp}
729
- **Duration**: {report.duration_ms:.2f}ms
730
-
731
- ---
732
-
733
- ## 📊 Summary
734
-
735
- | Metric | Count |
736
- |--------|-------|
737
- | **Total Issues** | {report.total_issues} |
738
- | **Critical** 🔴 | {report.critical_issues} |
739
- | **Warnings** 🟡 | {report.warnings} |
740
- | **Info** 🔵 | {report.info_issues} |
741
- | **Auto-Fixed** ✅ | {len(report.fixed_issues)} |
742
-
743
- ---
744
-
745
- ## 🔍 Issues Detected
746
-
747
- """
748
-
749
- if not report.issues:
750
- output += "✅ **No issues detected!** System is healthy.\n"
751
- else:
752
- # Group by category
753
- by_category = {}
754
- for issue in report.issues:
755
- cat = issue.category
756
- if cat not in by_category:
757
- by_category[cat] = []
758
- by_category[cat].append(issue)
759
-
760
- for category, issues in sorted(by_category.items()):
761
- output += f"\n### {category.upper()}\n\n"
762
-
763
- for issue in issues:
764
- emoji = {"critical": "🔴", "warning": "🟡", "info": "🔵"}.get(issue.severity, "⚪")
765
- fixed_mark = " ✅ **AUTO-FIXED**" if issue.auto_fixed else ""
766
-
767
- output += f"**{emoji} {issue.title}**{fixed_mark}\n\n"
768
- output += f"{issue.description}\n\n"
769
-
770
- if issue.fixable and issue.fix_action and not issue.auto_fixed:
771
- output += f"💡 **Fix**: `{issue.fix_action}`\n\n"
772
-
773
- output += "---\n\n"
774
-
775
- # System info
776
- output += "\n## 💻 System Information\n\n"
777
- output += "```json\n"
778
- output += json.dumps(report.system_info, indent=2)
779
- output += "\n```\n"
780
-
781
- return output
782
-
783
- except Exception as e:
784
- logger.error(f"Error running diagnostics: {e}\n{traceback.format_exc()}")
785
- return f"❌ Diagnostics failed: {str(e)}\n\nCheck logs for details."
786
-
787
-
788
- # ==================== TAB 7: LOGS ====================
789
-
790
- def get_logs(log_type: str = "recent", lines: int = 100) -> str:
791
- """Get system logs with copy-friendly format"""
792
- try:
793
- log_file = config.LOG_FILE
794
-
795
- if not log_file.exists():
796
- return "⚠️ Log file not found"
797
-
798
- # Read log file
799
- with open(log_file, 'r') as f:
800
- all_lines = f.readlines()
801
-
802
- # Filter based on log_type
803
- if log_type == "errors":
804
- filtered_lines = [line for line in all_lines if 'ERROR' in line or 'CRITICAL' in line]
805
- elif log_type == "warnings":
806
- filtered_lines = [line for line in all_lines if 'WARNING' in line]
807
- else: # recent
808
- filtered_lines = all_lines
809
-
810
- # Get last N lines
811
- recent_lines = filtered_lines[-lines:] if len(filtered_lines) > lines else filtered_lines
812
-
813
- if not recent_lines:
814
- return f"ℹ️ No {log_type} logs found"
815
-
816
- # Format output with line numbers for easy reference
817
- output = f"# 📋 {log_type.upper()} Logs (Last {len(recent_lines)} lines)\n\n"
818
- output += "**Quick Stats:**\n"
819
- output += f"- Total lines shown: `{len(recent_lines)}`\n"
820
- output += f"- Log file: `{log_file}`\n"
821
- output += f"- Type: `{log_type}`\n\n"
822
- output += "---\n\n"
823
- output += "```log\n"
824
- for i, line in enumerate(recent_lines, 1):
825
- output += f"{i:4d} | {line}"
826
- output += "\n```\n"
827
- output += "\n---\n"
828
- output += "💡 **Tip**: You can now copy individual lines or the entire log block\n"
829
-
830
- return output
831
-
832
- except Exception as e:
833
- logger.error(f"Error reading logs: {e}")
834
- return f"❌ Error reading logs: {str(e)}"
835
-
836
-
837
- def clear_logs() -> str:
838
- """Clear log file"""
839
- try:
840
- log_file = config.LOG_FILE
841
-
842
- if log_file.exists():
843
- # Backup first
844
- backup_path = log_file.parent / f"{log_file.name}.backup.{int(datetime.now().timestamp())}"
845
- import shutil
846
- shutil.copy2(log_file, backup_path)
847
-
848
- # Clear
849
- with open(log_file, 'w') as f:
850
- f.write("")
851
-
852
- logger.info("Log file cleared")
853
- return f"✅ Logs cleared (backup saved to {backup_path.name})"
854
- else:
855
- return "⚠️ No log file to clear"
856
-
857
- except Exception as e:
858
- logger.error(f"Error clearing logs: {e}")
859
- return f"❌ Error clearing logs: {str(e)}"
860
-
861
-
862
- # ==================== GRADIO INTERFACE ====================
863
-
864
- def build_interface():
865
- """Build the complete Gradio Blocks interface"""
866
-
867
- with gr.Blocks(title="Crypto Admin Dashboard", theme=gr.themes.Soft()) as demo:
868
-
869
- gr.Markdown("""
870
- # 🚀 Crypto Data Aggregator - Admin Dashboard
871
-
872
- **Real-time cryptocurrency data aggregation and analysis platform**
873
-
874
- Features: Provider Management | Market Data | Auto Provider Loader | HF Models | System Diagnostics
875
- """)
876
-
877
- with gr.Tabs():
878
-
879
- # ==================== TAB 1: STATUS ====================
880
- with gr.Tab("📊 Status"):
881
- gr.Markdown("### System Status Overview")
882
-
883
- with gr.Row():
884
- status_refresh_btn = gr.Button("🔄 Refresh Status", variant="primary")
885
- status_diag_btn = gr.Button("🔧 Run Quick Diagnostics")
886
-
887
- status_summary = gr.Markdown()
888
-
889
- with gr.Row():
890
- with gr.Column():
891
- gr.Markdown("#### Database Statistics")
892
- db_stats_json = gr.JSON()
893
-
894
- with gr.Column():
895
- gr.Markdown("#### System Information")
896
- system_info_json = gr.JSON()
897
-
898
- diag_output = gr.Markdown()
899
-
900
- # Load initial status
901
- demo.load(
902
- fn=get_status_tab,
903
- outputs=[status_summary, db_stats_json, system_info_json]
904
- )
905
-
906
- # Refresh button
907
- status_refresh_btn.click(
908
- fn=get_status_tab,
909
- outputs=[status_summary, db_stats_json, system_info_json]
910
- )
911
-
912
- # Quick diagnostics
913
- status_diag_btn.click(
914
- fn=lambda: run_diagnostics_from_status(False),
915
- outputs=diag_output
916
- )
917
-
918
- # ==================== TAB 2: PROVIDERS ====================
919
- with gr.Tab("🔌 Providers"):
920
- gr.Markdown("### API Provider Management")
921
-
922
- with gr.Row():
923
- provider_category = gr.Dropdown(
924
- label="Filter by Category",
925
- choices=get_provider_categories(),
926
- value="All"
927
- )
928
- provider_reload_btn = gr.Button("🔄 Reload Providers", variant="primary")
929
-
930
- providers_table = gr.Dataframe(
931
- label="Providers",
932
- interactive=False,
933
- wrap=True
934
- ) if PANDAS_AVAILABLE else gr.JSON(label="Providers")
935
-
936
- provider_status = gr.Textbox(label="Status", interactive=False)
937
-
938
- # Load initial providers
939
- demo.load(
940
- fn=lambda: get_providers_table("All"),
941
- outputs=providers_table
942
- )
943
-
944
- # Category filter
945
- provider_category.change(
946
- fn=get_providers_table,
947
- inputs=provider_category,
948
- outputs=providers_table
949
- )
950
-
951
- # Reload button
952
- provider_reload_btn.click(
953
- fn=reload_providers_config,
954
- outputs=[providers_table, provider_status]
955
- )
956
-
957
- # ==================== TAB 3: MARKET DATA ====================
958
- with gr.Tab("📈 Market Data"):
959
- gr.Markdown("### Live Cryptocurrency Market Data")
960
-
961
- with gr.Row():
962
- market_search = gr.Textbox(
963
- label="Search",
964
- placeholder="Search by name or symbol..."
965
- )
966
- market_refresh_btn = gr.Button("🔄 Refresh Prices", variant="primary")
967
-
968
- market_table = gr.Dataframe(
969
- label="Market Data",
970
- interactive=False,
971
- wrap=True,
972
- height=400
973
- ) if PANDAS_AVAILABLE else gr.JSON(label="Market Data")
974
-
975
- market_status = gr.Textbox(label="Status", interactive=False)
976
-
977
- # Price chart section
978
- if PLOTLY_AVAILABLE:
979
- gr.Markdown("#### Price History Chart")
980
-
981
- with gr.Row():
982
- chart_symbol = gr.Textbox(
983
- label="Symbol",
984
- placeholder="BTC",
985
- value="BTC"
986
- )
987
- chart_timeframe = gr.Dropdown(
988
- label="Timeframe",
989
- choices=["24h", "7d", "30d", "90d"],
990
- value="7d"
991
- )
992
- chart_plot_btn = gr.Button("📊 Plot")
993
-
994
- price_chart = gr.Plot(label="Price History")
995
-
996
- chart_plot_btn.click(
997
- fn=plot_price_history,
998
- inputs=[chart_symbol, chart_timeframe],
999
- outputs=price_chart
1000
- )
1001
-
1002
- # Load initial data
1003
- demo.load(
1004
- fn=lambda: get_market_data_table(""),
1005
- outputs=market_table
1006
- )
1007
-
1008
- # Search
1009
- market_search.change(
1010
- fn=get_market_data_table,
1011
- inputs=market_search,
1012
- outputs=market_table
1013
- )
1014
-
1015
- # Refresh
1016
- market_refresh_btn.click(
1017
- fn=refresh_market_data,
1018
- outputs=[market_table, market_status]
1019
- )
1020
-
1021
- # ==================== TAB 4: APL SCANNER ====================
1022
- with gr.Tab("🔍 APL Scanner"):
1023
- gr.Markdown("### Auto Provider Loader")
1024
- gr.Markdown("Automatically discover, validate, and integrate API providers and HuggingFace models.")
1025
-
1026
- with gr.Row():
1027
- apl_scan_btn = gr.Button("▶️ Run APL Scan", variant="primary", size="lg")
1028
- apl_report_btn = gr.Button("📄 View Last Report")
1029
-
1030
- apl_output = gr.Markdown()
1031
-
1032
- apl_scan_btn.click(
1033
- fn=run_apl_scan,
1034
- outputs=apl_output
1035
- )
1036
-
1037
- apl_report_btn.click(
1038
- fn=get_apl_report,
1039
- outputs=apl_output
1040
- )
1041
-
1042
- # Load last report on startup
1043
- demo.load(
1044
- fn=get_apl_report,
1045
- outputs=apl_output
1046
- )
1047
-
1048
- # ==================== TAB 5: HF MODELS ====================
1049
- with gr.Tab("🤖 HF Models"):
1050
- gr.Markdown("### HuggingFace Models Status & Testing")
1051
-
1052
- with gr.Row():
1053
- hf_init_btn = gr.Button("🔄 Initialize Models", variant="primary")
1054
- hf_refresh_btn = gr.Button("🔄 Refresh Status")
1055
-
1056
- hf_models_table = gr.Dataframe(
1057
- label="Models",
1058
- interactive=False
1059
- ) if PANDAS_AVAILABLE else gr.JSON(label="Models")
1060
-
1061
- hf_status = gr.Textbox(label="Status", interactive=False)
1062
-
1063
- gr.Markdown("#### Test Model")
1064
-
1065
- with gr.Row():
1066
- test_model_dropdown = gr.Dropdown(
1067
- label="Model",
1068
- choices=["sentiment", "sentiment_twitter", "sentiment_financial", "summarization"],
1069
- value="sentiment"
1070
- )
1071
-
1072
- test_input = gr.Textbox(
1073
- label="Test Input",
1074
- placeholder="Enter text to test the model...",
1075
- lines=3
1076
- )
1077
-
1078
- test_btn = gr.Button("▶️ Run Test", variant="secondary")
1079
-
1080
- test_output = gr.Markdown(label="Test Output")
1081
-
1082
- # Load initial status
1083
- demo.load(
1084
- fn=get_hf_models_status,
1085
- outputs=hf_models_table
1086
- )
1087
-
1088
- # Initialize models
1089
- hf_init_btn.click(
1090
- fn=initialize_hf_models,
1091
- outputs=[hf_models_table, hf_status]
1092
- )
1093
-
1094
- # Refresh status
1095
- hf_refresh_btn.click(
1096
- fn=get_hf_models_status,
1097
- outputs=hf_models_table
1098
- )
1099
-
1100
- # Test model
1101
- test_btn.click(
1102
- fn=test_hf_model,
1103
- inputs=[test_model_dropdown, test_input],
1104
- outputs=test_output
1105
- )
1106
-
1107
- # ==================== TAB 6: DIAGNOSTICS ====================
1108
- with gr.Tab("🔧 Diagnostics"):
1109
- gr.Markdown("### System Diagnostics & Auto-Repair")
1110
-
1111
- with gr.Row():
1112
- diag_run_btn = gr.Button("▶️ Run Diagnostics", variant="primary")
1113
- diag_autofix_btn = gr.Button("🔧 Run with Auto-Fix", variant="secondary")
1114
-
1115
- diagnostics_output = gr.Markdown()
1116
-
1117
- diag_run_btn.click(
1118
- fn=lambda: run_full_diagnostics(False),
1119
- outputs=diagnostics_output
1120
- )
1121
-
1122
- diag_autofix_btn.click(
1123
- fn=lambda: run_full_diagnostics(True),
1124
- outputs=diagnostics_output
1125
- )
1126
-
1127
- # ==================== TAB 7: LOGS ====================
1128
- with gr.Tab("📋 Logs"):
1129
- gr.Markdown("### System Logs Viewer")
1130
-
1131
- with gr.Row():
1132
- log_type = gr.Dropdown(
1133
- label="Log Type",
1134
- choices=["recent", "errors", "warnings"],
1135
- value="recent"
1136
- )
1137
- log_lines = gr.Slider(
1138
- label="Lines to Show",
1139
- minimum=10,
1140
- maximum=500,
1141
- value=100,
1142
- step=10
1143
- )
1144
-
1145
- with gr.Row():
1146
- log_refresh_btn = gr.Button("🔄 Refresh Logs", variant="primary")
1147
- log_clear_btn = gr.Button("🗑️ Clear Logs", variant="secondary")
1148
-
1149
- logs_output = gr.Markdown()
1150
- log_clear_status = gr.Textbox(label="Status", interactive=False, visible=False)
1151
-
1152
- # Load initial logs
1153
- demo.load(
1154
- fn=lambda: get_logs("recent", 100),
1155
- outputs=logs_output
1156
- )
1157
-
1158
- # Refresh logs
1159
- log_refresh_btn.click(
1160
- fn=get_logs,
1161
- inputs=[log_type, log_lines],
1162
- outputs=logs_output
1163
- )
1164
-
1165
- # Update when dropdown changes
1166
- log_type.change(
1167
- fn=get_logs,
1168
- inputs=[log_type, log_lines],
1169
- outputs=logs_output
1170
- )
1171
-
1172
- # Clear logs
1173
- log_clear_btn.click(
1174
- fn=clear_logs,
1175
- outputs=log_clear_status
1176
- ).then(
1177
- fn=lambda: get_logs("recent", 100),
1178
- outputs=logs_output
1179
- )
1180
-
1181
- # Footer
1182
- gr.Markdown("""
1183
- ---
1184
- **Crypto Data Aggregator Admin Dashboard** | Real Data Only | No Mock/Fake Data
1185
- """)
1186
-
1187
- return demo
1188
-
1189
-
1190
- # ==================== MAIN ENTRY POINT ====================
1191
-
1192
- demo = build_interface()
1193
-
1194
- if __name__ == "__main__":
1195
- logger.info("Launching Gradio dashboard...")
1196
-
1197
- # Try to mount FastAPI app for API endpoints
1198
- try:
1199
- from fastapi import FastAPI as FastAPIApp
1200
- from fastapi.middleware.wsgi import WSGIMiddleware
1201
- import uvicorn
1202
- from threading import Thread
1203
- import time
1204
-
1205
- # Import the FastAPI app from hf_unified_server
1206
- try:
1207
- from hf_unified_server import app as fastapi_app
1208
- logger.info("✅ FastAPI app imported successfully")
1209
-
1210
- # Start FastAPI server in a separate thread on port 7861
1211
- def run_fastapi():
1212
- uvicorn.run(
1213
- fastapi_app,
1214
- host="0.0.0.0",
1215
- port=7861,
1216
- log_level="info"
1217
- )
1218
-
1219
- fastapi_thread = Thread(target=run_fastapi, daemon=True)
1220
- fastapi_thread.start()
1221
- time.sleep(2) # Give FastAPI time to start
1222
- logger.info("✅ FastAPI server started on port 7861")
1223
- except ImportError as e:
1224
- logger.warning(f"⚠️ Could not import FastAPI app: {e}")
1225
- except Exception as e:
1226
- logger.warning(f"⚠️ Could not start FastAPI server: {e}")
1227
-
1228
- demo.launch(
1229
- server_name="0.0.0.0",
1230
- server_port=7860,
1231
- share=False
1232
- )
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Crypto Intelligence Hub - Hugging Face Space Application
4
+ یکپارچه‌سازی کامل بک‌اند و فرانت‌اند برای جمع‌آوری داده‌های رمز ارز
5
+ Hub کامل با منابع رایگان و مدل‌های Hugging Face
6
+ """
7
+
8
+ import os
9
+ import json
10
+ import asyncio
11
+ import logging
12
+ from pathlib import Path
13
+ from typing import Dict, List, Optional, Any
14
+ from datetime import datetime
15
+ import gradio as gr
16
+ import pandas as pd
17
+ import plotly.graph_objects as go
18
+ import plotly.express as px
19
+ import httpx
20
+
21
+ # Import backend services
22
+ try:
23
+ from api_server_extended import app as fastapi_app
24
+ from ai_models import ModelRegistry, MODEL_SPECS, get_model_info, registry_status
25
+ FASTAPI_AVAILABLE = True
26
+ except ImportError as e:
27
+ logging.warning(f"FastAPI not available: {e}")
28
+ FASTAPI_AVAILABLE = False
29
+ ModelRegistry = None
30
+ MODEL_SPECS = {}
31
+ get_model_info = None
32
+ registry_status = None
33
+
34
+ # Setup logging
35
+ logging.basicConfig(level=logging.INFO)
36
+ logger = logging.getLogger(__name__)
37
+
38
+ # Global state
39
+ WORKSPACE_ROOT = Path("/app" if Path("/app").exists() else Path("."))
40
+ RESOURCES_JSON = WORKSPACE_ROOT / "api-resources" / "crypto_resources_unified_2025-11-11.json"
41
+ ALL_APIS_JSON = WORKSPACE_ROOT / "all_apis_merged_2025.json"
42
+
43
+ # Fallback paths
44
+ if not RESOURCES_JSON.exists():
45
+ RESOURCES_JSON = WORKSPACE_ROOT / "all_apis_merged_2025.json"
46
+ if not ALL_APIS_JSON.exists():
47
+ ALL_APIS_JSON = WORKSPACE_ROOT / "all_apis_merged_2025.json"
48
+
49
+ # Initialize model registry
50
+ model_registry = ModelRegistry() if ModelRegistry else None
51
+
52
+
53
+ class CryptoDataHub:
54
+ """مرکز داده‌های رمز ارز با پشتیبانی از منابع رایگان و مدل‌های Hugging Face"""
55
+
56
+ def __init__(self):
57
+ self.resources = {}
58
+ self.models_loaded = False
59
+ self.load_resources()
60
+ self.initialize_models()
61
+
62
+ def load_resources(self):
63
+ """بارگذاری منابع از فایل‌های JSON"""
64
+ try:
65
+ # Load unified resources
66
+ if RESOURCES_JSON.exists():
67
+ with open(RESOURCES_JSON, 'r', encoding='utf-8') as f:
68
+ data = json.load(f)
69
+ self.resources['unified'] = data
70
+ logger.info(f"✅ Loaded unified resources: {RESOURCES_JSON}")
71
+
72
+ # Load all APIs merged
73
+ if ALL_APIS_JSON.exists():
74
+ with open(ALL_APIS_JSON, 'r', encoding='utf-8') as f:
75
+ data = json.load(f)
76
+ self.resources['all_apis'] = data
77
+ logger.info(f"✅ Loaded all APIs: {ALL_APIS_JSON}")
78
+
79
+ logger.info(f"📊 Total resource files loaded: {len(self.resources)}")
80
+ except Exception as e:
81
+ logger.error(f"❌ Error loading resources: {e}")
82
+
83
+ def initialize_models(self):
84
+ """بارگذاری مدل‌های Hugging Face"""
85
+ if not model_registry:
86
+ logger.warning("Model registry not available")
87
+ return
88
+
89
+ try:
90
+ # Initialize available models
91
+ result = model_registry.initialize_models()
92
+ self.models_loaded = result.get('status') == 'ok'
93
+ logger.info(f"✅ Hugging Face models initialized: {result}")
94
+ except Exception as e:
95
+ logger.warning(f"⚠️ Could not initialize all models: {e}")
96
+
97
+ def get_market_data_sources(self) -> List[Dict]:
98
+ """دریافت منابع داده‌های بازار"""
99
+ sources = []
100
+
101
+ # Try unified resources first
102
+ if 'unified' in self.resources:
103
+ registry = self.resources['unified'].get('registry', {})
104
+
105
+ # Market data APIs
106
+ market_apis = registry.get('market_data', [])
107
+ for api in market_apis:
108
+ sources.append({
109
+ 'name': api.get('name', 'Unknown'),
110
+ 'category': 'market',
111
+ 'base_url': api.get('base_url', ''),
112
+ 'free': api.get('free', False),
113
+ 'auth_required': bool(api.get('auth', {}).get('key'))
114
+ })
115
+
116
+ # Try all_apis structure
117
+ if 'all_apis' in self.resources:
118
+ data = self.resources['all_apis']
119
+
120
+ # Check for discovered_keys which indicates market data sources
121
+ if 'discovered_keys' in data:
122
+ for provider, keys in data['discovered_keys'].items():
123
+ if provider in ['coinmarketcap', 'cryptocompare']:
124
+ sources.append({
125
+ 'name': provider.upper(),
126
+ 'category': 'market',
127
+ 'base_url': f'https://api.{provider}.com' if provider == 'coinmarketcap' else f'https://min-api.{provider}.com',
128
+ 'free': False,
129
+ 'auth_required': True
130
+ })
131
+
132
+ # Check raw_files for API configurations
133
+ if 'raw_files' in data:
134
+ for file_info in data['raw_files']:
135
+ content = file_info.get('content', '')
136
+ if 'CoinGecko' in content or 'coingecko' in content.lower():
137
+ sources.append({
138
+ 'name': 'CoinGecko',
139
+ 'category': 'market',
140
+ 'base_url': 'https://api.coingecko.com/api/v3',
141
+ 'free': True,
142
+ 'auth_required': False
143
+ })
144
+ if 'Binance' in content or 'binance' in content.lower():
145
+ sources.append({
146
+ 'name': 'Binance Public',
147
+ 'category': 'market',
148
+ 'base_url': 'https://api.binance.com/api/v3',
149
+ 'free': True,
150
+ 'auth_required': False
151
+ })
152
+
153
+ # Remove duplicates
154
+ seen = set()
155
+ unique_sources = []
156
+ for source in sources:
157
+ key = source['name']
158
+ if key not in seen:
159
+ seen.add(key)
160
+ unique_sources.append(source)
161
+
162
+ return unique_sources
163
+
164
+ def get_available_models(self) -> List[Dict]:
165
+ """دریافت لیست مدل‌های در دسترس"""
166
+ models = []
167
+
168
+ if MODEL_SPECS:
169
+ for key, spec in MODEL_SPECS.items():
170
+ models.append({
171
+ 'key': key,
172
+ 'name': spec.model_id,
173
+ 'task': spec.task,
174
+ 'category': spec.category,
175
+ 'requires_auth': spec.requires_auth
176
+ })
177
+
178
+ return models
179
+
180
+ async def analyze_sentiment(self, text: str, model_key: str = "crypto_sent_0", use_backend: bool = False) -> Dict:
181
+ """تحلیل احساسات با استفاده از مدل‌های Hugging Face"""
182
+ # Try backend API first if requested and available
183
+ if use_backend and FASTAPI_AVAILABLE:
184
+ try:
185
+ async with httpx.AsyncClient(timeout=30.0) as client:
186
+ response = await client.post(
187
+ "http://localhost:7860/api/hf/run-sentiment",
188
+ json={"texts": [text]},
189
+ headers={"Content-Type": "application/json"}
190
+ )
191
+ if response.status_code == 200:
192
+ data = response.json()
193
+ if data.get("results"):
194
+ result = data["results"][0]
195
+ return {
196
+ 'sentiment': result.get('label', 'unknown'),
197
+ 'confidence': result.get('confidence', 0.0),
198
+ 'model': 'backend_api',
199
+ 'text': text[:100],
200
+ 'vote': result.get('vote', 0.0)
201
+ }
202
+ except Exception as e:
203
+ logger.warning(f"Backend API call failed, falling back to direct model: {e}")
204
+
205
+ # Direct model access
206
+ if not model_registry or not self.models_loaded:
207
+ return {
208
+ 'error': 'Models not available',
209
+ 'sentiment': 'unknown',
210
+ 'confidence': 0.0
211
+ }
212
+
213
+ try:
214
+ pipeline = model_registry.get_pipeline(model_key)
215
+ result = pipeline(text)
216
+
217
+ # Handle different result formats
218
+ if isinstance(result, list) and len(result) > 0:
219
+ result = result[0]
220
+
221
+ return {
222
+ 'sentiment': result.get('label', 'unknown'),
223
+ 'confidence': result.get('score', 0.0),
224
+ 'model': model_key,
225
+ 'text': text[:100]
226
+ }
227
+ except Exception as e:
228
+ logger.error(f"Error analyzing sentiment: {e}")
229
+ return {
230
+ 'error': str(e),
231
+ 'sentiment': 'error',
232
+ 'confidence': 0.0
233
+ }
234
+
235
+ def get_resource_summary(self) -> Dict:
236
+ """خلاصه منابع موجود"""
237
+ summary = {
238
+ 'total_resources': 0,
239
+ 'categories': {},
240
+ 'free_resources': 0,
241
+ 'models_available': len(self.get_available_models())
242
+ }
243
+
244
+ if 'unified' in self.resources:
245
+ registry = self.resources['unified'].get('registry', {})
246
+
247
+ for category, items in registry.items():
248
+ if isinstance(items, list):
249
+ count = len(items)
250
+ summary['total_resources'] += count
251
+ summary['categories'][category] = count
252
+
253
+ # Count free resources
254
+ free_count = sum(1 for item in items if item.get('free', False))
255
+ summary['free_resources'] += free_count
256
+
257
+ # Add market sources
258
+ market_sources = self.get_market_data_sources()
259
+ if market_sources:
260
+ summary['total_resources'] += len(market_sources)
261
+ summary['categories']['market_data'] = len(market_sources)
262
+ summary['free_resources'] += sum(1 for s in market_sources if s.get('free', False))
263
+
264
+ return summary
265
+
266
+
267
+ # Initialize global hub
268
+ hub = CryptoDataHub()
269
+
270
+
271
+ # =============================================================================
272
+ # Gradio Interface Functions
273
+ # =============================================================================
274
+
275
+ def get_dashboard_summary():
276
+ """نمایش خلاصه داشبورد"""
277
+ summary = hub.get_resource_summary()
278
+
279
+ html = f"""
280
+ <div style="padding: 20px; font-family: Arial, sans-serif;">
281
+ <h2>📊 خلاصه منابع و مدل‌ها</h2>
282
+
283
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin: 20px 0;">
284
+ <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 20px; border-radius: 10px; color: white;">
285
+ <h3>منابع کل</h3>
286
+ <p style="font-size: 32px; margin: 10px 0; font-weight: bold;">{summary['total_resources']}</p>
287
+ </div>
288
+
289
+ <div style="background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); padding: 20px; border-radius: 10px; color: white;">
290
+ <h3>منابع رایگان</h3>
291
+ <p style="font-size: 32px; margin: 10px 0; font-weight: bold;">{summary['free_resources']}</p>
292
+ </div>
293
+
294
+ <div style="background: linear-gradient(135deg, #ee0979 0%, #ff6a00 100%); padding: 20px; border-radius: 10px; color: white;">
295
+ <h3>مدل‌های AI</h3>
296
+ <p style="font-size: 32px; margin: 10px 0; font-weight: bold;">{summary['models_available']}</p>
297
+ </div>
298
+
299
+ <div style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); padding: 20px; border-radius: 10px; color: white;">
300
+ <h3>دسته‌بندی‌ها</h3>
301
+ <p style="font-size: 32px; margin: 10px 0; font-weight: bold;">{len(summary['categories'])}</p>
302
+ </div>
303
+ </div>
304
+
305
+ <h3>دسته‌بندی منابع:</h3>
306
+ <ul>
307
+ """
308
+
309
+ for category, count in summary['categories'].items():
310
+ html += f"<li><strong>{category}:</strong> {count} منبع</li>"
311
+
312
+ html += """
313
+ </ul>
314
+ </div>
315
+ """
316
+
317
+ return html
318
+
319
+
320
+ def get_resources_table():
321
+ """جدول منابع"""
322
+ sources = hub.get_market_data_sources()
323
+
324
+ if not sources:
325
+ return pd.DataFrame({'پیام': ['هیچ منبعی یافت نشد. لطفاً فایل‌های JSON را بررسی کنید.']})
326
+
327
+ df_data = []
328
+ for source in sources[:100]: # Limit to 100 for display
329
+ df_data.append({
330
+ 'نام': source['name'],
331
+ 'دسته': source['category'],
332
+ 'رایگان': '✅' if source['free'] else '❌',
333
+ 'نیاز به کلید': '✅' if source['auth_required'] else '❌',
334
+ 'URL پایه': source['base_url'][:60] + '...' if len(source['base_url']) > 60 else source['base_url']
335
+ })
336
+
337
+ return pd.DataFrame(df_data)
338
+
339
+
340
+ def get_models_table():
341
+ """جدول مدل‌ها"""
342
+ models = hub.get_available_models()
343
+
344
+ if not models:
345
+ return pd.DataFrame({'پیام': ['هیچ مدلی یافت نشد. مدل‌ها در حال بارگذاری هستند...']})
346
+
347
+ df_data = []
348
+ for model in models:
349
+ df_data.append({
350
+ 'کلید': model['key'],
351
+ 'نام مدل': model['name'],
352
+ 'نوع کار': model['task'],
353
+ 'دسته': model['category'],
354
+ 'نیاز به احراز هویت': '✅' if model['requires_auth'] else '❌'
355
+ })
356
+
357
+ return pd.DataFrame(df_data)
358
+
359
+
360
+ def analyze_text_sentiment(text: str, model_selection: str, use_backend: bool = False):
361
+ """تحلیل احساسات متن"""
362
+ if not text.strip():
363
+ return "⚠️ لطفاً متنی وارد کنید", ""
364
+
365
+ try:
366
+ # Extract model key from dropdown selection
367
+ if model_selection and " - " in model_selection:
368
+ model_key = model_selection.split(" - ")[0]
369
+ else:
370
+ model_key = model_selection if model_selection else "crypto_sent_0"
371
+
372
+ result = asyncio.run(hub.analyze_sentiment(text, model_key, use_backend=use_backend))
373
+
374
+ if 'error' in result:
375
+ return f" خطا: {result['error']}", ""
376
+
377
+ sentiment_emoji = {
378
+ 'POSITIVE': '📈',
379
+ 'NEGATIVE': '📉',
380
+ 'NEUTRAL': '➡️',
381
+ 'LABEL_0': '📈',
382
+ 'LABEL_1': '📉',
383
+ 'LABEL_2': '➡️',
384
+ 'positive': '📈',
385
+ 'negative': '📉',
386
+ 'neutral': '➡️',
387
+ 'bullish': '📈',
388
+ 'bearish': '📉'
389
+ }.get(result['sentiment'], '❓')
390
+
391
+ confidence_pct = result['confidence'] * 100 if result['confidence'] <= 1.0 else result['confidence']
392
+
393
+ vote_info = ""
394
+ if 'vote' in result:
395
+ vote_emoji = '📈' if result['vote'] > 0 else '📉' if result['vote'] < 0 else '➡️'
396
+ vote_info = f"\n**رأی مدل:** {vote_emoji} {result['vote']:.2f}"
397
+
398
+ result_text = f"""
399
+ ## نتیجه تحلیل احساسات
400
+
401
+ **احساسات:** {sentiment_emoji} {result['sentiment']}
402
+ **اعتماد:** {confidence_pct:.2f}%
403
+ **مدل استفاده شده:** {result['model']}
404
+ **متن تحلیل شده:** {result['text']}
405
+ {vote_info}
406
+ """
407
+
408
+ result_json = json.dumps(result, indent=2, ensure_ascii=False)
409
+
410
+ return result_text, result_json
411
+ except Exception as e:
412
+ return f"❌ خطا در تحلیل: {str(e)}", ""
413
+
414
+
415
+ def create_category_chart():
416
+ """نمودار دسته‌بندی منابع"""
417
+ summary = hub.get_resource_summary()
418
+
419
+ categories = list(summary['categories'].keys())
420
+ counts = list(summary['categories'].values())
421
+
422
+ if not categories:
423
+ fig = go.Figure()
424
+ fig.add_annotation(
425
+ text="No data available",
426
+ xref="paper", yref="paper",
427
+ x=0.5, y=0.5, showarrow=False
428
+ )
429
+ return fig
430
+
431
+ fig = go.Figure(data=[
432
+ go.Bar(
433
+ x=categories,
434
+ y=counts,
435
+ marker_color='lightblue',
436
+ text=counts,
437
+ textposition='auto'
438
+ )
439
+ ])
440
+
441
+ fig.update_layout(
442
+ title='توزیع منابع بر اساس دسته‌بندی',
443
+ xaxis_title='دسته‌بندی',
444
+ yaxis_title='تعداد منابع',
445
+ template='plotly_white',
446
+ height=400
447
+ )
448
+
449
+ return fig
450
+
451
+
452
+ def get_model_status():
453
+ """وضعیت مدل‌ها"""
454
+ if not registry_status:
455
+ return "❌ Model registry not available"
456
+
457
+ status = registry_status()
458
+
459
+ html = f"""
460
+ <div style="padding: 20px;">
461
+ <h3>وضعیت مدل‌ها</h3>
462
+ <p><strong>وضعیت:</strong> {'✅ فعال' if status.get('ok') else '❌ غیرفعال'}</p>
463
+ <p><strong>مدل‌های بارگذاری شده:</strong> {status.get('pipelines_loaded', 0)}</p>
464
+ <p><strong>مدل‌های در دسترس:</strong> {len(status.get('available_models', []))}</p>
465
+ <p><strong>حالت Hugging Face:</strong> {status.get('hf_mode', 'unknown')}</p>
466
+ <p><strong>Transformers موجود:</strong> {'✅' if status.get('transformers_available') else '❌'}</p>
467
+ </div>
468
+ """
469
+
470
+ return html
471
+
472
+
473
+ # =============================================================================
474
+ # Build Gradio Interface
475
+ # =============================================================================
476
+
477
+ def create_interface():
478
+ """ایجاد رابط کاربری Gradio"""
479
+
480
+ # Get available models for dropdown
481
+ models = hub.get_available_models()
482
+ model_choices = [f"{m['key']} - {m['name']}" for m in models] if models else ["crypto_sent_0 - CryptoBERT"]
483
+ model_keys = [m['key'] for m in models] if models else ["crypto_sent_0"]
484
+
485
+ with gr.Blocks(
486
+ theme=gr.themes.Soft(primary_hue="blue", secondary_hue="purple"),
487
+ title="Crypto Intelligence Hub - مرکز هوش رمز ارز",
488
+ css="""
489
+ .gradio-container {
490
+ max-width: 1400px !important;
491
+ }
492
+ """
493
+ ) as app:
494
+
495
+ gr.Markdown("""
496
+ # 🚀 Crypto Intelligence Hub
497
+ ## مرکز هوش مصنوعی و جمع‌آوری داده‌های رمز ارز
498
+
499
+ **منابع رایگان | مدل‌های Hugging Face | رابط کاربری کامل**
500
+
501
+ این برنامه یک رابط کامل برای دسترسی به منابع رایگان داده‌های رمز ارز و استفاده از مدل‌های هوش مصنوعی Hugging Face است.
502
+ """)
503
+
504
+ # Tab 1: Dashboard
505
+ with gr.Tab("📊 داشبورد"):
506
+ dashboard_summary = gr.HTML()
507
+ refresh_dashboard_btn = gr.Button("🔄 به‌روزرسانی", variant="primary")
508
+
509
+ refresh_dashboard_btn.click(
510
+ fn=get_dashboard_summary,
511
+ outputs=dashboard_summary
512
+ )
513
+
514
+ app.load(
515
+ fn=get_dashboard_summary,
516
+ outputs=dashboard_summary
517
+ )
518
+
519
+ # Tab 2: Resources
520
+ with gr.Tab("📚 منابع داده"):
521
+ gr.Markdown("### منابع رایگان برای جمع‌آوری داده‌های رمز ارز")
522
+
523
+ resources_table = gr.DataFrame(
524
+ label="لیست منابع",
525
+ wrap=True
526
+ )
527
+
528
+ refresh_resources_btn = gr.Button("🔄 به‌روزرسانی", variant="primary")
529
+
530
+ refresh_resources_btn.click(
531
+ fn=get_resources_table,
532
+ outputs=resources_table
533
+ )
534
+
535
+ app.load(
536
+ fn=get_resources_table,
537
+ outputs=resources_table
538
+ )
539
+
540
+ category_chart = gr.Plot(label="نمودار دسته‌بندی")
541
+
542
+ refresh_resources_btn.click(
543
+ fn=create_category_chart,
544
+ outputs=category_chart
545
+ )
546
+
547
+ # Tab 3: AI Models
548
+ with gr.Tab("🤖 مدل‌های AI"):
549
+ gr.Markdown("### مدل‌های Hugging Face برای تحلیل احساسات و هوش مصنوعی")
550
+
551
+ model_status_html = gr.HTML()
552
+
553
+ models_table = gr.DataFrame(
554
+ label="لیست مدل‌ها",
555
+ wrap=True
556
+ )
557
+
558
+ refresh_models_btn = gr.Button("🔄 به‌روزرسانی", variant="primary")
559
+
560
+ refresh_models_btn.click(
561
+ fn=get_models_table,
562
+ outputs=models_table
563
+ )
564
+
565
+ refresh_models_btn.click(
566
+ fn=get_model_status,
567
+ outputs=model_status_html
568
+ )
569
+
570
+ app.load(
571
+ fn=get_models_table,
572
+ outputs=models_table
573
+ )
574
+
575
+ app.load(
576
+ fn=get_model_status,
577
+ outputs=model_status_html
578
+ )
579
+
580
+ # Tab 4: Sentiment Analysis
581
+ with gr.Tab("💭 تحلیل احساسات"):
582
+ gr.Markdown("### تحلیل احساسات متن با استفاده از مدل‌های Hugging Face")
583
+
584
+ with gr.Row():
585
+ sentiment_text = gr.Textbox(
586
+ label="متن برای تحلیل",
587
+ placeholder="مثال: Bitcoin price is rising rapidly! The market shows strong bullish momentum.",
588
+ lines=5
589
+ )
590
+
591
+ with gr.Row():
592
+ model_dropdown = gr.Dropdown(
593
+ choices=model_choices,
594
+ value=model_choices[0] if model_choices else None,
595
+ label="انتخاب مدل"
596
+ )
597
+ use_backend_check = gr.Checkbox(
598
+ label="استفاده از بک‌اند API (در صورت موجود بودن)",
599
+ value=False
600
+ )
601
+ analyze_btn = gr.Button("🔍 تحلیل", variant="primary")
602
+
603
+ with gr.Row():
604
+ sentiment_result = gr.Markdown(label="نتیجه")
605
+ sentiment_json = gr.Code(
606
+ label="JSON خروجی",
607
+ language="json"
608
+ )
609
+
610
+ def analyze_with_selected_model(text, model_choice, use_backend):
611
+ return analyze_text_sentiment(text, model_choice, use_backend=use_backend)
612
+
613
+ analyze_btn.click(
614
+ fn=analyze_with_selected_model,
615
+ inputs=[sentiment_text, model_dropdown, use_backend_check],
616
+ outputs=[sentiment_result, sentiment_json]
617
+ )
618
+
619
+ # Example texts
620
+ gr.Markdown("""
621
+ ### مثال‌های متن:
622
+ - "Bitcoin is showing strong bullish momentum"
623
+ - "Market crash expected due to regulatory concerns"
624
+ - "Ethereum network upgrade successful"
625
+ - "Crypto market sentiment is very positive today"
626
+ """)
627
+
628
+ # Tab 5: API Integration
629
+ with gr.Tab("🔌 یکپارچه‌سازی API"):
630
+ gr.Markdown("""
631
+ ### اتصال به بک‌اند FastAPI
632
+
633
+ این بخش به سرویس‌های بک‌اند متصل می‌شود که از منابع JSON استفاده می‌کنند.
634
+
635
+ **وضعیت:** {'✅ فعال' if FASTAPI_AVAILABLE else '❌ غیرفعال'}
636
+ """)
637
+
638
+ if FASTAPI_AVAILABLE:
639
+ gr.Markdown("""
640
+ **API Endpoints در دسترس:**
641
+ - `/api/market-data` - داده‌های بازار
642
+ - `/api/sentiment` - تحلیل احساسات
643
+ - `/api/news` - اخبار رمز ارز
644
+ - `/api/resources` - لیست منابع
645
+ """)
646
+
647
+ # Show resource summary
648
+ resource_info = gr.Markdown()
649
+
650
+ def get_resource_info():
651
+ summary = hub.get_resource_summary()
652
+ return f"""
653
+ ## اطلاعات منابع
654
+
655
+ - **کل منابع:** {summary['total_resources']}
656
+ - **منابع رایگان:** {summary['free_resources']}
657
+ - **مدل‌های AI:** {summary['models_available']}
658
+ - **دسته‌بندی‌ها:** {len(summary['categories'])}
659
+
660
+ ### دسته‌بندی‌های موجود:
661
+ {', '.join(summary['categories'].keys()) if summary['categories'] else 'هیچ دسته‌ای یافت نشد'}
662
+ """
663
+
664
+ app.load(
665
+ fn=get_resource_info,
666
+ outputs=resource_info
667
+ )
668
+
669
+ # Footer
670
+ gr.Markdown("""
671
+ ---
672
+ ### 📝 اطلاعات
673
+ - **منابع:** از فایل‌های JSON بارگذاری شده
674
+ - **مدل‌ها:** Hugging Face Transformers
675
+ - **بک‌اند:** FastAPI (در صورت موجود بودن)
676
+ - **فرانت‌اند:** Gradio
677
+ - **محیط:** Hugging Face Spaces (Docker)
678
+ """)
679
+
680
+ return app
681
+
682
+
683
+ # =============================================================================
684
+ # Main Entry Point
685
+ # =============================================================================
686
+
687
+ if __name__ == "__main__":
688
+ logger.info("🚀 Starting Crypto Intelligence Hub...")
689
+ logger.info(f"📁 Workspace: {WORKSPACE_ROOT}")
690
+ logger.info(f"📊 Resources loaded: {len(hub.resources)}")
691
+ logger.info(f"🤖 Models available: {len(hub.get_available_models())}")
692
+ logger.info(f"🔌 FastAPI available: {FASTAPI_AVAILABLE}")
693
+
694
+ # Create and launch interface
695
+ app = create_interface()
696
+
697
+ app.launch(
698
+ server_name="0.0.0.0",
699
+ server_port=7860,
700
+ share=False,
701
+ show_error=True
702
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/__pycache__/__init__.cpython-313.pyc CHANGED
Binary files a/backend/__pycache__/__init__.cpython-313.pyc and b/backend/__pycache__/__init__.cpython-313.pyc differ
 
backend/routers/__pycache__/__init__.cpython-313.pyc CHANGED
Binary files a/backend/routers/__pycache__/__init__.cpython-313.pyc and b/backend/routers/__pycache__/__init__.cpython-313.pyc differ
 
backend/routers/__pycache__/hf_connect.cpython-313.pyc CHANGED
Binary files a/backend/routers/__pycache__/hf_connect.cpython-313.pyc and b/backend/routers/__pycache__/hf_connect.cpython-313.pyc differ
 
backend/services/__pycache__/__init__.cpython-313.pyc CHANGED
Binary files a/backend/services/__pycache__/__init__.cpython-313.pyc and b/backend/services/__pycache__/__init__.cpython-313.pyc differ
 
backend/services/__pycache__/hf_client.cpython-313.pyc CHANGED
Binary files a/backend/services/__pycache__/hf_client.cpython-313.pyc and b/backend/services/__pycache__/hf_client.cpython-313.pyc differ
 
backend/services/__pycache__/hf_registry.cpython-313.pyc CHANGED
Binary files a/backend/services/__pycache__/hf_registry.cpython-313.pyc and b/backend/services/__pycache__/hf_registry.cpython-313.pyc differ
 
backend/services/auto_discovery_service.py CHANGED
@@ -91,10 +91,7 @@ class AutoDiscoveryService:
91
  if InferenceClient is None:
92
  logger.warning("huggingface-hub package not available. Auto discovery will use fallback heuristics.")
93
  else:
94
- # Get HF token from environment or use default
95
- from config import get_settings
96
- settings = get_settings()
97
- hf_token = os.getenv("HF_TOKEN") or os.getenv("HF_API_TOKEN") or settings.hf_token or "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"
98
  try:
99
  self._hf_client = InferenceClient(model=self.hf_model, token=hf_token)
100
  logger.info("Auto discovery Hugging Face client initialized with model %s", self.hf_model)
 
91
  if InferenceClient is None:
92
  logger.warning("huggingface-hub package not available. Auto discovery will use fallback heuristics.")
93
  else:
94
+ hf_token = os.getenv("HF_API_TOKEN")
 
 
 
95
  try:
96
  self._hf_client = InferenceClient(model=self.hf_model, token=hf_token)
97
  logger.info("Auto discovery Hugging Face client initialized with model %s", self.hf_model)
backend/services/diagnostics_service.py CHANGED
@@ -260,14 +260,7 @@ class DiagnosticsService:
260
 
261
  try:
262
  from huggingface_hub import InferenceClient, HfApi
263
- import os
264
- from config import get_settings
265
-
266
- # Get HF token from settings or use default
267
- settings = get_settings()
268
- hf_token = settings.hf_token or os.getenv("HF_TOKEN") or "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"
269
-
270
- api = HfApi(token=hf_token)
271
 
272
  # بررسی مدل‌های استفاده شده
273
  models_to_check = [
 
260
 
261
  try:
262
  from huggingface_hub import InferenceClient, HfApi
263
+ api = HfApi()
 
 
 
 
 
 
 
264
 
265
  # بررسی مدل‌های استفاده شده
266
  models_to_check = [
backend/services/hf_registry.py CHANGED
@@ -8,16 +8,6 @@ HF_API_DATASETS = "https://huggingface.co/api/datasets"
8
  REFRESH_INTERVAL_SEC = int(os.getenv("HF_REGISTRY_REFRESH_SEC", "21600"))
9
  HTTP_TIMEOUT = float(os.getenv("HF_HTTP_TIMEOUT", "8.0"))
10
 
11
- HF_MODE = os.getenv("HF_MODE", "off").lower()
12
- if HF_MODE not in ("off", "public", "auth"):
13
- HF_MODE = "off"
14
-
15
- HF_TOKEN = None
16
- if HF_MODE == "auth":
17
- HF_TOKEN = os.getenv("HF_TOKEN")
18
- if not HF_TOKEN:
19
- HF_MODE = "off"
20
-
21
  # Curated Crypto Datasets
22
  CRYPTO_DATASETS = {
23
  "price": [
@@ -55,81 +45,68 @@ class HFRegistry:
55
  self.fail_reason: Optional[str] = None
56
 
57
  async def _hf_json(self, url: str, params: Dict[str, Any]) -> Any:
58
- headers = {}
59
- if HF_MODE == "auth" and HF_TOKEN:
60
- headers["Authorization"] = f"Bearer {HF_TOKEN}"
61
-
62
- async with httpx.AsyncClient(timeout=HTTP_TIMEOUT, headers=headers) as client:
63
  r = await client.get(url, params=params)
64
  r.raise_for_status()
65
  return r.json()
66
 
67
  async def refresh(self) -> Dict[str, Any]:
68
- if HF_MODE == "off":
69
- self.fail_reason = "HF_MODE=off"
70
- return {"ok": False, "error": "HF_MODE=off", "models": 0, "datasets": 0}
71
-
72
  try:
 
73
  for name in _SEED_MODELS:
74
  self.models.setdefault(name, {"id": name, "source": "seed", "pipeline_tag": "sentiment-analysis"})
75
 
 
76
  for category, dataset_list in CRYPTO_DATASETS.items():
77
  for name in dataset_list:
78
  self.datasets.setdefault(name, {"id": name, "source": "seed", "category": category, "tags": ["crypto", category]})
79
 
80
- if HF_MODE in ("public", "auth"):
81
- try:
82
- q_sent = {"pipeline_tag": "sentiment-analysis", "search": "crypto", "limit": 50}
83
- models = await self._hf_json(HF_API_MODELS, q_sent)
84
- for m in models or []:
85
- mid = m.get("modelId") or m.get("id") or m.get("name")
86
- if not mid: continue
87
- self.models[mid] = {
88
- "id": mid,
89
- "pipeline_tag": m.get("pipeline_tag"),
90
- "likes": m.get("likes"),
91
- "downloads": m.get("downloads"),
92
- "tags": m.get("tags") or [],
93
- "source": "hub"
94
- }
95
-
96
- q_crypto = {"search": "crypto", "limit": 100}
97
- datasets = await self._hf_json(HF_API_DATASETS, q_crypto)
98
- for d in datasets or []:
99
- did = d.get("id") or d.get("name")
100
- if not did: continue
101
- category = "other"
102
- tags_str = " ".join(d.get("tags") or []).lower()
103
- name_lower = did.lower()
104
- if "price" in tags_str or "ohlc" in tags_str or "price" in name_lower:
105
- category = "price"
106
- elif "news" in tags_str or "news" in name_lower:
107
- if "label" in tags_str or "sentiment" in tags_str:
108
- category = "news_labeled"
109
- else:
110
- category = "news_raw"
111
-
112
- self.datasets[did] = {
113
- "id": did,
114
- "likes": d.get("likes"),
115
- "downloads": d.get("downloads"),
116
- "tags": d.get("tags") or [],
117
- "category": category,
118
- "source": "hub"
119
- }
120
- except Exception as e:
121
- error_msg = str(e)[:200]
122
- if "401" in error_msg or "unauthorized" in error_msg.lower():
123
- self.fail_reason = "Authentication failed"
124
  else:
125
- self.fail_reason = error_msg
 
 
 
 
 
 
 
 
 
126
 
127
  self.last_refresh = time.time()
128
- if self.fail_reason is None:
129
- return {"ok": True, "models": len(self.models), "datasets": len(self.datasets)}
130
- return {"ok": False, "error": self.fail_reason, "models": len(self.models), "datasets": len(self.datasets)}
131
  except Exception as e:
132
- self.fail_reason = str(e)[:200]
133
  return {"ok": False, "error": self.fail_reason, "models": len(self.models), "datasets": len(self.datasets)}
134
 
135
  def list(self, kind: Literal["models","datasets"]="models", category: Optional[str]=None) -> List[Dict[str, Any]]:
 
8
  REFRESH_INTERVAL_SEC = int(os.getenv("HF_REGISTRY_REFRESH_SEC", "21600"))
9
  HTTP_TIMEOUT = float(os.getenv("HF_HTTP_TIMEOUT", "8.0"))
10
 
 
 
 
 
 
 
 
 
 
 
11
  # Curated Crypto Datasets
12
  CRYPTO_DATASETS = {
13
  "price": [
 
45
  self.fail_reason: Optional[str] = None
46
 
47
  async def _hf_json(self, url: str, params: Dict[str, Any]) -> Any:
48
+ async with httpx.AsyncClient(timeout=HTTP_TIMEOUT) as client:
 
 
 
 
49
  r = await client.get(url, params=params)
50
  r.raise_for_status()
51
  return r.json()
52
 
53
  async def refresh(self) -> Dict[str, Any]:
 
 
 
 
54
  try:
55
+ # Seed models
56
  for name in _SEED_MODELS:
57
  self.models.setdefault(name, {"id": name, "source": "seed", "pipeline_tag": "sentiment-analysis"})
58
 
59
+ # Seed datasets with category metadata
60
  for category, dataset_list in CRYPTO_DATASETS.items():
61
  for name in dataset_list:
62
  self.datasets.setdefault(name, {"id": name, "source": "seed", "category": category, "tags": ["crypto", category]})
63
 
64
+ # Fetch from HF Hub
65
+ q_sent = {"pipeline_tag": "sentiment-analysis", "search": "crypto", "limit": 50}
66
+ models = await self._hf_json(HF_API_MODELS, q_sent)
67
+ for m in models or []:
68
+ mid = m.get("modelId") or m.get("id") or m.get("name")
69
+ if not mid: continue
70
+ self.models[mid] = {
71
+ "id": mid,
72
+ "pipeline_tag": m.get("pipeline_tag"),
73
+ "likes": m.get("likes"),
74
+ "downloads": m.get("downloads"),
75
+ "tags": m.get("tags") or [],
76
+ "source": "hub"
77
+ }
78
+
79
+ q_crypto = {"search": "crypto", "limit": 100}
80
+ datasets = await self._hf_json(HF_API_DATASETS, q_crypto)
81
+ for d in datasets or []:
82
+ did = d.get("id") or d.get("name")
83
+ if not did: continue
84
+ # Infer category from tags or name
85
+ category = "other"
86
+ tags_str = " ".join(d.get("tags") or []).lower()
87
+ name_lower = did.lower()
88
+ if "price" in tags_str or "ohlc" in tags_str or "price" in name_lower:
89
+ category = "price"
90
+ elif "news" in tags_str or "news" in name_lower:
91
+ if "label" in tags_str or "sentiment" in tags_str:
92
+ category = "news_labeled"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  else:
94
+ category = "news_raw"
95
+
96
+ self.datasets[did] = {
97
+ "id": did,
98
+ "likes": d.get("likes"),
99
+ "downloads": d.get("downloads"),
100
+ "tags": d.get("tags") or [],
101
+ "category": category,
102
+ "source": "hub"
103
+ }
104
 
105
  self.last_refresh = time.time()
106
+ self.fail_reason = None
107
+ return {"ok": True, "models": len(self.models), "datasets": len(self.datasets)}
 
108
  except Exception as e:
109
+ self.fail_reason = str(e)
110
  return {"ok": False, "error": self.fail_reason, "models": len(self.models), "datasets": len(self.datasets)}
111
 
112
  def list(self, kind: Literal["models","datasets"]="models", category: Optional[str]=None) -> List[Dict[str, Any]]:
collectors/aggregator.py CHANGED
@@ -88,8 +88,6 @@ class MarketDataCollector:
88
  self._symbol_map = {symbol.lower(): coin_id for coin_id, symbol in COIN_SYMBOL_MAPPING.items()}
89
  self.headers = {"User-Agent": settings.user_agent or USER_AGENT}
90
  self.timeout = 15.0
91
- self._last_error_log: Dict[str, float] = {} # Track last error log time per provider
92
- self._error_log_throttle = 60.0 # Only log same error once per 60 seconds
93
 
94
  async def _request(self, provider_key: str, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
95
  provider = self.registry.providers.get(provider_key)
@@ -97,54 +95,8 @@ class MarketDataCollector:
97
  raise CollectorError(f"Provider {provider_key} not configured", provider=provider_key)
98
 
99
  url = provider["base_url"].rstrip("/") + path
100
-
101
- # Rate limit tracking per provider
102
- if not hasattr(self, '_rate_limit_timestamps'):
103
- self._rate_limit_timestamps: Dict[str, List[float]] = {}
104
- if provider_key not in self._rate_limit_timestamps:
105
- self._rate_limit_timestamps[provider_key] = []
106
-
107
- # Get rate limits from provider config
108
- rate_limit_rpm = provider.get("rate_limit", {}).get("requests_per_minute", 30)
109
- if rate_limit_rpm and len(self._rate_limit_timestamps[provider_key]) >= rate_limit_rpm:
110
- # Check if oldest request is older than 1 minute
111
- oldest_time = self._rate_limit_timestamps[provider_key][0]
112
- if time.time() - oldest_time < 60:
113
- wait_time = 60 - (time.time() - oldest_time) + 1
114
- if self._should_log_error(provider_key, "rate_limit_wait"):
115
- logger.warning(f"Rate limiting {provider_key}, waiting {wait_time:.1f}s")
116
- await asyncio.sleep(wait_time)
117
- # Clean old timestamps
118
- cutoff = time.time() - 60
119
- self._rate_limit_timestamps[provider_key] = [
120
- ts for ts in self._rate_limit_timestamps[provider_key] if ts > cutoff
121
- ]
122
-
123
  async with httpx.AsyncClient(timeout=self.timeout, headers=self.headers) as client:
124
  response = await client.get(url, params=params)
125
-
126
- # Record request timestamp
127
- self._rate_limit_timestamps[provider_key].append(time.time())
128
- # Keep only last minute of timestamps
129
- cutoff = time.time() - 60
130
- self._rate_limit_timestamps[provider_key] = [
131
- ts for ts in self._rate_limit_timestamps[provider_key] if ts > cutoff
132
- ]
133
-
134
- # Handle HTTP 429 (Rate Limit) with exponential backoff
135
- if response.status_code == 429:
136
- retry_after = int(response.headers.get("Retry-After", "60"))
137
- error_msg = f"{provider_key} rate limited (HTTP 429), retry after {retry_after}s"
138
-
139
- if self._should_log_error(provider_key, "HTTP 429"):
140
- logger.warning(error_msg)
141
-
142
- raise CollectorError(
143
- error_msg,
144
- provider=provider_key,
145
- status_code=429,
146
- )
147
-
148
  if response.status_code != 200:
149
  raise CollectorError(
150
  f"{provider_key} request failed with HTTP {response.status_code}",
@@ -153,31 +105,14 @@ class MarketDataCollector:
153
  )
154
  return response.json()
155
 
156
- def _should_log_error(self, provider: str, error_msg: str) -> bool:
157
- """Check if error should be logged (throttle repeated errors)."""
158
- error_key = f"{provider}:{error_msg}"
159
- now = time.time()
160
- last_log_time = self._last_error_log.get(error_key, 0)
161
-
162
- if now - last_log_time > self._error_log_throttle:
163
- self._last_error_log[error_key] = now
164
- # Clean up old entries (keep only last hour)
165
- cutoff = now - 3600
166
- self._last_error_log = {k: v for k, v in self._last_error_log.items() if v > cutoff}
167
- return True
168
- return False
169
-
170
  async def get_top_coins(self, limit: int = 10) -> List[Dict[str, Any]]:
171
  cache_key = f"top_coins:{limit}"
172
  cached = await self.cache.get(cache_key)
173
  if cached:
174
  return cached
175
 
176
- # Provider list with priority order (add more fallbacks from resource files)
177
- providers = ["coingecko", "coincap", "coinpaprika"]
178
  last_error: Optional[Exception] = None
179
- last_error_details: Optional[str] = None
180
-
181
  for provider in providers:
182
  try:
183
  if provider == "coingecko":
@@ -225,53 +160,11 @@ class MarketDataCollector:
225
  ]
226
  await self.cache.set(cache_key, coins)
227
  return coins
228
-
229
- if provider == "coinpaprika":
230
- data = await self._request("coinpaprika", "/tickers", {"quotes": "USD", "limit": limit})
231
- coins = [
232
- {
233
- "name": item.get("name"),
234
- "symbol": item.get("symbol", "").upper(),
235
- "price": float(item.get("quotes", {}).get("USD", {}).get("price", 0)),
236
- "change_24h": float(item.get("quotes", {}).get("USD", {}).get("percent_change_24h", 0)),
237
- "market_cap": float(item.get("quotes", {}).get("USD", {}).get("market_cap", 0)),
238
- "volume_24h": float(item.get("quotes", {}).get("USD", {}).get("volume_24h", 0)),
239
- "rank": int(item.get("rank", 0)),
240
- "last_updated": item.get("last_updated"),
241
- }
242
- for item in data[:limit] if item.get("quotes", {}).get("USD")
243
- ]
244
- await self.cache.set(cache_key, coins)
245
- return coins
246
  except Exception as exc: # pragma: no cover - network heavy
247
  last_error = exc
248
- error_msg = str(exc) if str(exc) else repr(exc)
249
- error_type = type(exc).__name__
250
-
251
- # Extract HTTP status code if available
252
- if hasattr(exc, 'status_code'):
253
- status_code = exc.status_code
254
- error_msg = f"HTTP {status_code}: {error_msg}" if error_msg else f"HTTP {status_code}"
255
- elif isinstance(exc, CollectorError) and hasattr(exc, 'status_code') and exc.status_code:
256
- status_code = exc.status_code
257
- error_msg = f"HTTP {status_code}: {error_msg}" if error_msg else f"HTTP {status_code}"
258
-
259
- # Ensure we always have a meaningful error message
260
- if not error_msg or error_msg.strip() == "":
261
- error_msg = f"{error_type} (no details available)"
262
-
263
- last_error_details = f"{error_type}: {error_msg}"
264
-
265
- # Throttle error logging to prevent spam
266
- error_key_for_logging = error_msg or error_type
267
- if self._should_log_error(provider, error_key_for_logging):
268
- logger.warning(
269
- "Provider %s failed: %s (error logged, will suppress similar errors for 60s)",
270
- provider,
271
- last_error_details
272
- )
273
 
274
- raise CollectorError(f"Unable to fetch top coins from any provider. Last error: {last_error_details or 'Unknown'}", provider=str(last_error) if last_error else None)
275
 
276
  async def _coin_id(self, symbol: str) -> str:
277
  symbol_lower = symbol.lower()
@@ -472,9 +365,7 @@ class ProviderStatusCollector:
472
  "latency_ms": latency,
473
  }
474
  except Exception as exc: # pragma: no cover - network heavy
475
- error_msg = str(exc)
476
- error_type = type(exc).__name__
477
- logger.warning("Provider %s health check failed: %s: %s", provider_id, error_type, error_msg)
478
  return {
479
  "provider_id": provider_id,
480
  "name": data.get("name", provider_id),
 
88
  self._symbol_map = {symbol.lower(): coin_id for coin_id, symbol in COIN_SYMBOL_MAPPING.items()}
89
  self.headers = {"User-Agent": settings.user_agent or USER_AGENT}
90
  self.timeout = 15.0
 
 
91
 
92
  async def _request(self, provider_key: str, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
93
  provider = self.registry.providers.get(provider_key)
 
95
  raise CollectorError(f"Provider {provider_key} not configured", provider=provider_key)
96
 
97
  url = provider["base_url"].rstrip("/") + path
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  async with httpx.AsyncClient(timeout=self.timeout, headers=self.headers) as client:
99
  response = await client.get(url, params=params)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  if response.status_code != 200:
101
  raise CollectorError(
102
  f"{provider_key} request failed with HTTP {response.status_code}",
 
105
  )
106
  return response.json()
107
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  async def get_top_coins(self, limit: int = 10) -> List[Dict[str, Any]]:
109
  cache_key = f"top_coins:{limit}"
110
  cached = await self.cache.get(cache_key)
111
  if cached:
112
  return cached
113
 
114
+ providers = ["coingecko", "coincap"]
 
115
  last_error: Optional[Exception] = None
 
 
116
  for provider in providers:
117
  try:
118
  if provider == "coingecko":
 
160
  ]
161
  await self.cache.set(cache_key, coins)
162
  return coins
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
  except Exception as exc: # pragma: no cover - network heavy
164
  last_error = exc
165
+ logger.warning("Provider %s failed: %s", provider, exc)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
 
167
+ raise CollectorError("Unable to fetch top coins", provider=str(last_error))
168
 
169
  async def _coin_id(self, symbol: str) -> str:
170
  symbol_lower = symbol.lower()
 
365
  "latency_ms": latency,
366
  }
367
  except Exception as exc: # pragma: no cover - network heavy
368
+ logger.warning("Provider %s health check failed: %s", provider_id, exc)
 
 
369
  return {
370
  "provider_id": provider_id,
371
  "name": data.get("name", provider_id),
config.py CHANGED
@@ -1,470 +1,24 @@
1
- #!/usr/bin/env python3
2
- """
3
- Configuration constants for Crypto Data Aggregator
4
- All configuration in one place - no hardcoded values
5
- """
6
-
7
- import os
8
- import json
9
- import base64
10
- import logging
11
- from functools import lru_cache
12
- from pathlib import Path
13
- from typing import Dict, Any, List, Optional
14
- from dataclasses import dataclass
15
-
16
- # Load .env file if python-dotenv is available
17
- try:
18
- from dotenv import load_dotenv
19
- load_dotenv()
20
- except ImportError:
21
- pass # python-dotenv not installed, skip loading .env
22
-
23
- # ==================== DIRECTORIES ====================
24
- BASE_DIR = Path(__file__).parent
25
- DATA_DIR = BASE_DIR / "data"
26
- LOG_DIR = BASE_DIR / "logs"
27
- DB_DIR = DATA_DIR / "database"
28
-
29
- # Create directories if they don't exist
30
- for directory in [DATA_DIR, LOG_DIR, DB_DIR]:
31
- directory.mkdir(parents=True, exist_ok=True)
32
-
33
- logger = logging.getLogger(__name__)
34
-
35
-
36
- # ==================== PROVIDER CONFIGURATION ====================
37
-
38
-
39
- @dataclass
40
- class ProviderConfig:
41
- """Configuration for an API provider"""
42
-
43
- name: str
44
- endpoint_url: str
45
- category: str = "market_data"
46
- requires_key: bool = False
47
- api_key: Optional[str] = None
48
- timeout_ms: int = 10000
49
- rate_limit_type: Optional[str] = None
50
- rate_limit_value: Optional[int] = None
51
- health_check_endpoint: Optional[str] = None
52
-
53
- def __post_init__(self):
54
- if self.health_check_endpoint is None:
55
- self.health_check_endpoint = self.endpoint_url
56
-
57
-
58
- @dataclass
59
- class Settings:
60
- """Runtime configuration loaded from environment variables."""
61
-
62
- hf_token: Optional[str] = None
63
- hf_token_encoded: Optional[str] = None
64
- cmc_api_key: Optional[str] = None
65
- etherscan_key: Optional[str] = None
66
- newsapi_key: Optional[str] = None
67
- log_level: str = "INFO"
68
- database_path: Path = DB_DIR / "crypto_aggregator.db"
69
- redis_url: Optional[str] = None
70
- cache_ttl: int = 300
71
- user_agent: str = "CryptoDashboard/1.0"
72
- providers_config_path: Path = BASE_DIR / "providers_config_extended.json"
73
-
74
-
75
- def _decode_token(value: Optional[str]) -> Optional[str]:
76
- """Decode a base64 encoded Hugging Face token."""
77
-
78
- if not value:
79
- return None
80
-
81
- try:
82
- decoded = base64.b64decode(value).decode("utf-8").strip()
83
- return decoded or None
84
- except Exception as exc: # pragma: no cover - defensive logging
85
- logger.warning("Failed to decode HF token: %s", exc)
86
- return None
87
-
88
-
89
- @lru_cache(maxsize=1)
90
- def get_settings() -> Settings:
91
- """Return cached runtime settings."""
92
-
93
- raw_token = os.environ.get("HF_TOKEN")
94
- encoded_token = os.environ.get("HF_TOKEN_ENCODED")
95
- decoded_token = raw_token or _decode_token(encoded_token)
96
- # Default token if none provided
97
- if not decoded_token:
98
- decoded_token = "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"
99
-
100
- database_path = Path(os.environ.get("DATABASE_PATH", str(DB_DIR / "crypto_aggregator.db")))
101
-
102
- settings = Settings(
103
- hf_token=decoded_token,
104
- hf_token_encoded=encoded_token,
105
- cmc_api_key=os.environ.get("CMC_API_KEY"),
106
- etherscan_key=os.environ.get("ETHERSCAN_KEY"),
107
- newsapi_key=os.environ.get("NEWSAPI_KEY"),
108
- log_level=os.environ.get("LOG_LEVEL", "INFO").upper(),
109
- database_path=database_path,
110
- redis_url=os.environ.get("REDIS_URL"),
111
- cache_ttl=int(os.environ.get("CACHE_TTL", "300")),
112
- user_agent=os.environ.get("USER_AGENT", "CryptoDashboard/1.0"),
113
- providers_config_path=Path(
114
- os.environ.get("PROVIDERS_CONFIG_PATH", str(BASE_DIR / "providers_config_extended.json"))
115
- ),
116
- )
117
-
118
- return settings
119
-
120
-
121
- class ConfigManager:
122
- """Configuration manager for API providers"""
123
-
124
- def __init__(self):
125
- self.providers: Dict[str, ProviderConfig] = {}
126
- self._load_default_providers()
127
- self._load_env_keys()
128
-
129
- def _load_default_providers(self):
130
- """Load default provider configurations"""
131
- # CoinGecko (Free, no key)
132
- self.providers["CoinGecko"] = ProviderConfig(
133
- name="CoinGecko",
134
- endpoint_url="https://api.coingecko.com/api/v3",
135
- category="market_data",
136
- requires_key=False,
137
- timeout_ms=10000
138
- )
139
-
140
- # CoinMarketCap (Requires API key)
141
- self.providers["CoinMarketCap"] = ProviderConfig(
142
- name="CoinMarketCap",
143
- endpoint_url="https://pro-api.coinmarketcap.com/v1",
144
- category="market_data",
145
- requires_key=True,
146
- timeout_ms=10000
147
- )
148
-
149
- # Binance (Free, no key)
150
- self.providers["Binance"] = ProviderConfig(
151
- name="Binance",
152
- endpoint_url="https://api.binance.com/api/v3",
153
- category="market_data",
154
- requires_key=False,
155
- timeout_ms=10000
156
- )
157
-
158
- # Etherscan (Requires API key)
159
- self.providers["Etherscan"] = ProviderConfig(
160
- name="Etherscan",
161
- endpoint_url="https://api.etherscan.io/api",
162
- category="blockchain_explorers",
163
- requires_key=True,
164
- timeout_ms=10000
165
- )
166
-
167
- # BscScan (Requires API key)
168
- self.providers["BscScan"] = ProviderConfig(
169
- name="BscScan",
170
- endpoint_url="https://api.bscscan.com/api",
171
- category="blockchain_explorers",
172
- requires_key=True,
173
- timeout_ms=10000
174
- )
175
-
176
- # TronScan (Requires API key)
177
- self.providers["TronScan"] = ProviderConfig(
178
- name="TronScan",
179
- endpoint_url="https://apilist.tronscan.org/api",
180
- category="blockchain_explorers",
181
- requires_key=True,
182
- timeout_ms=10000
183
- )
184
-
185
- # CryptoPanic (Requires API key)
186
- self.providers["CryptoPanic"] = ProviderConfig(
187
- name="CryptoPanic",
188
- endpoint_url="https://cryptopanic.com/api/v1",
189
- category="news",
190
- requires_key=True,
191
- timeout_ms=10000
192
- )
193
-
194
- # NewsAPI (Requires API key)
195
- self.providers["NewsAPI"] = ProviderConfig(
196
- name="NewsAPI",
197
- endpoint_url="https://newsapi.org/v2",
198
- category="news",
199
- requires_key=True,
200
- timeout_ms=10000
201
- )
202
-
203
- # Alternative.me Fear & Greed Index (Free, no key)
204
- self.providers["Alternative.me"] = ProviderConfig(
205
- name="Alternative.me",
206
- endpoint_url="https://api.alternative.me",
207
- category="sentiment",
208
- requires_key=False,
209
- timeout_ms=10000
210
- )
211
-
212
- def _load_env_keys(self):
213
- """Load API keys from environment variables"""
214
- key_mapping = {
215
- "CoinMarketCap": "CMC_API_KEY",
216
- "Etherscan": "ETHERSCAN_KEY",
217
- "BscScan": "BSCSCAN_KEY",
218
- "TronScan": "TRONSCAN_KEY",
219
- "CryptoPanic": "CRYPTOPANIC_KEY",
220
- "NewsAPI": "NEWSAPI_KEY",
221
- }
222
-
223
- for provider_name, env_var in key_mapping.items():
224
- if provider_name in self.providers:
225
- api_key = os.environ.get(env_var)
226
- if api_key:
227
- self.providers[provider_name].api_key = api_key
228
-
229
- def get_provider(self, provider_name: str) -> Optional[ProviderConfig]:
230
- """Get provider configuration by name"""
231
- return self.providers.get(provider_name)
232
-
233
- def get_all_providers(self) -> List[ProviderConfig]:
234
- """Get all provider configurations"""
235
- return list(self.providers.values())
236
-
237
- def get_providers_by_category(self, category: str) -> List[ProviderConfig]:
238
- """Get providers filtered by category"""
239
- return [p for p in self.providers.values() if p.category == category]
240
-
241
- def get_categories(self) -> List[str]:
242
- """Get all unique categories"""
243
- return list(set(p.category for p in self.providers.values()))
244
-
245
- def add_provider(self, provider: ProviderConfig):
246
- """Add a new provider configuration"""
247
- self.providers[provider.name] = provider
248
-
249
- def stats(self) -> Dict[str, Any]:
250
- """Get configuration statistics"""
251
- providers_list = list(self.providers.values())
252
- return {
253
- 'total_resources': len(providers_list),
254
- 'total_categories': len(self.get_categories()),
255
- 'free_resources': sum(1 for p in providers_list if not p.requires_key),
256
- 'tier1_count': 0, # Placeholder for tier support
257
- 'tier2_count': 0,
258
- 'tier3_count': len(providers_list),
259
- 'api_keys_count': sum(1 for p in providers_list if p.api_key),
260
- 'cors_proxies_count': 0,
261
- 'categories': self.get_categories()
262
- }
263
-
264
- def get_by_tier(self, tier: int) -> List[Dict[str, Any]]:
265
- """Get resources by tier (placeholder for compatibility)"""
266
- # Return all providers for now
267
- return [{'name': p.name} for p in self.providers.values()]
268
-
269
- def get_all_resources(self) -> List[Dict[str, Any]]:
270
- """Get all resources in dictionary format (for compatibility)"""
271
- return [
272
- {
273
- 'name': p.name,
274
- 'endpoint': p.endpoint_url,
275
- 'url': p.endpoint_url,
276
- 'category': p.category,
277
- 'requires_key': p.requires_key,
278
- 'api_key': p.api_key,
279
- 'timeout': p.timeout_ms,
280
- }
281
- for p in self.providers.values()
282
- ]
283
-
284
-
285
- # Create global config instance
286
- config = ConfigManager()
287
-
288
- # Runtime settings loaded from environment
289
- settings = get_settings()
290
-
291
- # ==================== DATABASE ====================
292
- DATABASE_PATH = Path(settings.database_path)
293
- DATABASE_BACKUP_DIR = DATA_DIR / "backups"
294
- DATABASE_BACKUP_DIR.mkdir(parents=True, exist_ok=True)
295
-
296
- # ==================== API ENDPOINTS (NO KEYS REQUIRED) ====================
297
-
298
- # CoinGecko API (Free, no key)
299
- COINGECKO_BASE_URL = "https://api.coingecko.com/api/v3"
300
- COINGECKO_ENDPOINTS = {
301
- "ping": "/ping",
302
- "price": "/simple/price",
303
- "coins_list": "/coins/list",
304
- "coins_markets": "/coins/markets",
305
- "coin_data": "/coins/{id}",
306
- "trending": "/search/trending",
307
- "global": "/global",
308
- }
309
-
310
- # CoinCap API (Free, no key)
311
- COINCAP_BASE_URL = "https://api.coincap.io/v2"
312
- COINCAP_ENDPOINTS = {
313
- "assets": "/assets",
314
- "asset_detail": "/assets/{id}",
315
- "asset_history": "/assets/{id}/history",
316
- "markets": "/markets",
317
- "rates": "/rates",
318
- }
319
-
320
- # Binance Public API (Free, no key)
321
- BINANCE_BASE_URL = "https://api.binance.com/api/v3"
322
- BINANCE_ENDPOINTS = {
323
- "ping": "/ping",
324
- "ticker_24h": "/ticker/24hr",
325
- "ticker_price": "/ticker/price",
326
- "klines": "/klines",
327
- "trades": "/trades",
328
- }
329
-
330
- # Alternative.me Fear & Greed Index (Free, no key)
331
- ALTERNATIVE_ME_URL = "https://api.alternative.me/fng/"
332
-
333
- # ==================== RSS FEEDS ====================
334
- RSS_FEEDS = {
335
- "coindesk": "https://www.coindesk.com/arc/outboundfeeds/rss/",
336
- "cointelegraph": "https://cointelegraph.com/rss",
337
- "bitcoin_magazine": "https://bitcoinmagazine.com/.rss/full/",
338
- "decrypt": "https://decrypt.co/feed",
339
- "bitcoinist": "https://bitcoinist.com/feed/",
340
- }
341
-
342
- # ==================== REDDIT ENDPOINTS (NO AUTH) ====================
343
- REDDIT_ENDPOINTS = {
344
- "cryptocurrency": "https://www.reddit.com/r/cryptocurrency/.json",
345
- "bitcoin": "https://www.reddit.com/r/bitcoin/.json",
346
- "ethtrader": "https://www.reddit.com/r/ethtrader/.json",
347
- "cryptomarkets": "https://www.reddit.com/r/CryptoMarkets/.json",
348
- }
349
-
350
- # ==================== HUGGING FACE MODELS ====================
351
- HUGGINGFACE_MODELS = {
352
- "sentiment_twitter": "cardiffnlp/twitter-roberta-base-sentiment-latest",
353
- "sentiment_financial": "ProsusAI/finbert",
354
- "summarization": "facebook/bart-large-cnn",
355
- "crypto_sentiment": "ElKulako/CryptoBERT", # Requires authentication
356
- }
357
-
358
- # Hugging Face Authentication
359
- HF_TOKEN = settings.hf_token or ""
360
- HF_USE_AUTH_TOKEN = bool(HF_TOKEN)
361
-
362
- # ==================== DATA COLLECTION SETTINGS ====================
363
- COLLECTION_INTERVALS = {
364
- "price_data": 300, # 5 minutes in seconds
365
- "news_data": 1800, # 30 minutes in seconds
366
- "sentiment_data": 1800, # 30 minutes in seconds
367
- }
368
-
369
- # Number of top cryptocurrencies to track
370
- TOP_COINS_LIMIT = 100
371
-
372
- # Request timeout in seconds
373
- REQUEST_TIMEOUT = 10
374
-
375
- # Max retries for failed requests
376
- MAX_RETRIES = 3
377
-
378
- # ==================== CACHE SETTINGS ====================
379
- CACHE_TTL = settings.cache_ttl or 300 # 5 minutes in seconds
380
- CACHE_MAX_SIZE = 1000 # Maximum number of cached items
381
-
382
- # ==================== LOGGING SETTINGS ====================
383
- LOG_FILE = LOG_DIR / "crypto_aggregator.log"
384
- LOG_LEVEL = settings.log_level
385
- LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
386
- LOG_MAX_BYTES = 10 * 1024 * 1024 # 10 MB
387
- LOG_BACKUP_COUNT = 5
388
-
389
- # ==================== GRADIO SETTINGS ====================
390
- GRADIO_SHARE = False
391
- GRADIO_SERVER_NAME = "0.0.0.0"
392
- GRADIO_SERVER_PORT = 7860
393
- GRADIO_THEME = "default"
394
- AUTO_REFRESH_INTERVAL = 30 # seconds
395
-
396
- # ==================== DATA VALIDATION ====================
397
- MIN_PRICE = 0.0
398
- MAX_PRICE = 1000000000.0 # 1 billion
399
- MIN_VOLUME = 0.0
400
- MIN_MARKET_CAP = 0.0
401
-
402
- # ==================== CHART SETTINGS ====================
403
- CHART_TIMEFRAMES = {
404
- "1d": {"days": 1, "interval": "1h"},
405
- "7d": {"days": 7, "interval": "4h"},
406
- "30d": {"days": 30, "interval": "1d"},
407
- "90d": {"days": 90, "interval": "1d"},
408
- "1y": {"days": 365, "interval": "1w"},
409
- }
410
-
411
- # Technical indicators
412
- MA_PERIODS = [7, 30] # Moving Average periods
413
- RSI_PERIOD = 14 # RSI period
414
-
415
- # ==================== SENTIMENT THRESHOLDS ====================
416
- SENTIMENT_LABELS = {
417
- "very_negative": (-1.0, -0.6),
418
- "negative": (-0.6, -0.2),
419
- "neutral": (-0.2, 0.2),
420
- "positive": (0.2, 0.6),
421
- "very_positive": (0.6, 1.0),
422
- }
423
-
424
- # ==================== AI ANALYSIS SETTINGS ====================
425
- AI_CONFIDENCE_THRESHOLD = 0.6
426
- PREDICTION_HORIZON_HOURS = 72
427
-
428
- # ==================== USER AGENT ====================
429
- USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
430
-
431
- # ==================== RATE LIMITING ====================
432
- RATE_LIMIT_CALLS = 50
433
- RATE_LIMIT_PERIOD = 60 # seconds
434
-
435
- # ==================== COIN SYMBOLS ====================
436
- # Top cryptocurrencies to focus on
437
- FOCUS_COINS = [
438
- "bitcoin", "ethereum", "binancecoin", "ripple", "cardano",
439
- "solana", "polkadot", "dogecoin", "avalanche-2", "polygon",
440
- "chainlink", "uniswap", "litecoin", "cosmos", "algorand"
441
- ]
442
-
443
- COIN_SYMBOL_MAPPING = {
444
- "bitcoin": "BTC",
445
- "ethereum": "ETH",
446
- "binancecoin": "BNB",
447
- "ripple": "XRP",
448
- "cardano": "ADA",
449
- "solana": "SOL",
450
- "polkadot": "DOT",
451
- "dogecoin": "DOGE",
452
- "avalanche-2": "AVAX",
453
- "polygon": "MATIC",
454
- }
455
-
456
- # ==================== ERROR MESSAGES ====================
457
- ERROR_MESSAGES = {
458
- "api_unavailable": "API service is currently unavailable. Using cached data.",
459
- "no_data": "No data available at the moment.",
460
- "database_error": "Database operation failed.",
461
- "network_error": "Network connection error.",
462
- "invalid_input": "Invalid input provided.",
463
- }
464
-
465
- # ==================== SUCCESS MESSAGES ====================
466
- SUCCESS_MESSAGES = {
467
- "data_collected": "Data successfully collected and saved.",
468
- "cache_cleared": "Cache cleared successfully.",
469
- "database_initialized": "Database initialized successfully.",
470
- }
 
1
+ #!/usr/bin/env python3
2
+ """Configuration module for Hugging Face models."""
3
+
4
+ import os
5
+ from typing import Optional, Dict, Any
6
+
7
+ HUGGINGFACE_MODELS: Dict[str, str] = {
8
+ "sentiment_twitter": "cardiffnlp/twitter-roberta-base-sentiment-latest",
9
+ "sentiment_financial": "ProsusAI/finbert",
10
+ "summarization": "facebook/bart-large-cnn",
11
+ "crypto_sentiment": "ElKulako/cryptobert",
12
+ }
13
+
14
+ class Settings:
15
+ """Application settings."""
16
+ def __init__(self):
17
+ self.hf_token: Optional[str] = os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACE_TOKEN")
18
+
19
+ _settings = Settings()
20
+
21
+ def get_settings() -> Settings:
22
+ """Get application settings instance."""
23
+ return _settings
24
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dashboard.html CHANGED
@@ -1,113 +1,638 @@
1
- <!DOCTYPE html>
2
  <html lang="en">
3
  <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1" />
6
- <title>Crypto Intelligence Dashboard</title>
7
- <link rel="preconnect" href="https://fonts.googleapis.com" />
8
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
10
- <link rel="stylesheet" href="/static/css/unified-ui.css" />
11
- <link rel="stylesheet" href="/static/css/components.css" />
12
- <script defer src="/static/js/ui-feedback.js"></script>
13
- <script defer src="/static/js/dashboard-app.js"></script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  </head>
15
- <body class="page page-dashboard">
16
- <header class="top-nav">
17
- <div class="branding">
18
- <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"/></svg>
19
- <div>
20
- <strong>Crypto Intelligence Hub</strong>
21
- <small style="color:var(--ui-text-muted);letter-spacing:0.2em;">Real-time data + HF models</small>
 
 
 
 
22
  </div>
23
- </div>
24
- <nav class="nav-links">
25
- <a class="active" href="/dashboard">Dashboard</a>
26
- <a href="/admin">Admin</a>
27
- <a href="/hf_console">HF Console</a>
28
- <a href="/docs" target="_blank" rel="noreferrer">API Docs</a>
29
- </nav>
30
- </header>
31
-
32
- <main class="page-content">
33
- <section class="card" id="intro-card">
34
- <div class="section-heading">
35
- <h2>Unified Market Pulse</h2>
36
- <span class="badge info" id="intro-source">Loading...</span>
37
- </div>
38
- <p style="color:var(--text-muted);max-width:780px;line-height:1.6;">
39
- Live collectors + local fallback registry guarantee resilient insights. All numbers below already honor the FastAPI routes
40
- (<code>/api/crypto/prices/top</code>, <code>/api/crypto/market-overview</code>, <code>/health</code>) so you can monitor status even when providers degrade.
41
- </p>
42
- </section>
43
-
44
- <section class="card-grid" id="market-metrics">
45
- <article class="card"><h3>Total Market Cap</h3><div class="metric-value" id="metric-market-cap">-</div><div class="metric-subtext" id="metric-cap-source"></div></article>
46
- <article class="card"><h3>24h Volume</h3><div class="metric-value" id="metric-volume">-</div><div class="metric-subtext" id="metric-volume-source"></div></article>
47
- <article class="card"><h3>BTC Dominance</h3><div class="metric-value" id="metric-btc-dom">-</div><div class="metric-subtext">Based on /api/crypto/market-overview</div></article>
48
- <article class="card"><h3>System Health</h3><div class="metric-value" id="metric-health">-</div><div class="metric-subtext" id="metric-health-details"></div></article>
49
- </section>
50
-
51
- <section class="card table-card">
52
- <div class="section-heading">
53
- <h2>Top Assets</h2>
54
- <span class="badge info" id="top-prices-source">Loading...</span>
55
- </div>
56
- <div class="table-wrapper">
57
- <table>
58
- <thead>
59
- <tr><th>Symbol</th><th>Price</th><th>24h %</th><th>Volume</th></tr>
60
- </thead>
61
- <tbody id="top-prices-table">
62
- <tr><td colspan="4">Loading...</td></tr>
63
- </tbody>
64
- </table>
65
- </div>
66
- </section>
67
-
68
- <section class="split-grid">
69
- <article class="card" id="overview-card">
70
- <div class="section-heading">
71
- <h2>Market Overview</h2>
72
- <span class="badge info" id="market-overview-source">Loading...</span>
73
  </div>
74
- <ul class="list" id="market-overview-list"></ul>
75
- </article>
76
- <article class="card" id="system-card">
77
- <div class="section-heading">
78
- <h2>System & Rate Limits</h2>
79
- <span class="badge info" id="system-status-source">/health</span>
80
  </div>
81
- <div id="system-health-status" class="metric-subtext"></div>
82
- <ul class="list" id="system-status-list"></ul>
83
- <div class="section-heading" style="margin-top:24px;">
84
- <h2>Configuration</h2>
85
  </div>
86
- <ul class="list" id="system-config-list"></ul>
87
- <div class="section-heading" style="margin-top:24px;">
88
- <h2>Rate Limits</h2>
 
89
  </div>
90
- <ul class="list" id="rate-limits-list"></ul>
91
- </article>
92
- </section>
93
 
94
- <section class="split-grid">
95
- <article class="card" id="hf-widget">
96
- <div class="section-heading">
97
- <h2>HuggingFace Snapshot</h2>
98
- <span class="badge info" id="hf-health-status">Loading...</span>
99
- </div>
100
- <div id="hf-widget-summary" class="metric-subtext"></div>
101
- <ul class="list" id="hf-registry-list"></ul>
102
- </article>
103
- <article class="card">
104
- <div class="section-heading">
105
- <h2>Live Stream (/ws)</h2>
106
- <span class="badge info" id="ws-status">Connecting...</span>
 
 
 
 
 
 
 
 
107
  </div>
108
- <div class="ws-stream" id="ws-stream"></div>
109
- </article>
110
- </section>
111
- </main>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  </body>
113
- </html>
 
1
+ <!DOCTYPE html>
2
  <html lang="en">
3
  <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Crypto API Monitor - Real Data Dashboard</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+
10
+ @keyframes gradientShift {
11
+ 0% { background-position: 0% 50%; }
12
+ 50% { background-position: 100% 50%; }
13
+ 100% { background-position: 0% 50%; }
14
+ }
15
+
16
+ @keyframes fadeInUp {
17
+ from {
18
+ opacity: 0;
19
+ transform: translateY(30px);
20
+ }
21
+ to {
22
+ opacity: 1;
23
+ transform: translateY(0);
24
+ }
25
+ }
26
+
27
+ @keyframes pulse {
28
+ 0%, 100% { transform: scale(1); }
29
+ 50% { transform: scale(1.05); }
30
+ }
31
+
32
+ @keyframes shimmer {
33
+ 0% { background-position: -1000px 0; }
34
+ 100% { background-position: 1000px 0; }
35
+ }
36
+
37
+ body {
38
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
39
+ background: linear-gradient(-45deg, #667eea, #764ba2, #f093fb, #4facfe);
40
+ background-size: 400% 400%;
41
+ animation: gradientShift 15s ease infinite;
42
+ padding: 20px;
43
+ color: #1a1a1a;
44
+ min-height: 100vh;
45
+ }
46
+
47
+ .container {
48
+ max-width: 1400px;
49
+ margin: 0 auto;
50
+ background: rgba(255, 255, 255, 0.95);
51
+ backdrop-filter: blur(10px);
52
+ border-radius: 24px;
53
+ padding: 40px;
54
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
55
+ animation: fadeInUp 0.6s ease;
56
+ }
57
+
58
+ h1 {
59
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
60
+ -webkit-background-clip: text;
61
+ -webkit-text-fill-color: transparent;
62
+ background-clip: text;
63
+ margin-bottom: 10px;
64
+ font-size: 42px;
65
+ font-weight: 900;
66
+ letter-spacing: -1px;
67
+ animation: shimmer 3s infinite linear;
68
+ background-size: 1000px 100%;
69
+ }
70
+
71
+ .subtitle {
72
+ color: #6c757d;
73
+ font-size: 16px;
74
+ margin-bottom: 20px;
75
+ }
76
+
77
+ .stats-grid {
78
+ display: grid;
79
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
80
+ gap: 24px;
81
+ margin: 30px 0;
82
+ }
83
+
84
+ .stat-card {
85
+ background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
86
+ padding: 28px;
87
+ border-radius: 20px;
88
+ border: 3px solid transparent;
89
+ background-clip: padding-box;
90
+ position: relative;
91
+ overflow: hidden;
92
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
93
+ box-shadow: 0 4px 20px rgba(0,0,0,0.08);
94
+ }
95
+
96
+ .stat-card::before {
97
+ content: '';
98
+ position: absolute;
99
+ top: 0;
100
+ left: 0;
101
+ right: 0;
102
+ bottom: 0;
103
+ border-radius: 20px;
104
+ padding: 3px;
105
+ background: linear-gradient(135deg, #667eea, #764ba2, #f093fb);
106
+ -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
107
+ -webkit-mask-composite: xor;
108
+ mask-composite: exclude;
109
+ opacity: 0;
110
+ transition: opacity 0.3s;
111
+ }
112
+
113
+ .stat-card:hover {
114
+ transform: translateY(-8px) scale(1.02);
115
+ box-shadow: 0 12px 40px rgba(102, 126, 234, 0.3);
116
+ }
117
+
118
+ .stat-card:hover::before {
119
+ opacity: 1;
120
+ }
121
+
122
+ .stat-icon {
123
+ font-size: 32px;
124
+ margin-bottom: 12px;
125
+ display: inline-block;
126
+ animation: pulse 2s infinite;
127
+ }
128
+
129
+ .stat-value {
130
+ font-size: 48px;
131
+ font-weight: 900;
132
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
133
+ -webkit-background-clip: text;
134
+ -webkit-text-fill-color: transparent;
135
+ background-clip: text;
136
+ margin: 12px 0;
137
+ line-height: 1;
138
+ }
139
+
140
+ .stat-value.green {
141
+ background: linear-gradient(135deg, #10b981 0%, #059669 100%);
142
+ -webkit-background-clip: text;
143
+ -webkit-text-fill-color: transparent;
144
+ }
145
+
146
+ .stat-value.red {
147
+ background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
148
+ -webkit-background-clip: text;
149
+ -webkit-text-fill-color: transparent;
150
+ }
151
+
152
+ .stat-value.orange {
153
+ background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
154
+ -webkit-background-clip: text;
155
+ -webkit-text-fill-color: transparent;
156
+ }
157
+
158
+ .stat-label {
159
+ font-size: 13px;
160
+ color: #6c757d;
161
+ text-transform: uppercase;
162
+ font-weight: 700;
163
+ letter-spacing: 1px;
164
+ }
165
+
166
+ .section-header {
167
+ display: flex;
168
+ align-items: center;
169
+ gap: 12px;
170
+ margin: 40px 0 20px 0;
171
+ padding-bottom: 16px;
172
+ border-bottom: 3px solid;
173
+ border-image: linear-gradient(90deg, #667eea, #764ba2, transparent) 1;
174
+ }
175
+
176
+ .section-header h2 {
177
+ font-size: 28px;
178
+ font-weight: 800;
179
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
180
+ -webkit-background-clip: text;
181
+ -webkit-text-fill-color: transparent;
182
+ }
183
+
184
+ .providers-table {
185
+ width: 100%;
186
+ border-collapse: separate;
187
+ border-spacing: 0;
188
+ margin: 20px 0;
189
+ border-radius: 16px;
190
+ overflow: hidden;
191
+ box-shadow: 0 4px 20px rgba(0,0,0,0.08);
192
+ }
193
+
194
+ .providers-table th {
195
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
196
+ color: white;
197
+ padding: 18px;
198
+ text-align: left;
199
+ font-weight: 700;
200
+ text-transform: uppercase;
201
+ font-size: 12px;
202
+ letter-spacing: 1px;
203
+ }
204
+
205
+ .providers-table td {
206
+ padding: 18px;
207
+ border-bottom: 1px solid #e9ecef;
208
+ background: white;
209
+ transition: all 0.2s;
210
+ }
211
+
212
+ .providers-table tr:hover td {
213
+ background: linear-gradient(90deg, #f8f9fa 0%, #ffffff 100%);
214
+ transform: scale(1.01);
215
+ }
216
+
217
+ .providers-table tr:last-child td {
218
+ border-bottom: none;
219
+ }
220
+
221
+ .status-badge {
222
+ display: inline-flex;
223
+ align-items: center;
224
+ gap: 6px;
225
+ padding: 6px 14px;
226
+ border-radius: 20px;
227
+ font-size: 12px;
228
+ font-weight: 700;
229
+ text-transform: uppercase;
230
+ letter-spacing: 0.5px;
231
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
232
+ }
233
+
234
+ .status-badge::before {
235
+ content: '';
236
+ width: 8px;
237
+ height: 8px;
238
+ border-radius: 50%;
239
+ animation: pulse 2s infinite;
240
+ }
241
+
242
+ .status-online {
243
+ background: linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%);
244
+ color: #065f46;
245
+ border: 2px solid #10b981;
246
+ }
247
+
248
+ .status-online::before {
249
+ background: #10b981;
250
+ box-shadow: 0 0 10px #10b981;
251
+ }
252
+
253
+ .status-offline {
254
+ background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%);
255
+ color: #991b1b;
256
+ border: 2px solid #ef4444;
257
+ }
258
+
259
+ .status-offline::before {
260
+ background: #ef4444;
261
+ box-shadow: 0 0 10px #ef4444;
262
+ }
263
+
264
+ .status-degraded {
265
+ background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
266
+ color: #92400e;
267
+ border: 2px solid #f59e0b;
268
+ }
269
+
270
+ .status-degraded::before {
271
+ background: #f59e0b;
272
+ box-shadow: 0 0 10px #f59e0b;
273
+ }
274
+
275
+ .refresh-btn {
276
+ padding: 14px 28px;
277
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
278
+ color: white;
279
+ border: none;
280
+ border-radius: 12px;
281
+ font-weight: 700;
282
+ cursor: pointer;
283
+ margin: 10px 5px;
284
+ font-size: 14px;
285
+ text-transform: uppercase;
286
+ letter-spacing: 0.5px;
287
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
288
+ box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
289
+ position: relative;
290
+ overflow: hidden;
291
+ }
292
+
293
+ .refresh-btn::before {
294
+ content: '';
295
+ position: absolute;
296
+ top: 50%;
297
+ left: 50%;
298
+ width: 0;
299
+ height: 0;
300
+ border-radius: 50%;
301
+ background: rgba(255,255,255,0.3);
302
+ transform: translate(-50%, -50%);
303
+ transition: width 0.6s, height 0.6s;
304
+ }
305
+
306
+ .refresh-btn:hover::before {
307
+ width: 300px;
308
+ height: 300px;
309
+ }
310
+
311
+ .refresh-btn:hover {
312
+ transform: translateY(-3px);
313
+ box-shadow: 0 8px 25px rgba(102, 126, 234, 0.5);
314
+ }
315
+
316
+ .refresh-btn:active {
317
+ transform: translateY(-1px);
318
+ }
319
+
320
+ .last-update {
321
+ display: inline-flex;
322
+ align-items: center;
323
+ gap: 8px;
324
+ color: #6c757d;
325
+ font-size: 14px;
326
+ margin: 10px 0;
327
+ padding: 8px 16px;
328
+ background: #f8f9fa;
329
+ border-radius: 20px;
330
+ font-weight: 600;
331
+ }
332
+
333
+ .hf-section {
334
+ margin-top: 40px;
335
+ padding: 32px;
336
+ background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
337
+ border-radius: 20px;
338
+ border: 3px solid #dee2e6;
339
+ box-shadow: 0 4px 20px rgba(0,0,0,0.08);
340
+ }
341
+
342
+ textarea {
343
+ width: 100%;
344
+ padding: 16px;
345
+ border: 3px solid #dee2e6;
346
+ border-radius: 12px;
347
+ font-family: 'Consolas', 'Monaco', monospace;
348
+ margin: 16px 0;
349
+ font-size: 14px;
350
+ transition: all 0.3s;
351
+ background: white;
352
+ }
353
+
354
+ textarea:focus {
355
+ outline: none;
356
+ border-color: #667eea;
357
+ box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);
358
+ }
359
+
360
+ .sentiment-result {
361
+ font-size: 56px;
362
+ font-weight: 900;
363
+ padding: 32px;
364
+ background: white;
365
+ border-radius: 16px;
366
+ text-align: center;
367
+ margin: 16px 0;
368
+ box-shadow: 0 4px 20px rgba(0,0,0,0.08);
369
+ border: 3px solid #dee2e6;
370
+ min-height: 120px;
371
+ display: flex;
372
+ align-items: center;
373
+ justify-content: center;
374
+ }
375
+
376
+ pre {
377
+ background: #1e293b !important;
378
+ color: #e2e8f0 !important;
379
+ padding: 20px !important;
380
+ border-radius: 12px !important;
381
+ overflow-x: auto !important;
382
+ font-size: 13px !important;
383
+ line-height: 1.6 !important;
384
+ box-shadow: inset 0 2px 8px rgba(0,0,0,0.3) !important;
385
+ }
386
+
387
+ .loading {
388
+ display: inline-block;
389
+ width: 20px;
390
+ height: 20px;
391
+ border: 3px solid #f3f4f6;
392
+ border-top-color: #667eea;
393
+ border-radius: 50%;
394
+ animation: spin 0.8s linear infinite;
395
+ }
396
+
397
+ @keyframes spin {
398
+ to { transform: rotate(360deg); }
399
+ }
400
+
401
+ .response-time {
402
+ font-weight: 700;
403
+ padding: 4px 10px;
404
+ border-radius: 8px;
405
+ font-size: 13px;
406
+ }
407
+
408
+ .response-fast {
409
+ background: #d1fae5;
410
+ color: #065f46;
411
+ }
412
+
413
+ .response-medium {
414
+ background: #fef3c7;
415
+ color: #92400e;
416
+ }
417
+
418
+ .response-slow {
419
+ background: #fee2e2;
420
+ color: #991b1b;
421
+ }
422
+ </style>
423
  </head>
424
+ <body>
425
+ <div class="container">
426
+ <h1>🚀 Crypto API Monitor</h1>
427
+ <p class="subtitle">Real-time monitoring of cryptocurrency APIs with live data</p>
428
+ <p class="last-update">⏱️ Last Update: <span id="lastUpdate">Loading...</span></p>
429
+
430
+ <div style="margin: 20px 0;">
431
+ <button class="refresh-btn" onclick="loadData()">🔄 Refresh Data</button>
432
+ <button class="refresh-btn" onclick="window.location.href='/hf_console.html'">🤗 HF Console</button>
433
+ <button class="refresh-btn" onclick="window.location.href='/admin.html'">⚙️ Admin Panel</button>
434
+ <button class="refresh-btn" onclick="window.location.href='/index.html'">📊 Full Dashboard</button>
435
  </div>
436
+
437
+ <div class="stats-grid">
438
+ <div class="stat-card">
439
+ <div class="stat-icon">📡</div>
440
+ <div class="stat-label">Total APIs</div>
441
+ <div class="stat-value" id="totalAPIs">0</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
442
  </div>
443
+ <div class="stat-card">
444
+ <div class="stat-icon">✅</div>
445
+ <div class="stat-label">Online</div>
446
+ <div class="stat-value green" id="onlineAPIs">0</div>
 
 
447
  </div>
448
+ <div class="stat-card">
449
+ <div class="stat-icon">❌</div>
450
+ <div class="stat-label">Offline</div>
451
+ <div class="stat-value red" id="offlineAPIs">0</div>
452
  </div>
453
+ <div class="stat-card">
454
+ <div class="stat-icon">⚡</div>
455
+ <div class="stat-label">Avg Response</div>
456
+ <div class="stat-value orange" id="avgResponse" style="font-size: 32px;">0ms</div>
457
  </div>
458
+ </div>
 
 
459
 
460
+ <div class="section-header">
461
+ <h2>📊 API Providers Status</h2>
462
+ </div>
463
+ <table class="providers-table">
464
+ <thead>
465
+ <tr>
466
+ <th>Provider</th>
467
+ <th>Category</th>
468
+ <th>Status</th>
469
+ <th>Response Time</th>
470
+ <th>Last Check</th>
471
+ </tr>
472
+ </thead>
473
+ <tbody id="providersTable">
474
+ <tr><td colspan="5" style="text-align: center;">Loading...</td></tr>
475
+ </tbody>
476
+ </table>
477
+
478
+ <div class="hf-section">
479
+ <div class="section-header" style="border: none; margin: 0 0 20px 0;">
480
+ <h2>🤗 HuggingFace Sentiment Analysis</h2>
481
  </div>
482
+ <p style="color: #6c757d; margin-bottom: 10px;">Enter crypto-related text (one per line) to analyze sentiment using AI:</p>
483
+ <textarea id="sentimentText" rows="5" placeholder="BTC strong breakout&#10;ETH looks weak&#10;Market is bullish">BTC strong breakout
484
+ ETH looks weak
485
+ Market is bullish today</textarea>
486
+ <button class="refresh-btn" onclick="runSentiment()">🧠 Analyze Sentiment</button>
487
+ <div class="sentiment-result" id="sentimentResult">—</div>
488
+ <pre id="sentimentDetails" style="background: white; padding: 15px; border-radius: 8px; overflow-x: auto; font-size: 12px;"></pre>
489
+ </div>
490
+ </div>
491
+
492
+ <script>
493
+ async function loadData() {
494
+ try {
495
+ // Show loading state
496
+ const tbody = document.getElementById('providersTable');
497
+ if (tbody) {
498
+ tbody.innerHTML = '<tr><td colspan="5" style="text-align: center; padding: 40px;"><div class="loading"></div><div style="margin-top: 10px; color: #6c757d;">در حال بارگذاری...</div></td></tr>';
499
+ }
500
+ if (document.getElementById('lastUpdate')) {
501
+ document.getElementById('lastUpdate').textContent = 'در حال بارگذاری...';
502
+ }
503
+
504
+ // Load status
505
+ const statusRes = await fetch('/api/status');
506
+ if (!statusRes.ok) {
507
+ throw new Error(`خطا در دریافت وضعیت: ${statusRes.status} ${statusRes.statusText}`);
508
+ }
509
+ const status = await statusRes.json();
510
+
511
+ if (!status || typeof status.total_providers === 'undefined') {
512
+ throw new Error('داده‌های وضعیت نامعتبر است');
513
+ }
514
+
515
+ if (document.getElementById('totalAPIs')) {
516
+ document.getElementById('totalAPIs').textContent = status.total_providers || 0;
517
+ }
518
+ if (document.getElementById('onlineAPIs')) {
519
+ document.getElementById('onlineAPIs').textContent = status.online || 0;
520
+ }
521
+ if (document.getElementById('offlineAPIs')) {
522
+ document.getElementById('offlineAPIs').textContent = status.offline || 0;
523
+ }
524
+ if (document.getElementById('avgResponse')) {
525
+ document.getElementById('avgResponse').textContent = (status.avg_response_time_ms || 0) + 'ms';
526
+ }
527
+ if (document.getElementById('lastUpdate')) {
528
+ document.getElementById('lastUpdate').textContent = status.timestamp ? new Date(status.timestamp).toLocaleString('fa-IR') : 'نامشخص';
529
+ }
530
+
531
+ // Load providers
532
+ const providersRes = await fetch('/api/providers');
533
+ if (!providersRes.ok) {
534
+ throw new Error(`خطا در دریافت لیست APIها: ${providersRes.status} ${providersRes.statusText}`);
535
+ }
536
+ const providers = await providersRes.json();
537
+
538
+ if (!providers || !Array.isArray(providers)) {
539
+ throw new Error('لیست APIها نامعتبر است');
540
+ }
541
+
542
+ if (tbody) {
543
+ if (providers.length === 0) {
544
+ tbody.innerHTML = '<tr><td colspan="5" style="text-align: center; padding: 40px; color: #6c757d;">هیچ APIای یافت نشد</td></tr>';
545
+ } else {
546
+ tbody.innerHTML = providers.map(p => {
547
+ let responseClass = 'response-fast';
548
+ const responseTime = p.response_time_ms || p.avg_response_time_ms || 0;
549
+ if (responseTime > 3000) responseClass = 'response-slow';
550
+ else if (responseTime > 1000) responseClass = 'response-medium';
551
+
552
+ return `
553
+ <tr>
554
+ <td><strong style="font-size: 15px;">${p.name || 'نامشخص'}</strong></td>
555
+ <td><span style="background: #f8f9fa; padding: 4px 10px; border-radius: 8px; font-size: 12px; font-weight: 600;">${p.category || 'نامشخص'}</span></td>
556
+ <td><span class="status-badge status-${p.status || 'unknown'}">${(p.status || 'unknown').toUpperCase()}</span></td>
557
+ <td><span class="response-time ${responseClass}">${responseTime}ms</span></td>
558
+ <td style="color: #6c757d; font-size: 13px;">${p.last_fetch ? new Date(p.last_fetch).toLocaleTimeString('fa-IR') : 'نامشخص'}</td>
559
+ </tr>
560
+ `}).join('');
561
+ }
562
+ }
563
+
564
+ } catch (error) {
565
+ console.error('Error loading data:', error);
566
+ const tbody = document.getElementById('providersTable');
567
+ if (tbody) {
568
+ tbody.innerHTML = `<tr><td colspan="5" style="text-align: center; padding: 40px; color: #ef4444;">
569
+ <div style="font-size: 24px; margin-bottom: 10px;">❌</div>
570
+ <div style="font-weight: 600; margin-bottom: 5px;">خطا در بارگذاری داده‌ها</div>
571
+ <div style="font-size: 14px; color: #6c757d; margin-bottom: 15px;">${error.message || 'خطای نامشخص'}</div>
572
+ <button onclick="loadData()" style="padding: 10px 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border: none; border-radius: 12px; color: white; cursor: pointer; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px;">تلاش مجدد</button>
573
+ </td></tr>`;
574
+ }
575
+ if (document.getElementById('lastUpdate')) {
576
+ document.getElementById('lastUpdate').textContent = 'خطا در بارگذاری';
577
+ }
578
+ alert('❌ خطا در بارگذاری داده‌ها:\n' + (error.message || 'خطای نامشخص'));
579
+ }
580
+ }
581
+
582
+ async function runSentiment() {
583
+ const text = document.getElementById('sentimentText').value;
584
+ const texts = text.split('\n').filter(t => t.trim());
585
+
586
+ if (texts.length === 0) {
587
+ alert('Please enter at least one line of text');
588
+ return;
589
+ }
590
+
591
+ try {
592
+ document.getElementById('sentimentResult').textContent = '⏳ Analyzing...';
593
+ document.getElementById('sentimentDetails').textContent = '';
594
+
595
+ const res = await fetch('/api/hf/run-sentiment', {
596
+ method: 'POST',
597
+ headers: { 'Content-Type': 'application/json' },
598
+ body: JSON.stringify({ texts })
599
+ });
600
+
601
+ if (!res.ok) {
602
+ throw new Error(`HTTP ${res.status}: ${await res.text()}`);
603
+ }
604
+
605
+ const data = await res.json();
606
+
607
+ const vote = data.vote || 0;
608
+ let emoji = '😐';
609
+ let color = '#6c757d';
610
+
611
+ if (vote > 0.2) {
612
+ emoji = '📈';
613
+ color = '#10b981';
614
+ } else if (vote < -0.2) {
615
+ emoji = '📉';
616
+ color = '#ef4444';
617
+ }
618
+
619
+ document.getElementById('sentimentResult').innerHTML = `
620
+ <span style="color: ${color};">${emoji} ${vote.toFixed(3)}</span>
621
+ `;
622
+ document.getElementById('sentimentDetails').textContent = JSON.stringify(data, null, 2);
623
+
624
+ } catch (error) {
625
+ console.error('Error running sentiment:', error);
626
+ document.getElementById('sentimentResult').innerHTML = '<span style="color: #ef4444;">❌ Error</span>';
627
+ document.getElementById('sentimentDetails').textContent = 'Error: ' + error.message;
628
+ }
629
+ }
630
+
631
+ // Load data on page load
632
+ loadData();
633
+
634
+ // Auto-refresh every 30 seconds
635
+ setInterval(loadData, 30000);
636
+ </script>
637
  </body>
638
+ </html>
enhanced_server.py CHANGED
@@ -199,9 +199,9 @@ from fastapi.responses import HTMLResponse, FileResponse
199
 
200
  @app.get("/", response_class=HTMLResponse)
201
  async def root():
202
- """Serve main admin dashboard"""
203
- if os.path.exists("admin.html"):
204
- return FileResponse("admin.html")
205
  else:
206
  return HTMLResponse("""
207
  <html>
 
199
 
200
  @app.get("/", response_class=HTMLResponse)
201
  async def root():
202
+ """Serve main dashboard"""
203
+ if os.path.exists("index.html"):
204
+ return FileResponse("index.html")
205
  else:
206
  return HTMLResponse("""
207
  <html>
hf_console.html CHANGED
@@ -1,97 +1,343 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
  <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1" />
6
- <title>HF Console · Crypto Intelligence</title>
7
- <link rel="preconnect" href="https://fonts.googleapis.com" />
8
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
10
- <link rel="stylesheet" href="/static/css/unified-ui.css" />
11
- <link rel="stylesheet" href="/static/css/components.css" />
12
- <script defer src="/static/js/ui-feedback.js"></script>
13
- <script defer src="/static/js/hf-console.js"></script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  </head>
15
- <body class="page page-hf-console">
16
- <header class="top-nav">
17
- <div class="branding">
18
- <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"/></svg>
19
- <div>
20
- <strong>HF Models & Datasets</strong>
21
- <small style="color:var(--ui-text-muted);letter-spacing:0.2em;">/api/hf/* endpoints</small>
22
  </div>
23
- </div>
24
- <nav class="nav-links">
25
- <a href="/dashboard">Dashboard</a>
26
- <a href="/admin">Admin</a>
27
- <a class="active" href="/hf_console">HF Console</a>
28
- <a href="/docs" target="_blank" rel="noreferrer">API Docs</a>
29
- </nav>
30
- </header>
31
 
32
- <main class="page-content">
33
- <section class="card">
34
- <div class="section-heading">
35
- <h2>Registry & Status</h2>
36
- <span class="badge info" id="hf-console-health">Loading...</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  </div>
38
- <p class="metric-subtext" id="hf-console-summary"></p>
39
- <ul class="list" id="hf-console-models"></ul>
40
- </section>
41
 
42
- <section class="split-grid">
43
- <article class="card">
44
- <div class="section-heading"><h2>Sentiment Playground</h2><span class="badge info">POST /api/hf/models/sentiment</span></div>
45
- <div class="form-field">
46
- <label for="sentiment-model">Sentiment model</label>
47
- <select id="sentiment-model">
48
- <option value="auto">auto (ensemble)</option>
49
- <option value="cryptobert">cryptobert</option>
50
- <option value="cryptobert_finbert">cryptobert_finbert</option>
51
- <option value="tiny_crypto_lm">tiny_crypto_lm</option>
52
- </select>
53
- </div>
54
- <div class="form-field">
55
- <label for="sentiment-texts">Texts (one per line)</label>
56
- <textarea id="sentiment-texts" rows="5" placeholder="BTC is breaking out...\nETH looks weak..."></textarea>
57
- </div>
58
- <button class="primary" id="run-sentiment">Run Sentiment</button>
59
- <div id="sentiment-results" class="ws-stream" style="margin-top:16px;"></div>
60
- </article>
61
- <article class="card">
62
- <div class="section-heading"><h2>Forecast Sandbox</h2><span class="badge info">POST /api/hf/models/forecast</span></div>
63
- <div class="form-field">
64
- <label for="forecast-model">Model</label>
65
- <select id="forecast-model">
66
- <option value="btc_lstm">btc_lstm</option>
67
- <option value="btc_arima">btc_arima</option>
68
- </select>
69
  </div>
70
- <div class="form-field">
71
- <label for="forecast-series">Closing Prices (comma separated)</label>
72
- <textarea id="forecast-series" rows="5" placeholder="67650, 67820, 68010, 68120"></textarea>
73
- </div>
74
- <div class="form-field">
75
- <label for="forecast-steps">Future Steps</label>
76
- <input type="number" id="forecast-steps" value="3" min="1" max="10" />
 
 
 
 
 
 
 
 
77
  </div>
78
- <button class="primary" id="run-forecast">Forecast</button>
79
- <div id="forecast-results" class="ws-stream" style="margin-top:16px;"></div>
80
- </article>
81
- </section>
82
 
83
- <section class="card">
84
- <div class="section-heading">
85
- <h2>HF Datasets</h2>
86
- <span class="badge info">GET /api/hf/datasets/*</span>
87
- </div>
88
- <div class="button-row" style="margin-bottom:16px;">
89
- <button class="secondary" data-dataset="market-ohlcv">Market OHLCV</button>
90
- <button class="secondary" data-dataset="market-btc">BTC Technicals</button>
91
- <button class="secondary" data-dataset="news-semantic">News Semantic</button>
92
- </div>
93
- <div id="dataset-output" class="ws-stream"></div>
94
- </section>
95
- </main>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  </body>
97
- </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="en" dir="rtl">
3
  <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>HF Console - Crypto API Monitor</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ body {
10
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
11
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
12
+ padding: 20px;
13
+ direction: rtl;
14
+ }
15
+ .container {
16
+ max-width: 1400px;
17
+ margin: 0 auto;
18
+ background: white;
19
+ border-radius: 16px;
20
+ padding: 30px;
21
+ box-shadow: 0 10px 40px rgba(0,0,0,0.2);
22
+ }
23
+ h1 {
24
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
25
+ -webkit-background-clip: text;
26
+ -webkit-text-fill-color: transparent;
27
+ margin-bottom: 10px;
28
+ font-size: 32px;
29
+ }
30
+ .subtitle { color: #666; margin-bottom: 30px; }
31
+ section {
32
+ margin-bottom: 30px;
33
+ padding: 20px;
34
+ background: #f8f9fa;
35
+ border-radius: 12px;
36
+ border: 2px solid #e9ecef;
37
+ }
38
+ h3 {
39
+ color: #333;
40
+ margin-bottom: 15px;
41
+ font-size: 20px;
42
+ display: flex;
43
+ align-items: center;
44
+ gap: 10px;
45
+ }
46
+ .badge {
47
+ display: inline-block;
48
+ padding: 4px 12px;
49
+ border-radius: 12px;
50
+ font-size: 14px;
51
+ font-weight: 600;
52
+ }
53
+ .badge-success { background: #d1fae5; color: #10b981; }
54
+ .badge-warning { background: #fef3c7; color: #f59e0b; }
55
+ .badge-info { background: #dbeafe; color: #3b82f6; }
56
+ pre {
57
+ background: #1e293b;
58
+ color: #e2e8f0;
59
+ padding: 15px;
60
+ border-radius: 8px;
61
+ overflow-x: auto;
62
+ font-size: 13px;
63
+ line-height: 1.6;
64
+ max-height: 300px;
65
+ overflow-y: auto;
66
+ }
67
+ button {
68
+ padding: 10px 20px;
69
+ border: none;
70
+ border-radius: 8px;
71
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
72
+ color: white;
73
+ font-weight: 600;
74
+ cursor: pointer;
75
+ transition: all 0.3s;
76
+ margin: 5px;
77
+ }
78
+ button:hover {
79
+ transform: translateY(-2px);
80
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
81
+ }
82
+ button:active { transform: translateY(0); }
83
+ input, textarea {
84
+ width: 100%;
85
+ padding: 10px;
86
+ border: 2px solid #e9ecef;
87
+ border-radius: 8px;
88
+ font-family: inherit;
89
+ margin: 10px 0;
90
+ }
91
+ input:focus, textarea:focus {
92
+ outline: none;
93
+ border-color: #667eea;
94
+ }
95
+ .list-box {
96
+ max-height: 250px;
97
+ overflow-y: auto;
98
+ border: 1px solid #e9ecef;
99
+ padding: 10px;
100
+ background: white;
101
+ border-radius: 8px;
102
+ margin: 10px 0;
103
+ }
104
+ .list-box ul { list-style: none; }
105
+ .list-box li {
106
+ padding: 8px;
107
+ border-bottom: 1px solid #f1f5f9;
108
+ font-size: 14px;
109
+ color: #475569;
110
+ }
111
+ .list-box li:last-child { border-bottom: none; }
112
+ .vote-display {
113
+ font-size: 24px;
114
+ font-weight: 700;
115
+ padding: 15px;
116
+ background: white;
117
+ border-radius: 8px;
118
+ text-align: center;
119
+ margin: 10px 0;
120
+ }
121
+ .vote-positive { color: #10b981; }
122
+ .vote-negative { color: #ef4444; }
123
+ .vote-neutral { color: #6b7280; }
124
+ .loading {
125
+ display: inline-block;
126
+ width: 16px;
127
+ height: 16px;
128
+ border: 3px solid #f3f4f6;
129
+ border-top-color: #667eea;
130
+ border-radius: 50%;
131
+ animation: spin 0.8s linear infinite;
132
+ }
133
+ @keyframes spin {
134
+ to { transform: rotate(360deg); }
135
+ }
136
+ .grid {
137
+ display: grid;
138
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
139
+ gap: 20px;
140
+ }
141
+ </style>
142
+ <!-- API Configuration -->
143
+ <script src="config.js"></script>
144
  </head>
145
+ <body>
146
+ <div class="container">
147
+ <h1>🤗 HuggingFace Console</h1>
148
+ <p class="subtitle">Test HF connectivity, registry, search, and sentiment analysis</p>
149
+ <div style="background: #f0f9ff; padding: 10px; border-radius: 8px; margin-bottom: 20px; font-size: 13px; color: #0369a1;">
150
+ <strong>🌐 Environment:</strong> <span id="envInfo">Loading...</span> |
151
+ <strong>📡 API:</strong> <span id="apiInfo">Loading...</span>
152
  </div>
 
 
 
 
 
 
 
 
153
 
154
+ <section>
155
+ <h3>
156
+ <span>📊 Health Status</span>
157
+ <span class="badge badge-info" id="healthBadge">Loading...</span>
158
+ </h3>
159
+ <button onclick="loadHealth()">🔄 Refresh Health</button>
160
+ <button onclick="doRefresh()">🔃 Force Registry Refresh</button>
161
+ <pre id="healthOutput">Loading...</pre>
162
+ </section>
163
+
164
+ <div class="grid">
165
+ <section>
166
+ <h3>
167
+ <span>🤖 Models Registry</span>
168
+ <span class="badge badge-success" id="modelsCount">0</span>
169
+ </h3>
170
+ <button onclick="loadModels()">Load Models</button>
171
+ <div class="list-box" id="modelsList">
172
+ <p style="color: #94a3b8;">Click "Load Models" to fetch...</p>
173
+ </div>
174
+ </section>
175
+
176
+ <section>
177
+ <h3>
178
+ <span>📚 Datasets Registry</span>
179
+ <span class="badge badge-success" id="datasetsCount">0</span>
180
+ </h3>
181
+ <button onclick="loadDatasets()">Load Datasets</button>
182
+ <div class="list-box" id="datasetsList">
183
+ <p style="color: #94a3b8;">Click "Load Datasets" to fetch...</p>
184
+ </div>
185
+ </section>
186
  </div>
 
 
 
187
 
188
+ <section>
189
+ <h3>🔍 Search Registry (Local Snapshot)</h3>
190
+ <input type="text" id="searchQuery" placeholder="Search query (e.g., crypto, bitcoin, sentiment)" value="crypto">
191
+ <button onclick="doSearch()">Search Models</button>
192
+ <button onclick="doSearchDatasets()">Search Datasets</button>
193
+ <div class="list-box" id="searchResults">
194
+ <p style="color: #94a3b8;">Enter a query and click search...</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  </div>
196
+ </section>
197
+
198
+ <section>
199
+ <h3>💭 Sentiment Analysis (Local Pipeline)</h3>
200
+ <p style="color: #666; font-size: 14px; margin-bottom: 10px;">
201
+ Enter text samples (one per line) to analyze crypto sentiment using local transformers
202
+ </p>
203
+ <textarea id="sentimentTexts" rows="5" placeholder="BTC looks strong&#10;ETH is weak today&#10;Market sentiment is bullish">BTC strong breakout
204
+ ETH looks weak
205
+ Crypto market is bullish today
206
+ Bears are taking control
207
+ Neutral market conditions</textarea>
208
+ <button onclick="doSentiment()">🧠 Run Sentiment Analysis</button>
209
+ <div class="vote-display" id="voteDisplay">
210
+ <span style="color: #94a3b8;">—</span>
211
  </div>
212
+ <pre id="sentimentOutput">Results will appear here...</pre>
213
+ </section>
214
+ </div>
 
215
 
216
+ <script>
217
+ // Use the CONFIG object from config.js
218
+ const API_BASE = CONFIG.API_BASE;
219
+ const fetchJSON = CONFIG.fetchJSON;
220
+ const postJSON = CONFIG.postJSON;
221
+
222
+ // Display environment info
223
+ function updateEnvironmentInfo() {
224
+ const envType = CONFIG.IS_HUGGINGFACE_SPACES ? '🤗 HuggingFace Spaces' :
225
+ CONFIG.IS_LOCALHOST ? '💻 Localhost' : '🌐 Custom Deployment';
226
+ document.getElementById('envInfo').textContent = envType;
227
+ document.getElementById('apiInfo').textContent = CONFIG.API_BASE;
228
+ }
229
+
230
+ async function loadHealth() {
231
+ try {
232
+ const data = await fetchJSON(CONFIG.ENDPOINTS.HF_HEALTH);
233
+ document.getElementById('healthOutput').textContent = JSON.stringify(data, null, 2);
234
+ document.getElementById('healthBadge').textContent = data.ok ? '✓ Healthy' : '✗ Unhealthy';
235
+ document.getElementById('healthBadge').className = data.ok ? 'badge badge-success' : 'badge badge-warning';
236
+ } catch (err) {
237
+ document.getElementById('healthOutput').textContent = `Error: ${err.message}`;
238
+ document.getElementById('healthBadge').textContent = '✗ Error';
239
+ document.getElementById('healthBadge').className = 'badge badge-warning';
240
+ }
241
+ }
242
+
243
+ async function doRefresh() {
244
+ try {
245
+ document.getElementById('healthOutput').textContent = 'Refreshing registry...';
246
+ const data = await postJSON(CONFIG.ENDPOINTS.HF_REFRESH, {});
247
+ document.getElementById('healthOutput').textContent = JSON.stringify(data, null, 2);
248
+ await loadHealth();
249
+ } catch (err) {
250
+ document.getElementById('healthOutput').textContent = `Error: ${err.message}`;
251
+ }
252
+ }
253
+
254
+ async function loadModels() {
255
+ try {
256
+ const data = await fetchJSON(`${CONFIG.ENDPOINTS.HF_REGISTRY}?kind=models`);
257
+ const items = data.items || [];
258
+ document.getElementById('modelsCount').textContent = items.length;
259
+ const html = items.length > 0
260
+ ? '<ul>' + items.slice(0, 50).map(i => `<li>🤖 ${i.id} • ${i.pipeline_tag || 'N/A'} • <small>${i.source}</small></li>`).join('') + '</ul>'
261
+ : '<p style="color: #94a3b8;">No models found</p>';
262
+ document.getElementById('modelsList').innerHTML = html;
263
+ } catch (err) {
264
+ document.getElementById('modelsList').innerHTML = `<p style="color: #ef4444;">Error: ${err.message}</p>`;
265
+ }
266
+ }
267
+
268
+ async function loadDatasets() {
269
+ try {
270
+ const data = await fetchJSON(`${CONFIG.ENDPOINTS.HF_REGISTRY}?kind=datasets`);
271
+ const items = data.items || [];
272
+ document.getElementById('datasetsCount').textContent = items.length;
273
+ const html = items.length > 0
274
+ ? '<ul>' + items.slice(0, 50).map(i => `<li>📚 ${i.id} • <small>${i.source}</small></li>`).join('') + '</ul>'
275
+ : '<p style="color: #94a3b8;">No datasets found</p>';
276
+ document.getElementById('datasetsList').innerHTML = html;
277
+ } catch (err) {
278
+ document.getElementById('datasetsList').innerHTML = `<p style="color: #ef4444;">Error: ${err.message}</p>`;
279
+ }
280
+ }
281
+
282
+ async function doSearch() {
283
+ const q = document.getElementById('searchQuery').value;
284
+ try {
285
+ const data = await fetchJSON(`${CONFIG.ENDPOINTS.HF_SEARCH}?q=${encodeURIComponent(q)}&kind=models`);
286
+ const items = data.items || [];
287
+ const html = items.length > 0
288
+ ? `<p style="color: #10b981; font-weight: 600;">Found ${items.length} models</p><ul>` + items.map(i => `<li>🤖 ${i.id}</li>`).join('') + '</ul>'
289
+ : '<p style="color: #94a3b8;">No results found</p>';
290
+ document.getElementById('searchResults').innerHTML = html;
291
+ } catch (err) {
292
+ document.getElementById('searchResults').innerHTML = `<p style="color: #ef4444;">Error: ${err.message}</p>`;
293
+ }
294
+ }
295
+
296
+ async function doSearchDatasets() {
297
+ const q = document.getElementById('searchQuery').value;
298
+ try {
299
+ const data = await fetchJSON(`${CONFIG.ENDPOINTS.HF_SEARCH}?q=${encodeURIComponent(q)}&kind=datasets`);
300
+ const items = data.items || [];
301
+ const html = items.length > 0
302
+ ? `<p style="color: #10b981; font-weight: 600;">Found ${items.length} datasets</p><ul>` + items.map(i => `<li>📚 ${i.id}</li>`).join('') + '</ul>'
303
+ : '<p style="color: #94a3b8;">No results found</p>';
304
+ document.getElementById('searchResults').innerHTML = html;
305
+ } catch (err) {
306
+ document.getElementById('searchResults').innerHTML = `<p style="color: #ef4444;">Error: ${err.message}</p>`;
307
+ }
308
+ }
309
+
310
+ async function doSentiment() {
311
+ const texts = document.getElementById('sentimentTexts').value.split('\n').filter(t => t.trim());
312
+ if (texts.length === 0) {
313
+ alert('Please enter at least one text sample');
314
+ return;
315
+ }
316
+ try {
317
+ document.getElementById('voteDisplay').innerHTML = '<span class="loading"></span>';
318
+ document.getElementById('sentimentOutput').textContent = 'Running sentiment analysis...';
319
+
320
+ const data = await postJSON(CONFIG.ENDPOINTS.HF_RUN_SENTIMENT, { texts });
321
+
322
+ const vote = data.vote || 0;
323
+ let voteClass = 'vote-neutral';
324
+ let voteEmoji = '😐';
325
+ if (vote > 0.2) { voteClass = 'vote-positive'; voteEmoji = '📈'; }
326
+ else if (vote < -0.2) { voteClass = 'vote-negative'; voteEmoji = '📉'; }
327
+
328
+ document.getElementById('voteDisplay').innerHTML = `<span class="${voteClass}">${voteEmoji} ${vote.toFixed(3)}</span>`;
329
+ document.getElementById('sentimentOutput').textContent = JSON.stringify(data, null, 2);
330
+ } catch (err) {
331
+ document.getElementById('voteDisplay').innerHTML = '<span style="color: #ef4444;">Error</span>';
332
+ document.getElementById('sentimentOutput').textContent = `Error: ${err.message}`;
333
+ }
334
+ }
335
+
336
+ // Auto-load health and environment info on page load
337
+ window.addEventListener('load', () => {
338
+ updateEnvironmentInfo();
339
+ loadHealth();
340
+ });
341
+ </script>
342
  </body>
343
+ </html>
hf_unified_server.py CHANGED
@@ -1,2575 +1,10 @@
1
- """Unified HuggingFace Space API Server leveraging shared collectors and AI helpers."""
2
-
3
- import asyncio
4
- import time
5
- import os
6
- import sys
7
- import io
8
-
9
- # Fix encoding for Windows console (must be done before any print/logging)
10
- if sys.platform == "win32":
11
- try:
12
- sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
13
- sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
14
- except Exception:
15
- pass # If already wrapped, ignore
16
-
17
- # Set environment variables to force PyTorch and avoid TensorFlow/Keras issues
18
- os.environ.setdefault('TRANSFORMERS_NO_ADVISORY_WARNINGS', '1')
19
- os.environ.setdefault('TRANSFORMERS_VERBOSITY', 'error')
20
- os.environ.setdefault('TF_CPP_MIN_LOG_LEVEL', '3') # Suppress TensorFlow warnings
21
- # Force PyTorch as default framework
22
- os.environ.setdefault('TRANSFORMERS_FRAMEWORK', 'pt')
23
-
24
- from datetime import datetime, timedelta
25
- from fastapi import Body, FastAPI, HTTPException, Query, WebSocket, WebSocketDisconnect
26
- from fastapi.middleware.cors import CORSMiddleware
27
- from fastapi.responses import FileResponse, JSONResponse, HTMLResponse
28
- from fastapi.staticfiles import StaticFiles
29
- from starlette.websockets import WebSocketState
30
- from typing import Any, Dict, List, Optional, Union
31
- from statistics import mean
32
- import logging
33
- import random
34
- import json
35
- from pathlib import Path
36
- import httpx
37
-
38
-
39
- from ai_models import (
40
- analyze_chart_points,
41
- analyze_crypto_sentiment,
42
- analyze_market_text,
43
- get_model_info,
44
- initialize_models,
45
- registry_status,
46
- )
47
- from backend.services.local_resource_service import LocalResourceService
48
- from collectors.aggregator import (
49
- CollectorError,
50
- MarketDataCollector,
51
- NewsCollector,
52
- ProviderStatusCollector,
53
- )
54
- from config import COIN_SYMBOL_MAPPING, get_settings
55
-
56
- # Setup logging
57
- logging.basicConfig(level=logging.INFO)
58
- logger = logging.getLogger(__name__)
59
-
60
- # Create FastAPI app
61
- app = FastAPI(
62
- title="Cryptocurrency Data & Analysis API",
63
- description="Complete API for cryptocurrency data, market analysis, and trading signals",
64
- version="3.0.0"
65
- )
66
-
67
- # CORS
68
- app.add_middleware(
69
- CORSMiddleware,
70
- allow_origins=["*"],
71
- allow_credentials=True,
72
- allow_methods=["*"],
73
- allow_headers=["*"],
74
- )
75
-
76
- # Runtime state
77
- START_TIME = time.time()
78
- cache = {"ohlcv": {}, "prices": {}, "market_data": {}, "providers": [], "last_update": None}
79
- settings = get_settings()
80
- market_collector = MarketDataCollector()
81
- news_collector = NewsCollector()
82
- provider_collector = ProviderStatusCollector()
83
-
84
- # Load providers config
85
- WORKSPACE_ROOT = Path(__file__).parent
86
- PROVIDERS_CONFIG_PATH = settings.providers_config_path
87
- FALLBACK_RESOURCE_PATH = WORKSPACE_ROOT / "crypto_resources_unified_2025-11-11.json"
88
- LOG_DIR = WORKSPACE_ROOT / "logs"
89
- APL_REPORT_PATH = WORKSPACE_ROOT / "PROVIDER_AUTO_DISCOVERY_REPORT.json"
90
-
91
- # Ensure log directory exists
92
- LOG_DIR.mkdir(parents=True, exist_ok=True)
93
-
94
- # Database path (managed by DatabaseManager in the admin API)
95
- DB_PATH = WORKSPACE_ROOT / "data" / "api_monitor.db"
96
-
97
- def tail_log_file(path: Path, max_lines: int = 200) -> List[str]:
98
- """Return the last max_lines from a log file, if it exists."""
99
- if not path.exists():
100
- return []
101
- try:
102
- with path.open("r", encoding="utf-8", errors="ignore") as f:
103
- lines = f.readlines()
104
- return lines[-max_lines:]
105
- except Exception as e:
106
- logger.error(f"Error reading log file {path}: {e}")
107
- return []
108
-
109
-
110
- def load_providers_config():
111
- """Load providers from providers_config_extended.json"""
112
- try:
113
- if PROVIDERS_CONFIG_PATH.exists():
114
- with open(PROVIDERS_CONFIG_PATH, 'r', encoding='utf-8') as f:
115
- config = json.load(f)
116
- providers = config.get('providers', {})
117
- logger.info(f"Loaded {len(providers)} providers from providers_config_extended.json")
118
- return providers
119
- else:
120
- logger.warning(f"providers_config_extended.json not found at {PROVIDERS_CONFIG_PATH}")
121
- return {}
122
- except Exception as e:
123
- logger.error(f"Error loading providers config: {e}")
124
- return {}
125
-
126
- # Load providers at startup
127
- PROVIDERS_CONFIG = load_providers_config()
128
- local_resource_service = LocalResourceService(FALLBACK_RESOURCE_PATH)
129
-
130
- HF_SAMPLE_NEWS = [
131
- {
132
- "title": "Bitcoin holds key liquidity zone",
133
- "source": "Fallback Ledger",
134
- "sentiment": "positive",
135
- "sentiment_score": 0.64,
136
- "entities": ["BTC"],
137
- "summary": "BTC consolidates near resistance with steady inflows",
138
- },
139
- {
140
- "title": "Ethereum staking demand remains resilient",
141
- "source": "Fallback Ledger",
142
- "sentiment": "neutral",
143
- "sentiment_score": 0.12,
144
- "entities": ["ETH"],
145
- "summary": "Validator queue shortens as fees stabilize around L2 adoption",
146
- },
147
- {
148
- "title": "Solana ecosystem sees TVL uptick",
149
- "source": "Fallback Ledger",
150
- "sentiment": "positive",
151
- "sentiment_score": 0.41,
152
- "entities": ["SOL"],
153
- "summary": "DeFi protocols move to Solana as mempool congestion drops",
154
- },
155
- ]
156
-
157
- # Mount static files (CSS, JS)
158
- try:
159
- static_path = WORKSPACE_ROOT / "static"
160
- if static_path.exists():
161
- app.mount("/static", StaticFiles(directory=str(static_path)), name="static")
162
- logger.info(f"Static files mounted from {static_path}")
163
- else:
164
- logger.warning(f"Static directory not found: {static_path}")
165
- except Exception as e:
166
- logger.error(f"Error mounting static files: {e}")
167
-
168
- # Mount api-resources for frontend access
169
- try:
170
- api_resources_path = WORKSPACE_ROOT / "api-resources"
171
- if api_resources_path.exists():
172
- app.mount("/api-resources", StaticFiles(directory=str(api_resources_path)), name="api-resources")
173
- logger.info(f"API resources mounted from {api_resources_path}")
174
- else:
175
- logger.warning(f"API resources directory not found: {api_resources_path}")
176
- except Exception as e:
177
- logger.error(f"Error mounting API resources: {e}")
178
-
179
- # ============================================================================
180
- # Helper utilities & Data Fetching Functions
181
- # ============================================================================
182
-
183
- def _normalize_asset_symbol(symbol: str) -> str:
184
- symbol = (symbol or "").upper()
185
- suffixes = ("USDT", "USD", "BTC", "ETH", "BNB")
186
- for suffix in suffixes:
187
- if symbol.endswith(suffix) and len(symbol) > len(suffix):
188
- return symbol[: -len(suffix)]
189
- return symbol
190
-
191
-
192
- def _format_price_record(record: Dict[str, Any]) -> Dict[str, Any]:
193
- price = record.get("price") or record.get("current_price")
194
- change_pct = record.get("change_24h") or record.get("price_change_percentage_24h")
195
- change_abs = None
196
- if price is not None and change_pct is not None:
197
- try:
198
- change_abs = float(price) * float(change_pct) / 100.0
199
- except (TypeError, ValueError):
200
- change_abs = None
201
-
202
- return {
203
- "id": record.get("id") or record.get("symbol", "").lower(),
204
- "symbol": record.get("symbol", "").upper(),
205
- "name": record.get("name"),
206
- "current_price": price,
207
- "market_cap": record.get("market_cap"),
208
- "market_cap_rank": record.get("rank"),
209
- "total_volume": record.get("volume_24h") or record.get("total_volume"),
210
- "price_change_24h": change_abs,
211
- "price_change_percentage_24h": change_pct,
212
- "high_24h": record.get("high_24h"),
213
- "low_24h": record.get("low_24h"),
214
- "last_updated": record.get("last_updated"),
215
- }
216
-
217
-
218
- async def fetch_binance_ohlcv(symbol: str = "BTCUSDT", interval: str = "1h", limit: int = 100):
219
- """Fetch OHLCV data from Binance via the shared collector."""
220
-
221
- try:
222
- candles = await market_collector.get_ohlcv(symbol, interval, limit)
223
- return [
224
- {
225
- **candle,
226
- "timestamp": int(datetime.fromisoformat(candle["timestamp"]).timestamp() * 1000),
227
- "datetime": candle["timestamp"],
228
- }
229
- for candle in candles
230
- ]
231
- except CollectorError as exc:
232
- logger.error("Error fetching OHLCV: %s", exc)
233
- fallback_symbol = _normalize_asset_symbol(symbol)
234
- fallback = local_resource_service.get_ohlcv(fallback_symbol, interval, limit)
235
- if fallback:
236
- return fallback
237
- return []
238
-
239
-
240
- async def fetch_coingecko_prices(symbols: Optional[List[str]] = None, limit: int = 10):
241
- """Fetch price snapshots using the shared market collector."""
242
-
243
- source = "coingecko"
244
- try:
245
- if symbols:
246
- tasks = [market_collector.get_coin_details(_normalize_asset_symbol(sym)) for sym in symbols]
247
- results = await asyncio.gather(*tasks, return_exceptions=True)
248
- coins: List[Dict[str, Any]] = []
249
- for result in results:
250
- if isinstance(result, Exception):
251
- continue
252
- coins.append(_format_price_record(result))
253
- if coins:
254
- return coins, source
255
- else:
256
- top = await market_collector.get_top_coins(limit=limit)
257
- formatted = [_format_price_record(entry) for entry in top]
258
- if formatted:
259
- return formatted, source
260
- except CollectorError as exc:
261
- logger.error("Error fetching aggregated prices: %s", exc)
262
-
263
- fallback = (
264
- local_resource_service.get_prices_for_symbols([sym for sym in symbols or []])
265
- if symbols
266
- else local_resource_service.get_top_prices(limit)
267
- )
268
- if fallback:
269
- return fallback, "local-fallback"
270
- return [], source
271
-
272
-
273
- async def fetch_binance_ticker(symbol: str):
274
- """Provide ticker-like information sourced from CoinGecko market data."""
275
-
276
- try:
277
- coin = await market_collector.get_coin_details(_normalize_asset_symbol(symbol))
278
- except CollectorError as exc:
279
- logger.error("Unable to load ticker for %s: %s", symbol, exc)
280
- coin = None
281
-
282
- if coin:
283
- price = coin.get("price")
284
- change_pct = coin.get("change_24h") or 0.0
285
- change_abs = price * change_pct / 100 if price is not None and change_pct is not None else None
286
- return {
287
- "symbol": symbol.upper(),
288
- "price": price,
289
- "price_change_24h": change_abs,
290
- "price_change_percent_24h": change_pct,
291
- "high_24h": coin.get("high_24h"),
292
- "low_24h": coin.get("low_24h"),
293
- "volume_24h": coin.get("volume_24h"),
294
- "quote_volume_24h": coin.get("volume_24h"),
295
- }, "binance"
296
-
297
- fallback_symbol = _normalize_asset_symbol(symbol)
298
- fallback = local_resource_service.get_ticker_snapshot(fallback_symbol)
299
- if fallback:
300
- fallback["symbol"] = symbol.upper()
301
- return fallback, "local-fallback"
302
- return None, "binance"
303
-
304
-
305
- # ============================================================================
306
- # Core Endpoints
307
- # ============================================================================
308
-
309
- @app.get("/health")
310
- async def health():
311
- """System health check using shared collectors."""
312
-
313
- async def _safe_call(coro):
314
- try:
315
- data = await coro
316
- return {"status": "ok", "count": len(data) if hasattr(data, "__len__") else 1}
317
- except Exception as exc: # pragma: no cover - network heavy
318
- return {"status": "error", "detail": str(exc)}
319
-
320
- market_task = asyncio.create_task(_safe_call(market_collector.get_top_coins(limit=3)))
321
- news_task = asyncio.create_task(_safe_call(news_collector.get_latest_news(limit=3)))
322
- providers_task = asyncio.create_task(_safe_call(provider_collector.get_providers_status()))
323
-
324
- market_status, news_status, providers_status = await asyncio.gather(
325
- market_task, news_task, providers_task
326
- )
327
-
328
- ai_status = registry_status()
329
- service_states = {
330
- "market_data": market_status,
331
- "news": news_status,
332
- "providers": providers_status,
333
- "ai_models": ai_status,
334
- }
335
-
336
- degraded = any(state.get("status") != "ok" for state in (market_status, news_status, providers_status))
337
- overall = "healthy" if not degraded else "degraded"
338
-
339
- return {
340
- "status": overall,
341
- "service": "cryptocurrency-data-api",
342
- "timestamp": datetime.utcnow().isoformat(),
343
- "version": app.version,
344
- "providers_loaded": market_status.get("count", 0),
345
- "services": service_states,
346
- }
347
-
348
-
349
- @app.get("/info")
350
- async def info():
351
- """System information"""
352
- hf_providers = [p for p in PROVIDERS_CONFIG.keys() if "huggingface_space" in p]
353
-
354
- return {
355
- "service": "Cryptocurrency Data & Analysis API",
356
- "version": app.version,
357
- "endpoints": {
358
- "core": ["/health", "/info", "/api/providers"],
359
- "data": ["/api/ohlcv", "/api/crypto/prices/top", "/api/crypto/price/{symbol}", "/api/crypto/market-overview"],
360
- "analysis": ["/api/analysis/signals", "/api/analysis/smc", "/api/scoring/snapshot"],
361
- "market": ["/api/market/prices", "/api/market-data/prices"],
362
- "system": ["/api/system/status", "/api/system/config"],
363
- "huggingface": ["/api/hf/health", "/api/hf/refresh", "/api/hf/registry", "/api/hf/run-sentiment"],
364
- },
365
- "data_sources": ["Binance", "CoinGecko", "CoinPaprika", "CoinCap"],
366
- "providers_loaded": len(PROVIDERS_CONFIG),
367
- "huggingface_space_providers": len(hf_providers),
368
- "features": [
369
- "Real-time price data",
370
- "OHLCV historical data",
371
- "Trading signals",
372
- "Market analysis",
373
- "Sentiment analysis",
374
- "HuggingFace model integration",
375
- f"{len(PROVIDERS_CONFIG)} providers from providers_config_extended.json",
376
- ],
377
- "ai_registry": registry_status(),
378
- }
379
-
380
-
381
- @app.get("/api/providers")
382
- async def get_providers():
383
- """Get list of API providers and their health."""
384
-
385
- try:
386
- statuses = await provider_collector.get_providers_status()
387
- except Exception as exc: # pragma: no cover - network heavy
388
- logger.error("Error getting providers: %s", exc)
389
- raise HTTPException(status_code=503, detail=str(exc))
390
-
391
- providers_list = []
392
- for status in statuses:
393
- meta = PROVIDERS_CONFIG.get(status["provider_id"], {})
394
- providers_list.append(
395
- {
396
- **status,
397
- "base_url": meta.get("base_url"),
398
- "requires_auth": meta.get("requires_auth"),
399
- "priority": meta.get("priority"),
400
- }
401
- )
402
-
403
- return {
404
- "providers": providers_list,
405
- "total": len(providers_list),
406
- "source": str(PROVIDERS_CONFIG_PATH),
407
- "last_updated": datetime.utcnow().isoformat(),
408
- }
409
-
410
-
411
- @app.get("/api/providers/{provider_id}/health")
412
- async def get_provider_health(provider_id: str):
413
- """Get health status for a specific provider."""
414
-
415
- # Check if provider exists in config
416
- provider_config = PROVIDERS_CONFIG.get(provider_id)
417
- if not provider_config:
418
- raise HTTPException(status_code=404, detail=f"Provider '{provider_id}' not found")
419
-
420
- try:
421
- # Perform health check using the collector
422
- async with httpx.AsyncClient(timeout=provider_collector.timeout, headers=provider_collector.headers) as client:
423
- health_result = await provider_collector._check_provider(client, provider_id, provider_config)
424
-
425
- # Add metadata from config
426
- health_result.update({
427
- "base_url": provider_config.get("base_url"),
428
- "requires_auth": provider_config.get("requires_auth"),
429
- "priority": provider_config.get("priority"),
430
- "category": provider_config.get("category"),
431
- "last_checked": datetime.utcnow().isoformat()
432
- })
433
-
434
- return health_result
435
- except Exception as exc: # pragma: no cover - network heavy
436
- logger.error("Error checking provider health for %s: %s", provider_id, exc)
437
- raise HTTPException(status_code=503, detail=f"Health check failed: {str(exc)}")
438
-
439
-
440
- @app.get("/api/providers/config")
441
- async def get_providers_config():
442
- """Get providers configuration in format expected by frontend."""
443
- try:
444
- return {
445
- "success": True,
446
- "providers": PROVIDERS_CONFIG,
447
- "total": len(PROVIDERS_CONFIG),
448
- "source": str(PROVIDERS_CONFIG_PATH),
449
- "last_updated": datetime.utcnow().isoformat()
450
- }
451
- except Exception as exc:
452
- logger.error("Error getting providers config: %s", exc)
453
- raise HTTPException(status_code=500, detail=str(exc))
454
-
455
-
456
- # ============================================================================
457
- # OHLCV Data Endpoint
458
- # ============================================================================
459
-
460
- @app.get("/api/ohlcv")
461
- async def get_ohlcv(
462
- symbol: str = Query("BTCUSDT", description="Trading pair symbol"),
463
- interval: str = Query("1h", description="Time interval (1m, 5m, 15m, 1h, 4h, 1d)"),
464
- limit: int = Query(100, ge=1, le=1000, description="Number of candles")
465
- ):
466
- """
467
- Get OHLCV (candlestick) data for a trading pair
468
-
469
- Supported intervals: 1m, 5m, 15m, 30m, 1h, 4h, 1d
470
- """
471
- try:
472
- # Check cache
473
- cache_key = f"{symbol}_{interval}_{limit}"
474
- if cache_key in cache["ohlcv"]:
475
- cached_data, cached_time = cache["ohlcv"][cache_key]
476
- if (datetime.now() - cached_time).seconds < 60: # 60s cache
477
- return {"symbol": symbol, "interval": interval, "data": cached_data, "source": "cache"}
478
-
479
- # Fetch from Binance
480
- ohlcv_data = await fetch_binance_ohlcv(symbol, interval, limit)
481
-
482
- if ohlcv_data:
483
- # Update cache
484
- cache["ohlcv"][cache_key] = (ohlcv_data, datetime.now())
485
-
486
- return {
487
- "symbol": symbol,
488
- "interval": interval,
489
- "count": len(ohlcv_data),
490
- "data": ohlcv_data,
491
- "source": "binance",
492
- "timestamp": datetime.now().isoformat()
493
- }
494
- else:
495
- raise HTTPException(status_code=503, detail="Unable to fetch OHLCV data")
496
-
497
- except HTTPException:
498
- raise
499
- except Exception as e:
500
- logger.error(f"Error in get_ohlcv: {e}")
501
- raise HTTPException(status_code=500, detail=str(e))
502
-
503
-
504
- # ============================================================================
505
- # Crypto Prices Endpoints
506
- # ============================================================================
507
-
508
- @app.get("/api/crypto/prices/top")
509
- async def get_top_prices(limit: int = Query(10, ge=1, le=100, description="Number of top cryptocurrencies")):
510
- """Get top cryptocurrencies by market cap"""
511
- try:
512
- # Check cache
513
- cache_key = f"top_{limit}"
514
- if cache_key in cache["prices"]:
515
- cached_data, cached_time = cache["prices"][cache_key]
516
- if (datetime.now() - cached_time).seconds < 60:
517
- return {"data": cached_data, "source": "cache"}
518
-
519
- # Fetch from CoinGecko
520
- prices, source = await fetch_coingecko_prices(limit=limit)
521
-
522
- if prices:
523
- # Update cache
524
- cache["prices"][cache_key] = (prices, datetime.now())
525
-
526
- return {
527
- "count": len(prices),
528
- "data": prices,
529
- "source": source,
530
- "timestamp": datetime.now().isoformat()
531
- }
532
- else:
533
- raise HTTPException(status_code=503, detail="Unable to fetch price data")
534
-
535
- except HTTPException:
536
- raise
537
- except Exception as e:
538
- logger.error(f"Error in get_top_prices: {e}")
539
- raise HTTPException(status_code=500, detail=str(e))
540
-
541
-
542
- @app.get("/api/crypto/price/{symbol}")
543
- async def get_single_price(symbol: str):
544
- """Get price for a single cryptocurrency"""
545
- try:
546
- # Try Binance first for common pairs
547
- binance_symbol = f"{symbol.upper()}USDT"
548
- ticker, ticker_source = await fetch_binance_ticker(binance_symbol)
549
-
550
- if ticker:
551
- return {
552
- "symbol": symbol.upper(),
553
- "price": ticker,
554
- "source": ticker_source,
555
- "timestamp": datetime.now().isoformat()
556
- }
557
-
558
- # Fallback to CoinGecko
559
- prices, source = await fetch_coingecko_prices([symbol])
560
- if prices:
561
- return {
562
- "symbol": symbol.upper(),
563
- "price": prices[0],
564
- "source": source,
565
- "timestamp": datetime.now().isoformat()
566
- }
567
-
568
- raise HTTPException(status_code=404, detail=f"Price data not found for {symbol}")
569
-
570
- except HTTPException:
571
- raise
572
- except Exception as e:
573
- logger.error(f"Error in get_single_price: {e}")
574
- raise HTTPException(status_code=500, detail=str(e))
575
-
576
-
577
- @app.get("/api/crypto/market-overview")
578
- async def get_market_overview():
579
- """Get comprehensive market overview"""
580
- try:
581
- # Fetch top 20 coins
582
- prices, source = await fetch_coingecko_prices(limit=20)
583
-
584
- if not prices:
585
- raise HTTPException(status_code=503, detail="Unable to fetch market data")
586
-
587
- # Calculate market stats
588
- # Try multiple field names for market cap and volume
589
- total_market_cap = 0
590
- total_volume = 0
591
-
592
- for p in prices:
593
- # Try different field names for market cap
594
- market_cap = (
595
- p.get("market_cap") or
596
- p.get("market_cap_usd") or
597
- p.get("market_cap_rank") or # Sometimes this is the value
598
- None
599
- )
600
- # If market_cap is not found, try calculating from price and supply
601
- if not market_cap:
602
- price = p.get("price") or p.get("current_price") or 0
603
- supply = p.get("circulating_supply") or p.get("total_supply") or 0
604
- if price and supply:
605
- market_cap = float(price) * float(supply)
606
-
607
- if market_cap:
608
- try:
609
- total_market_cap += float(market_cap)
610
- except (TypeError, ValueError):
611
- pass
612
-
613
- # Try different field names for volume
614
- volume = (
615
- p.get("total_volume") or
616
- p.get("volume_24h") or
617
- p.get("volume_24h_usd") or
618
- None
619
- )
620
- if volume:
621
- try:
622
- total_volume += float(volume)
623
- except (TypeError, ValueError):
624
- pass
625
-
626
- logger.info(f"Market overview: {len(prices)} coins, total_market_cap={total_market_cap:,.0f}, total_volume={total_volume:,.0f}")
627
-
628
- # Sort by 24h change
629
- gainers = sorted(
630
- [p for p in prices if p.get("price_change_percentage_24h")],
631
- key=lambda x: x.get("price_change_percentage_24h", 0),
632
- reverse=True
633
- )[:5]
634
-
635
- losers = sorted(
636
- [p for p in prices if p.get("price_change_percentage_24h")],
637
- key=lambda x: x.get("price_change_percentage_24h", 0)
638
- )[:5]
639
-
640
- return {
641
- "total_market_cap": total_market_cap,
642
- "total_volume_24h": total_volume,
643
- "btc_dominance": (prices[0].get("market_cap", 0) / total_market_cap * 100) if total_market_cap > 0 else 0,
644
- "top_gainers": gainers,
645
- "top_losers": losers,
646
- "top_by_volume": sorted(prices, key=lambda x: x.get("total_volume", 0) or 0, reverse=True)[:5],
647
- "timestamp": datetime.now().isoformat(),
648
- "source": source
649
- }
650
-
651
- except HTTPException:
652
- raise
653
- except Exception as e:
654
- logger.error(f"Error in get_market_overview: {e}")
655
- raise HTTPException(status_code=500, detail=str(e))
656
-
657
-
658
- @app.get("/api/market")
659
- async def get_market():
660
- """Get market data in format expected by frontend dashboard"""
661
- try:
662
- overview = await get_market_overview()
663
- prices, source = await fetch_coingecko_prices(limit=50)
664
-
665
- if not prices:
666
- raise HTTPException(status_code=503, detail="Unable to fetch market data")
667
-
668
- return {
669
- "total_market_cap": overview.get("total_market_cap", 0),
670
- "btc_dominance": overview.get("btc_dominance", 0),
671
- "total_volume_24h": overview.get("total_volume_24h", 0),
672
- "cryptocurrencies": prices,
673
- "timestamp": datetime.now().isoformat(),
674
- "source": source
675
- }
676
- except HTTPException:
677
- raise
678
- except Exception as e:
679
- logger.error(f"Error in get_market: {e}")
680
- raise HTTPException(status_code=500, detail=str(e))
681
-
682
-
683
- @app.get("/api/trending")
684
- async def get_trending():
685
- """Get trending cryptocurrencies (top gainers by 24h change)"""
686
- try:
687
- prices, source = await fetch_coingecko_prices(limit=100)
688
-
689
- if not prices:
690
- raise HTTPException(status_code=503, detail="Unable to fetch trending data")
691
-
692
- trending = sorted(
693
- [p for p in prices if p.get("price_change_percentage_24h") is not None],
694
- key=lambda x: x.get("price_change_percentage_24h", 0),
695
- reverse=True
696
- )[:10]
697
-
698
- return {
699
- "trending": trending,
700
- "count": len(trending),
701
- "timestamp": datetime.now().isoformat(),
702
- "source": source
703
- }
704
- except HTTPException:
705
- raise
706
- except Exception as e:
707
- logger.error(f"Error in get_trending: {e}")
708
- raise HTTPException(status_code=500, detail=str(e))
709
-
710
-
711
- @app.get("/api/market/prices")
712
- async def get_multiple_prices(symbols: str = Query("BTC,ETH,SOL", description="Comma-separated symbols")):
713
- """Get prices for multiple cryptocurrencies"""
714
- try:
715
- symbol_list = [s.strip().upper() for s in symbols.split(",")]
716
-
717
- # Fetch prices
718
- prices_data = []
719
- source = "binance"
720
- for symbol in symbol_list:
721
- try:
722
- ticker, ticker_source = await fetch_binance_ticker(f"{symbol}USDT")
723
- if ticker:
724
- prices_data.append(ticker)
725
- if ticker_source != "binance":
726
- source = ticker_source
727
- except:
728
- continue
729
- if not prices_data:
730
- # Fallback to CoinGecko
731
- prices_data, source = await fetch_coingecko_prices(symbol_list)
732
-
733
- if not prices_data:
734
- fallback_prices = local_resource_service.get_prices_for_symbols(symbol_list)
735
- if fallback_prices:
736
- prices_data = fallback_prices
737
- source = "local-fallback"
738
-
739
- return {
740
- "symbols": symbol_list,
741
- "count": len(prices_data),
742
- "data": prices_data,
743
- "source": source,
744
- "timestamp": datetime.now().isoformat()
745
- }
746
-
747
- except Exception as e:
748
- logger.error(f"Error in get_multiple_prices: {e}")
749
- raise HTTPException(status_code=500, detail=str(e))
750
-
751
-
752
- @app.get("/api/market-data/prices")
753
- async def get_market_data_prices(symbols: str = Query("BTC,ETH", description="Comma-separated symbols")):
754
- """Alternative endpoint for market data prices"""
755
- return await get_multiple_prices(symbols)
756
-
757
-
758
- # ============================================================================
759
- # Analysis Endpoints
760
- # ============================================================================
761
-
762
- @app.get("/api/analysis/signals")
763
- async def get_trading_signals(
764
- symbol: str = Query("BTCUSDT", description="Trading pair"),
765
- timeframe: str = Query("1h", description="Timeframe")
766
- ):
767
- """Get trading signals for a symbol"""
768
- try:
769
- # Fetch OHLCV data for analysis
770
- ohlcv = await fetch_binance_ohlcv(symbol, timeframe, 100)
771
-
772
- if not ohlcv:
773
- raise HTTPException(status_code=503, detail="Unable to fetch data for analysis")
774
-
775
- # Simple signal generation (can be enhanced)
776
- latest = ohlcv[-1]
777
- prev = ohlcv[-2] if len(ohlcv) > 1 else latest
778
-
779
- # Calculate simple indicators
780
- close_prices = [c["close"] for c in ohlcv[-20:]]
781
- sma_20 = sum(close_prices) / len(close_prices)
782
-
783
- # Generate signal
784
- trend = "bullish" if latest["close"] > sma_20 else "bearish"
785
- momentum = "strong" if abs(latest["close"] - prev["close"]) / prev["close"] > 0.01 else "weak"
786
-
787
- signal = "buy" if trend == "bullish" and momentum == "strong" else (
788
- "sell" if trend == "bearish" and momentum == "strong" else "hold"
789
- )
790
-
791
- ai_summary = analyze_chart_points(symbol, timeframe, ohlcv)
792
-
793
- return {
794
- "symbol": symbol,
795
- "timeframe": timeframe,
796
- "signal": signal,
797
- "trend": trend,
798
- "momentum": momentum,
799
- "indicators": {
800
- "sma_20": sma_20,
801
- "current_price": latest["close"],
802
- "price_change": latest["close"] - prev["close"],
803
- "price_change_percent": ((latest["close"] - prev["close"]) / prev["close"]) * 100
804
- },
805
- "analysis": ai_summary,
806
- "timestamp": datetime.now().isoformat()
807
- }
808
-
809
- except HTTPException:
810
- raise
811
- except Exception as e:
812
- logger.error(f"Error in get_trading_signals: {e}")
813
- raise HTTPException(status_code=500, detail=str(e))
814
-
815
-
816
- @app.get("/api/analysis/smc")
817
- async def get_smc_analysis(symbol: str = Query("BTCUSDT", description="Trading pair")):
818
- """Get Smart Money Concepts (SMC) analysis"""
819
- try:
820
- # Fetch OHLCV data
821
- ohlcv = await fetch_binance_ohlcv(symbol, "1h", 200)
822
-
823
- if not ohlcv:
824
- raise HTTPException(status_code=503, detail="Unable to fetch data")
825
-
826
- # Calculate key levels
827
- highs = [c["high"] for c in ohlcv]
828
- lows = [c["low"] for c in ohlcv]
829
- closes = [c["close"] for c in ohlcv]
830
-
831
- resistance = max(highs[-50:])
832
- support = min(lows[-50:])
833
- current_price = closes[-1]
834
-
835
- # Structure analysis
836
- market_structure = "higher_highs" if closes[-1] > closes[-10] > closes[-20] else "lower_lows"
837
-
838
- return {
839
- "symbol": symbol,
840
- "market_structure": market_structure,
841
- "key_levels": {
842
- "resistance": resistance,
843
- "support": support,
844
- "current_price": current_price,
845
- "mid_point": (resistance + support) / 2
846
- },
847
- "order_blocks": {
848
- "bullish": support,
849
- "bearish": resistance
850
- },
851
- "liquidity_zones": {
852
- "above": resistance,
853
- "below": support
854
- },
855
- "timestamp": datetime.now().isoformat()
856
- }
857
-
858
- except HTTPException:
859
- raise
860
- except Exception as e:
861
- logger.error(f"Error in get_smc_analysis: {e}")
862
- raise HTTPException(status_code=500, detail=str(e))
863
-
864
-
865
- @app.get("/api/scoring/snapshot")
866
- async def get_scoring_snapshot(symbol: str = Query("BTCUSDT", description="Trading pair")):
867
- """Get comprehensive scoring snapshot"""
868
- try:
869
- # Fetch data
870
- ticker, _ = await fetch_binance_ticker(symbol)
871
- ohlcv = await fetch_binance_ohlcv(symbol, "1h", 100)
872
-
873
- if not ticker or not ohlcv:
874
- raise HTTPException(status_code=503, detail="Unable to fetch data")
875
-
876
- # Calculate scores (0-100)
877
- volatility_score = min(abs(ticker["price_change_percent_24h"]) * 5, 100)
878
- volume_score = min((ticker["volume_24h"] / 1000000) * 10, 100)
879
- trend_score = 50 + (ticker["price_change_percent_24h"] * 2)
880
-
881
- # Overall score
882
- overall_score = (volatility_score + volume_score + trend_score) / 3
883
-
884
- return {
885
- "symbol": symbol,
886
- "overall_score": round(overall_score, 2),
887
- "scores": {
888
- "volatility": round(volatility_score, 2),
889
- "volume": round(volume_score, 2),
890
- "trend": round(trend_score, 2),
891
- "momentum": round(50 + ticker["price_change_percent_24h"], 2)
892
- },
893
- "rating": "excellent" if overall_score > 80 else (
894
- "good" if overall_score > 60 else (
895
- "average" if overall_score > 40 else "poor"
896
- )
897
- ),
898
- "timestamp": datetime.now().isoformat()
899
- }
900
-
901
- except HTTPException:
902
- raise
903
- except Exception as e:
904
- logger.error(f"Error in get_scoring_snapshot: {e}")
905
- raise HTTPException(status_code=500, detail=str(e))
906
-
907
-
908
- @app.get("/api/signals")
909
- async def get_all_signals():
910
- """Get signals for multiple assets"""
911
- symbols = ["BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT"]
912
- signals = []
913
-
914
- for symbol in symbols:
915
- try:
916
- signal_data = await get_trading_signals(symbol, "1h")
917
- signals.append(signal_data)
918
- except:
919
- continue
920
-
921
- return {
922
- "count": len(signals),
923
- "signals": signals,
924
- "timestamp": datetime.now().isoformat()
925
- }
926
-
927
-
928
- @app.get("/api/sentiment")
929
- async def get_sentiment():
930
- """Get market sentiment data"""
931
- try:
932
- news = await news_collector.get_latest_news(limit=5)
933
- except CollectorError as exc:
934
- logger.warning("Sentiment fallback due to news error: %s", exc)
935
- news = []
936
-
937
- text = " ".join(item.get("title", "") for item in news).strip() or "Crypto market update"
938
- analysis = analyze_market_text(text)
939
- score = analysis.get("signals", {}).get("crypto", {}).get("score", 0.0)
940
- normalized_value = int((score + 1) * 50)
941
-
942
- if normalized_value < 20:
943
- classification = "extreme_fear"
944
- elif normalized_value < 40:
945
- classification = "fear"
946
- elif normalized_value < 60:
947
- classification = "neutral"
948
- elif normalized_value < 80:
949
- classification = "greed"
950
- else:
951
- classification = "extreme_greed"
952
-
953
- return {
954
- "value": normalized_value,
955
- "classification": classification,
956
- "description": f"Market sentiment is {classification.replace('_', ' ')}",
957
- "analysis": analysis,
958
- "timestamp": datetime.utcnow().isoformat(),
959
- }
960
-
961
-
962
- # ============================================================================
963
- # System Endpoints
964
- # ============================================================================
965
-
966
- @app.get("/api/system/status")
967
- async def get_system_status():
968
- """Get system status"""
969
- providers = await provider_collector.get_providers_status()
970
- online = sum(1 for provider in providers if provider.get("status") == "online")
971
-
972
- cache_items = (
973
- len(getattr(market_collector.cache, "_store", {}))
974
- + len(getattr(news_collector.cache, "_store", {}))
975
- + len(getattr(provider_collector.cache, "_store", {}))
976
- )
977
-
978
- return {
979
- "status": "operational" if online else "maintenance",
980
- "uptime_seconds": round(time.time() - START_TIME, 2),
981
- "cache_size": cache_items,
982
- "providers_online": online,
983
- "requests_per_minute": 0,
984
- "timestamp": datetime.utcnow().isoformat(),
985
- }
986
-
987
-
988
- @app.get("/api/system/config")
989
- async def get_system_config():
990
- """Get system configuration"""
991
- return {
992
- "version": app.version,
993
- "api_version": "v1",
994
- "cache_ttl_seconds": settings.cache_ttl,
995
- "supported_symbols": sorted(set(COIN_SYMBOL_MAPPING.values())),
996
- "supported_intervals": ["1m", "5m", "15m", "30m", "1h", "4h", "1d"],
997
- "max_ohlcv_limit": 1000,
998
- "timestamp": datetime.utcnow().isoformat(),
999
- }
1000
-
1001
-
1002
- @app.get("/api/categories")
1003
- async def get_categories():
1004
- """Get data categories"""
1005
- return {
1006
- "categories": [
1007
- {"name": "market_data", "endpoints": 5, "status": "active"},
1008
- {"name": "analysis", "endpoints": 4, "status": "active"},
1009
- {"name": "signals", "endpoints": 2, "status": "active"},
1010
- {"name": "sentiment", "endpoints": 1, "status": "active"}
1011
- ]
1012
- }
1013
-
1014
-
1015
- @app.get("/api/rate-limits")
1016
- async def get_rate_limits():
1017
- """Get rate limit information"""
1018
- return {
1019
- "rate_limits": [
1020
- {"endpoint": "/api/ohlcv", "limit": 1200, "window": "per_minute"},
1021
- {"endpoint": "/api/crypto/prices/top", "limit": 600, "window": "per_minute"},
1022
- {"endpoint": "/api/analysis/*", "limit": 300, "window": "per_minute"}
1023
- ],
1024
- "current_usage": {
1025
- "requests_this_minute": 0,
1026
- "percentage": 0
1027
- }
1028
- }
1029
-
1030
-
1031
- @app.get("/api/logs")
1032
- async def get_logs(limit: int = Query(50, ge=1, le=500)):
1033
- """Get recent API logs"""
1034
- # Mock logs (can be enhanced with real logging)
1035
- logs = []
1036
- for i in range(min(limit, 10)):
1037
- logs.append({
1038
- "timestamp": (datetime.now() - timedelta(minutes=i)).isoformat(),
1039
- "endpoint": "/api/ohlcv",
1040
- "status": "success",
1041
- "response_time_ms": random.randint(50, 200)
1042
- })
1043
-
1044
- return {"logs": logs, "count": len(logs)}
1045
-
1046
-
1047
- @app.get("/api/alerts")
1048
- async def get_alerts():
1049
- """Get system alerts"""
1050
- return {
1051
- "alerts": [],
1052
- "count": 0,
1053
- "timestamp": datetime.now().isoformat()
1054
- }
1055
-
1056
-
1057
- # ============================================================================
1058
- # HuggingFace Integration Endpoints
1059
- # ============================================================================
1060
-
1061
- @app.get("/api/hf/health")
1062
- async def hf_health():
1063
- """HuggingFace integration health"""
1064
- from ai_models import AI_MODELS_SUMMARY
1065
- status = registry_status()
1066
- status["models"] = AI_MODELS_SUMMARY
1067
- status["timestamp"] = datetime.utcnow().isoformat()
1068
- return status
1069
-
1070
-
1071
- @app.post("/api/hf/refresh")
1072
- async def hf_refresh():
1073
- """Refresh HuggingFace data"""
1074
- from ai_models import initialize_models
1075
- result = initialize_models()
1076
- return {"status": "ok" if result.get("models_loaded", 0) > 0 else "degraded", **result, "timestamp": datetime.utcnow().isoformat()}
1077
-
1078
-
1079
- @app.get("/api/hf/registry")
1080
- async def hf_registry(kind: str = "models"):
1081
- """Get HuggingFace registry"""
1082
- info = get_model_info()
1083
- return {"kind": kind, "items": info.get("model_names", info)}
1084
-
1085
-
1086
- @app.get("/api/resources/unified")
1087
- async def get_unified_resources():
1088
- """Get unified API resources from crypto_resources_unified_2025-11-11.json"""
1089
- try:
1090
- data = local_resource_service.get_registry()
1091
- if data:
1092
- metadata = data.get("registry", {}).get("metadata", {})
1093
- return {
1094
- "success": True,
1095
- "data": data,
1096
- "metadata": metadata,
1097
- "count": metadata.get("total_entries", 0),
1098
- "fallback_assets": len(local_resource_service.get_supported_symbols())
1099
- }
1100
- return {"success": False, "error": "Resources file not found"}
1101
- except Exception as e:
1102
- logger.error(f"Error loading unified resources: {e}")
1103
- return {"success": False, "error": str(e)}
1104
-
1105
-
1106
- @app.get("/api/resources/ultimate")
1107
- async def get_ultimate_resources():
1108
- """Get ultimate API resources from ultimate_crypto_pipeline_2025_NZasinich.json"""
1109
- try:
1110
- resources_path = WORKSPACE_ROOT / "api-resources" / "ultimate_crypto_pipeline_2025_NZasinich.json"
1111
- if resources_path.exists():
1112
- with open(resources_path, 'r', encoding='utf-8') as f:
1113
- data = json.load(f)
1114
- return {
1115
- "success": True,
1116
- "data": data,
1117
- "total_sources": data.get("total_sources", 0),
1118
- "files": len(data.get("files", []))
1119
- }
1120
- return {"success": False, "error": "Resources file not found"}
1121
- except Exception as e:
1122
- logger.error(f"Error loading ultimate resources: {e}")
1123
- return {"success": False, "error": str(e)}
1124
-
1125
-
1126
- @app.get("/api/resources/stats")
1127
- async def get_resources_stats():
1128
- """Get statistics about available API resources"""
1129
- try:
1130
- stats = {
1131
- "unified": {"available": False, "count": 0},
1132
- "ultimate": {"available": False, "count": 0},
1133
- "total_apis": 0
1134
- }
1135
-
1136
- # Check unified resources via the centralized loader
1137
- registry = local_resource_service.get_registry()
1138
- if registry:
1139
- stats["unified"] = {
1140
- "available": True,
1141
- "count": registry.get("registry", {}).get("metadata", {}).get("total_entries", 0),
1142
- "fallback_assets": len(local_resource_service.get_supported_symbols())
1143
- }
1144
-
1145
- # Check ultimate resources
1146
- ultimate_path = WORKSPACE_ROOT / "api-resources" / "ultimate_crypto_pipeline_2025_NZasinich.json"
1147
- if ultimate_path.exists():
1148
- with open(ultimate_path, 'r', encoding='utf-8') as f:
1149
- ultimate_data = json.load(f)
1150
- stats["ultimate"] = {
1151
- "available": True,
1152
- "count": ultimate_data.get("total_sources", 0)
1153
- }
1154
-
1155
- stats["total_apis"] = stats["unified"].get("count", 0) + stats["ultimate"].get("count", 0)
1156
-
1157
- return {"success": True, "stats": stats}
1158
- except Exception as e:
1159
- logger.error(f"Error getting resources stats: {e}")
1160
- return {"success": False, "error": str(e)}
1161
-
1162
-
1163
- def _resolve_sentiment_payload(payload: Union[List[str], Dict[str, Any]]) -> Dict[str, Any]:
1164
- if isinstance(payload, list):
1165
- return {"texts": payload, "mode": "auto"}
1166
- if isinstance(payload, dict):
1167
- texts = payload.get("texts") or payload.get("text")
1168
- if isinstance(texts, str):
1169
- texts = [texts]
1170
- if not isinstance(texts, list):
1171
- raise ValueError("texts must be provided")
1172
- mode = payload.get("mode") or payload.get("model") or "auto"
1173
- return {"texts": texts, "mode": mode}
1174
- raise ValueError("Invalid payload")
1175
-
1176
-
1177
- @app.post("/api/hf/run-sentiment")
1178
- @app.post("/api/hf/sentiment")
1179
- async def hf_sentiment(payload: Union[List[str], Dict[str, Any]] = Body(...)):
1180
- """Run sentiment analysis using shared AI helpers."""
1181
- from ai_models import AI_MODELS_SUMMARY
1182
-
1183
- if AI_MODELS_SUMMARY.get("models_loaded", 0) == 0 or AI_MODELS_SUMMARY.get("mode") == "off":
1184
- return {
1185
- "ok": False,
1186
- "error": "No HF models are currently loaded.",
1187
- "mode": AI_MODELS_SUMMARY.get("mode", "off"),
1188
- "models_loaded": AI_MODELS_SUMMARY.get("models_loaded", 0)
1189
- }
1190
-
1191
- try:
1192
- resolved = _resolve_sentiment_payload(payload)
1193
- except ValueError as exc:
1194
- raise HTTPException(status_code=400, detail=str(exc)) from exc
1195
-
1196
- mode = (resolved.get("mode") or "auto").lower()
1197
- texts = resolved["texts"]
1198
- results: List[Dict[str, Any]] = []
1199
- for text in texts:
1200
- if mode == "crypto":
1201
- analysis = analyze_crypto_sentiment(text)
1202
- elif mode == "financial":
1203
- analysis = analyze_market_text(text).get("signals", {}).get("financial", {})
1204
- elif mode == "social":
1205
- analysis = analyze_market_text(text).get("signals", {}).get("social", {})
1206
- else:
1207
- analysis = analyze_market_text(text)
1208
- results.append({"text": text, "result": analysis})
1209
-
1210
- return {"mode": mode, "results": results, "timestamp": datetime.utcnow().isoformat()}
1211
-
1212
-
1213
- @app.post("/api/hf/models/sentiment")
1214
- async def hf_models_sentiment(payload: Union[List[str], Dict[str, Any]] = Body(...)):
1215
- """Compatibility endpoint for HF console sentiment panel."""
1216
- from ai_models import AI_MODELS_SUMMARY
1217
-
1218
- if AI_MODELS_SUMMARY.get("models_loaded", 0) == 0 or AI_MODELS_SUMMARY.get("mode") == "off":
1219
- return {
1220
- "ok": False,
1221
- "error": "No HF models are currently loaded.",
1222
- "mode": AI_MODELS_SUMMARY.get("mode", "off"),
1223
- "models_loaded": AI_MODELS_SUMMARY.get("models_loaded", 0)
1224
- }
1225
-
1226
- return await hf_sentiment(payload)
1227
-
1228
-
1229
- @app.post("/api/hf/models/forecast")
1230
- async def hf_models_forecast(payload: Dict[str, Any] = Body(...)):
1231
- """Generate quick technical forecasts from provided closing prices."""
1232
- series = payload.get("series") or payload.get("values") or payload.get("close")
1233
- if not isinstance(series, list) or len(series) < 3:
1234
- raise HTTPException(status_code=400, detail="Provide at least 3 closing prices in 'series'.")
1235
-
1236
- try:
1237
- floats = [float(x) for x in series]
1238
- except (TypeError, ValueError) as exc:
1239
- raise HTTPException(status_code=400, detail="Series must contain numeric values") from exc
1240
-
1241
- model_name = (payload.get("model") or payload.get("model_name") or "btc_lstm").lower()
1242
- steps = int(payload.get("steps") or 3)
1243
-
1244
- deltas = [floats[i] - floats[i - 1] for i in range(1, len(floats))]
1245
- avg_delta = mean(deltas)
1246
- volatility = mean(abs(delta - avg_delta) for delta in deltas) if deltas else 0
1247
-
1248
- predictions = []
1249
- last = floats[-1]
1250
- decay = 0.95 if model_name == "btc_arima" else 1.02
1251
- for _ in range(steps):
1252
- last = last + (avg_delta * decay)
1253
- predictions.append(round(last, 4))
1254
-
1255
- return {
1256
- "model": model_name,
1257
- "steps": steps,
1258
- "input_count": len(floats),
1259
- "volatility": round(volatility, 5),
1260
- "predictions": predictions,
1261
- "source": "local-fallback" if model_name == "btc_arima" else "hybrid",
1262
- "timestamp": datetime.utcnow().isoformat()
1263
- }
1264
-
1265
-
1266
- @app.get("/api/hf/datasets/market/ohlcv")
1267
- async def hf_dataset_market_ohlcv(symbol: str = Query("BTC"), interval: str = Query("1h"), limit: int = Query(120, ge=10, le=500)):
1268
- """Expose fallback OHLCV snapshots as a pseudo HF dataset slice."""
1269
- data = local_resource_service.get_ohlcv(symbol.upper(), interval, limit)
1270
- source = "local-fallback"
1271
-
1272
- if not data:
1273
- return {
1274
- "symbol": symbol.upper(),
1275
- "interval": interval,
1276
- "count": 0,
1277
- "data": [],
1278
- "source": source,
1279
- "message": "No cached OHLCV available yet"
1280
- }
1281
-
1282
- return {
1283
- "symbol": symbol.upper(),
1284
- "interval": interval,
1285
- "count": len(data),
1286
- "data": data,
1287
- "source": source,
1288
- "timestamp": datetime.utcnow().isoformat()
1289
- }
1290
-
1291
-
1292
- @app.get("/api/hf/datasets/market/btc_technical")
1293
- async def hf_dataset_market_btc(limit: int = Query(50, ge=10, le=200)):
1294
- """Simplified technical metrics derived from fallback OHLCV data."""
1295
- candles = local_resource_service.get_ohlcv("BTC", "1h", limit + 20)
1296
-
1297
- if not candles:
1298
- raise HTTPException(status_code=503, detail="Fallback OHLCV unavailable")
1299
-
1300
- rows = []
1301
- closes = [c["close"] for c in candles]
1302
- for idx, candle in enumerate(candles[-limit:]):
1303
- window = closes[max(0, idx): idx + 20]
1304
- sma = sum(window) / len(window) if window else candle["close"]
1305
- momentum = candle["close"] - candle["open"]
1306
- rows.append({
1307
- "timestamp": candle["timestamp"],
1308
- "datetime": candle["datetime"],
1309
- "close": candle["close"],
1310
- "sma_20": round(sma, 4),
1311
- "momentum": round(momentum, 4),
1312
- "volatility": round((candle["high"] - candle["low"]) / candle["low"], 4)
1313
- })
1314
-
1315
- return {
1316
- "symbol": "BTC",
1317
- "interval": "1h",
1318
- "count": len(rows),
1319
- "items": rows,
1320
- "source": "local-fallback"
1321
- }
1322
-
1323
-
1324
- @app.get("/api/hf/datasets/news/semantic")
1325
- async def hf_dataset_news(limit: int = Query(10, ge=3, le=25)):
1326
- """News slice augmented with sentiment tags for HF demos."""
1327
- try:
1328
- news = await news_collector.get_latest_news(limit=limit)
1329
- source = "providers"
1330
- except CollectorError:
1331
- news = []
1332
- source = "local-fallback"
1333
-
1334
- if not news:
1335
- items = HF_SAMPLE_NEWS[:limit]
1336
- else:
1337
- items = []
1338
- for item in news:
1339
- items.append({
1340
- "title": item.get("title"),
1341
- "source": item.get("source") or item.get("provider"),
1342
- "sentiment": item.get("sentiment") or "neutral",
1343
- "sentiment_score": item.get("sentiment_confidence", 0.5),
1344
- "entities": item.get("symbols") or [],
1345
- "summary": item.get("summary") or item.get("description"),
1346
- "published_at": item.get("date") or item.get("published_at")
1347
- })
1348
- return {
1349
- "count": len(items),
1350
- "items": items,
1351
- "source": source,
1352
- "timestamp": datetime.utcnow().isoformat()
1353
- }
1354
-
1355
-
1356
- # ============================================================================
1357
- # HTML Routes - Serve UI files
1358
- # ============================================================================
1359
-
1360
- @app.get("/favicon.ico")
1361
- async def favicon():
1362
- """Serve favicon"""
1363
- favicon_path = WORKSPACE_ROOT / "static" / "favicon.ico"
1364
- if favicon_path.exists():
1365
- return FileResponse(favicon_path)
1366
- return JSONResponse({"status": "no favicon"}, status_code=404)
1367
-
1368
- @app.get("/", response_class=HTMLResponse)
1369
- async def root():
1370
- """Serve main HTML UI page (index.html)"""
1371
- index_path = WORKSPACE_ROOT / "index.html"
1372
- if index_path.exists():
1373
- return FileResponse(
1374
- path=str(index_path),
1375
- media_type="text/html",
1376
- filename="index.html"
1377
- )
1378
- return HTMLResponse("<h1>Cryptocurrency Data & Analysis API</h1><p>See <a href='/docs'>/docs</a> for API documentation</p>")
1379
-
1380
- @app.get("/index.html", response_class=HTMLResponse)
1381
- async def index():
1382
- """Serve index.html"""
1383
- return FileResponse(WORKSPACE_ROOT / "index.html")
1384
-
1385
- @app.get("/dashboard.html", response_class=HTMLResponse)
1386
- async def dashboard():
1387
- """Serve dashboard.html"""
1388
- return FileResponse(WORKSPACE_ROOT / "dashboard.html")
1389
-
1390
- @app.get("/dashboard", response_class=HTMLResponse)
1391
- async def dashboard_alt():
1392
- """Alternative route for dashboard"""
1393
- return FileResponse(WORKSPACE_ROOT / "dashboard.html")
1394
-
1395
- @app.get("/admin.html", response_class=HTMLResponse)
1396
- async def admin():
1397
- """Serve admin panel"""
1398
- admin_path = WORKSPACE_ROOT / "admin.html"
1399
- if admin_path.exists():
1400
- return FileResponse(
1401
- path=str(admin_path),
1402
- media_type="text/html",
1403
- filename="admin.html"
1404
- )
1405
- return HTMLResponse("<h1>Admin panel not found</h1>")
1406
-
1407
- @app.get("/admin", response_class=HTMLResponse)
1408
- async def admin_alt():
1409
- """Alternative route for admin"""
1410
- admin_path = WORKSPACE_ROOT / "admin.html"
1411
- if admin_path.exists():
1412
- return FileResponse(
1413
- path=str(admin_path),
1414
- media_type="text/html",
1415
- filename="admin.html"
1416
- )
1417
- return HTMLResponse("<h1>Admin panel not found</h1>")
1418
-
1419
- @app.get("/hf_console.html", response_class=HTMLResponse)
1420
- async def hf_console():
1421
- """Serve HuggingFace console"""
1422
- return FileResponse(WORKSPACE_ROOT / "hf_console.html")
1423
-
1424
- @app.get("/console", response_class=HTMLResponse)
1425
- async def console_alt():
1426
- """Alternative route for HF console"""
1427
- return FileResponse(WORKSPACE_ROOT / "hf_console.html")
1428
-
1429
- @app.get("/pool_management.html", response_class=HTMLResponse)
1430
- async def pool_management():
1431
- """Serve pool management UI"""
1432
- return FileResponse(WORKSPACE_ROOT / "pool_management.html")
1433
-
1434
- @app.get("/unified_dashboard.html", response_class=HTMLResponse)
1435
- async def unified_dashboard():
1436
- """Serve unified dashboard"""
1437
- return FileResponse(WORKSPACE_ROOT / "unified_dashboard.html")
1438
-
1439
- @app.get("/simple_overview.html", response_class=HTMLResponse)
1440
- async def simple_overview():
1441
- """Serve simple overview"""
1442
- return FileResponse(WORKSPACE_ROOT / "simple_overview.html")
1443
-
1444
- # Generic HTML file handler
1445
- @app.get("/{filename}.html", response_class=HTMLResponse)
1446
- async def serve_html(filename: str):
1447
- """Serve any HTML file from workspace root"""
1448
- file_path = WORKSPACE_ROOT / f"{filename}.html"
1449
- if file_path.exists():
1450
- return FileResponse(file_path)
1451
- return HTMLResponse(f"<h1>File {filename}.html not found</h1>", status_code=404)
1452
-
1453
-
1454
- # ============================================================================
1455
- # Startup Event
1456
- # ============================================================================
1457
-
1458
-
1459
- # ============================================================================
1460
- # ADMIN DASHBOARD ENDPOINTS
1461
- # ============================================================================
1462
-
1463
- from fastapi import WebSocket, WebSocketDisconnect
1464
- import asyncio
1465
-
1466
- class ConnectionManager:
1467
- def __init__(self):
1468
- self.active_connections = []
1469
- async def connect(self, websocket: WebSocket):
1470
- await websocket.accept()
1471
- self.active_connections.append(websocket)
1472
- def disconnect(self, websocket: WebSocket):
1473
- if websocket in self.active_connections:
1474
- self.active_connections.remove(websocket)
1475
- async def broadcast(self, message: dict):
1476
- disconnected = []
1477
- for conn in list(self.active_connections):
1478
- try:
1479
- # Check connection state before sending
1480
- if conn.client_state == WebSocketState.CONNECTED:
1481
- await conn.send_json(message)
1482
- else:
1483
- disconnected.append(conn)
1484
- except Exception as e:
1485
- logger.debug(f"Error broadcasting to client: {e}")
1486
- disconnected.append(conn)
1487
-
1488
- # Clean up disconnected clients
1489
- for conn in disconnected:
1490
- self.disconnect(conn)
1491
-
1492
- ws_manager = ConnectionManager()
1493
-
1494
- @app.get("/api/health")
1495
- async def api_health():
1496
- h = await health()
1497
- return {"status": "healthy" if h.get("status") == "ok" else "degraded", **h}
1498
-
1499
- # Removed duplicate - using improved version below
1500
-
1501
- @app.get("/api/coins/{symbol}")
1502
- async def get_coin_detail(symbol: str):
1503
- coins = await market_collector.get_top_coins(limit=250)
1504
- coin = next((c for c in coins if c.get("symbol", "").upper() == symbol.upper()), None)
1505
- if not coin:
1506
- raise HTTPException(404, f"Coin {symbol} not found")
1507
- return {"success": True, "symbol": symbol.upper(), "name": coin.get("name", ""),
1508
- "price": coin.get("price") or coin.get("current_price", 0),
1509
- "change_24h": coin.get("change_24h") or coin.get("price_change_percentage_24h", 0),
1510
- "market_cap": coin.get("market_cap", 0)}
1511
-
1512
- @app.get("/api/market/stats")
1513
- async def get_market_stats():
1514
- """Get global market statistics (duplicate endpoint - keeping for compatibility)"""
1515
- try:
1516
- overview = await get_market_overview()
1517
-
1518
- # Calculate ETH dominance from prices if available
1519
- eth_dominance = 0
1520
- if overview.get("total_market_cap", 0) > 0:
1521
- try:
1522
- eth_prices, _ = await fetch_coingecko_prices(symbols=["ETH"], limit=1)
1523
- if eth_prices and len(eth_prices) > 0:
1524
- eth_market_cap = eth_prices[0].get("market_cap", 0) or 0
1525
- eth_dominance = (eth_market_cap / overview.get("total_market_cap", 1)) * 100
1526
- except:
1527
- pass
1528
-
1529
- return {
1530
- "success": True,
1531
- "stats": {
1532
- "total_market_cap": overview.get("total_market_cap", 0) or 0,
1533
- "total_volume_24h": overview.get("total_volume_24h", 0) or 0,
1534
- "btc_dominance": overview.get("btc_dominance", 0) or 0,
1535
- "eth_dominance": eth_dominance,
1536
- "active_cryptocurrencies": 10000,
1537
- "markets": 500,
1538
- "market_cap_change_24h": 0.0,
1539
- "timestamp": datetime.now().isoformat()
1540
- }
1541
- }
1542
- except Exception as e:
1543
- logger.error(f"Error in /api/market/stats (duplicate): {e}")
1544
- return {
1545
- "success": True,
1546
- "stats": {
1547
- "total_market_cap": 0,
1548
- "total_volume_24h": 0,
1549
- "btc_dominance": 0,
1550
- "eth_dominance": 0,
1551
- "active_cryptocurrencies": 0,
1552
- "markets": 0,
1553
- "market_cap_change_24h": 0.0,
1554
- "timestamp": datetime.now().isoformat()
1555
- }
1556
- }
1557
-
1558
-
1559
- @app.get("/api/stats")
1560
- async def get_stats_alias():
1561
- """Alias endpoint for /api/market/stats - backward compatibility"""
1562
- return await get_market_stats()
1563
-
1564
-
1565
- @app.get("/api/news/latest")
1566
- async def get_latest_news(limit: int = Query(default=40, ge=1, le=100)):
1567
- from ai_models import analyze_news_item
1568
- news = await news_collector.get_latest_news(limit=limit)
1569
- enriched = []
1570
- for item in news[:limit]:
1571
- try:
1572
- e = analyze_news_item(item)
1573
- enriched.append({"title": e.get("title", ""), "source": e.get("source", ""),
1574
- "published_at": e.get("published_at") or e.get("date", ""),
1575
- "symbols": e.get("symbols", []), "sentiment": e.get("sentiment", "neutral"),
1576
- "sentiment_confidence": e.get("sentiment_confidence", 0.5)})
1577
- except:
1578
- enriched.append({"title": item.get("title", ""), "source": item.get("source", ""),
1579
- "published_at": item.get("date", ""), "symbols": item.get("symbols", []),
1580
- "sentiment": "neutral", "sentiment_confidence": 0.5})
1581
- return {"success": True, "news": enriched, "count": len(enriched)}
1582
-
1583
- @app.post("/api/news/summarize")
1584
- async def summarize_news(item: Dict[str, Any] = Body(...)):
1585
- from ai_models import analyze_news_item
1586
- e = analyze_news_item(item)
1587
- return {"success": True, "summary": e.get("title", ""), "sentiment": e.get("sentiment", "neutral")}
1588
-
1589
- # Duplicate endpoints removed - using the improved versions below in CHARTS ENDPOINTS section
1590
-
1591
- @app.post("/api/sentiment/analyze")
1592
- async def analyze_sentiment(payload: Dict[str, Any] = Body(...)):
1593
- from ai_models import ensemble_crypto_sentiment
1594
- result = ensemble_crypto_sentiment(payload.get("text", ""))
1595
- return {"success": True, "sentiment": result["label"], "confidence": result["confidence"], "details": result}
1596
-
1597
- @app.post("/api/query")
1598
- async def process_query(payload: Dict[str, Any] = Body(...)):
1599
- query = payload.get("query", "").lower()
1600
- if "price" in query or "btc" in query:
1601
- coins = await market_collector.get_top_coins(limit=10)
1602
- btc = next((c for c in coins if c.get("symbol", "").upper() == "BTC"), None)
1603
- if btc:
1604
- return {"success": True, "type": "price", "message": f"Bitcoin is ${btc.get('price', 0):,.2f}", "data": btc}
1605
- return {"success": True, "type": "general", "message": "Query processed"}
1606
-
1607
- @app.get("/api/datasets/list")
1608
- async def list_datasets():
1609
- from backend.services.hf_registry import REGISTRY
1610
- datasets = REGISTRY.list(kind="datasets")
1611
- formatted = [{"name": d.get("id"), "category": d.get("category", "other"), "tags": d.get("tags", [])} for d in datasets]
1612
- return {"success": True, "datasets": formatted, "count": len(formatted)}
1613
-
1614
- @app.get("/api/datasets/sample")
1615
- async def get_dataset_sample(name: str = Query(...), limit: int = Query(default=20)):
1616
- return {"success": False, "name": name, "sample": [], "message": "Auth required"}
1617
-
1618
- @app.get("/api/models/list")
1619
- async def list_models():
1620
- from ai_models import get_model_info
1621
- info = get_model_info()
1622
- models = []
1623
- for cat, mlist in info.get("model_catalog", {}).items():
1624
- for mid in mlist:
1625
- models.append({"name": mid, "task": "sentiment" if "sentiment" in cat else "analysis", "category": cat})
1626
- return {"success": True, "models": models, "count": len(models)}
1627
-
1628
- @app.post("/api/models/test")
1629
- async def test_model(payload: Dict[str, Any] = Body(...)):
1630
- from ai_models import ensemble_crypto_sentiment
1631
- result = ensemble_crypto_sentiment(payload.get("text", ""))
1632
- return {"success": True, "model": payload.get("model", ""), "result": result}
1633
-
1634
- @app.websocket("/ws")
1635
- async def websocket_endpoint(websocket: WebSocket):
1636
- await ws_manager.connect(websocket)
1637
- try:
1638
- while True:
1639
- # Check if connection is still open before sending
1640
- if websocket.client_state != WebSocketState.CONNECTED:
1641
- logger.info("WebSocket connection closed, breaking loop")
1642
- break
1643
-
1644
- try:
1645
- top_coins = await market_collector.get_top_coins(limit=5)
1646
- news = await news_collector.get_latest_news(limit=3)
1647
- from ai_models import ensemble_crypto_sentiment
1648
- sentiment = ensemble_crypto_sentiment(" ".join([n.get("title", "") for n in news])) if news else {"label": "neutral", "confidence": 0.5}
1649
-
1650
- # Double-check connection state before sending
1651
- if websocket.client_state == WebSocketState.CONNECTED:
1652
- await websocket.send_json({
1653
- "type": "update",
1654
- "payload": {
1655
- "market_data": top_coins,
1656
- "news": news,
1657
- "sentiment": sentiment,
1658
- "timestamp": datetime.now().isoformat()
1659
- }
1660
- })
1661
- else:
1662
- logger.info("WebSocket disconnected, breaking loop")
1663
- break
1664
-
1665
- except CollectorError as e:
1666
- # Provider errors are already logged by the collector, just continue
1667
- logger.debug(f"Provider error in WebSocket update (this is expected with fallbacks): {e}")
1668
- # Use cached data if available, or empty data
1669
- top_coins = []
1670
- news = []
1671
- sentiment = {"label": "neutral", "confidence": 0.5}
1672
- except Exception as e:
1673
- # Log other errors with full details
1674
- error_msg = str(e) if str(e) else repr(e)
1675
- logger.error(f"Error in WebSocket update loop: {type(e).__name__}: {error_msg}")
1676
- # Don't break on data errors, just log and continue
1677
- # Only break on connection errors
1678
- if "send" in str(e).lower() or "close" in str(e).lower():
1679
- break
1680
-
1681
- await asyncio.sleep(10)
1682
- except WebSocketDisconnect:
1683
- logger.info("WebSocket disconnect exception caught")
1684
- except Exception as e:
1685
- logger.error(f"WebSocket endpoint error: {e}")
1686
- finally:
1687
- try:
1688
- ws_manager.disconnect(websocket)
1689
- except:
1690
- pass
1691
-
1692
-
1693
- @app.on_event("startup")
1694
- async def startup_event():
1695
- """Initialize on startup - non-blocking"""
1696
- logger.info("=" * 70)
1697
- logger.info("Starting Cryptocurrency Data & Analysis API")
1698
- logger.info("=" * 70)
1699
- logger.info("FastAPI initialized")
1700
- logger.info("CORS configured")
1701
- logger.info("Cache initialized")
1702
- logger.info(f"Providers loaded: {len(PROVIDERS_CONFIG)}")
1703
-
1704
- # Initialize AI models in background (non-blocking)
1705
- async def init_models_background():
1706
- try:
1707
- from ai_models import initialize_models
1708
- models_init = initialize_models()
1709
- logger.info(f"AI Models initialized: {models_init}")
1710
- except Exception as e:
1711
- logger.warning(f"AI Models initialization failed: {e}")
1712
-
1713
- # Initialize HF Registry in background (non-blocking)
1714
- async def init_registry_background():
1715
- try:
1716
- from backend.services.hf_registry import REGISTRY
1717
- registry_result = await REGISTRY.refresh()
1718
- logger.info(f"HF Registry initialized: {registry_result}")
1719
- except Exception as e:
1720
- logger.warning(f"HF Registry initialization failed: {e}")
1721
-
1722
- # Start background tasks
1723
- asyncio.create_task(init_models_background())
1724
- asyncio.create_task(init_registry_background())
1725
- logger.info("Background initialization tasks started")
1726
-
1727
- # Show loaded HuggingFace Space providers
1728
- hf_providers = [p for p in PROVIDERS_CONFIG.keys() if 'huggingface_space' in p]
1729
- if hf_providers:
1730
- logger.info(f"HuggingFace Space providers: {', '.join(hf_providers)}")
1731
-
1732
- logger.info("Data sources: Binance, CoinGecko, providers_config_extended.json")
1733
-
1734
- # Check HTML files
1735
- html_files = ["index.html", "dashboard.html", "admin.html", "hf_console.html"]
1736
- available_html = [f for f in html_files if (WORKSPACE_ROOT / f).exists()]
1737
- logger.info(f"UI files: {len(available_html)}/{len(html_files)} available")
1738
- logger.info(f"HTML UI available at: http://0.0.0.0:7860/ (index.html)")
1739
-
1740
- logger.info("=" * 70)
1741
- logger.info("API ready at http://0.0.0.0:7860")
1742
- logger.info("Docs at http://0.0.0.0:7860/docs")
1743
- logger.info("UI at http://0.0.0.0:7860/ (index.html - default HTML page)")
1744
- logger.info("=" * 70)
1745
-
1746
-
1747
- # ============================================================================
1748
- # Main Entry Point
1749
- # ============================================================================
1750
-
1751
- if __name__ == "__main__":
1752
- import uvicorn
1753
- import sys
1754
- import io
1755
-
1756
- # Fix encoding for Windows console
1757
- if sys.platform == "win32":
1758
- sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
1759
- sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
1760
-
1761
- try:
1762
- print("=" * 70)
1763
- print("Starting Cryptocurrency Data & Analysis API")
1764
- print("=" * 70)
1765
- print("Server: http://localhost:7860")
1766
- print("API Docs: http://localhost:7860/docs")
1767
- print("Health: http://localhost:7860/health")
1768
- print("=" * 70)
1769
- except UnicodeEncodeError:
1770
- # Fallback if encoding still fails
1771
- print("=" * 70)
1772
- print("Starting Cryptocurrency Data & Analysis API")
1773
- print("=" * 70)
1774
- print("Server: http://localhost:7860")
1775
- print("API Docs: http://localhost:7860/docs")
1776
- print("Health: http://localhost:7860/health")
1777
- print("=" * 70)
1778
-
1779
- uvicorn.run(
1780
- app,
1781
- host="0.0.0.0",
1782
- port=7860,
1783
- log_level="info"
1784
- )
1785
- # NEW ENDPOINTS FOR ADMIN.HTML - ADD TO hf_unified_server.py
1786
-
1787
- from fastapi import WebSocket, WebSocketDisconnect
1788
- from collections import defaultdict
1789
-
1790
- # WebSocket Manager
1791
- class ConnectionManager:
1792
- def __init__(self):
1793
- self.active_connections: List[WebSocket] = []
1794
-
1795
- async def connect(self, websocket: WebSocket):
1796
- await websocket.accept()
1797
- self.active_connections.append(websocket)
1798
- logger.info(f"WebSocket connected. Total: {len(self.active_connections)}")
1799
-
1800
- def disconnect(self, websocket: WebSocket):
1801
- if websocket in self.active_connections:
1802
- self.active_connections.remove(websocket)
1803
- logger.info(f"WebSocket disconnected. Total: {len(self.active_connections)}")
1804
-
1805
- async def broadcast(self, message: dict):
1806
- disconnected = []
1807
- for connection in list(self.active_connections):
1808
- try:
1809
- # Check connection state before sending
1810
- if connection.client_state == WebSocketState.CONNECTED:
1811
- await connection.send_json(message)
1812
- else:
1813
- disconnected.append(connection)
1814
- except Exception as e:
1815
- logger.debug(f"Error broadcasting to client: {e}")
1816
- disconnected.append(connection)
1817
-
1818
- # Clean up disconnected clients
1819
- for connection in disconnected:
1820
- self.disconnect(connection)
1821
-
1822
- ws_manager = ConnectionManager()
1823
-
1824
-
1825
- # ===== API HEALTH =====
1826
- @app.get("/api/health")
1827
- async def api_health():
1828
- """Health check for admin dashboard"""
1829
- health_data = await health()
1830
- return {
1831
- "status": "healthy" if health_data.get("status") == "ok" else "degraded",
1832
- **health_data
1833
- }
1834
-
1835
-
1836
- # ===== COINS ENDPOINTS =====
1837
- @app.get("/api/coins/top")
1838
- async def get_top_coins(limit: int = Query(default=10, ge=1, le=100)):
1839
- """Get top cryptocurrencies by market cap"""
1840
- try:
1841
- coins = await market_collector.get_top_coins(limit=limit)
1842
-
1843
- result = []
1844
- for coin in coins:
1845
- result.append({
1846
- "id": coin.get("id", coin.get("symbol", "").lower()),
1847
- "rank": coin.get("rank", 0),
1848
- "symbol": coin.get("symbol", "").upper(),
1849
- "name": coin.get("name", ""),
1850
- "price": coin.get("price") or coin.get("current_price", 0),
1851
- "current_price": coin.get("price") or coin.get("current_price", 0),
1852
- "price_change_24h": coin.get("change_24h") or coin.get("price_change_percentage_24h", 0),
1853
- "price_change_percentage_24h": coin.get("change_24h") or coin.get("price_change_percentage_24h", 0),
1854
- "price_change_percentage_7d_in_currency": coin.get("price_change_percentage_7d", 0),
1855
- "volume_24h": coin.get("volume_24h") or coin.get("total_volume", 0),
1856
- "total_volume": coin.get("volume_24h") or coin.get("total_volume", 0),
1857
- "market_cap": coin.get("market_cap", 0),
1858
- "image": coin.get("image", ""),
1859
- "sparkline_in_7d": coin.get("sparkline_in_7d") or {"price": []},
1860
- "sparkline_data": coin.get("sparkline_data") or [],
1861
- "last_updated": coin.get("last_updated", datetime.now().isoformat())
1862
- })
1863
-
1864
- return {
1865
- "success": True,
1866
- "coins": result,
1867
- "count": len(result),
1868
- "timestamp": datetime.now().isoformat()
1869
- }
1870
- except Exception as e:
1871
- logger.error(f"Error in /api/coins/top: {e}")
1872
- raise HTTPException(status_code=503, detail=str(e))
1873
-
1874
-
1875
- @app.get("/api/coins/{symbol}")
1876
- async def get_coin_detail(symbol: str):
1877
- """Get specific coin details"""
1878
- try:
1879
- coins = await market_collector.get_top_coins(limit=250)
1880
- coin = next((c for c in coins if c.get("symbol", "").upper() == symbol.upper()), None)
1881
-
1882
- if not coin:
1883
- raise HTTPException(status_code=404, detail=f"Coin {symbol} not found")
1884
-
1885
- return {
1886
- "success": True,
1887
- "symbol": symbol.upper(),
1888
- "name": coin.get("name", ""),
1889
- "price": coin.get("price") or coin.get("current_price", 0),
1890
- "change_24h": coin.get("change_24h") or coin.get("price_change_percentage_24h", 0),
1891
- "volume_24h": coin.get("volume_24h") or coin.get("total_volume", 0),
1892
- "market_cap": coin.get("market_cap", 0),
1893
- "rank": coin.get("rank", 0),
1894
- "last_updated": coin.get("last_updated", datetime.now().isoformat())
1895
- }
1896
- except HTTPException:
1897
- raise
1898
- except Exception as e:
1899
- logger.error(f"Error in /api/coins/{symbol}: {e}")
1900
- raise HTTPException(status_code=503, detail=str(e))
1901
-
1902
-
1903
- # ===== MARKET STATS =====
1904
- @app.get("/api/market/stats")
1905
- async def get_market_stats():
1906
- """Get global market statistics"""
1907
- try:
1908
- # Use existing endpoint - get_market_overview returns total_market_cap and total_volume_24h
1909
- overview = await get_market_overview()
1910
-
1911
- # Calculate ETH dominance from prices if available
1912
- eth_dominance = 0
1913
- if overview.get("total_market_cap", 0) > 0:
1914
- # Try to get ETH market cap from top coins
1915
- try:
1916
- eth_prices, _ = await fetch_coingecko_prices(symbols=["ETH"], limit=1)
1917
- if eth_prices and len(eth_prices) > 0:
1918
- eth_market_cap = eth_prices[0].get("market_cap", 0) or 0
1919
- eth_dominance = (eth_market_cap / overview.get("total_market_cap", 1)) * 100
1920
- except:
1921
- pass
1922
-
1923
- stats = {
1924
- "total_market_cap": overview.get("total_market_cap", 0) or 0,
1925
- "total_volume_24h": overview.get("total_volume_24h", 0) or 0,
1926
- "btc_dominance": overview.get("btc_dominance", 0) or 0,
1927
- "eth_dominance": eth_dominance,
1928
- "active_cryptocurrencies": 10000, # Approximate
1929
- "markets": 500, # Approximate
1930
- "market_cap_change_24h": 0.0,
1931
- "timestamp": datetime.now().isoformat()
1932
- }
1933
-
1934
- return {"success": True, "stats": stats}
1935
- except Exception as e:
1936
- logger.error(f"Error in /api/market/stats: {e}")
1937
- raise HTTPException(status_code=503, detail=str(e))
1938
-
1939
-
1940
- # ===== NEWS ENDPOINTS =====
1941
- @app.get("/api/news/latest")
1942
- async def get_latest_news(limit: int = Query(default=40, ge=1, le=100)):
1943
- """Get latest crypto news with sentiment"""
1944
- try:
1945
- news_items = await news_collector.get_latest_news(limit=limit)
1946
-
1947
- # Attach sentiment to each news item
1948
- from ai_models import analyze_news_item
1949
- enriched_news = []
1950
- for item in news_items:
1951
- try:
1952
- enriched = analyze_news_item(item)
1953
- enriched_news.append({
1954
- "title": enriched.get("title", ""),
1955
- "source": enriched.get("source", ""),
1956
- "published_at": enriched.get("published_at") or enriched.get("date", ""),
1957
- "symbols": enriched.get("symbols", []),
1958
- "sentiment": enriched.get("sentiment", "neutral"),
1959
- "sentiment_confidence": enriched.get("sentiment_confidence", 0.5),
1960
- "url": enriched.get("url", "")
1961
- })
1962
- except:
1963
- enriched_news.append({
1964
- "title": item.get("title", ""),
1965
- "source": item.get("source", ""),
1966
- "published_at": item.get("published_at") or item.get("date", ""),
1967
- "symbols": item.get("symbols", []),
1968
- "sentiment": "neutral",
1969
- "sentiment_confidence": 0.5,
1970
- "url": item.get("url", "")
1971
- })
1972
-
1973
- return {
1974
- "success": True,
1975
- "news": enriched_news,
1976
- "count": len(enriched_news),
1977
- "timestamp": datetime.now().isoformat()
1978
- }
1979
- except Exception as e:
1980
- logger.error(f"Error in /api/news/latest: {e}")
1981
- return {"success": True, "news": [], "count": 0, "timestamp": datetime.now().isoformat()}
1982
-
1983
-
1984
- @app.get("/api/news")
1985
- async def get_news(limit: int = Query(default=40, ge=1, le=100)):
1986
- """Alias for /api/news/latest for backward compatibility"""
1987
- return await get_latest_news(limit=limit)
1988
-
1989
-
1990
- @app.post("/api/news/summarize")
1991
- async def summarize_news(item: Dict[str, Any] = Body(...)):
1992
- """Summarize a news article"""
1993
- try:
1994
- from ai_models import analyze_news_item
1995
- enriched = analyze_news_item(item)
1996
-
1997
- return {
1998
- "success": True,
1999
- "summary": enriched.get("title", ""),
2000
- "sentiment": enriched.get("sentiment", "neutral"),
2001
- "sentiment_confidence": enriched.get("sentiment_confidence", 0.5)
2002
- }
2003
- except Exception as e:
2004
- logger.error(f"Error in /api/news/summarize: {e}")
2005
- return {
2006
- "success": False,
2007
- "error": str(e),
2008
- "summary": item.get("title", ""),
2009
- "sentiment": "neutral"
2010
- }
2011
-
2012
-
2013
- # ===== CHARTS ENDPOINTS =====
2014
- @app.get("/api/charts/price/{symbol}")
2015
- async def get_price_chart(symbol: str, timeframe: str = Query(default="7d")):
2016
- """Get price chart data"""
2017
- try:
2018
- # Clean and validate symbol
2019
- symbol = symbol.strip().upper()
2020
- if not symbol:
2021
- return JSONResponse(
2022
- status_code=400,
2023
- content={
2024
- "success": False,
2025
- "symbol": "",
2026
- "timeframe": timeframe,
2027
- "data": [],
2028
- "count": 0,
2029
- "error": "Symbol cannot be empty"
2030
- }
2031
- )
2032
-
2033
- logger.info(f"Fetching price history for {symbol} with timeframe {timeframe}")
2034
-
2035
- # market_collector.get_price_history expects timeframe as string, not hours
2036
- price_history = await market_collector.get_price_history(symbol, timeframe=timeframe)
2037
-
2038
- if not price_history or len(price_history) == 0:
2039
- logger.warning(f"No price history returned for {symbol}")
2040
- return {
2041
- "success": True,
2042
- "symbol": symbol,
2043
- "timeframe": timeframe,
2044
- "data": [],
2045
- "count": 0,
2046
- "message": "No data available"
2047
- }
2048
-
2049
- chart_data = []
2050
- for point in price_history:
2051
- # Handle different timestamp formats
2052
- timestamp = point.get("timestamp") or point.get("time") or point.get("date")
2053
- price = point.get("price") or point.get("close") or point.get("value") or 0
2054
-
2055
- # Convert timestamp to ISO format if needed
2056
- if timestamp:
2057
- try:
2058
- # If it's already a string, use it
2059
- if isinstance(timestamp, str):
2060
- # Try to parse and format
2061
- try:
2062
- # Try ISO format first
2063
- dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
2064
- timestamp = dt.isoformat()
2065
- except:
2066
- try:
2067
- # Try other common formats
2068
- from dateutil import parser
2069
- dt = parser.parse(timestamp)
2070
- timestamp = dt.isoformat()
2071
- except:
2072
- pass
2073
- elif isinstance(timestamp, (int, float)):
2074
- # Unix timestamp
2075
- dt = datetime.fromtimestamp(timestamp)
2076
- timestamp = dt.isoformat()
2077
- except Exception as e:
2078
- logger.warning(f"Error parsing timestamp {timestamp}: {e}")
2079
-
2080
- chart_data.append({
2081
- "timestamp": timestamp or "",
2082
- "time": timestamp or "",
2083
- "date": timestamp or "",
2084
- "price": float(price) if price else 0,
2085
- "close": float(price) if price else 0,
2086
- "value": float(price) if price else 0
2087
- })
2088
-
2089
- logger.info(f"Returning {len(chart_data)} data points for {symbol}")
2090
-
2091
- return {
2092
- "success": True,
2093
- "symbol": symbol,
2094
- "timeframe": timeframe,
2095
- "data": chart_data,
2096
- "count": len(chart_data)
2097
- }
2098
- except CollectorError as e:
2099
- logger.error(f"Collector error in /api/charts/price/{symbol}: {e}", exc_info=True)
2100
- return JSONResponse(
2101
- status_code=200,
2102
- content={
2103
- "success": False,
2104
- "symbol": symbol.upper() if symbol else "",
2105
- "timeframe": timeframe,
2106
- "data": [],
2107
- "count": 0,
2108
- "error": str(e)
2109
- }
2110
- )
2111
- except Exception as e:
2112
- logger.error(f"Error in /api/charts/price/{symbol}: {e}", exc_info=True)
2113
- return JSONResponse(
2114
- status_code=200,
2115
- content={
2116
- "success": False,
2117
- "symbol": symbol.upper() if symbol else "",
2118
- "timeframe": timeframe,
2119
- "data": [],
2120
- "count": 0,
2121
- "error": str(e)
2122
- }
2123
- )
2124
-
2125
-
2126
- @app.post("/api/charts/analyze")
2127
- async def analyze_chart(payload: Dict[str, Any] = Body(...)):
2128
- """Analyze chart data"""
2129
- try:
2130
- symbol = payload.get("symbol")
2131
- timeframe = payload.get("timeframe", "7d")
2132
- indicators = payload.get("indicators", [])
2133
-
2134
- if not symbol:
2135
- return JSONResponse(
2136
- status_code=400,
2137
- content={"success": False, "error": "Symbol is required"}
2138
- )
2139
-
2140
- symbol = symbol.strip().upper()
2141
- logger.info(f"Analyzing chart for {symbol} with timeframe {timeframe}")
2142
-
2143
- # Get price data - use timeframe string, not hours
2144
- price_history = await market_collector.get_price_history(symbol, timeframe=timeframe)
2145
-
2146
- if not price_history or len(price_history) == 0:
2147
- return {
2148
- "success": False,
2149
- "symbol": symbol,
2150
- "timeframe": timeframe,
2151
- "error": "No price data available for analysis"
2152
- }
2153
-
2154
- # Analyze with AI
2155
- from ai_models import analyze_chart_points
2156
- try:
2157
- analysis = analyze_chart_points(price_history, indicators)
2158
- except Exception as ai_error:
2159
- logger.error(f"AI analysis error: {ai_error}", exc_info=True)
2160
- # Return a basic analysis if AI fails
2161
- analysis = {
2162
- "direction": "neutral",
2163
- "summary": "Analysis unavailable",
2164
- "signals": []
2165
- }
2166
-
2167
- return {
2168
- "success": True,
2169
- "symbol": symbol,
2170
- "timeframe": timeframe,
2171
- "analysis": analysis
2172
- }
2173
- except CollectorError as e:
2174
- logger.error(f"Collector error in /api/charts/analyze: {e}", exc_info=True)
2175
- return JSONResponse(
2176
- status_code=200,
2177
- content={"success": False, "error": str(e)}
2178
- )
2179
- except Exception as e:
2180
- logger.error(f"Error in /api/charts/analyze: {e}", exc_info=True)
2181
- return JSONResponse(
2182
- status_code=200,
2183
- content={"success": False, "error": str(e)}
2184
- )
2185
-
2186
-
2187
- # ===== SENTIMENT ENDPOINTS =====
2188
- @app.post("/api/sentiment/analyze")
2189
- async def analyze_sentiment(payload: Dict[str, Any] = Body(...)):
2190
- """Analyze sentiment of text"""
2191
- try:
2192
- text = payload.get("text", "")
2193
-
2194
- from ai_models import ensemble_crypto_sentiment
2195
- result = ensemble_crypto_sentiment(text)
2196
-
2197
- return {
2198
- "success": True,
2199
- "sentiment": result["label"],
2200
- "confidence": result["confidence"],
2201
- "details": result
2202
- }
2203
- except Exception as e:
2204
- logger.error(f"Error in /api/sentiment/analyze: {e}")
2205
- return {"success": False, "error": str(e)}
2206
-
2207
-
2208
- # ===== QUERY ENDPOINT =====
2209
- @app.post("/api/query")
2210
- async def process_query(payload: Dict[str, Any] = Body(...)):
2211
- """Process natural language query"""
2212
- try:
2213
- query = payload.get("query", "").lower()
2214
-
2215
- # Simple query processing
2216
- if "price" in query or "btc" in query or "bitcoin" in query:
2217
- coins = await market_collector.get_top_coins(limit=10)
2218
- btc = next((c for c in coins if c.get("symbol", "").upper() == "BTC"), None)
2219
-
2220
- if btc:
2221
- price = btc.get("price") or btc.get("current_price", 0)
2222
- return {
2223
- "success": True,
2224
- "type": "price",
2225
- "message": f"Bitcoin (BTC) is currently trading at ${price:,.2f}",
2226
- "data": btc
2227
- }
2228
-
2229
- return {
2230
- "success": True,
2231
- "type": "general",
2232
- "message": "Query processed",
2233
- "data": None
2234
- }
2235
- except Exception as e:
2236
- logger.error(f"Error in /api/query: {e}")
2237
- return {"success": False, "error": str(e), "message": "Query failed"}
2238
-
2239
-
2240
- # ===== DATASETS & MODELS =====
2241
- @app.get("/api/datasets/list")
2242
- async def list_datasets():
2243
- """List available datasets"""
2244
- try:
2245
- from backend.services.hf_registry import REGISTRY
2246
- datasets = REGISTRY.list(kind="datasets")
2247
-
2248
- formatted = []
2249
- for d in datasets:
2250
- formatted.append({
2251
- "name": d.get("id"),
2252
- "category": d.get("category", "other"),
2253
- "records": "N/A",
2254
- "updated_at": "",
2255
- "tags": d.get("tags", []),
2256
- "source": d.get("source", "hub")
2257
- })
2258
-
2259
- return {
2260
- "success": True,
2261
- "datasets": formatted,
2262
- "count": len(formatted)
2263
- }
2264
- except Exception as e:
2265
- logger.error(f"Error in /api/datasets/list: {e}")
2266
- return {"success": True, "datasets": [], "count": 0}
2267
-
2268
-
2269
- @app.get("/api/datasets/sample")
2270
- async def get_dataset_sample(name: str = Query(...), limit: int = Query(default=20)):
2271
- """Get sample from dataset"""
2272
- try:
2273
- # Attempt to load dataset
2274
- try:
2275
- from datasets import load_dataset
2276
- from config import get_settings
2277
-
2278
- # Get HF token for dataset loading
2279
- settings = get_settings()
2280
- hf_token = settings.hf_token or "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"
2281
-
2282
- # Set token in environment for datasets library
2283
- import os
2284
- if hf_token and not os.environ.get("HF_TOKEN"):
2285
- os.environ["HF_TOKEN"] = hf_token
2286
-
2287
- dataset = load_dataset(name, split="train", streaming=True, token=hf_token)
2288
-
2289
- sample = []
2290
- for i, row in enumerate(dataset):
2291
- if i >= limit:
2292
- break
2293
- sample.append({k: str(v) for k, v in row.items()})
2294
-
2295
- return {
2296
- "success": True,
2297
- "name": name,
2298
- "sample": sample,
2299
- "count": len(sample)
2300
- }
2301
- except:
2302
- return {
2303
- "success": False,
2304
- "name": name,
2305
- "sample": [],
2306
- "count": 0,
2307
- "message": "Dataset loading requires authentication or is not available"
2308
- }
2309
- except Exception as e:
2310
- logger.error(f"Error in /api/datasets/sample: {e}")
2311
- return {"success": False, "error": str(e)}
2312
-
2313
-
2314
- @app.get("/api/models/list")
2315
- async def list_models():
2316
- """List available models"""
2317
- try:
2318
- from ai_models import get_model_info
2319
- info = get_model_info()
2320
-
2321
- models = []
2322
- catalog = info.get("model_catalog", {})
2323
-
2324
- for category, model_list in catalog.items():
2325
- for model_id in model_list:
2326
- models.append({
2327
- "name": model_id,
2328
- "task": "sentiment" if "sentiment" in category else "decision" if category == "decision" else "analysis",
2329
- "status": "available",
2330
- "category": category,
2331
- "notes": f"{category.replace('_', ' ').title()} model"
2332
- })
2333
-
2334
- return {
2335
- "success": True,
2336
- "models": models,
2337
- "count": len(models)
2338
- }
2339
- except Exception as e:
2340
- logger.error(f"Error in /api/models/list: {e}")
2341
- return {"success": True, "models": [], "count": 0}
2342
-
2343
-
2344
- @app.post("/api/models/test")
2345
- async def test_model(payload: Dict[str, Any] = Body(...)):
2346
- """Test a specific model"""
2347
- try:
2348
- model_id = payload.get("model", "")
2349
- text = payload.get("text", "")
2350
-
2351
- from ai_models import ensemble_crypto_sentiment
2352
- result = ensemble_crypto_sentiment(text)
2353
-
2354
- return {
2355
- "success": True,
2356
- "model": model_id,
2357
- "result": result
2358
- }
2359
- except Exception as e:
2360
- logger.error(f"Error in /api/models/test: {e}")
2361
- return {"success": False, "error": str(e)}
2362
-
2363
-
2364
- # ===== WEBSOCKET =====
2365
- @app.websocket("/ws")
2366
- async def websocket_endpoint(websocket: WebSocket):
2367
- """WebSocket endpoint for real-time updates"""
2368
- await ws_manager.connect(websocket)
2369
-
2370
- try:
2371
- while True:
2372
- # Check if connection is still open before sending
2373
- if websocket.client_state != WebSocketState.CONNECTED:
2374
- logger.info("WebSocket connection closed, breaking loop")
2375
- break
2376
-
2377
- # Send market updates every 10 seconds
2378
- try:
2379
- # Get latest data
2380
- top_coins = await market_collector.get_top_coins(limit=5)
2381
- news_items = await news_collector.get_latest_news(limit=3)
2382
-
2383
- # Compute global sentiment from news
2384
- from ai_models import ensemble_crypto_sentiment
2385
- news_texts = " ".join([n.get("title", "") for n in news_items])
2386
- global_sentiment = ensemble_crypto_sentiment(news_texts) if news_texts else {"label": "neutral", "confidence": 0.5}
2387
-
2388
- payload = {
2389
- "market_data": top_coins,
2390
- "stats": {
2391
- "total_market_cap": sum([c.get("market_cap", 0) for c in top_coins]),
2392
- "sentiment": global_sentiment
2393
- },
2394
- "news": news_items,
2395
- "sentiment": global_sentiment,
2396
- "timestamp": datetime.now().isoformat()
2397
- }
2398
-
2399
- # Double-check connection state before sending
2400
- if websocket.client_state == WebSocketState.CONNECTED:
2401
- await websocket.send_json({
2402
- "type": "update",
2403
- "payload": payload
2404
- })
2405
- else:
2406
- logger.info("WebSocket disconnected, breaking loop")
2407
- break
2408
- except CollectorError as e:
2409
- # Provider errors are already logged by the collector, just continue
2410
- logger.debug(f"Provider error in WebSocket update (this is expected with fallbacks): {e}")
2411
- # Use empty data on provider errors
2412
- payload = {
2413
- "market_data": [],
2414
- "stats": {"total_market_cap": 0, "sentiment": {"label": "neutral", "confidence": 0.5}},
2415
- "news": [],
2416
- "sentiment": {"label": "neutral", "confidence": 0.5},
2417
- "timestamp": datetime.now().isoformat()
2418
- }
2419
- except Exception as e:
2420
- # Log other errors with full details
2421
- error_msg = str(e) if str(e) else repr(e)
2422
- logger.error(f"Error in WebSocket update: {type(e).__name__}: {error_msg}")
2423
- # Don't break on data errors, just log and continue
2424
- # Only break on connection errors
2425
- if "send" in str(e).lower() or "close" in str(e).lower():
2426
- break
2427
-
2428
- await asyncio.sleep(10)
2429
- except WebSocketDisconnect:
2430
- logger.info("WebSocket disconnect exception caught")
2431
- except Exception as e:
2432
- logger.error(f"WebSocket error: {e}")
2433
- finally:
2434
- try:
2435
- ws_manager.disconnect(websocket)
2436
- except:
2437
- pass
2438
-
2439
- @app.get("/api/market/history")
2440
- async def get_market_history(symbol: str = "BTC", limit: int = 10):
2441
- """
2442
- Get historical prices from the local database if available.
2443
-
2444
- For this deployment we avoid touching the internal DatabaseManager
2445
- and simply report that no history API is wired yet.
2446
- """
2447
- symbol = symbol.upper()
2448
- # We don't fabricate data here; if you need real history, it should
2449
- # be implemented via the shared database models.
2450
- return {
2451
- "symbol": symbol,
2452
- "history": [],
2453
- "count": 0,
2454
- "message": "History endpoint not wired to DB in this Space",
2455
- }
2456
-
2457
-
2458
-
2459
- @app.get("/api/status")
2460
- async def get_status():
2461
- """
2462
- System status endpoint used by the admin UI.
2463
-
2464
- This reports real-time information about providers and database,
2465
- without fabricating any market data.
2466
- """
2467
- providers_cfg = load_providers_config()
2468
- providers = providers_cfg or {}
2469
- validated_count = sum(1 for p in providers.values() if p.get("validated"))
2470
-
2471
- db_path = DB_PATH
2472
- db_status = "connected" if db_path.exists() else "initializing"
2473
-
2474
- return {
2475
- "system_health": "healthy",
2476
- "timestamp": datetime.now().isoformat(),
2477
- "total_providers": len(providers),
2478
- "validated_providers": validated_count,
2479
- "database_status": db_status,
2480
- "apl_available": APL_REPORT_PATH.exists(),
2481
- "use_mock_data": False,
2482
- }
2483
-
2484
-
2485
- @app.get("/api/logs/recent")
2486
- async def get_recent_logs():
2487
- """
2488
- Return recent log lines for the admin UI.
2489
-
2490
- We read from the main server log file if available.
2491
- This does not fabricate content; if there are no logs,
2492
- an empty list is returned.
2493
- """
2494
- log_file = LOG_DIR / "server.log"
2495
- lines = tail_log_file(log_file, max_lines=200)
2496
- # Wrap plain text lines as structured entries
2497
- logs = [{"line": line.rstrip("\n")} for line in lines]
2498
- return {"logs": logs, "count": len(logs)}
2499
-
2500
-
2501
- @app.get("/api/logs/errors")
2502
- async def get_error_logs():
2503
- """
2504
- Return recent error log lines from the same log file.
2505
-
2506
- This is a best-effort filter based on typical ERROR prefixes.
2507
- """
2508
- log_file = LOG_DIR / "server.log"
2509
- lines = tail_log_file(log_file, max_lines=400)
2510
- error_lines = [line for line in lines if "ERROR" in line or "WARNING" in line]
2511
- logs = [{"line": line.rstrip("\n")} for line in error_lines[-200:]]
2512
- return {"errors": logs, "count": len(logs)}
2513
-
2514
-
2515
- def _load_apl_report() -> Optional[Dict[str, Any]]:
2516
- """Load the APL (Auto Provider Loader) validation report if available."""
2517
- if not APL_REPORT_PATH.exists():
2518
- return None
2519
- try:
2520
- with APL_REPORT_PATH.open("r", encoding="utf-8") as f:
2521
- return json.load(f)
2522
- except Exception as e:
2523
- logger.error(f"Error reading APL report: {e}")
2524
- return None
2525
-
2526
-
2527
- @app.get("/api/apl/summary")
2528
- async def get_apl_summary():
2529
- """
2530
- Summary of the Auto Provider Loader (APL) report.
2531
-
2532
- If the report is missing, we return a clear not_available status
2533
- instead of fabricating metrics.
2534
- """
2535
- report = _load_apl_report()
2536
- if not report or "stats" not in report:
2537
- return {
2538
- "status": "not_available",
2539
- "message": "APL report not found",
2540
- }
2541
-
2542
- stats = report.get("stats", {})
2543
- return {
2544
- "status": "ok",
2545
- "http_candidates": stats.get("total_http_candidates", 0),
2546
- "http_valid": stats.get("http_valid", 0),
2547
- "http_invalid": stats.get("http_invalid", 0),
2548
- "http_conditional": stats.get("http_conditional", 0),
2549
- "hf_candidates": stats.get("total_hf_candidates", 0),
2550
- "hf_valid": stats.get("hf_valid", 0),
2551
- "hf_invalid": stats.get("hf_invalid", 0),
2552
- "hf_conditional": stats.get("hf_conditional", 0),
2553
- "timestamp": datetime.now().isoformat(),
2554
- }
2555
-
2556
-
2557
- @app.get("/api/hf/models")
2558
- async def get_hf_models_from_apl():
2559
- """
2560
- Return the list of Hugging Face models discovered by the APL report.
2561
-
2562
- This is used by the admin UI. The data comes from the real
2563
- PROVIDER_AUTO_DISCOVERY_REPORT.json file if present.
2564
- """
2565
- report = _load_apl_report()
2566
- if not report:
2567
- return {"models": [], "count": 0, "source": "none"}
2568
-
2569
- hf_models = report.get("hf_models", {}).get("results", [])
2570
- return {
2571
- "models": hf_models,
2572
- "count": len(hf_models),
2573
- "source": "APL report",
2574
- }
2575
-
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Hugging Face Unified Server - Main FastAPI application entry point.
4
+ This module imports the FastAPI app from api_server_extended for HF Docker Space deployment.
5
+ """
6
+
7
+ from api_server_extended import app
8
+
9
+ __all__ = ["app"]
10
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
index.html CHANGED
The diff for this file is too large to render. See raw diff
 
package.json CHANGED
@@ -10,16 +10,7 @@
10
  "dashboard": "python3 -m http.server 8080",
11
  "full-check": "node api-monitor.js && node failover-manager.js && echo 'Open http://localhost:8080/dashboard.html in your browser' && python3 -m http.server 8080",
12
  "test:free-resources": "node free_resources_selftest.mjs",
13
- "test:free-resources:win": "powershell -NoProfile -ExecutionPolicy Bypass -File test_free_endpoints.ps1",
14
- "test:theme": "node tests/verify_theme.js",
15
- "test:api-client": "node tests/test_apiClient.test.js",
16
- "test:ui-feedback": "node tests/test_ui_feedback.test.js",
17
- "test:fallback": "pytest tests/test_fallback_service.py -m fallback",
18
- "test:api-health": "pytest tests/test_fallback_service.py -m api_health"
19
- },
20
- "devDependencies": {
21
- "fast-check": "^3.15.0",
22
- "jsdom": "^23.0.0"
23
  },
24
  "keywords": [
25
  "cryptocurrency",
@@ -41,8 +32,5 @@
41
  "repository": {
42
  "type": "git",
43
  "url": "https://github.com/nimazasinich/crypto-dt-source.git"
44
- },
45
- "dependencies": {
46
- "charmap": "^1.1.6"
47
  }
48
  }
 
10
  "dashboard": "python3 -m http.server 8080",
11
  "full-check": "node api-monitor.js && node failover-manager.js && echo 'Open http://localhost:8080/dashboard.html in your browser' && python3 -m http.server 8080",
12
  "test:free-resources": "node free_resources_selftest.mjs",
13
+ "test:free-resources:win": "powershell -NoProfile -ExecutionPolicy Bypass -File test_free_endpoints.ps1"
 
 
 
 
 
 
 
 
 
14
  },
15
  "keywords": [
16
  "cryptocurrency",
 
32
  "repository": {
33
  "type": "git",
34
  "url": "https://github.com/nimazasinich/crypto-dt-source.git"
 
 
 
35
  }
36
  }
production_server.py CHANGED
@@ -441,7 +441,7 @@ async def remove_custom_api(name: str):
441
  # Serve static files
442
  @app.get("/")
443
  async def root():
444
- return FileResponse("admin.html")
445
 
446
  @app.get("/index.html")
447
  async def index():
 
441
  # Serve static files
442
  @app.get("/")
443
  async def root():
444
+ return FileResponse("index.html")
445
 
446
  @app.get("/index.html")
447
  async def index():
provider_validator.py CHANGED
@@ -278,13 +278,7 @@ class ProviderValidator:
278
  try:
279
  start = time.time()
280
 
281
- # Get HF token from environment or use default
282
- hf_token = os.getenv("HF_TOKEN") or "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"
283
- headers = {}
284
- if hf_token:
285
- headers["Authorization"] = f"Bearer {hf_token}"
286
-
287
- async with httpx.AsyncClient(timeout=self.timeout, headers=headers) as client:
288
  response = await client.get(f"https://huggingface.co/api/models/{model_id}")
289
  elapsed_ms = (time.time() - start) * 1000
290
 
 
278
  try:
279
  start = time.time()
280
 
281
+ async with httpx.AsyncClient(timeout=self.timeout) as client:
 
 
 
 
 
 
282
  response = await client.get(f"https://huggingface.co/api/models/{model_id}")
283
  elapsed_ms = (time.time() - start) * 1000
284
 
real_server.py CHANGED
@@ -380,7 +380,7 @@ async def api_config_keys():
380
  # Serve static files
381
  @app.get("/")
382
  async def root():
383
- return FileResponse("admin.html")
384
 
385
  @app.get("/dashboard.html")
386
  async def dashboard():
 
380
  # Serve static files
381
  @app.get("/")
382
  async def root():
383
+ return FileResponse("dashboard.html")
384
 
385
  @app.get("/dashboard.html")
386
  async def dashboard():
requirements_hf.txt CHANGED
@@ -1,10 +1,32 @@
1
- fastapi>=0.104.0,<1.0.0
2
- uvicorn>=0.24.0,<1.0.0
3
- aiohttp>=3.8.0,<4.0.0
4
- pydantic>=2.5.0,<3.0.0
5
- sqlalchemy>=2.0.0,<3.0.0
6
- pandas>=2.0.0,<3.0.0
7
- numpy>=1.25.0,<2.0.0
8
- transformers>=4.38.0,<5.0.0
9
- torch
10
- python-dotenv>=1.0.0,<2.0.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.115.0
2
+ uvicorn[standard]==0.30.0
3
+ pydantic==2.9.0
4
+ pydantic-settings==2.5.0
5
+ sqlalchemy==2.0.35
6
+ httpx==0.27.2
7
+ websockets>=10.4,<12.0
8
+ python-dotenv==1.0.1
9
+ python-multipart==0.0.9
10
+ requests==2.32.3
11
+ aiohttp==3.10.5
12
+ pandas==2.2.3
13
+ numpy>=1.26.0,<2.0.0
14
+ gradio==4.44.0
15
+ plotly==5.24.1
16
+ psutil==6.0.0
17
+ transformers>=4.45.0
18
+ tokenizers>=0.20.0
19
+ huggingface-hub>=0.25.0
20
+ safetensors>=0.4.0
21
+ datasets>=3.0.0
22
+ torch>=2.4.0,<2.5.0
23
+ torchaudio>=2.4.0,<2.5.0
24
+ sentence-transformers>=3.1.0
25
+ sentencepiece==0.2.0
26
+ feedparser==6.0.11
27
+ beautifulsoup4==4.12.3
28
+ lxml==5.3.0
29
+ python-dateutil>=2.9.0
30
+ pytz>=2024.1
31
+ tenacity>=9.0.0
32
+
simple_server.py CHANGED
@@ -1,23 +1,11 @@
1
  """Simple FastAPI server for testing HF integration"""
2
  import asyncio
3
- import os
4
- import sys
5
- import io
6
- from datetime import datetime
7
- from fastapi import FastAPI, Query, WebSocket, WebSocketDisconnect
8
  from fastapi.middleware.cors import CORSMiddleware
9
  from fastapi.responses import FileResponse, JSONResponse
10
  from fastapi.staticfiles import StaticFiles
11
  import uvicorn
12
 
13
- # Fix encoding for Windows console
14
- if sys.platform == "win32":
15
- try:
16
- sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
17
- sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
18
- except Exception:
19
- pass
20
-
21
  # Create FastAPI app
22
  app = FastAPI(title="Crypto API Monitor - Simple", version="1.0.0")
23
 
@@ -34,20 +22,9 @@ app.add_middleware(
34
  try:
35
  from backend.routers import hf_connect
36
  app.include_router(hf_connect.router)
37
- print("[OK] HF router loaded")
38
- except Exception as e:
39
- print(f"[ERROR] HF router failed: {e}")
40
-
41
- # Mount static files directory
42
- try:
43
- static_path = os.path.join(os.path.dirname(__file__), "static")
44
- if os.path.exists(static_path):
45
- app.mount("/static", StaticFiles(directory=static_path), name="static")
46
- print(f"[OK] Static files mounted from {static_path}")
47
- else:
48
- print(f"[WARNING] Static directory not found: {static_path}")
49
  except Exception as e:
50
- print(f"[ERROR] Could not mount static files: {e}")
51
 
52
  # Background task for HF registry
53
  @app.on_event("startup")
@@ -55,9 +32,9 @@ async def startup_hf():
55
  try:
56
  from backend.services.hf_registry import periodic_refresh
57
  asyncio.create_task(periodic_refresh())
58
- print("[OK] HF background refresh started")
59
  except Exception as e:
60
- print(f"[ERROR] HF background refresh failed: {e}")
61
 
62
  # Health endpoint
63
  @app.get("/health")
@@ -71,10 +48,7 @@ async def api_health():
71
  # Serve static files
72
  @app.get("/")
73
  async def root():
74
- """Serve default HTML UI page (index.html)"""
75
- if os.path.exists("index.html"):
76
- return FileResponse("index.html")
77
- return FileResponse("admin.html")
78
 
79
  @app.get("/index.html")
80
  async def index():
@@ -84,15 +58,6 @@ async def index():
84
  async def hf_console():
85
  return FileResponse("hf_console.html")
86
 
87
- # Serve config.js
88
- @app.get("/config.js")
89
- async def config_js():
90
- """Serve config.js file"""
91
- config_path = os.path.join(os.path.dirname(__file__), "config.js")
92
- if os.path.exists(config_path):
93
- return FileResponse(config_path, media_type="application/javascript")
94
- return JSONResponse({"error": "config.js not found"}, status_code=404)
95
-
96
  # Mock API endpoints for dashboard
97
  @app.get("/api/status")
98
  async def api_status():
@@ -423,301 +388,14 @@ async def api_config_keys():
423
  }
424
  ]
425
 
426
- # API endpoints for dashboard
427
- @app.get("/api/coins/top")
428
- async def api_coins_top(limit: int = 10):
429
- """Get top cryptocurrencies"""
430
- from datetime import datetime
431
- try:
432
- # Try to use real collectors if available
433
- from collectors.aggregator import MarketDataCollector
434
- collector = MarketDataCollector()
435
- coins = await collector.get_top_coins(limit=limit)
436
- result = []
437
- for coin in coins:
438
- result.append({
439
- "id": coin.get("id", coin.get("symbol", "").lower()),
440
- "rank": coin.get("rank", 0),
441
- "symbol": coin.get("symbol", "").upper(),
442
- "name": coin.get("name", ""),
443
- "price": coin.get("price") or coin.get("current_price", 0),
444
- "current_price": coin.get("price") or coin.get("current_price", 0),
445
- "price_change_24h": coin.get("change_24h") or coin.get("price_change_percentage_24h", 0),
446
- "price_change_percentage_24h": coin.get("change_24h") or coin.get("price_change_percentage_24h", 0),
447
- "volume_24h": coin.get("volume_24h") or coin.get("total_volume", 0),
448
- "market_cap": coin.get("market_cap", 0),
449
- "image": coin.get("image", ""),
450
- "last_updated": coin.get("last_updated", datetime.now().isoformat())
451
- })
452
- return {"success": True, "coins": result, "count": len(result), "timestamp": datetime.now().isoformat()}
453
- except Exception as e:
454
- # Return mock data on error
455
- from datetime import datetime
456
- import random
457
- mock_coins = [
458
- {"id": "bitcoin", "rank": 1, "symbol": "BTC", "name": "Bitcoin", "price": 43250.50 + random.uniform(-1000, 1000),
459
- "current_price": 43250.50, "price_change_24h": 2.34, "price_change_percentage_24h": 2.34,
460
- "volume_24h": 25000000000, "market_cap": 845000000000, "image": "", "last_updated": datetime.now().isoformat()},
461
- {"id": "ethereum", "rank": 2, "symbol": "ETH", "name": "Ethereum", "price": 2450.30 + random.uniform(-100, 100),
462
- "current_price": 2450.30, "price_change_24h": 1.25, "price_change_percentage_24h": 1.25,
463
- "volume_24h": 12000000000, "market_cap": 295000000000, "image": "", "last_updated": datetime.now().isoformat()},
464
- ]
465
- return {"success": True, "coins": mock_coins[:limit], "count": min(limit, len(mock_coins)), "timestamp": datetime.now().isoformat()}
466
-
467
- @app.get("/api/market/stats")
468
- async def api_market_stats():
469
- """Get global market statistics"""
470
- from datetime import datetime
471
- try:
472
- # Try to get real data from collectors
473
- from collectors.aggregator import MarketDataCollector
474
- collector = MarketDataCollector()
475
- coins = await collector.get_top_coins(limit=100)
476
- total_market_cap = sum(c.get("market_cap", 0) for c in coins)
477
- total_volume = sum(c.get("volume_24h", 0) or c.get("total_volume", 0) for c in coins)
478
- btc_market_cap = next((c.get("market_cap", 0) for c in coins if c.get("symbol", "").upper() == "BTC"), 0)
479
- btc_dominance = (btc_market_cap / total_market_cap * 100) if total_market_cap > 0 else 0
480
-
481
- stats = {
482
- "total_market_cap": total_market_cap,
483
- "total_volume_24h": total_volume,
484
- "btc_dominance": btc_dominance,
485
- "eth_dominance": 0,
486
- "active_cryptocurrencies": 10000,
487
- "markets": 500,
488
- "market_cap_change_24h": 0.0,
489
- "timestamp": datetime.now().isoformat()
490
- }
491
- return {"success": True, "stats": stats}
492
- except Exception:
493
- # Return mock data on error
494
- from datetime import datetime
495
- return {
496
- "success": True,
497
- "stats": {
498
- "total_market_cap": 2100000000000,
499
- "total_volume_24h": 89500000000,
500
- "btc_dominance": 48.2,
501
- "eth_dominance": 15.5,
502
- "active_cryptocurrencies": 10000,
503
- "markets": 500,
504
- "market_cap_change_24h": 2.5,
505
- "timestamp": datetime.now().isoformat()
506
- }
507
- }
508
-
509
- @app.get("/api/news/latest")
510
- async def api_news_latest(limit: int = 40):
511
- """Get latest cryptocurrency news"""
512
- from datetime import datetime
513
- try:
514
- # Try to use real collectors if available
515
- from collectors.aggregator import NewsCollector
516
- collector = NewsCollector()
517
- news_items = await collector.get_latest_news(limit=limit)
518
-
519
- # Format news items
520
- enriched_news = []
521
- for item in news_items:
522
- enriched_news.append({
523
- "title": item.get("title", ""),
524
- "source": item.get("source", ""),
525
- "published_at": item.get("published_at") or item.get("date", ""),
526
- "symbols": item.get("symbols", []),
527
- "sentiment": item.get("sentiment", "neutral"),
528
- "sentiment_confidence": item.get("sentiment_confidence", 0.5),
529
- "url": item.get("url", "")
530
- })
531
- return {"success": True, "news": enriched_news, "count": len(enriched_news), "timestamp": datetime.now().isoformat()}
532
- except Exception:
533
- # Return mock data on error
534
- from datetime import datetime, timedelta
535
- mock_news = [
536
- {
537
- "title": "Bitcoin reaches new milestone",
538
- "source": "CoinDesk",
539
- "published_at": (datetime.now() - timedelta(hours=2)).isoformat(),
540
- "symbols": ["BTC"],
541
- "sentiment": "positive",
542
- "sentiment_confidence": 0.75,
543
- "url": "https://example.com/news1"
544
- },
545
- {
546
- "title": "Ethereum upgrade scheduled",
547
- "source": "CryptoNews",
548
- "published_at": (datetime.now() - timedelta(hours=5)).isoformat(),
549
- "symbols": ["ETH"],
550
- "sentiment": "neutral",
551
- "sentiment_confidence": 0.65,
552
- "url": "https://example.com/news2"
553
- },
554
- ]
555
- return {"success": True, "news": mock_news[:limit], "count": min(limit, len(mock_news)), "timestamp": datetime.now().isoformat()}
556
-
557
- @app.get("/api/market")
558
- async def api_market():
559
- """Get market data (combines coins and stats)"""
560
- from datetime import datetime
561
- try:
562
- # Get top coins and market stats
563
- coins_data = await api_coins_top(20)
564
- stats_data = await api_market_stats()
565
-
566
- return {
567
- "success": True,
568
- "cryptocurrencies": coins_data.get("coins", []),
569
- "stats": stats_data.get("stats", {}),
570
- "timestamp": datetime.now().isoformat()
571
- }
572
- except Exception as e:
573
- # Return basic structure on error
574
- from datetime import datetime
575
- return {
576
- "success": True,
577
- "cryptocurrencies": [],
578
- "stats": {
579
- "total_market_cap": 0,
580
- "total_volume_24h": 0,
581
- "btc_dominance": 0
582
- },
583
- "timestamp": datetime.now().isoformat()
584
- }
585
-
586
- @app.get("/api/sentiment")
587
- async def api_sentiment():
588
- """Get market sentiment data"""
589
- from datetime import datetime
590
- try:
591
- # Try to get real sentiment data
592
- from collectors.aggregator import ProviderStatusCollector
593
- collector = ProviderStatusCollector()
594
-
595
- # Try to get fear & greed index
596
- import httpx
597
- async with httpx.AsyncClient() as client:
598
- try:
599
- fng_response = await client.get("https://api.alternative.me/fng/?limit=1", timeout=5)
600
- if fng_response.status_code == 200:
601
- fng_data = fng_response.json()
602
- if fng_data.get("data") and len(fng_data["data"]) > 0:
603
- fng_value = int(fng_data["data"][0].get("value", 50))
604
- return {
605
- "success": True,
606
- "fear_greed": {
607
- "value": fng_value,
608
- "classification": "Extreme Fear" if fng_value < 25 else "Fear" if fng_value < 45 else "Neutral" if fng_value < 55 else "Greed" if fng_value < 75 else "Extreme Greed"
609
- },
610
- "overall_sentiment": "neutral",
611
- "timestamp": datetime.now().isoformat()
612
- }
613
- except:
614
- pass
615
-
616
- # Fallback to default sentiment
617
- return {
618
- "success": True,
619
- "fear_greed": {
620
- "value": 50,
621
- "classification": "Neutral"
622
- },
623
- "overall_sentiment": "neutral",
624
- "timestamp": datetime.now().isoformat()
625
- }
626
- except Exception:
627
- # Return default sentiment on error
628
- from datetime import datetime
629
- return {
630
- "success": True,
631
- "fear_greed": {
632
- "value": 50,
633
- "classification": "Neutral"
634
- },
635
- "overall_sentiment": "neutral",
636
- "timestamp": datetime.now().isoformat()
637
- }
638
-
639
- @app.get("/api/trending")
640
- async def api_trending():
641
- """Get trending cryptocurrencies"""
642
- # Use top coins as trending for now
643
- return await api_coins_top(10)
644
-
645
- # WebSocket support
646
- class ConnectionManager:
647
- def __init__(self):
648
- self.active_connections = []
649
-
650
- async def connect(self, websocket: WebSocket):
651
- await websocket.accept()
652
- self.active_connections.append(websocket)
653
-
654
- def disconnect(self, websocket: WebSocket):
655
- if websocket in self.active_connections:
656
- self.active_connections.remove(websocket)
657
-
658
- async def broadcast(self, message: dict):
659
- for conn in list(self.active_connections):
660
- try:
661
- await conn.send_json(message)
662
- except:
663
- self.disconnect(conn)
664
-
665
- ws_manager = ConnectionManager()
666
-
667
- @app.websocket("/ws")
668
- async def websocket_endpoint(websocket: WebSocket):
669
- """WebSocket endpoint for real-time updates"""
670
- await ws_manager.connect(websocket)
671
- try:
672
- # Send initial connection message
673
- await websocket.send_json({
674
- "type": "connected",
675
- "message": "WebSocket connected",
676
- "timestamp": datetime.now().isoformat()
677
- })
678
-
679
- # Send periodic updates
680
- while True:
681
- try:
682
- # Send heartbeat
683
- await websocket.send_json({
684
- "type": "heartbeat",
685
- "timestamp": datetime.now().isoformat()
686
- })
687
-
688
- # Try to get market data and send update
689
- try:
690
- coins_data = await api_coins_top(5)
691
- news_data = await api_news_latest(3)
692
-
693
- await websocket.send_json({
694
- "type": "update",
695
- "payload": {
696
- "market_data": coins_data.get("coins", []),
697
- "news": news_data.get("news", []),
698
- "timestamp": datetime.now().isoformat()
699
- }
700
- })
701
- except:
702
- pass # If data fetch fails, just send heartbeat
703
-
704
- await asyncio.sleep(30) # Update every 30 seconds
705
- except WebSocketDisconnect:
706
- break
707
- except WebSocketDisconnect:
708
- ws_manager.disconnect(websocket)
709
- except Exception as e:
710
- print(f"[WS] Error: {e}")
711
- ws_manager.disconnect(websocket)
712
-
713
  if __name__ == "__main__":
714
  print("=" * 70)
715
- print("Starting Crypto API Monitor - Simple Server")
716
  print("=" * 70)
717
- print("Server: http://localhost:7860")
718
- print("Main Dashboard: http://localhost:7860/ (index.html - default HTML UI)")
719
- print("HF Console: http://localhost:7860/hf_console.html")
720
- print("API Docs: http://localhost:7860/docs")
721
  print("=" * 70)
722
  print()
723
 
 
1
  """Simple FastAPI server for testing HF integration"""
2
  import asyncio
3
+ from fastapi import FastAPI
 
 
 
 
4
  from fastapi.middleware.cors import CORSMiddleware
5
  from fastapi.responses import FileResponse, JSONResponse
6
  from fastapi.staticfiles import StaticFiles
7
  import uvicorn
8
 
 
 
 
 
 
 
 
 
9
  # Create FastAPI app
10
  app = FastAPI(title="Crypto API Monitor - Simple", version="1.0.0")
11
 
 
22
  try:
23
  from backend.routers import hf_connect
24
  app.include_router(hf_connect.router)
25
+ print(" HF router loaded")
 
 
 
 
 
 
 
 
 
 
 
26
  except Exception as e:
27
+ print(f" HF router failed: {e}")
28
 
29
  # Background task for HF registry
30
  @app.on_event("startup")
 
32
  try:
33
  from backend.services.hf_registry import periodic_refresh
34
  asyncio.create_task(periodic_refresh())
35
+ print(" HF background refresh started")
36
  except Exception as e:
37
+ print(f" HF background refresh failed: {e}")
38
 
39
  # Health endpoint
40
  @app.get("/health")
 
48
  # Serve static files
49
  @app.get("/")
50
  async def root():
51
+ return FileResponse("index.html")
 
 
 
52
 
53
  @app.get("/index.html")
54
  async def index():
 
58
  async def hf_console():
59
  return FileResponse("hf_console.html")
60
 
 
 
 
 
 
 
 
 
 
61
  # Mock API endpoints for dashboard
62
  @app.get("/api/status")
63
  async def api_status():
 
388
  }
389
  ]
390
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
391
  if __name__ == "__main__":
392
  print("=" * 70)
393
+ print("🚀 Starting Crypto API Monitor - Simple Server")
394
  print("=" * 70)
395
+ print("📍 Server: http://localhost:7860")
396
+ print("📄 Main Dashboard: http://localhost:7860/index.html")
397
+ print("🤗 HF Console: http://localhost:7860/hf_console.html")
398
+ print("📚 API Docs: http://localhost:7860/docs")
399
  print("=" * 70)
400
  print()
401
 
unified_dashboard.html CHANGED
@@ -8,152 +8,54 @@
8
  <link rel="stylesheet" href="static/css/design-system.css" />
9
  <link rel="stylesheet" href="static/css/dashboard.css" />
10
  <link rel="stylesheet" href="static/css/pro-dashboard.css" />
11
- <link rel="stylesheet" href="static/css/modern-dashboard.css" />
12
- <link rel="stylesheet" href="static/css/glassmorphism.css" />
13
- <link rel="stylesheet" href="static/css/light-minimal-theme.css" />
14
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js" defer></script>
15
- <script src="static/js/animations.js" defer></script>
16
- <script src="static/js/menu-system.js" defer></script>
17
- <script src="static/js/huggingface-integration.js" defer></script>
18
  </head>
19
- <body data-theme="light">
20
  <div class="app-shell">
21
- <aside class="sidebar sidebar-modern">
22
- <div class="brand brand-modern">
23
- <div class="brand-icon">
24
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
 
25
  <path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="1.5" />
26
  <path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="1.5" />
27
  <path d="M2 12L12 17L22 12" stroke="currentColor" stroke-width="1.5" />
28
  </svg>
29
- </div>
30
- <div class="brand-text">
31
- <strong>Crypto Monitor HF</strong>
32
- <span class="env-pill">
33
- <svg width="10" height="10" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
34
- <path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="1.5" />
35
- </svg>
36
- HF Space
37
- </span>
38
- </div>
39
  </div>
40
- <nav class="nav nav-modern">
41
- <button class="nav-button nav-button-modern active" data-nav="page-overview">
42
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
43
- <path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
44
- <path d="M9 22V12h6v10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
45
- </svg>
46
- <span>Overview</span>
47
- </button>
48
- <button class="nav-button nav-button-modern" data-nav="page-market">
49
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
50
- <path d="M3 3v18h18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
51
- <path d="M7 10l4-4 4 4 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
52
- </svg>
53
- <span>Market</span>
54
- </button>
55
- <button class="nav-button nav-button-modern" data-nav="page-chart">
56
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
57
- <path d="M3 3v18h18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
58
- <path d="M7 16l4-4 4 4 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
59
- </svg>
60
- <span>Chart Lab</span>
61
- </button>
62
- <button class="nav-button nav-button-modern" data-nav="page-ai">
63
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
64
- <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
65
- </svg>
66
- <span>Sentiment & AI</span>
67
- </button>
68
- <button class="nav-button nav-button-modern" data-nav="page-news">
69
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
70
- <path d="M4 19.5A2.5 2.5 0 016.5 17H20" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
71
- <path d="M6.5 2H20v20H6.5A2.5 2.5 0 014 19.5v-15A2.5 2.5 0 016.5 2z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
72
- </svg>
73
- <span>News</span>
74
- </button>
75
- <button class="nav-button nav-button-modern" data-nav="page-providers">
76
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
77
- <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
78
- <path d="M12 6v6l4 2" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
79
- </svg>
80
- <span>Providers</span>
81
- </button>
82
- <button class="nav-button nav-button-modern" data-nav="page-api">
83
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
84
- <path d="M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 003 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0021 16z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
85
- </svg>
86
- <span>API Explorer</span>
87
- </button>
88
- <button class="nav-button nav-button-modern" data-nav="page-debug">
89
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
90
- <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
91
- <path d="M12 6v6l4 2" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
92
- </svg>
93
- <span>Diagnostics</span>
94
- </button>
95
- <button class="nav-button nav-button-modern" data-nav="page-datasets">
96
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
97
- <path d="M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 003 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0021 16z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
98
- </svg>
99
- <span>Datasets & Models</span>
100
- </button>
101
- <button class="nav-button nav-button-modern" data-nav="page-settings">
102
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
103
- <circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2"/>
104
- <path d="M12 1v6m0 6v6M5.64 5.64l4.24 4.24m4.24 4.24l4.24 4.24M1 12h6m6 0h6M5.64 18.36l4.24-4.24m4.24-4.24l4.24-4.24" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
105
- </svg>
106
- <span>Settings</span>
107
- </button>
108
  </nav>
109
  <div class="sidebar-footer">
110
- <div class="footer-badge">
111
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
112
- <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
113
- </svg>
114
- Unified Intelligence Console
115
- </div>
116
  </div>
117
  </aside>
118
  <main class="main-area">
119
- <header class="modern-header">
120
- <div style="display: flex; align-items: center; justify-content: space-between; width: 100%; flex-wrap: wrap; gap: 16px;">
121
- <div>
122
- <h1 style="margin: 0; font-size: 1.75rem; font-weight: 800; background: linear-gradient(135deg, #00D4FF, #8B5CF6, #EC4899); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;">Unified Intelligence Dashboard</h1>
123
- <p class="text-muted" style="margin: 4px 0 0 0; font-size: 0.875rem;">Live market telemetry, AI signals, diagnostics, and provider health.</p>
 
 
 
 
124
  </div>
125
- <div class="status-group" style="display: flex; gap: 12px; align-items: center; position: relative;">
126
- <div class="status-pill" data-api-health data-state="warn" style="padding: 8px 16px; border-radius: 12px; background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.1);">
127
- <span class="status-dot"></span>
128
- <span>checking</span>
129
- </div>
130
- <div class="status-pill" data-ws-status data-state="warn" style="padding: 8px 16px; border-radius: 12px; background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.1);">
131
- <span class="status-dot"></span>
132
- <span>connecting</span>
133
- </div>
134
- <button class="button-3d" data-menu-trigger="theme-menu" style="padding: 8px 16px; position: relative;">
135
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
136
- <circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2"/>
137
- <path d="M12 1v6m0 6v6M5.64 5.64l4.24 4.24m4.24 4.24l4.24 4.24M1 12h6m6 0h6M5.64 18.36l4.24-4.24m4.24-4.24l4.24-4.24" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
138
- </svg>
139
- </button>
140
- <div class="menu-dropdown" data-menu="theme-menu" style="display: none; top: 100%; right: 0; margin-top: 8px;">
141
- <div class="menu-item" data-action="theme-light">
142
- <span>☀️ Light Theme</span>
143
- </div>
144
- <div class="menu-item" data-action="theme-dark">
145
- <span>🌙 Dark Theme</span>
146
- </div>
147
- <div class="menu-separator"></div>
148
- <div class="menu-item" data-action="settings">
149
- <span>⚙️ Settings</span>
150
- </div>
151
- </div>
152
  </div>
153
  </div>
154
- <div class="header-crypto-list" data-header-crypto-list style="margin-top: 16px; width: 100%;">
155
- <!-- Crypto list will be populated by JavaScript -->
156
- </div>
157
  </header>
158
  <div class="page-container">
159
  <section id="page-overview" class="page active">
@@ -193,30 +95,6 @@
193
  <canvas id="sentiment-chart" height="220"></canvas>
194
  </div>
195
  </div>
196
- <div class="glass-card" style="margin-top: 24px;">
197
- <div class="section-header">
198
- <h3>Backend Information</h3>
199
- <span class="text-muted">System Status</span>
200
- </div>
201
- <div data-backend-info class="backend-info-grid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-top: 16px;">
202
- <div class="backend-info-item">
203
- <span class="info-label">API Status</span>
204
- <span class="info-value" data-api-status>Checking...</span>
205
- </div>
206
- <div class="backend-info-item">
207
- <span class="info-label">WebSocket</span>
208
- <span class="info-value" data-ws-status>Connecting...</span>
209
- </div>
210
- <div class="backend-info-item">
211
- <span class="info-label">Providers</span>
212
- <span class="info-value" data-providers-count>—</span>
213
- </div>
214
- <div class="backend-info-item">
215
- <span class="info-label">Last Update</span>
216
- <span class="info-value" data-last-update>—</span>
217
- </div>
218
- </div>
219
- </div>
220
  </section>
221
 
222
  <section id="page-market" class="page">
@@ -275,53 +153,32 @@
275
 
276
  <section id="page-chart" class="page">
277
  <div class="section-header">
278
- <h2 class="section-title">Chart Lab - TradingView Style</h2>
279
  <div class="controls-bar">
280
- <select data-chart-symbol style="padding: 8px 12px; border-radius: 8px; background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.1); color: var(--text-primary);">
281
  <option value="BTC">BTC</option>
282
  <option value="ETH">ETH</option>
283
  <option value="SOL">SOL</option>
284
  <option value="BNB">BNB</option>
285
- <option value="ADA">ADA</option>
286
- <option value="DOT">DOT</option>
287
- <option value="MATIC">MATIC</option>
288
- <option value="AVAX">AVAX</option>
289
  </select>
290
- <div class="chart-toolbar">
291
- <button class="chart-timeframe-btn active" data-chart-timeframe="1d">1D</button>
292
- <button class="chart-timeframe-btn" data-chart-timeframe="7d">7D</button>
293
- <button class="chart-timeframe-btn" data-chart-timeframe="30d">30D</button>
294
- <button class="chart-timeframe-btn" data-chart-timeframe="90d">90D</button>
295
  </div>
296
  </div>
297
  </div>
298
- <div class="tradingview-chart-container glass-vibrant">
299
- <div class="chart-toolbar">
300
- <div class="chart-indicators">
301
- <label class="chart-indicator-toggle">
302
- <input type="checkbox" data-indicator="MA20" checked />
303
- <span>MA 20</span>
304
- </label>
305
- <label class="chart-indicator-toggle">
306
- <input type="checkbox" data-indicator="MA50" />
307
- <span>MA 50</span>
308
- </label>
309
- <label class="chart-indicator-toggle">
310
- <input type="checkbox" data-indicator="RSI" />
311
- <span>RSI</span>
312
- </label>
313
- <label class="chart-indicator-toggle">
314
- <input type="checkbox" data-indicator="Volume" checked />
315
- <span>Volume</span>
316
- </label>
317
- </div>
318
- </div>
319
- <canvas id="chart-lab-canvas" height="400"></canvas>
320
  </div>
321
- <div class="glass-card glass-vibrant" style="margin-top: 24px;">
322
- <div class="controls-bar" style="margin-bottom: 16px;">
323
- <button class="primary" data-run-analysis style="background: linear-gradient(135deg, #00D4FF, #8B5CF6);">Analyze Chart with AI</button>
 
 
 
324
  </div>
 
325
  <div data-ai-insights class="ai-insights"></div>
326
  </div>
327
  </section>
 
8
  <link rel="stylesheet" href="static/css/design-system.css" />
9
  <link rel="stylesheet" href="static/css/dashboard.css" />
10
  <link rel="stylesheet" href="static/css/pro-dashboard.css" />
 
 
 
11
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js" defer></script>
 
 
 
12
  </head>
13
+ <body data-theme="dark">
14
  <div class="app-shell">
15
+ <aside class="sidebar">
16
+ <div class="brand">
17
+ <strong>Crypto Monitor HF</strong>
18
+ <span class="env-pill">
19
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
20
  <path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="1.5" />
21
  <path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="1.5" />
22
  <path d="M2 12L12 17L22 12" stroke="currentColor" stroke-width="1.5" />
23
  </svg>
24
+ HF Space
25
+ </span>
 
 
 
 
 
 
 
 
26
  </div>
27
+ <nav class="nav">
28
+ <button class="nav-button active" data-nav="page-overview">Overview</button>
29
+ <button class="nav-button" data-nav="page-market">Market</button>
30
+ <button class="nav-button" data-nav="page-chart">Chart Lab</button>
31
+ <button class="nav-button" data-nav="page-ai">Sentiment & AI</button>
32
+ <button class="nav-button" data-nav="page-news">News</button>
33
+ <button class="nav-button" data-nav="page-providers">Providers</button>
34
+ <button class="nav-button" data-nav="page-api">API Explorer</button>
35
+ <button class="nav-button" data-nav="page-debug">Diagnostics</button>
36
+ <button class="nav-button" data-nav="page-datasets">Datasets & Models</button>
37
+ <button class="nav-button" data-nav="page-settings">Settings</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  </nav>
39
  <div class="sidebar-footer">
40
+ Unified crypto intelligence console<br />Realtime data • HF optimized
 
 
 
 
 
41
  </div>
42
  </aside>
43
  <main class="main-area">
44
+ <header class="topbar">
45
+ <div>
46
+ <h1>Unified Intelligence Dashboard</h1>
47
+ <p class="text-muted">Live market telemetry, AI signals, diagnostics, and provider health.</p>
48
+ </div>
49
+ <div class="status-group">
50
+ <div class="status-pill" data-api-health data-state="warn">
51
+ <span class="status-dot"></span>
52
+ <span>checking</span>
53
  </div>
54
+ <div class="status-pill" data-ws-status data-state="warn">
55
+ <span class="status-dot"></span>
56
+ <span>connecting</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  </div>
58
  </div>
 
 
 
59
  </header>
60
  <div class="page-container">
61
  <section id="page-overview" class="page active">
 
95
  <canvas id="sentiment-chart" height="220"></canvas>
96
  </div>
97
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  </section>
99
 
100
  <section id="page-market" class="page">
 
153
 
154
  <section id="page-chart" class="page">
155
  <div class="section-header">
156
+ <h2 class="section-title">Chart Lab</h2>
157
  <div class="controls-bar">
158
+ <select data-chart-symbol>
159
  <option value="BTC">BTC</option>
160
  <option value="ETH">ETH</option>
161
  <option value="SOL">SOL</option>
162
  <option value="BNB">BNB</option>
 
 
 
 
163
  </select>
164
+ <div class="input-chip">
165
+ <button class="ghost active" data-chart-timeframe="7d">7D</button>
166
+ <button class="ghost" data-chart-timeframe="30d">30D</button>
167
+ <button class="ghost" data-chart-timeframe="90d">90D</button>
 
168
  </div>
169
  </div>
170
  </div>
171
+ <div class="glass-card">
172
+ <canvas id="chart-lab-canvas" height="260"></canvas>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  </div>
174
+ <div class="glass-card">
175
+ <div class="controls-bar">
176
+ <label><input type="checkbox" data-indicator value="MA20" checked /> MA 20</label>
177
+ <label><input type="checkbox" data-indicator value="MA50" /> MA 50</label>
178
+ <label><input type="checkbox" data-indicator value="RSI" /> RSI</label>
179
+ <label><input type="checkbox" data-indicator value="Volume" /> Volume</label>
180
  </div>
181
+ <button class="primary" data-run-analysis>Analyze Chart with AI</button>
182
  <div data-ai-insights class="ai-insights"></div>
183
  </div>
184
  </section>