Upload 287 files
Browse files- .dockerignore +16 -121
- DEPLOYMENT_CHECK_REPORT.md +178 -0
- Dockerfile +21 -12
- HF_DOCKER_FIX.md +67 -0
- MODELS_AS_DATA_SOURCES.md +143 -0
- QUICK_START.md +46 -189
- README.md +141 -844
- TEST_ENDPOINTS.sh +88 -161
- __pycache__/config.cpython-313.pyc +0 -0
- admin.html +518 -72
- ai_models.py +50 -148
- api-resources/crypto_resources_unified_2025-11-11.json +0 -0
- api_server_extended.py +911 -18
- app.py +702 -1232
- backend/__pycache__/__init__.cpython-313.pyc +0 -0
- backend/routers/__pycache__/__init__.cpython-313.pyc +0 -0
- backend/routers/__pycache__/hf_connect.cpython-313.pyc +0 -0
- backend/services/__pycache__/__init__.cpython-313.pyc +0 -0
- backend/services/__pycache__/hf_client.cpython-313.pyc +0 -0
- backend/services/__pycache__/hf_registry.cpython-313.pyc +0 -0
- backend/services/auto_discovery_service.py +1 -4
- backend/services/diagnostics_service.py +1 -8
- backend/services/hf_registry.py +45 -68
- collectors/aggregator.py +4 -113
- config.py +24 -470
- dashboard.html +627 -102
- enhanced_server.py +3 -3
- hf_console.html +333 -87
- hf_unified_server.py +10 -2575
- index.html +0 -0
- package.json +1 -13
- production_server.py +1 -1
- provider_validator.py +1 -7
- real_server.py +1 -1
- requirements_hf.txt +32 -10
- simple_server.py +11 -333
- unified_dashboard.html +47 -190
.dockerignore
CHANGED
|
@@ -1,121 +1,16 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
*.
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
dist/
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 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.
|
| 2 |
|
| 3 |
WORKDIR /app
|
| 4 |
|
| 5 |
-
#
|
| 6 |
-
RUN
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
-
# Copy requirements
|
| 9 |
-
COPY
|
| 10 |
-
RUN pip install --no-cache-dir -r requirements.txt
|
| 11 |
|
| 12 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
| 24 |
-
CMD ["
|
|
|
|
| 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 |
-
# 🚀
|
| 2 |
|
| 3 |
-
##
|
| 4 |
|
| 5 |
-
### 1️⃣ نصب وابستگیها
|
| 6 |
```bash
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
pip install -r requirements.txt
|
|
|
|
| 8 |
```
|
| 9 |
|
| 10 |
-
|
| 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 |
-
|
| 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 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
curl http://localhost:8000/api/logs?level=error
|
| 86 |
-
|
| 87 |
-
# جستجو
|
| 88 |
-
curl http://localhost:8000/api/logs?search=timeout
|
| 89 |
```
|
| 90 |
|
| 91 |
-
|
| 92 |
-
```bash
|
| 93 |
-
# Export به JSON
|
| 94 |
-
curl http://localhost:8000/api/logs/export/json?level=error
|
| 95 |
|
| 96 |
-
|
| 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 |
-
|
| 143 |
-
|
| 144 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
|
| 146 |
-
|
| 147 |
-
log_error(LogCategory.PROVIDER, "Provider failed",
|
| 148 |
-
provider_id="etherscan", error="Timeout")
|
| 149 |
-
```
|
| 150 |
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 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 |
-
##
|
| 173 |
|
|
|
|
| 174 |
```bash
|
| 175 |
-
|
| 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 |
-
|
| 201 |
```bash
|
| 202 |
-
|
| 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 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
```
|
| 70 |
-
|
| 71 |
-
###
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
# 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 |
-
[](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 |
-
#
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
local
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
echo
|
| 25 |
-
|
| 26 |
-
if [ "$
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
echo ""
|
| 47 |
-
|
| 48 |
-
test_endpoint "
|
| 49 |
-
test_endpoint "
|
| 50 |
-
test_endpoint "
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
echo ""
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
test_endpoint "
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
echo ""
|
| 88 |
-
echo "
|
| 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 |
-
|
| 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
|
| 7 |
-
<link rel="
|
| 8 |
-
<link rel="
|
| 9 |
-
<link
|
| 10 |
-
<link rel="stylesheet" href="
|
| 11 |
-
<
|
| 12 |
-
<script defer src="/static/js/ui-feedback.js"></script>
|
| 13 |
-
<script defer src="/static/js/admin-app.js"></script>
|
| 14 |
</head>
|
| 15 |
-
<body
|
| 16 |
-
<
|
| 17 |
-
|
| 18 |
-
<
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 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 |
-
<
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
</div>
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
</
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 43 |
-
|
|
|
|
|
|
|
| 44 |
|
| 45 |
if HF_MODE not in ("off", "public", "auth"):
|
| 46 |
HF_MODE = "off"
|
| 47 |
-
logger.warning(f"Invalid HF_MODE,
|
| 48 |
|
| 49 |
if HF_MODE == "auth" and not HF_TOKEN_ENV:
|
| 50 |
HF_MODE = "off"
|
| 51 |
-
logger.warning("HF_MODE='auth' but HF_TOKEN
|
| 52 |
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
"kk08/CryptoBERT",
|
| 56 |
-
"
|
| 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 |
-
|
| 70 |
-
|
| 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 |
-
|
| 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 == "
|
| 146 |
-
|
| 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 |
-
|
| 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 |
-
|
| 171 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 228 |
-
failed.append((key, error_msg))
|
| 229 |
|
| 230 |
self._initialized = True
|
| 231 |
-
status
|
| 232 |
-
return {"status": status, "mode": HF_MODE, "models_loaded": len(loaded), "loaded": loaded, "failed": failed}
|
| 233 |
|
| 234 |
_registry = ModelRegistry()
|
| 235 |
|
| 236 |
-
|
| 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
|
| 246 |
-
return {"label": "neutral", "confidence": 0.0, "scores": {}, "model_count": 0, "error": "
|
| 247 |
|
| 248 |
results, labels_count, total_conf = {}, {"bullish": 0, "bearish": 0, "neutral": 0}, 0.0
|
| 249 |
|
| 250 |
-
|
| 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
|
| 268 |
-
|
| 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
|
| 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 |
-
"
|
| 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 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 249 |
-
"""Serve
|
| 250 |
-
|
| 251 |
-
if
|
| 252 |
-
|
| 253 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 681 |
@app.get("/api/defi")
|
| 682 |
async def get_defi():
|
| 683 |
-
"""DeFi endpoint
|
| 684 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 685 |
|
| 686 |
|
| 687 |
-
# ===== HuggingFace ML Sentiment - NOT IMPLEMENTED =====
|
| 688 |
@app.post("/api/hf/run-sentiment")
|
| 689 |
-
async def
|
| 690 |
-
"""
|
| 691 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
import
|
| 17 |
-
import
|
| 18 |
-
import
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
import
|
| 24 |
-
import
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
#
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
#
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
#
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
#
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
for
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
#
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
"
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
{
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
|
| 127 |
self.last_refresh = time.time()
|
| 128 |
-
|
| 129 |
-
|
| 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)
|
| 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 |
-
|
| 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 |
-
|
| 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(
|
| 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 |
-
|
| 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 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 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 |
-
|
| 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
|
| 7 |
-
<
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
</head>
|
| 15 |
-
<body
|
| 16 |
-
<
|
| 17 |
-
|
| 18 |
-
<
|
| 19 |
-
<
|
| 20 |
-
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
</div>
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 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 |
-
<
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
<h2>System & Rate Limits</h2>
|
| 79 |
-
<span class="badge info" id="system-status-source">/health</span>
|
| 80 |
</div>
|
| 81 |
-
<div
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
<
|
| 85 |
</div>
|
| 86 |
-
<
|
| 87 |
-
|
| 88 |
-
<
|
|
|
|
| 89 |
</div>
|
| 90 |
-
|
| 91 |
-
</article>
|
| 92 |
-
</section>
|
| 93 |
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
</div>
|
| 108 |
-
<
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
</
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 ETH looks weak 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
|
| 203 |
-
if os.path.exists("
|
| 204 |
-
return FileResponse("
|
| 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 |
-
|
| 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
|
| 7 |
-
<
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
</head>
|
| 15 |
-
<body
|
| 16 |
-
<
|
| 17 |
-
|
| 18 |
-
<
|
| 19 |
-
<div>
|
| 20 |
-
<strong>
|
| 21 |
-
<
|
| 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 |
-
<
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 43 |
-
|
| 44 |
-
<
|
| 45 |
-
<
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 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 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
</
|
| 74 |
-
<
|
| 75 |
-
|
| 76 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
</div>
|
| 78 |
-
<
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
</section>
|
| 82 |
|
| 83 |
-
<
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 ETH is weak today 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 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
import
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 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("
|
| 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 |
-
|
| 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("
|
| 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
|
| 2 |
-
uvicorn
|
| 3 |
-
|
| 4 |
-
pydantic
|
| 5 |
-
sqlalchemy
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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("
|
| 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"
|
| 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("
|
| 59 |
except Exception as e:
|
| 60 |
-
print(f"
|
| 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 |
-
|
| 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/
|
| 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="
|
| 20 |
<div class="app-shell">
|
| 21 |
-
<aside class="sidebar
|
| 22 |
-
<div class="brand
|
| 23 |
-
<
|
| 24 |
-
|
|
|
|
| 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 |
-
|
| 30 |
-
|
| 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
|
| 41 |
-
<button class="nav-button
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
</button>
|
| 48 |
-
<button class="nav-button
|
| 49 |
-
|
| 50 |
-
|
| 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 |
-
<
|
| 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="
|
| 120 |
-
<div
|
| 121 |
-
<
|
| 122 |
-
|
| 123 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
</div>
|
| 125 |
-
<div class="status-
|
| 126 |
-
<
|
| 127 |
-
|
| 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
|
| 279 |
<div class="controls-bar">
|
| 280 |
-
<select data-chart-symbol
|
| 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="
|
| 291 |
-
<button class="
|
| 292 |
-
<button class="
|
| 293 |
-
<button class="
|
| 294 |
-
<button class="chart-timeframe-btn" data-chart-timeframe="90d">90D</button>
|
| 295 |
</div>
|
| 296 |
</div>
|
| 297 |
</div>
|
| 298 |
-
<div class="
|
| 299 |
-
<
|
| 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
|
| 322 |
-
<div class="controls-bar"
|
| 323 |
-
<
|
|
|
|
|
|
|
|
|
|
| 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>
|