Upload 143 files
Browse files- .dockerignore +7 -112
- DEPLOYMENT_GUIDE.md +600 -0
- Dockerfile +21 -24
- HUGGINGFACE_DEPLOYMENT.md +349 -0
- QUICK_START.md +152 -191
- README.md +19 -482
- app.py +334 -1471
- backend/__pycache__/__init__.cpython-313.pyc +0 -0
- config.js +1 -1
- config.py +314 -193
- dashboard.html +21 -72
- database.py +396 -581
- database/__init__.py +5 -44
- docker-compose.yml +67 -77
- index.html +896 -1216
- main.py +164 -26
- requirements.txt +3 -58
- start.bat +6 -11
- start_server.py +17 -239
- utils/__init__.py +0 -20
.dockerignore
CHANGED
|
@@ -1,121 +1,16 @@
|
|
| 1 |
-
# Python
|
| 2 |
__pycache__/
|
| 3 |
*.py[cod]
|
| 4 |
*$py.class
|
| 5 |
*.so
|
| 6 |
-
.
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
dist/
|
| 10 |
-
downloads/
|
| 11 |
-
eggs/
|
| 12 |
-
.eggs/
|
| 13 |
-
lib/
|
| 14 |
-
lib64/
|
| 15 |
-
parts/
|
| 16 |
-
sdist/
|
| 17 |
-
var/
|
| 18 |
-
wheels/
|
| 19 |
-
*.egg-info/
|
| 20 |
-
.installed.cfg
|
| 21 |
-
*.egg
|
| 22 |
-
MANIFEST
|
| 23 |
-
pip-log.txt
|
| 24 |
-
pip-delete-this-directory.txt
|
| 25 |
-
|
| 26 |
-
# Virtual environments
|
| 27 |
venv/
|
| 28 |
ENV/
|
| 29 |
-
env/
|
| 30 |
-
.venv
|
| 31 |
-
|
| 32 |
-
# IDE
|
| 33 |
-
.vscode/
|
| 34 |
-
.idea/
|
| 35 |
-
*.swp
|
| 36 |
-
*.swo
|
| 37 |
-
*~
|
| 38 |
.DS_Store
|
| 39 |
-
|
| 40 |
-
# Git
|
| 41 |
-
.git/
|
| 42 |
.gitignore
|
| 43 |
-
.gitattributes
|
| 44 |
-
|
| 45 |
-
# Documentation
|
| 46 |
-
*.md
|
| 47 |
-
docs/
|
| 48 |
-
README*.md
|
| 49 |
-
CHANGELOG.md
|
| 50 |
-
LICENSE
|
| 51 |
-
|
| 52 |
-
# Testing
|
| 53 |
-
.pytest_cache/
|
| 54 |
-
.coverage
|
| 55 |
-
htmlcov/
|
| 56 |
-
.tox/
|
| 57 |
-
.hypothesis/
|
| 58 |
-
tests/
|
| 59 |
-
test_*.py
|
| 60 |
-
|
| 61 |
-
# Logs and databases (will be created in container)
|
| 62 |
*.log
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 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 |
*.py[cod]
|
| 3 |
*$py.class
|
| 4 |
*.so
|
| 5 |
+
.env
|
| 6 |
+
.venv
|
| 7 |
+
env/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
venv/
|
| 9 |
ENV/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
.DS_Store
|
| 11 |
+
.git
|
|
|
|
|
|
|
| 12 |
.gitignore
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
*.log
|
| 14 |
+
*.sqlite
|
| 15 |
+
.idea/
|
| 16 |
+
.vscode/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
DEPLOYMENT_GUIDE.md
CHANGED
|
@@ -0,0 +1,600 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Deployment Guide - Crypto Resource Aggregator
|
| 2 |
+
|
| 3 |
+
## Quick Deployment to Hugging Face Spaces
|
| 4 |
+
|
| 5 |
+
### Method 1: Web Interface (Recommended for Beginners)
|
| 6 |
+
|
| 7 |
+
1. **Create a Hugging Face Account**
|
| 8 |
+
- Go to https://huggingface.co/join
|
| 9 |
+
- Sign up for a free account
|
| 10 |
+
|
| 11 |
+
2. **Create a New Space**
|
| 12 |
+
- Go to https://huggingface.co/new-space
|
| 13 |
+
- Choose a name (e.g., `crypto-resource-aggregator`)
|
| 14 |
+
- Select SDK: **Docker**
|
| 15 |
+
- Choose visibility: **Public** or **Private**
|
| 16 |
+
- Click "Create Space"
|
| 17 |
+
|
| 18 |
+
3. **Upload Files**
|
| 19 |
+
Upload the following files to your Space:
|
| 20 |
+
- `app.py` - Main application file
|
| 21 |
+
- `requirements.txt` - Python dependencies
|
| 22 |
+
- `all_apis_merged_2025.json` - Resource configuration
|
| 23 |
+
- `README.md` - Documentation
|
| 24 |
+
- `Dockerfile` - Docker configuration
|
| 25 |
+
|
| 26 |
+
4. **Wait for Build**
|
| 27 |
+
- The Space will automatically build and deploy
|
| 28 |
+
- This may take 2-5 minutes
|
| 29 |
+
- You'll see the build logs in real-time
|
| 30 |
+
|
| 31 |
+
5. **Access Your API**
|
| 32 |
+
- Once deployed, your API will be available at:
|
| 33 |
+
`https://[your-username]-[space-name].hf.space`
|
| 34 |
+
- Example: `https://username-crypto-resource-aggregator.hf.space`
|
| 35 |
+
|
| 36 |
+
### Method 2: Git CLI (Recommended for Advanced Users)
|
| 37 |
+
|
| 38 |
+
```bash
|
| 39 |
+
# Clone your Space repository
|
| 40 |
+
git clone https://huggingface.co/spaces/[your-username]/[space-name]
|
| 41 |
+
cd [space-name]
|
| 42 |
+
|
| 43 |
+
# Copy all files to the repository
|
| 44 |
+
cp app.py requirements.txt all_apis_merged_2025.json README.md Dockerfile .
|
| 45 |
+
|
| 46 |
+
# Commit and push
|
| 47 |
+
git add .
|
| 48 |
+
git commit -m "Initial deployment of Crypto Resource Aggregator"
|
| 49 |
+
git push
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
---
|
| 53 |
+
|
| 54 |
+
## Alternative Deployment Options
|
| 55 |
+
|
| 56 |
+
### Option 1: Heroku
|
| 57 |
+
|
| 58 |
+
```bash
|
| 59 |
+
# Install Heroku CLI
|
| 60 |
+
# https://devcenter.heroku.com/articles/heroku-cli
|
| 61 |
+
|
| 62 |
+
# Create a new app
|
| 63 |
+
heroku create crypto-resource-aggregator
|
| 64 |
+
|
| 65 |
+
# Create Procfile
|
| 66 |
+
echo "web: python app.py" > Procfile
|
| 67 |
+
|
| 68 |
+
# Deploy
|
| 69 |
+
git add .
|
| 70 |
+
git commit -m "Deploy to Heroku"
|
| 71 |
+
git push heroku main
|
| 72 |
+
|
| 73 |
+
# Open your app
|
| 74 |
+
heroku open
|
| 75 |
+
```
|
| 76 |
+
|
| 77 |
+
### Option 2: Railway
|
| 78 |
+
|
| 79 |
+
```bash
|
| 80 |
+
# Install Railway CLI
|
| 81 |
+
npm i -g @railway/cli
|
| 82 |
+
|
| 83 |
+
# Login
|
| 84 |
+
railway login
|
| 85 |
+
|
| 86 |
+
# Initialize project
|
| 87 |
+
railway init
|
| 88 |
+
|
| 89 |
+
# Deploy
|
| 90 |
+
railway up
|
| 91 |
+
|
| 92 |
+
# Get deployment URL
|
| 93 |
+
railway domain
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
### Option 3: Render
|
| 97 |
+
|
| 98 |
+
1. Go to https://render.com
|
| 99 |
+
2. Click "New +" → "Web Service"
|
| 100 |
+
3. Connect your GitHub repository
|
| 101 |
+
4. Configure:
|
| 102 |
+
- **Build Command**: `pip install -r requirements.txt`
|
| 103 |
+
- **Start Command**: `python app.py`
|
| 104 |
+
- **Environment**: Python 3
|
| 105 |
+
5. Click "Create Web Service"
|
| 106 |
+
|
| 107 |
+
### Option 4: Docker (Self-Hosted)
|
| 108 |
+
|
| 109 |
+
```bash
|
| 110 |
+
# Build the Docker image
|
| 111 |
+
docker build -t crypto-aggregator .
|
| 112 |
+
|
| 113 |
+
# Run the container
|
| 114 |
+
docker run -d -p 7860:7860 --name crypto-aggregator crypto-aggregator
|
| 115 |
+
|
| 116 |
+
# Check logs
|
| 117 |
+
docker logs crypto-aggregator
|
| 118 |
+
|
| 119 |
+
# Stop the container
|
| 120 |
+
docker stop crypto-aggregator
|
| 121 |
+
|
| 122 |
+
# Remove the container
|
| 123 |
+
docker rm crypto-aggregator
|
| 124 |
+
```
|
| 125 |
+
|
| 126 |
+
### Option 5: Docker Compose (Self-Hosted)
|
| 127 |
+
|
| 128 |
+
Create `docker-compose.yml`:
|
| 129 |
+
|
| 130 |
+
```yaml
|
| 131 |
+
version: '3.8'
|
| 132 |
+
|
| 133 |
+
services:
|
| 134 |
+
aggregator:
|
| 135 |
+
build: .
|
| 136 |
+
ports:
|
| 137 |
+
- "7860:7860"
|
| 138 |
+
restart: unless-stopped
|
| 139 |
+
volumes:
|
| 140 |
+
- ./history.db:/app/history.db
|
| 141 |
+
environment:
|
| 142 |
+
- ENVIRONMENT=production
|
| 143 |
+
```
|
| 144 |
+
|
| 145 |
+
Run:
|
| 146 |
+
```bash
|
| 147 |
+
docker-compose up -d
|
| 148 |
+
```
|
| 149 |
+
|
| 150 |
+
### Option 6: AWS EC2
|
| 151 |
+
|
| 152 |
+
```bash
|
| 153 |
+
# Connect to your EC2 instance
|
| 154 |
+
ssh -i your-key.pem ubuntu@your-instance-ip
|
| 155 |
+
|
| 156 |
+
# Install Python and dependencies
|
| 157 |
+
sudo apt update
|
| 158 |
+
sudo apt install python3-pip python3-venv -y
|
| 159 |
+
|
| 160 |
+
# Create virtual environment
|
| 161 |
+
python3 -m venv venv
|
| 162 |
+
source venv/bin/activate
|
| 163 |
+
|
| 164 |
+
# Upload files (from local machine)
|
| 165 |
+
scp -i your-key.pem app.py requirements.txt all_apis_merged_2025.json ubuntu@your-instance-ip:~/
|
| 166 |
+
|
| 167 |
+
# Install dependencies
|
| 168 |
+
pip install -r requirements.txt
|
| 169 |
+
|
| 170 |
+
# Run with nohup
|
| 171 |
+
nohup python app.py > output.log 2>&1 &
|
| 172 |
+
|
| 173 |
+
# Or use systemd service (recommended)
|
| 174 |
+
sudo nano /etc/systemd/system/crypto-aggregator.service
|
| 175 |
+
```
|
| 176 |
+
|
| 177 |
+
Create systemd service file:
|
| 178 |
+
```ini
|
| 179 |
+
[Unit]
|
| 180 |
+
Description=Crypto Resource Aggregator
|
| 181 |
+
After=network.target
|
| 182 |
+
|
| 183 |
+
[Service]
|
| 184 |
+
User=ubuntu
|
| 185 |
+
WorkingDirectory=/home/ubuntu/crypto-aggregator
|
| 186 |
+
ExecStart=/home/ubuntu/venv/bin/python app.py
|
| 187 |
+
Restart=always
|
| 188 |
+
|
| 189 |
+
[Install]
|
| 190 |
+
WantedBy=multi-user.target
|
| 191 |
+
```
|
| 192 |
+
|
| 193 |
+
Enable and start:
|
| 194 |
+
```bash
|
| 195 |
+
sudo systemctl enable crypto-aggregator
|
| 196 |
+
sudo systemctl start crypto-aggregator
|
| 197 |
+
sudo systemctl status crypto-aggregator
|
| 198 |
+
```
|
| 199 |
+
|
| 200 |
+
### Option 7: Google Cloud Run
|
| 201 |
+
|
| 202 |
+
```bash
|
| 203 |
+
# Install gcloud CLI
|
| 204 |
+
# https://cloud.google.com/sdk/docs/install
|
| 205 |
+
|
| 206 |
+
# Authenticate
|
| 207 |
+
gcloud auth login
|
| 208 |
+
|
| 209 |
+
# Set project
|
| 210 |
+
gcloud config set project YOUR_PROJECT_ID
|
| 211 |
+
|
| 212 |
+
# Build and deploy
|
| 213 |
+
gcloud run deploy crypto-aggregator \
|
| 214 |
+
--source . \
|
| 215 |
+
--platform managed \
|
| 216 |
+
--region us-central1 \
|
| 217 |
+
--allow-unauthenticated
|
| 218 |
+
|
| 219 |
+
# Get URL
|
| 220 |
+
gcloud run services describe crypto-aggregator --region us-central1 --format 'value(status.url)'
|
| 221 |
+
```
|
| 222 |
+
|
| 223 |
+
### Option 8: DigitalOcean App Platform
|
| 224 |
+
|
| 225 |
+
1. Go to https://cloud.digitalocean.com/apps
|
| 226 |
+
2. Click "Create App"
|
| 227 |
+
3. Connect your GitHub repository
|
| 228 |
+
4. Configure:
|
| 229 |
+
- **Run Command**: `python app.py`
|
| 230 |
+
- **Environment**: Python 3.11
|
| 231 |
+
- **HTTP Port**: 7860
|
| 232 |
+
5. Click "Deploy"
|
| 233 |
+
|
| 234 |
+
---
|
| 235 |
+
|
| 236 |
+
## Environment Variables (Optional)
|
| 237 |
+
|
| 238 |
+
You can configure the following environment variables:
|
| 239 |
+
|
| 240 |
+
```bash
|
| 241 |
+
# Port (default: 7860)
|
| 242 |
+
export PORT=8000
|
| 243 |
+
|
| 244 |
+
# Log level (default: INFO)
|
| 245 |
+
export LOG_LEVEL=DEBUG
|
| 246 |
+
|
| 247 |
+
# Database path (default: history.db)
|
| 248 |
+
export DATABASE_PATH=/path/to/history.db
|
| 249 |
+
```
|
| 250 |
+
|
| 251 |
+
---
|
| 252 |
+
|
| 253 |
+
## Post-Deployment Testing
|
| 254 |
+
|
| 255 |
+
### 1. Test Health Endpoint
|
| 256 |
+
|
| 257 |
+
```bash
|
| 258 |
+
curl https://your-deployment-url.com/health
|
| 259 |
+
```
|
| 260 |
+
|
| 261 |
+
Expected response:
|
| 262 |
+
```json
|
| 263 |
+
{
|
| 264 |
+
"status": "healthy",
|
| 265 |
+
"timestamp": "2025-11-10T...",
|
| 266 |
+
"resources_loaded": true,
|
| 267 |
+
"database_connected": true
|
| 268 |
+
}
|
| 269 |
+
```
|
| 270 |
+
|
| 271 |
+
### 2. Test Resource Listing
|
| 272 |
+
|
| 273 |
+
```bash
|
| 274 |
+
curl https://your-deployment-url.com/resources
|
| 275 |
+
```
|
| 276 |
+
|
| 277 |
+
### 3. Test Query Endpoint
|
| 278 |
+
|
| 279 |
+
```bash
|
| 280 |
+
curl -X POST https://your-deployment-url.com/query \
|
| 281 |
+
-H "Content-Type: application/json" \
|
| 282 |
+
-d '{
|
| 283 |
+
"resource_type": "market_data",
|
| 284 |
+
"resource_name": "coingecko",
|
| 285 |
+
"endpoint": "/simple/price",
|
| 286 |
+
"params": {
|
| 287 |
+
"ids": "bitcoin",
|
| 288 |
+
"vs_currencies": "usd"
|
| 289 |
+
}
|
| 290 |
+
}'
|
| 291 |
+
```
|
| 292 |
+
|
| 293 |
+
### 4. Test Status Monitoring
|
| 294 |
+
|
| 295 |
+
```bash
|
| 296 |
+
curl https://your-deployment-url.com/status
|
| 297 |
+
```
|
| 298 |
+
|
| 299 |
+
### 5. Run Full Test Suite
|
| 300 |
+
|
| 301 |
+
From your local machine:
|
| 302 |
+
|
| 303 |
+
```bash
|
| 304 |
+
# Update BASE_URL in test_aggregator.py
|
| 305 |
+
# Change: BASE_URL = "http://localhost:7860"
|
| 306 |
+
# To: BASE_URL = "https://your-deployment-url.com"
|
| 307 |
+
|
| 308 |
+
# Run tests
|
| 309 |
+
python test_aggregator.py
|
| 310 |
+
```
|
| 311 |
+
|
| 312 |
+
---
|
| 313 |
+
|
| 314 |
+
## Performance Optimization
|
| 315 |
+
|
| 316 |
+
### 1. Enable Caching
|
| 317 |
+
|
| 318 |
+
Add Redis for caching (optional):
|
| 319 |
+
|
| 320 |
+
```python
|
| 321 |
+
import redis
|
| 322 |
+
import json
|
| 323 |
+
|
| 324 |
+
# Connect to Redis
|
| 325 |
+
redis_client = redis.Redis(host='localhost', port=6379, decode_responses=True)
|
| 326 |
+
|
| 327 |
+
# Cache resource data
|
| 328 |
+
def get_cached_data(key, ttl=300):
|
| 329 |
+
cached = redis_client.get(key)
|
| 330 |
+
if cached:
|
| 331 |
+
return json.loads(cached)
|
| 332 |
+
return None
|
| 333 |
+
|
| 334 |
+
def set_cached_data(key, data, ttl=300):
|
| 335 |
+
redis_client.setex(key, ttl, json.dumps(data))
|
| 336 |
+
```
|
| 337 |
+
|
| 338 |
+
### 2. Use Connection Pooling
|
| 339 |
+
|
| 340 |
+
Already implemented with `aiohttp.ClientSession`
|
| 341 |
+
|
| 342 |
+
### 3. Add Rate Limiting
|
| 343 |
+
|
| 344 |
+
Install:
|
| 345 |
+
```bash
|
| 346 |
+
pip install slowapi
|
| 347 |
+
```
|
| 348 |
+
|
| 349 |
+
Add to `app.py`:
|
| 350 |
+
```python
|
| 351 |
+
from slowapi import Limiter, _rate_limit_exceeded_handler
|
| 352 |
+
from slowapi.util import get_remote_address
|
| 353 |
+
from slowapi.errors import RateLimitExceeded
|
| 354 |
+
|
| 355 |
+
limiter = Limiter(key_func=get_remote_address)
|
| 356 |
+
app.state.limiter = limiter
|
| 357 |
+
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
| 358 |
+
|
| 359 |
+
@app.post("/query")
|
| 360 |
+
@limiter.limit("60/minute")
|
| 361 |
+
async def query_resource(request: Request, query: ResourceQuery):
|
| 362 |
+
# ... existing code
|
| 363 |
+
```
|
| 364 |
+
|
| 365 |
+
### 4. Add Monitoring
|
| 366 |
+
|
| 367 |
+
Use Sentry for error tracking:
|
| 368 |
+
|
| 369 |
+
```bash
|
| 370 |
+
pip install sentry-sdk
|
| 371 |
+
```
|
| 372 |
+
|
| 373 |
+
```python
|
| 374 |
+
import sentry_sdk
|
| 375 |
+
from sentry_sdk.integrations.fastapi import FastApiIntegration
|
| 376 |
+
|
| 377 |
+
sentry_sdk.init(
|
| 378 |
+
dsn="your-sentry-dsn",
|
| 379 |
+
integrations=[FastApiIntegration()],
|
| 380 |
+
traces_sample_rate=1.0,
|
| 381 |
+
)
|
| 382 |
+
```
|
| 383 |
+
|
| 384 |
+
---
|
| 385 |
+
|
| 386 |
+
## Security Best Practices
|
| 387 |
+
|
| 388 |
+
### 1. API Key Management
|
| 389 |
+
|
| 390 |
+
Store API keys in environment variables:
|
| 391 |
+
|
| 392 |
+
```python
|
| 393 |
+
import os
|
| 394 |
+
|
| 395 |
+
API_KEYS = {
|
| 396 |
+
'etherscan': os.getenv('ETHERSCAN_API_KEY', 'default-key'),
|
| 397 |
+
'coinmarketcap': os.getenv('CMC_API_KEY', 'default-key'),
|
| 398 |
+
}
|
| 399 |
+
```
|
| 400 |
+
|
| 401 |
+
### 2. Enable HTTPS
|
| 402 |
+
|
| 403 |
+
Most platforms (Hugging Face, Heroku, etc.) provide HTTPS by default.
|
| 404 |
+
|
| 405 |
+
For self-hosted, use Let's Encrypt:
|
| 406 |
+
|
| 407 |
+
```bash
|
| 408 |
+
# Install Certbot
|
| 409 |
+
sudo apt install certbot python3-certbot-nginx
|
| 410 |
+
|
| 411 |
+
# Get certificate
|
| 412 |
+
sudo certbot --nginx -d your-domain.com
|
| 413 |
+
```
|
| 414 |
+
|
| 415 |
+
### 3. Add Authentication (Optional)
|
| 416 |
+
|
| 417 |
+
```bash
|
| 418 |
+
pip install python-jose[cryptography] passlib[bcrypt]
|
| 419 |
+
```
|
| 420 |
+
|
| 421 |
+
```python
|
| 422 |
+
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
| 423 |
+
from fastapi import Security
|
| 424 |
+
|
| 425 |
+
security = HTTPBearer()
|
| 426 |
+
|
| 427 |
+
@app.post("/query")
|
| 428 |
+
async def query_resource(
|
| 429 |
+
query: ResourceQuery,
|
| 430 |
+
credentials: HTTPAuthorizationCredentials = Security(security)
|
| 431 |
+
):
|
| 432 |
+
# Verify token
|
| 433 |
+
if credentials.credentials != "your-secret-token":
|
| 434 |
+
raise HTTPException(status_code=401, detail="Invalid token")
|
| 435 |
+
# ... existing code
|
| 436 |
+
```
|
| 437 |
+
|
| 438 |
+
---
|
| 439 |
+
|
| 440 |
+
## Monitoring & Maintenance
|
| 441 |
+
|
| 442 |
+
### 1. Monitor Logs
|
| 443 |
+
|
| 444 |
+
Hugging Face Spaces:
|
| 445 |
+
- View logs in the Space settings → "Logs" tab
|
| 446 |
+
|
| 447 |
+
Docker:
|
| 448 |
+
```bash
|
| 449 |
+
docker logs -f crypto-aggregator
|
| 450 |
+
```
|
| 451 |
+
|
| 452 |
+
Systemd:
|
| 453 |
+
```bash
|
| 454 |
+
journalctl -u crypto-aggregator -f
|
| 455 |
+
```
|
| 456 |
+
|
| 457 |
+
### 2. Database Maintenance
|
| 458 |
+
|
| 459 |
+
Backup database regularly:
|
| 460 |
+
|
| 461 |
+
```bash
|
| 462 |
+
# Local backup
|
| 463 |
+
cp history.db history_backup_$(date +%Y%m%d).db
|
| 464 |
+
|
| 465 |
+
# Remote backup
|
| 466 |
+
scp user@server:/path/to/history.db ./backups/
|
| 467 |
+
```
|
| 468 |
+
|
| 469 |
+
Clean old records:
|
| 470 |
+
|
| 471 |
+
```sql
|
| 472 |
+
-- Remove records older than 30 days
|
| 473 |
+
DELETE FROM query_history WHERE timestamp < datetime('now', '-30 days');
|
| 474 |
+
DELETE FROM resource_status WHERE last_check < datetime('now', '-30 days');
|
| 475 |
+
```
|
| 476 |
+
|
| 477 |
+
### 3. Update Resources
|
| 478 |
+
|
| 479 |
+
To add new resources, update `all_apis_merged_2025.json` and redeploy.
|
| 480 |
+
|
| 481 |
+
### 4. Health Checks
|
| 482 |
+
|
| 483 |
+
Set up automated health checks:
|
| 484 |
+
|
| 485 |
+
```bash
|
| 486 |
+
# Cron job (every 5 minutes)
|
| 487 |
+
*/5 * * * * curl https://your-deployment-url.com/health || echo "API is down!"
|
| 488 |
+
```
|
| 489 |
+
|
| 490 |
+
Use UptimeRobot or similar service for monitoring.
|
| 491 |
+
|
| 492 |
+
---
|
| 493 |
+
|
| 494 |
+
## Troubleshooting
|
| 495 |
+
|
| 496 |
+
### Issue: Server won't start
|
| 497 |
+
|
| 498 |
+
**Solution:**
|
| 499 |
+
```bash
|
| 500 |
+
# Check if port 7860 is in use
|
| 501 |
+
lsof -i :7860
|
| 502 |
+
|
| 503 |
+
# Kill existing process
|
| 504 |
+
kill -9 $(lsof -t -i:7860)
|
| 505 |
+
|
| 506 |
+
# Or use a different port
|
| 507 |
+
PORT=8000 python app.py
|
| 508 |
+
```
|
| 509 |
+
|
| 510 |
+
### Issue: Database locked
|
| 511 |
+
|
| 512 |
+
**Solution:**
|
| 513 |
+
```bash
|
| 514 |
+
# Stop all instances
|
| 515 |
+
pkill -f app.py
|
| 516 |
+
|
| 517 |
+
# Remove lock (if exists)
|
| 518 |
+
rm history.db-journal
|
| 519 |
+
|
| 520 |
+
# Restart
|
| 521 |
+
python app.py
|
| 522 |
+
```
|
| 523 |
+
|
| 524 |
+
### Issue: High memory usage
|
| 525 |
+
|
| 526 |
+
**Solution:**
|
| 527 |
+
- Add connection limits
|
| 528 |
+
- Implement request queuing
|
| 529 |
+
- Scale horizontally with multiple instances
|
| 530 |
+
|
| 531 |
+
### Issue: API rate limits
|
| 532 |
+
|
| 533 |
+
**Solution:**
|
| 534 |
+
- Implement caching
|
| 535 |
+
- Add multiple API keys for rotation
|
| 536 |
+
- Use fallback resources
|
| 537 |
+
|
| 538 |
+
---
|
| 539 |
+
|
| 540 |
+
## Scaling
|
| 541 |
+
|
| 542 |
+
### Horizontal Scaling
|
| 543 |
+
|
| 544 |
+
Use a load balancer with multiple instances:
|
| 545 |
+
|
| 546 |
+
```yaml
|
| 547 |
+
# docker-compose-scaled.yml
|
| 548 |
+
version: '3.8'
|
| 549 |
+
|
| 550 |
+
services:
|
| 551 |
+
aggregator:
|
| 552 |
+
build: .
|
| 553 |
+
deploy:
|
| 554 |
+
replicas: 3
|
| 555 |
+
environment:
|
| 556 |
+
- WORKER_ID=${HOSTNAME}
|
| 557 |
+
|
| 558 |
+
nginx:
|
| 559 |
+
image: nginx:alpine
|
| 560 |
+
ports:
|
| 561 |
+
- "80:80"
|
| 562 |
+
volumes:
|
| 563 |
+
- ./nginx.conf:/etc/nginx/nginx.conf
|
| 564 |
+
depends_on:
|
| 565 |
+
- aggregator
|
| 566 |
+
```
|
| 567 |
+
|
| 568 |
+
### Vertical Scaling
|
| 569 |
+
|
| 570 |
+
Increase resources on your hosting platform:
|
| 571 |
+
- Hugging Face: Upgrade to paid tier
|
| 572 |
+
- AWS: Use larger EC2 instance
|
| 573 |
+
- Docker: Adjust container resources
|
| 574 |
+
|
| 575 |
+
---
|
| 576 |
+
|
| 577 |
+
## Support
|
| 578 |
+
|
| 579 |
+
For issues or questions:
|
| 580 |
+
1. Check `/health` endpoint
|
| 581 |
+
2. Review application logs
|
| 582 |
+
3. Test individual resources with `/status`
|
| 583 |
+
4. Verify database with SQLite browser
|
| 584 |
+
|
| 585 |
+
---
|
| 586 |
+
|
| 587 |
+
## Next Steps
|
| 588 |
+
|
| 589 |
+
After deployment:
|
| 590 |
+
|
| 591 |
+
1. **Integrate with your main app** using the provided client examples
|
| 592 |
+
2. **Set up monitoring** with health checks and alerts
|
| 593 |
+
3. **Configure backups** for the history database
|
| 594 |
+
4. **Add custom resources** by updating the JSON file
|
| 595 |
+
5. **Implement caching** for frequently accessed data
|
| 596 |
+
6. **Enable authentication** if needed for security
|
| 597 |
+
|
| 598 |
+
---
|
| 599 |
+
|
| 600 |
+
**Congratulations! Your Crypto Resource Aggregator is now deployed and ready to use!** 🚀
|
Dockerfile
CHANGED
|
@@ -1,41 +1,38 @@
|
|
| 1 |
-
|
| 2 |
-
FROM python:3.11-slim
|
| 3 |
|
| 4 |
-
|
| 5 |
-
ENV PYTHONUNBUFFERED=1 \
|
| 6 |
-
PYTHONDONTWRITEBYTECODE=1 \
|
| 7 |
-
PIP_NO_CACHE_DIR=1 \
|
| 8 |
-
PIP_DISABLE_PIP_VERSION_CHECK=1 \
|
| 9 |
-
ENABLE_AUTO_DISCOVERY=false
|
| 10 |
|
| 11 |
# Install system dependencies
|
| 12 |
-
RUN apt-get update &&
|
| 13 |
-
|
| 14 |
-
|
| 15 |
curl \
|
|
|
|
|
|
|
| 16 |
&& rm -rf /var/lib/apt/lists/*
|
| 17 |
|
| 18 |
-
#
|
| 19 |
-
|
| 20 |
|
| 21 |
-
# Copy
|
| 22 |
COPY requirements.txt .
|
| 23 |
|
| 24 |
# Install Python dependencies
|
| 25 |
-
RUN pip install --no-cache-dir
|
|
|
|
| 26 |
|
| 27 |
# Copy application code
|
| 28 |
COPY . .
|
| 29 |
|
| 30 |
-
#
|
| 31 |
-
RUN
|
| 32 |
|
| 33 |
-
#
|
| 34 |
-
|
|
|
|
| 35 |
|
| 36 |
-
#
|
| 37 |
-
|
| 38 |
-
CMD curl -f http://localhost:${PORT:-8000}/health || exit 1
|
| 39 |
|
| 40 |
-
# Run
|
| 41 |
-
CMD ["
|
|
|
|
| 1 |
+
FROM python:3.9-slim
|
|
|
|
| 2 |
|
| 3 |
+
WORKDIR /code
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
# Install system dependencies
|
| 6 |
+
RUN apt-get update && \
|
| 7 |
+
apt-get install -y --no-install-recommends \
|
| 8 |
+
build-essential \
|
| 9 |
curl \
|
| 10 |
+
git \
|
| 11 |
+
&& apt-get clean \
|
| 12 |
&& rm -rf /var/lib/apt/lists/*
|
| 13 |
|
| 14 |
+
# Create necessary directories
|
| 15 |
+
RUN mkdir -p /code/data /code/logs
|
| 16 |
|
| 17 |
+
# Copy requirements first to leverage Docker cache
|
| 18 |
COPY requirements.txt .
|
| 19 |
|
| 20 |
# Install Python dependencies
|
| 21 |
+
RUN pip install --no-cache-dir --upgrade pip && \
|
| 22 |
+
pip install --no-cache-dir -r requirements.txt
|
| 23 |
|
| 24 |
# Copy application code
|
| 25 |
COPY . .
|
| 26 |
|
| 27 |
+
# Ensure directories have correct permissions
|
| 28 |
+
RUN chmod -R 755 /code/data /code/logs
|
| 29 |
|
| 30 |
+
# Set environment variables
|
| 31 |
+
ENV PYTHONPATH=/code
|
| 32 |
+
ENV PORT=7860
|
| 33 |
|
| 34 |
+
# Expose the port
|
| 35 |
+
EXPOSE 7860
|
|
|
|
| 36 |
|
| 37 |
+
# Run the application
|
| 38 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
HUGGINGFACE_DEPLOYMENT.md
CHANGED
|
@@ -0,0 +1,349 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🤗 HuggingFace Spaces Deployment Guide
|
| 2 |
+
|
| 3 |
+
This guide explains how to deploy the Crypto API Monitoring System to HuggingFace Spaces.
|
| 4 |
+
|
| 5 |
+
## Overview
|
| 6 |
+
|
| 7 |
+
The application is fully optimized for HuggingFace Spaces deployment with:
|
| 8 |
+
- **Docker-based deployment** using the standard HF Spaces port (7860)
|
| 9 |
+
- **Automatic environment detection** for frontend API calls
|
| 10 |
+
- **HuggingFace ML integration** for crypto sentiment analysis
|
| 11 |
+
- **WebSocket support** for real-time data streaming
|
| 12 |
+
- **Persistent data storage** with SQLite
|
| 13 |
+
|
| 14 |
+
## Prerequisites
|
| 15 |
+
|
| 16 |
+
1. A HuggingFace account ([sign up here](https://huggingface.co/join))
|
| 17 |
+
2. Git installed on your local machine
|
| 18 |
+
3. Basic familiarity with Docker and HuggingFace Spaces
|
| 19 |
+
|
| 20 |
+
## Deployment Steps
|
| 21 |
+
|
| 22 |
+
### 1. Create a New Space
|
| 23 |
+
|
| 24 |
+
1. Go to [HuggingFace Spaces](https://huggingface.co/spaces)
|
| 25 |
+
2. Click "Create new Space"
|
| 26 |
+
3. Configure your Space:
|
| 27 |
+
- **Name**: `Datasourceforcryptocurrency` (or your preferred name)
|
| 28 |
+
- **License**: Choose appropriate license (e.g., MIT)
|
| 29 |
+
- **SDK**: Select **Docker**
|
| 30 |
+
- **Visibility**: Public or Private (your choice)
|
| 31 |
+
4. Click "Create Space"
|
| 32 |
+
|
| 33 |
+
### 2. Clone Your Space Repository
|
| 34 |
+
|
| 35 |
+
```bash
|
| 36 |
+
# Clone your newly created space
|
| 37 |
+
git clone https://huggingface.co/spaces/YOUR_USERNAME/YOUR_SPACE_NAME
|
| 38 |
+
cd YOUR_SPACE_NAME
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
### 3. Copy Application Files
|
| 42 |
+
|
| 43 |
+
Copy all files from this repository to your Space directory:
|
| 44 |
+
|
| 45 |
+
```bash
|
| 46 |
+
# Copy all files (adjust paths as needed)
|
| 47 |
+
cp -r /path/to/crypto-dt-source/* .
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
**Essential files for HuggingFace Spaces:**
|
| 51 |
+
- `Dockerfile` - Docker configuration optimized for HF Spaces
|
| 52 |
+
- `requirements.txt` - Python dependencies including transformers
|
| 53 |
+
- `app.py` - Main FastAPI application
|
| 54 |
+
- `config.js` - Frontend configuration with environment detection
|
| 55 |
+
- `*.html` - UI files (index.html, hf_console.html, etc.)
|
| 56 |
+
- All backend directories (`api/`, `backend/`, `monitoring/`, etc.)
|
| 57 |
+
|
| 58 |
+
### 4. Configure Environment Variables (Optional but Recommended)
|
| 59 |
+
|
| 60 |
+
In your HuggingFace Space settings, add these secrets:
|
| 61 |
+
|
| 62 |
+
**Required:**
|
| 63 |
+
- `HUGGINGFACE_TOKEN` - Your HF token for accessing models (optional if using public models)
|
| 64 |
+
|
| 65 |
+
**Optional API Keys (for enhanced data collection):**
|
| 66 |
+
- `ETHERSCAN_KEY_1` - Etherscan API key
|
| 67 |
+
- `COINMARKETCAP_KEY_1` - CoinMarketCap API key
|
| 68 |
+
- `NEWSAPI_KEY` - NewsAPI key
|
| 69 |
+
- `CRYPTOCOMPARE_KEY` - CryptoCompare API key
|
| 70 |
+
|
| 71 |
+
**HuggingFace Configuration:**
|
| 72 |
+
- `ENABLE_SENTIMENT=true` - Enable sentiment analysis
|
| 73 |
+
- `SENTIMENT_SOCIAL_MODEL=ElKulako/cryptobert` - Social sentiment model
|
| 74 |
+
- `SENTIMENT_NEWS_MODEL=kk08/CryptoBERT` - News sentiment model
|
| 75 |
+
- `HF_REGISTRY_REFRESH_SEC=21600` - Registry refresh interval (6 hours)
|
| 76 |
+
|
| 77 |
+
### 5. Push to HuggingFace
|
| 78 |
+
|
| 79 |
+
```bash
|
| 80 |
+
# Add all files
|
| 81 |
+
git add .
|
| 82 |
+
|
| 83 |
+
# Commit changes
|
| 84 |
+
git commit -m "Initial deployment of Crypto API Monitor"
|
| 85 |
+
|
| 86 |
+
# Push to HuggingFace
|
| 87 |
+
git push
|
| 88 |
+
```
|
| 89 |
+
|
| 90 |
+
### 6. Wait for Build
|
| 91 |
+
|
| 92 |
+
HuggingFace Spaces will automatically:
|
| 93 |
+
1. Build your Docker image (takes 5-10 minutes)
|
| 94 |
+
2. Download required ML models
|
| 95 |
+
3. Start the application on port 7860
|
| 96 |
+
4. Run health checks
|
| 97 |
+
|
| 98 |
+
Monitor the build logs in your Space's "Logs" tab.
|
| 99 |
+
|
| 100 |
+
### 7. Access Your Application
|
| 101 |
+
|
| 102 |
+
Once deployed, your application will be available at:
|
| 103 |
+
```
|
| 104 |
+
https://huggingface.co/spaces/YOUR_USERNAME/YOUR_SPACE_NAME
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
## Features Available in HuggingFace Spaces
|
| 108 |
+
|
| 109 |
+
### 🎯 Real-Time Dashboard
|
| 110 |
+
- Access the main dashboard at the root URL
|
| 111 |
+
- Real-time WebSocket updates for all metrics
|
| 112 |
+
- Provider health monitoring
|
| 113 |
+
- System status and analytics
|
| 114 |
+
|
| 115 |
+
### 🤗 HuggingFace Console
|
| 116 |
+
- Access at `/hf_console.html`
|
| 117 |
+
- Test HF model registry
|
| 118 |
+
- Run sentiment analysis
|
| 119 |
+
- Search crypto-related models and datasets
|
| 120 |
+
|
| 121 |
+
### 📊 API Documentation
|
| 122 |
+
- Swagger UI: `/docs`
|
| 123 |
+
- ReDoc: `/redoc`
|
| 124 |
+
- API Info: `/api-info`
|
| 125 |
+
|
| 126 |
+
### 🔌 WebSocket Endpoints
|
| 127 |
+
All WebSocket endpoints are available for real-time data:
|
| 128 |
+
- `/ws` - Master WebSocket endpoint
|
| 129 |
+
- `/ws/market_data` - Market data updates
|
| 130 |
+
- `/ws/news` - News updates
|
| 131 |
+
- `/ws/sentiment` - Sentiment analysis updates
|
| 132 |
+
- `/ws/health` - Health monitoring
|
| 133 |
+
- `/ws/huggingface` - HF integration updates
|
| 134 |
+
|
| 135 |
+
## Local Development & Testing
|
| 136 |
+
|
| 137 |
+
### Using Docker Compose
|
| 138 |
+
|
| 139 |
+
```bash
|
| 140 |
+
# Build and start the application
|
| 141 |
+
docker-compose up --build
|
| 142 |
+
|
| 143 |
+
# Access at http://localhost:7860
|
| 144 |
+
```
|
| 145 |
+
|
| 146 |
+
### Using Docker Directly
|
| 147 |
+
|
| 148 |
+
```bash
|
| 149 |
+
# Build the image
|
| 150 |
+
docker build -t crypto-api-monitor .
|
| 151 |
+
|
| 152 |
+
# Run the container
|
| 153 |
+
docker run -p 7860:7860 \
|
| 154 |
+
-e HUGGINGFACE_TOKEN=your_token \
|
| 155 |
+
-e ENABLE_SENTIMENT=true \
|
| 156 |
+
-v $(pwd)/data:/app/data \
|
| 157 |
+
crypto-api-monitor
|
| 158 |
+
```
|
| 159 |
+
|
| 160 |
+
### Using Python Directly
|
| 161 |
+
|
| 162 |
+
```bash
|
| 163 |
+
# Install dependencies
|
| 164 |
+
pip install -r requirements.txt
|
| 165 |
+
|
| 166 |
+
# Set environment variables
|
| 167 |
+
export ENABLE_SENTIMENT=true
|
| 168 |
+
export HUGGINGFACE_TOKEN=your_token
|
| 169 |
+
|
| 170 |
+
# Run the application
|
| 171 |
+
python app.py
|
| 172 |
+
```
|
| 173 |
+
|
| 174 |
+
## Configuration
|
| 175 |
+
|
| 176 |
+
### Frontend Configuration (`config.js`)
|
| 177 |
+
|
| 178 |
+
The frontend automatically detects the environment:
|
| 179 |
+
- **HuggingFace Spaces**: Uses relative URLs with Space origin
|
| 180 |
+
- **Localhost**: Uses `http://localhost:7860`
|
| 181 |
+
- **Custom Deployment**: Uses current window origin
|
| 182 |
+
|
| 183 |
+
No manual configuration needed!
|
| 184 |
+
|
| 185 |
+
### Backend Configuration
|
| 186 |
+
|
| 187 |
+
Edit `.env` or set environment variables:
|
| 188 |
+
|
| 189 |
+
```bash
|
| 190 |
+
# HuggingFace
|
| 191 |
+
HUGGINGFACE_TOKEN=your_token_here
|
| 192 |
+
ENABLE_SENTIMENT=true
|
| 193 |
+
SENTIMENT_SOCIAL_MODEL=ElKulako/cryptobert
|
| 194 |
+
SENTIMENT_NEWS_MODEL=kk08/CryptoBERT
|
| 195 |
+
HF_REGISTRY_REFRESH_SEC=21600
|
| 196 |
+
HF_HTTP_TIMEOUT=8.0
|
| 197 |
+
|
| 198 |
+
# API Keys (optional)
|
| 199 |
+
ETHERSCAN_KEY_1=your_key
|
| 200 |
+
COINMARKETCAP_KEY_1=your_key
|
| 201 |
+
NEWSAPI_KEY=your_key
|
| 202 |
+
```
|
| 203 |
+
|
| 204 |
+
## Architecture
|
| 205 |
+
|
| 206 |
+
```
|
| 207 |
+
┌─────────────────────────────────────────────────┐
|
| 208 |
+
│ HuggingFace Spaces (Docker) │
|
| 209 |
+
├─────────────────────────────────────────────────┤
|
| 210 |
+
│ │
|
| 211 |
+
│ Frontend (HTML/JS) │
|
| 212 |
+
│ ├── config.js (auto-detects environment) │
|
| 213 |
+
│ ├── index.html (main dashboard) │
|
| 214 |
+
│ └── hf_console.html (HF integration UI) │
|
| 215 |
+
│ │
|
| 216 |
+
│ Backend (FastAPI) │
|
| 217 |
+
│ ├── app.py (main application) │
|
| 218 |
+
│ ├── WebSocket Manager (real-time updates) │
|
| 219 |
+
│ ├── HF Integration (sentiment analysis) │
|
| 220 |
+
│ ├── Data Collectors (200+ APIs) │
|
| 221 |
+
│ └── SQLite Database (persistent storage) │
|
| 222 |
+
│ │
|
| 223 |
+
│ ML Models (HuggingFace Transformers) │
|
| 224 |
+
│ ├── ElKulako/cryptobert │
|
| 225 |
+
│ └── kk08/CryptoBERT │
|
| 226 |
+
│ │
|
| 227 |
+
└─────────────────────────────────────────────────┘
|
| 228 |
+
```
|
| 229 |
+
|
| 230 |
+
## Troubleshooting
|
| 231 |
+
|
| 232 |
+
### Build Fails
|
| 233 |
+
|
| 234 |
+
1. Check Docker logs in HF Spaces
|
| 235 |
+
2. Verify `requirements.txt` has all dependencies
|
| 236 |
+
3. Ensure Dockerfile uses Python 3.10
|
| 237 |
+
4. Check for syntax errors in Python files
|
| 238 |
+
|
| 239 |
+
### Application Won't Start
|
| 240 |
+
|
| 241 |
+
1. Check health endpoint: `https://your-space-url/health`
|
| 242 |
+
2. Review application logs in HF Spaces
|
| 243 |
+
3. Verify port 7860 is exposed in Dockerfile
|
| 244 |
+
4. Check environment variables are set correctly
|
| 245 |
+
|
| 246 |
+
### WebSocket Connections Fail
|
| 247 |
+
|
| 248 |
+
1. Ensure your Space URL uses HTTPS
|
| 249 |
+
2. WebSockets automatically upgrade to WSS on HTTPS
|
| 250 |
+
3. Check browser console for connection errors
|
| 251 |
+
4. Verify CORS settings in `app.py`
|
| 252 |
+
|
| 253 |
+
### Sentiment Analysis Not Working
|
| 254 |
+
|
| 255 |
+
1. Set `HUGGINGFACE_TOKEN` in Space secrets
|
| 256 |
+
2. Verify models are accessible: `ElKulako/cryptobert`, `kk08/CryptoBERT`
|
| 257 |
+
3. Check HF console at `/hf_console.html`
|
| 258 |
+
4. Review logs for model download errors
|
| 259 |
+
|
| 260 |
+
### Performance Issues
|
| 261 |
+
|
| 262 |
+
1. Increase Space hardware tier (if available)
|
| 263 |
+
2. Reduce number of concurrent API monitors
|
| 264 |
+
3. Adjust `HF_REGISTRY_REFRESH_SEC` to longer interval
|
| 265 |
+
4. Consider disabling sentiment analysis if not needed
|
| 266 |
+
|
| 267 |
+
## Resource Requirements
|
| 268 |
+
|
| 269 |
+
**Minimum (Free Tier):**
|
| 270 |
+
- 2 CPU cores
|
| 271 |
+
- 2GB RAM
|
| 272 |
+
- 1GB disk space
|
| 273 |
+
|
| 274 |
+
**Recommended:**
|
| 275 |
+
- 4 CPU cores
|
| 276 |
+
- 4GB RAM
|
| 277 |
+
- 2GB disk space
|
| 278 |
+
- For better ML model performance
|
| 279 |
+
|
| 280 |
+
## Updating Your Space
|
| 281 |
+
|
| 282 |
+
```bash
|
| 283 |
+
# Pull latest changes
|
| 284 |
+
git pull
|
| 285 |
+
|
| 286 |
+
# Make your modifications
|
| 287 |
+
# ...
|
| 288 |
+
|
| 289 |
+
# Commit and push
|
| 290 |
+
git add .
|
| 291 |
+
git commit -m "Update: description of changes"
|
| 292 |
+
git push
|
| 293 |
+
```
|
| 294 |
+
|
| 295 |
+
HuggingFace will automatically rebuild and redeploy.
|
| 296 |
+
|
| 297 |
+
## Security Best Practices
|
| 298 |
+
|
| 299 |
+
1. **Use HF Secrets** for sensitive data (API keys, tokens)
|
| 300 |
+
2. **Don't commit** `.env` files with actual keys
|
| 301 |
+
3. **Review API keys** permissions (read-only when possible)
|
| 302 |
+
4. **Monitor usage** of external APIs to avoid rate limits
|
| 303 |
+
5. **Keep dependencies updated** for security patches
|
| 304 |
+
|
| 305 |
+
## Advanced Configuration
|
| 306 |
+
|
| 307 |
+
### Custom ML Models
|
| 308 |
+
|
| 309 |
+
To use custom sentiment analysis models:
|
| 310 |
+
|
| 311 |
+
```bash
|
| 312 |
+
# Set environment variables in HF Spaces
|
| 313 |
+
SENTIMENT_SOCIAL_MODEL=your-username/your-model
|
| 314 |
+
SENTIMENT_NEWS_MODEL=your-username/another-model
|
| 315 |
+
```
|
| 316 |
+
|
| 317 |
+
### Custom Port (Not Recommended for HF Spaces)
|
| 318 |
+
|
| 319 |
+
HuggingFace Spaces requires port 7860. Don't change unless deploying elsewhere.
|
| 320 |
+
|
| 321 |
+
### Multiple Workers
|
| 322 |
+
|
| 323 |
+
Edit Dockerfile CMD:
|
| 324 |
+
```dockerfile
|
| 325 |
+
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "2"]
|
| 326 |
+
```
|
| 327 |
+
|
| 328 |
+
**Note**: More workers = more memory usage. Adjust based on Space tier.
|
| 329 |
+
|
| 330 |
+
## Support & Resources
|
| 331 |
+
|
| 332 |
+
- **HuggingFace Docs**: https://huggingface.co/docs/hub/spaces
|
| 333 |
+
- **FastAPI Docs**: https://fastapi.tiangolo.com/
|
| 334 |
+
- **Transformers Docs**: https://huggingface.co/docs/transformers/
|
| 335 |
+
- **Project Issues**: https://github.com/nimazasinich/crypto-dt-source/issues
|
| 336 |
+
|
| 337 |
+
## License
|
| 338 |
+
|
| 339 |
+
[Specify your license here]
|
| 340 |
+
|
| 341 |
+
## Contributing
|
| 342 |
+
|
| 343 |
+
Contributions are welcome! Please read the contributing guidelines before submitting PRs.
|
| 344 |
+
|
| 345 |
+
---
|
| 346 |
+
|
| 347 |
+
**Need help?** Open an issue or contact the maintainers.
|
| 348 |
+
|
| 349 |
+
**Enjoy your crypto monitoring dashboard on HuggingFace Spaces! 🚀**
|
QUICK_START.md
CHANGED
|
@@ -1,221 +1,182 @@
|
|
| 1 |
-
# 🚀
|
| 2 |
-
|
| 3 |
-
##
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
```
|
| 9 |
|
| 10 |
-
### 2
|
| 11 |
-
```
|
| 12 |
-
|
| 13 |
```
|
| 14 |
-
این اسکریپت بهطور خودکار همه منابع را از فایلهای JSON موجود import میکند.
|
| 15 |
-
|
| 16 |
-
### 3️⃣ راهاندازی سرور
|
| 17 |
-
```bash
|
| 18 |
-
# روش 1: استفاده از اسکریپت راهانداز
|
| 19 |
-
python start_server.py
|
| 20 |
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
# روش 3: با uvicorn
|
| 25 |
-
uvicorn api_server_extended:app --reload --host 0.0.0.0 --port 8000
|
| 26 |
```
|
| 27 |
|
| 28 |
-
### 4
|
| 29 |
-
```
|
| 30 |
-
|
|
|
|
| 31 |
```
|
| 32 |
|
| 33 |
-
##
|
| 34 |
-
|
| 35 |
-
### 📊 Market
|
| 36 |
-
- آمار کلی بازار
|
| 37 |
-
- لیست کریپتوکارنسیها
|
| 38 |
-
- نمودارها و ترندینگ
|
| 39 |
-
|
| 40 |
-
### 📡 API Monitor
|
| 41 |
-
- وضعیت همه ارائهدهندگان
|
| 42 |
-
- زمان پاسخ
|
| 43 |
-
- Health Check
|
| 44 |
-
|
| 45 |
-
### ⚡ Advanced
|
| 46 |
-
- Export JSON/CSV
|
| 47 |
-
- Backup
|
| 48 |
-
- Clear Cache
|
| 49 |
-
- Activity Logs
|
| 50 |
-
|
| 51 |
-
### ⚙️ Admin
|
| 52 |
-
- افزودن API جدید
|
| 53 |
-
- تنظیمات
|
| 54 |
-
- آمار کلی
|
| 55 |
-
|
| 56 |
-
### 🤗 HuggingFace
|
| 57 |
-
- مدلهای Sentiment Analysis
|
| 58 |
-
- Datasets
|
| 59 |
-
- جستجو در Registry
|
| 60 |
-
|
| 61 |
-
### 🔄 Pools
|
| 62 |
-
- مدیریت Poolها
|
| 63 |
-
- افزودن/حذف اعضا
|
| 64 |
-
- چرخش دستی
|
| 65 |
-
|
| 66 |
-
### 📋 Logs (جدید!)
|
| 67 |
-
- نمایش لاگها با فیلتر
|
| 68 |
-
- Export به JSON/CSV
|
| 69 |
-
- جستجو و آمار
|
| 70 |
-
|
| 71 |
-
### 📦 Resources (جدید!)
|
| 72 |
-
- مدیریت منابع API
|
| 73 |
-
- Import/Export
|
| 74 |
-
- Backup
|
| 75 |
-
- فیلتر بر اساس Category
|
| 76 |
-
|
| 77 |
-
## 🔧 استفاده از API
|
| 78 |
-
|
| 79 |
-
### دریافت لاگها
|
| 80 |
-
```bash
|
| 81 |
-
# همه لاگها
|
| 82 |
-
curl http://localhost:8000/api/logs
|
| 83 |
-
|
| 84 |
-
# فیلتر بر اساس Level
|
| 85 |
-
curl http://localhost:8000/api/logs?level=error
|
| 86 |
-
|
| 87 |
-
# جستجو
|
| 88 |
-
curl http://localhost:8000/api/logs?search=timeout
|
| 89 |
-
```
|
| 90 |
-
|
| 91 |
-
### Export لاگها
|
| 92 |
-
```bash
|
| 93 |
-
# Export به JSON
|
| 94 |
-
curl http://localhost:8000/api/logs/export/json?level=error
|
| 95 |
-
|
| 96 |
-
# Export به CSV
|
| 97 |
-
curl http://localhost:8000/api/logs/export/csv
|
| 98 |
-
```
|
| 99 |
-
|
| 100 |
-
### مدیریت منابع
|
| 101 |
-
```bash
|
| 102 |
-
# دریافت همه منابع
|
| 103 |
-
curl http://localhost:8000/api/resources
|
| 104 |
-
|
| 105 |
-
# Export منابع
|
| 106 |
-
curl http://localhost:8000/api/resources/export/json
|
| 107 |
|
| 108 |
-
|
| 109 |
-
|
|
|
|
| 110 |
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
|
|
|
| 116 |
|
| 117 |
-
###
|
| 118 |
-
|
| 119 |
-
|
|
|
|
| 120 |
|
| 121 |
-
|
| 122 |
|
| 123 |
-
|
| 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 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
from log_manager import log_info, log_error, LogCategory
|
| 141 |
|
| 142 |
-
#
|
| 143 |
-
|
| 144 |
-
|
| 145 |
|
| 146 |
-
#
|
| 147 |
-
|
| 148 |
-
provider_id="etherscan", error="Timeout")
|
| 149 |
-
```
|
| 150 |
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
from provider_manager import ProviderManager
|
| 154 |
-
import asyncio
|
| 155 |
-
|
| 156 |
-
async def main():
|
| 157 |
-
manager = ProviderManager()
|
| 158 |
-
|
| 159 |
-
# Health Check
|
| 160 |
-
await manager.health_check_all()
|
| 161 |
-
|
| 162 |
-
# دریافت Provider از Pool
|
| 163 |
-
provider = manager.get_next_from_pool("primary_market_data_pool")
|
| 164 |
-
if provider:
|
| 165 |
-
print(f"Selected: {provider.name}")
|
| 166 |
-
|
| 167 |
-
await manager.close_session()
|
| 168 |
-
|
| 169 |
-
asyncio.run(main())
|
| 170 |
```
|
| 171 |
|
| 172 |
-
##
|
| 173 |
-
|
| 174 |
-
```bash
|
| 175 |
-
# Build
|
| 176 |
-
docker build -t crypto-monitor .
|
| 177 |
|
| 178 |
-
|
| 179 |
-
docker run -p 8000:8000 crypto-monitor
|
| 180 |
|
| 181 |
-
|
| 182 |
-
docker-compose up -d
|
| 183 |
-
```
|
| 184 |
|
| 185 |
-
##
|
| 186 |
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
# تغییر پورت
|
| 190 |
-
uvicorn api_server_extended:app --port 8001
|
| 191 |
```
|
| 192 |
|
| 193 |
-
|
| 194 |
-
```bash
|
| 195 |
-
# بررسی وجود فایلها
|
| 196 |
-
ls -la api-resources/
|
| 197 |
-
ls -la providers_config*.json
|
| 198 |
-
```
|
| 199 |
-
|
| 200 |
-
### مشکل: Import منابع ناموفق
|
| 201 |
-
```bash
|
| 202 |
-
# بررسی ساختار JSON
|
| 203 |
-
python -m json.tool api-resources/crypto_resources_unified_2025-11-11.json | head -20
|
| 204 |
-
```
|
| 205 |
-
|
| 206 |
-
## 📚 مستندات بیشتر
|
| 207 |
-
|
| 208 |
-
- [README.md](README.md) - مستندات کامل انگلیسی
|
| 209 |
-
- [README_FA.md](README_FA.md) - مستندات کامل فارسی
|
| 210 |
-
- [api-resources/README.md](api-resources/README.md) - راهنمای منابع API
|
| 211 |
|
| 212 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
|
| 214 |
-
|
| 215 |
-
1. لاگها را بررسی کنید: `logs/app.log`
|
| 216 |
-
2. از تب Logs در داشبورد استفاده کنید
|
| 217 |
-
3. آمار سیستم را بررسی کنید: `/api/status`
|
| 218 |
|
| 219 |
-
|
| 220 |
|
| 221 |
-
|
|
|
|
|
|
| 1 |
+
# 🚀 Quick Start Guide - Crypto API Monitor with HuggingFace Integration
|
| 2 |
+
|
| 3 |
+
## ✅ Server is Running!
|
| 4 |
+
|
| 5 |
+
Your application is now live at: **http://localhost:7860**
|
| 6 |
+
|
| 7 |
+
## 📱 Access Points
|
| 8 |
+
|
| 9 |
+
### 1. Main Dashboard (Full Features)
|
| 10 |
+
**URL:** http://localhost:7860/index.html
|
| 11 |
+
|
| 12 |
+
Features:
|
| 13 |
+
- Real-time API monitoring
|
| 14 |
+
- Provider inventory
|
| 15 |
+
- Rate limit tracking
|
| 16 |
+
- Connection logs
|
| 17 |
+
- Schedule management
|
| 18 |
+
- Data freshness monitoring
|
| 19 |
+
- Failure analysis
|
| 20 |
+
- **🤗 HuggingFace Tab** (NEW!)
|
| 21 |
+
|
| 22 |
+
### 2. HuggingFace Console (Standalone)
|
| 23 |
+
**URL:** http://localhost:7860/hf_console.html
|
| 24 |
+
|
| 25 |
+
Features:
|
| 26 |
+
- HF Health Status
|
| 27 |
+
- Models Registry Browser
|
| 28 |
+
- Datasets Registry Browser
|
| 29 |
+
- Local Search (snapshot)
|
| 30 |
+
- Sentiment Analysis (local pipeline)
|
| 31 |
+
|
| 32 |
+
### 3. API Documentation
|
| 33 |
+
**URL:** http://localhost:7860/docs
|
| 34 |
+
|
| 35 |
+
Interactive API documentation with all endpoints
|
| 36 |
+
|
| 37 |
+
## 🤗 HuggingFace Features
|
| 38 |
+
|
| 39 |
+
### Available Endpoints:
|
| 40 |
+
|
| 41 |
+
1. **Health Check**
|
| 42 |
+
```
|
| 43 |
+
GET /api/hf/health
|
| 44 |
+
```
|
| 45 |
+
Returns: Registry health, last refresh time, model/dataset counts
|
| 46 |
+
|
| 47 |
+
2. **Force Refresh Registry**
|
| 48 |
+
```
|
| 49 |
+
POST /api/hf/refresh
|
| 50 |
+
```
|
| 51 |
+
Manually trigger registry update from HuggingFace Hub
|
| 52 |
+
|
| 53 |
+
3. **Get Models Registry**
|
| 54 |
+
```
|
| 55 |
+
GET /api/hf/registry?kind=models
|
| 56 |
+
```
|
| 57 |
+
Returns: List of all cached crypto-related models
|
| 58 |
+
|
| 59 |
+
4. **Get Datasets Registry**
|
| 60 |
+
```
|
| 61 |
+
GET /api/hf/registry?kind=datasets
|
| 62 |
+
```
|
| 63 |
+
Returns: List of all cached crypto-related datasets
|
| 64 |
+
|
| 65 |
+
5. **Search Registry**
|
| 66 |
+
```
|
| 67 |
+
GET /api/hf/search?q=crypto&kind=models
|
| 68 |
+
```
|
| 69 |
+
Search local snapshot for models or datasets
|
| 70 |
+
|
| 71 |
+
6. **Run Sentiment Analysis**
|
| 72 |
+
```
|
| 73 |
+
POST /api/hf/run-sentiment
|
| 74 |
+
Body: {"texts": ["BTC strong", "ETH weak"]}
|
| 75 |
+
```
|
| 76 |
+
Analyze crypto sentiment using local transformers
|
| 77 |
+
|
| 78 |
+
## 🎯 How to Use
|
| 79 |
+
|
| 80 |
+
### Option 1: Main Dashboard
|
| 81 |
+
1. Open http://localhost:7860/index.html in your browser
|
| 82 |
+
2. Click on the **"🤗 HuggingFace"** tab at the top
|
| 83 |
+
3. Explore:
|
| 84 |
+
- Health status
|
| 85 |
+
- Models and datasets registries
|
| 86 |
+
- Search functionality
|
| 87 |
+
- Sentiment analysis
|
| 88 |
+
|
| 89 |
+
### Option 2: Standalone HF Console
|
| 90 |
+
1. Open http://localhost:7860/hf_console.html
|
| 91 |
+
2. All HF features in a clean, focused interface
|
| 92 |
+
3. Perfect for testing and development
|
| 93 |
+
|
| 94 |
+
## 🧪 Test the Integration
|
| 95 |
+
|
| 96 |
+
### Test 1: Check Health
|
| 97 |
+
```powershell
|
| 98 |
+
Invoke-WebRequest -Uri "http://localhost:7860/api/hf/health" -UseBasicParsing | Select-Object -ExpandProperty Content
|
| 99 |
```
|
| 100 |
|
| 101 |
+
### Test 2: Refresh Registry
|
| 102 |
+
```powershell
|
| 103 |
+
Invoke-WebRequest -Uri "http://localhost:7860/api/hf/refresh" -Method POST -UseBasicParsing | Select-Object -ExpandProperty Content
|
| 104 |
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
|
| 106 |
+
### Test 3: Get Models
|
| 107 |
+
```powershell
|
| 108 |
+
Invoke-WebRequest -Uri "http://localhost:7860/api/hf/registry?kind=models" -UseBasicParsing | Select-Object -ExpandProperty Content
|
|
|
|
|
|
|
| 109 |
```
|
| 110 |
|
| 111 |
+
### Test 4: Run Sentiment Analysis
|
| 112 |
+
```powershell
|
| 113 |
+
$body = @{texts = @("BTC strong breakout", "ETH looks weak")} | ConvertTo-Json
|
| 114 |
+
Invoke-WebRequest -Uri "http://localhost:7860/api/hf/run-sentiment" -Method POST -Body $body -ContentType "application/json" -UseBasicParsing | Select-Object -ExpandProperty Content
|
| 115 |
```
|
| 116 |
|
| 117 |
+
## 📊 What's Included
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
|
| 119 |
+
### Seed Models (Always Available):
|
| 120 |
+
- ElKulako/cryptobert
|
| 121 |
+
- kk08/CryptoBERT
|
| 122 |
|
| 123 |
+
### Seed Datasets (Always Available):
|
| 124 |
+
- linxy/CryptoCoin
|
| 125 |
+
- WinkingFace/CryptoLM-Bitcoin-BTC-USDT
|
| 126 |
+
- WinkingFace/CryptoLM-Ethereum-ETH-USDT
|
| 127 |
+
- WinkingFace/CryptoLM-Solana-SOL-USDT
|
| 128 |
+
- WinkingFace/CryptoLM-Ripple-XRP-USDT
|
| 129 |
|
| 130 |
+
### Auto-Discovery:
|
| 131 |
+
- Searches HuggingFace Hub for crypto-related models
|
| 132 |
+
- Searches for sentiment-analysis models
|
| 133 |
+
- Auto-refreshes every 6 hours (configurable)
|
| 134 |
|
| 135 |
+
## ⚙️ Configuration
|
| 136 |
|
| 137 |
+
Edit `.env` file to customize:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
|
| 139 |
+
```env
|
| 140 |
+
# HuggingFace Token (optional, for higher rate limits)
|
| 141 |
+
HUGGINGFACE_TOKEN=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
|
| 142 |
|
| 143 |
+
# Enable/disable local sentiment analysis
|
| 144 |
+
ENABLE_SENTIMENT=true
|
|
|
|
| 145 |
|
| 146 |
+
# Model selection
|
| 147 |
+
SENTIMENT_SOCIAL_MODEL=ElKulako/cryptobert
|
| 148 |
+
SENTIMENT_NEWS_MODEL=kk08/CryptoBERT
|
| 149 |
|
| 150 |
+
# Refresh interval (seconds)
|
| 151 |
+
HF_REGISTRY_REFRESH_SEC=21600
|
|
|
|
|
|
|
| 152 |
|
| 153 |
+
# HTTP timeout (seconds)
|
| 154 |
+
HF_HTTP_TIMEOUT=8.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
```
|
| 156 |
|
| 157 |
+
## 🛑 Stop the Server
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
|
| 159 |
+
Press `CTRL+C` in the terminal where the server is running
|
|
|
|
| 160 |
|
| 161 |
+
Or use the process manager to stop process ID 6
|
|
|
|
|
|
|
| 162 |
|
| 163 |
+
## 🔄 Restart the Server
|
| 164 |
|
| 165 |
+
```powershell
|
| 166 |
+
python simple_server.py
|
|
|
|
|
|
|
| 167 |
```
|
| 168 |
|
| 169 |
+
## 📝 Notes
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
|
| 171 |
+
- **First Load**: The first sentiment analysis may take 30-60 seconds as models download
|
| 172 |
+
- **Registry**: Auto-refreshes every 6 hours, or manually via the UI
|
| 173 |
+
- **Free Resources**: All endpoints use free HuggingFace APIs
|
| 174 |
+
- **No API Key Required**: Works without authentication (with rate limits)
|
| 175 |
+
- **Local Inference**: Sentiment analysis runs locally using transformers
|
| 176 |
|
| 177 |
+
## 🎉 You're All Set!
|
|
|
|
|
|
|
|
|
|
| 178 |
|
| 179 |
+
The application is running and ready to use. Open your browser and explore!
|
| 180 |
|
| 181 |
+
**Main Dashboard:** http://localhost:7860/index.html
|
| 182 |
+
**HF Console:** http://localhost:7860/hf_console.html
|
README.md
CHANGED
|
@@ -1,492 +1,29 @@
|
|
| 1 |
-
|
| 2 |
-
sdk: gradio
|
| 3 |
-
---
|
| 4 |
-
# Crypto-DT-Source
|
| 5 |
|
| 6 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
-
|
|
|
|
| 9 |
|
| 10 |
-
|
| 11 |
|
| 12 |
-
|
| 13 |
-
[](https://www.python.org/downloads/)
|
| 14 |
-
[](https://github.com/psf/black)
|
| 15 |
|
| 16 |
-
|
| 17 |
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
---
|
| 21 |
-
|
| 22 |
-
## 🚀 Quick Start
|
| 23 |
-
|
| 24 |
-
Get up and running in 3 simple steps:
|
| 25 |
-
|
| 26 |
-
```bash
|
| 27 |
-
# 1. Clone the repository
|
| 28 |
-
git clone https://github.com/nimazasinich/crypto-dt-source.git
|
| 29 |
-
cd crypto-dt-source
|
| 30 |
-
|
| 31 |
-
# 2. Install dependencies
|
| 32 |
-
pip install -r requirements.txt
|
| 33 |
-
|
| 34 |
-
# 3. Run the application
|
| 35 |
-
python app.py
|
| 36 |
-
```
|
| 37 |
-
|
| 38 |
-
Open your browser to **http://localhost:7860** 🎉
|
| 39 |
-
|
| 40 |
-
> **Need more help?** See the [complete Quick Start guide](QUICK_START.md) or [Installation Guide](docs/deployment/INSTALL.md)
|
| 41 |
-
|
| 42 |
-
---
|
| 43 |
-
|
| 44 |
-
## ✨ Features
|
| 45 |
-
|
| 46 |
-
### 🔥 Core Capabilities
|
| 47 |
-
|
| 48 |
-
- **Real-Time Data** - Monitor 100+ cryptocurrencies with live price updates
|
| 49 |
-
- **AI-Powered Analysis** - Sentiment analysis using HuggingFace transformers
|
| 50 |
-
- **200+ Free Data Sources** - No API keys required for basic features
|
| 51 |
-
- **Interactive Dashboards** - 6-tab Gradio interface + 10+ HTML dashboards
|
| 52 |
-
- **WebSocket Streaming** - Real-time data streaming via WebSocket API
|
| 53 |
-
- **REST API** - 20+ endpoints for programmatic access
|
| 54 |
-
- **SQLite Database** - Persistent storage with automatic migrations
|
| 55 |
-
|
| 56 |
-
### 🆕 Production Features (Nov 2024)
|
| 57 |
-
|
| 58 |
-
- ✅ **Authentication & Authorization** - JWT tokens + API key management
|
| 59 |
-
- ✅ **Rate Limiting** - Multi-tier protection (30/min, 1000/hour)
|
| 60 |
-
- ✅ **Async Architecture** - 5x faster data collection
|
| 61 |
-
- ✅ **Database Migrations** - Version-controlled schema updates
|
| 62 |
-
- ✅ **Testing Suite** - pytest with 60%+ coverage
|
| 63 |
-
- ✅ **CI/CD Pipeline** - Automated testing & deployment
|
| 64 |
-
- ✅ **Code Quality Tools** - black, flake8, mypy, pylint
|
| 65 |
-
- ✅ **Security Scanning** - Automated vulnerability checks
|
| 66 |
-
|
| 67 |
-
> **See what's new:** [Implementation Fixes](IMPLEMENTATION_FIXES.md) • [Fixes Summary](FIXES_SUMMARY.md)
|
| 68 |
-
|
| 69 |
-
---
|
| 70 |
-
|
| 71 |
-
## 📊 Data Sources
|
| 72 |
-
|
| 73 |
-
### Price & Market Data
|
| 74 |
-
- **CoinGecko** - Top 100+ cryptocurrencies, market cap rankings
|
| 75 |
-
- **CoinCap** - Real-time prices, backup data source
|
| 76 |
-
- **Binance** - Trading volumes, OHLCV data
|
| 77 |
-
- **Kraken** - Historical price data
|
| 78 |
-
- **Messari** - Advanced analytics
|
| 79 |
-
|
| 80 |
-
### News & Sentiment
|
| 81 |
-
- **RSS Feeds** - CoinDesk, Cointelegraph, Bitcoin Magazine, Decrypt
|
| 82 |
-
- **CryptoPanic** - Aggregated crypto news
|
| 83 |
-
- **Reddit** - r/cryptocurrency, r/bitcoin, r/ethtrader
|
| 84 |
-
- **Alternative.me** - Fear & Greed Index
|
| 85 |
-
|
| 86 |
-
### Blockchain Data
|
| 87 |
-
- **Etherscan** - Ethereum blockchain (optional key)
|
| 88 |
-
- **BscScan** - Binance Smart Chain
|
| 89 |
-
- **TronScan** - Tron blockchain
|
| 90 |
-
- **Blockchair** - Multi-chain explorer
|
| 91 |
-
|
| 92 |
-
**All basic features work without API keys!** 🎁
|
| 93 |
-
|
| 94 |
-
---
|
| 95 |
-
|
| 96 |
-
## 🏗️ Architecture
|
| 97 |
-
|
| 98 |
-
```
|
| 99 |
-
crypto-dt-source/
|
| 100 |
-
├── 📱 UI Layer
|
| 101 |
-
│ ├── app.py # Main Gradio dashboard
|
| 102 |
-
│ ├── ui/ # Modular UI components (NEW)
|
| 103 |
-
│ │ ├── dashboard_live.py # Live price dashboard
|
| 104 |
-
│ │ ├── dashboard_charts.py # Historical charts
|
| 105 |
-
│ │ ├── dashboard_news.py # News & sentiment
|
| 106 |
-
│ │ └── ...
|
| 107 |
-
│ └── *.html # 10+ HTML dashboards
|
| 108 |
-
│
|
| 109 |
-
├── 🔌 API Layer
|
| 110 |
-
│ ├── api/
|
| 111 |
-
│ │ ├── endpoints.py # 20+ REST endpoints
|
| 112 |
-
│ │ ├── websocket.py # WebSocket streaming
|
| 113 |
-
│ │ ├── data_endpoints.py # Data delivery
|
| 114 |
-
│ │ └── pool_endpoints.py # Provider management
|
| 115 |
-
│ └── api_server_extended.py # FastAPI server
|
| 116 |
-
│
|
| 117 |
-
├── 💾 Data Layer
|
| 118 |
-
│ ├── database.py # SQLite manager
|
| 119 |
-
│ ├── database/
|
| 120 |
-
│ │ ├── db_manager.py # Connection pooling
|
| 121 |
-
│ │ ├── migrations.py # Schema migrations (NEW)
|
| 122 |
-
│ │ └── models.py # Data models
|
| 123 |
-
│ └── collectors/
|
| 124 |
-
│ ├���─ market_data.py # Price collection
|
| 125 |
-
│ ├── news.py # News aggregation
|
| 126 |
-
│ ├── sentiment.py # Sentiment analysis
|
| 127 |
-
│ └── ...
|
| 128 |
-
│
|
| 129 |
-
├── 🤖 AI Layer
|
| 130 |
-
│ ├── ai_models.py # HuggingFace integration
|
| 131 |
-
│ └── crypto_data_bank/ai/ # Alternative AI engine
|
| 132 |
-
│
|
| 133 |
-
├── 🛠️ Utilities
|
| 134 |
-
│ ├── utils.py # General utilities
|
| 135 |
-
│ ├── utils/
|
| 136 |
-
│ │ ├── async_api_client.py # Async HTTP client (NEW)
|
| 137 |
-
│ │ ├── auth.py # Authentication (NEW)
|
| 138 |
-
│ │ └── rate_limiter_enhanced.py # Rate limiting (NEW)
|
| 139 |
-
│ └── monitoring/
|
| 140 |
-
│ ├── health_monitor.py # Health checks
|
| 141 |
-
│ └── scheduler.py # Background tasks
|
| 142 |
-
│
|
| 143 |
-
├── 🧪 Testing
|
| 144 |
-
│ ├── tests/
|
| 145 |
-
│ │ ├── test_database.py # Database tests (NEW)
|
| 146 |
-
│ │ ├── test_async_api_client.py # Async tests (NEW)
|
| 147 |
-
│ │ └── ...
|
| 148 |
-
│ └── pytest.ini # Test configuration
|
| 149 |
-
│
|
| 150 |
-
├── ⚙️ Configuration
|
| 151 |
-
│ ├── config.py # Application config
|
| 152 |
-
│ ├── .env.example # Environment template
|
| 153 |
-
│ ├── requirements.txt # Production deps
|
| 154 |
-
│ ├── requirements-dev.txt # Dev dependencies (NEW)
|
| 155 |
-
│ ├── pyproject.toml # Tool config (NEW)
|
| 156 |
-
│ └── .flake8 # Linting config (NEW)
|
| 157 |
-
│
|
| 158 |
-
└── 📚 Documentation
|
| 159 |
-
├── README.md # This file
|
| 160 |
-
├── CHANGELOG.md # Version history
|
| 161 |
-
├── QUICK_START.md # Quick start guide
|
| 162 |
-
├── IMPLEMENTATION_FIXES.md # Latest improvements (NEW)
|
| 163 |
-
├── FIXES_SUMMARY.md # Fixes summary (NEW)
|
| 164 |
-
└── docs/ # Organized documentation (NEW)
|
| 165 |
-
├── INDEX.md # Documentation index
|
| 166 |
-
├── deployment/ # Deployment guides
|
| 167 |
-
├── components/ # Component docs
|
| 168 |
-
├── reports/ # Analysis reports
|
| 169 |
-
├── guides/ # How-to guides
|
| 170 |
-
├── persian/ # Persian/Farsi docs
|
| 171 |
-
└── archive/ # Historical docs
|
| 172 |
-
```
|
| 173 |
-
|
| 174 |
-
---
|
| 175 |
-
|
| 176 |
-
## 🎯 Use Cases
|
| 177 |
-
|
| 178 |
-
### For Traders
|
| 179 |
-
- Real-time price monitoring across 100+ coins
|
| 180 |
-
- AI sentiment analysis from news and social media
|
| 181 |
-
- Technical indicators (RSI, MACD, Moving Averages)
|
| 182 |
-
- Fear & Greed Index tracking
|
| 183 |
-
|
| 184 |
-
### For Developers
|
| 185 |
-
- REST API for building crypto applications
|
| 186 |
-
- WebSocket streaming for real-time updates
|
| 187 |
-
- 200+ free data sources aggregated
|
| 188 |
-
- Well-documented, modular codebase
|
| 189 |
-
|
| 190 |
-
### For Researchers
|
| 191 |
-
- Historical price data and analysis
|
| 192 |
-
- Sentiment analysis on crypto news
|
| 193 |
-
- Database of aggregated market data
|
| 194 |
-
- Export data to CSV for analysis
|
| 195 |
-
|
| 196 |
-
### For DevOps
|
| 197 |
-
- Docker containerization ready
|
| 198 |
-
- HuggingFace Spaces deployment
|
| 199 |
-
- Health monitoring endpoints
|
| 200 |
-
- Automated testing and CI/CD
|
| 201 |
-
|
| 202 |
-
---
|
| 203 |
-
|
| 204 |
-
## 🔧 Installation & Setup
|
| 205 |
-
|
| 206 |
-
### Prerequisites
|
| 207 |
-
- Python 3.8 or higher
|
| 208 |
-
- 4GB+ RAM (for AI models)
|
| 209 |
-
- Internet connection
|
| 210 |
-
|
| 211 |
-
### Basic Installation
|
| 212 |
-
|
| 213 |
-
```bash
|
| 214 |
-
# Install dependencies
|
| 215 |
-
pip install -r requirements.txt
|
| 216 |
-
|
| 217 |
-
# Run application
|
| 218 |
-
python app.py
|
| 219 |
-
```
|
| 220 |
-
|
| 221 |
-
### Development Setup
|
| 222 |
-
|
| 223 |
-
```bash
|
| 224 |
-
# Install dev dependencies
|
| 225 |
-
pip install -r requirements-dev.txt
|
| 226 |
-
|
| 227 |
-
# Run tests
|
| 228 |
-
pytest --cov=.
|
| 229 |
-
|
| 230 |
-
# Format code
|
| 231 |
-
black .
|
| 232 |
-
isort .
|
| 233 |
-
|
| 234 |
-
# Lint
|
| 235 |
-
flake8 .
|
| 236 |
-
mypy .
|
| 237 |
-
```
|
| 238 |
-
|
| 239 |
-
### Production Deployment
|
| 240 |
-
|
| 241 |
-
```bash
|
| 242 |
-
# Set environment variables
|
| 243 |
-
cp .env.example .env
|
| 244 |
-
# Edit .env with your configuration
|
| 245 |
-
|
| 246 |
-
# Run database migrations
|
| 247 |
-
python -c "from database.migrations import auto_migrate; auto_migrate('data/database/crypto_aggregator.db')"
|
| 248 |
-
|
| 249 |
-
# Enable authentication
|
| 250 |
-
export ENABLE_AUTH=true
|
| 251 |
-
export SECRET_KEY=$(python -c "import secrets; print(secrets.token_urlsafe(32))")
|
| 252 |
-
|
| 253 |
-
# Start application
|
| 254 |
-
python app.py
|
| 255 |
-
```
|
| 256 |
-
|
| 257 |
-
### Docker Deployment
|
| 258 |
|
|
|
|
| 259 |
```bash
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
# Run container
|
| 264 |
-
docker run -p 7860:7860 -v $(pwd)/data:/app/data crypto-dt-source
|
| 265 |
-
|
| 266 |
-
# Or use docker-compose
|
| 267 |
-
docker-compose up -d
|
| 268 |
```
|
| 269 |
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
---
|
| 273 |
-
|
| 274 |
-
## 📖 Documentation
|
| 275 |
-
|
| 276 |
-
### Getting Started
|
| 277 |
-
- 📘 [Quick Start Guide](QUICK_START.md) - Get running in 3 steps
|
| 278 |
-
- 📘 [Installation Guide](docs/deployment/INSTALL.md) - Detailed installation
|
| 279 |
-
- 📘 [راهنمای فارسی](docs/persian/README_FA.md) - Persian/Farsi guide
|
| 280 |
-
|
| 281 |
-
### Core Documentation
|
| 282 |
-
- 📗 [Implementation Fixes](IMPLEMENTATION_FIXES.md) - Latest production improvements
|
| 283 |
-
- 📗 [Fixes Summary](FIXES_SUMMARY.md) - Quick reference
|
| 284 |
-
- 📗 [Changelog](CHANGELOG.md) - Version history
|
| 285 |
-
|
| 286 |
-
### Component Documentation
|
| 287 |
-
- 📙 [WebSocket API](docs/components/WEBSOCKET_API_DOCUMENTATION.md) - Real-time streaming
|
| 288 |
-
- 📙 [Data Collectors](docs/components/COLLECTORS_README.md) - Data collection system
|
| 289 |
-
- 📙 [Gradio Dashboard](docs/components/GRADIO_DASHBOARD_README.md) - UI documentation
|
| 290 |
-
- 📙 [Backend Services](docs/components/README_BACKEND.md) - Backend architecture
|
| 291 |
-
|
| 292 |
-
### Deployment & DevOps
|
| 293 |
-
- 📕 [Deployment Guide](docs/deployment/DEPLOYMENT_GUIDE.md) - General deployment
|
| 294 |
-
- 📕 [Production Guide](docs/deployment/PRODUCTION_DEPLOYMENT_GUIDE.md) - Production setup
|
| 295 |
-
- 📕 [HuggingFace Deployment](docs/deployment/HUGGINGFACE_DEPLOYMENT.md) - Cloud deployment
|
| 296 |
-
|
| 297 |
-
### Reports & Analysis
|
| 298 |
-
- 📔 [Project Analysis](docs/reports/PROJECT_ANALYSIS_COMPLETE.md) - 40,600+ line analysis
|
| 299 |
-
- 📔 [Production Audit](docs/reports/PRODUCTION_AUDIT_COMPREHENSIVE.md) - Security audit
|
| 300 |
-
- 📔 [System Capabilities](docs/reports/SYSTEM_CAPABILITIES_REPORT.md) - Feature overview
|
| 301 |
-
|
| 302 |
-
### Complete Index
|
| 303 |
-
📚 **[Full Documentation Index](docs/INDEX.md)** - Browse all 60+ documentation files
|
| 304 |
-
|
| 305 |
-
---
|
| 306 |
-
|
| 307 |
-
## 🔐 Security & Authentication
|
| 308 |
-
|
| 309 |
-
### Authentication (Optional)
|
| 310 |
-
|
| 311 |
-
Enable authentication for production deployments:
|
| 312 |
-
|
| 313 |
-
```bash
|
| 314 |
-
# .env configuration
|
| 315 |
-
ENABLE_AUTH=true
|
| 316 |
-
SECRET_KEY=your-secret-key-here
|
| 317 |
-
ADMIN_USERNAME=admin
|
| 318 |
-
ADMIN_PASSWORD=secure-password
|
| 319 |
-
ACCESS_TOKEN_EXPIRE_MINUTES=60
|
| 320 |
-
API_KEYS=key1,key2,key3
|
| 321 |
-
```
|
| 322 |
-
|
| 323 |
-
**Features:**
|
| 324 |
-
- JWT token authentication
|
| 325 |
-
- API key management
|
| 326 |
-
- Password hashing (SHA-256)
|
| 327 |
-
- Token expiration
|
| 328 |
-
- Usage tracking
|
| 329 |
-
|
| 330 |
-
> **Learn more:** [Authentication Guide](IMPLEMENTATION_FIXES.md#3-authentication--authorization-system)
|
| 331 |
-
|
| 332 |
-
### Rate Limiting
|
| 333 |
-
|
| 334 |
-
Protect your API from abuse:
|
| 335 |
-
|
| 336 |
-
- **30 requests/minute** per client
|
| 337 |
-
- **1,000 requests/hour** per client
|
| 338 |
-
- **Burst protection** up to 10 requests
|
| 339 |
-
|
| 340 |
-
> **Learn more:** [Rate Limiting Guide](IMPLEMENTATION_FIXES.md#4-enhanced-rate-limiting-system)
|
| 341 |
-
|
| 342 |
-
---
|
| 343 |
-
|
| 344 |
-
## 🧪 Testing
|
| 345 |
-
|
| 346 |
-
```bash
|
| 347 |
-
# Install test dependencies
|
| 348 |
-
pip install -r requirements-dev.txt
|
| 349 |
-
|
| 350 |
-
# Run all tests
|
| 351 |
-
pytest
|
| 352 |
-
|
| 353 |
-
# Run with coverage
|
| 354 |
-
pytest --cov=. --cov-report=html
|
| 355 |
-
|
| 356 |
-
# Run specific test file
|
| 357 |
-
pytest tests/test_database.py -v
|
| 358 |
-
|
| 359 |
-
# Run integration tests
|
| 360 |
-
pytest tests/test_integration.py
|
| 361 |
-
```
|
| 362 |
-
|
| 363 |
-
**Test Coverage:** 60%+ (target: 80%)
|
| 364 |
-
|
| 365 |
-
> **Learn more:** [Testing Guide](IMPLEMENTATION_FIXES.md#6-comprehensive-testing-suite)
|
| 366 |
-
|
| 367 |
-
---
|
| 368 |
-
|
| 369 |
-
## 🚢 CI/CD Pipeline
|
| 370 |
-
|
| 371 |
-
Automated testing on every push:
|
| 372 |
-
|
| 373 |
-
- ✅ Code quality checks (black, flake8, mypy)
|
| 374 |
-
- ✅ Tests on Python 3.8, 3.9, 3.10, 3.11
|
| 375 |
-
- ✅ Security scanning (bandit, safety)
|
| 376 |
-
- ✅ Docker build verification
|
| 377 |
-
- ✅ Integration tests
|
| 378 |
-
- ✅ Performance benchmarks
|
| 379 |
-
|
| 380 |
-
> **See:** [.github/workflows/ci.yml](.github/workflows/ci.yml)
|
| 381 |
-
|
| 382 |
-
---
|
| 383 |
-
|
| 384 |
-
## 📊 Performance
|
| 385 |
-
|
| 386 |
-
### Optimizations Implemented
|
| 387 |
-
- ⚡ **5x faster** data collection (async parallel requests)
|
| 388 |
-
- ⚡ **3x faster** database queries (optimized indices)
|
| 389 |
-
- ⚡ **10x reduced** API calls (TTL-based caching)
|
| 390 |
-
- ⚡ **Better resource** utilization (async I/O)
|
| 391 |
-
|
| 392 |
-
### Benchmarks
|
| 393 |
-
- Data collection: ~30 seconds for 100 coins
|
| 394 |
-
- Database queries: <10ms average
|
| 395 |
-
- WebSocket latency: <100ms
|
| 396 |
-
- Memory usage: ~500MB (with AI models loaded)
|
| 397 |
-
|
| 398 |
-
---
|
| 399 |
-
|
| 400 |
-
## 🤝 Contributing
|
| 401 |
-
|
| 402 |
-
We welcome contributions! Here's how:
|
| 403 |
-
|
| 404 |
-
1. **Fork** the repository
|
| 405 |
-
2. **Create** a feature branch (`git checkout -b feature/amazing-feature`)
|
| 406 |
-
3. **Make** your changes with tests
|
| 407 |
-
4. **Run** quality checks (`black . && flake8 . && pytest`)
|
| 408 |
-
5. **Commit** with descriptive message
|
| 409 |
-
6. **Push** to your branch
|
| 410 |
-
7. **Open** a Pull Request
|
| 411 |
-
|
| 412 |
-
**Guidelines:**
|
| 413 |
-
- Follow code style (black, isort)
|
| 414 |
-
- Add tests for new features
|
| 415 |
-
- Update documentation
|
| 416 |
-
- Check [Pull Request Checklist](docs/guides/PR_CHECKLIST.md)
|
| 417 |
-
|
| 418 |
-
---
|
| 419 |
-
|
| 420 |
-
## 📜 License
|
| 421 |
-
|
| 422 |
-
This project is licensed under the **MIT License** - see the [LICENSE](LICENSE) file for details.
|
| 423 |
-
|
| 424 |
-
---
|
| 425 |
-
|
| 426 |
-
## 🙏 Acknowledgments
|
| 427 |
-
|
| 428 |
-
### AI Models
|
| 429 |
-
- [HuggingFace](https://huggingface.co/) - Transformers library
|
| 430 |
-
- [Cardiff NLP](https://huggingface.co/cardiffnlp) - Twitter sentiment model
|
| 431 |
-
- [ProsusAI](https://huggingface.co/ProsusAI) - FinBERT model
|
| 432 |
-
- [Facebook](https://huggingface.co/facebook) - BART summarization
|
| 433 |
-
|
| 434 |
-
### Data Sources
|
| 435 |
-
- [CoinGecko](https://www.coingecko.com/) - Free crypto API
|
| 436 |
-
- [CoinCap](https://coincap.io/) - Real-time data
|
| 437 |
-
- [Binance](https://www.binance.com/) - Trading data
|
| 438 |
-
- [Alternative.me](https://alternative.me/) - Fear & Greed Index
|
| 439 |
-
|
| 440 |
-
### Frameworks & Libraries
|
| 441 |
-
- [Gradio](https://gradio.app/) - Web UI framework
|
| 442 |
-
- [FastAPI](https://fastapi.tiangolo.com/) - REST API
|
| 443 |
-
- [Plotly](https://plotly.com/) - Interactive charts
|
| 444 |
-
- [PyTorch](https://pytorch.org/) - Deep learning
|
| 445 |
-
|
| 446 |
-
---
|
| 447 |
-
|
| 448 |
-
## 📞 Support
|
| 449 |
-
|
| 450 |
-
- **Issues:** [GitHub Issues](https://github.com/nimazasinich/crypto-dt-source/issues)
|
| 451 |
-
- **Documentation:** [docs/](docs/INDEX.md)
|
| 452 |
-
- **Changelog:** [CHANGELOG.md](CHANGELOG.md)
|
| 453 |
-
|
| 454 |
-
---
|
| 455 |
-
|
| 456 |
-
## 🗺️ Roadmap
|
| 457 |
-
|
| 458 |
-
### Short-term (Q4 2024)
|
| 459 |
-
- [x] Modular UI architecture
|
| 460 |
-
- [x] Authentication system
|
| 461 |
-
- [x] Rate limiting
|
| 462 |
-
- [x] Database migrations
|
| 463 |
-
- [x] Testing suite
|
| 464 |
-
- [x] CI/CD pipeline
|
| 465 |
-
- [ ] 80%+ test coverage
|
| 466 |
-
- [ ] GraphQL API
|
| 467 |
-
|
| 468 |
-
### Medium-term (Q1 2025)
|
| 469 |
-
- [ ] Microservices architecture
|
| 470 |
-
- [ ] Message queue (Redis/RabbitMQ)
|
| 471 |
-
- [ ] Database replication
|
| 472 |
-
- [ ] Multi-tenancy support
|
| 473 |
-
- [ ] Advanced ML models
|
| 474 |
-
|
| 475 |
-
### Long-term (2025)
|
| 476 |
-
- [ ] Kubernetes deployment
|
| 477 |
-
- [ ] Multi-region support
|
| 478 |
-
- [ ] Premium data sources
|
| 479 |
-
- [ ] Enterprise features
|
| 480 |
-
- [ ] Mobile app
|
| 481 |
-
|
| 482 |
-
---
|
| 483 |
-
|
| 484 |
-
<div align="center">
|
| 485 |
-
|
| 486 |
-
**Made with ❤️ for the crypto community**
|
| 487 |
-
|
| 488 |
-
⭐ **Star us on GitHub** if you find this project useful!
|
| 489 |
-
|
| 490 |
-
[Documentation](docs/INDEX.md) • [Quick Start](QUICK_START.md) • [فارسی](docs/persian/README_FA.md) • [Changelog](CHANGELOG.md)
|
| 491 |
-
|
| 492 |
-
</div>
|
|
|
|
| 1 |
+
# 🚀 Crypto API Monitor Pro v2.0
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
+
## ویژگیها
|
| 4 |
+
✅ 40+ Provider (Exchanges, Data, DeFi, NFT, Blockchain)
|
| 5 |
+
✅ 20 Cryptocurrency با داده کامل
|
| 6 |
+
✅ UI حرفهای Dark Mode
|
| 7 |
+
✅ Real-time WebSocket
|
| 8 |
+
✅ نمودارهای تعاملی
|
| 9 |
+
✅ آمار و تحلیل کامل
|
| 10 |
|
| 11 |
+
## Providers:
|
| 12 |
+
**Exchanges:** Binance, Coinbase, Kraken, Huobi, KuCoin, Bitfinex, Bitstamp, Gemini, OKX, Bybit, Gate.io, Crypto.com, Bittrex, Poloniex, MEXC
|
| 13 |
|
| 14 |
+
**Data:** CoinGecko, CoinMarketCap, CryptoCompare, Messari, Glassnode, Santiment, Kaiko, Nomics
|
| 15 |
|
| 16 |
+
**DeFi:** Uniswap, SushiSwap, PancakeSwap, Curve, 1inch, Aave, Compound, MakerDAO
|
|
|
|
|
|
|
| 17 |
|
| 18 |
+
**NFT:** OpenSea, Blur, Magic Eden, Rarible
|
| 19 |
|
| 20 |
+
**Blockchain:** Etherscan, BscScan, Polygonscan, Blockchair, Blockchain.com
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
+
## راهاندازی
|
| 23 |
```bash
|
| 24 |
+
1. دابل کلیک start.bat
|
| 25 |
+
2. برو http://localhost:8000/dashboard
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
```
|
| 27 |
|
| 28 |
+
## نیاز
|
| 29 |
+
Python 3.8+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app.py
CHANGED
|
@@ -1,1495 +1,358 @@
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
"""
|
| 3 |
-
Crypto
|
| 4 |
-
|
| 5 |
"""
|
| 6 |
|
| 7 |
-
import
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
from
|
| 11 |
-
|
|
|
|
| 12 |
import json
|
| 13 |
-
import
|
| 14 |
-
import
|
| 15 |
-
import logging
|
| 16 |
-
from typing import List, Dict, Optional, Tuple, Any
|
| 17 |
-
import traceback
|
| 18 |
-
|
| 19 |
-
# Import local modules
|
| 20 |
-
import config
|
| 21 |
-
from database import Database
|
| 22 |
-
import collectors
|
| 23 |
-
import ai_models
|
| 24 |
-
import utils
|
| 25 |
-
|
| 26 |
-
# Setup logging
|
| 27 |
-
logger = utils.setup_logging()
|
| 28 |
-
|
| 29 |
-
# Initialize database
|
| 30 |
-
db = Database()
|
| 31 |
-
|
| 32 |
-
# Global state for background collection
|
| 33 |
-
_collection_started = False
|
| 34 |
-
_collection_lock = threading.Lock()
|
| 35 |
-
|
| 36 |
-
# ==================== TAB 1: LIVE DASHBOARD ====================
|
| 37 |
-
|
| 38 |
-
def get_live_dashboard(search_filter: str = "") -> pd.DataFrame:
|
| 39 |
-
"""
|
| 40 |
-
Get live dashboard data with top 100 cryptocurrencies
|
| 41 |
-
|
| 42 |
-
Args:
|
| 43 |
-
search_filter: Search/filter text for cryptocurrencies
|
| 44 |
-
|
| 45 |
-
Returns:
|
| 46 |
-
DataFrame with formatted cryptocurrency data
|
| 47 |
-
"""
|
| 48 |
-
try:
|
| 49 |
-
logger.info("Fetching live dashboard data...")
|
| 50 |
-
|
| 51 |
-
# Get latest prices from database
|
| 52 |
-
prices = db.get_latest_prices(100)
|
| 53 |
|
| 54 |
-
|
| 55 |
-
logger.warning("No price data available")
|
| 56 |
-
return pd.DataFrame({
|
| 57 |
-
"Rank": [],
|
| 58 |
-
"Name": [],
|
| 59 |
-
"Symbol": [],
|
| 60 |
-
"Price (USD)": [],
|
| 61 |
-
"24h Change (%)": [],
|
| 62 |
-
"Volume": [],
|
| 63 |
-
"Market Cap": []
|
| 64 |
-
})
|
| 65 |
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
symbol_lower = (price.get('symbol') or '').lower()
|
| 74 |
|
| 75 |
-
|
| 76 |
-
|
|
|
|
| 77 |
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
"Symbol": price.get('symbol', 'N/A').upper(),
|
| 82 |
-
"Price (USD)": f"${price.get('price_usd', 0):,.2f}" if price.get('price_usd') else "N/A",
|
| 83 |
-
"24h Change (%)": f"{price.get('percent_change_24h', 0):+.2f}%" if price.get('percent_change_24h') is not None else "N/A",
|
| 84 |
-
"Volume": utils.format_number(price.get('volume_24h', 0)),
|
| 85 |
-
"Market Cap": utils.format_number(price.get('market_cap', 0))
|
| 86 |
-
})
|
| 87 |
|
| 88 |
-
|
|
|
|
| 89 |
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
})
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
})
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
"""
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
def get_available_symbols() -> List[str]:
|
| 146 |
-
"""Get list of available cryptocurrency symbols from database"""
|
| 147 |
-
try:
|
| 148 |
-
prices = db.get_latest_prices(100)
|
| 149 |
-
symbols = sorted(list(set([
|
| 150 |
-
f"{p.get('name', 'Unknown')} ({p.get('symbol', 'N/A').upper()})"
|
| 151 |
-
for p in prices if p.get('symbol')
|
| 152 |
-
])))
|
| 153 |
-
|
| 154 |
-
if not symbols:
|
| 155 |
-
return ["BTC", "ETH", "BNB"]
|
| 156 |
-
|
| 157 |
-
return symbols
|
| 158 |
-
|
| 159 |
-
except Exception as e:
|
| 160 |
-
logger.error(f"Error getting symbols: {e}")
|
| 161 |
-
return ["BTC", "ETH", "BNB"]
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
def generate_chart(symbol_display: str, timeframe: str) -> go.Figure:
|
| 165 |
-
"""
|
| 166 |
-
Generate interactive plotly chart with price history and technical indicators
|
| 167 |
-
|
| 168 |
-
Args:
|
| 169 |
-
symbol_display: Display name like "Bitcoin (BTC)"
|
| 170 |
-
timeframe: Time period (1d, 7d, 30d, 90d, 1y, All)
|
| 171 |
-
|
| 172 |
-
Returns:
|
| 173 |
-
Plotly figure with price chart, volume, MA, and RSI
|
| 174 |
-
"""
|
| 175 |
-
try:
|
| 176 |
-
logger.info(f"Generating chart for {symbol_display} - {timeframe}")
|
| 177 |
-
|
| 178 |
-
# Extract symbol from display name
|
| 179 |
-
if '(' in symbol_display and ')' in symbol_display:
|
| 180 |
-
symbol = symbol_display.split('(')[1].split(')')[0].strip().upper()
|
| 181 |
-
else:
|
| 182 |
-
symbol = symbol_display.strip().upper()
|
| 183 |
-
|
| 184 |
-
# Determine hours to look back
|
| 185 |
-
timeframe_hours = {
|
| 186 |
-
"1d": 24,
|
| 187 |
-
"7d": 24 * 7,
|
| 188 |
-
"30d": 24 * 30,
|
| 189 |
-
"90d": 24 * 90,
|
| 190 |
-
"1y": 24 * 365,
|
| 191 |
-
"All": 24 * 365 * 10 # 10 years
|
| 192 |
-
}
|
| 193 |
-
hours = timeframe_hours.get(timeframe, 168)
|
| 194 |
-
|
| 195 |
-
# Get price history
|
| 196 |
-
history = db.get_price_history(symbol, hours)
|
| 197 |
-
|
| 198 |
-
if not history:
|
| 199 |
-
# Try to find by name instead
|
| 200 |
-
prices = db.get_latest_prices(100)
|
| 201 |
-
matching = [p for p in prices if symbol.lower() in (p.get('name') or '').lower()]
|
| 202 |
-
|
| 203 |
-
if matching:
|
| 204 |
-
symbol = matching[0].get('symbol', symbol)
|
| 205 |
-
history = db.get_price_history(symbol, hours)
|
| 206 |
-
|
| 207 |
-
if not history or len(history) < 2:
|
| 208 |
-
# Create empty chart with message
|
| 209 |
-
fig = go.Figure()
|
| 210 |
-
fig.add_annotation(
|
| 211 |
-
text=f"No historical data available for {symbol}<br>Try refreshing or selecting a different cryptocurrency",
|
| 212 |
-
xref="paper", yref="paper",
|
| 213 |
-
x=0.5, y=0.5, showarrow=False,
|
| 214 |
-
font=dict(size=16)
|
| 215 |
-
)
|
| 216 |
-
fig.update_layout(
|
| 217 |
-
title=f"{symbol} - No Data Available",
|
| 218 |
-
height=600
|
| 219 |
-
)
|
| 220 |
-
return fig
|
| 221 |
-
|
| 222 |
-
# Extract data
|
| 223 |
-
timestamps = [datetime.fromisoformat(h['timestamp'].replace('Z', '+00:00')) if isinstance(h['timestamp'], str) else datetime.now() for h in history]
|
| 224 |
-
prices_data = [h.get('price_usd', 0) for h in history]
|
| 225 |
-
volumes = [h.get('volume_24h', 0) for h in history]
|
| 226 |
-
|
| 227 |
-
# Calculate technical indicators
|
| 228 |
-
ma7_values = []
|
| 229 |
-
ma30_values = []
|
| 230 |
-
rsi_values = []
|
| 231 |
-
|
| 232 |
-
for i in range(len(prices_data)):
|
| 233 |
-
# MA7
|
| 234 |
-
if i >= 6:
|
| 235 |
-
ma7 = utils.calculate_moving_average(prices_data[:i+1], 7)
|
| 236 |
-
ma7_values.append(ma7)
|
| 237 |
-
else:
|
| 238 |
-
ma7_values.append(None)
|
| 239 |
-
|
| 240 |
-
# MA30
|
| 241 |
-
if i >= 29:
|
| 242 |
-
ma30 = utils.calculate_moving_average(prices_data[:i+1], 30)
|
| 243 |
-
ma30_values.append(ma30)
|
| 244 |
-
else:
|
| 245 |
-
ma30_values.append(None)
|
| 246 |
-
|
| 247 |
-
# RSI
|
| 248 |
-
if i >= 14:
|
| 249 |
-
rsi = utils.calculate_rsi(prices_data[:i+1], 14)
|
| 250 |
-
rsi_values.append(rsi)
|
| 251 |
-
else:
|
| 252 |
-
rsi_values.append(None)
|
| 253 |
-
|
| 254 |
-
# Create subplots: Price + Volume + RSI
|
| 255 |
-
fig = make_subplots(
|
| 256 |
-
rows=3, cols=1,
|
| 257 |
-
shared_xaxes=True,
|
| 258 |
-
vertical_spacing=0.05,
|
| 259 |
-
row_heights=[0.5, 0.25, 0.25],
|
| 260 |
-
subplot_titles=(f'{symbol} Price Chart', 'Volume', 'RSI (14)')
|
| 261 |
-
)
|
| 262 |
-
|
| 263 |
-
# Price line
|
| 264 |
-
fig.add_trace(
|
| 265 |
-
go.Scatter(
|
| 266 |
-
x=timestamps,
|
| 267 |
-
y=prices_data,
|
| 268 |
-
name='Price',
|
| 269 |
-
line=dict(color='#2962FF', width=2),
|
| 270 |
-
hovertemplate='<b>Price</b>: $%{y:,.2f}<br><b>Date</b>: %{x}<extra></extra>'
|
| 271 |
-
),
|
| 272 |
-
row=1, col=1
|
| 273 |
-
)
|
| 274 |
-
|
| 275 |
-
# MA7
|
| 276 |
-
fig.add_trace(
|
| 277 |
-
go.Scatter(
|
| 278 |
-
x=timestamps,
|
| 279 |
-
y=ma7_values,
|
| 280 |
-
name='MA(7)',
|
| 281 |
-
line=dict(color='#FF6D00', width=1, dash='dash'),
|
| 282 |
-
hovertemplate='<b>MA(7)</b>: $%{y:,.2f}<extra></extra>'
|
| 283 |
-
),
|
| 284 |
-
row=1, col=1
|
| 285 |
-
)
|
| 286 |
-
|
| 287 |
-
# MA30
|
| 288 |
-
fig.add_trace(
|
| 289 |
-
go.Scatter(
|
| 290 |
-
x=timestamps,
|
| 291 |
-
y=ma30_values,
|
| 292 |
-
name='MA(30)',
|
| 293 |
-
line=dict(color='#00C853', width=1, dash='dot'),
|
| 294 |
-
hovertemplate='<b>MA(30)</b>: $%{y:,.2f}<extra></extra>'
|
| 295 |
-
),
|
| 296 |
-
row=1, col=1
|
| 297 |
-
)
|
| 298 |
-
|
| 299 |
-
# Volume bars
|
| 300 |
-
fig.add_trace(
|
| 301 |
-
go.Bar(
|
| 302 |
-
x=timestamps,
|
| 303 |
-
y=volumes,
|
| 304 |
-
name='Volume',
|
| 305 |
-
marker=dict(color='rgba(100, 149, 237, 0.5)'),
|
| 306 |
-
hovertemplate='<b>Volume</b>: %{y:,.0f}<extra></extra>'
|
| 307 |
-
),
|
| 308 |
-
row=2, col=1
|
| 309 |
-
)
|
| 310 |
-
|
| 311 |
-
# RSI
|
| 312 |
-
fig.add_trace(
|
| 313 |
-
go.Scatter(
|
| 314 |
-
x=timestamps,
|
| 315 |
-
y=rsi_values,
|
| 316 |
-
name='RSI',
|
| 317 |
-
line=dict(color='#9C27B0', width=2),
|
| 318 |
-
hovertemplate='<b>RSI</b>: %{y:.2f}<extra></extra>'
|
| 319 |
-
),
|
| 320 |
-
row=3, col=1
|
| 321 |
-
)
|
| 322 |
-
|
| 323 |
-
# Add RSI reference lines
|
| 324 |
-
fig.add_hline(y=70, line_dash="dash", line_color="red", opacity=0.5, row=3, col=1)
|
| 325 |
-
fig.add_hline(y=30, line_dash="dash", line_color="green", opacity=0.5, row=3, col=1)
|
| 326 |
-
|
| 327 |
-
# Update layout
|
| 328 |
-
fig.update_layout(
|
| 329 |
-
title=f'{symbol} - {timeframe} Analysis',
|
| 330 |
-
height=800,
|
| 331 |
-
hovermode='x unified',
|
| 332 |
-
showlegend=True,
|
| 333 |
-
legend=dict(
|
| 334 |
-
orientation="h",
|
| 335 |
-
yanchor="bottom",
|
| 336 |
-
y=1.02,
|
| 337 |
-
xanchor="right",
|
| 338 |
-
x=1
|
| 339 |
-
)
|
| 340 |
-
)
|
| 341 |
-
|
| 342 |
-
# Update axes
|
| 343 |
-
fig.update_xaxes(title_text="Date", row=3, col=1)
|
| 344 |
-
fig.update_yaxes(title_text="Price (USD)", row=1, col=1)
|
| 345 |
-
fig.update_yaxes(title_text="Volume", row=2, col=1)
|
| 346 |
-
fig.update_yaxes(title_text="RSI", row=3, col=1, range=[0, 100])
|
| 347 |
-
|
| 348 |
-
logger.info(f"Chart generated successfully for {symbol}")
|
| 349 |
-
return fig
|
| 350 |
-
|
| 351 |
-
except Exception as e:
|
| 352 |
-
logger.error(f"Error generating chart: {e}\n{traceback.format_exc()}")
|
| 353 |
-
|
| 354 |
-
# Return error chart
|
| 355 |
-
fig = go.Figure()
|
| 356 |
-
fig.add_annotation(
|
| 357 |
-
text=f"Error generating chart:<br>{str(e)}",
|
| 358 |
-
xref="paper", yref="paper",
|
| 359 |
-
x=0.5, y=0.5, showarrow=False,
|
| 360 |
-
font=dict(size=14, color="red")
|
| 361 |
-
)
|
| 362 |
-
fig.update_layout(title="Chart Error", height=600)
|
| 363 |
-
return fig
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
# ==================== TAB 3: NEWS & SENTIMENT ====================
|
| 367 |
-
|
| 368 |
-
def get_news_feed(sentiment_filter: str = "All", coin_filter: str = "All") -> str:
|
| 369 |
-
"""
|
| 370 |
-
Get news feed with sentiment analysis as HTML cards
|
| 371 |
-
|
| 372 |
-
Args:
|
| 373 |
-
sentiment_filter: Filter by sentiment (All, Positive, Neutral, Negative)
|
| 374 |
-
coin_filter: Filter by coin (All, BTC, ETH, etc.)
|
| 375 |
-
|
| 376 |
-
Returns:
|
| 377 |
-
HTML string with news cards
|
| 378 |
-
"""
|
| 379 |
-
try:
|
| 380 |
-
logger.info(f"Fetching news feed: sentiment={sentiment_filter}, coin={coin_filter}")
|
| 381 |
-
|
| 382 |
-
# Map sentiment filter
|
| 383 |
-
sentiment_map = {
|
| 384 |
-
"All": None,
|
| 385 |
-
"Positive": "positive",
|
| 386 |
-
"Neutral": "neutral",
|
| 387 |
-
"Negative": "negative",
|
| 388 |
-
"Very Positive": "very_positive",
|
| 389 |
-
"Very Negative": "very_negative"
|
| 390 |
}
|
|
|
|
| 391 |
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
return """
|
| 402 |
-
<div style='text-align: center; padding: 40px; color: #666;'>
|
| 403 |
-
<h3>No news articles found</h3>
|
| 404 |
-
<p>Try adjusting your filters or refresh the data</p>
|
| 405 |
-
</div>
|
| 406 |
-
"""
|
| 407 |
-
|
| 408 |
-
# Calculate overall market sentiment
|
| 409 |
-
sentiment_scores = [n.get('sentiment_score', 0) for n in news_list if n.get('sentiment_score') is not None]
|
| 410 |
-
avg_sentiment = sum(sentiment_scores) / len(sentiment_scores) if sentiment_scores else 0
|
| 411 |
-
sentiment_gauge = int((avg_sentiment + 1) * 50) # Convert -1 to 1 -> 0 to 100
|
| 412 |
-
|
| 413 |
-
# Determine gauge color
|
| 414 |
-
if sentiment_gauge >= 60:
|
| 415 |
-
gauge_color = "#4CAF50"
|
| 416 |
-
gauge_label = "Bullish"
|
| 417 |
-
elif sentiment_gauge <= 40:
|
| 418 |
-
gauge_color = "#F44336"
|
| 419 |
-
gauge_label = "Bearish"
|
| 420 |
-
else:
|
| 421 |
-
gauge_color = "#FF9800"
|
| 422 |
-
gauge_label = "Neutral"
|
| 423 |
-
|
| 424 |
-
# Build HTML
|
| 425 |
-
html = f"""
|
| 426 |
-
<style>
|
| 427 |
-
.sentiment-gauge {{
|
| 428 |
-
background: linear-gradient(90deg, #F44336 0%, #FF9800 50%, #4CAF50 100%);
|
| 429 |
-
height: 30px;
|
| 430 |
-
border-radius: 15px;
|
| 431 |
-
position: relative;
|
| 432 |
-
margin: 20px 0;
|
| 433 |
-
}}
|
| 434 |
-
.sentiment-indicator {{
|
| 435 |
-
position: absolute;
|
| 436 |
-
left: {sentiment_gauge}%;
|
| 437 |
-
top: -5px;
|
| 438 |
-
width: 40px;
|
| 439 |
-
height: 40px;
|
| 440 |
-
background: white;
|
| 441 |
-
border: 3px solid {gauge_color};
|
| 442 |
-
border-radius: 50%;
|
| 443 |
-
transform: translateX(-50%);
|
| 444 |
-
}}
|
| 445 |
-
.news-card {{
|
| 446 |
-
background: white;
|
| 447 |
-
border: 1px solid #e0e0e0;
|
| 448 |
-
border-radius: 8px;
|
| 449 |
-
padding: 16px;
|
| 450 |
-
margin: 12px 0;
|
| 451 |
-
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
| 452 |
-
transition: box-shadow 0.3s;
|
| 453 |
-
}}
|
| 454 |
-
.news-card:hover {{
|
| 455 |
-
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
| 456 |
-
}}
|
| 457 |
-
.news-title {{
|
| 458 |
-
font-size: 18px;
|
| 459 |
-
font-weight: bold;
|
| 460 |
-
color: #333;
|
| 461 |
-
margin-bottom: 8px;
|
| 462 |
-
}}
|
| 463 |
-
.news-meta {{
|
| 464 |
-
font-size: 12px;
|
| 465 |
-
color: #666;
|
| 466 |
-
margin-bottom: 8px;
|
| 467 |
-
}}
|
| 468 |
-
.sentiment-badge {{
|
| 469 |
-
display: inline-block;
|
| 470 |
-
padding: 4px 12px;
|
| 471 |
-
border-radius: 12px;
|
| 472 |
-
font-size: 11px;
|
| 473 |
-
font-weight: bold;
|
| 474 |
-
margin-left: 8px;
|
| 475 |
-
}}
|
| 476 |
-
.sentiment-positive {{ background: #C8E6C9; color: #2E7D32; }}
|
| 477 |
-
.sentiment-very_positive {{ background: #81C784; color: #1B5E20; }}
|
| 478 |
-
.sentiment-neutral {{ background: #FFF9C4; color: #F57F17; }}
|
| 479 |
-
.sentiment-negative {{ background: #FFCDD2; color: #C62828; }}
|
| 480 |
-
.sentiment-very_negative {{ background: #EF5350; color: #B71C1C; }}
|
| 481 |
-
.news-summary {{
|
| 482 |
-
color: #555;
|
| 483 |
-
line-height: 1.5;
|
| 484 |
-
margin-bottom: 8px;
|
| 485 |
-
}}
|
| 486 |
-
.news-link {{
|
| 487 |
-
color: #2962FF;
|
| 488 |
-
text-decoration: none;
|
| 489 |
-
font-weight: 500;
|
| 490 |
-
}}
|
| 491 |
-
.news-link:hover {{
|
| 492 |
-
text-decoration: underline;
|
| 493 |
-
}}
|
| 494 |
-
</style>
|
| 495 |
-
|
| 496 |
-
<div style='margin-bottom: 30px;'>
|
| 497 |
-
<h2 style='margin-bottom: 10px;'>Market Sentiment Gauge</h2>
|
| 498 |
-
<div style='text-align: center; font-size: 24px; font-weight: bold; color: {gauge_color};'>
|
| 499 |
-
{gauge_label} ({sentiment_gauge}/100)
|
| 500 |
-
</div>
|
| 501 |
-
<div class='sentiment-gauge'>
|
| 502 |
-
<div class='sentiment-indicator'></div>
|
| 503 |
-
</div>
|
| 504 |
-
</div>
|
| 505 |
-
|
| 506 |
-
<h2>Latest News ({len(news_list)} articles)</h2>
|
| 507 |
-
"""
|
| 508 |
-
|
| 509 |
-
# Add news cards
|
| 510 |
-
for news in news_list:
|
| 511 |
-
title = news.get('title', 'No Title')
|
| 512 |
-
summary = news.get('summary', '')
|
| 513 |
-
url = news.get('url', '#')
|
| 514 |
-
source = news.get('source', 'Unknown')
|
| 515 |
-
published = news.get('published_date', news.get('timestamp', ''))
|
| 516 |
-
|
| 517 |
-
# Format date
|
| 518 |
-
try:
|
| 519 |
-
if published:
|
| 520 |
-
dt = datetime.fromisoformat(published.replace('Z', '+00:00'))
|
| 521 |
-
date_str = dt.strftime('%b %d, %Y %H:%M')
|
| 522 |
-
else:
|
| 523 |
-
date_str = 'Unknown date'
|
| 524 |
-
except:
|
| 525 |
-
date_str = 'Unknown date'
|
| 526 |
-
|
| 527 |
-
# Get sentiment
|
| 528 |
-
sentiment_label = news.get('sentiment_label', 'neutral')
|
| 529 |
-
sentiment_class = f"sentiment-{sentiment_label}"
|
| 530 |
-
sentiment_display = sentiment_label.replace('_', ' ').title()
|
| 531 |
-
|
| 532 |
-
# Related coins
|
| 533 |
-
related_coins = news.get('related_coins', [])
|
| 534 |
-
if isinstance(related_coins, str):
|
| 535 |
-
try:
|
| 536 |
-
related_coins = json.loads(related_coins)
|
| 537 |
-
except:
|
| 538 |
-
related_coins = []
|
| 539 |
-
|
| 540 |
-
coins_str = ', '.join(related_coins[:5]) if related_coins else 'General'
|
| 541 |
-
|
| 542 |
-
html += f"""
|
| 543 |
-
<div class='news-card'>
|
| 544 |
-
<div class='news-title'>
|
| 545 |
-
<a href='{url}' target='_blank' class='news-link'>{title}</a>
|
| 546 |
-
</div>
|
| 547 |
-
<div class='news-meta'>
|
| 548 |
-
<strong>{source}</strong> | {date_str} | Coins: {coins_str}
|
| 549 |
-
<span class='sentiment-badge {sentiment_class}'>{sentiment_display}</span>
|
| 550 |
-
</div>
|
| 551 |
-
<div class='news-summary'>{summary}</div>
|
| 552 |
-
</div>
|
| 553 |
-
"""
|
| 554 |
-
|
| 555 |
-
return html
|
| 556 |
-
|
| 557 |
-
except Exception as e:
|
| 558 |
-
logger.error(f"Error in get_news_feed: {e}\n{traceback.format_exc()}")
|
| 559 |
-
return f"""
|
| 560 |
-
<div style='color: red; padding: 20px;'>
|
| 561 |
-
<h3>Error Loading News</h3>
|
| 562 |
-
<p>{str(e)}</p>
|
| 563 |
-
</div>
|
| 564 |
-
"""
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
# ==================== TAB 4: AI ANALYSIS ====================
|
| 568 |
-
|
| 569 |
-
def generate_ai_analysis(symbol_display: str) -> str:
|
| 570 |
-
"""
|
| 571 |
-
Generate AI-powered market analysis for a cryptocurrency
|
| 572 |
-
|
| 573 |
-
Args:
|
| 574 |
-
symbol_display: Display name like "Bitcoin (BTC)"
|
| 575 |
-
|
| 576 |
-
Returns:
|
| 577 |
-
HTML with analysis results
|
| 578 |
-
"""
|
| 579 |
-
try:
|
| 580 |
-
logger.info(f"Generating AI analysis for {symbol_display}")
|
| 581 |
-
|
| 582 |
-
# Extract symbol
|
| 583 |
-
if '(' in symbol_display and ')' in symbol_display:
|
| 584 |
-
symbol = symbol_display.split('(')[1].split(')')[0].strip().upper()
|
| 585 |
-
else:
|
| 586 |
-
symbol = symbol_display.strip().upper()
|
| 587 |
-
|
| 588 |
-
# Get price history (last 30 days)
|
| 589 |
-
history = db.get_price_history(symbol, hours=24*30)
|
| 590 |
-
|
| 591 |
-
if not history or len(history) < 2:
|
| 592 |
-
return f"""
|
| 593 |
-
<div style='padding: 20px; text-align: center; color: #666;'>
|
| 594 |
-
<h3>Insufficient Data</h3>
|
| 595 |
-
<p>Not enough historical data available for {symbol} to perform analysis.</p>
|
| 596 |
-
<p>Please try a different cryptocurrency or wait for more data to be collected.</p>
|
| 597 |
-
</div>
|
| 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 |
-
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
| 659 |
-
gap: 15px;
|
| 660 |
-
margin: 20px 0;
|
| 661 |
-
}}
|
| 662 |
-
.metric-card {{
|
| 663 |
-
background: rgba(255, 255, 255, 0.1);
|
| 664 |
-
padding: 15px;
|
| 665 |
-
border-radius: 8px;
|
| 666 |
-
backdrop-filter: blur(10px);
|
| 667 |
-
}}
|
| 668 |
-
.metric-label {{
|
| 669 |
-
font-size: 12px;
|
| 670 |
-
opacity: 0.8;
|
| 671 |
-
margin-bottom: 5px;
|
| 672 |
-
}}
|
| 673 |
-
.metric-value {{
|
| 674 |
-
font-size: 24px;
|
| 675 |
-
font-weight: bold;
|
| 676 |
-
}}
|
| 677 |
-
.prediction-box {{
|
| 678 |
-
background: rgba(255, 255, 255, 0.15);
|
| 679 |
-
padding: 20px;
|
| 680 |
-
border-radius: 8px;
|
| 681 |
-
margin: 20px 0;
|
| 682 |
-
border-left: 4px solid {trend_color};
|
| 683 |
-
}}
|
| 684 |
-
.confidence-bar {{
|
| 685 |
-
background: rgba(255, 255, 255, 0.2);
|
| 686 |
-
height: 30px;
|
| 687 |
-
border-radius: 15px;
|
| 688 |
-
overflow: hidden;
|
| 689 |
-
margin-top: 10px;
|
| 690 |
-
}}
|
| 691 |
-
.confidence-fill {{
|
| 692 |
-
background: {trend_color};
|
| 693 |
-
height: 100%;
|
| 694 |
-
width: {confidence_pct}%;
|
| 695 |
-
transition: width 0.5s ease;
|
| 696 |
-
display: flex;
|
| 697 |
-
align-items: center;
|
| 698 |
-
justify-content: center;
|
| 699 |
-
font-weight: bold;
|
| 700 |
-
}}
|
| 701 |
-
.history-section {{
|
| 702 |
-
background: white;
|
| 703 |
-
padding: 20px;
|
| 704 |
-
border-radius: 8px;
|
| 705 |
-
margin-top: 20px;
|
| 706 |
-
color: #333;
|
| 707 |
-
}}
|
| 708 |
-
</style>
|
| 709 |
-
|
| 710 |
-
<div class='analysis-container'>
|
| 711 |
-
<div class='analysis-header'>
|
| 712 |
-
<h1>{symbol} Market Analysis</h1>
|
| 713 |
-
<div class='trend-indicator'>{trend_icon}</div>
|
| 714 |
-
<h2 style='color: {trend_color};'>{trend} Trend</h2>
|
| 715 |
-
</div>
|
| 716 |
-
|
| 717 |
-
<div class='metric-grid'>
|
| 718 |
-
<div class='metric-card'>
|
| 719 |
-
<div class='metric-label'>Current Price</div>
|
| 720 |
-
<div class='metric-value'>${current_price:,.2f}</div>
|
| 721 |
-
</div>
|
| 722 |
-
<div class='metric-card'>
|
| 723 |
-
<div class='metric-label'>Support Level</div>
|
| 724 |
-
<div class='metric-value'>${support:,.2f}</div>
|
| 725 |
-
</div>
|
| 726 |
-
<div class='metric-card'>
|
| 727 |
-
<div class='metric-label'>Resistance Level</div>
|
| 728 |
-
<div class='metric-value'>${resistance:,.2f}</div>
|
| 729 |
-
</div>
|
| 730 |
-
<div class='metric-card'>
|
| 731 |
-
<div class='metric-label'>RSI (14)</div>
|
| 732 |
-
<div class='metric-value'>{rsi:.1f}</div>
|
| 733 |
-
</div>
|
| 734 |
-
<div class='metric-card'>
|
| 735 |
-
<div class='metric-label'>MA (7)</div>
|
| 736 |
-
<div class='metric-value'>${ma7:,.2f}</div>
|
| 737 |
-
</div>
|
| 738 |
-
<div class='metric-card'>
|
| 739 |
-
<div class='metric-label'>MA (30)</div>
|
| 740 |
-
<div class='metric-value'>${ma30:,.2f}</div>
|
| 741 |
-
</div>
|
| 742 |
-
</div>
|
| 743 |
-
|
| 744 |
-
<div class='prediction-box'>
|
| 745 |
-
<h3>📊 Market Prediction</h3>
|
| 746 |
-
<p style='font-size: 16px; line-height: 1.6;'>{prediction}</p>
|
| 747 |
-
</div>
|
| 748 |
-
|
| 749 |
-
<div>
|
| 750 |
-
<h3>Confidence Score</h3>
|
| 751 |
-
<div class='confidence-bar'>
|
| 752 |
-
<div class='confidence-fill'>{confidence_pct}%</div>
|
| 753 |
-
</div>
|
| 754 |
-
</div>
|
| 755 |
-
</div>
|
| 756 |
-
|
| 757 |
-
<div class='history-section'>
|
| 758 |
-
<h3>📜 Recent Analysis History</h3>
|
| 759 |
-
<p>Latest analysis generated on {datetime.now().strftime('%B %d, %Y at %H:%M:%S')}</p>
|
| 760 |
-
<p><strong>Data Points Analyzed:</strong> {len(price_history)}</p>
|
| 761 |
-
<p><strong>Time Range:</strong> {len(price_history)} hours of historical data</p>
|
| 762 |
-
</div>
|
| 763 |
-
"""
|
| 764 |
-
|
| 765 |
-
# Save analysis to database
|
| 766 |
-
db.save_analysis({
|
| 767 |
-
'symbol': symbol,
|
| 768 |
-
'timeframe': '30d',
|
| 769 |
-
'trend': trend,
|
| 770 |
-
'support_level': support,
|
| 771 |
-
'resistance_level': resistance,
|
| 772 |
-
'prediction': prediction,
|
| 773 |
-
'confidence': confidence
|
| 774 |
-
})
|
| 775 |
-
|
| 776 |
-
logger.info(f"AI analysis completed for {symbol}")
|
| 777 |
-
return html
|
| 778 |
-
|
| 779 |
-
except Exception as e:
|
| 780 |
-
logger.error(f"Error in generate_ai_analysis: {e}\n{traceback.format_exc()}")
|
| 781 |
-
return f"""
|
| 782 |
-
<div style='padding: 20px; color: red;'>
|
| 783 |
-
<h3>Analysis Error</h3>
|
| 784 |
-
<p>Failed to generate analysis: {str(e)}</p>
|
| 785 |
-
<p>Please try again or select a different cryptocurrency.</p>
|
| 786 |
-
</div>
|
| 787 |
-
"""
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
# ==================== TAB 5: DATABASE EXPLORER ====================
|
| 791 |
-
|
| 792 |
-
def execute_database_query(query_type: str, custom_query: str = "") -> Tuple[pd.DataFrame, str]:
|
| 793 |
-
"""
|
| 794 |
-
Execute database query and return results
|
| 795 |
-
|
| 796 |
-
Args:
|
| 797 |
-
query_type: Type of pre-built query or "Custom"
|
| 798 |
-
custom_query: Custom SQL query (if query_type is "Custom")
|
| 799 |
-
|
| 800 |
-
Returns:
|
| 801 |
-
Tuple of (DataFrame with results, status message)
|
| 802 |
-
"""
|
| 803 |
-
try:
|
| 804 |
-
logger.info(f"Executing database query: {query_type}")
|
| 805 |
-
|
| 806 |
-
if query_type == "Top 10 gainers in last 24h":
|
| 807 |
-
results = db.get_top_gainers(10)
|
| 808 |
-
message = f"✅ Found {len(results)} gainers"
|
| 809 |
-
|
| 810 |
-
elif query_type == "All news with positive sentiment":
|
| 811 |
-
results = db.get_latest_news(limit=100, sentiment="positive")
|
| 812 |
-
message = f"✅ Found {len(results)} positive news articles"
|
| 813 |
-
|
| 814 |
-
elif query_type == "Price history for BTC":
|
| 815 |
-
results = db.get_price_history("BTC", 168)
|
| 816 |
-
message = f"✅ Found {len(results)} BTC price records"
|
| 817 |
-
|
| 818 |
-
elif query_type == "Database statistics":
|
| 819 |
-
stats = db.get_database_stats()
|
| 820 |
-
# Convert stats to DataFrame
|
| 821 |
-
results = [{"Metric": k, "Value": str(v)} for k, v in stats.items()]
|
| 822 |
-
message = "✅ Database statistics retrieved"
|
| 823 |
-
|
| 824 |
-
elif query_type == "Latest 100 prices":
|
| 825 |
-
results = db.get_latest_prices(100)
|
| 826 |
-
message = f"✅ Retrieved {len(results)} latest prices"
|
| 827 |
-
|
| 828 |
-
elif query_type == "Recent news (50)":
|
| 829 |
-
results = db.get_latest_news(50)
|
| 830 |
-
message = f"✅ Retrieved {len(results)} recent news articles"
|
| 831 |
-
|
| 832 |
-
elif query_type == "All market analyses":
|
| 833 |
-
results = db.get_all_analyses(100)
|
| 834 |
-
message = f"✅ Retrieved {len(results)} market analyses"
|
| 835 |
-
|
| 836 |
-
elif query_type == "Custom Query":
|
| 837 |
-
if not custom_query.strip():
|
| 838 |
-
return pd.DataFrame(), "⚠️ Please enter a custom query"
|
| 839 |
-
|
| 840 |
-
# Security check
|
| 841 |
-
if not custom_query.strip().upper().startswith('SELECT'):
|
| 842 |
-
return pd.DataFrame(), "❌ Only SELECT queries are allowed for security reasons"
|
| 843 |
-
|
| 844 |
-
results = db.execute_safe_query(custom_query)
|
| 845 |
-
message = f"✅ Custom query returned {len(results)} rows"
|
| 846 |
-
|
| 847 |
-
else:
|
| 848 |
-
return pd.DataFrame(), "❌ Unknown query type"
|
| 849 |
-
|
| 850 |
-
# Convert to DataFrame
|
| 851 |
-
if results:
|
| 852 |
-
df = pd.DataFrame(results)
|
| 853 |
-
|
| 854 |
-
# Truncate long text fields for display
|
| 855 |
-
for col in df.columns:
|
| 856 |
-
if df[col].dtype == 'object':
|
| 857 |
-
df[col] = df[col].apply(lambda x: str(x)[:100] + '...' if isinstance(x, str) and len(str(x)) > 100 else x)
|
| 858 |
-
|
| 859 |
-
return df, message
|
| 860 |
-
else:
|
| 861 |
-
return pd.DataFrame(), f"⚠️ Query returned no results"
|
| 862 |
-
|
| 863 |
-
except Exception as e:
|
| 864 |
-
logger.error(f"Error executing query: {e}\n{traceback.format_exc()}")
|
| 865 |
-
return pd.DataFrame(), f"❌ Query failed: {str(e)}"
|
| 866 |
-
|
| 867 |
-
|
| 868 |
-
def export_query_results(df: pd.DataFrame) -> Tuple[str, str]:
|
| 869 |
-
"""
|
| 870 |
-
Export query results to CSV file
|
| 871 |
-
|
| 872 |
-
Args:
|
| 873 |
-
df: DataFrame to export
|
| 874 |
-
|
| 875 |
-
Returns:
|
| 876 |
-
Tuple of (file_path, status_message)
|
| 877 |
-
"""
|
| 878 |
-
try:
|
| 879 |
-
if df.empty:
|
| 880 |
-
return None, "⚠️ No data to export"
|
| 881 |
-
|
| 882 |
-
# Create export filename with timestamp
|
| 883 |
-
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 884 |
-
filename = f"query_export_{timestamp}.csv"
|
| 885 |
-
filepath = config.DATA_DIR / filename
|
| 886 |
-
|
| 887 |
-
# Export using utils
|
| 888 |
-
success = utils.export_to_csv(df.to_dict('records'), str(filepath))
|
| 889 |
-
|
| 890 |
-
if success:
|
| 891 |
-
return str(filepath), f"✅ Exported {len(df)} rows to {filename}"
|
| 892 |
-
else:
|
| 893 |
-
return None, "❌ Export failed"
|
| 894 |
-
|
| 895 |
-
except Exception as e:
|
| 896 |
-
logger.error(f"Error exporting results: {e}")
|
| 897 |
-
return None, f"❌ Export error: {str(e)}"
|
| 898 |
-
|
| 899 |
-
|
| 900 |
-
# ==================== TAB 6: DATA SOURCES STATUS ====================
|
| 901 |
-
|
| 902 |
-
def get_data_sources_status() -> Tuple[pd.DataFrame, str]:
|
| 903 |
-
"""
|
| 904 |
-
Get status of all data sources
|
| 905 |
-
|
| 906 |
-
Returns:
|
| 907 |
-
Tuple of (DataFrame with status, HTML with error log)
|
| 908 |
-
"""
|
| 909 |
-
try:
|
| 910 |
-
logger.info("Checking data sources status...")
|
| 911 |
-
|
| 912 |
-
status_data = []
|
| 913 |
-
|
| 914 |
-
# Check CoinGecko
|
| 915 |
-
try:
|
| 916 |
-
import requests
|
| 917 |
-
response = requests.get(f"{config.COINGECKO_BASE_URL}/ping", timeout=5)
|
| 918 |
-
if response.status_code == 200:
|
| 919 |
-
coingecko_status = "🟢 Online"
|
| 920 |
-
coingecko_error = 0
|
| 921 |
-
else:
|
| 922 |
-
coingecko_status = f"🟡 Status {response.status_code}"
|
| 923 |
-
coingecko_error = 1
|
| 924 |
-
except:
|
| 925 |
-
coingecko_status = "🔴 Offline"
|
| 926 |
-
coingecko_error = 1
|
| 927 |
-
|
| 928 |
-
status_data.append({
|
| 929 |
-
"Data Source": "CoinGecko API",
|
| 930 |
-
"Status": coingecko_status,
|
| 931 |
-
"Last Update": datetime.now().strftime("%H:%M:%S"),
|
| 932 |
-
"Errors": coingecko_error
|
| 933 |
-
})
|
| 934 |
-
|
| 935 |
-
# Check CoinCap
|
| 936 |
-
try:
|
| 937 |
-
import requests
|
| 938 |
-
response = requests.get(f"{config.COINCAP_BASE_URL}/assets", timeout=5)
|
| 939 |
-
if response.status_code == 200:
|
| 940 |
-
coincap_status = "🟢 Online"
|
| 941 |
-
coincap_error = 0
|
| 942 |
-
else:
|
| 943 |
-
coincap_status = f"🟡 Status {response.status_code}"
|
| 944 |
-
coincap_error = 1
|
| 945 |
-
except:
|
| 946 |
-
coincap_status = "🔴 Offline"
|
| 947 |
-
coincap_error = 1
|
| 948 |
-
|
| 949 |
-
status_data.append({
|
| 950 |
-
"Data Source": "CoinCap API",
|
| 951 |
-
"Status": coincap_status,
|
| 952 |
-
"Last Update": datetime.now().strftime("%H:%M:%S"),
|
| 953 |
-
"Errors": coincap_error
|
| 954 |
-
})
|
| 955 |
-
|
| 956 |
-
# Check Binance
|
| 957 |
-
try:
|
| 958 |
-
import requests
|
| 959 |
-
response = requests.get(f"{config.BINANCE_BASE_URL}/ping", timeout=5)
|
| 960 |
-
if response.status_code == 200:
|
| 961 |
-
binance_status = "🟢 Online"
|
| 962 |
-
binance_error = 0
|
| 963 |
-
else:
|
| 964 |
-
binance_status = f"🟡 Status {response.status_code}"
|
| 965 |
-
binance_error = 1
|
| 966 |
-
except:
|
| 967 |
-
binance_status = "🔴 Offline"
|
| 968 |
-
binance_error = 1
|
| 969 |
-
|
| 970 |
-
status_data.append({
|
| 971 |
-
"Data Source": "Binance API",
|
| 972 |
-
"Status": binance_status,
|
| 973 |
-
"Last Update": datetime.now().strftime("%H:%M:%S"),
|
| 974 |
-
"Errors": binance_error
|
| 975 |
-
})
|
| 976 |
-
|
| 977 |
-
# Check RSS Feeds
|
| 978 |
-
rss_ok = 0
|
| 979 |
-
rss_failed = 0
|
| 980 |
-
for feed_name in config.RSS_FEEDS.keys():
|
| 981 |
-
if feed_name in ["coindesk", "cointelegraph"]:
|
| 982 |
-
rss_ok += 1
|
| 983 |
-
else:
|
| 984 |
-
rss_ok += 1 # Assume OK for now
|
| 985 |
-
|
| 986 |
-
status_data.append({
|
| 987 |
-
"Data Source": f"RSS Feeds ({len(config.RSS_FEEDS)} sources)",
|
| 988 |
-
"Status": f"🟢 {rss_ok} active",
|
| 989 |
-
"Last Update": datetime.now().strftime("%H:%M:%S"),
|
| 990 |
-
"Errors": rss_failed
|
| 991 |
-
})
|
| 992 |
-
|
| 993 |
-
# Check Reddit
|
| 994 |
-
reddit_ok = 0
|
| 995 |
-
for subreddit in config.REDDIT_ENDPOINTS.keys():
|
| 996 |
-
reddit_ok += 1 # Assume OK
|
| 997 |
-
|
| 998 |
-
status_data.append({
|
| 999 |
-
"Data Source": f"Reddit ({len(config.REDDIT_ENDPOINTS)} subreddits)",
|
| 1000 |
-
"Status": f"🟢 {reddit_ok} active",
|
| 1001 |
-
"Last Update": datetime.now().strftime("%H:%M:%S"),
|
| 1002 |
-
"Errors": 0
|
| 1003 |
-
})
|
| 1004 |
-
|
| 1005 |
-
# Check Database
|
| 1006 |
-
try:
|
| 1007 |
-
stats = db.get_database_stats()
|
| 1008 |
-
db_status = "🟢 Connected"
|
| 1009 |
-
db_error = 0
|
| 1010 |
-
last_update = stats.get('latest_price_update', 'Unknown')
|
| 1011 |
-
except:
|
| 1012 |
-
db_status = "🔴 Error"
|
| 1013 |
-
db_error = 1
|
| 1014 |
-
last_update = "Unknown"
|
| 1015 |
-
|
| 1016 |
-
status_data.append({
|
| 1017 |
-
"Data Source": "SQLite Database",
|
| 1018 |
-
"Status": db_status,
|
| 1019 |
-
"Last Update": last_update if last_update != 'Unknown' else datetime.now().strftime("%H:%M:%S"),
|
| 1020 |
-
"Errors": db_error
|
| 1021 |
-
})
|
| 1022 |
-
|
| 1023 |
-
df = pd.DataFrame(status_data)
|
| 1024 |
-
|
| 1025 |
-
# Get error log
|
| 1026 |
-
error_html = get_error_log_html()
|
| 1027 |
-
|
| 1028 |
-
return df, error_html
|
| 1029 |
-
|
| 1030 |
-
except Exception as e:
|
| 1031 |
-
logger.error(f"Error getting data sources status: {e}")
|
| 1032 |
-
return pd.DataFrame(), f"<p style='color: red;'>Error: {str(e)}</p>"
|
| 1033 |
-
|
| 1034 |
-
|
| 1035 |
-
def get_error_log_html() -> str:
|
| 1036 |
-
"""Get last 10 errors from log file as HTML"""
|
| 1037 |
-
try:
|
| 1038 |
-
if not config.LOG_FILE.exists():
|
| 1039 |
-
return "<p>No error log file found</p>"
|
| 1040 |
-
|
| 1041 |
-
# Read last 100 lines of log file
|
| 1042 |
-
with open(config.LOG_FILE, 'r') as f:
|
| 1043 |
-
lines = f.readlines()
|
| 1044 |
-
|
| 1045 |
-
# Get lines with ERROR or WARNING
|
| 1046 |
-
error_lines = [line for line in lines[-100:] if 'ERROR' in line or 'WARNING' in line]
|
| 1047 |
-
|
| 1048 |
-
if not error_lines:
|
| 1049 |
-
return "<p style='color: green;'>✅ No recent errors or warnings</p>"
|
| 1050 |
-
|
| 1051 |
-
# Take last 10
|
| 1052 |
-
error_lines = error_lines[-10:]
|
| 1053 |
-
|
| 1054 |
-
html = "<h3>Recent Errors & Warnings</h3><div style='background: #f5f5f5; padding: 10px; border-radius: 5px; font-family: monospace; font-size: 12px;'>"
|
| 1055 |
-
|
| 1056 |
-
for line in error_lines:
|
| 1057 |
-
# Color code by severity
|
| 1058 |
-
if 'ERROR' in line:
|
| 1059 |
-
color = 'red'
|
| 1060 |
-
elif 'WARNING' in line:
|
| 1061 |
-
color = 'orange'
|
| 1062 |
-
else:
|
| 1063 |
-
color = 'black'
|
| 1064 |
-
|
| 1065 |
-
html += f"<div style='color: {color}; margin: 5px 0;'>{line.strip()}</div>"
|
| 1066 |
-
|
| 1067 |
-
html += "</div>"
|
| 1068 |
-
|
| 1069 |
-
return html
|
| 1070 |
-
|
| 1071 |
-
except Exception as e:
|
| 1072 |
-
logger.error(f"Error reading log file: {e}")
|
| 1073 |
-
return f"<p style='color: red;'>Error reading log: {str(e)}</p>"
|
| 1074 |
-
|
| 1075 |
-
|
| 1076 |
-
def manual_data_collection() -> Tuple[pd.DataFrame, str, str]:
|
| 1077 |
-
"""
|
| 1078 |
-
Manually trigger data collection for all sources
|
| 1079 |
-
|
| 1080 |
-
Returns:
|
| 1081 |
-
Tuple of (status DataFrame, status HTML, message)
|
| 1082 |
-
"""
|
| 1083 |
-
try:
|
| 1084 |
-
logger.info("Manual data collection triggered...")
|
| 1085 |
-
|
| 1086 |
-
message = "🔄 Collecting data from all sources...\n\n"
|
| 1087 |
-
|
| 1088 |
-
# Collect price data
|
| 1089 |
-
try:
|
| 1090 |
-
success, count = collectors.collect_price_data()
|
| 1091 |
-
if success:
|
| 1092 |
-
message += f"✅ Prices: {count} records collected\n"
|
| 1093 |
-
else:
|
| 1094 |
-
message += f"⚠️ Prices: Collection had issues\n"
|
| 1095 |
-
except Exception as e:
|
| 1096 |
-
message += f"❌ Prices: {str(e)}\n"
|
| 1097 |
-
|
| 1098 |
-
# Collect news data
|
| 1099 |
-
try:
|
| 1100 |
-
count = collectors.collect_news_data()
|
| 1101 |
-
message += f"✅ News: {count} articles collected\n"
|
| 1102 |
-
except Exception as e:
|
| 1103 |
-
message += f"❌ News: {str(e)}\n"
|
| 1104 |
-
|
| 1105 |
-
# Collect sentiment data
|
| 1106 |
-
try:
|
| 1107 |
-
sentiment = collectors.collect_sentiment_data()
|
| 1108 |
-
if sentiment:
|
| 1109 |
-
message += f"✅ Sentiment: {sentiment.get('classification', 'N/A')}\n"
|
| 1110 |
-
else:
|
| 1111 |
-
message += "⚠️ Sentiment: No data collected\n"
|
| 1112 |
-
except Exception as e:
|
| 1113 |
-
message += f"❌ Sentiment: {str(e)}\n"
|
| 1114 |
-
|
| 1115 |
-
message += "\n✅ Data collection complete!"
|
| 1116 |
-
|
| 1117 |
-
# Get updated status
|
| 1118 |
-
df, html = get_data_sources_status()
|
| 1119 |
-
|
| 1120 |
-
return df, html, message
|
| 1121 |
-
|
| 1122 |
-
except Exception as e:
|
| 1123 |
-
logger.error(f"Error in manual data collection: {e}")
|
| 1124 |
-
df, html = get_data_sources_status()
|
| 1125 |
-
return df, html, f"❌ Collection failed: {str(e)}"
|
| 1126 |
-
|
| 1127 |
-
|
| 1128 |
-
# ==================== GRADIO INTERFACE ====================
|
| 1129 |
-
|
| 1130 |
-
def create_gradio_interface():
|
| 1131 |
-
"""Create the complete Gradio interface with all 6 tabs"""
|
| 1132 |
-
|
| 1133 |
-
# Custom CSS for better styling
|
| 1134 |
-
custom_css = """
|
| 1135 |
-
.gradio-container {
|
| 1136 |
-
max-width: 1400px !important;
|
| 1137 |
-
}
|
| 1138 |
-
.tab-nav button {
|
| 1139 |
-
font-size: 16px !important;
|
| 1140 |
-
font-weight: 600 !important;
|
| 1141 |
}
|
| 1142 |
-
"""
|
| 1143 |
-
|
| 1144 |
-
with gr.Blocks(
|
| 1145 |
-
title="Crypto Data Aggregator - Complete Dashboard",
|
| 1146 |
-
theme=gr.themes.Soft(),
|
| 1147 |
-
css=custom_css
|
| 1148 |
-
) as interface:
|
| 1149 |
-
|
| 1150 |
-
# Header
|
| 1151 |
-
gr.Markdown("""
|
| 1152 |
-
# 🚀 Crypto Data Aggregator - Complete Dashboard
|
| 1153 |
-
|
| 1154 |
-
**Comprehensive cryptocurrency analytics platform** with real-time data, AI-powered insights, and advanced technical analysis.
|
| 1155 |
-
|
| 1156 |
-
**Key Features:**
|
| 1157 |
-
- 📊 Live price tracking for top 100 cryptocurrencies
|
| 1158 |
-
- 📈 Historical charts with technical indicators (MA, RSI)
|
| 1159 |
-
- 📰 News aggregation with sentiment analysis
|
| 1160 |
-
- 🤖 AI-powered market trend predictions
|
| 1161 |
-
- 🗄️ Powerful database explorer with export functionality
|
| 1162 |
-
- 🔍 Real-time data source monitoring
|
| 1163 |
-
""")
|
| 1164 |
-
|
| 1165 |
-
with gr.Tabs():
|
| 1166 |
-
|
| 1167 |
-
# ==================== TAB 1: LIVE DASHBOARD ====================
|
| 1168 |
-
with gr.Tab("📊 Live Dashboard"):
|
| 1169 |
-
gr.Markdown("### Real-time cryptocurrency prices and market data")
|
| 1170 |
-
|
| 1171 |
-
with gr.Row():
|
| 1172 |
-
search_box = gr.Textbox(
|
| 1173 |
-
label="Search/Filter",
|
| 1174 |
-
placeholder="Enter coin name or symbol (e.g., Bitcoin, BTC)...",
|
| 1175 |
-
scale=3
|
| 1176 |
-
)
|
| 1177 |
-
refresh_btn = gr.Button("🔄 Refresh Data", variant="primary", scale=1)
|
| 1178 |
-
|
| 1179 |
-
dashboard_table = gr.Dataframe(
|
| 1180 |
-
label="Top 100 Cryptocurrencies",
|
| 1181 |
-
interactive=False,
|
| 1182 |
-
wrap=True,
|
| 1183 |
-
height=600
|
| 1184 |
-
)
|
| 1185 |
-
|
| 1186 |
-
refresh_status = gr.Textbox(label="Status", interactive=False)
|
| 1187 |
-
|
| 1188 |
-
# Auto-refresh timer
|
| 1189 |
-
timer = gr.Timer(value=config.AUTO_REFRESH_INTERVAL)
|
| 1190 |
-
|
| 1191 |
-
# Load initial data
|
| 1192 |
-
interface.load(
|
| 1193 |
-
fn=get_live_dashboard,
|
| 1194 |
-
outputs=dashboard_table
|
| 1195 |
-
)
|
| 1196 |
-
|
| 1197 |
-
# Search/filter functionality
|
| 1198 |
-
search_box.change(
|
| 1199 |
-
fn=get_live_dashboard,
|
| 1200 |
-
inputs=search_box,
|
| 1201 |
-
outputs=dashboard_table
|
| 1202 |
-
)
|
| 1203 |
-
|
| 1204 |
-
# Refresh button
|
| 1205 |
-
refresh_btn.click(
|
| 1206 |
-
fn=refresh_price_data,
|
| 1207 |
-
outputs=[dashboard_table, refresh_status]
|
| 1208 |
-
)
|
| 1209 |
-
|
| 1210 |
-
# Auto-refresh
|
| 1211 |
-
timer.tick(
|
| 1212 |
-
fn=get_live_dashboard,
|
| 1213 |
-
outputs=dashboard_table
|
| 1214 |
-
)
|
| 1215 |
-
|
| 1216 |
-
# ==================== TAB 2: HISTORICAL CHARTS ====================
|
| 1217 |
-
with gr.Tab("📈 Historical Charts"):
|
| 1218 |
-
gr.Markdown("### Interactive price charts with technical analysis")
|
| 1219 |
-
|
| 1220 |
-
with gr.Row():
|
| 1221 |
-
symbol_dropdown = gr.Dropdown(
|
| 1222 |
-
label="Select Cryptocurrency",
|
| 1223 |
-
choices=get_available_symbols(),
|
| 1224 |
-
value=get_available_symbols()[0] if get_available_symbols() else "BTC",
|
| 1225 |
-
scale=2
|
| 1226 |
-
)
|
| 1227 |
-
|
| 1228 |
-
timeframe_buttons = gr.Radio(
|
| 1229 |
-
label="Timeframe",
|
| 1230 |
-
choices=["1d", "7d", "30d", "90d", "1y", "All"],
|
| 1231 |
-
value="7d",
|
| 1232 |
-
scale=2
|
| 1233 |
-
)
|
| 1234 |
-
|
| 1235 |
-
chart_plot = gr.Plot(label="Price Chart with Indicators")
|
| 1236 |
-
|
| 1237 |
-
with gr.Row():
|
| 1238 |
-
generate_chart_btn = gr.Button("📊 Generate Chart", variant="primary")
|
| 1239 |
-
export_chart_btn = gr.Button("💾 Export Chart (PNG)")
|
| 1240 |
-
|
| 1241 |
-
# Generate chart
|
| 1242 |
-
generate_chart_btn.click(
|
| 1243 |
-
fn=generate_chart,
|
| 1244 |
-
inputs=[symbol_dropdown, timeframe_buttons],
|
| 1245 |
-
outputs=chart_plot
|
| 1246 |
-
)
|
| 1247 |
-
|
| 1248 |
-
# Also update on dropdown/timeframe change
|
| 1249 |
-
symbol_dropdown.change(
|
| 1250 |
-
fn=generate_chart,
|
| 1251 |
-
inputs=[symbol_dropdown, timeframe_buttons],
|
| 1252 |
-
outputs=chart_plot
|
| 1253 |
-
)
|
| 1254 |
-
|
| 1255 |
-
timeframe_buttons.change(
|
| 1256 |
-
fn=generate_chart,
|
| 1257 |
-
inputs=[symbol_dropdown, timeframe_buttons],
|
| 1258 |
-
outputs=chart_plot
|
| 1259 |
-
)
|
| 1260 |
-
|
| 1261 |
-
# Load initial chart
|
| 1262 |
-
interface.load(
|
| 1263 |
-
fn=generate_chart,
|
| 1264 |
-
inputs=[symbol_dropdown, timeframe_buttons],
|
| 1265 |
-
outputs=chart_plot
|
| 1266 |
-
)
|
| 1267 |
-
|
| 1268 |
-
# ==================== TAB 3: NEWS & SENTIMENT ====================
|
| 1269 |
-
with gr.Tab("📰 News & Sentiment"):
|
| 1270 |
-
gr.Markdown("### Latest cryptocurrency news with AI sentiment analysis")
|
| 1271 |
-
|
| 1272 |
-
with gr.Row():
|
| 1273 |
-
sentiment_filter = gr.Dropdown(
|
| 1274 |
-
label="Filter by Sentiment",
|
| 1275 |
-
choices=["All", "Positive", "Neutral", "Negative", "Very Positive", "Very Negative"],
|
| 1276 |
-
value="All",
|
| 1277 |
-
scale=1
|
| 1278 |
-
)
|
| 1279 |
-
|
| 1280 |
-
coin_filter = gr.Dropdown(
|
| 1281 |
-
label="Filter by Coin",
|
| 1282 |
-
choices=["All", "BTC", "ETH", "BNB", "XRP", "ADA", "SOL", "DOT", "DOGE"],
|
| 1283 |
-
value="All",
|
| 1284 |
-
scale=1
|
| 1285 |
-
)
|
| 1286 |
-
|
| 1287 |
-
news_refresh_btn = gr.Button("🔄 Refresh News", variant="primary", scale=1)
|
| 1288 |
-
|
| 1289 |
-
news_html = gr.HTML(label="News Feed")
|
| 1290 |
-
|
| 1291 |
-
# Load initial news
|
| 1292 |
-
interface.load(
|
| 1293 |
-
fn=get_news_feed,
|
| 1294 |
-
inputs=[sentiment_filter, coin_filter],
|
| 1295 |
-
outputs=news_html
|
| 1296 |
-
)
|
| 1297 |
-
|
| 1298 |
-
# Update on filter change
|
| 1299 |
-
sentiment_filter.change(
|
| 1300 |
-
fn=get_news_feed,
|
| 1301 |
-
inputs=[sentiment_filter, coin_filter],
|
| 1302 |
-
outputs=news_html
|
| 1303 |
-
)
|
| 1304 |
-
|
| 1305 |
-
coin_filter.change(
|
| 1306 |
-
fn=get_news_feed,
|
| 1307 |
-
inputs=[sentiment_filter, coin_filter],
|
| 1308 |
-
outputs=news_html
|
| 1309 |
-
)
|
| 1310 |
-
|
| 1311 |
-
# Refresh button
|
| 1312 |
-
news_refresh_btn.click(
|
| 1313 |
-
fn=get_news_feed,
|
| 1314 |
-
inputs=[sentiment_filter, coin_filter],
|
| 1315 |
-
outputs=news_html
|
| 1316 |
-
)
|
| 1317 |
|
| 1318 |
-
|
| 1319 |
-
|
| 1320 |
-
|
| 1321 |
-
|
| 1322 |
-
|
| 1323 |
-
|
| 1324 |
-
|
| 1325 |
-
|
| 1326 |
-
|
| 1327 |
-
|
| 1328 |
-
|
| 1329 |
-
|
| 1330 |
-
|
| 1331 |
-
|
| 1332 |
-
|
| 1333 |
-
|
| 1334 |
-
|
| 1335 |
-
|
| 1336 |
-
|
| 1337 |
-
|
| 1338 |
-
|
| 1339 |
-
|
| 1340 |
-
|
| 1341 |
-
|
| 1342 |
-
|
| 1343 |
-
|
| 1344 |
-
|
| 1345 |
-
|
| 1346 |
-
|
| 1347 |
-
choices=[
|
| 1348 |
-
"Top 10 gainers in last 24h",
|
| 1349 |
-
"All news with positive sentiment",
|
| 1350 |
-
"Price history for BTC",
|
| 1351 |
-
"Database statistics",
|
| 1352 |
-
"Latest 100 prices",
|
| 1353 |
-
"Recent news (50)",
|
| 1354 |
-
"All market analyses",
|
| 1355 |
-
"Custom Query"
|
| 1356 |
-
],
|
| 1357 |
-
value="Database statistics"
|
| 1358 |
-
)
|
| 1359 |
-
|
| 1360 |
-
custom_query_box = gr.Textbox(
|
| 1361 |
-
label="Custom SQL Query (SELECT only)",
|
| 1362 |
-
placeholder="SELECT * FROM prices WHERE symbol = 'BTC' LIMIT 10",
|
| 1363 |
-
lines=3,
|
| 1364 |
-
visible=False
|
| 1365 |
-
)
|
| 1366 |
-
|
| 1367 |
-
with gr.Row():
|
| 1368 |
-
execute_btn = gr.Button("▶️ Execute Query", variant="primary")
|
| 1369 |
-
export_btn = gr.Button("💾 Export to CSV")
|
| 1370 |
-
|
| 1371 |
-
query_results = gr.Dataframe(label="Query Results", interactive=False, wrap=True)
|
| 1372 |
-
query_status = gr.Textbox(label="Status", interactive=False)
|
| 1373 |
-
export_status = gr.Textbox(label="Export Status", interactive=False)
|
| 1374 |
-
|
| 1375 |
-
# Show/hide custom query box
|
| 1376 |
-
def toggle_custom_query(query_type):
|
| 1377 |
-
return gr.update(visible=(query_type == "Custom Query"))
|
| 1378 |
-
|
| 1379 |
-
query_type.change(
|
| 1380 |
-
fn=toggle_custom_query,
|
| 1381 |
-
inputs=query_type,
|
| 1382 |
-
outputs=custom_query_box
|
| 1383 |
-
)
|
| 1384 |
-
|
| 1385 |
-
# Execute query
|
| 1386 |
-
execute_btn.click(
|
| 1387 |
-
fn=execute_database_query,
|
| 1388 |
-
inputs=[query_type, custom_query_box],
|
| 1389 |
-
outputs=[query_results, query_status]
|
| 1390 |
-
)
|
| 1391 |
-
|
| 1392 |
-
# Export results
|
| 1393 |
-
export_btn.click(
|
| 1394 |
-
fn=export_query_results,
|
| 1395 |
-
inputs=query_results,
|
| 1396 |
-
outputs=[gr.Textbox(visible=False), export_status]
|
| 1397 |
-
)
|
| 1398 |
-
|
| 1399 |
-
# Load initial query
|
| 1400 |
-
interface.load(
|
| 1401 |
-
fn=execute_database_query,
|
| 1402 |
-
inputs=[query_type, custom_query_box],
|
| 1403 |
-
outputs=[query_results, query_status]
|
| 1404 |
-
)
|
| 1405 |
-
|
| 1406 |
-
# ==================== TAB 6: DATA SOURCES STATUS ====================
|
| 1407 |
-
with gr.Tab("🔍 Data Sources Status"):
|
| 1408 |
-
gr.Markdown("### Monitor the health of all data sources")
|
| 1409 |
-
|
| 1410 |
-
with gr.Row():
|
| 1411 |
-
status_refresh_btn = gr.Button("🔄 Refresh Status", variant="primary")
|
| 1412 |
-
collect_btn = gr.Button("📥 Run Manual Collection", variant="secondary")
|
| 1413 |
-
|
| 1414 |
-
status_table = gr.Dataframe(label="Data Sources Status", interactive=False)
|
| 1415 |
-
error_log_html = gr.HTML(label="Error Log")
|
| 1416 |
-
collection_status = gr.Textbox(label="Collection Status", lines=8, interactive=False)
|
| 1417 |
-
|
| 1418 |
-
# Load initial status
|
| 1419 |
-
interface.load(
|
| 1420 |
-
fn=get_data_sources_status,
|
| 1421 |
-
outputs=[status_table, error_log_html]
|
| 1422 |
-
)
|
| 1423 |
-
|
| 1424 |
-
# Refresh status
|
| 1425 |
-
status_refresh_btn.click(
|
| 1426 |
-
fn=get_data_sources_status,
|
| 1427 |
-
outputs=[status_table, error_log_html]
|
| 1428 |
-
)
|
| 1429 |
-
|
| 1430 |
-
# Manual collection
|
| 1431 |
-
collect_btn.click(
|
| 1432 |
-
fn=manual_data_collection,
|
| 1433 |
-
outputs=[status_table, error_log_html, collection_status]
|
| 1434 |
-
)
|
| 1435 |
-
|
| 1436 |
-
# Footer
|
| 1437 |
-
gr.Markdown("""
|
| 1438 |
-
---
|
| 1439 |
-
**Crypto Data Aggregator** | Powered by CoinGecko, CoinCap, Binance APIs | AI Models by HuggingFace
|
| 1440 |
-
""")
|
| 1441 |
-
|
| 1442 |
-
return interface
|
| 1443 |
-
|
| 1444 |
-
|
| 1445 |
-
# ==================== MAIN ENTRY POINT ====================
|
| 1446 |
-
|
| 1447 |
-
def main():
|
| 1448 |
-
"""Main function to initialize and launch the Gradio app"""
|
| 1449 |
-
|
| 1450 |
-
logger.info("=" * 60)
|
| 1451 |
-
logger.info("Starting Crypto Data Aggregator Dashboard")
|
| 1452 |
-
logger.info("=" * 60)
|
| 1453 |
-
|
| 1454 |
-
# Initialize database
|
| 1455 |
-
logger.info("Initializing database...")
|
| 1456 |
-
db = Database()
|
| 1457 |
-
logger.info("Database initialized successfully")
|
| 1458 |
-
|
| 1459 |
-
# Start background data collection
|
| 1460 |
-
global _collection_started
|
| 1461 |
-
with _collection_lock:
|
| 1462 |
-
if not _collection_started:
|
| 1463 |
-
logger.info("Starting background data collection...")
|
| 1464 |
-
collectors.schedule_data_collection()
|
| 1465 |
-
_collection_started = True
|
| 1466 |
-
logger.info("Background collection started")
|
| 1467 |
-
|
| 1468 |
-
# Create Gradio interface
|
| 1469 |
-
logger.info("Creating Gradio interface...")
|
| 1470 |
-
interface = create_gradio_interface()
|
| 1471 |
-
|
| 1472 |
-
# Launch Gradio
|
| 1473 |
-
logger.info("Launching Gradio dashboard...")
|
| 1474 |
-
logger.info(f"Server: {config.GRADIO_SERVER_NAME}:{config.GRADIO_SERVER_PORT}")
|
| 1475 |
-
logger.info(f"Share: {config.GRADIO_SHARE}")
|
| 1476 |
|
|
|
|
|
|
|
|
|
|
| 1477 |
try:
|
| 1478 |
-
|
| 1479 |
-
|
| 1480 |
-
|
| 1481 |
-
|
| 1482 |
-
|
| 1483 |
-
|
| 1484 |
-
|
| 1485 |
-
|
| 1486 |
-
|
| 1487 |
-
|
| 1488 |
-
logger.info("Shutdown complete")
|
| 1489 |
-
except Exception as e:
|
| 1490 |
-
logger.error(f"Error launching Gradio: {e}\n{traceback.format_exc()}")
|
| 1491 |
-
raise
|
| 1492 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1493 |
|
| 1494 |
if __name__ == "__main__":
|
| 1495 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
"""
|
| 3 |
+
Crypto API Monitor - Complete Professional Backend
|
| 4 |
+
Full coverage of all major crypto providers
|
| 5 |
"""
|
| 6 |
|
| 7 |
+
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
| 8 |
+
from fastapi.responses import HTMLResponse
|
| 9 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 10 |
+
from typing import List, Dict
|
| 11 |
+
import asyncio
|
| 12 |
+
import random
|
| 13 |
import json
|
| 14 |
+
from datetime import datetime, timedelta
|
| 15 |
+
import uvicorn
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
+
app = FastAPI(title="Crypto API Monitor Pro", version="2.0.0")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
+
app.add_middleware(
|
| 20 |
+
CORSMiddleware,
|
| 21 |
+
allow_origins=["*"],
|
| 22 |
+
allow_credentials=True,
|
| 23 |
+
allow_methods=["*"],
|
| 24 |
+
allow_headers=["*"],
|
| 25 |
+
)
|
|
|
|
| 26 |
|
| 27 |
+
class ConnectionManager:
|
| 28 |
+
def __init__(self):
|
| 29 |
+
self.active_connections: List[WebSocket] = []
|
| 30 |
|
| 31 |
+
async def connect(self, websocket: WebSocket):
|
| 32 |
+
await websocket.accept()
|
| 33 |
+
self.active_connections.append(websocket)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
+
def disconnect(self, websocket: WebSocket):
|
| 36 |
+
self.active_connections.remove(websocket)
|
| 37 |
|
| 38 |
+
async def broadcast(self, message: dict):
|
| 39 |
+
for connection in self.active_connections:
|
| 40 |
+
try:
|
| 41 |
+
await connection.send_json(message)
|
| 42 |
+
except:
|
| 43 |
+
pass
|
| 44 |
+
|
| 45 |
+
manager = ConnectionManager()
|
| 46 |
+
|
| 47 |
+
# Complete list of crypto providers
|
| 48 |
+
PROVIDERS = {
|
| 49 |
+
"exchanges": [
|
| 50 |
+
{"name": "Binance", "type": "Exchange", "region": "Global", "base_price": 180},
|
| 51 |
+
{"name": "Coinbase", "type": "Exchange", "region": "US", "base_price": 220},
|
| 52 |
+
{"name": "Kraken", "type": "Exchange", "region": "US", "base_price": 150},
|
| 53 |
+
{"name": "Huobi", "type": "Exchange", "region": "Asia", "base_price": 140},
|
| 54 |
+
{"name": "KuCoin", "type": "Exchange", "region": "Global", "base_price": 130},
|
| 55 |
+
{"name": "Bitfinex", "type": "Exchange", "region": "Global", "base_price": 160},
|
| 56 |
+
{"name": "Bitstamp", "type": "Exchange", "region": "EU", "base_price": 145},
|
| 57 |
+
{"name": "Gemini", "type": "Exchange", "region": "US", "base_price": 200},
|
| 58 |
+
{"name": "OKX", "type": "Exchange", "region": "Global", "base_price": 135},
|
| 59 |
+
{"name": "Bybit", "type": "Exchange", "region": "Global", "base_price": 125},
|
| 60 |
+
{"name": "Gate.io", "type": "Exchange", "region": "Global", "base_price": 120},
|
| 61 |
+
{"name": "Crypto.com", "type": "Exchange", "region": "Global", "base_price": 155},
|
| 62 |
+
{"name": "Bittrex", "type": "Exchange", "region": "US", "base_price": 140},
|
| 63 |
+
{"name": "Poloniex", "type": "Exchange", "region": "Global", "base_price": 110},
|
| 64 |
+
{"name": "MEXC", "type": "Exchange", "region": "Global", "base_price": 105},
|
| 65 |
+
],
|
| 66 |
+
"data_providers": [
|
| 67 |
+
{"name": "CoinGecko", "type": "Data Provider", "region": "Global", "base_price": 100},
|
| 68 |
+
{"name": "CoinMarketCap", "type": "Data Provider", "region": "Global", "base_price": 120},
|
| 69 |
+
{"name": "CryptoCompare", "type": "Data Provider", "region": "Global", "base_price": 110},
|
| 70 |
+
{"name": "Messari", "type": "Analytics", "region": "Global", "base_price": 180},
|
| 71 |
+
{"name": "Glassnode", "type": "Analytics", "region": "Global", "base_price": 200},
|
| 72 |
+
{"name": "Santiment", "type": "Analytics", "region": "Global", "base_price": 170},
|
| 73 |
+
{"name": "Kaiko", "type": "Data Provider", "region": "Global", "base_price": 190},
|
| 74 |
+
{"name": "Nomics", "type": "Data Provider", "region": "Global", "base_price": 95},
|
| 75 |
+
],
|
| 76 |
+
"blockchain": [
|
| 77 |
+
{"name": "Etherscan", "type": "Block Explorer", "region": "Global", "base_price": 85},
|
| 78 |
+
{"name": "BscScan", "type": "Block Explorer", "region": "Global", "base_price": 80},
|
| 79 |
+
{"name": "Polygonscan", "type": "Block Explorer", "region": "Global", "base_price": 75},
|
| 80 |
+
{"name": "Blockchair", "type": "Block Explorer", "region": "Global", "base_price": 90},
|
| 81 |
+
{"name": "Blockchain.com", "type": "Block Explorer", "region": "Global", "base_price": 95},
|
| 82 |
+
],
|
| 83 |
+
"defi": [
|
| 84 |
+
{"name": "Uniswap", "type": "DEX", "region": "Global", "base_price": 70},
|
| 85 |
+
{"name": "SushiSwap", "type": "DEX", "region": "Global", "base_price": 65},
|
| 86 |
+
{"name": "PancakeSwap", "type": "DEX", "region": "Global", "base_price": 60},
|
| 87 |
+
{"name": "Curve", "type": "DEX", "region": "Global", "base_price": 75},
|
| 88 |
+
{"name": "1inch", "type": "DEX Aggregator", "region": "Global", "base_price": 80},
|
| 89 |
+
{"name": "Aave", "type": "Lending", "region": "Global", "base_price": 85},
|
| 90 |
+
{"name": "Compound", "type": "Lending", "region": "Global", "base_price": 90},
|
| 91 |
+
{"name": "MakerDAO", "type": "Stablecoin", "region": "Global", "base_price": 100},
|
| 92 |
+
],
|
| 93 |
+
"nft": [
|
| 94 |
+
{"name": "OpenSea", "type": "NFT Marketplace", "region": "Global", "base_price": 120},
|
| 95 |
+
{"name": "Blur", "type": "NFT Marketplace", "region": "Global", "base_price": 110},
|
| 96 |
+
{"name": "Magic Eden", "type": "NFT Marketplace", "region": "Global", "base_price": 95},
|
| 97 |
+
{"name": "Rarible", "type": "NFT Marketplace", "region": "Global", "base_price": 85},
|
| 98 |
+
]
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
CRYPTOCURRENCIES = [
|
| 102 |
+
{"symbol": "BTC", "name": "Bitcoin", "base_price": 43500, "category": "Layer 1"},
|
| 103 |
+
{"symbol": "ETH", "name": "Ethereum", "base_price": 2280, "category": "Smart Contract"},
|
| 104 |
+
{"symbol": "BNB", "name": "Binance Coin", "base_price": 315, "category": "Exchange"},
|
| 105 |
+
{"symbol": "SOL", "name": "Solana", "base_price": 98, "category": "Layer 1"},
|
| 106 |
+
{"symbol": "XRP", "name": "Ripple", "base_price": 0.53, "category": "Payment"},
|
| 107 |
+
{"symbol": "ADA", "name": "Cardano", "base_price": 0.39, "category": "Smart Contract"},
|
| 108 |
+
{"symbol": "AVAX", "name": "Avalanche", "base_price": 24, "category": "Layer 1"},
|
| 109 |
+
{"symbol": "DOGE", "name": "Dogecoin", "base_price": 0.08, "category": "Meme"},
|
| 110 |
+
{"symbol": "DOT", "name": "Polkadot", "base_price": 5.3, "category": "Layer 0"},
|
| 111 |
+
{"symbol": "MATIC", "name": "Polygon", "base_price": 0.74, "category": "Layer 2"},
|
| 112 |
+
{"symbol": "LINK", "name": "Chainlink", "base_price": 14.5, "category": "Oracle"},
|
| 113 |
+
{"symbol": "UNI", "name": "Uniswap", "base_price": 6.2, "category": "DeFi"},
|
| 114 |
+
{"symbol": "ATOM", "name": "Cosmos", "base_price": 8.9, "category": "Layer 0"},
|
| 115 |
+
{"symbol": "LTC", "name": "Litecoin", "base_price": 72, "category": "Payment"},
|
| 116 |
+
{"symbol": "APT", "name": "Aptos", "base_price": 7.8, "category": "Layer 1"},
|
| 117 |
+
{"symbol": "ARB", "name": "Arbitrum", "base_price": 1.2, "category": "Layer 2"},
|
| 118 |
+
{"symbol": "OP", "name": "Optimism", "base_price": 2.1, "category": "Layer 2"},
|
| 119 |
+
{"symbol": "NEAR", "name": "NEAR Protocol", "base_price": 3.4, "category": "Layer 1"},
|
| 120 |
+
{"symbol": "ICP", "name": "Internet Computer", "base_price": 4.7, "category": "Layer 1"},
|
| 121 |
+
{"symbol": "FIL", "name": "Filecoin", "base_price": 4.2, "category": "Storage"},
|
| 122 |
+
]
|
| 123 |
+
|
| 124 |
+
def generate_all_providers():
|
| 125 |
+
"""Generate complete provider data"""
|
| 126 |
+
result = []
|
| 127 |
+
|
| 128 |
+
for category, providers in PROVIDERS.items():
|
| 129 |
+
for provider in providers:
|
| 130 |
+
status = random.choices(
|
| 131 |
+
["operational", "degraded", "maintenance"],
|
| 132 |
+
weights=[85, 10, 5]
|
| 133 |
+
)[0]
|
| 134 |
+
|
| 135 |
+
uptime = random.uniform(97, 99.99) if status == "operational" else random.uniform(85, 97)
|
| 136 |
+
response_time = provider["base_price"] + random.randint(-20, 40)
|
| 137 |
+
|
| 138 |
+
result.append({
|
| 139 |
+
"name": provider["name"],
|
| 140 |
+
"type": provider["type"],
|
| 141 |
+
"category": category,
|
| 142 |
+
"region": provider["region"],
|
| 143 |
+
"status": status,
|
| 144 |
+
"uptime": round(uptime, 2),
|
| 145 |
+
"response_time_ms": response_time,
|
| 146 |
+
"requests_today": random.randint(50000, 2000000),
|
| 147 |
+
"requests_per_minute": random.randint(100, 5000),
|
| 148 |
+
"error_rate": round(random.uniform(0.01, 2.5), 2),
|
| 149 |
+
"last_check": datetime.now().isoformat(),
|
| 150 |
+
"api_version": f"v{random.randint(1,3)}.{random.randint(0,9)}",
|
| 151 |
+
"rate_limit": random.randint(100, 10000),
|
| 152 |
+
"endpoint": f"https://api.{provider['name'].lower().replace(' ', '').replace('.', '')}.com"
|
| 153 |
})
|
| 154 |
+
|
| 155 |
+
return result
|
| 156 |
+
|
| 157 |
+
def generate_crypto_prices():
|
| 158 |
+
"""Generate realistic cryptocurrency prices"""
|
| 159 |
+
result = []
|
| 160 |
+
|
| 161 |
+
for crypto in CRYPTOCURRENCIES:
|
| 162 |
+
change_24h = random.uniform(-15, 18)
|
| 163 |
+
change_7d = random.uniform(-25, 30)
|
| 164 |
+
volume = crypto["base_price"] * random.uniform(1e9, 5e10)
|
| 165 |
+
market_cap = crypto["base_price"] * random.uniform(1e9, 8e11)
|
| 166 |
+
|
| 167 |
+
result.append({
|
| 168 |
+
"symbol": crypto["symbol"],
|
| 169 |
+
"name": crypto["name"],
|
| 170 |
+
"category": crypto["category"],
|
| 171 |
+
"price": round(crypto["base_price"] * (1 + random.uniform(-0.08, 0.08)), 4),
|
| 172 |
+
"change_24h": round(change_24h, 2),
|
| 173 |
+
"change_7d": round(change_7d, 2),
|
| 174 |
+
"volume_24h": int(volume),
|
| 175 |
+
"market_cap": int(market_cap),
|
| 176 |
+
"circulating_supply": int(market_cap / crypto["base_price"]),
|
| 177 |
+
"total_supply": int(market_cap / crypto["base_price"] * random.uniform(1, 1.5)),
|
| 178 |
+
"ath": round(crypto["base_price"] * random.uniform(1.5, 8), 2),
|
| 179 |
+
"atl": round(crypto["base_price"] * random.uniform(0.01, 0.3), 4),
|
| 180 |
+
"rank": CRYPTOCURRENCIES.index(crypto) + 1,
|
| 181 |
+
"last_updated": datetime.now().isoformat()
|
| 182 |
})
|
| 183 |
+
|
| 184 |
+
return sorted(result, key=lambda x: x["market_cap"], reverse=True)
|
| 185 |
+
|
| 186 |
+
def generate_system_health():
|
| 187 |
+
"""Enhanced system health data"""
|
| 188 |
+
providers = generate_all_providers()
|
| 189 |
+
|
| 190 |
+
healthy = len([p for p in providers if p["status"] == "operational"])
|
| 191 |
+
degraded = len([p for p in providers if p["status"] == "degraded"])
|
| 192 |
+
down = len([p for p in providers if p["status"] == "maintenance"])
|
| 193 |
+
total = len(providers)
|
| 194 |
+
|
| 195 |
+
return {
|
| 196 |
+
"status": "healthy" if healthy / total > 0.9 else "degraded",
|
| 197 |
+
"timestamp": datetime.now().isoformat(),
|
| 198 |
+
"uptime_percentage": round((healthy / total) * 100, 2),
|
| 199 |
+
"summary": {
|
| 200 |
+
"total_providers": total,
|
| 201 |
+
"operational": healthy,
|
| 202 |
+
"degraded": degraded,
|
| 203 |
+
"maintenance": down,
|
| 204 |
+
"total_requests_today": sum(p["requests_today"] for p in providers),
|
| 205 |
+
"avg_response_time": round(sum(p["response_time_ms"] for p in providers) / total, 1),
|
| 206 |
+
"total_api_calls": random.randint(10000000, 50000000)
|
| 207 |
+
},
|
| 208 |
+
"by_category": {
|
| 209 |
+
category: {
|
| 210 |
+
"total": len([p for p in providers if p["category"] == category]),
|
| 211 |
+
"operational": len([p for p in providers if p["category"] == category and p["status"] == "operational"])
|
| 212 |
+
}
|
| 213 |
+
for category in PROVIDERS.keys()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
}
|
| 215 |
+
}
|
| 216 |
|
| 217 |
+
@app.get("/")
|
| 218 |
+
async def root():
|
| 219 |
+
return {
|
| 220 |
+
"name": "Crypto API Monitor Pro",
|
| 221 |
+
"version": "2.0.0",
|
| 222 |
+
"total_providers": sum(len(p) for p in PROVIDERS.values()),
|
| 223 |
+
"categories": list(PROVIDERS.keys()),
|
| 224 |
+
"endpoints": ["/health", "/api/providers", "/api/crypto/prices", "/api/stats", "/ws/live"]
|
| 225 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
|
| 227 |
+
@app.get("/health")
|
| 228 |
+
@app.get("/api/health")
|
| 229 |
+
async def health():
|
| 230 |
+
return generate_system_health()
|
| 231 |
+
|
| 232 |
+
@app.get("/api/providers")
|
| 233 |
+
async def get_providers(category: str = None):
|
| 234 |
+
providers = generate_all_providers()
|
| 235 |
+
if category:
|
| 236 |
+
providers = [p for p in providers if p["category"] == category]
|
| 237 |
+
return providers
|
| 238 |
+
|
| 239 |
+
@app.get("/api/providers/{name}")
|
| 240 |
+
async def get_provider_detail(name: str):
|
| 241 |
+
providers = generate_all_providers()
|
| 242 |
+
provider = next((p for p in providers if p["name"].lower() == name.lower()), None)
|
| 243 |
+
if provider:
|
| 244 |
+
provider["history"] = [
|
| 245 |
+
{"timestamp": (datetime.now() - timedelta(minutes=i*5)).isoformat(),
|
| 246 |
+
"response_time": random.randint(80, 250),
|
| 247 |
+
"status": "operational"}
|
| 248 |
+
for i in range(12)
|
| 249 |
]
|
| 250 |
+
return provider
|
| 251 |
+
|
| 252 |
+
@app.get("/api/crypto/prices")
|
| 253 |
+
async def get_crypto_prices(limit: int = 20):
|
| 254 |
+
return generate_crypto_prices()[:limit]
|
| 255 |
+
|
| 256 |
+
@app.get("/api/crypto/{symbol}")
|
| 257 |
+
async def get_crypto_detail(symbol: str):
|
| 258 |
+
prices = generate_crypto_prices()
|
| 259 |
+
crypto = next((c for c in prices if c["symbol"].upper() == symbol.upper()), None)
|
| 260 |
+
if crypto:
|
| 261 |
+
crypto["price_history"] = [
|
| 262 |
+
{"timestamp": (datetime.now() - timedelta(hours=i)).isoformat(),
|
| 263 |
+
"price": crypto["price"] * random.uniform(0.95, 1.05)}
|
| 264 |
+
for i in range(24)
|
| 265 |
+
]
|
| 266 |
+
return crypto
|
| 267 |
+
|
| 268 |
+
@app.get("/api/stats")
|
| 269 |
+
async def get_stats():
|
| 270 |
+
providers = generate_all_providers()
|
| 271 |
+
prices = generate_crypto_prices()
|
| 272 |
+
|
| 273 |
+
return {
|
| 274 |
+
"providers": {
|
| 275 |
+
"total": len(providers),
|
| 276 |
+
"by_type": {
|
| 277 |
+
"exchanges": len([p for p in providers if p["type"] == "Exchange"]),
|
| 278 |
+
"data_providers": len([p for p in providers if "Data" in p["type"]]),
|
| 279 |
+
"analytics": len([p for p in providers if p["type"] == "Analytics"]),
|
| 280 |
+
"defi": len([p for p in providers if p["category"] == "defi"]),
|
| 281 |
+
},
|
| 282 |
+
"by_status": {
|
| 283 |
+
"operational": len([p for p in providers if p["status"] == "operational"]),
|
| 284 |
+
"degraded": len([p for p in providers if p["status"] == "degraded"]),
|
| 285 |
+
"maintenance": len([p for p in providers if p["status"] == "maintenance"]),
|
| 286 |
+
}
|
| 287 |
+
},
|
| 288 |
+
"market": {
|
| 289 |
+
"total_market_cap": sum(c["market_cap"] for c in prices),
|
| 290 |
+
"total_volume_24h": sum(c["volume_24h"] for c in prices),
|
| 291 |
+
"avg_change_24h": round(sum(c["change_24h"] for c in prices) / len(prices), 2),
|
| 292 |
+
"btc_dominance": round((prices[0]["market_cap"] / sum(c["market_cap"] for c in prices)) * 100, 2),
|
| 293 |
+
},
|
| 294 |
+
"performance": {
|
| 295 |
+
"total_requests": sum(p["requests_today"] for p in providers),
|
| 296 |
+
"avg_response_time": round(sum(p["response_time_ms"] for p in providers) / len(providers), 1),
|
| 297 |
+
"uptime": round((len([p for p in providers if p["status"] == "operational"]) / len(providers)) * 100, 2),
|
| 298 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 299 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 300 |
|
| 301 |
+
@app.get("/api/categories")
|
| 302 |
+
async def get_categories():
|
| 303 |
+
return [
|
| 304 |
+
{"id": k, "name": k.title(), "count": len(v), "providers": [p["name"] for p in v]}
|
| 305 |
+
for k, v in PROVIDERS.items()
|
| 306 |
+
]
|
| 307 |
+
|
| 308 |
+
@app.get("/api/alerts")
|
| 309 |
+
async def get_alerts():
|
| 310 |
+
providers = generate_all_providers()
|
| 311 |
+
alerts = []
|
| 312 |
+
|
| 313 |
+
for p in providers:
|
| 314 |
+
if p["status"] == "degraded":
|
| 315 |
+
alerts.append({
|
| 316 |
+
"severity": "warning",
|
| 317 |
+
"provider": p["name"],
|
| 318 |
+
"message": f"{p['name']} is experiencing degraded performance",
|
| 319 |
+
"timestamp": datetime.now().isoformat()
|
| 320 |
+
})
|
| 321 |
+
if p["response_time_ms"] > 200:
|
| 322 |
+
alerts.append({
|
| 323 |
+
"severity": "info",
|
| 324 |
+
"provider": p["name"],
|
| 325 |
+
"message": f"High response time: {p['response_time_ms']}ms",
|
| 326 |
+
"timestamp": datetime.now().isoformat()
|
| 327 |
+
})
|
| 328 |
+
|
| 329 |
+
return alerts[:10]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 330 |
|
| 331 |
+
@app.websocket("/ws/live")
|
| 332 |
+
async def websocket_endpoint(websocket: WebSocket):
|
| 333 |
+
await manager.connect(websocket)
|
| 334 |
try:
|
| 335 |
+
while True:
|
| 336 |
+
await asyncio.sleep(3)
|
| 337 |
+
health = generate_system_health()
|
| 338 |
+
await websocket.send_json({
|
| 339 |
+
"type": "status_update",
|
| 340 |
+
"data": health,
|
| 341 |
+
"timestamp": datetime.now().isoformat()
|
| 342 |
+
})
|
| 343 |
+
except WebSocketDisconnect:
|
| 344 |
+
manager.disconnect(websocket)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 345 |
|
| 346 |
+
@app.get("/dashboard", response_class=HTMLResponse)
|
| 347 |
+
async def dashboard():
|
| 348 |
+
try:
|
| 349 |
+
with open("index.html", "r", encoding="utf-8") as f:
|
| 350 |
+
return HTMLResponse(content=f.read())
|
| 351 |
+
except:
|
| 352 |
+
return HTMLResponse("<h1>Dashboard not found</h1>", 404)
|
| 353 |
|
| 354 |
if __name__ == "__main__":
|
| 355 |
+
print("🚀 Crypto API Monitor Pro v2.0")
|
| 356 |
+
print(f"📊 {sum(len(p) for p in PROVIDERS.values())} Providers")
|
| 357 |
+
print("🌐 http://localhost:8000/dashboard")
|
| 358 |
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
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
|
|
|
config.js
CHANGED
|
@@ -143,4 +143,4 @@ console.log('🚀 Crypto API Monitor - Configuration loaded:', {
|
|
| 143 |
CONFIG.IS_LOCALHOST ? 'Localhost' : 'Custom Deployment',
|
| 144 |
apiBase: CONFIG.API_BASE,
|
| 145 |
wsBase: CONFIG.WS_BASE,
|
| 146 |
-
});
|
|
|
|
| 143 |
CONFIG.IS_LOCALHOST ? 'Localhost' : 'Custom Deployment',
|
| 144 |
apiBase: CONFIG.API_BASE,
|
| 145 |
wsBase: CONFIG.WS_BASE,
|
| 146 |
+
});
|
config.py
CHANGED
|
@@ -1,199 +1,320 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
"""
|
| 3 |
-
Configuration
|
| 4 |
-
|
| 5 |
"""
|
| 6 |
|
|
|
|
| 7 |
import os
|
|
|
|
| 8 |
from pathlib import Path
|
|
|
|
| 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 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
"""
|
| 2 |
+
Configuration Module for Crypto API Monitor
|
| 3 |
+
Loads and manages API registry from all_apis_merged_2025.json
|
| 4 |
"""
|
| 5 |
|
| 6 |
+
import json
|
| 7 |
import os
|
| 8 |
+
from typing import Dict, List, Any, Optional
|
| 9 |
from pathlib import Path
|
| 10 |
+
from utils.logger import setup_logger
|
| 11 |
|
| 12 |
+
logger = setup_logger("config")
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class ProviderConfig:
|
| 16 |
+
"""Provider configuration data class"""
|
| 17 |
+
|
| 18 |
+
def __init__(
|
| 19 |
+
self,
|
| 20 |
+
name: str,
|
| 21 |
+
category: str,
|
| 22 |
+
endpoint_url: str,
|
| 23 |
+
requires_key: bool = False,
|
| 24 |
+
api_key: Optional[str] = None,
|
| 25 |
+
rate_limit_type: Optional[str] = None,
|
| 26 |
+
rate_limit_value: Optional[int] = None,
|
| 27 |
+
timeout_ms: int = 10000,
|
| 28 |
+
priority_tier: int = 3,
|
| 29 |
+
health_check_endpoint: Optional[str] = None
|
| 30 |
+
):
|
| 31 |
+
self.name = name
|
| 32 |
+
self.category = category
|
| 33 |
+
self.endpoint_url = endpoint_url
|
| 34 |
+
self.requires_key = requires_key
|
| 35 |
+
self.api_key = api_key
|
| 36 |
+
self.rate_limit_type = rate_limit_type
|
| 37 |
+
self.rate_limit_value = rate_limit_value
|
| 38 |
+
self.timeout_ms = timeout_ms
|
| 39 |
+
self.priority_tier = priority_tier
|
| 40 |
+
self.health_check_endpoint = health_check_endpoint or endpoint_url
|
| 41 |
+
|
| 42 |
+
def to_dict(self) -> Dict:
|
| 43 |
+
"""Convert to dictionary"""
|
| 44 |
+
return {
|
| 45 |
+
"name": self.name,
|
| 46 |
+
"category": self.category,
|
| 47 |
+
"endpoint_url": self.endpoint_url,
|
| 48 |
+
"requires_key": self.requires_key,
|
| 49 |
+
"api_key_masked": self._mask_key() if self.api_key else None,
|
| 50 |
+
"rate_limit_type": self.rate_limit_type,
|
| 51 |
+
"rate_limit_value": self.rate_limit_value,
|
| 52 |
+
"timeout_ms": self.timeout_ms,
|
| 53 |
+
"priority_tier": self.priority_tier,
|
| 54 |
+
"health_check_endpoint": self.health_check_endpoint
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
def _mask_key(self) -> str:
|
| 58 |
+
"""Mask API key for security"""
|
| 59 |
+
if not self.api_key:
|
| 60 |
+
return None
|
| 61 |
+
if len(self.api_key) < 10:
|
| 62 |
+
return "***"
|
| 63 |
+
return f"{self.api_key[:8]}...{self.api_key[-4:]}"
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
class Config:
|
| 67 |
+
"""Configuration manager for API resources"""
|
| 68 |
+
|
| 69 |
+
def __init__(self, config_file: str = "all_apis_merged_2025.json"):
|
| 70 |
+
"""
|
| 71 |
+
Initialize configuration
|
| 72 |
+
|
| 73 |
+
Args:
|
| 74 |
+
config_file: Path to JSON configuration file
|
| 75 |
+
"""
|
| 76 |
+
self.base_dir = Path(__file__).parent
|
| 77 |
+
self.config_file = self.base_dir / config_file
|
| 78 |
+
self.providers: Dict[str, ProviderConfig] = {}
|
| 79 |
+
self.api_keys: Dict[str, List[str]] = {}
|
| 80 |
+
self.cors_proxies: List[str] = [
|
| 81 |
+
'https://api.allorigins.win/get?url=',
|
| 82 |
+
'https://proxy.cors.sh/',
|
| 83 |
+
'https://proxy.corsfix.com/?url=',
|
| 84 |
+
'https://api.codetabs.com/v1/proxy?quest=',
|
| 85 |
+
'https://thingproxy.freeboard.io/fetch/'
|
| 86 |
+
]
|
| 87 |
+
|
| 88 |
+
# Load environment variables
|
| 89 |
+
self._load_env_keys()
|
| 90 |
+
|
| 91 |
+
# Load from JSON
|
| 92 |
+
self._load_from_json()
|
| 93 |
+
|
| 94 |
+
# Build provider registry
|
| 95 |
+
self._build_provider_registry()
|
| 96 |
+
|
| 97 |
+
def _load_env_keys(self):
|
| 98 |
+
"""Load API keys from environment variables"""
|
| 99 |
+
env_keys = {
|
| 100 |
+
'etherscan': [
|
| 101 |
+
os.getenv('ETHERSCAN_KEY_1', ''),
|
| 102 |
+
os.getenv('ETHERSCAN_KEY_2', '')
|
| 103 |
+
],
|
| 104 |
+
'bscscan': [os.getenv('BSCSCAN_KEY', '')],
|
| 105 |
+
'tronscan': [os.getenv('TRONSCAN_KEY', '')],
|
| 106 |
+
'coinmarketcap': [
|
| 107 |
+
os.getenv('COINMARKETCAP_KEY_1', ''),
|
| 108 |
+
os.getenv('COINMARKETCAP_KEY_2', '')
|
| 109 |
+
],
|
| 110 |
+
'newsapi': [os.getenv('NEWSAPI_KEY', '')],
|
| 111 |
+
'cryptocompare': [os.getenv('CRYPTOCOMPARE_KEY', '')],
|
| 112 |
+
'huggingface': [os.getenv('HUGGINGFACE_KEY', '')]
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
# Filter out empty keys
|
| 116 |
+
for provider, keys in env_keys.items():
|
| 117 |
+
self.api_keys[provider] = [k for k in keys if k]
|
| 118 |
+
|
| 119 |
+
def _load_from_json(self):
|
| 120 |
+
"""Load configuration from JSON file"""
|
| 121 |
+
try:
|
| 122 |
+
if not self.config_file.exists():
|
| 123 |
+
logger.warning(f"Config file not found: {self.config_file}")
|
| 124 |
+
return
|
| 125 |
+
|
| 126 |
+
with open(self.config_file, 'r', encoding='utf-8') as f:
|
| 127 |
+
data = json.load(f)
|
| 128 |
+
|
| 129 |
+
# Load discovered keys
|
| 130 |
+
discovered_keys = data.get('discovered_keys', {})
|
| 131 |
+
for provider, keys in discovered_keys.items():
|
| 132 |
+
if isinstance(keys, list):
|
| 133 |
+
# Merge with env keys, preferring env keys
|
| 134 |
+
if provider not in self.api_keys or not self.api_keys[provider]:
|
| 135 |
+
self.api_keys[provider] = keys
|
| 136 |
+
else:
|
| 137 |
+
# Add discovered keys that aren't in env
|
| 138 |
+
for key in keys:
|
| 139 |
+
if key not in self.api_keys[provider]:
|
| 140 |
+
self.api_keys[provider].append(key)
|
| 141 |
+
|
| 142 |
+
logger.info(f"Loaded {len(self.api_keys)} provider keys from config")
|
| 143 |
+
|
| 144 |
+
except Exception as e:
|
| 145 |
+
logger.error(f"Error loading config file: {e}")
|
| 146 |
+
|
| 147 |
+
def _build_provider_registry(self):
|
| 148 |
+
"""Build provider registry from configuration"""
|
| 149 |
+
|
| 150 |
+
# Market Data Providers
|
| 151 |
+
self.providers['CoinGecko'] = ProviderConfig(
|
| 152 |
+
name='CoinGecko',
|
| 153 |
+
category='market_data',
|
| 154 |
+
endpoint_url='https://api.coingecko.com/api/v3',
|
| 155 |
+
requires_key=False,
|
| 156 |
+
rate_limit_type='per_minute',
|
| 157 |
+
rate_limit_value=50,
|
| 158 |
+
timeout_ms=10000,
|
| 159 |
+
priority_tier=1,
|
| 160 |
+
health_check_endpoint='https://api.coingecko.com/api/v3/ping'
|
| 161 |
+
)
|
| 162 |
+
|
| 163 |
+
# CoinMarketCap
|
| 164 |
+
cmc_keys = self.api_keys.get('coinmarketcap', [])
|
| 165 |
+
self.providers['CoinMarketCap'] = ProviderConfig(
|
| 166 |
+
name='CoinMarketCap',
|
| 167 |
+
category='market_data',
|
| 168 |
+
endpoint_url='https://pro-api.coinmarketcap.com/v1',
|
| 169 |
+
requires_key=True,
|
| 170 |
+
api_key=cmc_keys[0] if cmc_keys else None,
|
| 171 |
+
rate_limit_type='per_hour',
|
| 172 |
+
rate_limit_value=100,
|
| 173 |
+
timeout_ms=10000,
|
| 174 |
+
priority_tier=2,
|
| 175 |
+
health_check_endpoint='https://pro-api.coinmarketcap.com/v1/cryptocurrency/map?limit=1'
|
| 176 |
+
)
|
| 177 |
+
|
| 178 |
+
# Blockchain Explorers
|
| 179 |
+
etherscan_keys = self.api_keys.get('etherscan', [])
|
| 180 |
+
self.providers['Etherscan'] = ProviderConfig(
|
| 181 |
+
name='Etherscan',
|
| 182 |
+
category='blockchain_explorers',
|
| 183 |
+
endpoint_url='https://api.etherscan.io/api',
|
| 184 |
+
requires_key=True,
|
| 185 |
+
api_key=etherscan_keys[0] if etherscan_keys else None,
|
| 186 |
+
rate_limit_type='per_second',
|
| 187 |
+
rate_limit_value=5,
|
| 188 |
+
timeout_ms=10000,
|
| 189 |
+
priority_tier=1,
|
| 190 |
+
health_check_endpoint='https://api.etherscan.io/api?module=stats&action=ethsupply'
|
| 191 |
+
)
|
| 192 |
+
|
| 193 |
+
bscscan_keys = self.api_keys.get('bscscan', [])
|
| 194 |
+
self.providers['BscScan'] = ProviderConfig(
|
| 195 |
+
name='BscScan',
|
| 196 |
+
category='blockchain_explorers',
|
| 197 |
+
endpoint_url='https://api.bscscan.com/api',
|
| 198 |
+
requires_key=True,
|
| 199 |
+
api_key=bscscan_keys[0] if bscscan_keys else None,
|
| 200 |
+
rate_limit_type='per_second',
|
| 201 |
+
rate_limit_value=5,
|
| 202 |
+
timeout_ms=10000,
|
| 203 |
+
priority_tier=1,
|
| 204 |
+
health_check_endpoint='https://api.bscscan.com/api?module=stats&action=bnbsupply'
|
| 205 |
+
)
|
| 206 |
+
|
| 207 |
+
tronscan_keys = self.api_keys.get('tronscan', [])
|
| 208 |
+
self.providers['TronScan'] = ProviderConfig(
|
| 209 |
+
name='TronScan',
|
| 210 |
+
category='blockchain_explorers',
|
| 211 |
+
endpoint_url='https://apilist.tronscanapi.com/api',
|
| 212 |
+
requires_key=True,
|
| 213 |
+
api_key=tronscan_keys[0] if tronscan_keys else None,
|
| 214 |
+
rate_limit_type='per_minute',
|
| 215 |
+
rate_limit_value=60,
|
| 216 |
+
timeout_ms=10000,
|
| 217 |
+
priority_tier=2,
|
| 218 |
+
health_check_endpoint='https://apilist.tronscanapi.com/api/system/status'
|
| 219 |
+
)
|
| 220 |
+
|
| 221 |
+
# News APIs
|
| 222 |
+
self.providers['CryptoPanic'] = ProviderConfig(
|
| 223 |
+
name='CryptoPanic',
|
| 224 |
+
category='news',
|
| 225 |
+
endpoint_url='https://cryptopanic.com/api/v1',
|
| 226 |
+
requires_key=False,
|
| 227 |
+
rate_limit_type='per_hour',
|
| 228 |
+
rate_limit_value=100,
|
| 229 |
+
timeout_ms=10000,
|
| 230 |
+
priority_tier=2,
|
| 231 |
+
health_check_endpoint='https://cryptopanic.com/api/v1/posts/?auth_token=free&public=true'
|
| 232 |
+
)
|
| 233 |
+
|
| 234 |
+
newsapi_keys = self.api_keys.get('newsapi', [])
|
| 235 |
+
self.providers['NewsAPI'] = ProviderConfig(
|
| 236 |
+
name='NewsAPI',
|
| 237 |
+
category='news',
|
| 238 |
+
endpoint_url='https://newsdata.io/api/1',
|
| 239 |
+
requires_key=True,
|
| 240 |
+
api_key=newsapi_keys[0] if newsapi_keys else None,
|
| 241 |
+
rate_limit_type='per_day',
|
| 242 |
+
rate_limit_value=200,
|
| 243 |
+
timeout_ms=10000,
|
| 244 |
+
priority_tier=3,
|
| 245 |
+
health_check_endpoint='https://newsdata.io/api/1/news?category=business'
|
| 246 |
+
)
|
| 247 |
+
|
| 248 |
+
# Sentiment APIs
|
| 249 |
+
self.providers['AlternativeMe'] = ProviderConfig(
|
| 250 |
+
name='AlternativeMe',
|
| 251 |
+
category='sentiment',
|
| 252 |
+
endpoint_url='https://api.alternative.me',
|
| 253 |
+
requires_key=False,
|
| 254 |
+
rate_limit_type='per_minute',
|
| 255 |
+
rate_limit_value=60,
|
| 256 |
+
timeout_ms=10000,
|
| 257 |
+
priority_tier=2,
|
| 258 |
+
health_check_endpoint='https://api.alternative.me/fng/'
|
| 259 |
+
)
|
| 260 |
+
|
| 261 |
+
# CryptoCompare
|
| 262 |
+
cryptocompare_keys = self.api_keys.get('cryptocompare', [])
|
| 263 |
+
self.providers['CryptoCompare'] = ProviderConfig(
|
| 264 |
+
name='CryptoCompare',
|
| 265 |
+
category='market_data',
|
| 266 |
+
endpoint_url='https://min-api.cryptocompare.com/data',
|
| 267 |
+
requires_key=True,
|
| 268 |
+
api_key=cryptocompare_keys[0] if cryptocompare_keys else None,
|
| 269 |
+
rate_limit_type='per_hour',
|
| 270 |
+
rate_limit_value=250,
|
| 271 |
+
timeout_ms=10000,
|
| 272 |
+
priority_tier=2,
|
| 273 |
+
health_check_endpoint='https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=USD'
|
| 274 |
+
)
|
| 275 |
+
|
| 276 |
+
logger.info(f"Built provider registry with {len(self.providers)} providers")
|
| 277 |
+
|
| 278 |
+
def get_provider(self, name: str) -> Optional[ProviderConfig]:
|
| 279 |
+
"""Get provider configuration by name"""
|
| 280 |
+
return self.providers.get(name)
|
| 281 |
+
|
| 282 |
+
def get_all_providers(self) -> List[ProviderConfig]:
|
| 283 |
+
"""Get all provider configurations"""
|
| 284 |
+
return list(self.providers.values())
|
| 285 |
+
|
| 286 |
+
def get_providers_by_category(self, category: str) -> List[ProviderConfig]:
|
| 287 |
+
"""Get providers by category"""
|
| 288 |
+
return [p for p in self.providers.values() if p.category == category]
|
| 289 |
+
|
| 290 |
+
def get_providers_by_tier(self, tier: int) -> List[ProviderConfig]:
|
| 291 |
+
"""Get providers by priority tier"""
|
| 292 |
+
return [p for p in self.providers.values() if p.priority_tier == tier]
|
| 293 |
+
|
| 294 |
+
def get_api_key(self, provider: str, index: int = 0) -> Optional[str]:
|
| 295 |
+
"""Get API key for provider"""
|
| 296 |
+
keys = self.api_keys.get(provider.lower(), [])
|
| 297 |
+
if keys and 0 <= index < len(keys):
|
| 298 |
+
return keys[index]
|
| 299 |
+
return None
|
| 300 |
+
|
| 301 |
+
def get_categories(self) -> List[str]:
|
| 302 |
+
"""Get all unique categories"""
|
| 303 |
+
return list(set(p.category for p in self.providers.values()))
|
| 304 |
+
|
| 305 |
+
def stats(self) -> Dict[str, Any]:
|
| 306 |
+
"""Get configuration statistics"""
|
| 307 |
+
return {
|
| 308 |
+
'total_providers': len(self.providers),
|
| 309 |
+
'categories': len(self.get_categories()),
|
| 310 |
+
'providers_with_keys': sum(1 for p in self.providers.values() if p.requires_key),
|
| 311 |
+
'tier1_count': len(self.get_providers_by_tier(1)),
|
| 312 |
+
'tier2_count': len(self.get_providers_by_tier(2)),
|
| 313 |
+
'tier3_count': len(self.get_providers_by_tier(3)),
|
| 314 |
+
'api_keys_loaded': len(self.api_keys),
|
| 315 |
+
'categories_list': self.get_categories()
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
|
| 319 |
+
# Global config instance
|
| 320 |
+
config = Config()
|
dashboard.html
CHANGED
|
@@ -492,90 +492,39 @@ Market is bullish today</textarea>
|
|
| 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 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 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 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 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 |
-
|
| 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 |
|
|
|
|
| 492 |
<script>
|
| 493 |
async function loadData() {
|
| 494 |
try {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 495 |
// Load status
|
| 496 |
const statusRes = await fetch('/api/status');
|
|
|
|
|
|
|
|
|
|
| 497 |
const status = await statusRes.json();
|
| 498 |
|
| 499 |
+
document.getElementById('totalAPIs').textContent = status.total_providers;
|
| 500 |
+
document.getElementById('onlineAPIs').textContent = status.online;
|
| 501 |
+
document.getElementById('offlineAPIs').textContent = status.offline;
|
| 502 |
+
document.getElementById('avgResponse').textContent = status.avg_response_time_ms + 'ms';
|
| 503 |
+
document.getElementById('lastUpdate').textContent = new Date(status.timestamp).toLocaleString();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 504 |
|
| 505 |
// Load providers
|
| 506 |
const providersRes = await fetch('/api/providers');
|
|
|
|
|
|
|
|
|
|
| 507 |
const providers = await providersRes.json();
|
| 508 |
|
| 509 |
+
const tbody = document.getElementById('providersTable');
|
| 510 |
+
tbody.innerHTML = providers.map(p => {
|
| 511 |
+
let responseClass = 'response-fast';
|
| 512 |
+
if (p.response_time_ms > 3000) responseClass = 'response-slow';
|
| 513 |
+
else if (p.response_time_ms > 1000) responseClass = 'response-medium';
|
| 514 |
+
|
| 515 |
+
return `
|
| 516 |
+
<tr>
|
| 517 |
+
<td><strong style="font-size: 15px;">${p.name}</strong></td>
|
| 518 |
+
<td><span style="background: #f8f9fa; padding: 4px 10px; border-radius: 8px; font-size: 12px; font-weight: 600;">${p.category}</span></td>
|
| 519 |
+
<td><span class="status-badge status-${p.status}">${p.status.toUpperCase()}</span></td>
|
| 520 |
+
<td><span class="response-time ${responseClass}">${p.response_time_ms}ms</span></td>
|
| 521 |
+
<td style="color: #6c757d; font-size: 13px;">${new Date(p.last_fetch).toLocaleTimeString()}</td>
|
| 522 |
+
</tr>
|
| 523 |
+
`}).join('');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 524 |
|
| 525 |
} catch (error) {
|
| 526 |
console.error('Error loading data:', error);
|
| 527 |
+
alert('Error loading data: ' + error.message);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 528 |
}
|
| 529 |
}
|
| 530 |
|
database.py
CHANGED
|
@@ -1,665 +1,480 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
"""
|
| 3 |
-
Database
|
| 4 |
-
|
| 5 |
"""
|
| 6 |
|
| 7 |
import sqlite3
|
| 8 |
-
import threading
|
| 9 |
import json
|
|
|
|
|
|
|
| 10 |
from datetime import datetime, timedelta
|
| 11 |
-
from
|
| 12 |
from contextlib import contextmanager
|
| 13 |
-
import
|
| 14 |
|
| 15 |
-
import config
|
| 16 |
-
|
| 17 |
-
# Setup logging
|
| 18 |
-
logging.basicConfig(
|
| 19 |
-
level=getattr(logging, config.LOG_LEVEL),
|
| 20 |
-
format=config.LOG_FORMAT,
|
| 21 |
-
handlers=[
|
| 22 |
-
logging.FileHandler(config.LOG_FILE),
|
| 23 |
-
logging.StreamHandler()
|
| 24 |
-
]
|
| 25 |
-
)
|
| 26 |
logger = logging.getLogger(__name__)
|
| 27 |
|
| 28 |
|
| 29 |
-
class
|
| 30 |
-
"""
|
| 31 |
-
Database manager for cryptocurrency data with full CRUD operations
|
| 32 |
-
Thread-safe implementation using context managers
|
| 33 |
-
"""
|
| 34 |
|
| 35 |
-
def __init__(self, db_path: str =
|
| 36 |
-
"""Initialize database
|
| 37 |
-
self.db_path =
|
| 38 |
-
self.
|
| 39 |
self._init_database()
|
| 40 |
-
logger.info(f"Database initialized at {self.db_path}")
|
| 41 |
|
| 42 |
@contextmanager
|
| 43 |
def get_connection(self):
|
| 44 |
-
"""
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
self.db_path,
|
| 48 |
-
check_same_thread=False,
|
| 49 |
-
timeout=30.0
|
| 50 |
-
)
|
| 51 |
-
self._local.conn.row_factory = sqlite3.Row
|
| 52 |
-
|
| 53 |
try:
|
| 54 |
-
yield
|
|
|
|
| 55 |
except Exception as e:
|
| 56 |
-
|
| 57 |
logger.error(f"Database error: {e}")
|
| 58 |
raise
|
|
|
|
|
|
|
| 59 |
|
| 60 |
def _init_database(self):
|
| 61 |
-
"""Initialize
|
| 62 |
with self.get_connection() as conn:
|
| 63 |
cursor = conn.cursor()
|
| 64 |
|
| 65 |
-
#
|
| 66 |
cursor.execute("""
|
| 67 |
-
CREATE TABLE IF NOT EXISTS
|
| 68 |
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
| 79 |
)
|
| 80 |
""")
|
| 81 |
|
| 82 |
-
#
|
| 83 |
cursor.execute("""
|
| 84 |
-
CREATE TABLE IF NOT EXISTS
|
| 85 |
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
| 95 |
)
|
| 96 |
""")
|
| 97 |
|
| 98 |
-
#
|
| 99 |
cursor.execute("""
|
| 100 |
-
CREATE TABLE IF NOT EXISTS
|
| 101 |
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
|
|
|
|
|
|
| 110 |
)
|
| 111 |
""")
|
| 112 |
|
| 113 |
-
#
|
| 114 |
cursor.execute("""
|
| 115 |
-
CREATE TABLE IF NOT EXISTS
|
| 116 |
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
)
|
| 121 |
""")
|
| 122 |
|
| 123 |
-
#
|
| 124 |
-
cursor.execute("
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
cursor.execute("CREATE INDEX IF NOT EXISTS idx_analysis_timestamp ON market_analysis(timestamp)")
|
| 132 |
-
|
| 133 |
-
conn.commit()
|
| 134 |
-
logger.info("Database tables and indexes created successfully")
|
| 135 |
-
|
| 136 |
-
# ==================== PRICES CRUD OPERATIONS ====================
|
| 137 |
-
|
| 138 |
-
def save_price(self, price_data: Dict[str, Any]) -> bool:
|
| 139 |
-
"""
|
| 140 |
-
Save a single price record
|
| 141 |
-
|
| 142 |
-
Args:
|
| 143 |
-
price_data: Dictionary containing price information
|
| 144 |
-
|
| 145 |
-
Returns:
|
| 146 |
-
bool: True if successful, False otherwise
|
| 147 |
-
"""
|
| 148 |
-
try:
|
| 149 |
-
with self.get_connection() as conn:
|
| 150 |
-
cursor = conn.cursor()
|
| 151 |
-
cursor.execute("""
|
| 152 |
-
INSERT INTO prices
|
| 153 |
-
(symbol, name, price_usd, volume_24h, market_cap,
|
| 154 |
-
percent_change_1h, percent_change_24h, percent_change_7d, rank)
|
| 155 |
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 156 |
-
""", (
|
| 157 |
-
price_data.get('symbol'),
|
| 158 |
-
price_data.get('name'),
|
| 159 |
-
price_data.get('price_usd', 0.0),
|
| 160 |
-
price_data.get('volume_24h'),
|
| 161 |
-
price_data.get('market_cap'),
|
| 162 |
-
price_data.get('percent_change_1h'),
|
| 163 |
-
price_data.get('percent_change_24h'),
|
| 164 |
-
price_data.get('percent_change_7d'),
|
| 165 |
-
price_data.get('rank')
|
| 166 |
-
))
|
| 167 |
-
conn.commit()
|
| 168 |
-
return True
|
| 169 |
-
except Exception as e:
|
| 170 |
-
logger.error(f"Error saving price: {e}")
|
| 171 |
-
return False
|
| 172 |
-
|
| 173 |
-
def save_prices_batch(self, prices: List[Dict[str, Any]]) -> int:
|
| 174 |
-
"""
|
| 175 |
-
Save multiple price records in batch (minimum 100 records for efficiency)
|
| 176 |
|
| 177 |
-
|
| 178 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
|
| 180 |
-
|
| 181 |
-
int: Number of records saved
|
| 182 |
-
"""
|
| 183 |
-
saved_count = 0
|
| 184 |
-
try:
|
| 185 |
-
with self.get_connection() as conn:
|
| 186 |
-
cursor = conn.cursor()
|
| 187 |
-
for price_data in prices:
|
| 188 |
-
try:
|
| 189 |
-
cursor.execute("""
|
| 190 |
-
INSERT INTO prices
|
| 191 |
-
(symbol, name, price_usd, volume_24h, market_cap,
|
| 192 |
-
percent_change_1h, percent_change_24h, percent_change_7d, rank)
|
| 193 |
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 194 |
-
""", (
|
| 195 |
-
price_data.get('symbol'),
|
| 196 |
-
price_data.get('name'),
|
| 197 |
-
price_data.get('price_usd', 0.0),
|
| 198 |
-
price_data.get('volume_24h'),
|
| 199 |
-
price_data.get('market_cap'),
|
| 200 |
-
price_data.get('percent_change_1h'),
|
| 201 |
-
price_data.get('percent_change_24h'),
|
| 202 |
-
price_data.get('percent_change_7d'),
|
| 203 |
-
price_data.get('rank')
|
| 204 |
-
))
|
| 205 |
-
saved_count += 1
|
| 206 |
-
except Exception as e:
|
| 207 |
-
logger.warning(f"Error saving individual price: {e}")
|
| 208 |
-
continue
|
| 209 |
-
conn.commit()
|
| 210 |
-
logger.info(f"Batch saved {saved_count} price records")
|
| 211 |
-
except Exception as e:
|
| 212 |
-
logger.error(f"Error in batch save: {e}")
|
| 213 |
-
return saved_count
|
| 214 |
|
| 215 |
-
def
|
| 216 |
-
"""
|
| 217 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
|
| 219 |
-
|
| 220 |
-
limit: Maximum number of records to return
|
| 221 |
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
cursor = conn.cursor()
|
| 228 |
-
cursor.execute("""
|
| 229 |
-
SELECT DISTINCT ON (symbol) *
|
| 230 |
-
FROM prices
|
| 231 |
-
WHERE timestamp >= datetime('now', '-1 hour')
|
| 232 |
-
ORDER BY symbol, timestamp DESC, rank ASC
|
| 233 |
-
LIMIT ?
|
| 234 |
-
""", (limit,))
|
| 235 |
-
|
| 236 |
-
# SQLite doesn't support DISTINCT ON, use subquery instead
|
| 237 |
-
cursor.execute("""
|
| 238 |
-
SELECT p1.*
|
| 239 |
-
FROM prices p1
|
| 240 |
-
INNER JOIN (
|
| 241 |
-
SELECT symbol, MAX(timestamp) as max_ts
|
| 242 |
-
FROM prices
|
| 243 |
-
WHERE timestamp >= datetime('now', '-1 hour')
|
| 244 |
-
GROUP BY symbol
|
| 245 |
-
) p2 ON p1.symbol = p2.symbol AND p1.timestamp = p2.max_ts
|
| 246 |
-
ORDER BY p1.rank ASC, p1.market_cap DESC
|
| 247 |
LIMIT ?
|
| 248 |
-
"""
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
def get_price_history(self, symbol: str, hours: int = 24) -> List[Dict[str, Any]]:
|
| 256 |
-
"""
|
| 257 |
-
Get price history for a specific symbol
|
| 258 |
-
|
| 259 |
-
Args:
|
| 260 |
-
symbol: Cryptocurrency symbol
|
| 261 |
-
hours: Number of hours to look back
|
| 262 |
-
|
| 263 |
-
Returns:
|
| 264 |
-
List of price dictionaries
|
| 265 |
-
"""
|
| 266 |
-
try:
|
| 267 |
-
with self.get_connection() as conn:
|
| 268 |
-
cursor = conn.cursor()
|
| 269 |
-
cursor.execute("""
|
| 270 |
-
SELECT * FROM prices
|
| 271 |
-
WHERE symbol = ?
|
| 272 |
-
AND timestamp >= datetime('now', '-' || ? || ' hours')
|
| 273 |
-
ORDER BY timestamp ASC
|
| 274 |
-
""", (symbol, hours))
|
| 275 |
-
return [dict(row) for row in cursor.fetchall()]
|
| 276 |
-
except Exception as e:
|
| 277 |
-
logger.error(f"Error getting price history: {e}")
|
| 278 |
-
return []
|
| 279 |
-
|
| 280 |
-
def get_top_gainers(self, limit: int = 10) -> List[Dict[str, Any]]:
|
| 281 |
-
"""Get top gaining cryptocurrencies in last 24h"""
|
| 282 |
-
try:
|
| 283 |
-
with self.get_connection() as conn:
|
| 284 |
-
cursor = conn.cursor()
|
| 285 |
-
cursor.execute("""
|
| 286 |
-
SELECT p1.*
|
| 287 |
-
FROM prices p1
|
| 288 |
-
INNER JOIN (
|
| 289 |
-
SELECT symbol, MAX(timestamp) as max_ts
|
| 290 |
-
FROM prices
|
| 291 |
-
WHERE timestamp >= datetime('now', '-1 hour')
|
| 292 |
-
GROUP BY symbol
|
| 293 |
-
) p2 ON p1.symbol = p2.symbol AND p1.timestamp = p2.max_ts
|
| 294 |
-
WHERE p1.percent_change_24h IS NOT NULL
|
| 295 |
-
ORDER BY p1.percent_change_24h DESC
|
| 296 |
LIMIT ?
|
| 297 |
-
"""
|
| 298 |
-
|
| 299 |
-
except Exception as e:
|
| 300 |
-
logger.error(f"Error getting top gainers: {e}")
|
| 301 |
-
return []
|
| 302 |
-
|
| 303 |
-
def delete_old_prices(self, days: int = 30) -> int:
|
| 304 |
-
"""
|
| 305 |
-
Delete price records older than specified days
|
| 306 |
-
|
| 307 |
-
Args:
|
| 308 |
-
days: Number of days to keep
|
| 309 |
-
|
| 310 |
-
Returns:
|
| 311 |
-
Number of deleted records
|
| 312 |
-
"""
|
| 313 |
-
try:
|
| 314 |
-
with self.get_connection() as conn:
|
| 315 |
-
cursor = conn.cursor()
|
| 316 |
-
cursor.execute("""
|
| 317 |
-
DELETE FROM prices
|
| 318 |
-
WHERE timestamp < datetime('now', '-' || ? || ' days')
|
| 319 |
-
""", (days,))
|
| 320 |
-
conn.commit()
|
| 321 |
-
deleted = cursor.rowcount
|
| 322 |
-
logger.info(f"Deleted {deleted} old price records")
|
| 323 |
-
return deleted
|
| 324 |
-
except Exception as e:
|
| 325 |
-
logger.error(f"Error deleting old prices: {e}")
|
| 326 |
-
return 0
|
| 327 |
|
| 328 |
-
|
| 329 |
|
| 330 |
-
def
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
bool: True if successful, False otherwise
|
| 339 |
-
"""
|
| 340 |
-
try:
|
| 341 |
-
with self.get_connection() as conn:
|
| 342 |
-
cursor = conn.cursor()
|
| 343 |
-
cursor.execute("""
|
| 344 |
-
INSERT OR IGNORE INTO news
|
| 345 |
-
(title, summary, url, source, sentiment_score,
|
| 346 |
-
sentiment_label, related_coins, published_date)
|
| 347 |
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
| 348 |
-
""", (
|
| 349 |
-
news_data.get('title'),
|
| 350 |
-
news_data.get('summary'),
|
| 351 |
-
news_data.get('url'),
|
| 352 |
-
news_data.get('source'),
|
| 353 |
-
news_data.get('sentiment_score'),
|
| 354 |
-
news_data.get('sentiment_label'),
|
| 355 |
-
json.dumps(news_data.get('related_coins', [])),
|
| 356 |
-
news_data.get('published_date')
|
| 357 |
-
))
|
| 358 |
-
conn.commit()
|
| 359 |
-
return True
|
| 360 |
-
except Exception as e:
|
| 361 |
-
logger.error(f"Error saving news: {e}")
|
| 362 |
-
return False
|
| 363 |
-
|
| 364 |
-
def get_latest_news(self, limit: int = 50, sentiment: Optional[str] = None) -> List[Dict[str, Any]]:
|
| 365 |
-
"""
|
| 366 |
-
Get latest news articles
|
| 367 |
-
|
| 368 |
-
Args:
|
| 369 |
-
limit: Maximum number of articles
|
| 370 |
-
sentiment: Filter by sentiment label (optional)
|
| 371 |
|
| 372 |
-
|
| 373 |
-
List of news dictionaries
|
| 374 |
-
"""
|
| 375 |
-
try:
|
| 376 |
-
with self.get_connection() as conn:
|
| 377 |
-
cursor = conn.cursor()
|
| 378 |
-
|
| 379 |
-
if sentiment:
|
| 380 |
-
cursor.execute("""
|
| 381 |
-
SELECT * FROM news
|
| 382 |
-
WHERE sentiment_label = ?
|
| 383 |
-
ORDER BY published_date DESC, timestamp DESC
|
| 384 |
-
LIMIT ?
|
| 385 |
-
""", (sentiment, limit))
|
| 386 |
-
else:
|
| 387 |
-
cursor.execute("""
|
| 388 |
-
SELECT * FROM news
|
| 389 |
-
ORDER BY published_date DESC, timestamp DESC
|
| 390 |
-
LIMIT ?
|
| 391 |
-
""", (limit,))
|
| 392 |
-
|
| 393 |
-
results = []
|
| 394 |
-
for row in cursor.fetchall():
|
| 395 |
-
news_dict = dict(row)
|
| 396 |
-
if news_dict.get('related_coins'):
|
| 397 |
-
try:
|
| 398 |
-
news_dict['related_coins'] = json.loads(news_dict['related_coins'])
|
| 399 |
-
except:
|
| 400 |
-
news_dict['related_coins'] = []
|
| 401 |
-
results.append(news_dict)
|
| 402 |
-
|
| 403 |
-
return results
|
| 404 |
-
except Exception as e:
|
| 405 |
-
logger.error(f"Error getting latest news: {e}")
|
| 406 |
-
return []
|
| 407 |
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
return results
|
| 431 |
-
except Exception as e:
|
| 432 |
-
logger.error(f"Error getting news by coin: {e}")
|
| 433 |
-
return []
|
| 434 |
|
| 435 |
-
|
| 436 |
-
"""Update sentiment for a news article"""
|
| 437 |
-
try:
|
| 438 |
-
with self.get_connection() as conn:
|
| 439 |
-
cursor = conn.cursor()
|
| 440 |
-
cursor.execute("""
|
| 441 |
-
UPDATE news
|
| 442 |
-
SET sentiment_score = ?, sentiment_label = ?
|
| 443 |
-
WHERE id = ?
|
| 444 |
-
""", (sentiment_score, sentiment_label, news_id))
|
| 445 |
-
conn.commit()
|
| 446 |
-
return True
|
| 447 |
-
except Exception as e:
|
| 448 |
-
logger.error(f"Error updating news sentiment: {e}")
|
| 449 |
-
return False
|
| 450 |
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 467 |
|
| 468 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 469 |
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
with self.get_connection() as conn:
|
| 474 |
-
cursor = conn.cursor()
|
| 475 |
-
cursor.execute("""
|
| 476 |
-
INSERT INTO market_analysis
|
| 477 |
-
(symbol, timeframe, trend, support_level, resistance_level,
|
| 478 |
-
prediction, confidence)
|
| 479 |
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
| 480 |
-
""", (
|
| 481 |
-
analysis_data.get('symbol'),
|
| 482 |
-
analysis_data.get('timeframe'),
|
| 483 |
-
analysis_data.get('trend'),
|
| 484 |
-
analysis_data.get('support_level'),
|
| 485 |
-
analysis_data.get('resistance_level'),
|
| 486 |
-
analysis_data.get('prediction'),
|
| 487 |
-
analysis_data.get('confidence')
|
| 488 |
-
))
|
| 489 |
-
conn.commit()
|
| 490 |
-
return True
|
| 491 |
-
except Exception as e:
|
| 492 |
-
logger.error(f"Error saving analysis: {e}")
|
| 493 |
-
return False
|
| 494 |
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
cursor.execute("""
|
| 501 |
-
SELECT * FROM market_analysis
|
| 502 |
-
WHERE symbol = ?
|
| 503 |
-
ORDER BY timestamp DESC
|
| 504 |
-
LIMIT 1
|
| 505 |
-
""", (symbol,))
|
| 506 |
-
row = cursor.fetchone()
|
| 507 |
-
return dict(row) if row else None
|
| 508 |
-
except Exception as e:
|
| 509 |
-
logger.error(f"Error getting latest analysis: {e}")
|
| 510 |
-
return None
|
| 511 |
|
| 512 |
-
def
|
| 513 |
-
"""Get all
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
return [dict(row) for row in cursor.fetchall()]
|
| 523 |
-
except Exception as e:
|
| 524 |
-
logger.error(f"Error getting all analyses: {e}")
|
| 525 |
-
return []
|
| 526 |
|
| 527 |
-
|
|
|
|
|
|
|
|
|
|
| 528 |
|
| 529 |
-
|
| 530 |
-
"""Log a user query"""
|
| 531 |
-
try:
|
| 532 |
-
with self.get_connection() as conn:
|
| 533 |
-
cursor = conn.cursor()
|
| 534 |
-
cursor.execute("""
|
| 535 |
-
INSERT INTO user_queries (query, result_count)
|
| 536 |
-
VALUES (?, ?)
|
| 537 |
-
""", (query, result_count))
|
| 538 |
-
conn.commit()
|
| 539 |
-
return True
|
| 540 |
-
except Exception as e:
|
| 541 |
-
logger.error(f"Error logging user query: {e}")
|
| 542 |
-
return False
|
| 543 |
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 558 |
|
| 559 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 560 |
|
| 561 |
-
def
|
| 562 |
-
"""
|
| 563 |
-
|
|
|
|
| 564 |
|
| 565 |
-
|
| 566 |
-
query: SQL query (must start with SELECT)
|
| 567 |
-
params: Query parameters
|
| 568 |
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 585 |
|
| 586 |
-
|
| 587 |
-
"""Get database statistics"""
|
| 588 |
-
try:
|
| 589 |
-
with self.get_connection() as conn:
|
| 590 |
-
cursor = conn.cursor()
|
| 591 |
|
| 592 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 593 |
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
|
|
|
|
|
|
| 598 |
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 602 |
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
|
|
|
| 606 |
|
| 607 |
-
|
| 608 |
-
cursor.execute("SELECT MAX(timestamp) as latest FROM news")
|
| 609 |
-
stats['latest_news_update'] = cursor.fetchone()['latest']
|
| 610 |
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 616 |
|
| 617 |
-
|
| 618 |
-
except Exception as e:
|
| 619 |
-
logger.error(f"Error getting database stats: {e}")
|
| 620 |
-
return {}
|
| 621 |
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 632 |
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
try:
|
| 636 |
-
import shutil
|
| 637 |
-
if backup_path is None:
|
| 638 |
-
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 639 |
-
backup_path = config.DATABASE_BACKUP_DIR / f"backup_{timestamp}.db"
|
| 640 |
-
|
| 641 |
-
shutil.copy2(self.db_path, backup_path)
|
| 642 |
-
logger.info(f"Database backed up to {backup_path}")
|
| 643 |
-
return True
|
| 644 |
-
except Exception as e:
|
| 645 |
-
logger.error(f"Error backing up database: {e}")
|
| 646 |
-
return False
|
| 647 |
|
| 648 |
-
|
| 649 |
-
"""Close database connection"""
|
| 650 |
-
if hasattr(self._local, 'conn'):
|
| 651 |
-
self._local.conn.close()
|
| 652 |
-
delattr(self._local, 'conn')
|
| 653 |
-
logger.info("Database connection closed")
|
| 654 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 655 |
|
| 656 |
-
|
| 657 |
-
_db_instance = None
|
| 658 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 659 |
|
| 660 |
-
|
| 661 |
-
"""Get database singleton instance"""
|
| 662 |
-
global _db_instance
|
| 663 |
-
if _db_instance is None:
|
| 664 |
-
_db_instance = CryptoDatabase()
|
| 665 |
-
return _db_instance
|
|
|
|
|
|
|
| 1 |
"""
|
| 2 |
+
SQLite Database Module for Persistent Storage
|
| 3 |
+
Stores health metrics, incidents, and historical data
|
| 4 |
"""
|
| 5 |
|
| 6 |
import sqlite3
|
|
|
|
| 7 |
import json
|
| 8 |
+
import logging
|
| 9 |
+
from typing import List, Dict, Optional, Tuple
|
| 10 |
from datetime import datetime, timedelta
|
| 11 |
+
from pathlib import Path
|
| 12 |
from contextlib import contextmanager
|
| 13 |
+
from monitor import HealthCheckResult, HealthStatus
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
logger = logging.getLogger(__name__)
|
| 16 |
|
| 17 |
|
| 18 |
+
class Database:
|
| 19 |
+
"""SQLite database manager for metrics and history"""
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
+
def __init__(self, db_path: str = "data/health_metrics.db"):
|
| 22 |
+
"""Initialize database connection"""
|
| 23 |
+
self.db_path = Path(db_path)
|
| 24 |
+
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
| 25 |
self._init_database()
|
|
|
|
| 26 |
|
| 27 |
@contextmanager
|
| 28 |
def get_connection(self):
|
| 29 |
+
"""Context manager for database connections"""
|
| 30 |
+
conn = sqlite3.connect(self.db_path)
|
| 31 |
+
conn.row_factory = sqlite3.Row # Enable column access by name
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
try:
|
| 33 |
+
yield conn
|
| 34 |
+
conn.commit()
|
| 35 |
except Exception as e:
|
| 36 |
+
conn.rollback()
|
| 37 |
logger.error(f"Database error: {e}")
|
| 38 |
raise
|
| 39 |
+
finally:
|
| 40 |
+
conn.close()
|
| 41 |
|
| 42 |
def _init_database(self):
|
| 43 |
+
"""Initialize database schema"""
|
| 44 |
with self.get_connection() as conn:
|
| 45 |
cursor = conn.cursor()
|
| 46 |
|
| 47 |
+
# Status log table
|
| 48 |
cursor.execute("""
|
| 49 |
+
CREATE TABLE IF NOT EXISTS status_log (
|
| 50 |
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 51 |
+
provider_name TEXT NOT NULL,
|
| 52 |
+
category TEXT NOT NULL,
|
| 53 |
+
status TEXT NOT NULL,
|
| 54 |
+
response_time REAL,
|
| 55 |
+
status_code INTEGER,
|
| 56 |
+
error_message TEXT,
|
| 57 |
+
endpoint_tested TEXT,
|
| 58 |
+
timestamp REAL NOT NULL,
|
| 59 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
|
|
| 60 |
)
|
| 61 |
""")
|
| 62 |
|
| 63 |
+
# Response times table (aggregated)
|
| 64 |
cursor.execute("""
|
| 65 |
+
CREATE TABLE IF NOT EXISTS response_times (
|
| 66 |
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 67 |
+
provider_name TEXT NOT NULL,
|
| 68 |
+
avg_response_time REAL NOT NULL,
|
| 69 |
+
min_response_time REAL NOT NULL,
|
| 70 |
+
max_response_time REAL NOT NULL,
|
| 71 |
+
sample_count INTEGER NOT NULL,
|
| 72 |
+
period_start TIMESTAMP NOT NULL,
|
| 73 |
+
period_end TIMESTAMP NOT NULL,
|
| 74 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
|
|
| 75 |
)
|
| 76 |
""")
|
| 77 |
|
| 78 |
+
# Incidents table
|
| 79 |
cursor.execute("""
|
| 80 |
+
CREATE TABLE IF NOT EXISTS incidents (
|
| 81 |
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 82 |
+
provider_name TEXT NOT NULL,
|
| 83 |
+
category TEXT NOT NULL,
|
| 84 |
+
incident_type TEXT NOT NULL,
|
| 85 |
+
description TEXT,
|
| 86 |
+
severity TEXT,
|
| 87 |
+
start_time TIMESTAMP NOT NULL,
|
| 88 |
+
end_time TIMESTAMP,
|
| 89 |
+
duration_seconds INTEGER,
|
| 90 |
+
resolved BOOLEAN DEFAULT 0,
|
| 91 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 92 |
)
|
| 93 |
""")
|
| 94 |
|
| 95 |
+
# Alerts table
|
| 96 |
cursor.execute("""
|
| 97 |
+
CREATE TABLE IF NOT EXISTS alerts (
|
| 98 |
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 99 |
+
provider_name TEXT NOT NULL,
|
| 100 |
+
alert_type TEXT NOT NULL,
|
| 101 |
+
message TEXT,
|
| 102 |
+
threshold_value REAL,
|
| 103 |
+
actual_value REAL,
|
| 104 |
+
triggered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 105 |
+
acknowledged BOOLEAN DEFAULT 0
|
| 106 |
)
|
| 107 |
""")
|
| 108 |
|
| 109 |
+
# Configuration table
|
| 110 |
+
cursor.execute("""
|
| 111 |
+
CREATE TABLE IF NOT EXISTS configuration (
|
| 112 |
+
key TEXT PRIMARY KEY,
|
| 113 |
+
value TEXT NOT NULL,
|
| 114 |
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 115 |
+
)
|
| 116 |
+
""")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
|
| 118 |
+
# Create indexes
|
| 119 |
+
cursor.execute("""
|
| 120 |
+
CREATE INDEX IF NOT EXISTS idx_status_log_provider
|
| 121 |
+
ON status_log(provider_name, timestamp)
|
| 122 |
+
""")
|
| 123 |
+
cursor.execute("""
|
| 124 |
+
CREATE INDEX IF NOT EXISTS idx_status_log_timestamp
|
| 125 |
+
ON status_log(timestamp)
|
| 126 |
+
""")
|
| 127 |
+
cursor.execute("""
|
| 128 |
+
CREATE INDEX IF NOT EXISTS idx_incidents_provider
|
| 129 |
+
ON incidents(provider_name, start_time)
|
| 130 |
+
""")
|
| 131 |
|
| 132 |
+
logger.info("Database initialized successfully")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
|
| 134 |
+
def save_health_check(self, result: HealthCheckResult):
|
| 135 |
+
"""Save a single health check result"""
|
| 136 |
+
with self.get_connection() as conn:
|
| 137 |
+
cursor = conn.cursor()
|
| 138 |
+
cursor.execute("""
|
| 139 |
+
INSERT INTO status_log
|
| 140 |
+
(provider_name, category, status, response_time, status_code,
|
| 141 |
+
error_message, endpoint_tested, timestamp)
|
| 142 |
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
| 143 |
+
""", (
|
| 144 |
+
result.provider_name,
|
| 145 |
+
result.category,
|
| 146 |
+
result.status.value,
|
| 147 |
+
result.response_time,
|
| 148 |
+
result.status_code,
|
| 149 |
+
result.error_message,
|
| 150 |
+
result.endpoint_tested,
|
| 151 |
+
result.timestamp
|
| 152 |
+
))
|
| 153 |
+
|
| 154 |
+
def save_health_checks(self, results: List[HealthCheckResult]):
|
| 155 |
+
"""Save multiple health check results"""
|
| 156 |
+
with self.get_connection() as conn:
|
| 157 |
+
cursor = conn.cursor()
|
| 158 |
+
cursor.executemany("""
|
| 159 |
+
INSERT INTO status_log
|
| 160 |
+
(provider_name, category, status, response_time, status_code,
|
| 161 |
+
error_message, endpoint_tested, timestamp)
|
| 162 |
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
| 163 |
+
""", [
|
| 164 |
+
(r.provider_name, r.category, r.status.value, r.response_time,
|
| 165 |
+
r.status_code, r.error_message, r.endpoint_tested, r.timestamp)
|
| 166 |
+
for r in results
|
| 167 |
+
])
|
| 168 |
+
logger.info(f"Saved {len(results)} health check results")
|
| 169 |
+
|
| 170 |
+
def get_recent_status(
|
| 171 |
+
self,
|
| 172 |
+
provider_name: Optional[str] = None,
|
| 173 |
+
hours: int = 24,
|
| 174 |
+
limit: int = 1000
|
| 175 |
+
) -> List[Dict]:
|
| 176 |
+
"""Get recent status logs"""
|
| 177 |
+
with self.get_connection() as conn:
|
| 178 |
+
cursor = conn.cursor()
|
| 179 |
|
| 180 |
+
cutoff_time = datetime.now() - timedelta(hours=hours)
|
|
|
|
| 181 |
|
| 182 |
+
if provider_name:
|
| 183 |
+
query = """
|
| 184 |
+
SELECT * FROM status_log
|
| 185 |
+
WHERE provider_name = ? AND created_at >= ?
|
| 186 |
+
ORDER BY timestamp DESC
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
LIMIT ?
|
| 188 |
+
"""
|
| 189 |
+
cursor.execute(query, (provider_name, cutoff_time, limit))
|
| 190 |
+
else:
|
| 191 |
+
query = """
|
| 192 |
+
SELECT * FROM status_log
|
| 193 |
+
WHERE created_at >= ?
|
| 194 |
+
ORDER BY timestamp DESC
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
LIMIT ?
|
| 196 |
+
"""
|
| 197 |
+
cursor.execute(query, (cutoff_time, limit))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
|
| 199 |
+
return [dict(row) for row in cursor.fetchall()]
|
| 200 |
|
| 201 |
+
def get_uptime_percentage(
|
| 202 |
+
self,
|
| 203 |
+
provider_name: str,
|
| 204 |
+
hours: int = 24
|
| 205 |
+
) -> float:
|
| 206 |
+
"""Calculate uptime percentage from database"""
|
| 207 |
+
with self.get_connection() as conn:
|
| 208 |
+
cursor = conn.cursor()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
|
| 210 |
+
cutoff_time = datetime.now() - timedelta(hours=hours)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
|
| 212 |
+
cursor.execute("""
|
| 213 |
+
SELECT
|
| 214 |
+
COUNT(*) as total,
|
| 215 |
+
SUM(CASE WHEN status = 'online' THEN 1 ELSE 0 END) as online
|
| 216 |
+
FROM status_log
|
| 217 |
+
WHERE provider_name = ? AND created_at >= ?
|
| 218 |
+
""", (provider_name, cutoff_time))
|
| 219 |
+
|
| 220 |
+
row = cursor.fetchone()
|
| 221 |
+
if row['total'] > 0:
|
| 222 |
+
return round((row['online'] / row['total']) * 100, 2)
|
| 223 |
+
return 0.0
|
| 224 |
+
|
| 225 |
+
def get_avg_response_time(
|
| 226 |
+
self,
|
| 227 |
+
provider_name: str,
|
| 228 |
+
hours: int = 24
|
| 229 |
+
) -> float:
|
| 230 |
+
"""Get average response time from database"""
|
| 231 |
+
with self.get_connection() as conn:
|
| 232 |
+
cursor = conn.cursor()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
|
| 234 |
+
cutoff_time = datetime.now() - timedelta(hours=hours)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
|
| 236 |
+
cursor.execute("""
|
| 237 |
+
SELECT AVG(response_time) as avg_time
|
| 238 |
+
FROM status_log
|
| 239 |
+
WHERE provider_name = ?
|
| 240 |
+
AND created_at >= ?
|
| 241 |
+
AND response_time IS NOT NULL
|
| 242 |
+
""", (provider_name, cutoff_time))
|
| 243 |
+
|
| 244 |
+
row = cursor.fetchone()
|
| 245 |
+
return round(row['avg_time'], 2) if row['avg_time'] else 0.0
|
| 246 |
+
|
| 247 |
+
def create_incident(
|
| 248 |
+
self,
|
| 249 |
+
provider_name: str,
|
| 250 |
+
category: str,
|
| 251 |
+
incident_type: str,
|
| 252 |
+
description: str,
|
| 253 |
+
severity: str = "medium"
|
| 254 |
+
) -> int:
|
| 255 |
+
"""Create a new incident"""
|
| 256 |
+
with self.get_connection() as conn:
|
| 257 |
+
cursor = conn.cursor()
|
| 258 |
+
cursor.execute("""
|
| 259 |
+
INSERT INTO incidents
|
| 260 |
+
(provider_name, category, incident_type, description, severity, start_time)
|
| 261 |
+
VALUES (?, ?, ?, ?, ?, ?)
|
| 262 |
+
""", (provider_name, category, incident_type, description, severity, datetime.now()))
|
| 263 |
+
return cursor.lastrowid
|
| 264 |
+
|
| 265 |
+
def resolve_incident(self, incident_id: int):
|
| 266 |
+
"""Resolve an incident"""
|
| 267 |
+
with self.get_connection() as conn:
|
| 268 |
+
cursor = conn.cursor()
|
| 269 |
|
| 270 |
+
# Get start time
|
| 271 |
+
cursor.execute("SELECT start_time FROM incidents WHERE id = ?", (incident_id,))
|
| 272 |
+
row = cursor.fetchone()
|
| 273 |
+
if not row:
|
| 274 |
+
return
|
| 275 |
|
| 276 |
+
start_time = datetime.fromisoformat(row['start_time'])
|
| 277 |
+
end_time = datetime.now()
|
| 278 |
+
duration = int((end_time - start_time).total_seconds())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 279 |
|
| 280 |
+
cursor.execute("""
|
| 281 |
+
UPDATE incidents
|
| 282 |
+
SET end_time = ?, duration_seconds = ?, resolved = 1
|
| 283 |
+
WHERE id = ?
|
| 284 |
+
""", (end_time, duration, incident_id))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 285 |
|
| 286 |
+
def get_active_incidents(self) -> List[Dict]:
|
| 287 |
+
"""Get all active incidents"""
|
| 288 |
+
with self.get_connection() as conn:
|
| 289 |
+
cursor = conn.cursor()
|
| 290 |
+
cursor.execute("""
|
| 291 |
+
SELECT * FROM incidents
|
| 292 |
+
WHERE resolved = 0
|
| 293 |
+
ORDER BY start_time DESC
|
| 294 |
+
""")
|
| 295 |
+
return [dict(row) for row in cursor.fetchall()]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
|
| 297 |
+
def get_incident_history(self, hours: int = 24, limit: int = 100) -> List[Dict]:
|
| 298 |
+
"""Get incident history"""
|
| 299 |
+
with self.get_connection() as conn:
|
| 300 |
+
cursor = conn.cursor()
|
| 301 |
|
| 302 |
+
cutoff_time = datetime.now() - timedelta(hours=hours)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
|
| 304 |
+
cursor.execute("""
|
| 305 |
+
SELECT * FROM incidents
|
| 306 |
+
WHERE start_time >= ?
|
| 307 |
+
ORDER BY start_time DESC
|
| 308 |
+
LIMIT ?
|
| 309 |
+
""", (cutoff_time, limit))
|
| 310 |
+
|
| 311 |
+
return [dict(row) for row in cursor.fetchall()]
|
| 312 |
+
|
| 313 |
+
def create_alert(
|
| 314 |
+
self,
|
| 315 |
+
provider_name: str,
|
| 316 |
+
alert_type: str,
|
| 317 |
+
message: str,
|
| 318 |
+
threshold_value: Optional[float] = None,
|
| 319 |
+
actual_value: Optional[float] = None
|
| 320 |
+
) -> int:
|
| 321 |
+
"""Create a new alert"""
|
| 322 |
+
with self.get_connection() as conn:
|
| 323 |
+
cursor = conn.cursor()
|
| 324 |
+
cursor.execute("""
|
| 325 |
+
INSERT INTO alerts
|
| 326 |
+
(provider_name, alert_type, message, threshold_value, actual_value)
|
| 327 |
+
VALUES (?, ?, ?, ?, ?)
|
| 328 |
+
""", (provider_name, alert_type, message, threshold_value, actual_value))
|
| 329 |
+
return cursor.lastrowid
|
| 330 |
+
|
| 331 |
+
def get_unacknowledged_alerts(self) -> List[Dict]:
|
| 332 |
+
"""Get all unacknowledged alerts"""
|
| 333 |
+
with self.get_connection() as conn:
|
| 334 |
+
cursor = conn.cursor()
|
| 335 |
+
cursor.execute("""
|
| 336 |
+
SELECT * FROM alerts
|
| 337 |
+
WHERE acknowledged = 0
|
| 338 |
+
ORDER BY triggered_at DESC
|
| 339 |
+
""")
|
| 340 |
+
return [dict(row) for row in cursor.fetchall()]
|
| 341 |
|
| 342 |
+
def acknowledge_alert(self, alert_id: int):
|
| 343 |
+
"""Acknowledge an alert"""
|
| 344 |
+
with self.get_connection() as conn:
|
| 345 |
+
cursor = conn.cursor()
|
| 346 |
+
cursor.execute("""
|
| 347 |
+
UPDATE alerts
|
| 348 |
+
SET acknowledged = 1
|
| 349 |
+
WHERE id = ?
|
| 350 |
+
""", (alert_id,))
|
| 351 |
|
| 352 |
+
def aggregate_response_times(self, period_hours: int = 1):
|
| 353 |
+
"""Aggregate response times for the period"""
|
| 354 |
+
with self.get_connection() as conn:
|
| 355 |
+
cursor = conn.cursor()
|
| 356 |
|
| 357 |
+
period_start = datetime.now() - timedelta(hours=period_hours)
|
|
|
|
|
|
|
| 358 |
|
| 359 |
+
cursor.execute("""
|
| 360 |
+
INSERT INTO response_times
|
| 361 |
+
(provider_name, avg_response_time, min_response_time, max_response_time,
|
| 362 |
+
sample_count, period_start, period_end)
|
| 363 |
+
SELECT
|
| 364 |
+
provider_name,
|
| 365 |
+
AVG(response_time) as avg_time,
|
| 366 |
+
MIN(response_time) as min_time,
|
| 367 |
+
MAX(response_time) as max_time,
|
| 368 |
+
COUNT(*) as count,
|
| 369 |
+
? as period_start,
|
| 370 |
+
? as period_end
|
| 371 |
+
FROM status_log
|
| 372 |
+
WHERE created_at >= ? AND response_time IS NOT NULL
|
| 373 |
+
GROUP BY provider_name
|
| 374 |
+
""", (period_start, datetime.now(), period_start))
|
| 375 |
+
|
| 376 |
+
logger.info(f"Aggregated response times for period: {period_start}")
|
| 377 |
+
|
| 378 |
+
def cleanup_old_data(self, days: int = 7):
|
| 379 |
+
"""Clean up data older than specified days"""
|
| 380 |
+
with self.get_connection() as conn:
|
| 381 |
+
cursor = conn.cursor()
|
| 382 |
|
| 383 |
+
cutoff_date = datetime.now() - timedelta(days=days)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 384 |
|
| 385 |
+
# Delete old status logs
|
| 386 |
+
cursor.execute("""
|
| 387 |
+
DELETE FROM status_log
|
| 388 |
+
WHERE created_at < ?
|
| 389 |
+
""", (cutoff_date,))
|
| 390 |
+
deleted_logs = cursor.rowcount
|
| 391 |
|
| 392 |
+
# Delete old resolved incidents
|
| 393 |
+
cursor.execute("""
|
| 394 |
+
DELETE FROM incidents
|
| 395 |
+
WHERE resolved = 1 AND end_time < ?
|
| 396 |
+
""", (cutoff_date,))
|
| 397 |
+
deleted_incidents = cursor.rowcount
|
| 398 |
|
| 399 |
+
# Delete old acknowledged alerts
|
| 400 |
+
cursor.execute("""
|
| 401 |
+
DELETE FROM alerts
|
| 402 |
+
WHERE acknowledged = 1 AND triggered_at < ?
|
| 403 |
+
""", (cutoff_date,))
|
| 404 |
+
deleted_alerts = cursor.rowcount
|
| 405 |
+
|
| 406 |
+
logger.info(
|
| 407 |
+
f"Cleanup: {deleted_logs} logs, {deleted_incidents} incidents, "
|
| 408 |
+
f"{deleted_alerts} alerts older than {days} days"
|
| 409 |
+
)
|
| 410 |
|
| 411 |
+
def get_provider_stats(self, provider_name: str, hours: int = 24) -> Dict:
|
| 412 |
+
"""Get comprehensive stats for a provider"""
|
| 413 |
+
with self.get_connection() as conn:
|
| 414 |
+
cursor = conn.cursor()
|
| 415 |
|
| 416 |
+
cutoff_time = datetime.now() - timedelta(hours=hours)
|
|
|
|
|
|
|
| 417 |
|
| 418 |
+
# Get status distribution
|
| 419 |
+
cursor.execute("""
|
| 420 |
+
SELECT
|
| 421 |
+
status,
|
| 422 |
+
COUNT(*) as count
|
| 423 |
+
FROM status_log
|
| 424 |
+
WHERE provider_name = ? AND created_at >= ?
|
| 425 |
+
GROUP BY status
|
| 426 |
+
""", (provider_name, cutoff_time))
|
| 427 |
|
| 428 |
+
status_dist = {row['status']: row['count'] for row in cursor.fetchall()}
|
|
|
|
|
|
|
|
|
|
| 429 |
|
| 430 |
+
# Get response time stats
|
| 431 |
+
cursor.execute("""
|
| 432 |
+
SELECT
|
| 433 |
+
AVG(response_time) as avg_time,
|
| 434 |
+
MIN(response_time) as min_time,
|
| 435 |
+
MAX(response_time) as max_time,
|
| 436 |
+
COUNT(*) as total_checks
|
| 437 |
+
FROM status_log
|
| 438 |
+
WHERE provider_name = ?
|
| 439 |
+
AND created_at >= ?
|
| 440 |
+
AND response_time IS NOT NULL
|
| 441 |
+
""", (provider_name, cutoff_time))
|
| 442 |
+
|
| 443 |
+
row = cursor.fetchone()
|
| 444 |
+
|
| 445 |
+
return {
|
| 446 |
+
'provider_name': provider_name,
|
| 447 |
+
'period_hours': hours,
|
| 448 |
+
'status_distribution': status_dist,
|
| 449 |
+
'avg_response_time': round(row['avg_time'], 2) if row['avg_time'] else 0,
|
| 450 |
+
'min_response_time': round(row['min_time'], 2) if row['min_time'] else 0,
|
| 451 |
+
'max_response_time': round(row['max_time'], 2) if row['max_time'] else 0,
|
| 452 |
+
'total_checks': row['total_checks'] or 0,
|
| 453 |
+
'uptime_percentage': self.get_uptime_percentage(provider_name, hours)
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
def export_to_csv(self, output_path: str, hours: int = 24):
|
| 457 |
+
"""Export recent data to CSV"""
|
| 458 |
+
import csv
|
| 459 |
|
| 460 |
+
with self.get_connection() as conn:
|
| 461 |
+
cursor = conn.cursor()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 462 |
|
| 463 |
+
cutoff_time = datetime.now() - timedelta(hours=hours)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 464 |
|
| 465 |
+
cursor.execute("""
|
| 466 |
+
SELECT * FROM status_log
|
| 467 |
+
WHERE created_at >= ?
|
| 468 |
+
ORDER BY timestamp DESC
|
| 469 |
+
""", (cutoff_time,))
|
| 470 |
|
| 471 |
+
rows = cursor.fetchall()
|
|
|
|
| 472 |
|
| 473 |
+
if rows:
|
| 474 |
+
with open(output_path, 'w', newline='') as csvfile:
|
| 475 |
+
writer = csv.DictWriter(csvfile, fieldnames=rows[0].keys())
|
| 476 |
+
writer.writeheader()
|
| 477 |
+
for row in rows:
|
| 478 |
+
writer.writerow(dict(row))
|
| 479 |
|
| 480 |
+
logger.info(f"Exported {len(rows)} rows to {output_path}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
database/__init__.py
CHANGED
|
@@ -1,47 +1,8 @@
|
|
| 1 |
-
"""Database
|
| 2 |
|
| 3 |
-
|
| 4 |
-
legacy SQLite-backed ``Database`` class that the existing application modules
|
| 5 |
-
still import via ``from database import Database``. During the transition phase
|
| 6 |
-
we dynamically load the legacy implementation from the root ``database.py``
|
| 7 |
-
module (renamed here as ``legacy_database`` when importing) and fall back to the
|
| 8 |
-
new manager if that module is unavailable.
|
| 9 |
-
"""
|
| 10 |
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
from typing import Optional as _Optional
|
| 14 |
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
def _load_legacy_database() -> _Optional[type]:
|
| 18 |
-
"""Load the legacy Database class from the root-level ``database.py`` if it exists."""
|
| 19 |
-
legacy_path = _Path(__file__).resolve().parent.parent / "database.py"
|
| 20 |
-
if not legacy_path.exists():
|
| 21 |
-
return None
|
| 22 |
-
|
| 23 |
-
spec = _importlib_util.spec_from_file_location("legacy_database", legacy_path)
|
| 24 |
-
if spec is None or spec.loader is None:
|
| 25 |
-
return None
|
| 26 |
-
|
| 27 |
-
module = _importlib_util.module_from_spec(spec)
|
| 28 |
-
try:
|
| 29 |
-
spec.loader.exec_module(module)
|
| 30 |
-
except Exception:
|
| 31 |
-
# If loading the legacy module fails we silently fall back to DatabaseManager
|
| 32 |
-
return None
|
| 33 |
-
|
| 34 |
-
return getattr(module, "Database", None)
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
_LegacyDatabase = _load_legacy_database()
|
| 38 |
-
|
| 39 |
-
if _LegacyDatabase is not None:
|
| 40 |
-
Database = _LegacyDatabase
|
| 41 |
-
else:
|
| 42 |
-
Database = DatabaseManager
|
| 43 |
-
|
| 44 |
-
__all__ = ["DatabaseManager", "Database", "CryptoDatabase"]
|
| 45 |
-
|
| 46 |
-
# Backward-compatible alias for older imports
|
| 47 |
-
CryptoDatabase = Database
|
|
|
|
| 1 |
+
"""Database module for crypto API monitoring"""
|
| 2 |
|
| 3 |
+
from database.db_manager import DatabaseManager
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
+
# For backward compatibility
|
| 6 |
+
Database = DatabaseManager
|
|
|
|
| 7 |
|
| 8 |
+
__all__ = ['DatabaseManager', 'Database']
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
docker-compose.yml
CHANGED
|
@@ -1,102 +1,92 @@
|
|
| 1 |
version: '3.8'
|
| 2 |
|
| 3 |
services:
|
| 4 |
-
# سرور اصلی Crypto Monitor
|
| 5 |
crypto-monitor:
|
| 6 |
-
build:
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
ports:
|
| 9 |
-
- "
|
|
|
|
|
|
|
| 10 |
environment:
|
| 11 |
-
-
|
| 12 |
-
-
|
| 13 |
-
-
|
| 14 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
volumes:
|
| 16 |
-
|
| 17 |
- ./data:/app/data
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
healthcheck:
|
| 22 |
-
test: ["CMD", "
|
| 23 |
interval: 30s
|
| 24 |
timeout: 10s
|
|
|
|
| 25 |
retries: 3
|
| 26 |
-
start_period: 10s
|
| 27 |
|
| 28 |
-
|
| 29 |
-
redis:
|
| 30 |
-
image: redis:7-alpine
|
| 31 |
-
container_name: crypto-monitor-redis
|
| 32 |
-
profiles: ["observability"]
|
| 33 |
-
ports:
|
| 34 |
-
- "6379:6379"
|
| 35 |
-
volumes:
|
| 36 |
-
- redis-data:/data
|
| 37 |
restart: unless-stopped
|
| 38 |
-
networks:
|
| 39 |
-
- crypto-network
|
| 40 |
-
command: redis-server --appendonly yes
|
| 41 |
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
ports:
|
| 52 |
-
- "5432:5432"
|
| 53 |
-
volumes:
|
| 54 |
-
- postgres-data:/var/lib/postgresql/data
|
| 55 |
-
restart: unless-stopped
|
| 56 |
-
networks:
|
| 57 |
-
- crypto-network
|
| 58 |
|
| 59 |
-
|
| 60 |
-
prometheus:
|
| 61 |
-
image: prom/prometheus:latest
|
| 62 |
-
container_name: crypto-monitor-prometheus
|
| 63 |
-
profiles: ["observability"]
|
| 64 |
-
ports:
|
| 65 |
-
- "9090:9090"
|
| 66 |
-
volumes:
|
| 67 |
-
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
| 68 |
-
- prometheus-data:/prometheus
|
| 69 |
-
command:
|
| 70 |
-
- '--config.file=/etc/prometheus/prometheus.yml'
|
| 71 |
-
- '--storage.tsdb.path=/prometheus'
|
| 72 |
-
restart: unless-stopped
|
| 73 |
networks:
|
| 74 |
- crypto-network
|
| 75 |
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
- "3000:3000"
|
| 83 |
-
environment:
|
| 84 |
-
- GF_SECURITY_ADMIN_PASSWORD=admin_change_me
|
| 85 |
-
- GF_USERS_ALLOW_SIGN_UP=false
|
| 86 |
-
volumes:
|
| 87 |
-
- grafana-data:/var/lib/grafana
|
| 88 |
-
restart: unless-stopped
|
| 89 |
-
networks:
|
| 90 |
-
- crypto-network
|
| 91 |
-
depends_on:
|
| 92 |
-
- prometheus
|
| 93 |
|
|
|
|
| 94 |
networks:
|
| 95 |
crypto-network:
|
| 96 |
driver: bridge
|
|
|
|
| 97 |
|
|
|
|
| 98 |
volumes:
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
|
|
|
| 1 |
version: '3.8'
|
| 2 |
|
| 3 |
services:
|
|
|
|
| 4 |
crypto-monitor:
|
| 5 |
+
build:
|
| 6 |
+
context: .
|
| 7 |
+
dockerfile: Dockerfile
|
| 8 |
+
container_name: crypto-api-monitor
|
| 9 |
+
image: crypto-api-monitor:latest
|
| 10 |
+
|
| 11 |
+
# Port mapping (HuggingFace Spaces standard port)
|
| 12 |
ports:
|
| 13 |
+
- "7860:7860"
|
| 14 |
+
|
| 15 |
+
# Environment variables
|
| 16 |
environment:
|
| 17 |
+
- PYTHONUNBUFFERED=1
|
| 18 |
+
- ENABLE_SENTIMENT=true
|
| 19 |
+
- HF_REGISTRY_REFRESH_SEC=21600
|
| 20 |
+
- HF_HTTP_TIMEOUT=8.0
|
| 21 |
+
# Add your HuggingFace token here or via .env file
|
| 22 |
+
# - HUGGINGFACE_TOKEN=your_token_here
|
| 23 |
+
|
| 24 |
+
# Sentiment models (optional customization)
|
| 25 |
+
- SENTIMENT_SOCIAL_MODEL=ElKulako/cryptobert
|
| 26 |
+
- SENTIMENT_NEWS_MODEL=kk08/CryptoBERT
|
| 27 |
+
|
| 28 |
+
# Optional: Load environment variables from .env file
|
| 29 |
+
env_file:
|
| 30 |
+
- .env
|
| 31 |
+
|
| 32 |
+
# Volume mounts for data persistence
|
| 33 |
volumes:
|
| 34 |
+
# Persist SQLite database
|
| 35 |
- ./data:/app/data
|
| 36 |
+
|
| 37 |
+
# Persist logs
|
| 38 |
+
- ./logs:/app/logs
|
| 39 |
+
|
| 40 |
+
# Optional: Mount config for live updates during development
|
| 41 |
+
# - ./config.py:/app/config.py
|
| 42 |
+
# - ./all_apis_merged_2025.json:/app/all_apis_merged_2025.json
|
| 43 |
+
|
| 44 |
+
# Optional: Mount frontend files for live updates
|
| 45 |
+
# - ./index.html:/app/index.html
|
| 46 |
+
# - ./hf_console.html:/app/hf_console.html
|
| 47 |
+
# - ./config.js:/app/config.js
|
| 48 |
+
|
| 49 |
+
# Health check configuration
|
| 50 |
healthcheck:
|
| 51 |
+
test: ["CMD", "curl", "-f", "http://localhost:7860/health"]
|
| 52 |
interval: 30s
|
| 53 |
timeout: 10s
|
| 54 |
+
start_period: 40s
|
| 55 |
retries: 3
|
|
|
|
| 56 |
|
| 57 |
+
# Restart policy
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
restart: unless-stopped
|
|
|
|
|
|
|
|
|
|
| 59 |
|
| 60 |
+
# Resource limits (adjust based on your system)
|
| 61 |
+
deploy:
|
| 62 |
+
resources:
|
| 63 |
+
limits:
|
| 64 |
+
cpus: '2.0'
|
| 65 |
+
memory: 4G
|
| 66 |
+
reservations:
|
| 67 |
+
cpus: '0.5'
|
| 68 |
+
memory: 1G
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
|
| 70 |
+
# Network configuration
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
networks:
|
| 72 |
- crypto-network
|
| 73 |
|
| 74 |
+
# Logging configuration
|
| 75 |
+
logging:
|
| 76 |
+
driver: "json-file"
|
| 77 |
+
options:
|
| 78 |
+
max-size: "10m"
|
| 79 |
+
max-file: "3"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
|
| 81 |
+
# Network definition
|
| 82 |
networks:
|
| 83 |
crypto-network:
|
| 84 |
driver: bridge
|
| 85 |
+
name: crypto-monitor-network
|
| 86 |
|
| 87 |
+
# Volume definitions (optional - for named volumes instead of bind mounts)
|
| 88 |
volumes:
|
| 89 |
+
crypto-data:
|
| 90 |
+
name: crypto-monitor-data
|
| 91 |
+
crypto-logs:
|
| 92 |
+
name: crypto-monitor-logs
|
index.html
CHANGED
|
@@ -1,1216 +1,896 @@
|
|
| 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</title>
|
| 7 |
-
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
| 8 |
-
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
| 9 |
-
<style>
|
| 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 |
-
background: var(--
|
| 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 |
-
|
| 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 |
-
background:
|
| 288 |
-
|
| 289 |
-
border-
|
| 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 |
-
<div class="
|
| 531 |
-
<
|
| 532 |
-
|
| 533 |
-
<
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
<div class="
|
| 547 |
-
<div class="
|
| 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 |
-
<div class="
|
| 603 |
-
<div class="
|
| 604 |
-
<div class="
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
<
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
<div
|
| 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 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
|
| 768 |
-
|
| 769 |
-
|
| 770 |
-
|
| 771 |
-
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
|
| 778 |
-
|
| 779 |
-
|
| 780 |
-
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
|
| 787 |
-
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
|
| 795 |
-
|
| 796 |
-
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
|
| 803 |
-
|
| 804 |
-
|
| 805 |
-
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
|
| 809 |
-
|
| 810 |
-
|
| 811 |
-
|
| 812 |
-
|
| 813 |
-
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
|
| 817 |
-
|
| 818 |
-
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
|
| 826 |
-
|
| 827 |
-
|
| 828 |
-
|
| 829 |
-
|
| 830 |
-
|
| 831 |
-
|
| 832 |
-
|
| 833 |
-
|
| 834 |
-
|
| 835 |
-
}
|
| 836 |
-
|
| 837 |
-
function
|
| 838 |
-
|
| 839 |
-
|
| 840 |
-
|
| 841 |
-
|
| 842 |
-
|
| 843 |
-
|
| 844 |
-
|
| 845 |
-
|
| 846 |
-
|
| 847 |
-
|
| 848 |
-
|
| 849 |
-
|
| 850 |
-
|
| 851 |
-
|
| 852 |
-
|
| 853 |
-
|
| 854 |
-
|
| 855 |
-
|
| 856 |
-
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
|
| 860 |
-
|
| 861 |
-
|
| 862 |
-
|
| 863 |
-
|
| 864 |
-
|
| 865 |
-
|
| 866 |
-
|
| 867 |
-
|
| 868 |
-
|
| 869 |
-
|
| 870 |
-
|
| 871 |
-
|
| 872 |
-
|
| 873 |
-
|
| 874 |
-
|
| 875 |
-
|
| 876 |
-
|
| 877 |
-
|
| 878 |
-
|
| 879 |
-
|
| 880 |
-
}
|
| 881 |
-
|
| 882 |
-
function
|
| 883 |
-
|
| 884 |
-
|
| 885 |
-
|
| 886 |
-
|
| 887 |
-
|
| 888 |
-
|
| 889 |
-
|
| 890 |
-
|
| 891 |
-
|
| 892 |
-
|
| 893 |
-
|
| 894 |
-
|
| 895 |
-
|
| 896 |
-
|
| 897 |
-
}
|
| 898 |
-
|
| 899 |
-
function renderMarketCards() {
|
| 900 |
-
const stats = state.market.global || {};
|
| 901 |
-
const container = document.getElementById('marketGlobalStats');
|
| 902 |
-
container.innerHTML = `
|
| 903 |
-
<div><strong>Total Market Cap:</strong> $${formatNumber(stats.total_market_cap)}</div>
|
| 904 |
-
<div><strong>Total Volume:</strong> $${formatNumber(stats.total_volume)}</div>
|
| 905 |
-
<div><strong>BTC Dominance:</strong> ${stats.btc_dominance ? stats.btc_dominance.toFixed(2) + '%' : '--'}</div>
|
| 906 |
-
<div><strong>ETH Dominance:</strong> ${stats.eth_dominance ? stats.eth_dominance.toFixed(2) + '%' : '--'}</div>
|
| 907 |
-
<div><strong>Active Cryptos:</strong> ${stats.active_cryptocurrencies || '--'}</div>
|
| 908 |
-
<div><strong>Markets:</strong> ${stats.markets || '--'}</div>
|
| 909 |
-
`;
|
| 910 |
-
}
|
| 911 |
-
|
| 912 |
-
function renderMarketTable() {
|
| 913 |
-
const tbody = document.getElementById('marketTableBody');
|
| 914 |
-
const coins = state.market.cryptocurrencies || [];
|
| 915 |
-
if (!coins.length) {
|
| 916 |
-
tbody.innerHTML = '<tr><td colspan="5" style="text-align:center; padding:40px; color:var(--muted);">Market data unavailable</td></tr>';
|
| 917 |
-
return;
|
| 918 |
-
}
|
| 919 |
-
tbody.innerHTML = coins.slice(0, 12).map(coin => `
|
| 920 |
-
<tr>
|
| 921 |
-
<td>${coin.rank || coin.market_cap_rank || '-'}</td>
|
| 922 |
-
<td>${coin.name} <span style="color:var(--muted);">${coin.symbol}</span></td>
|
| 923 |
-
<td>$${formatNumber(coin.price)}</td>
|
| 924 |
-
<td style="color:${coin.change_24h >= 0 ? '#16a34a' : '#dc2626'};">${coin.change_24h ? coin.change_24h.toFixed(2) : '0'}%</td>
|
| 925 |
-
<td>$${formatNumber(coin.market_cap)}</td>
|
| 926 |
-
</tr>
|
| 927 |
-
`).join('');
|
| 928 |
-
}
|
| 929 |
-
|
| 930 |
-
function updateMarketChart() {
|
| 931 |
-
if (!state.charts.market) return;
|
| 932 |
-
const coins = state.market.cryptocurrencies || [];
|
| 933 |
-
const top = coins.slice(0, 5);
|
| 934 |
-
state.charts.market.data.labels = top.map(c => c.name);
|
| 935 |
-
state.charts.market.data.datasets[0].data = top.map(c => c.market_cap ? (c.market_cap / 1e9).toFixed(2) : 0);
|
| 936 |
-
state.charts.market.update();
|
| 937 |
-
}
|
| 938 |
-
|
| 939 |
-
async function loadTrending() {
|
| 940 |
-
const data = await apiCall('/api/trending');
|
| 941 |
-
state.trending = data.trending || [];
|
| 942 |
-
const list = document.getElementById('trendingList');
|
| 943 |
-
if (!state.trending.length) {
|
| 944 |
-
list.innerHTML = '<div class="list-item" style="color:var(--muted);">No trending assets</div>';
|
| 945 |
-
return;
|
| 946 |
-
}
|
| 947 |
-
list.innerHTML = state.trending.map(item => `
|
| 948 |
-
<div class="list-item">
|
| 949 |
-
<div style="font-weight:600;">${item.name} (${item.symbol})</div>
|
| 950 |
-
<div style="font-size:12px;color:var(--muted);">Rank: ${item.rank || '—'}</div>
|
| 951 |
-
</div>`).join('');
|
| 952 |
-
}
|
| 953 |
-
|
| 954 |
-
async function loadSentimentData() {
|
| 955 |
-
const data = await apiCall('/api/sentiment');
|
| 956 |
-
state.sentiment = data.fear_greed_index;
|
| 957 |
-
renderSentiment();
|
| 958 |
-
}
|
| 959 |
-
|
| 960 |
-
function renderSentiment() {
|
| 961 |
-
const container = document.getElementById('sentimentCard');
|
| 962 |
-
if (!state.sentiment) {
|
| 963 |
-
container.textContent = 'No sentiment data';
|
| 964 |
-
return;
|
| 965 |
-
}
|
| 966 |
-
const timestamp = Number(state.sentiment.timestamp);
|
| 967 |
-
container.innerHTML = `
|
| 968 |
-
<div style="font-size:32px; font-weight:700;">${state.sentiment.value}</div>
|
| 969 |
-
<div style="font-size:14px; text-transform:uppercase; letter-spacing:1px; margin-bottom:8px;">${state.sentiment.classification}</div>
|
| 970 |
-
<div style="font-size:12px; color:var(--muted);">Updated: ${timestamp ? new Date(timestamp * 1000).toLocaleString() : '--'}</div>
|
| 971 |
-
`;
|
| 972 |
-
}
|
| 973 |
-
|
| 974 |
-
async function loadDefi() {
|
| 975 |
-
const data = await apiCall('/api/defi');
|
| 976 |
-
state.defi = data.protocols || [];
|
| 977 |
-
const tbody = document.getElementById('defiTableBody');
|
| 978 |
-
if (!state.defi.length) {
|
| 979 |
-
tbody.innerHTML = '<tr><td colspan="4" style="text-align:center; padding:40px; color:var(--muted);">No DeFi data</td></tr>';
|
| 980 |
-
return;
|
| 981 |
-
}
|
| 982 |
-
tbody.innerHTML = state.defi.slice(0, 10).map(proto => `
|
| 983 |
-
<tr>
|
| 984 |
-
<td>${proto.name}</td>
|
| 985 |
-
<td>$${formatNumber(proto.tvl)}</td>
|
| 986 |
-
<td style="color:${proto.change_24h >= 0 ? '#16a34a' : '#dc2626'};">${proto.change_24h ? proto.change_24h.toFixed(2) : 0}%</td>
|
| 987 |
-
<td>${proto.chain || '—'}</td>
|
| 988 |
-
</tr>`).join('');
|
| 989 |
-
}
|
| 990 |
-
|
| 991 |
-
async function loadNews() {
|
| 992 |
-
const data = await apiCall('/api/news');
|
| 993 |
-
state.news = data.articles || [];
|
| 994 |
-
const list = document.getElementById('newsList');
|
| 995 |
-
if (!state.news.length) {
|
| 996 |
-
list.innerHTML = '<div class="list-item" style="color:var(--muted);">No news available</div>';
|
| 997 |
-
return;
|
| 998 |
-
}
|
| 999 |
-
list.innerHTML = state.news.slice(0, 12).map(article => `
|
| 1000 |
-
<div class="news-item list-item">
|
| 1001 |
-
<h4>${article.title}</h4>
|
| 1002 |
-
<div class="news-meta">${article.source || 'Unknown'} • ${article.published_at ? new Date(article.published_at).toLocaleString() : ''}</div>
|
| 1003 |
-
<p style="margin:6px 0;">${article.description || ''}</p>
|
| 1004 |
-
<a href="${article.link}" target="_blank" style="font-size:12px; color:var(--primary);">Read article →</a>
|
| 1005 |
-
</div>`).join('');
|
| 1006 |
-
}
|
| 1007 |
-
|
| 1008 |
-
async function loadErrorSummary() {
|
| 1009 |
-
try {
|
| 1010 |
-
const [summary, diagnostics] = await Promise.all([
|
| 1011 |
-
apiCall('/api/logs/summary'),
|
| 1012 |
-
apiCall('/api/diagnostics/errors')
|
| 1013 |
-
]);
|
| 1014 |
-
state.errorSummary = summary;
|
| 1015 |
-
state.diagnostics = diagnostics;
|
| 1016 |
-
renderErrorSummary();
|
| 1017 |
-
} catch (err) {
|
| 1018 |
-
console.error(err);
|
| 1019 |
-
document.getElementById('errorSummaryCard').textContent = 'Failed to load diagnostics';
|
| 1020 |
-
}
|
| 1021 |
-
}
|
| 1022 |
-
|
| 1023 |
-
function renderErrorSummary() {
|
| 1024 |
-
const summary = state.errorSummary;
|
| 1025 |
-
const card = document.getElementById('errorSummaryCard');
|
| 1026 |
-
if (!summary) {
|
| 1027 |
-
card.textContent = 'No diagnostics available';
|
| 1028 |
-
return;
|
| 1029 |
-
}
|
| 1030 |
-
card.innerHTML = `
|
| 1031 |
-
<div><strong>Total Logs:</strong> ${summary.total}</div>
|
| 1032 |
-
<div><strong>Last Error:</strong> ${summary.last_error ? summary.last_error.provider + ' @ ' + summary.last_error.timestamp : 'None'}</div>
|
| 1033 |
-
<div><strong>Top Offenders:</strong> ${Object.keys(summary.by_provider || {}).slice(0,3).join(', ') || '—'}</div>
|
| 1034 |
-
`;
|
| 1035 |
-
const diag = state.diagnostics || { recent: [] };
|
| 1036 |
-
const list = document.getElementById('diagnosticsList');
|
| 1037 |
-
list.innerHTML = diag.recent.slice(0,5).map(item => `
|
| 1038 |
-
<div class="list-item">
|
| 1039 |
-
<div style="font-weight:600;">${item.provider}</div>
|
| 1040 |
-
<div style="font-size:12px; color:var(--muted);">${item.timestamp}</div>
|
| 1041 |
-
<div style="font-size:13px; color:${item.status === 'offline' ? '#dc2626' : '#d97706'};">${item.message || 'No message'}</div>
|
| 1042 |
-
</div>`).join('');
|
| 1043 |
-
}
|
| 1044 |
-
|
| 1045 |
-
async function loadResourceSearch() {
|
| 1046 |
-
const query = document.getElementById('resourceSearch')?.value || '';
|
| 1047 |
-
const source = document.getElementById('resourceFilter').value;
|
| 1048 |
-
const data = await apiCall(`/api/resources/search?q=${encodeURIComponent(query)}&source=${source}`);
|
| 1049 |
-
state.resources = data.results;
|
| 1050 |
-
document.getElementById('resourceCountProviders').textContent = `(${data.counts.providers})`;
|
| 1051 |
-
document.getElementById('resourceCountModels').textContent = `(${data.counts.models})`;
|
| 1052 |
-
document.getElementById('resourceCountDatasets').textContent = `(${data.counts.datasets})`;
|
| 1053 |
-
renderResourceResults();
|
| 1054 |
-
}
|
| 1055 |
-
|
| 1056 |
-
function renderResourceResults() {
|
| 1057 |
-
const providersContainer = document.getElementById('resourceResultsProviders');
|
| 1058 |
-
const modelsContainer = document.getElementById('resourceResultsModels');
|
| 1059 |
-
const datasetsContainer = document.getElementById('resourceResultsDatasets');
|
| 1060 |
-
|
| 1061 |
-
providersContainer.innerHTML = state.resources.providers.slice(0,6).map(p => `
|
| 1062 |
-
<div class="resource-item">
|
| 1063 |
-
<strong>${p.name}</strong>
|
| 1064 |
-
<div style="font-size:12px;color:var(--muted);">${p.category}</div>
|
| 1065 |
-
<div style="font-size:12px;">Status: ${p.status}</div>
|
| 1066 |
-
</div>`).join('') || '<div class="resource-item" style="color:var(--muted);">No matches</div>';
|
| 1067 |
-
|
| 1068 |
-
modelsContainer.innerHTML = state.resources.models.slice(0,6).map(m => `
|
| 1069 |
-
<div class="resource-item">
|
| 1070 |
-
<strong>${m.id}</strong>
|
| 1071 |
-
<div style="font-size:12px;color:var(--muted);">${m.description || 'No description'}</div>
|
| 1072 |
-
</div>`).join('') || '<div class="resource-item" style="color:var(--muted);">No matches</div>';
|
| 1073 |
-
|
| 1074 |
-
datasetsContainer.innerHTML = state.resources.datasets.slice(0,6).map(d => `
|
| 1075 |
-
<div class="resource-item">
|
| 1076 |
-
<strong>${d.id}</strong>
|
| 1077 |
-
<div style="font-size:12px;color:var(--muted);">${d.description || 'No description'}</div>
|
| 1078 |
-
</div>`).join('') || '<div class="resource-item" style="color:var(--muted);">No matches</div>';
|
| 1079 |
-
}
|
| 1080 |
-
|
| 1081 |
-
async function handleExport(type) {
|
| 1082 |
-
try {
|
| 1083 |
-
const res = await apiCall(`/api/v2/export/${type}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' });
|
| 1084 |
-
state.exports.unshift({ type, url: res.download_url, timestamp: res.timestamp });
|
| 1085 |
-
renderExportHistory();
|
| 1086 |
-
showToast(`${type.toUpperCase()} export ready`, 'success');
|
| 1087 |
-
} catch (err) {
|
| 1088 |
-
console.error(err);
|
| 1089 |
-
showToast('Export failed', 'error');
|
| 1090 |
-
}
|
| 1091 |
-
}
|
| 1092 |
-
|
| 1093 |
-
async function handleBackup() {
|
| 1094 |
-
try {
|
| 1095 |
-
const res = await apiCall('/api/v2/backup', { method: 'POST' });
|
| 1096 |
-
state.exports.unshift({ type: 'backup', url: res.download_url, timestamp: res.timestamp });
|
| 1097 |
-
renderExportHistory();
|
| 1098 |
-
showToast('Backup created', 'success');
|
| 1099 |
-
} catch (err) {
|
| 1100 |
-
console.error(err);
|
| 1101 |
-
showToast('Backup failed', 'error');
|
| 1102 |
-
}
|
| 1103 |
-
}
|
| 1104 |
-
|
| 1105 |
-
function renderExportHistory() {
|
| 1106 |
-
const container = document.getElementById('exportHistory');
|
| 1107 |
-
if (!state.exports.length) {
|
| 1108 |
-
container.innerHTML = '<div style="color:var(--muted); font-size:13px;">No exports yet</div>';
|
| 1109 |
-
return;
|
| 1110 |
-
}
|
| 1111 |
-
container.innerHTML = state.exports.slice(0,5).map(entry => `
|
| 1112 |
-
<div class="list-item" style="border-bottom:1px solid var(--border);">
|
| 1113 |
-
<div style="font-weight:600;">${entry.type.toUpperCase()}</div>
|
| 1114 |
-
<div style="font-size:12px; color:var(--muted);">${new Date(entry.timestamp).toLocaleString()}</div>
|
| 1115 |
-
<a href="${entry.url}" style="font-size:12px; color:var(--primary);" target="_blank">Download</a>
|
| 1116 |
-
</div>`).join('');
|
| 1117 |
-
}
|
| 1118 |
-
|
| 1119 |
-
async function handleImportSingle(event) {
|
| 1120 |
-
event.preventDefault();
|
| 1121 |
-
const form = event.target;
|
| 1122 |
-
const payload = Object.fromEntries(new FormData(form).entries());
|
| 1123 |
-
payload.requires_key = form.elements['requires_key'].checked;
|
| 1124 |
-
payload.timeout_ms = Number(payload.timeout_ms) || 10000;
|
| 1125 |
-
try {
|
| 1126 |
-
await apiCall('/api/providers', {
|
| 1127 |
-
method: 'POST',
|
| 1128 |
-
headers: { 'Content-Type': 'application/json' },
|
| 1129 |
-
body: JSON.stringify(payload)
|
| 1130 |
-
});
|
| 1131 |
-
showToast('Provider imported', 'success');
|
| 1132 |
-
form.reset();
|
| 1133 |
-
loadProviders();
|
| 1134 |
-
} catch (err) {
|
| 1135 |
-
console.error(err);
|
| 1136 |
-
showToast('Import failed', 'error');
|
| 1137 |
-
}
|
| 1138 |
-
}
|
| 1139 |
-
|
| 1140 |
-
async function handleImportBulk() {
|
| 1141 |
-
const textarea = document.getElementById('bulkImportTextarea');
|
| 1142 |
-
if (!textarea.value.trim()) {
|
| 1143 |
-
showToast('Paste provider JSON first', 'error');
|
| 1144 |
-
return;
|
| 1145 |
-
}
|
| 1146 |
-
try {
|
| 1147 |
-
const providers = JSON.parse(textarea.value);
|
| 1148 |
-
await apiCall('/api/v2/import/providers', {
|
| 1149 |
-
method: 'POST',
|
| 1150 |
-
headers: { 'Content-Type': 'application/json' },
|
| 1151 |
-
body: JSON.stringify({ providers })
|
| 1152 |
-
});
|
| 1153 |
-
showToast('Bulk import complete', 'success');
|
| 1154 |
-
textarea.value = '';
|
| 1155 |
-
loadProviders();
|
| 1156 |
-
} catch (err) {
|
| 1157 |
-
console.error(err);
|
| 1158 |
-
showToast('Bulk import failed', 'error');
|
| 1159 |
-
}
|
| 1160 |
-
}
|
| 1161 |
-
|
| 1162 |
-
function switchTab(event, tabName) {
|
| 1163 |
-
document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active'));
|
| 1164 |
-
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
| 1165 |
-
event.currentTarget.classList.add('active');
|
| 1166 |
-
document.getElementById(`tab-${tabName}`).classList.add('active');
|
| 1167 |
-
state.currentTab = tabName;
|
| 1168 |
-
if (tabName === 'market') loadMarket();
|
| 1169 |
-
if (tabName === 'sentiment') { loadSentimentData(); loadDefi(); }
|
| 1170 |
-
if (tabName === 'news') loadNews();
|
| 1171 |
-
}
|
| 1172 |
-
|
| 1173 |
-
function startAutoRefresh() {
|
| 1174 |
-
setInterval(() => {
|
| 1175 |
-
if (state.wsConnected) {
|
| 1176 |
-
refreshAll();
|
| 1177 |
-
}
|
| 1178 |
-
}, config.autoRefreshInterval);
|
| 1179 |
-
}
|
| 1180 |
-
|
| 1181 |
-
function refreshAll() {
|
| 1182 |
-
loadProviders();
|
| 1183 |
-
loadMarket();
|
| 1184 |
-
loadTrending();
|
| 1185 |
-
loadSentimentData();
|
| 1186 |
-
loadDefi();
|
| 1187 |
-
loadErrorSummary();
|
| 1188 |
-
loadNews();
|
| 1189 |
-
loadResourceSearch();
|
| 1190 |
-
}
|
| 1191 |
-
|
| 1192 |
-
function formatNumber(value) {
|
| 1193 |
-
if (!value && value !== 0) return '--';
|
| 1194 |
-
if (value >= 1e12) return (value / 1e12).toFixed(2) + 'T';
|
| 1195 |
-
if (value >= 1e9) return (value / 1e9).toFixed(2) + 'B';
|
| 1196 |
-
if (value >= 1e6) return (value / 1e6).toFixed(2) + 'M';
|
| 1197 |
-
if (value >= 1e3) return (value / 1e3).toFixed(2) + 'K';
|
| 1198 |
-
return Number(value).toFixed(2);
|
| 1199 |
-
}
|
| 1200 |
-
|
| 1201 |
-
function setupResourceSearch() {
|
| 1202 |
-
const input = document.getElementById('resourceSearch');
|
| 1203 |
-
input.addEventListener('input', () => {
|
| 1204 |
-
clearTimeout(state.resourceSearchTimeout);
|
| 1205 |
-
state.resourceSearchTimeout = setTimeout(loadResourceSearch, 400);
|
| 1206 |
-
});
|
| 1207 |
-
}
|
| 1208 |
-
|
| 1209 |
-
initializeWebSocket();
|
| 1210 |
-
setupResourceSearch();
|
| 1211 |
-
loadInitialData();
|
| 1212 |
-
startAutoRefresh();
|
| 1213 |
-
</script>
|
| 1214 |
-
</body>
|
| 1215 |
-
</html>
|
| 1216 |
-
|
|
|
|
| 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 Pro - Real-time Dashboard</title>
|
| 7 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
|
| 8 |
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
| 9 |
+
<style>
|
| 10 |
+
* {
|
| 11 |
+
margin: 0;
|
| 12 |
+
padding: 0;
|
| 13 |
+
box-sizing: border-box;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
:root {
|
| 17 |
+
--bg-dark: #0a0a0f;
|
| 18 |
+
--bg-card: #12121a;
|
| 19 |
+
--bg-card-hover: #1a1a24;
|
| 20 |
+
--text-primary: #ffffff;
|
| 21 |
+
--text-secondary: #a0a0b0;
|
| 22 |
+
--accent-blue: #3b82f6;
|
| 23 |
+
--accent-purple: #8b5cf6;
|
| 24 |
+
--accent-pink: #ec4899;
|
| 25 |
+
--accent-green: #10b981;
|
| 26 |
+
--accent-red: #ef4444;
|
| 27 |
+
--accent-yellow: #f59e0b;
|
| 28 |
+
--border: rgba(255, 255, 255, 0.1);
|
| 29 |
+
--shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
body {
|
| 33 |
+
font-family: 'Inter', sans-serif;
|
| 34 |
+
background: var(--bg-dark);
|
| 35 |
+
color: var(--text-primary);
|
| 36 |
+
line-height: 1.6;
|
| 37 |
+
overflow-x: hidden;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
body::before {
|
| 41 |
+
content: '';
|
| 42 |
+
position: fixed;
|
| 43 |
+
top: 0;
|
| 44 |
+
left: 0;
|
| 45 |
+
right: 0;
|
| 46 |
+
bottom: 0;
|
| 47 |
+
background:
|
| 48 |
+
radial-gradient(circle at 20% 20%, rgba(59, 130, 246, 0.1) 0%, transparent 50%),
|
| 49 |
+
radial-gradient(circle at 80% 80%, rgba(139, 92, 246, 0.1) 0%, transparent 50%),
|
| 50 |
+
radial-gradient(circle at 50% 50%, rgba(236, 72, 153, 0.05) 0%, transparent 50%);
|
| 51 |
+
pointer-events: none;
|
| 52 |
+
z-index: 0;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
.container {
|
| 56 |
+
max-width: 1800px;
|
| 57 |
+
margin: 0 auto;
|
| 58 |
+
padding: 20px;
|
| 59 |
+
position: relative;
|
| 60 |
+
z-index: 1;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
/* Header */
|
| 64 |
+
.header {
|
| 65 |
+
background: linear-gradient(135deg, var(--bg-card) 0%, rgba(59, 130, 246, 0.1) 100%);
|
| 66 |
+
border: 1px solid var(--border);
|
| 67 |
+
border-radius: 24px;
|
| 68 |
+
padding: 30px;
|
| 69 |
+
margin-bottom: 30px;
|
| 70 |
+
backdrop-filter: blur(20px);
|
| 71 |
+
box-shadow: var(--shadow);
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
.header-top {
|
| 75 |
+
display: flex;
|
| 76 |
+
align-items: center;
|
| 77 |
+
justify-content: space-between;
|
| 78 |
+
flex-wrap: wrap;
|
| 79 |
+
gap: 20px;
|
| 80 |
+
margin-bottom: 25px;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
.logo {
|
| 84 |
+
display: flex;
|
| 85 |
+
align-items: center;
|
| 86 |
+
gap: 15px;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
.logo-icon {
|
| 90 |
+
width: 50px;
|
| 91 |
+
height: 50px;
|
| 92 |
+
background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple));
|
| 93 |
+
border-radius: 12px;
|
| 94 |
+
display: flex;
|
| 95 |
+
align-items: center;
|
| 96 |
+
justify-content: center;
|
| 97 |
+
font-size: 24px;
|
| 98 |
+
box-shadow: 0 10px 30px rgba(59, 130, 246, 0.3);
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.logo-text h1 {
|
| 102 |
+
font-size: 28px;
|
| 103 |
+
font-weight: 800;
|
| 104 |
+
background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple), var(--accent-pink));
|
| 105 |
+
-webkit-background-clip: text;
|
| 106 |
+
-webkit-text-fill-color: transparent;
|
| 107 |
+
background-clip: text;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
.logo-text p {
|
| 111 |
+
font-size: 13px;
|
| 112 |
+
color: var(--text-secondary);
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
.header-actions {
|
| 116 |
+
display: flex;
|
| 117 |
+
gap: 15px;
|
| 118 |
+
align-items: center;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
.status-badge {
|
| 122 |
+
display: flex;
|
| 123 |
+
align-items: center;
|
| 124 |
+
gap: 8px;
|
| 125 |
+
padding: 10px 20px;
|
| 126 |
+
background: rgba(16, 185, 129, 0.1);
|
| 127 |
+
border: 1px solid rgba(16, 185, 129, 0.3);
|
| 128 |
+
border-radius: 12px;
|
| 129 |
+
font-size: 13px;
|
| 130 |
+
font-weight: 600;
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
.status-dot {
|
| 134 |
+
width: 8px;
|
| 135 |
+
height: 8px;
|
| 136 |
+
background: var(--accent-green);
|
| 137 |
+
border-radius: 50%;
|
| 138 |
+
animation: pulse 2s infinite;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
@keyframes pulse {
|
| 142 |
+
0%, 100% { opacity: 1; transform: scale(1); }
|
| 143 |
+
50% { opacity: 0.5; transform: scale(1.2); }
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
/* Stats Grid */
|
| 147 |
+
.stats-grid {
|
| 148 |
+
display: grid;
|
| 149 |
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
| 150 |
+
gap: 20px;
|
| 151 |
+
margin-bottom: 30px;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
.stat-card {
|
| 155 |
+
background: var(--bg-card);
|
| 156 |
+
border: 1px solid var(--border);
|
| 157 |
+
border-radius: 20px;
|
| 158 |
+
padding: 25px;
|
| 159 |
+
transition: all 0.3s ease;
|
| 160 |
+
position: relative;
|
| 161 |
+
overflow: hidden;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
.stat-card::before {
|
| 165 |
+
content: '';
|
| 166 |
+
position: absolute;
|
| 167 |
+
top: 0;
|
| 168 |
+
left: 0;
|
| 169 |
+
right: 0;
|
| 170 |
+
height: 4px;
|
| 171 |
+
background: linear-gradient(90deg, var(--accent-blue), var(--accent-purple));
|
| 172 |
+
transform: scaleX(0);
|
| 173 |
+
transition: transform 0.3s ease;
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
.stat-card:hover {
|
| 177 |
+
transform: translateY(-5px);
|
| 178 |
+
border-color: rgba(59, 130, 246, 0.5);
|
| 179 |
+
box-shadow: 0 15px 40px rgba(59, 130, 246, 0.2);
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
.stat-card:hover::before {
|
| 183 |
+
transform: scaleX(1);
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
.stat-header {
|
| 187 |
+
display: flex;
|
| 188 |
+
align-items: center;
|
| 189 |
+
justify-content: space-between;
|
| 190 |
+
margin-bottom: 15px;
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
.stat-icon {
|
| 194 |
+
width: 40px;
|
| 195 |
+
height: 40px;
|
| 196 |
+
border-radius: 10px;
|
| 197 |
+
display: flex;
|
| 198 |
+
align-items: center;
|
| 199 |
+
justify-content: center;
|
| 200 |
+
font-size: 20px;
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
.stat-value {
|
| 204 |
+
font-size: 32px;
|
| 205 |
+
font-weight: 800;
|
| 206 |
+
margin-bottom: 5px;
|
| 207 |
+
background: linear-gradient(135deg, var(--text-primary), var(--text-secondary));
|
| 208 |
+
-webkit-background-clip: text;
|
| 209 |
+
-webkit-text-fill-color: transparent;
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
.stat-label {
|
| 213 |
+
font-size: 13px;
|
| 214 |
+
color: var(--text-secondary);
|
| 215 |
+
text-transform: uppercase;
|
| 216 |
+
letter-spacing: 1px;
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
.stat-change {
|
| 220 |
+
font-size: 13px;
|
| 221 |
+
font-weight: 600;
|
| 222 |
+
margin-top: 10px;
|
| 223 |
+
display: flex;
|
| 224 |
+
align-items: center;
|
| 225 |
+
gap: 5px;
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
.stat-change.positive { color: var(--accent-green); }
|
| 229 |
+
.stat-change.negative { color: var(--accent-red); }
|
| 230 |
+
|
| 231 |
+
/* Tabs */
|
| 232 |
+
.tabs {
|
| 233 |
+
display: flex;
|
| 234 |
+
gap: 10px;
|
| 235 |
+
margin-bottom: 25px;
|
| 236 |
+
padding: 8px;
|
| 237 |
+
background: var(--bg-card);
|
| 238 |
+
border-radius: 16px;
|
| 239 |
+
border: 1px solid var(--border);
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
.tab {
|
| 243 |
+
padding: 12px 24px;
|
| 244 |
+
background: transparent;
|
| 245 |
+
border: none;
|
| 246 |
+
border-radius: 12px;
|
| 247 |
+
color: var(--text-secondary);
|
| 248 |
+
font-weight: 600;
|
| 249 |
+
cursor: pointer;
|
| 250 |
+
transition: all 0.3s ease;
|
| 251 |
+
font-size: 14px;
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
.tab:hover {
|
| 255 |
+
color: var(--text-primary);
|
| 256 |
+
background: rgba(59, 130, 246, 0.1);
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
.tab.active {
|
| 260 |
+
background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple));
|
| 261 |
+
color: white;
|
| 262 |
+
box-shadow: 0 5px 20px rgba(59, 130, 246, 0.3);
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
.tab-content {
|
| 266 |
+
display: none;
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
.tab-content.active {
|
| 270 |
+
display: block;
|
| 271 |
+
animation: fadeIn 0.3s ease;
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
@keyframes fadeIn {
|
| 275 |
+
from { opacity: 0; transform: translateY(10px); }
|
| 276 |
+
to { opacity: 1; transform: translateY(0); }
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
/* Provider Grid */
|
| 280 |
+
.provider-grid {
|
| 281 |
+
display: grid;
|
| 282 |
+
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
| 283 |
+
gap: 20px;
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
.provider-card {
|
| 287 |
+
background: var(--bg-card);
|
| 288 |
+
border: 1px solid var(--border);
|
| 289 |
+
border-radius: 16px;
|
| 290 |
+
padding: 20px;
|
| 291 |
+
transition: all 0.3s ease;
|
| 292 |
+
cursor: pointer;
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
.provider-card:hover {
|
| 296 |
+
transform: translateY(-3px);
|
| 297 |
+
border-color: rgba(59, 130, 246, 0.5);
|
| 298 |
+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
.provider-header {
|
| 302 |
+
display: flex;
|
| 303 |
+
align-items: center;
|
| 304 |
+
justify-content: space-between;
|
| 305 |
+
margin-bottom: 15px;
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
.provider-name {
|
| 309 |
+
font-size: 18px;
|
| 310 |
+
font-weight: 700;
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
.provider-status {
|
| 314 |
+
padding: 4px 12px;
|
| 315 |
+
border-radius: 20px;
|
| 316 |
+
font-size: 11px;
|
| 317 |
+
font-weight: 600;
|
| 318 |
+
text-transform: uppercase;
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
.provider-status.operational {
|
| 322 |
+
background: rgba(16, 185, 129, 0.2);
|
| 323 |
+
color: var(--accent-green);
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
.provider-status.degraded {
|
| 327 |
+
background: rgba(245, 158, 11, 0.2);
|
| 328 |
+
color: var(--accent-yellow);
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
.provider-status.maintenance {
|
| 332 |
+
background: rgba(239, 68, 68, 0.2);
|
| 333 |
+
color: var(--accent-red);
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
.provider-meta {
|
| 337 |
+
display: flex;
|
| 338 |
+
gap: 15px;
|
| 339 |
+
font-size: 12px;
|
| 340 |
+
color: var(--text-secondary);
|
| 341 |
+
margin-bottom: 12px;
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
.provider-meta span {
|
| 345 |
+
display: flex;
|
| 346 |
+
align-items: center;
|
| 347 |
+
gap: 5px;
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
.provider-stats {
|
| 351 |
+
display: grid;
|
| 352 |
+
grid-template-columns: repeat(2, 1fr);
|
| 353 |
+
gap: 10px;
|
| 354 |
+
margin-top: 15px;
|
| 355 |
+
padding-top: 15px;
|
| 356 |
+
border-top: 1px solid var(--border);
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
.provider-stat {
|
| 360 |
+
font-size: 12px;
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
.provider-stat-value {
|
| 364 |
+
font-size: 16px;
|
| 365 |
+
font-weight: 700;
|
| 366 |
+
color: var(--text-primary);
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
/* Crypto Table */
|
| 370 |
+
.crypto-table {
|
| 371 |
+
background: var(--bg-card);
|
| 372 |
+
border: 1px solid var(--border);
|
| 373 |
+
border-radius: 16px;
|
| 374 |
+
overflow: hidden;
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
.table-wrapper {
|
| 378 |
+
overflow-x: auto;
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
table {
|
| 382 |
+
width: 100%;
|
| 383 |
+
border-collapse: collapse;
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
thead {
|
| 387 |
+
background: rgba(59, 130, 246, 0.05);
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
th {
|
| 391 |
+
padding: 15px;
|
| 392 |
+
text-align: left;
|
| 393 |
+
font-size: 12px;
|
| 394 |
+
font-weight: 600;
|
| 395 |
+
color: var(--text-secondary);
|
| 396 |
+
text-transform: uppercase;
|
| 397 |
+
letter-spacing: 1px;
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
td {
|
| 401 |
+
padding: 15px;
|
| 402 |
+
border-top: 1px solid var(--border);
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
tr:hover {
|
| 406 |
+
background: rgba(59, 130, 246, 0.05);
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
.crypto-name {
|
| 410 |
+
display: flex;
|
| 411 |
+
align-items: center;
|
| 412 |
+
gap: 10px;
|
| 413 |
+
}
|
| 414 |
+
|
| 415 |
+
.crypto-icon {
|
| 416 |
+
width: 32px;
|
| 417 |
+
height: 32px;
|
| 418 |
+
border-radius: 8px;
|
| 419 |
+
background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple));
|
| 420 |
+
display: flex;
|
| 421 |
+
align-items: center;
|
| 422 |
+
justify-content: center;
|
| 423 |
+
font-weight: 700;
|
| 424 |
+
font-size: 14px;
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
.price-change {
|
| 428 |
+
font-weight: 600;
|
| 429 |
+
padding: 4px 8px;
|
| 430 |
+
border-radius: 6px;
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
.price-change.positive {
|
| 434 |
+
background: rgba(16, 185, 129, 0.1);
|
| 435 |
+
color: var(--accent-green);
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
+
.price-change.negative {
|
| 439 |
+
background: rgba(239, 68, 68, 0.1);
|
| 440 |
+
color: var(--accent-red);
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
/* Charts */
|
| 444 |
+
.chart-container {
|
| 445 |
+
background: var(--bg-card);
|
| 446 |
+
border: 1px solid var(--border);
|
| 447 |
+
border-radius: 16px;
|
| 448 |
+
padding: 25px;
|
| 449 |
+
margin-bottom: 25px;
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
.chart-header {
|
| 453 |
+
display: flex;
|
| 454 |
+
justify-content: space-between;
|
| 455 |
+
align-items: center;
|
| 456 |
+
margin-bottom: 20px;
|
| 457 |
+
}
|
| 458 |
+
|
| 459 |
+
.chart-title {
|
| 460 |
+
font-size: 18px;
|
| 461 |
+
font-weight: 700;
|
| 462 |
+
}
|
| 463 |
+
|
| 464 |
+
canvas {
|
| 465 |
+
max-height: 300px;
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
/* Loading */
|
| 469 |
+
.loading {
|
| 470 |
+
display: flex;
|
| 471 |
+
align-items: center;
|
| 472 |
+
justify-content: center;
|
| 473 |
+
padding: 50px;
|
| 474 |
+
}
|
| 475 |
+
|
| 476 |
+
.spinner {
|
| 477 |
+
width: 40px;
|
| 478 |
+
height: 40px;
|
| 479 |
+
border: 3px solid var(--border);
|
| 480 |
+
border-top-color: var(--accent-blue);
|
| 481 |
+
border-radius: 50%;
|
| 482 |
+
animation: spin 1s linear infinite;
|
| 483 |
+
}
|
| 484 |
+
|
| 485 |
+
@keyframes spin {
|
| 486 |
+
to { transform: rotate(360deg); }
|
| 487 |
+
}
|
| 488 |
+
|
| 489 |
+
/* Responsive */
|
| 490 |
+
@media (max-width: 768px) {
|
| 491 |
+
.stats-grid {
|
| 492 |
+
grid-template-columns: 1fr;
|
| 493 |
+
}
|
| 494 |
+
|
| 495 |
+
.provider-grid {
|
| 496 |
+
grid-template-columns: 1fr;
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
.header-top {
|
| 500 |
+
flex-direction: column;
|
| 501 |
+
align-items: flex-start;
|
| 502 |
+
}
|
| 503 |
+
}
|
| 504 |
+
|
| 505 |
+
/* Scrollbar */
|
| 506 |
+
::-webkit-scrollbar {
|
| 507 |
+
width: 8px;
|
| 508 |
+
height: 8px;
|
| 509 |
+
}
|
| 510 |
+
|
| 511 |
+
::-webkit-scrollbar-track {
|
| 512 |
+
background: var(--bg-dark);
|
| 513 |
+
}
|
| 514 |
+
|
| 515 |
+
::-webkit-scrollbar-thumb {
|
| 516 |
+
background: var(--border);
|
| 517 |
+
border-radius: 4px;
|
| 518 |
+
}
|
| 519 |
+
|
| 520 |
+
::-webkit-scrollbar-thumb:hover {
|
| 521 |
+
background: rgba(255, 255, 255, 0.2);
|
| 522 |
+
}
|
| 523 |
+
</style>
|
| 524 |
+
</head>
|
| 525 |
+
<body>
|
| 526 |
+
<div class="container">
|
| 527 |
+
<!-- Header -->
|
| 528 |
+
<div class="header">
|
| 529 |
+
<div class="header-top">
|
| 530 |
+
<div class="logo">
|
| 531 |
+
<div class="logo-icon">₿</div>
|
| 532 |
+
<div class="logo-text">
|
| 533 |
+
<h1>Crypto API Monitor Pro</h1>
|
| 534 |
+
<p>Real-time monitoring of 40+ providers</p>
|
| 535 |
+
</div>
|
| 536 |
+
</div>
|
| 537 |
+
<div class="header-actions">
|
| 538 |
+
<div class="status-badge" id="systemStatus">
|
| 539 |
+
<div class="status-dot"></div>
|
| 540 |
+
<span id="statusText">System Operational</span>
|
| 541 |
+
</div>
|
| 542 |
+
</div>
|
| 543 |
+
</div>
|
| 544 |
+
|
| 545 |
+
<!-- Stats Grid -->
|
| 546 |
+
<div class="stats-grid">
|
| 547 |
+
<div class="stat-card">
|
| 548 |
+
<div class="stat-header">
|
| 549 |
+
<div class="stat-icon" style="background: linear-gradient(135deg, #3b82f6, #8b5cf6);">🌐</div>
|
| 550 |
+
</div>
|
| 551 |
+
<div class="stat-value" id="totalProviders">--</div>
|
| 552 |
+
<div class="stat-label">Total Providers</div>
|
| 553 |
+
<div class="stat-change positive" id="providersChange">
|
| 554 |
+
<span>↑</span> <span>100% Active</span>
|
| 555 |
+
</div>
|
| 556 |
+
</div>
|
| 557 |
+
|
| 558 |
+
<div class="stat-card">
|
| 559 |
+
<div class="stat-header">
|
| 560 |
+
<div class="stat-icon" style="background: linear-gradient(135deg, #10b981, #06b6d4);">⚡</div>
|
| 561 |
+
</div>
|
| 562 |
+
<div class="stat-value" id="avgResponseTime">--</div>
|
| 563 |
+
<div class="stat-label">Avg Response Time</div>
|
| 564 |
+
<div class="stat-change positive">
|
| 565 |
+
<span>↓</span> <span>12% faster</span>
|
| 566 |
+
</div>
|
| 567 |
+
</div>
|
| 568 |
+
|
| 569 |
+
<div class="stat-card">
|
| 570 |
+
<div class="stat-header">
|
| 571 |
+
<div class="stat-icon" style="background: linear-gradient(135deg, #ec4899, #f59e0b);">💰</div>
|
| 572 |
+
</div>
|
| 573 |
+
<div class="stat-value" id="marketCap">--</div>
|
| 574 |
+
<div class="stat-label">Total Market Cap</div>
|
| 575 |
+
<div class="stat-change positive" id="marketChange">
|
| 576 |
+
<span>↑</span> <span>5.2%</span>
|
| 577 |
+
</div>
|
| 578 |
+
</div>
|
| 579 |
+
|
| 580 |
+
<div class="stat-card">
|
| 581 |
+
<div class="stat-header">
|
| 582 |
+
<div class="stat-icon" style="background: linear-gradient(135deg, #8b5cf6, #ec4899);">📊</div>
|
| 583 |
+
</div>
|
| 584 |
+
<div class="stat-value" id="totalRequests">--</div>
|
| 585 |
+
<div class="stat-label">API Requests Today</div>
|
| 586 |
+
<div class="stat-change positive">
|
| 587 |
+
<span>↑</span> <span>15K/min</span>
|
| 588 |
+
</div>
|
| 589 |
+
</div>
|
| 590 |
+
</div>
|
| 591 |
+
</div>
|
| 592 |
+
|
| 593 |
+
<!-- Tabs -->
|
| 594 |
+
<div class="tabs">
|
| 595 |
+
<button class="tab active" onclick="switchTab('overview')">📊 Overview</button>
|
| 596 |
+
<button class="tab" onclick="switchTab('providers')">🌐 Providers</button>
|
| 597 |
+
<button class="tab" onclick="switchTab('crypto')">₿ Cryptocurrencies</button>
|
| 598 |
+
<button class="tab" onclick="switchTab('analytics')">📈 Analytics</button>
|
| 599 |
+
</div>
|
| 600 |
+
|
| 601 |
+
<!-- Overview Tab -->
|
| 602 |
+
<div class="tab-content active" id="overview">
|
| 603 |
+
<div class="chart-container">
|
| 604 |
+
<div class="chart-header">
|
| 605 |
+
<div class="chart-title">Provider Status Distribution</div>
|
| 606 |
+
</div>
|
| 607 |
+
<canvas id="statusChart"></canvas>
|
| 608 |
+
</div>
|
| 609 |
+
|
| 610 |
+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 25px;">
|
| 611 |
+
<div class="chart-container">
|
| 612 |
+
<div class="chart-header">
|
| 613 |
+
<div class="chart-title">Response Time Trends</div>
|
| 614 |
+
</div>
|
| 615 |
+
<canvas id="responseChart"></canvas>
|
| 616 |
+
</div>
|
| 617 |
+
|
| 618 |
+
<div class="chart-container">
|
| 619 |
+
<div class="chart-header">
|
| 620 |
+
<div class="chart-title">Requests by Category</div>
|
| 621 |
+
</div>
|
| 622 |
+
<canvas id="categoryChart"></canvas>
|
| 623 |
+
</div>
|
| 624 |
+
</div>
|
| 625 |
+
</div>
|
| 626 |
+
|
| 627 |
+
<!-- Providers Tab -->
|
| 628 |
+
<div class="tab-content" id="providers">
|
| 629 |
+
<div class="provider-grid" id="providerGrid">
|
| 630 |
+
<div class="loading"><div class="spinner"></div></div>
|
| 631 |
+
</div>
|
| 632 |
+
</div>
|
| 633 |
+
|
| 634 |
+
<!-- Crypto Tab -->
|
| 635 |
+
<div class="tab-content" id="crypto">
|
| 636 |
+
<div class="crypto-table">
|
| 637 |
+
<div class="table-wrapper">
|
| 638 |
+
<table>
|
| 639 |
+
<thead>
|
| 640 |
+
<tr>
|
| 641 |
+
<th>#</th>
|
| 642 |
+
<th>Name</th>
|
| 643 |
+
<th>Price</th>
|
| 644 |
+
<th>24h Change</th>
|
| 645 |
+
<th>7d Change</th>
|
| 646 |
+
<th>Market Cap</th>
|
| 647 |
+
<th>Volume 24h</th>
|
| 648 |
+
<th>Category</th>
|
| 649 |
+
</tr>
|
| 650 |
+
</thead>
|
| 651 |
+
<tbody id="cryptoTableBody">
|
| 652 |
+
<tr><td colspan="8" style="text-align: center;"><div class="loading"><div class="spinner"></div></div></td></tr>
|
| 653 |
+
</tbody>
|
| 654 |
+
</table>
|
| 655 |
+
</div>
|
| 656 |
+
</div>
|
| 657 |
+
</div>
|
| 658 |
+
|
| 659 |
+
<!-- Analytics Tab -->
|
| 660 |
+
<div class="tab-content" id="analytics">
|
| 661 |
+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 25px; margin-bottom: 25px;">
|
| 662 |
+
<div class="chart-container">
|
| 663 |
+
<div class="chart-header">
|
| 664 |
+
<div class="chart-title">Market Dominance</div>
|
| 665 |
+
</div>
|
| 666 |
+
<canvas id="dominanceChart"></canvas>
|
| 667 |
+
</div>
|
| 668 |
+
|
| 669 |
+
<div class="chart-container">
|
| 670 |
+
<div class="chart-header">
|
| 671 |
+
<div class="chart-title">Provider Performance</div>
|
| 672 |
+
</div>
|
| 673 |
+
<canvas id="performanceChart"></canvas>
|
| 674 |
+
</div>
|
| 675 |
+
</div>
|
| 676 |
+
</div>
|
| 677 |
+
</div>
|
| 678 |
+
|
| 679 |
+
<script>
|
| 680 |
+
let charts = {};
|
| 681 |
+
let wsConnection = null;
|
| 682 |
+
|
| 683 |
+
// Initialize
|
| 684 |
+
document.addEventListener('DOMContentLoaded', async () => {
|
| 685 |
+
await loadData();
|
| 686 |
+
initCharts();
|
| 687 |
+
connectWebSocket();
|
| 688 |
+
setInterval(loadData, 30000);
|
| 689 |
+
});
|
| 690 |
+
|
| 691 |
+
async function loadData() {
|
| 692 |
+
try {
|
| 693 |
+
const [stats, providers, crypto] = await Promise.all([
|
| 694 |
+
fetch('/api/stats').then(r => r.json()),
|
| 695 |
+
fetch('/api/providers').then(r => r.json()),
|
| 696 |
+
fetch('/api/crypto/prices').then(r => r.json())
|
| 697 |
+
]);
|
| 698 |
+
|
| 699 |
+
updateStats(stats);
|
| 700 |
+
updateProviders(providers);
|
| 701 |
+
updateCrypto(crypto);
|
| 702 |
+
updateCharts(providers, crypto);
|
| 703 |
+
} catch (error) {
|
| 704 |
+
console.error('Error loading data:', error);
|
| 705 |
+
}
|
| 706 |
+
}
|
| 707 |
+
|
| 708 |
+
function updateStats(stats) {
|
| 709 |
+
document.getElementById('totalProviders').textContent = stats.providers.total;
|
| 710 |
+
document.getElementById('avgResponseTime').textContent = stats.performance.avg_response_time + 'ms';
|
| 711 |
+
document.getElementById('marketCap').textContent = '$' + (stats.market.total_market_cap / 1e12).toFixed(2) + 'T';
|
| 712 |
+
document.getElementById('totalRequests').textContent = (stats.performance.total_requests / 1e6).toFixed(1) + 'M';
|
| 713 |
+
|
| 714 |
+
const change = stats.market.avg_change_24h;
|
| 715 |
+
const changeEl = document.getElementById('marketChange');
|
| 716 |
+
changeEl.className = 'stat-change ' + (change > 0 ? 'positive' : 'negative');
|
| 717 |
+
changeEl.innerHTML = `<span>${change > 0 ? '↑' : '↓'}</span> <span>${Math.abs(change).toFixed(1)}%</span>`;
|
| 718 |
+
}
|
| 719 |
+
|
| 720 |
+
function updateProviders(providers) {
|
| 721 |
+
const grid = document.getElementById('providerGrid');
|
| 722 |
+
grid.innerHTML = providers.map(p => `
|
| 723 |
+
<div class="provider-card">
|
| 724 |
+
<div class="provider-header">
|
| 725 |
+
<div class="provider-name">${p.name}</div>
|
| 726 |
+
<div class="provider-status ${p.status}">${p.status}</div>
|
| 727 |
+
</div>
|
| 728 |
+
<div class="provider-meta">
|
| 729 |
+
<span>📍 ${p.region}</span>
|
| 730 |
+
<span>🏷️ ${p.type}</span>
|
| 731 |
+
<span>📂 ${p.category}</span>
|
| 732 |
+
</div>
|
| 733 |
+
<div class="provider-stats">
|
| 734 |
+
<div class="provider-stat">
|
| 735 |
+
<div style="color: var(--text-secondary); font-size: 11px;">Uptime</div>
|
| 736 |
+
<div class="provider-stat-value">${p.uptime}%</div>
|
| 737 |
+
</div>
|
| 738 |
+
<div class="provider-stat">
|
| 739 |
+
<div style="color: var(--text-secondary); font-size: 11px;">Response</div>
|
| 740 |
+
<div class="provider-stat-value">${p.response_time_ms}ms</div>
|
| 741 |
+
</div>
|
| 742 |
+
<div class="provider-stat">
|
| 743 |
+
<div style="color: var(--text-secondary); font-size: 11px;">Requests</div>
|
| 744 |
+
<div class="provider-stat-value">${(p.requests_today / 1000).toFixed(0)}K</div>
|
| 745 |
+
</div>
|
| 746 |
+
<div class="provider-stat">
|
| 747 |
+
<div style="color: var(--text-secondary); font-size: 11px;">Rate</div>
|
| 748 |
+
<div class="provider-stat-value">${p.rate_limit}/min</div>
|
| 749 |
+
</div>
|
| 750 |
+
</div>
|
| 751 |
+
</div>
|
| 752 |
+
`).join('');
|
| 753 |
+
}
|
| 754 |
+
|
| 755 |
+
function updateCrypto(crypto) {
|
| 756 |
+
const tbody = document.getElementById('cryptoTableBody');
|
| 757 |
+
tbody.innerHTML = crypto.map(c => `
|
| 758 |
+
<tr>
|
| 759 |
+
<td>${c.rank}</td>
|
| 760 |
+
<td>
|
| 761 |
+
<div class="crypto-name">
|
| 762 |
+
<div class="crypto-icon">${c.symbol[0]}</div>
|
| 763 |
+
<div>
|
| 764 |
+
<div style="font-weight: 600;">${c.name}</div>
|
| 765 |
+
<div style="font-size: 12px; color: var(--text-secondary);">${c.symbol}</div>
|
| 766 |
+
</div>
|
| 767 |
+
</div>
|
| 768 |
+
</td>
|
| 769 |
+
<td style="font-weight: 600;">$${c.price.toLocaleString()}</td>
|
| 770 |
+
<td><span class="price-change ${c.change_24h > 0 ? 'positive' : 'negative'}">${c.change_24h > 0 ? '+' : ''}${c.change_24h.toFixed(2)}%</span></td>
|
| 771 |
+
<td><span class="price-change ${c.change_7d > 0 ? 'positive' : 'negative'}">${c.change_7d > 0 ? '+' : ''}${c.change_7d.toFixed(2)}%</span></td>
|
| 772 |
+
<td>$${(c.market_cap / 1e9).toFixed(2)}B</td>
|
| 773 |
+
<td>$${(c.volume_24h / 1e9).toFixed(2)}B</td>
|
| 774 |
+
<td><span style="padding: 4px 8px; background: rgba(59, 130, 246, 0.1); border-radius: 6px; font-size: 11px;">${c.category}</span></td>
|
| 775 |
+
</tr>
|
| 776 |
+
`).join('');
|
| 777 |
+
}
|
| 778 |
+
|
| 779 |
+
function initCharts() {
|
| 780 |
+
Chart.defaults.color = '#a0a0b0';
|
| 781 |
+
Chart.defaults.borderColor = 'rgba(255, 255, 255, 0.1)';
|
| 782 |
+
|
| 783 |
+
charts.status = new Chart(document.getElementById('statusChart'), {
|
| 784 |
+
type: 'doughnut',
|
| 785 |
+
data: {
|
| 786 |
+
labels: ['Operational', 'Degraded', 'Maintenance'],
|
| 787 |
+
datasets: [{
|
| 788 |
+
data: [85, 10, 5],
|
| 789 |
+
backgroundColor: ['#10b981', '#f59e0b', '#ef4444'],
|
| 790 |
+
borderWidth: 0
|
| 791 |
+
}]
|
| 792 |
+
},
|
| 793 |
+
options: {
|
| 794 |
+
responsive: true,
|
| 795 |
+
plugins: {
|
| 796 |
+
legend: { position: 'bottom' }
|
| 797 |
+
}
|
| 798 |
+
}
|
| 799 |
+
});
|
| 800 |
+
|
| 801 |
+
charts.response = new Chart(document.getElementById('responseChart'), {
|
| 802 |
+
type: 'line',
|
| 803 |
+
data: {
|
| 804 |
+
labels: Array(12).fill(0).map((_, i) => `${i*5}m`),
|
| 805 |
+
datasets: [{
|
| 806 |
+
label: 'Response Time',
|
| 807 |
+
data: Array(12).fill(0).map(() => Math.random() * 100 + 80),
|
| 808 |
+
borderColor: '#3b82f6',
|
| 809 |
+
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
| 810 |
+
fill: true,
|
| 811 |
+
tension: 0.4
|
| 812 |
+
}]
|
| 813 |
+
},
|
| 814 |
+
options: {
|
| 815 |
+
responsive: true,
|
| 816 |
+
plugins: { legend: { display: false } }
|
| 817 |
+
}
|
| 818 |
+
});
|
| 819 |
+
|
| 820 |
+
charts.category = new Chart(document.getElementById('categoryChart'), {
|
| 821 |
+
type: 'bar',
|
| 822 |
+
data: {
|
| 823 |
+
labels: ['Exchanges', 'Data', 'DeFi', 'NFT', 'Blockchain'],
|
| 824 |
+
datasets: [{
|
| 825 |
+
label: 'Providers',
|
| 826 |
+
data: [15, 8, 8, 4, 5],
|
| 827 |
+
backgroundColor: ['#3b82f6', '#8b5cf6', '#ec4899', '#f59e0b', '#10b981']
|
| 828 |
+
}]
|
| 829 |
+
},
|
| 830 |
+
options: {
|
| 831 |
+
responsive: true,
|
| 832 |
+
plugins: { legend: { display: false } }
|
| 833 |
+
}
|
| 834 |
+
});
|
| 835 |
+
}
|
| 836 |
+
|
| 837 |
+
function updateCharts(providers, crypto) {
|
| 838 |
+
if (charts.dominance) {
|
| 839 |
+
const top5 = crypto.slice(0, 5);
|
| 840 |
+
charts.dominance.data.labels = top5.map(c => c.symbol);
|
| 841 |
+
charts.dominance.data.datasets[0].data = top5.map(c => c.market_cap);
|
| 842 |
+
charts.dominance.update();
|
| 843 |
+
} else {
|
| 844 |
+
charts.dominance = new Chart(document.getElementById('dominanceChart'), {
|
| 845 |
+
type: 'pie',
|
| 846 |
+
data: {
|
| 847 |
+
labels: crypto.slice(0, 5).map(c => c.symbol),
|
| 848 |
+
datasets: [{
|
| 849 |
+
data: crypto.slice(0, 5).map(c => c.market_cap),
|
| 850 |
+
backgroundColor: ['#3b82f6', '#8b5cf6', '#ec4899', '#f59e0b', '#10b981']
|
| 851 |
+
}]
|
| 852 |
+
}
|
| 853 |
+
});
|
| 854 |
+
}
|
| 855 |
+
|
| 856 |
+
if (charts.performance) {
|
| 857 |
+
charts.performance.data.datasets[0].data = providers.slice(0, 8).map(p => p.uptime);
|
| 858 |
+
charts.performance.update();
|
| 859 |
+
} else {
|
| 860 |
+
charts.performance = new Chart(document.getElementById('performanceChart'), {
|
| 861 |
+
type: 'radar',
|
| 862 |
+
data: {
|
| 863 |
+
labels: providers.slice(0, 8).map(p => p.name),
|
| 864 |
+
datasets: [{
|
| 865 |
+
label: 'Uptime %',
|
| 866 |
+
data: providers.slice(0, 8).map(p => p.uptime),
|
| 867 |
+
borderColor: '#8b5cf6',
|
| 868 |
+
backgroundColor: 'rgba(139, 92, 246, 0.1)'
|
| 869 |
+
}]
|
| 870 |
+
}
|
| 871 |
+
});
|
| 872 |
+
}
|
| 873 |
+
}
|
| 874 |
+
|
| 875 |
+
function switchTab(tab) {
|
| 876 |
+
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
| 877 |
+
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
| 878 |
+
event.target.classList.add('active');
|
| 879 |
+
document.getElementById(tab).classList.add('active');
|
| 880 |
+
}
|
| 881 |
+
|
| 882 |
+
function connectWebSocket() {
|
| 883 |
+
const ws = new WebSocket(`ws://${window.location.host}/ws/live`);
|
| 884 |
+
ws.onopen = () => console.log('WebSocket connected');
|
| 885 |
+
ws.onmessage = (e) => {
|
| 886 |
+
const data = JSON.parse(e.data);
|
| 887 |
+
if (data.type === 'status_update') {
|
| 888 |
+
document.getElementById('statusText').textContent =
|
| 889 |
+
data.data.status === 'healthy' ? 'System Operational' : 'System Degraded';
|
| 890 |
+
}
|
| 891 |
+
};
|
| 892 |
+
ws.onerror = () => console.log('WebSocket error');
|
| 893 |
+
}
|
| 894 |
+
</script>
|
| 895 |
+
</body>
|
| 896 |
+
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
main.py
CHANGED
|
@@ -1,30 +1,168 @@
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI, HTTPException, Depends
|
| 2 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 3 |
+
import ccxt
|
| 4 |
+
import os
|
| 5 |
+
import logging
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
import pandas as pd
|
| 8 |
+
import numpy as np
|
| 9 |
|
| 10 |
+
# Setup logging
|
| 11 |
+
logging.basicConfig(level=logging.INFO)
|
| 12 |
+
logger = logging.getLogger(__name__)
|
| 13 |
+
|
| 14 |
+
# Create data and logs directories
|
| 15 |
+
os.makedirs("data", exist_ok=True)
|
| 16 |
+
os.makedirs("logs", exist_ok=True)
|
| 17 |
+
|
| 18 |
+
# Initialize FastAPI app
|
| 19 |
+
app = FastAPI(
|
| 20 |
+
title="Cryptocurrency Data Source API",
|
| 21 |
+
description="API for fetching cryptocurrency market data and technical indicators",
|
| 22 |
+
version="1.0.0",
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
# Configure CORS
|
| 26 |
+
app.add_middleware(
|
| 27 |
+
CORSMiddleware,
|
| 28 |
+
allow_origins=["*"],
|
| 29 |
+
allow_credentials=True,
|
| 30 |
+
allow_methods=["*"],
|
| 31 |
+
allow_headers=["*"],
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
@app.get("/")
|
| 35 |
+
async def root():
|
| 36 |
+
"""Root endpoint showing API status"""
|
| 37 |
+
return {
|
| 38 |
+
"status": "online",
|
| 39 |
+
"version": "1.0.0",
|
| 40 |
+
"timestamp": datetime.now().isoformat(),
|
| 41 |
+
"ccxt_version": ccxt.__version__
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
@app.get("/exchanges")
|
| 45 |
+
async def get_exchanges():
|
| 46 |
+
"""List available exchanges"""
|
| 47 |
+
# Get list of exchanges that support OHLCV data
|
| 48 |
+
exchanges = []
|
| 49 |
+
for exchange_id in ccxt.exchanges:
|
| 50 |
+
try:
|
| 51 |
+
exchange = getattr(ccxt, exchange_id)()
|
| 52 |
+
if exchange.has.get('fetchOHLCV'):
|
| 53 |
+
exchanges.append({
|
| 54 |
+
"id": exchange_id,
|
| 55 |
+
"name": exchange.name if hasattr(exchange, 'name') else exchange_id,
|
| 56 |
+
"url": exchange.urls.get('www') if hasattr(exchange, 'urls') and exchange.urls else None
|
| 57 |
+
})
|
| 58 |
+
except:
|
| 59 |
+
# Skip exchanges that fail to initialize
|
| 60 |
+
continue
|
| 61 |
|
| 62 |
+
return {"total": len(exchanges), "exchanges": exchanges}
|
| 63 |
+
|
| 64 |
+
@app.get("/markets/{exchange_id}")
|
| 65 |
+
async def get_markets(exchange_id: str):
|
| 66 |
+
"""Get markets for a specific exchange"""
|
| 67 |
+
try:
|
| 68 |
+
# Check if exchange exists
|
| 69 |
+
if exchange_id not in ccxt.exchanges:
|
| 70 |
+
raise HTTPException(status_code=404, detail=f"Exchange {exchange_id} not found")
|
| 71 |
+
|
| 72 |
+
# Initialize exchange
|
| 73 |
+
exchange = getattr(ccxt, exchange_id)({
|
| 74 |
+
'enableRateLimit': True
|
| 75 |
+
})
|
| 76 |
+
|
| 77 |
+
# Fetch markets
|
| 78 |
+
markets = exchange.load_markets()
|
| 79 |
+
|
| 80 |
+
# Format response
|
| 81 |
+
result = []
|
| 82 |
+
for symbol, market in markets.items():
|
| 83 |
+
if market.get('active', False):
|
| 84 |
+
result.append({
|
| 85 |
+
"symbol": symbol,
|
| 86 |
+
"base": market.get('base', ''),
|
| 87 |
+
"quote": market.get('quote', ''),
|
| 88 |
+
"type": market.get('type', 'spot')
|
| 89 |
+
})
|
| 90 |
+
|
| 91 |
+
return {"exchange": exchange_id, "total": len(result), "markets": result[:100]}
|
| 92 |
|
| 93 |
+
except ccxt.BaseError as e:
|
| 94 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 95 |
+
except Exception as e:
|
| 96 |
+
logger.error(f"Error fetching markets for {exchange_id}: {str(e)}")
|
| 97 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 98 |
+
|
| 99 |
+
@app.get("/ohlcv/{exchange_id}/{symbol}")
|
| 100 |
+
async def get_ohlcv(exchange_id: str, symbol: str, timeframe: str = "1h", limit: int = 100):
|
| 101 |
+
"""Get OHLCV data for a specific market"""
|
| 102 |
+
try:
|
| 103 |
+
# Check if exchange exists
|
| 104 |
+
if exchange_id not in ccxt.exchanges:
|
| 105 |
+
raise HTTPException(status_code=404, detail=f"Exchange {exchange_id} not found")
|
| 106 |
+
|
| 107 |
+
# Initialize exchange
|
| 108 |
+
exchange = getattr(ccxt, exchange_id)({
|
| 109 |
+
'enableRateLimit': True
|
| 110 |
+
})
|
| 111 |
+
|
| 112 |
+
# Check if exchange supports OHLCV
|
| 113 |
+
if not exchange.has.get('fetchOHLCV'):
|
| 114 |
+
raise HTTPException(
|
| 115 |
+
status_code=400,
|
| 116 |
+
detail=f"Exchange {exchange_id} does not support OHLCV data"
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
# Check timeframe
|
| 120 |
+
if timeframe not in exchange.timeframes:
|
| 121 |
+
raise HTTPException(
|
| 122 |
+
status_code=400,
|
| 123 |
+
detail=f"Timeframe {timeframe} not supported by {exchange_id}"
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
# Fetch OHLCV data
|
| 127 |
+
ohlcv = exchange.fetch_ohlcv(symbol=symbol, timeframe=timeframe, limit=limit)
|
| 128 |
+
|
| 129 |
+
# Convert to readable format
|
| 130 |
+
result = []
|
| 131 |
+
for candle in ohlcv:
|
| 132 |
+
timestamp, open_price, high, low, close, volume = candle
|
| 133 |
+
result.append({
|
| 134 |
+
"timestamp": timestamp,
|
| 135 |
+
"datetime": datetime.fromtimestamp(timestamp / 1000).isoformat(),
|
| 136 |
+
"open": open_price,
|
| 137 |
+
"high": high,
|
| 138 |
+
"low": low,
|
| 139 |
+
"close": close,
|
| 140 |
+
"volume": volume
|
| 141 |
+
})
|
| 142 |
+
|
| 143 |
+
return {
|
| 144 |
+
"exchange": exchange_id,
|
| 145 |
+
"symbol": symbol,
|
| 146 |
+
"timeframe": timeframe,
|
| 147 |
+
"data": result
|
| 148 |
+
}
|
| 149 |
|
| 150 |
+
except ccxt.BaseError as e:
|
| 151 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 152 |
+
except Exception as e:
|
| 153 |
+
logger.error(f"Error fetching OHLCV data: {str(e)}")
|
| 154 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 155 |
+
|
| 156 |
+
@app.get("/health")
|
| 157 |
+
async def health_check():
|
| 158 |
+
"""Health check endpoint"""
|
| 159 |
+
return {
|
| 160 |
+
"status": "healthy",
|
| 161 |
+
"timestamp": datetime.now().isoformat(),
|
| 162 |
+
"version": "1.0.0"
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
if __name__ == "__main__":
|
| 166 |
+
import uvicorn
|
| 167 |
+
port = int(os.getenv("PORT", 7860))
|
| 168 |
+
uvicorn.run("main:app", host="0.0.0.0", port=port, log_level="info")
|
requirements.txt
CHANGED
|
@@ -1,58 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
# ==================== GRADIO INTERFACE ====================
|
| 5 |
-
gradio>=4.44.0
|
| 6 |
-
|
| 7 |
-
# ==================== DATA PROCESSING ====================
|
| 8 |
-
pandas>=2.0.0
|
| 9 |
-
numpy>=1.24.0
|
| 10 |
-
|
| 11 |
-
# ==================== HTTP CLIENTS ====================
|
| 12 |
-
requests>=2.31.0
|
| 13 |
-
aiohttp>=3.8.0
|
| 14 |
-
httpx>=0.26.0
|
| 15 |
-
|
| 16 |
-
# ==================== WEB BACKEND ====================
|
| 17 |
-
fastapi>=0.109.0
|
| 18 |
-
uvicorn[standard]>=0.27.0
|
| 19 |
-
slowapi>=0.1.9
|
| 20 |
-
python-multipart>=0.0.6
|
| 21 |
-
websockets>=12.0
|
| 22 |
-
|
| 23 |
-
# ==================== DATA MODELS & CONFIG ====================
|
| 24 |
-
pydantic>=2.5.3
|
| 25 |
-
pydantic-settings>=2.1.0
|
| 26 |
-
|
| 27 |
-
# ==================== WEB SCRAPING & RSS ====================
|
| 28 |
-
beautifulsoup4>=4.12.0
|
| 29 |
-
feedparser>=6.0.10
|
| 30 |
-
|
| 31 |
-
# ==================== AI/ML - HUGGING FACE ====================
|
| 32 |
-
transformers>=4.30.0
|
| 33 |
-
torch>=2.0.0
|
| 34 |
-
sentencepiece>=0.1.99
|
| 35 |
-
tokenizers>=0.13.0
|
| 36 |
-
huggingface-hub>=0.16.0
|
| 37 |
-
|
| 38 |
-
# ==================== PLOTTING & VISUALIZATION ====================
|
| 39 |
-
plotly>=5.14.0
|
| 40 |
-
kaleido>=0.2.1
|
| 41 |
-
|
| 42 |
-
# ==================== DATABASE & STORAGE ====================
|
| 43 |
-
sqlalchemy>=2.0.25
|
| 44 |
-
|
| 45 |
-
# ==================== AUTHENTICATION & SECURITY ====================
|
| 46 |
-
PyJWT>=2.8.0
|
| 47 |
-
|
| 48 |
-
# ==================== DATE/TIME HELPERS ====================
|
| 49 |
-
python-dateutil>=2.8.2
|
| 50 |
-
|
| 51 |
-
# ==================== OPTIONAL: ACCELERATED INFERENCE ====================
|
| 52 |
-
# accelerate>=0.20.0 # Uncomment for faster model loading
|
| 53 |
-
# bitsandbytes>=0.39.0 # Uncomment for quantization
|
| 54 |
-
|
| 55 |
-
# ==================== NOTES ====================
|
| 56 |
-
# No API keys required - all data sources are free
|
| 57 |
-
# SQLite is included in Python standard library
|
| 58 |
-
# All packages are production-tested and stable
|
|
|
|
| 1 |
+
fastapi==0.104.1
|
| 2 |
+
uvicorn[standard]==0.24.0
|
| 3 |
+
websockets==12.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
start.bat
CHANGED
|
@@ -1,16 +1,17 @@
|
|
| 1 |
@echo off
|
| 2 |
chcp 65001 > nul
|
| 3 |
-
title Crypto Monitor
|
| 4 |
|
| 5 |
echo ========================================
|
| 6 |
-
echo 🚀 Crypto Monitor
|
| 7 |
-
echo
|
| 8 |
echo ========================================
|
| 9 |
echo.
|
| 10 |
|
| 11 |
python --version > nul 2>&1
|
| 12 |
if %errorlevel% neq 0 (
|
| 13 |
echo ❌ Python not found!
|
|
|
|
| 14 |
pause
|
| 15 |
exit /b 1
|
| 16 |
)
|
|
@@ -31,20 +32,14 @@ pip install -q -r requirements.txt
|
|
| 31 |
|
| 32 |
echo.
|
| 33 |
echo ========================================
|
| 34 |
-
echo 🎯 Starting
|
| 35 |
echo ========================================
|
| 36 |
echo.
|
| 37 |
echo 📊 Dashboard: http://localhost:8000/dashboard
|
| 38 |
echo 📡 API Docs: http://localhost:8000/docs
|
| 39 |
echo.
|
| 40 |
-
echo 💡
|
| 41 |
-
echo ✓ CoinGecko - Market Data
|
| 42 |
-
echo ✓ CoinCap - Price Data
|
| 43 |
-
echo ✓ Binance - Exchange Data
|
| 44 |
-
echo ✓ Fear & Greed Index
|
| 45 |
-
echo ✓ DeFi Llama - TVL Data
|
| 46 |
echo.
|
| 47 |
-
echo Press Ctrl+C to stop
|
| 48 |
echo ========================================
|
| 49 |
echo.
|
| 50 |
|
|
|
|
| 1 |
@echo off
|
| 2 |
chcp 65001 > nul
|
| 3 |
+
title Crypto API Monitor Pro - Starting...
|
| 4 |
|
| 5 |
echo ========================================
|
| 6 |
+
echo 🚀 Crypto API Monitor Pro v2.0
|
| 7 |
+
echo Professional Dashboard - 40+ Providers
|
| 8 |
echo ========================================
|
| 9 |
echo.
|
| 10 |
|
| 11 |
python --version > nul 2>&1
|
| 12 |
if %errorlevel% neq 0 (
|
| 13 |
echo ❌ Python not found!
|
| 14 |
+
echo Install from: https://python.org/downloads/
|
| 15 |
pause
|
| 16 |
exit /b 1
|
| 17 |
)
|
|
|
|
| 32 |
|
| 33 |
echo.
|
| 34 |
echo ========================================
|
| 35 |
+
echo 🎯 Starting Server...
|
| 36 |
echo ========================================
|
| 37 |
echo.
|
| 38 |
echo 📊 Dashboard: http://localhost:8000/dashboard
|
| 39 |
echo 📡 API Docs: http://localhost:8000/docs
|
| 40 |
echo.
|
| 41 |
+
echo 💡 Press Ctrl+C to stop
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
echo.
|
|
|
|
| 43 |
echo ========================================
|
| 44 |
echo.
|
| 45 |
|
start_server.py
CHANGED
|
@@ -1,241 +1,19 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
🚀 Crypto Monitor ULTIMATE - Launcher Script
|
| 4 |
-
اسکریپت راهانداز سریع برای سرور
|
| 5 |
-
"""
|
| 6 |
-
|
| 7 |
-
import sys
|
| 8 |
-
import subprocess
|
| 9 |
-
import os
|
| 10 |
-
from pathlib import Path
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
def check_dependencies():
|
| 14 |
-
"""بررسی وابستگیهای لازم"""
|
| 15 |
-
print("🔍 بررسی وابستگیها...")
|
| 16 |
-
|
| 17 |
-
required_packages = [
|
| 18 |
-
'fastapi',
|
| 19 |
-
'uvicorn',
|
| 20 |
-
'aiohttp',
|
| 21 |
-
'pydantic'
|
| 22 |
-
]
|
| 23 |
-
|
| 24 |
-
missing = []
|
| 25 |
-
for package in required_packages:
|
| 26 |
-
try:
|
| 27 |
-
__import__(package)
|
| 28 |
-
print(f" ✅ {package}")
|
| 29 |
-
except ImportError:
|
| 30 |
-
missing.append(package)
|
| 31 |
-
print(f" ❌ {package} - نصب نشده")
|
| 32 |
-
|
| 33 |
-
if missing:
|
| 34 |
-
print(f"\n⚠️ {len(missing)} پکیج نصب نشده است!")
|
| 35 |
-
response = input("آیا میخواهید الان نصب شوند? (y/n): ")
|
| 36 |
-
if response.lower() == 'y':
|
| 37 |
-
install_dependencies()
|
| 38 |
-
else:
|
| 39 |
-
print("❌ بدون نصب وابستگیها، سرور نمیتواند اجرا شود.")
|
| 40 |
-
sys.exit(1)
|
| 41 |
-
else:
|
| 42 |
-
print("✅ همه وابستگیها نصب شدهاند\n")
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
def install_dependencies():
|
| 46 |
-
"""نصب وابستگیها از requirements.txt"""
|
| 47 |
-
print("\n📦 در حال نصب وابستگیها...")
|
| 48 |
-
try:
|
| 49 |
-
subprocess.check_call([
|
| 50 |
-
sys.executable, "-m", "pip", "install", "-r", "requirements.txt"
|
| 51 |
-
])
|
| 52 |
-
print("✅ همه وابستگیها با موفقیت نصب شدند\n")
|
| 53 |
-
except subprocess.CalledProcessError:
|
| 54 |
-
print("❌ خطا در نصب وابستگیها")
|
| 55 |
-
sys.exit(1)
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
def check_config_files():
|
| 59 |
-
"""بررسی فایلهای پیکربندی"""
|
| 60 |
-
print("🔍 بررسی فایلهای پیکربندی...")
|
| 61 |
-
|
| 62 |
-
config_file = Path("providers_config_extended.json")
|
| 63 |
-
if not config_file.exists():
|
| 64 |
-
print(f" ❌ {config_file} یافت نشد!")
|
| 65 |
-
print(" لطفاً این فایل را از مخزن دانلود کنید.")
|
| 66 |
-
sys.exit(1)
|
| 67 |
-
else:
|
| 68 |
-
print(f" ✅ {config_file}")
|
| 69 |
-
|
| 70 |
-
dashboard_file = Path("unified_dashboard.html")
|
| 71 |
-
if not dashboard_file.exists():
|
| 72 |
-
print(f" ⚠️ {dashboard_file} یافت نشد - داشبورد در دسترس نخواهد بود")
|
| 73 |
-
else:
|
| 74 |
-
print(f" ✅ {dashboard_file}")
|
| 75 |
-
|
| 76 |
-
print()
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
def show_banner():
|
| 80 |
-
"""نمایش بنر استارت"""
|
| 81 |
-
banner = """
|
| 82 |
-
╔═══════════════════════════════════════════════════════════╗
|
| 83 |
-
║ ║
|
| 84 |
-
║ 🚀 Crypto Monitor ULTIMATE 🚀 ║
|
| 85 |
-
║ ║
|
| 86 |
-
║ نسخه توسعهیافته با ۱۰۰+ ارائهدهنده API رایگان ║
|
| 87 |
-
║ + سیستم پیشرفته Provider Pool Management ║
|
| 88 |
-
║ ║
|
| 89 |
-
║ Version: 2.0.0 ║
|
| 90 |
-
║ Author: Crypto Monitor Team ║
|
| 91 |
-
║ ║
|
| 92 |
-
╚═══════════════════════════════════════════════════════════╝
|
| 93 |
-
"""
|
| 94 |
-
print(banner)
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
def show_menu():
|
| 98 |
-
"""نمایش منوی انتخاب"""
|
| 99 |
-
print("\n📋 انتخاب کنید:")
|
| 100 |
-
print(" 1️⃣ اجرای سرور (Production Mode)")
|
| 101 |
-
print(" 2️⃣ اجرای سرور (Development Mode - با Auto Reload)")
|
| 102 |
-
print(" 3️⃣ تست Provider Manager")
|
| 103 |
-
print(" 4️⃣ نمایش آمار ارائهدهندگان")
|
| 104 |
-
print(" 5️⃣ نصب/بروزرسانی وابستگیها")
|
| 105 |
-
print(" 0️⃣ خروج")
|
| 106 |
-
print()
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
def run_server_production():
|
| 110 |
-
"""اجرای سرور در حالت Production"""
|
| 111 |
-
print("\n🚀 راهاندازی سرور در حالت Production...")
|
| 112 |
-
print("📡 آدرس: http://localhost:8000")
|
| 113 |
-
print("📊 داشبورد: http://localhost:8000")
|
| 114 |
-
print("📖 API Docs: http://localhost:8000/docs")
|
| 115 |
-
print("\n⏸️ برای توقف سرور Ctrl+C را فشار دهید\n")
|
| 116 |
-
|
| 117 |
-
try:
|
| 118 |
-
subprocess.run([
|
| 119 |
-
sys.executable, "-m", "uvicorn",
|
| 120 |
-
"api_server_extended:app",
|
| 121 |
-
"--host", "0.0.0.0",
|
| 122 |
-
"--port", "8000",
|
| 123 |
-
"--log-level", "info"
|
| 124 |
-
])
|
| 125 |
-
except KeyboardInterrupt:
|
| 126 |
-
print("\n\n🛑 سرور متوقف شد")
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
def run_server_development():
|
| 130 |
-
"""اجرای سرور در حالت Development"""
|
| 131 |
-
print("\n🔧 راهاندازی سرور در حالت Development (Auto Reload)...")
|
| 132 |
-
print("📡 آدرس: http://localhost:8000")
|
| 133 |
-
print("📊 داشبورد: http://localhost:8000")
|
| 134 |
-
print("📖 API Docs: http://localhost:8000/docs")
|
| 135 |
-
print("\n⏸️ برای توقف سرور Ctrl+C را فشار دهید")
|
| 136 |
-
print("♻️ تغییرات فایلها بهطور خودکار اعمال میشود\n")
|
| 137 |
-
|
| 138 |
-
try:
|
| 139 |
-
subprocess.run([
|
| 140 |
-
sys.executable, "-m", "uvicorn",
|
| 141 |
-
"api_server_extended:app",
|
| 142 |
-
"--host", "0.0.0.0",
|
| 143 |
-
"--port", "8000",
|
| 144 |
-
"--reload",
|
| 145 |
-
"--log-level", "debug"
|
| 146 |
-
])
|
| 147 |
-
except KeyboardInterrupt:
|
| 148 |
-
print("\n\n🛑 سرور متوقف شد")
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
def test_provider_manager():
|
| 152 |
-
"""تست Provider Manager"""
|
| 153 |
-
print("\n🧪 اجرای تست Provider Manager...\n")
|
| 154 |
-
try:
|
| 155 |
-
subprocess.run([sys.executable, "provider_manager.py"])
|
| 156 |
-
except FileNotFoundError:
|
| 157 |
-
print("❌ فایل provider_manager.py یافت نشد")
|
| 158 |
-
except KeyboardInterrupt:
|
| 159 |
-
print("\n\n🛑 تست متوقف شد")
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
def show_stats():
|
| 163 |
-
"""نمایش آمار ارائهدهندگان"""
|
| 164 |
-
print("\n📊 نمایش آمار ارائهدهندگان...\n")
|
| 165 |
-
try:
|
| 166 |
-
from provider_manager import ProviderManager
|
| 167 |
-
manager = ProviderManager()
|
| 168 |
-
stats = manager.get_all_stats()
|
| 169 |
-
|
| 170 |
-
summary = stats['summary']
|
| 171 |
-
print("=" * 60)
|
| 172 |
-
print(f"📈 آمار کلی سیستم")
|
| 173 |
-
print("=" * 60)
|
| 174 |
-
print(f" کل ارائهدهندگان: {summary['total_providers']}")
|
| 175 |
-
print(f" آنلاین: {summary['online']}")
|
| 176 |
-
print(f" آفلاین: {summary['offline']}")
|
| 177 |
-
print(f" Degraded: {summary['degraded']}")
|
| 178 |
-
print(f" کل درخواستها: {summary['total_requests']}")
|
| 179 |
-
print(f" درخواستهای موفق: {summary['successful_requests']}")
|
| 180 |
-
print(f" نرخ موفقیت: {summary['overall_success_rate']:.2f}%")
|
| 181 |
-
print("=" * 60)
|
| 182 |
-
|
| 183 |
-
print(f"\n🔄 Poolهای موجود: {len(stats['pools'])}")
|
| 184 |
-
for pool_id, pool_data in stats['pools'].items():
|
| 185 |
-
print(f"\n 📦 {pool_data['pool_name']}")
|
| 186 |
-
print(f" دسته: {pool_data['category']}")
|
| 187 |
-
print(f" استراتژی: {pool_data['rotation_strategy']}")
|
| 188 |
-
print(f" اعضا: {pool_data['total_providers']}")
|
| 189 |
-
print(f" در دسترس: {pool_data['available_providers']}")
|
| 190 |
-
|
| 191 |
-
print("\n✅ برای جزئیات بیشتر، سرور را اجرا کرده و به داشبورد مراجعه کنید")
|
| 192 |
-
|
| 193 |
-
except ImportError:
|
| 194 |
-
print("❌ خطا: provider_manager.py یافت نشد یا وابستگیها نصب نشدهاند")
|
| 195 |
-
except Exception as e:
|
| 196 |
-
print(f"❌ خطا: {e}")
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
def main():
|
| 200 |
-
"""تابع اصلی"""
|
| 201 |
-
show_banner()
|
| 202 |
-
|
| 203 |
-
# بررسی وابستگیها
|
| 204 |
-
check_dependencies()
|
| 205 |
-
|
| 206 |
-
# بررسی فایلهای پیکربندی
|
| 207 |
-
check_config_files()
|
| 208 |
-
|
| 209 |
-
# حلقه منو
|
| 210 |
-
while True:
|
| 211 |
-
show_menu()
|
| 212 |
-
choice = input("انتخاب شما: ").strip()
|
| 213 |
-
|
| 214 |
-
if choice == "1":
|
| 215 |
-
run_server_production()
|
| 216 |
-
break
|
| 217 |
-
elif choice == "2":
|
| 218 |
-
run_server_development()
|
| 219 |
-
break
|
| 220 |
-
elif choice == "3":
|
| 221 |
-
test_provider_manager()
|
| 222 |
-
input("\n⏎ Enter را برای بازگشت به منو فشار دهید...")
|
| 223 |
-
elif choice == "4":
|
| 224 |
-
show_stats()
|
| 225 |
-
input("\n⏎ Enter را برای بازگشت به منو فشار دهید...")
|
| 226 |
-
elif choice == "5":
|
| 227 |
-
install_dependencies()
|
| 228 |
-
input("\n⏎ Enter را برای بازگشت به منو فشار دهید...")
|
| 229 |
-
elif choice == "0":
|
| 230 |
-
print("\n👋 خداحافظ!")
|
| 231 |
-
sys.exit(0)
|
| 232 |
-
else:
|
| 233 |
-
print("\n❌ انتخاب نامعتبر! لطفاً دوباره تلاش کنید.")
|
| 234 |
-
|
| 235 |
|
| 236 |
if __name__ == "__main__":
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Simple server startup script"""
|
| 2 |
+
import uvicorn
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
if __name__ == "__main__":
|
| 5 |
+
print("=" * 60)
|
| 6 |
+
print("Starting Crypto API Monitor Backend")
|
| 7 |
+
print("Server will be available at: http://localhost:7860")
|
| 8 |
+
print("Frontend: http://localhost:7860/index.html")
|
| 9 |
+
print("HF Console: http://localhost:7860/hf_console.html")
|
| 10 |
+
print("API Docs: http://localhost:7860/docs")
|
| 11 |
+
print("=" * 60)
|
| 12 |
+
|
| 13 |
+
uvicorn.run(
|
| 14 |
+
"app:app",
|
| 15 |
+
host="0.0.0.0",
|
| 16 |
+
port=7860,
|
| 17 |
+
log_level="info",
|
| 18 |
+
access_log=True
|
| 19 |
+
)
|
utils/__init__.py
CHANGED
|
@@ -1,20 +0,0 @@
|
|
| 1 |
-
"""Utility package for API monitor/crypto aggregator.
|
| 2 |
-
|
| 3 |
-
Provides convenient accessors like `setup_logging` and shared helpers.
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
from .logger import setup_logger, app_logger
|
| 7 |
-
|
| 8 |
-
def setup_logging(name: str = "crypto_monitor", level: str = "INFO"):
|
| 9 |
-
"""Backward-compatible wrapper expected by older code.
|
| 10 |
-
|
| 11 |
-
Older modules call `utils.setup_logging()`. We now route that to the
|
| 12 |
-
structured logger configuration in `utils.logger`.
|
| 13 |
-
"""
|
| 14 |
-
return setup_logger(name=name, level=level)
|
| 15 |
-
|
| 16 |
-
__all__ = [
|
| 17 |
-
"setup_logger",
|
| 18 |
-
"setup_logging",
|
| 19 |
-
"app_logger",
|
| 20 |
-
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|