Upload Short Hunter datasource gateway
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .env.example +86 -16
- .gitattributes +0 -1
- Dockerfile +44 -37
- README.md +66 -310
- SHORT_HUNTER_GATEWAY_IMPLEMENTATION_REPORT.md +153 -0
- START_HERE.md +17 -2
- ai_models.py +889 -889
- all_apis_merged_2025.json +0 -0
- api-resources/crypto_resources_unified_2025-11-11.json +24 -24
- api-resources/ultimate_crypto_pipeline_2025_NZasinich.json +7 -8
- api/HF_IMPLEMENTATION_COMPLETE.md +237 -237
- api/PRODUCTION_AUDIT_COMPREHENSIVE.md +12 -12
- api/PRODUCTION_READY.md +3 -3
- api/QUICK_START.md +182 -182
- api/api/ws_integration_services.py +334 -334
- api/backend/routers/hf_connect.py +35 -35
- api/crypto_resources_unified_2025-11-11.json +24 -24
- api/ultimate_crypto_pipeline_2025_NZasinich.json +7 -8
- api/ws_integration_services.py +334 -334
- api_compat_routes.py +569 -503
- api_server_extended.py +403 -278
- backend/routers/hf_connect.py +35 -35
- config.js +389 -389
- crypto_resources_unified_2025-11-11.json +24 -24
- docs/CRYPTOBERT_INTEGRATION.md +5 -5
- docs/DOCUMENTATION_MANIFEST.json +52 -0
- docs/active/API_RESOURCES_RUNTIME_PLAN.md +40 -0
- docs/active/DOCUMENTATION_ORGANIZATION.md +12 -0
- docs/active/PRODUCTION_ENTRYPOINT.md +11 -0
- docs/active/SHORT_HUNTER_DATASOURCE_CONTRACT.md +57 -0
- docs/api/API_DOCS.md +527 -0
- docs/archive/CHANGELOG.md +95 -0
- docs/components/WEBSOCKET_GUIDE.md +446 -446
- docs/deployment/DEPLOYMENT.md +438 -0
- docs/deployment/SET_HF_TOKEN.md +105 -0
- docs/persian/REALTIME_FEATURES_FA.md +374 -374
- docs/reports/HF_SPACE_HUB_PRESERVATION_REPORT.md +68 -0
- docs/reports/HF_SPACE_HUB_UPGRADE_REPORT.md +66 -0
- docs/reports/HF_SPACE_REPAIR_REPORT.md +69 -0
- docs/reports/PRODUCTION_AUDIT_COMPREHENSIVE.md +12 -12
- docs/reports/PROVIDER_AUTO_DISCOVERY_REPORT.json +0 -0
- docs/reports/VALIDATION_REPORT.txt +2 -0
- docs/security/INPUT_API_FILES_SECURITY_NOTE.md +10 -0
- gradio_dashboard.py +476 -476
- hf-data-engine/HUGGINGFACE_DIAGNOSTIC_GUIDE.md +0 -0
- hf-data-engine/api-resources/crypto_resources_unified_2025-11-11.json +24 -24
- hf-data-engine/api-resources/ultimate_crypto_pipeline_2025_NZasinich.json +6 -6
- hf-data-engine/api/ws_integration_services.py +334 -334
- hf-data-engine/backend/routers/hf_connect.py +35 -35
- hf-data-engine/crypto_resources_unified_2025-11-11.json +24 -24
.env.example
CHANGED
|
@@ -1,17 +1,87 @@
|
|
| 1 |
-
#
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
#
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
CRYPTOCOMPARE_KEY=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Short Hunter Datasource Gateway — Environment Variables
|
| 2 |
+
# Copy this file to .env for local development.
|
| 3 |
+
# In HuggingFace Spaces, add these as Space secrets (never commit real keys).
|
| 4 |
+
|
| 5 |
+
# ============================================================
|
| 6 |
+
# PROXY / NETWORK (region-blocked exchange workaround)
|
| 7 |
+
# ============================================================
|
| 8 |
+
# If KuCoin or Binance are blocked in the HF Space region, configure a proxy.
|
| 9 |
+
# The app will not modify system DNS. Use these env vars instead.
|
| 10 |
+
HTTP_PROXY=
|
| 11 |
+
HTTPS_PROXY=
|
| 12 |
+
ALL_PROXY=
|
| 13 |
+
|
| 14 |
+
# ============================================================
|
| 15 |
+
# EXCHANGE BASE URL OVERRIDES
|
| 16 |
+
# ============================================================
|
| 17 |
+
# Override if you need to route through a regional endpoint or proxy.
|
| 18 |
+
KUCOIN_FUTURES_BASE_URL=https://api-futures.kucoin.com
|
| 19 |
+
BINANCE_FUTURES_BASE_URL=https://fapi.binance.com
|
| 20 |
+
|
| 21 |
+
# ============================================================
|
| 22 |
+
# PROVIDER API KEYS (all optional — system works without them)
|
| 23 |
+
# ============================================================
|
| 24 |
+
# CoinGecko Pro — optional, increases rate limits
|
| 25 |
+
COINGECKO_API_KEY=
|
| 26 |
+
|
| 27 |
+
# CryptoCompare — optional, increases rate limits on OHLCV/price endpoints
|
| 28 |
CRYPTOCOMPARE_KEY=
|
| 29 |
+
CRYPTOCOMPARE_API_KEY=
|
| 30 |
+
|
| 31 |
+
# CoinMarketCap — optional, enables CMC as primary market data source
|
| 32 |
+
COINMARKETCAP_KEY=
|
| 33 |
+
CMC_API_KEY=
|
| 34 |
+
|
| 35 |
+
# CryptoPanic — optional, enables news sentiment capability
|
| 36 |
+
CRYPTOPANIC_KEY=
|
| 37 |
+
CRYPTOPANIC_API_KEY=
|
| 38 |
+
|
| 39 |
+
# NewsAPI — optional, enables headline news capability
|
| 40 |
+
NEWS_API_KEY=
|
| 41 |
+
NEWSAPI_KEY=
|
| 42 |
+
|
| 43 |
+
# ============================================================
|
| 44 |
+
# PROVIDER BEHAVIOR TUNING
|
| 45 |
+
# ============================================================
|
| 46 |
+
# HTTP request timeout per provider call (milliseconds)
|
| 47 |
+
PROVIDER_TIMEOUT_MS=10000
|
| 48 |
+
|
| 49 |
+
# Cache TTL for in-memory provider results (seconds)
|
| 50 |
+
PROVIDER_CACHE_TTL_SECONDS=30
|
| 51 |
+
|
| 52 |
+
# Circuit breaker: failures before cooldown
|
| 53 |
+
SH_PROVIDER_MAX_FAILURES=3
|
| 54 |
+
|
| 55 |
+
# Circuit breaker: cooldown duration (seconds)
|
| 56 |
+
SH_PROVIDER_COOLDOWN_SECONDS=90
|
| 57 |
+
|
| 58 |
+
# Stale cache threshold: if cached data is older than this, activate no-trade guard (seconds)
|
| 59 |
+
PROVIDER_STALE_CACHE_THRESHOLD_SECONDS=300
|
| 60 |
+
|
| 61 |
+
# Rate limit cooldown after 429 response (seconds)
|
| 62 |
+
PROVIDER_RATE_LIMIT_COOLDOWN_SECONDS=120
|
| 63 |
+
|
| 64 |
+
# ============================================================
|
| 65 |
+
# PROVIDER ORDER OVERRIDES (comma-separated, lowercase)
|
| 66 |
+
# ============================================================
|
| 67 |
+
SH_PROVIDER_ORDER_UNIVERSE=kucoin,binance,coingecko
|
| 68 |
+
SH_PROVIDER_ORDER_CONTRACT=kucoin,binance
|
| 69 |
+
SH_PROVIDER_ORDER_TICKER=kucoin,binance,coingecko,cryptocompare
|
| 70 |
+
SH_PROVIDER_ORDER_OHLCV=kucoin,binance,cryptocompare
|
| 71 |
+
SH_PROVIDER_ORDER_ORDERBOOK=kucoin,binance
|
| 72 |
+
SH_PROVIDER_ORDER_FUNDING=kucoin,binance
|
| 73 |
+
SH_PROVIDER_ORDER_OPEN_INTEREST=kucoin,binance
|
| 74 |
+
SH_PROVIDER_ORDER_MARK_INDEX=kucoin,binance
|
| 75 |
+
SH_PROVIDER_ORDER_SENTIMENT=alternative_me,cryptopanic
|
| 76 |
+
|
| 77 |
+
# ============================================================
|
| 78 |
+
# HUGGING FACE
|
| 79 |
+
# ============================================================
|
| 80 |
+
HF_TOKEN=
|
| 81 |
+
HF_MODE=public
|
| 82 |
+
|
| 83 |
+
# ============================================================
|
| 84 |
+
# APPLICATION
|
| 85 |
+
# ============================================================
|
| 86 |
+
PORT=7860
|
| 87 |
+
USE_MOCK_DATA=false
|
.gitattributes
CHANGED
|
@@ -42,4 +42,3 @@ final/data/crypto_monitor.db filter=lfs diff=lfs merge=lfs -text
|
|
| 42 |
app/final/__pycache__/hf_unified_server.cpython-313.pyc filter=lfs diff=lfs merge=lfs -text
|
| 43 |
app/final/data/crypto_monitor.db filter=lfs diff=lfs merge=lfs -text
|
| 44 |
__pycache__/api_server_extended.cpython-313.pyc filter=lfs diff=lfs merge=lfs -text
|
| 45 |
-
Data/crypto_monitor.db filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 42 |
app/final/__pycache__/hf_unified_server.cpython-313.pyc filter=lfs diff=lfs merge=lfs -text
|
| 43 |
app/final/data/crypto_monitor.db filter=lfs diff=lfs merge=lfs -text
|
| 44 |
__pycache__/api_server_extended.cpython-313.pyc filter=lfs diff=lfs merge=lfs -text
|
|
|
Dockerfile
CHANGED
|
@@ -1,37 +1,44 @@
|
|
| 1 |
-
FROM python:3.11-slim
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
#
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
# HuggingFace Spaces runs as non-root user 1000
|
| 4 |
+
# Create user first so we can set permissions
|
| 5 |
+
RUN useradd -m -u 1000 hfuser
|
| 6 |
+
|
| 7 |
+
WORKDIR /app
|
| 8 |
+
|
| 9 |
+
# Install system dependencies
|
| 10 |
+
RUN apt-get update && apt-get install -y \
|
| 11 |
+
build-essential \
|
| 12 |
+
curl \
|
| 13 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 14 |
+
|
| 15 |
+
# Copy requirements first for better layer caching
|
| 16 |
+
COPY requirements.txt ./requirements.txt
|
| 17 |
+
|
| 18 |
+
# Install Python dependencies
|
| 19 |
+
RUN pip install --upgrade pip setuptools wheel && \
|
| 20 |
+
pip install --no-cache-dir -r requirements.txt
|
| 21 |
+
|
| 22 |
+
# Copy application files
|
| 23 |
+
COPY . .
|
| 24 |
+
|
| 25 |
+
# Create necessary runtime directories and set permissions
|
| 26 |
+
RUN mkdir -p data/database logs api-resources && \
|
| 27 |
+
chown -R hfuser:hfuser /app
|
| 28 |
+
|
| 29 |
+
# Switch to non-root user (required by HuggingFace Spaces)
|
| 30 |
+
USER hfuser
|
| 31 |
+
|
| 32 |
+
# Set environment variables
|
| 33 |
+
ENV PYTHONUNBUFFERED=1
|
| 34 |
+
ENV PORT=7860
|
| 35 |
+
ENV DOCKER_CONTAINER=true
|
| 36 |
+
ENV USE_FASTAPI_HTML=true
|
| 37 |
+
ENV USE_GRADIO=false
|
| 38 |
+
ENV PYTHONPATH=/app
|
| 39 |
+
|
| 40 |
+
EXPOSE 7860
|
| 41 |
+
|
| 42 |
+
# Production entrypoint for HuggingFace Space
|
| 43 |
+
# Short Hunter Datasource Gateway — public crypto data only, no order execution
|
| 44 |
+
CMD ["python", "-m", "uvicorn", "api_server_extended:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "1", "--log-level", "info"]
|
README.md
CHANGED
|
@@ -1,343 +1,99 @@
|
|
| 1 |
---
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
sdk: docker
|
| 3 |
pinned: true
|
| 4 |
---
|
| 5 |
-
# 🚀 Crypto Intelligence Hub
|
| 6 |
|
| 7 |
-
|
| 8 |
|
| 9 |
-
---
|
| 10 |
-
|
| 11 |
-
## ⚡ Quick Start
|
| 12 |
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
```powershell
|
| 16 |
-
.\run_server.ps1
|
| 17 |
-
```
|
| 18 |
|
| 19 |
-
|
| 20 |
-
- ✅ Set HF_TOKEN environment variable
|
| 21 |
-
- ✅ Run system tests
|
| 22 |
-
- ✅ Start the server
|
| 23 |
-
|
| 24 |
-
Then open: **http://localhost:7860/**
|
| 25 |
|
| 26 |
---
|
| 27 |
|
| 28 |
-
##
|
| 29 |
-
|
| 30 |
-
### ✨ Features
|
| 31 |
-
|
| 32 |
-
- 🤖 **AI Sentiment Analysis** - Using Hugging Face models
|
| 33 |
-
- 📊 **Market Data** - Real-time crypto prices from CoinGecko
|
| 34 |
-
- 📰 **News Analysis** - Sentiment analysis on crypto news
|
| 35 |
-
- 💹 **Trading Pairs** - 300+ pairs with searchable dropdown
|
| 36 |
-
- 📈 **Charts & Visualizations** - Interactive data charts
|
| 37 |
-
- 🔍 **Provider Management** - Track API providers status
|
| 38 |
-
|
| 39 |
-
### 🎨 Pages
|
| 40 |
-
|
| 41 |
-
- **Main Dashboard** (`/`) - Overview and statistics
|
| 42 |
-
- **AI Tools** (`/ai-tools`) - Standalone sentiment & summarization tools
|
| 43 |
-
- **API Docs** (`/docs`) - FastAPI auto-generated documentation
|
| 44 |
-
|
| 45 |
-
---
|
| 46 |
-
|
| 47 |
-
## 🛠️ Setup
|
| 48 |
-
|
| 49 |
-
### Prerequisites
|
| 50 |
|
| 51 |
-
|
| 52 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
-
##
|
| 55 |
|
| 56 |
-
1. **Clone/Download** this repository
|
| 57 |
-
|
| 58 |
-
2. **Install dependencies:**
|
| 59 |
-
```bash
|
| 60 |
-
pip install -r requirements.txt
|
| 61 |
-
```
|
| 62 |
-
|
| 63 |
-
3. **Run the server:**
|
| 64 |
-
```powershell
|
| 65 |
-
.\run_server.ps1
|
| 66 |
-
```
|
| 67 |
-
|
| 68 |
-
---
|
| 69 |
-
|
| 70 |
-
## 🔑 Configuration
|
| 71 |
-
|
| 72 |
-
### Hugging Face Token
|
| 73 |
-
|
| 74 |
-
Your HF token is already configured in `run_server.ps1`:
|
| 75 |
```
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
3. Add: `HF_MODE` = `public`
|
| 84 |
-
4. Restart Space
|
| 85 |
-
|
| 86 |
-
---
|
| 87 |
-
|
| 88 |
-
## 📁 Project Structure
|
| 89 |
-
|
| 90 |
```
|
| 91 |
-
.
|
| 92 |
-
├── api_server_extended.py # Main FastAPI server
|
| 93 |
-
├── ai_models.py # HF models & sentiment analysis
|
| 94 |
-
├── config.py # Configuration
|
| 95 |
-
├── index.html # Main dashboard UI
|
| 96 |
-
├── ai_tools.html # Standalone AI tools page
|
| 97 |
-
├── static/
|
| 98 |
-
│ ├── css/
|
| 99 |
-
│ │ └── main.css # Styles
|
| 100 |
-
│ └── js/
|
| 101 |
-
│ ├── app.js # Main JavaScript
|
| 102 |
-
│ └── trading-pairs-loader.js # Trading pairs loader
|
| 103 |
-
├── trading_pairs.txt # 300+ trading pairs
|
| 104 |
-
├── run_server.ps1 # Start script (Windows)
|
| 105 |
-
├── test_fixes.py # System tests
|
| 106 |
-
└── README.md # This file
|
| 107 |
-
```
|
| 108 |
-
|
| 109 |
-
---
|
| 110 |
-
|
| 111 |
-
## 🧪 Testing
|
| 112 |
-
|
| 113 |
-
### Run all tests:
|
| 114 |
-
```bash
|
| 115 |
-
python test_fixes.py
|
| 116 |
-
```
|
| 117 |
-
|
| 118 |
-
### Expected output:
|
| 119 |
-
```
|
| 120 |
-
============================================================
|
| 121 |
-
[TEST] Testing All Fixes
|
| 122 |
-
============================================================
|
| 123 |
-
[*] Testing file existence...
|
| 124 |
-
[OK] Found: index.html
|
| 125 |
-
... (all files)
|
| 126 |
-
|
| 127 |
-
[*] Testing trading pairs file...
|
| 128 |
-
[OK] Found 300 trading pairs
|
| 129 |
-
|
| 130 |
-
[*] Testing AI models configuration...
|
| 131 |
-
[OK] All essential models linked
|
| 132 |
-
|
| 133 |
-
============================================================
|
| 134 |
-
Overall: 6/6 tests passed (100.0%)
|
| 135 |
-
============================================================
|
| 136 |
-
[SUCCESS] All tests passed! System is ready to use!
|
| 137 |
-
```
|
| 138 |
-
|
| 139 |
-
---
|
| 140 |
-
|
| 141 |
-
## 📊 Current Test Status
|
| 142 |
-
|
| 143 |
-
Your latest test results:
|
| 144 |
-
```
|
| 145 |
-
✅ File Existence - PASS
|
| 146 |
-
✅ Trading Pairs - PASS
|
| 147 |
-
✅ Index.html Links - PASS
|
| 148 |
-
✅ AI Models Config - PASS
|
| 149 |
-
⚠️ Environment Variables - FAIL (Fixed by run_server.ps1)
|
| 150 |
-
✅ App.js Functions - PASS
|
| 151 |
-
|
| 152 |
-
Score: 5/6 (83.3%) → Will be 6/6 after running run_server.ps1
|
| 153 |
-
```
|
| 154 |
-
|
| 155 |
-
---
|
| 156 |
-
|
| 157 |
-
## 🎯 Features Overview
|
| 158 |
-
|
| 159 |
-
### 1. **Sentiment Analysis**
|
| 160 |
-
- 5 modes: Auto, Crypto, Financial, Social, News
|
| 161 |
-
- HuggingFace models with fallback system
|
| 162 |
-
- Real-time analysis with confidence scores
|
| 163 |
-
- Score breakdown with progress bars
|
| 164 |
-
|
| 165 |
-
### 2. **Trading Pairs**
|
| 166 |
-
- 300+ pairs loaded from `trading_pairs.txt`
|
| 167 |
-
- Searchable dropdown/combobox
|
| 168 |
-
- Auto-complete functionality
|
| 169 |
-
- Used in Per-Asset Sentiment Analysis
|
| 170 |
|
| 171 |
-
##
|
| 172 |
-
- **Crypto:** CryptoBERT, twitter-roberta
|
| 173 |
-
- **Financial:** FinBERT, distilroberta-financial
|
| 174 |
-
- **Social:** twitter-roberta-sentiment
|
| 175 |
-
- **Fallback:** Lexical keyword-based analysis
|
| 176 |
|
| 177 |
-
|
| 178 |
-
-
|
| 179 |
-
-
|
| 180 |
-
-
|
| 181 |
-
-
|
|
|
|
| 182 |
|
| 183 |
-
##
|
| 184 |
-
- News sentiment analysis
|
| 185 |
-
- Database storage (SQLite)
|
| 186 |
-
- Related symbols tracking
|
| 187 |
-
- Analyzed timestamp
|
| 188 |
|
| 189 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
|
| 191 |
-
##
|
| 192 |
|
| 193 |
-
|
| 194 |
|
| 195 |
-
**Check token:**
|
| 196 |
-
```powershell
|
| 197 |
-
$env:HF_TOKEN
|
| 198 |
-
$env:HF_MODE
|
| 199 |
```
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
**Check:** Console should show "Loaded 300 trading pairs"
|
| 211 |
-
**Solution:** File `trading_pairs.txt` must exist in root
|
| 212 |
-
|
| 213 |
-
### No news articles?
|
| 214 |
-
|
| 215 |
-
**Reason:** Database is empty
|
| 216 |
-
**Solution:** Use "News & Financial Sentiment Analysis" to add news
|
| 217 |
-
|
| 218 |
-
---
|
| 219 |
-
|
| 220 |
-
## 📚 Documentation
|
| 221 |
-
|
| 222 |
-
- **START_HERE.md** - Quick start guide (فارسی)
|
| 223 |
-
- **QUICK_START_FA.md** - Fast start guide (فارسی)
|
| 224 |
-
- **FINAL_FIXES_SUMMARY.md** - Complete changes summary
|
| 225 |
-
- **SET_HF_TOKEN.md** - HF token setup guide
|
| 226 |
-
- **HF_SETUP_GUIDE.md** - Complete HF setup
|
| 227 |
-
|
| 228 |
-
---
|
| 229 |
-
|
| 230 |
-
## 🌐 API Endpoints
|
| 231 |
-
|
| 232 |
-
### Core Endpoints
|
| 233 |
-
- `GET /` - Main dashboard
|
| 234 |
-
- `GET /ai-tools` - AI tools page
|
| 235 |
-
- `GET /docs` - API documentation
|
| 236 |
-
- `GET /health` - Health check
|
| 237 |
-
|
| 238 |
-
### Market Data
|
| 239 |
-
- `GET /api/market` - Current prices
|
| 240 |
-
- `GET /api/trending` - Trending coins
|
| 241 |
-
- `GET /api/sentiment` - Fear & Greed Index
|
| 242 |
-
|
| 243 |
-
### AI/ML
|
| 244 |
-
- `POST /api/sentiment/analyze` - Sentiment analysis
|
| 245 |
-
- `POST /api/news/analyze` - News sentiment
|
| 246 |
-
- `POST /api/ai/summarize` - Text summarization
|
| 247 |
-
- `GET /api/models/status` - Models status
|
| 248 |
-
- `GET /api/models/list` - Available models
|
| 249 |
-
|
| 250 |
-
### Resources
|
| 251 |
-
- `GET /api/providers` - API providers
|
| 252 |
-
- `GET /api/resources` - Resources summary
|
| 253 |
-
- `GET /api/news` - News articles
|
| 254 |
-
|
| 255 |
-
---
|
| 256 |
-
|
| 257 |
-
## 🎨 UI Features
|
| 258 |
-
|
| 259 |
-
- 🌓 Dark theme optimized
|
| 260 |
-
- 📱 Responsive design
|
| 261 |
-
- ✨ Smooth animations
|
| 262 |
-
- 🎯 Interactive charts
|
| 263 |
-
- 🔍 Search & filters
|
| 264 |
-
- 📊 Real-time updates
|
| 265 |
-
|
| 266 |
-
---
|
| 267 |
-
|
| 268 |
-
## 🚀 Deployment
|
| 269 |
-
|
| 270 |
-
### Hugging Face Space
|
| 271 |
-
|
| 272 |
-
1. Push code to HF Space
|
| 273 |
-
2. Add secrets:
|
| 274 |
-
- `HF_TOKEN` = `HF_TOKEN_FROM_SPACE_SECRET`
|
| 275 |
-
- `HF_MODE` = `public`
|
| 276 |
-
3. Restart Space
|
| 277 |
-
4. Done!
|
| 278 |
-
|
| 279 |
-
### Local
|
| 280 |
-
|
| 281 |
-
```powershell
|
| 282 |
-
.\run_server.ps1
|
| 283 |
```
|
| 284 |
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
## 📈 Performance
|
| 288 |
-
|
| 289 |
-
- **Models:** 4+ loaded (with fallback)
|
| 290 |
-
- **API Sources:** 10+ providers
|
| 291 |
-
- **Trading Pairs:** 300+
|
| 292 |
-
- **Response Time:** < 200ms (cached)
|
| 293 |
-
- **First Load:** 30-60s (model loading)
|
| 294 |
-
|
| 295 |
-
---
|
| 296 |
-
|
| 297 |
-
## 🔐 Security
|
| 298 |
-
|
| 299 |
-
- ✅ Token stored in environment variables
|
| 300 |
-
- ✅ CORS configured
|
| 301 |
-
- ✅ Rate limiting (planned)
|
| 302 |
-
- ⚠️ **Never commit tokens to git**
|
| 303 |
-
- ⚠️ **Use secrets for production**
|
| 304 |
-
|
| 305 |
-
---
|
| 306 |
|
| 307 |
-
|
|
|
|
|
|
|
|
|
|
| 308 |
|
| 309 |
-
|
| 310 |
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
- **HuggingFace** - AI Models
|
| 316 |
-
- **CoinGecko** - Market Data
|
| 317 |
-
- **Alternative.me** - Fear & Greed Index
|
| 318 |
-
- **FastAPI** - Backend Framework
|
| 319 |
-
- **Chart.js** - Visualizations
|
| 320 |
-
|
| 321 |
-
---
|
| 322 |
-
|
| 323 |
-
## 📞 Support
|
| 324 |
-
|
| 325 |
-
**Quick Issues?**
|
| 326 |
-
1. Run: `python test_fixes.py`
|
| 327 |
-
2. Check: Browser console (F12)
|
| 328 |
-
3. Review: `FINAL_FIXES_SUMMARY.md`
|
| 329 |
-
|
| 330 |
-
**Ready to start?**
|
| 331 |
-
```powershell
|
| 332 |
-
.\run_server.ps1
|
| 333 |
```
|
| 334 |
|
| 335 |
---
|
| 336 |
|
| 337 |
-
*
|
| 338 |
-
**Status:** ✅ Ready for production
|
| 339 |
-
**Last Updated:** November 19, 2025
|
| 340 |
-
|
| 341 |
-
---
|
| 342 |
-
|
| 343 |
-
Made with ❤️ for the Crypto Community 🚀
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Datasourceforcryptocurrency 2
|
| 3 |
+
emoji: 📊
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: green
|
| 6 |
sdk: docker
|
| 7 |
pinned: true
|
| 8 |
---
|
|
|
|
| 9 |
|
| 10 |
+
# Short Hunter | Futures Desk — Datasource Gateway
|
| 11 |
|
| 12 |
+
**Production entrypoint:** `uvicorn api_server_extended:app --host 0.0.0.0 --port 7860`
|
|
|
|
|
|
|
| 13 |
|
| 14 |
+
**Primary contract:** `/api/short-hunter/*`
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
+
This is a **public-data-only, read-only crypto market/futures datasource gateway** for the SHORT HUNTER paper/manual decision support application. It is **not** a trading bot and **never** places orders.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
---
|
| 19 |
|
| 20 |
+
## Key Endpoints
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
+
| Endpoint | Description |
|
| 23 |
+
|---|---|
|
| 24 |
+
| `GET /api/short-hunter/health` | Gateway health + provider router status |
|
| 25 |
+
| `GET /api/short-hunter/capabilities` | All capabilities with provider rotation info |
|
| 26 |
+
| `GET /api/short-hunter/providers/status` | Per-provider health, cooldown, error classification |
|
| 27 |
+
| `GET /api/short-hunter/universe` | Futures contracts universe |
|
| 28 |
+
| `GET /api/short-hunter/market/{symbol}` | Ticker / 24h market data |
|
| 29 |
+
| `GET /api/short-hunter/ohlcv/{symbol}` | Normalized OHLCV candles |
|
| 30 |
+
| `GET /api/short-hunter/orderbook/{symbol}` | Order book bids/asks |
|
| 31 |
+
| `GET /api/short-hunter/funding/{symbol}` | Funding rate + history |
|
| 32 |
+
| `GET /api/short-hunter/open-interest/{symbol}` | Open interest |
|
| 33 |
+
| `GET /api/short-hunter/indicators/{symbol}` | Locally computed indicators (RSI, MACD, BB) |
|
| 34 |
+
| `GET /api/short-hunter/sentiment/{symbol}` | Fear/Greed + news sentiment |
|
| 35 |
+
| `GET /api/short-hunter/snapshot/{symbol}` | Full aggregated snapshot with noTradeGuard |
|
| 36 |
+
| `POST /api/short-hunter/batch-snapshot` | Batch snapshots for multiple symbols |
|
| 37 |
|
| 38 |
+
## Architecture
|
| 39 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
```
|
| 41 |
+
Short Hunter Dashboard
|
| 42 |
+
→ Short Hunter local backend
|
| 43 |
+
→ HF Datasource Gateway (this Space)
|
| 44 |
+
→ Smart Provider Router (per capability, priority-ordered)
|
| 45 |
+
→ Provider Adapters (KuCoin, Binance, CoinGecko, CryptoCompare, Alternative.me)
|
| 46 |
+
→ Normalized ProviderResult contract
|
| 47 |
+
→ Source health / freshness / noTradeGuard
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
|
| 50 |
+
## Provider Rotation Policy
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
+
- **Critical capabilities** (ticker, OHLCV, orderbook, funding, openInterest) use KuCoin Futures → Binance Futures fallback
|
| 53 |
+
- **Discovery** uses CoinGecko as tertiary fallback
|
| 54 |
+
- **Sentiment** uses Alternative.me (no-key, public)
|
| 55 |
+
- **All providers** are subject to circuit-breaker cooldown and rate-limit detection
|
| 56 |
+
- **No provider requires a key to operate** — all public/no-key providers are used by default
|
| 57 |
+
- Optional keys (`CRYPTOCOMPARE_KEY`, `COINGECKO_API_KEY`) increase rate limits when configured
|
| 58 |
|
| 59 |
+
## No Fake Live Data Policy
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
+
If all providers fail:
|
| 62 |
+
- `sourceMode: UNAVAILABLE`, `dataState: UNAVAILABLE`
|
| 63 |
+
- `noTradeGuard: true`
|
| 64 |
+
- `success: false`
|
| 65 |
+
- No fabricated prices, rates, or candles are ever returned as live data
|
| 66 |
|
| 67 |
+
## Environment Variables
|
| 68 |
|
| 69 |
+
See `.env.example` for full list. Key variables:
|
| 70 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
```
|
| 72 |
+
HTTP_PROXY= # Optional proxy for region-blocked exchanges
|
| 73 |
+
HTTPS_PROXY= # Optional proxy
|
| 74 |
+
KUCOIN_FUTURES_BASE_URL=https://api-futures.kucoin.com
|
| 75 |
+
BINANCE_FUTURES_BASE_URL=https://fapi.binance.com
|
| 76 |
+
CRYPTOCOMPARE_KEY= # Optional — increases rate limits
|
| 77 |
+
COINGECKO_API_KEY= # Optional — increases rate limits
|
| 78 |
+
CRYPTOPANIC_KEY= # Optional — enables news sentiment
|
| 79 |
+
PROVIDER_TIMEOUT_MS=10000
|
| 80 |
+
PROVIDER_CACHE_TTL_SECONDS=30
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
```
|
| 82 |
|
| 83 |
+
## Docs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
+
- `docs/active/SHORT_HUNTER_DATASOURCE_CONTRACT.md` — API contract
|
| 86 |
+
- `docs/active/PRODUCTION_ENTRYPOINT.md` — Entrypoint details
|
| 87 |
+
- `docs/deployment/` — HF Space deployment, Docker, env vars
|
| 88 |
+
- `SHORT_HUNTER_GATEWAY_IMPLEMENTATION_REPORT.md` — Implementation report
|
| 89 |
|
| 90 |
+
## Quick Start (Local)
|
| 91 |
|
| 92 |
+
```bash
|
| 93 |
+
pip install -r requirements.txt
|
| 94 |
+
uvicorn api_server_extended:app --host 0.0.0.0 --port 7860
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
```
|
| 96 |
|
| 97 |
---
|
| 98 |
|
| 99 |
+
*Paper/manual decision support only. No order execution. No private exchange credentials.*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
SHORT_HUNTER_GATEWAY_IMPLEMENTATION_REPORT.md
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Short Hunter Datasource Gateway Implementation Report
|
| 2 |
+
|
| 3 |
+
## Summary
|
| 4 |
+
|
| 5 |
+
Implemented a production-truthful Short Hunter datasource gateway on top of the preserved HuggingFace Space project. The project remains public-data/enrichment only and does not include private exchange execution.
|
| 6 |
+
|
| 7 |
+
## Active entrypoint
|
| 8 |
+
|
| 9 |
+
Production FastAPI entrypoint remains:
|
| 10 |
+
|
| 11 |
+
```bash
|
| 12 |
+
uvicorn api_server_extended:app --host 0.0.0.0 --port 7860
|
| 13 |
+
```
|
| 14 |
+
|
| 15 |
+
`api_server_extended.py` now registers:
|
| 16 |
+
|
| 17 |
+
- existing compatibility routes from `api_compat_routes.py`
|
| 18 |
+
- new Short Hunter gateway routes from `short_hunter_routes.py`
|
| 19 |
+
|
| 20 |
+
## Added runtime provider layer
|
| 21 |
+
|
| 22 |
+
New package:
|
| 23 |
+
|
| 24 |
+
- `providers/base.py`
|
| 25 |
+
- `providers/provider_catalog.py`
|
| 26 |
+
- `providers/market_math.py`
|
| 27 |
+
- `providers/kucoin_futures.py`
|
| 28 |
+
- `providers/binance_public.py`
|
| 29 |
+
- `providers/coingecko.py`
|
| 30 |
+
- `providers/cryptocompare.py`
|
| 31 |
+
- `providers/alternative_me.py`
|
| 32 |
+
- `providers/news.py`
|
| 33 |
+
|
| 34 |
+
The provider layer normalizes every response with:
|
| 35 |
+
|
| 36 |
+
- `success`
|
| 37 |
+
- `source`
|
| 38 |
+
- `capability`
|
| 39 |
+
- `sourceMode`
|
| 40 |
+
- `dataState`
|
| 41 |
+
- `latencyMs`
|
| 42 |
+
- `timestamp`
|
| 43 |
+
- `data`
|
| 44 |
+
- `errors`
|
| 45 |
+
- `warnings`
|
| 46 |
+
- `missingCapabilities`
|
| 47 |
+
- `noTradeGuard`
|
| 48 |
+
- `noTradeGuardReason`
|
| 49 |
+
|
| 50 |
+
## New Short Hunter endpoints
|
| 51 |
+
|
| 52 |
+
- `GET /api/short-hunter/health`
|
| 53 |
+
- `GET /api/short-hunter/catalog`
|
| 54 |
+
- `GET /api/short-hunter/universe`
|
| 55 |
+
- `GET /api/short-hunter/market/{symbol}`
|
| 56 |
+
- `GET /api/short-hunter/ohlcv/{symbol}`
|
| 57 |
+
- `GET /api/short-hunter/orderbook/{symbol}`
|
| 58 |
+
- `GET /api/short-hunter/funding/{symbol}`
|
| 59 |
+
- `GET /api/short-hunter/open-interest/{symbol}`
|
| 60 |
+
- `GET /api/short-hunter/indicators/{symbol}`
|
| 61 |
+
- `GET /api/short-hunter/sentiment/{symbol}`
|
| 62 |
+
- `GET /api/short-hunter/snapshot/{symbol}`
|
| 63 |
+
- `POST /api/short-hunter/batch-snapshot`
|
| 64 |
+
- `GET /api/short-hunter/network/diagnostics`
|
| 65 |
+
|
| 66 |
+
## Provider rotation
|
| 67 |
+
|
| 68 |
+
Rotation is configurable through environment variables:
|
| 69 |
+
|
| 70 |
+
- `SH_PROVIDER_ORDER_UNIVERSE`
|
| 71 |
+
- `SH_PROVIDER_ORDER_CONTRACT`
|
| 72 |
+
- `SH_PROVIDER_ORDER_TICKER`
|
| 73 |
+
- `SH_PROVIDER_ORDER_OHLCV`
|
| 74 |
+
- `SH_PROVIDER_ORDER_ORDERBOOK`
|
| 75 |
+
- `SH_PROVIDER_ORDER_FUNDING`
|
| 76 |
+
- `SH_PROVIDER_ORDER_OPEN_INTEREST`
|
| 77 |
+
- `SH_PROVIDER_ORDER_MARK_INDEX`
|
| 78 |
+
- `SH_PROVIDER_ORDER_SENTIMENT`
|
| 79 |
+
|
| 80 |
+
Defaults prioritize KuCoin Futures for futures-specific data, then Binance Futures, then public fallback sources where appropriate.
|
| 81 |
+
|
| 82 |
+
## Network / DNS / regional access policy
|
| 83 |
+
|
| 84 |
+
The gateway does not mutate system DNS at runtime because that is unsafe/unreliable inside HF Spaces. Instead it supports:
|
| 85 |
+
|
| 86 |
+
- `KUCOIN_FUTURES_BASE_URL`
|
| 87 |
+
- `BINANCE_FUTURES_BASE_URL`
|
| 88 |
+
- `HTTP_PROXY`
|
| 89 |
+
- `HTTPS_PROXY`
|
| 90 |
+
- `ALL_PROXY`
|
| 91 |
+
|
| 92 |
+
The `/api/short-hunter/network/diagnostics` endpoint exposes this policy and current proxy/base URL configuration.
|
| 93 |
+
|
| 94 |
+
## Truthfulness behavior
|
| 95 |
+
|
| 96 |
+
The gateway never fabricates live data. If a capability is unavailable, it returns:
|
| 97 |
+
|
| 98 |
+
- `sourceMode=UNAVAILABLE`
|
| 99 |
+
- `dataState=UNAVAILABLE`
|
| 100 |
+
- `missingCapabilities=[...]`
|
| 101 |
+
- `noTradeGuard=true` for critical capabilities
|
| 102 |
+
|
| 103 |
+
If partial data exists, it returns:
|
| 104 |
+
|
| 105 |
+
- `sourceMode=DEGRADED`
|
| 106 |
+
- `dataState=PARTIAL`
|
| 107 |
+
- warnings and missing capabilities
|
| 108 |
+
|
| 109 |
+
## Docker / dependencies
|
| 110 |
+
|
| 111 |
+
Fixed Dockerfile dependency reference:
|
| 112 |
+
|
| 113 |
+
- Dockerfile now copies `requirements.txt`
|
| 114 |
+
- root `requirements.txt` exists
|
| 115 |
+
- `requirements_hf.txt` kept as compatibility copy
|
| 116 |
+
|
| 117 |
+
## api-resources
|
| 118 |
+
|
| 119 |
+
Validated and normalized api-resources JSON files. `ultimate_crypto_pipeline_2025_NZasinich.json` had a preserved filename prefix and is now valid JSON.
|
| 120 |
+
|
| 121 |
+
## Documentation
|
| 122 |
+
|
| 123 |
+
Added:
|
| 124 |
+
|
| 125 |
+
- `docs/active/PRODUCTION_ENTRYPOINT.md`
|
| 126 |
+
- `docs/active/SHORT_HUNTER_DATASOURCE_CONTRACT.md`
|
| 127 |
+
- `docs/active/API_RESOURCES_RUNTIME_PLAN.md`
|
| 128 |
+
- `docs/active/DOCUMENTATION_ORGANIZATION.md`
|
| 129 |
+
|
| 130 |
+
## Validation run
|
| 131 |
+
|
| 132 |
+
Commands run successfully:
|
| 133 |
+
|
| 134 |
+
```bash
|
| 135 |
+
python -m py_compile providers/*.py short_hunter_routes.py api_server_extended.py
|
| 136 |
+
python - <<'PY'
|
| 137 |
+
from api_server_extended import app
|
| 138 |
+
print([r.path for r in app.routes if 'short-hunter' in r.path])
|
| 139 |
+
PY
|
| 140 |
+
pytest -q tests/test_short_hunter_gateway.py
|
| 141 |
+
```
|
| 142 |
+
|
| 143 |
+
Test result:
|
| 144 |
+
|
| 145 |
+
```text
|
| 146 |
+
5 passed
|
| 147 |
+
```
|
| 148 |
+
|
| 149 |
+
## Remaining limitations
|
| 150 |
+
|
| 151 |
+
- Live provider behavior depends on HF Space network reachability and public provider rate limits.
|
| 152 |
+
- KuCoin/Binance regional restrictions cannot be solved by code-only DNS mutation; use proxy/base URL overrides when needed.
|
| 153 |
+
- Funding/open-interest endpoint availability can vary by provider API generation; failures are structured as degraded/unavailable instead of fake data.
|
START_HERE.md
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# 🚀 شروع سریع - START HERE
|
| 2 |
|
| 3 |
## یک دستور برای اجرای کامل! ⚡
|
|
@@ -67,7 +82,7 @@ Score: 5/6 (83.3%)
|
|
| 67 |
|
| 68 |
### گزینه 2: دستی
|
| 69 |
```powershell
|
| 70 |
-
$env:HF_TOKEN="HF_TOKEN_FROM_SPACE_SECRET"
|
| 71 |
$env:HF_MODE="public"
|
| 72 |
python api_server_extended.py
|
| 73 |
```
|
|
@@ -75,7 +90,7 @@ python api_server_extended.py
|
|
| 75 |
### گزینه 3: دائمی (در System Environment Variables)
|
| 76 |
1. Win + R → `sysdm.cpl`
|
| 77 |
2. Advanced → Environment Variables
|
| 78 |
-
3. New → Name: `HF_TOKEN`, Value: `HF_TOKEN_FROM_SPACE_SECRET`
|
| 79 |
4. New → Name: `HF_MODE`, Value: `public`
|
| 80 |
|
| 81 |
---
|
|
|
|
| 1 |
+
|
| 2 |
+
## Short Hunter Datasource Gateway
|
| 3 |
+
|
| 4 |
+
Production entrypoint: `uvicorn api_server_extended:app --host 0.0.0.0 --port 7860`.
|
| 5 |
+
|
| 6 |
+
Primary Short Hunter contract: `/api/short-hunter/*`, especially `/api/short-hunter/health` and `/api/short-hunter/snapshot/{symbol}`.
|
| 7 |
+
|
| 8 |
+
See `docs/active/SHORT_HUNTER_DATASOURCE_CONTRACT.md` and `SHORT_HUNTER_GATEWAY_IMPLEMENTATION_REPORT.md`.
|
| 9 |
+
|
| 10 |
+
# SOURCE-PRESERVED HUB BUILD
|
| 11 |
+
|
| 12 |
+
This build preserves the large API/resource/model hub. It is not the minimal deploy-clean package. Use this when you want all JSON registries, API pools, model pipelines, and fallback engines kept available.
|
| 13 |
+
|
| 14 |
+
Do not commit raw keys. Configure all provider keys through HuggingFace Space Secrets.
|
| 15 |
+
|
| 16 |
# 🚀 شروع سریع - START HERE
|
| 17 |
|
| 18 |
## یک دستور برای اجرای کامل! ⚡
|
|
|
|
| 82 |
|
| 83 |
### گزینه 2: دستی
|
| 84 |
```powershell
|
| 85 |
+
$env:HF_TOKEN="<HF_TOKEN_FROM_SPACE_SECRET>"
|
| 86 |
$env:HF_MODE="public"
|
| 87 |
python api_server_extended.py
|
| 88 |
```
|
|
|
|
| 90 |
### گزینه 3: دائمی (در System Environment Variables)
|
| 91 |
1. Win + R → `sysdm.cpl`
|
| 92 |
2. Advanced → Environment Variables
|
| 93 |
+
3. New → Name: `HF_TOKEN`, Value: `<HF_TOKEN_FROM_SPACE_SECRET>`
|
| 94 |
4. New → Name: `HF_MODE`, Value: `public`
|
| 95 |
|
| 96 |
---
|
ai_models.py
CHANGED
|
@@ -1,889 +1,889 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""Centralized access to Hugging Face models with lazy loading and self-healing."""
|
| 3 |
-
|
| 4 |
-
from __future__ import annotations
|
| 5 |
-
import logging
|
| 6 |
-
import os
|
| 7 |
-
import threading
|
| 8 |
-
import time
|
| 9 |
-
from dataclasses import dataclass
|
| 10 |
-
from typing import Any, Dict, List, Mapping, Optional, Sequence
|
| 11 |
-
|
| 12 |
-
try:
|
| 13 |
-
from transformers import pipeline
|
| 14 |
-
TRANSFORMERS_AVAILABLE = True
|
| 15 |
-
except ImportError:
|
| 16 |
-
TRANSFORMERS_AVAILABLE = False
|
| 17 |
-
pipeline = None
|
| 18 |
-
|
| 19 |
-
try:
|
| 20 |
-
from huggingface_hub.errors import RepositoryNotFoundError
|
| 21 |
-
from huggingface_hub import InferenceClient
|
| 22 |
-
HF_HUB_AVAILABLE = True
|
| 23 |
-
INFERENCE_CLIENT_AVAILABLE = True
|
| 24 |
-
except ImportError:
|
| 25 |
-
HF_HUB_AVAILABLE = False
|
| 26 |
-
INFERENCE_CLIENT_AVAILABLE = False
|
| 27 |
-
RepositoryNotFoundError = Exception
|
| 28 |
-
InferenceClient = None # type: ignore
|
| 29 |
-
|
| 30 |
-
logger = logging.getLogger(__name__)
|
| 31 |
-
|
| 32 |
-
# Environment configuration
|
| 33 |
-
HF_TOKEN_ENV = os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACE_TOKEN")
|
| 34 |
-
HF_MAX_STARTUP_MODELS = max(1, int(os.getenv("HF_MAX_STARTUP_MODELS", "6")))
|
| 35 |
-
|
| 36 |
-
if HF_TOKEN_ENV:
|
| 37 |
-
_default_mode = "auth"
|
| 38 |
-
elif TRANSFORMERS_AVAILABLE:
|
| 39 |
-
_default_mode = "public"
|
| 40 |
-
else:
|
| 41 |
-
_default_mode = "off"
|
| 42 |
-
|
| 43 |
-
HF_MODE = os.getenv("HF_MODE", _default_mode).lower()
|
| 44 |
-
|
| 45 |
-
if HF_MODE not in ("off", "public", "auth", "inference"):
|
| 46 |
-
HF_MODE = _default_mode
|
| 47 |
-
logger.warning("Invalid HF_MODE, resetting to %s", _default_mode)
|
| 48 |
-
|
| 49 |
-
# When local torch/transformers unavailable, use HF Inference API with token
|
| 50 |
-
INFERENCE_API_MODE = (
|
| 51 |
-
not TRANSFORMERS_AVAILABLE
|
| 52 |
-
and bool(HF_TOKEN_ENV)
|
| 53 |
-
and INFERENCE_CLIENT_AVAILABLE
|
| 54 |
-
)
|
| 55 |
-
|
| 56 |
-
if TRANSFORMERS_AVAILABLE:
|
| 57 |
-
logger.info("Transformers library available (mode: %s)", HF_MODE)
|
| 58 |
-
if HF_TOKEN_ENV:
|
| 59 |
-
logger.info("HF Token found — gated models enabled")
|
| 60 |
-
else:
|
| 61 |
-
if INFERENCE_API_MODE:
|
| 62 |
-
HF_MODE = "inference" if HF_MODE == "off" else HF_MODE
|
| 63 |
-
logger.info("Local transformers unavailable — HF Inference API mode enabled")
|
| 64 |
-
else:
|
| 65 |
-
logger.warning("Transformers unavailable and no HF token — fallback only")
|
| 66 |
-
HF_MODE = "off"
|
| 67 |
-
|
| 68 |
-
if HF_MODE == "auth" and not HF_TOKEN_ENV:
|
| 69 |
-
HF_MODE = "public" if TRANSFORMERS_AVAILABLE else "off"
|
| 70 |
-
logger.warning("HF_MODE=auth but no token — downgraded to %s", HF_MODE)
|
| 71 |
-
|
| 72 |
-
if HF_MODE == "public" and HF_TOKEN_ENV:
|
| 73 |
-
HF_MODE = "auth"
|
| 74 |
-
|
| 75 |
-
# Model catalog - FIXED: Replaced broken model
|
| 76 |
-
CRYPTO_SENTIMENT_MODELS = [
|
| 77 |
-
"kk08/CryptoBERT",
|
| 78 |
-
"ElKulako/cryptobert",
|
| 79 |
-
"cardiffnlp/twitter-roberta-base-sentiment-latest",
|
| 80 |
-
]
|
| 81 |
-
|
| 82 |
-
SOCIAL_SENTIMENT_MODELS = [
|
| 83 |
-
"ElKulako/cryptobert",
|
| 84 |
-
"cardiffnlp/twitter-roberta-base-sentiment-latest",
|
| 85 |
-
]
|
| 86 |
-
|
| 87 |
-
FINANCIAL_SENTIMENT_MODELS = [
|
| 88 |
-
"StephanAkkerman/FinTwitBERT-sentiment",
|
| 89 |
-
"ProsusAI/finbert",
|
| 90 |
-
"cardiffnlp/twitter-roberta-base-sentiment-latest",
|
| 91 |
-
]
|
| 92 |
-
|
| 93 |
-
NEWS_SENTIMENT_MODELS = [
|
| 94 |
-
"StephanAkkerman/FinTwitBERT-sentiment",
|
| 95 |
-
"cardiffnlp/twitter-roberta-base-sentiment-latest",
|
| 96 |
-
]
|
| 97 |
-
|
| 98 |
-
GENERATION_MODELS = [
|
| 99 |
-
"OpenC/crypto-gpt-o3-mini",
|
| 100 |
-
]
|
| 101 |
-
|
| 102 |
-
# FIXED: Use ElKulako/cryptobert for trading signals (classification-based)
|
| 103 |
-
TRADING_SIGNAL_MODELS = [
|
| 104 |
-
"ElKulako/cryptobert",
|
| 105 |
-
]
|
| 106 |
-
|
| 107 |
-
SUMMARIZATION_MODELS = [
|
| 108 |
-
"FurkanGozukara/Crypto-Financial-News-Summarizer",
|
| 109 |
-
]
|
| 110 |
-
|
| 111 |
-
@dataclass(frozen=True)
|
| 112 |
-
class PipelineSpec:
|
| 113 |
-
key: str
|
| 114 |
-
task: str
|
| 115 |
-
model_id: str
|
| 116 |
-
requires_auth: bool = False
|
| 117 |
-
category: str = "sentiment"
|
| 118 |
-
|
| 119 |
-
# Build MODEL_SPECS
|
| 120 |
-
MODEL_SPECS: Dict[str, PipelineSpec] = {}
|
| 121 |
-
|
| 122 |
-
# Crypto sentiment
|
| 123 |
-
for i, mid in enumerate(CRYPTO_SENTIMENT_MODELS):
|
| 124 |
-
key = f"crypto_sent_{i}"
|
| 125 |
-
MODEL_SPECS[key] = PipelineSpec(
|
| 126 |
-
key=key, task="text-classification", model_id=mid,
|
| 127 |
-
category="sentiment_crypto", requires_auth=("ElKulako" in mid)
|
| 128 |
-
)
|
| 129 |
-
|
| 130 |
-
MODEL_SPECS["crypto_sent_kk08"] = PipelineSpec(
|
| 131 |
-
key="crypto_sent_kk08", task="sentiment-analysis", model_id="kk08/CryptoBERT",
|
| 132 |
-
category="sentiment_crypto", requires_auth=False
|
| 133 |
-
)
|
| 134 |
-
|
| 135 |
-
# Social
|
| 136 |
-
for i, mid in enumerate(SOCIAL_SENTIMENT_MODELS):
|
| 137 |
-
key = f"social_sent_{i}"
|
| 138 |
-
MODEL_SPECS[key] = PipelineSpec(
|
| 139 |
-
key=key, task="text-classification", model_id=mid,
|
| 140 |
-
category="sentiment_social", requires_auth=("ElKulako" in mid)
|
| 141 |
-
)
|
| 142 |
-
|
| 143 |
-
MODEL_SPECS["crypto_sent_social"] = PipelineSpec(
|
| 144 |
-
key="crypto_sent_social", task="text-classification", model_id="ElKulako/cryptobert",
|
| 145 |
-
category="sentiment_social", requires_auth=True
|
| 146 |
-
)
|
| 147 |
-
|
| 148 |
-
# Financial
|
| 149 |
-
for i, mid in enumerate(FINANCIAL_SENTIMENT_MODELS):
|
| 150 |
-
key = f"financial_sent_{i}"
|
| 151 |
-
MODEL_SPECS[key] = PipelineSpec(
|
| 152 |
-
key=key, task="text-classification", model_id=mid, category="sentiment_financial"
|
| 153 |
-
)
|
| 154 |
-
|
| 155 |
-
MODEL_SPECS["crypto_sent_fin"] = PipelineSpec(
|
| 156 |
-
key="crypto_sent_fin", task="sentiment-analysis",
|
| 157 |
-
model_id="StephanAkkerman/FinTwitBERT-sentiment",
|
| 158 |
-
category="sentiment_financial", requires_auth=False
|
| 159 |
-
)
|
| 160 |
-
|
| 161 |
-
# News
|
| 162 |
-
for i, mid in enumerate(NEWS_SENTIMENT_MODELS):
|
| 163 |
-
key = f"news_sent_{i}"
|
| 164 |
-
MODEL_SPECS[key] = PipelineSpec(
|
| 165 |
-
key=key, task="text-classification", model_id=mid, category="sentiment_news"
|
| 166 |
-
)
|
| 167 |
-
|
| 168 |
-
# Generation
|
| 169 |
-
for i, mid in enumerate(GENERATION_MODELS):
|
| 170 |
-
key = f"crypto_gen_{i}"
|
| 171 |
-
MODEL_SPECS[key] = PipelineSpec(
|
| 172 |
-
key=key, task="text-generation", model_id=mid, category="analysis_generation"
|
| 173 |
-
)
|
| 174 |
-
|
| 175 |
-
MODEL_SPECS["crypto_ai_analyst"] = PipelineSpec(
|
| 176 |
-
key="crypto_ai_analyst", task="text-generation", model_id="OpenC/crypto-gpt-o3-mini",
|
| 177 |
-
category="analysis_generation", requires_auth=False
|
| 178 |
-
)
|
| 179 |
-
|
| 180 |
-
# FIXED: Trading signals - Use classification model
|
| 181 |
-
for i, mid in enumerate(TRADING_SIGNAL_MODELS):
|
| 182 |
-
key = f"crypto_trade_{i}"
|
| 183 |
-
MODEL_SPECS[key] = PipelineSpec(
|
| 184 |
-
key=key, task="text-classification", model_id=mid, category="trading_signal"
|
| 185 |
-
)
|
| 186 |
-
|
| 187 |
-
# FIXED: Use ElKulako/cryptobert with classification
|
| 188 |
-
MODEL_SPECS["crypto_trading_lm"] = PipelineSpec(
|
| 189 |
-
key="crypto_trading_lm", task="text-classification",
|
| 190 |
-
model_id="ElKulako/cryptobert",
|
| 191 |
-
category="trading_signal", requires_auth=True
|
| 192 |
-
)
|
| 193 |
-
|
| 194 |
-
# Summarization
|
| 195 |
-
for i, mid in enumerate(SUMMARIZATION_MODELS):
|
| 196 |
-
MODEL_SPECS[f"summarization_{i}"] = PipelineSpec(
|
| 197 |
-
key=f"summarization_{i}", task="summarization", model_id=mid,
|
| 198 |
-
category="summarization"
|
| 199 |
-
)
|
| 200 |
-
|
| 201 |
-
class ModelNotAvailable(RuntimeError):
|
| 202 |
-
pass
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
def _map_sentiment_label(label: str) -> str:
|
| 206 |
-
label = (label or "").upper()
|
| 207 |
-
if "POSITIVE" in label or "BULLISH" in label or "LABEL_2" in label:
|
| 208 |
-
return "bullish"
|
| 209 |
-
if "NEGATIVE" in label or "BEARISH" in label or "LABEL_0" in label:
|
| 210 |
-
return "bearish"
|
| 211 |
-
return "neutral"
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
def inference_classify(text: str, model_id: str) -> Dict[str, Any]:
|
| 215 |
-
"""Remote inference via HF Inference API (no local torch)."""
|
| 216 |
-
if not HF_TOKEN_ENV or not INFERENCE_CLIENT_AVAILABLE:
|
| 217 |
-
raise ModelNotAvailable("HF Inference API unavailable (no token or huggingface_hub)")
|
| 218 |
-
client = InferenceClient(token=HF_TOKEN_ENV)
|
| 219 |
-
result = client.text_classification(text[:512], model=model_id)
|
| 220 |
-
if isinstance(result, list) and result:
|
| 221 |
-
item = result[0]
|
| 222 |
-
elif isinstance(result, dict):
|
| 223 |
-
item = result
|
| 224 |
-
else:
|
| 225 |
-
raise ModelNotAvailable(f"Empty inference response from {model_id}")
|
| 226 |
-
label_raw = item.get("label", "neutral")
|
| 227 |
-
score = float(item.get("score", 0.5))
|
| 228 |
-
mapped = _map_sentiment_label(label_raw)
|
| 229 |
-
return {
|
| 230 |
-
"label": mapped,
|
| 231 |
-
"confidence": score,
|
| 232 |
-
"score": score,
|
| 233 |
-
"raw_label": label_raw,
|
| 234 |
-
"available": True,
|
| 235 |
-
"engine": "
|
| 236 |
-
"model": model_id,
|
| 237 |
-
}
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
@dataclass
|
| 241 |
-
class ModelHealthEntry:
|
| 242 |
-
key: str
|
| 243 |
-
name: str
|
| 244 |
-
status: str = "unknown"
|
| 245 |
-
last_success: Optional[float] = None
|
| 246 |
-
last_error: Optional[float] = None
|
| 247 |
-
error_count: int = 0
|
| 248 |
-
success_count: int = 0
|
| 249 |
-
cooldown_until: Optional[float] = None
|
| 250 |
-
last_error_message: Optional[str] = None
|
| 251 |
-
|
| 252 |
-
class ModelRegistry:
|
| 253 |
-
def __init__(self):
|
| 254 |
-
self._pipelines = {}
|
| 255 |
-
self._inference_ready: set = set()
|
| 256 |
-
self._lock = threading.Lock()
|
| 257 |
-
self._initialized = False
|
| 258 |
-
self._failed_models = {}
|
| 259 |
-
self._health_registry = {}
|
| 260 |
-
|
| 261 |
-
# Health settings
|
| 262 |
-
self.health_error_threshold = 3
|
| 263 |
-
self.health_cooldown_seconds = 300
|
| 264 |
-
self.health_success_recovery_count = 2
|
| 265 |
-
self.health_reinit_cooldown_seconds = 60
|
| 266 |
-
|
| 267 |
-
def _get_or_create_health_entry(self, key: str) -> ModelHealthEntry:
|
| 268 |
-
if key not in self._health_registry:
|
| 269 |
-
spec = MODEL_SPECS.get(key)
|
| 270 |
-
self._health_registry[key] = ModelHealthEntry(
|
| 271 |
-
key=key,
|
| 272 |
-
name=spec.model_id if spec else key,
|
| 273 |
-
status="unknown"
|
| 274 |
-
)
|
| 275 |
-
return self._health_registry[key]
|
| 276 |
-
|
| 277 |
-
def _update_health_on_success(self, key: str):
|
| 278 |
-
entry = self._get_or_create_health_entry(key)
|
| 279 |
-
entry.last_success = time.time()
|
| 280 |
-
entry.success_count += 1
|
| 281 |
-
|
| 282 |
-
if entry.error_count > 0:
|
| 283 |
-
entry.error_count = max(0, entry.error_count - 1)
|
| 284 |
-
|
| 285 |
-
if entry.success_count >= self.health_success_recovery_count:
|
| 286 |
-
entry.status = "healthy"
|
| 287 |
-
entry.cooldown_until = None
|
| 288 |
-
if key in self._failed_models:
|
| 289 |
-
del self._failed_models[key]
|
| 290 |
-
|
| 291 |
-
def _update_health_on_failure(self, key: str, error_msg: str):
|
| 292 |
-
entry = self._get_or_create_health_entry(key)
|
| 293 |
-
entry.last_error = time.time()
|
| 294 |
-
entry.error_count += 1
|
| 295 |
-
entry.last_error_message = error_msg[:500]
|
| 296 |
-
entry.success_count = 0
|
| 297 |
-
|
| 298 |
-
if entry.error_count >= self.health_error_threshold:
|
| 299 |
-
entry.status = "unavailable"
|
| 300 |
-
entry.cooldown_until = time.time() + self.health_cooldown_seconds
|
| 301 |
-
elif entry.error_count >= (self.health_error_threshold // 2):
|
| 302 |
-
entry.status = "degraded"
|
| 303 |
-
else:
|
| 304 |
-
entry.status = "healthy"
|
| 305 |
-
|
| 306 |
-
def _is_in_cooldown(self, key: str) -> bool:
|
| 307 |
-
if key not in self._health_registry:
|
| 308 |
-
return False
|
| 309 |
-
entry = self._health_registry[key]
|
| 310 |
-
if entry.cooldown_until is None:
|
| 311 |
-
return False
|
| 312 |
-
return time.time() < entry.cooldown_until
|
| 313 |
-
|
| 314 |
-
def attempt_model_reinit(self, key: str) -> Dict[str, Any]:
|
| 315 |
-
if key not in MODEL_SPECS:
|
| 316 |
-
return {"status": "error", "message": f"Unknown model key: {key}"}
|
| 317 |
-
|
| 318 |
-
entry = self._get_or_create_health_entry(key)
|
| 319 |
-
|
| 320 |
-
if entry.last_error:
|
| 321 |
-
time_since_error = time.time() - entry.last_error
|
| 322 |
-
if time_since_error < self.health_reinit_cooldown_seconds:
|
| 323 |
-
return {
|
| 324 |
-
"status": "cooldown",
|
| 325 |
-
"message": f"Model in cooldown, wait {int(self.health_reinit_cooldown_seconds - time_since_error)}s",
|
| 326 |
-
"cooldown_remaining": int(self.health_reinit_cooldown_seconds - time_since_error)
|
| 327 |
-
}
|
| 328 |
-
|
| 329 |
-
with self._lock:
|
| 330 |
-
if key in self._failed_models:
|
| 331 |
-
del self._failed_models[key]
|
| 332 |
-
if key in self._pipelines:
|
| 333 |
-
del self._pipelines[key]
|
| 334 |
-
|
| 335 |
-
entry.error_count = 0
|
| 336 |
-
entry.status = "unknown"
|
| 337 |
-
entry.cooldown_until = None
|
| 338 |
-
|
| 339 |
-
try:
|
| 340 |
-
pipe = self.get_pipeline(key)
|
| 341 |
-
return {
|
| 342 |
-
"status": "success",
|
| 343 |
-
"message": f"Model {key} successfully reinitialized",
|
| 344 |
-
"model": MODEL_SPECS[key].model_id
|
| 345 |
-
}
|
| 346 |
-
except Exception as e:
|
| 347 |
-
return {
|
| 348 |
-
"status": "failed",
|
| 349 |
-
"message": f"Reinitialization failed: {str(e)[:200]}",
|
| 350 |
-
"error": str(e)[:200]
|
| 351 |
-
}
|
| 352 |
-
|
| 353 |
-
def get_model_health_registry(self) -> List[Dict[str, Any]]:
|
| 354 |
-
result = []
|
| 355 |
-
for key, entry in self._health_registry.items():
|
| 356 |
-
spec = MODEL_SPECS.get(key)
|
| 357 |
-
result.append({
|
| 358 |
-
"key": entry.key,
|
| 359 |
-
"name": entry.name,
|
| 360 |
-
"model_id": spec.model_id if spec else entry.name,
|
| 361 |
-
"category": spec.category if spec else "unknown",
|
| 362 |
-
"status": entry.status,
|
| 363 |
-
"last_success": entry.last_success,
|
| 364 |
-
"last_error": entry.last_error,
|
| 365 |
-
"error_count": entry.error_count,
|
| 366 |
-
"success_count": entry.success_count,
|
| 367 |
-
"cooldown_until": entry.cooldown_until,
|
| 368 |
-
"in_cooldown": self._is_in_cooldown(key),
|
| 369 |
-
"last_error_message": entry.last_error_message,
|
| 370 |
-
"loaded": key in self._pipelines or key in self._inference_ready
|
| 371 |
-
})
|
| 372 |
-
|
| 373 |
-
for key, spec in MODEL_SPECS.items():
|
| 374 |
-
if key not in self._health_registry:
|
| 375 |
-
result.append({
|
| 376 |
-
"key": key,
|
| 377 |
-
"name": spec.model_id,
|
| 378 |
-
"model_id": spec.model_id,
|
| 379 |
-
"category": spec.category,
|
| 380 |
-
"status": "unknown",
|
| 381 |
-
"last_success": None,
|
| 382 |
-
"last_error": None,
|
| 383 |
-
"error_count": 0,
|
| 384 |
-
"success_count": 0,
|
| 385 |
-
"cooldown_until": None,
|
| 386 |
-
"in_cooldown": False,
|
| 387 |
-
"last_error_message": None,
|
| 388 |
-
"loaded": key in self._pipelines or key in self._inference_ready
|
| 389 |
-
})
|
| 390 |
-
|
| 391 |
-
return result
|
| 392 |
-
|
| 393 |
-
def _should_use_token(self, spec: PipelineSpec) -> Optional[str]:
|
| 394 |
-
if HF_MODE == "off":
|
| 395 |
-
return None
|
| 396 |
-
if HF_MODE in ("public", "auth", "inference"):
|
| 397 |
-
return HF_TOKEN_ENV if HF_TOKEN_ENV else None
|
| 398 |
-
return None
|
| 399 |
-
|
| 400 |
-
def get_pipeline(self, key: str):
|
| 401 |
-
"""LAZY LOADING: Load pipeline on first request (or mark inference-ready)."""
|
| 402 |
-
if HF_MODE == "off":
|
| 403 |
-
raise ModelNotAvailable("HF_MODE=off - models disabled")
|
| 404 |
-
if INFERENCE_API_MODE and key in self._inference_ready:
|
| 405 |
-
return key # sentinel — callers use inference_classify
|
| 406 |
-
if not TRANSFORMERS_AVAILABLE:
|
| 407 |
-
if INFERENCE_API_MODE and key in MODEL_SPECS:
|
| 408 |
-
self._inference_ready.add(key)
|
| 409 |
-
return key
|
| 410 |
-
raise ModelNotAvailable("transformers library not installed")
|
| 411 |
-
if key not in MODEL_SPECS:
|
| 412 |
-
raise ModelNotAvailable(f"Unknown model key: {key}")
|
| 413 |
-
|
| 414 |
-
spec = MODEL_SPECS[key]
|
| 415 |
-
|
| 416 |
-
if self._is_in_cooldown(key):
|
| 417 |
-
entry = self._health_registry[key]
|
| 418 |
-
cooldown_remaining = int(entry.cooldown_until - time.time())
|
| 419 |
-
raise ModelNotAvailable(
|
| 420 |
-
f"Model in cooldown for {cooldown_remaining}s: {entry.last_error_message or 'previous failures'}"
|
| 421 |
-
)
|
| 422 |
-
|
| 423 |
-
# Return cached pipeline if available
|
| 424 |
-
if key in self._pipelines:
|
| 425 |
-
return self._pipelines[key]
|
| 426 |
-
|
| 427 |
-
if key in self._failed_models:
|
| 428 |
-
raise ModelNotAvailable(f"Model failed previously: {self._failed_models[key]}")
|
| 429 |
-
|
| 430 |
-
with self._lock:
|
| 431 |
-
if key in self._pipelines:
|
| 432 |
-
return self._pipelines[key]
|
| 433 |
-
if key in self._failed_models:
|
| 434 |
-
raise ModelNotAvailable(f"Model failed previously: {self._failed_models[key]}")
|
| 435 |
-
|
| 436 |
-
auth_token = self._should_use_token(spec)
|
| 437 |
-
logger.info(f"🔄 Loading model: {spec.model_id} (mode={HF_MODE})")
|
| 438 |
-
|
| 439 |
-
try:
|
| 440 |
-
pipeline_kwargs = {
|
| 441 |
-
"task": spec.task,
|
| 442 |
-
"model": spec.model_id,
|
| 443 |
-
}
|
| 444 |
-
|
| 445 |
-
if auth_token:
|
| 446 |
-
pipeline_kwargs["token"] = auth_token
|
| 447 |
-
else:
|
| 448 |
-
pipeline_kwargs["token"] = None
|
| 449 |
-
|
| 450 |
-
self._pipelines[key] = pipeline(**pipeline_kwargs)
|
| 451 |
-
logger.info(f"✅ Successfully loaded model: {spec.model_id}")
|
| 452 |
-
self._update_health_on_success(key)
|
| 453 |
-
return self._pipelines[key]
|
| 454 |
-
|
| 455 |
-
except RepositoryNotFoundError as e:
|
| 456 |
-
error_msg = f"Repository not found: {spec.model_id}"
|
| 457 |
-
logger.warning(f"{error_msg} - {str(e)}")
|
| 458 |
-
self._failed_models[key] = error_msg
|
| 459 |
-
self._update_health_on_failure(key, error_msg)
|
| 460 |
-
raise ModelNotAvailable(error_msg) from e
|
| 461 |
-
|
| 462 |
-
except Exception as e:
|
| 463 |
-
error_msg = f"{type(e).__name__}: {str(e)[:100]}"
|
| 464 |
-
logger.warning(f"❌ Failed to load {spec.model_id}: {error_msg}")
|
| 465 |
-
self._failed_models[key] = error_msg
|
| 466 |
-
self._update_health_on_failure(key, error_msg)
|
| 467 |
-
raise ModelNotAvailable(error_msg) from e
|
| 468 |
-
|
| 469 |
-
def call_model_safe(self, key: str, text: str, **kwargs) -> Dict[str, Any]:
|
| 470 |
-
try:
|
| 471 |
-
pipe = self.get_pipeline(key)
|
| 472 |
-
result = pipe(text[:512], **kwargs)
|
| 473 |
-
self._update_health_on_success(key)
|
| 474 |
-
return {
|
| 475 |
-
"status": "success",
|
| 476 |
-
"data": result,
|
| 477 |
-
"model_key": key,
|
| 478 |
-
"model_id": MODEL_SPECS[key].model_id if key in MODEL_SPECS else key
|
| 479 |
-
}
|
| 480 |
-
except ModelNotAvailable as e:
|
| 481 |
-
return {
|
| 482 |
-
"status": "unavailable",
|
| 483 |
-
"error": str(e),
|
| 484 |
-
"model_key": key
|
| 485 |
-
}
|
| 486 |
-
except Exception as e:
|
| 487 |
-
error_msg = f"{type(e).__name__}: {str(e)[:200]}"
|
| 488 |
-
self._update_health_on_failure(key, error_msg)
|
| 489 |
-
return {
|
| 490 |
-
"status": "error",
|
| 491 |
-
"error": error_msg,
|
| 492 |
-
"model_key": key
|
| 493 |
-
}
|
| 494 |
-
|
| 495 |
-
def get_registry_status(self) -> Dict[str, Any]:
|
| 496 |
-
items = []
|
| 497 |
-
for key, spec in MODEL_SPECS.items():
|
| 498 |
-
loaded = key in self._pipelines or key in self._inference_ready
|
| 499 |
-
error = self._failed_models.get(key) if key in self._failed_models else None
|
| 500 |
-
|
| 501 |
-
items.append({
|
| 502 |
-
"key": key,
|
| 503 |
-
"name": spec.model_id,
|
| 504 |
-
"task": spec.task,
|
| 505 |
-
"category": spec.category,
|
| 506 |
-
"loaded": loaded,
|
| 507 |
-
"error": error,
|
| 508 |
-
"requires_auth": spec.requires_auth,
|
| 509 |
-
"backend": "inference_api" if key in self._inference_ready else (
|
| 510 |
-
"local" if key in self._pipelines else "pending"
|
| 511 |
-
),
|
| 512 |
-
})
|
| 513 |
-
|
| 514 |
-
loaded_count = len(set(self._pipelines.keys()) | self._inference_ready)
|
| 515 |
-
return {
|
| 516 |
-
"models_total": len(MODEL_SPECS),
|
| 517 |
-
"models_loaded": loaded_count,
|
| 518 |
-
"models_failed": len(self._failed_models),
|
| 519 |
-
"items": items,
|
| 520 |
-
"hf_mode": HF_MODE,
|
| 521 |
-
"transformers_available": TRANSFORMERS_AVAILABLE,
|
| 522 |
-
"inference_api_mode": INFERENCE_API_MODE,
|
| 523 |
-
"initialized": self._initialized
|
| 524 |
-
}
|
| 525 |
-
|
| 526 |
-
def initialize_models(self, max_models: Optional[int] = None):
|
| 527 |
-
"""Initialize registry; warm inference API models or lazy-load local pipelines."""
|
| 528 |
-
max_models = max_models if max_models is not None else HF_MAX_STARTUP_MODELS
|
| 529 |
-
|
| 530 |
-
if self._initialized:
|
| 531 |
-
return {
|
| 532 |
-
"status": "already_initialized",
|
| 533 |
-
"mode": HF_MODE,
|
| 534 |
-
"models_loaded": len(self._pipelines) + len(self._inference_ready),
|
| 535 |
-
"failed_count": len(self._failed_models),
|
| 536 |
-
"lazy_loading": not INFERENCE_API_MODE,
|
| 537 |
-
}
|
| 538 |
-
|
| 539 |
-
self._initialized = True
|
| 540 |
-
|
| 541 |
-
if HF_MODE == "off":
|
| 542 |
-
logger.info("HF_MODE=off, using fallback-only mode")
|
| 543 |
-
return {
|
| 544 |
-
"status": "fallback_only",
|
| 545 |
-
"mode": HF_MODE,
|
| 546 |
-
"models_loaded": 0,
|
| 547 |
-
"error": "HF_MODE=off",
|
| 548 |
-
}
|
| 549 |
-
|
| 550 |
-
if INFERENCE_API_MODE:
|
| 551 |
-
priority_keys = [
|
| 552 |
-
"crypto_sent_kk08", "crypto_sent_0", "crypto_sent_1",
|
| 553 |
-
"financial_sent_0", "social_sent_0", "news_sent_0",
|
| 554 |
-
]
|
| 555 |
-
warmed = 0
|
| 556 |
-
for key in priority_keys:
|
| 557 |
-
if warmed >= max_models:
|
| 558 |
-
break
|
| 559 |
-
if key in MODEL_SPECS:
|
| 560 |
-
self._inference_ready.add(key)
|
| 561 |
-
warmed += 1
|
| 562 |
-
logger.info("Inference API mode: %d models ready (max=%d)", warmed, max_models)
|
| 563 |
-
return {
|
| 564 |
-
"status": "ok",
|
| 565 |
-
"mode": HF_MODE,
|
| 566 |
-
"models_loaded": warmed,
|
| 567 |
-
"models_available": len(MODEL_SPECS),
|
| 568 |
-
"inference_api": True,
|
| 569 |
-
"token_available": bool(HF_TOKEN_ENV),
|
| 570 |
-
}
|
| 571 |
-
|
| 572 |
-
if not TRANSFORMERS_AVAILABLE:
|
| 573 |
-
logger.warning("Transformers not available, using fallback")
|
| 574 |
-
return {
|
| 575 |
-
"status": "fallback_only",
|
| 576 |
-
"mode": HF_MODE,
|
| 577 |
-
"models_loaded": 0,
|
| 578 |
-
"error": "transformers not installed",
|
| 579 |
-
}
|
| 580 |
-
|
| 581 |
-
loaded = 0
|
| 582 |
-
for key in list(MODEL_SPECS.keys())[:max_models]:
|
| 583 |
-
try:
|
| 584 |
-
self.get_pipeline(key)
|
| 585 |
-
loaded += 1
|
| 586 |
-
except Exception as exc:
|
| 587 |
-
logger.warning("Startup load skipped %s: %s", key, str(exc)[:80])
|
| 588 |
-
|
| 589 |
-
logger.info("Local model init: %d/%d loaded (mode=%s)", loaded, max_models, HF_MODE)
|
| 590 |
-
return {
|
| 591 |
-
"status": "ok",
|
| 592 |
-
"mode": HF_MODE,
|
| 593 |
-
"models_loaded": loaded,
|
| 594 |
-
"models_available": len(MODEL_SPECS),
|
| 595 |
-
"lazy_loading": loaded < len(MODEL_SPECS),
|
| 596 |
-
"token_available": bool(HF_TOKEN_ENV),
|
| 597 |
-
}
|
| 598 |
-
|
| 599 |
-
_registry = ModelRegistry()
|
| 600 |
-
|
| 601 |
-
def initialize_models(max_models: Optional[int] = None):
|
| 602 |
-
return _registry.initialize_models(max_models=max_models)
|
| 603 |
-
|
| 604 |
-
def get_model_health_registry() -> List[Dict[str, Any]]:
|
| 605 |
-
return _registry.get_model_health_registry()
|
| 606 |
-
|
| 607 |
-
def attempt_model_reinit(model_key: str) -> Dict[str, Any]:
|
| 608 |
-
return _registry.attempt_model_reinit(model_key)
|
| 609 |
-
|
| 610 |
-
def call_model_safe(model_key: str, text: str, **kwargs) -> Dict[str, Any]:
|
| 611 |
-
return _registry.call_model_safe(model_key, text, **kwargs)
|
| 612 |
-
|
| 613 |
-
def ensemble_crypto_sentiment(text: str) -> Dict[str, Any]:
|
| 614 |
-
if HF_MODE == "off":
|
| 615 |
-
return basic_sentiment_fallback(text)
|
| 616 |
-
|
| 617 |
-
if INFERENCE_API_MODE:
|
| 618 |
-
for model_id in CRYPTO_SENTIMENT_MODELS:
|
| 619 |
-
try:
|
| 620 |
-
return inference_classify(text, model_id)
|
| 621 |
-
except Exception as exc:
|
| 622 |
-
logger.warning("Inference API failed for %s: %s", model_id, str(exc)[:80])
|
| 623 |
-
return basic_sentiment_fallback(text)
|
| 624 |
-
|
| 625 |
-
if not TRANSFORMERS_AVAILABLE:
|
| 626 |
-
return basic_sentiment_fallback(text)
|
| 627 |
-
|
| 628 |
-
results, labels_count, total_conf = {}, {"bullish": 0, "bearish": 0, "neutral": 0}, 0.0
|
| 629 |
-
candidate_keys = ["crypto_sent_0", "crypto_sent_kk08", "crypto_sent_1"]
|
| 630 |
-
|
| 631 |
-
loaded_keys = [key for key in candidate_keys if key in _registry._pipelines]
|
| 632 |
-
if loaded_keys:
|
| 633 |
-
candidate_keys = loaded_keys + [k for k in candidate_keys if k not in loaded_keys]
|
| 634 |
-
|
| 635 |
-
for key in candidate_keys:
|
| 636 |
-
if key not in MODEL_SPECS:
|
| 637 |
-
continue
|
| 638 |
-
try:
|
| 639 |
-
pipe = _registry.get_pipeline(key)
|
| 640 |
-
res = pipe(text[:512])
|
| 641 |
-
if isinstance(res, list) and res:
|
| 642 |
-
res = res[0]
|
| 643 |
-
|
| 644 |
-
label = res.get("label", "NEUTRAL").upper()
|
| 645 |
-
score = res.get("score", 0.5)
|
| 646 |
-
mapped = _map_sentiment_label(label)
|
| 647 |
-
|
| 648 |
-
spec = MODEL_SPECS[key]
|
| 649 |
-
results[spec.model_id] = {"label": mapped, "score": score}
|
| 650 |
-
labels_count[mapped] += 1
|
| 651 |
-
total_conf += score
|
| 652 |
-
|
| 653 |
-
if len(results) >= 1:
|
| 654 |
-
break
|
| 655 |
-
|
| 656 |
-
except ModelNotAvailable:
|
| 657 |
-
continue
|
| 658 |
-
except Exception as e:
|
| 659 |
-
logger.warning(f"Ensemble failed for {key}: {str(e)[:100]}")
|
| 660 |
-
continue
|
| 661 |
-
|
| 662 |
-
if not results:
|
| 663 |
-
return basic_sentiment_fallback(text)
|
| 664 |
-
|
| 665 |
-
final = max(labels_count, key=labels_count.get)
|
| 666 |
-
avg_conf = total_conf / len(results)
|
| 667 |
-
|
| 668 |
-
return {
|
| 669 |
-
"label": final,
|
| 670 |
-
"confidence": avg_conf,
|
| 671 |
-
"scores": results,
|
| 672 |
-
"model_count": len(results),
|
| 673 |
-
"available": True,
|
| 674 |
-
"engine": "huggingface"
|
| 675 |
-
}
|
| 676 |
-
|
| 677 |
-
def analyze_crypto_sentiment(text: str):
|
| 678 |
-
return ensemble_crypto_sentiment(text)
|
| 679 |
-
|
| 680 |
-
def analyze_financial_sentiment(text: str):
|
| 681 |
-
if HF_MODE == "off":
|
| 682 |
-
return basic_sentiment_fallback(text)
|
| 683 |
-
if INFERENCE_API_MODE:
|
| 684 |
-
for model_id in FINANCIAL_SENTIMENT_MODELS:
|
| 685 |
-
try:
|
| 686 |
-
return inference_classify(text, model_id)
|
| 687 |
-
except Exception:
|
| 688 |
-
continue
|
| 689 |
-
return basic_sentiment_fallback(text)
|
| 690 |
-
if not TRANSFORMERS_AVAILABLE:
|
| 691 |
-
return basic_sentiment_fallback(text)
|
| 692 |
-
|
| 693 |
-
for key in ["financial_sent_0", "financial_sent_1"]:
|
| 694 |
-
if key not in MODEL_SPECS:
|
| 695 |
-
continue
|
| 696 |
-
try:
|
| 697 |
-
pipe = _registry.get_pipeline(key)
|
| 698 |
-
res = pipe(text[:512])
|
| 699 |
-
if isinstance(res, list) and res:
|
| 700 |
-
res = res[0]
|
| 701 |
-
|
| 702 |
-
label = res.get("label", "neutral").upper()
|
| 703 |
-
score = res.get("score", 0.5)
|
| 704 |
-
|
| 705 |
-
mapped = "bullish" if "POSITIVE" in label or "LABEL_2" in label else (
|
| 706 |
-
"bearish" if "NEGATIVE" in label or "LABEL_0" in label else "neutral"
|
| 707 |
-
)
|
| 708 |
-
|
| 709 |
-
return {
|
| 710 |
-
"label": mapped, "score": score, "confidence": score,
|
| 711 |
-
"available": True, "engine": "huggingface",
|
| 712 |
-
"model": MODEL_SPECS[key].model_id
|
| 713 |
-
}
|
| 714 |
-
except ModelNotAvailable:
|
| 715 |
-
continue
|
| 716 |
-
except Exception as e:
|
| 717 |
-
logger.warning(f"Financial sentiment failed for {key}: {str(e)[:100]}")
|
| 718 |
-
continue
|
| 719 |
-
|
| 720 |
-
return basic_sentiment_fallback(text)
|
| 721 |
-
|
| 722 |
-
def analyze_social_sentiment(text: str):
|
| 723 |
-
if HF_MODE == "off":
|
| 724 |
-
return basic_sentiment_fallback(text)
|
| 725 |
-
if INFERENCE_API_MODE:
|
| 726 |
-
for model_id in SOCIAL_SENTIMENT_MODELS:
|
| 727 |
-
try:
|
| 728 |
-
return inference_classify(text, model_id)
|
| 729 |
-
except Exception:
|
| 730 |
-
continue
|
| 731 |
-
return basic_sentiment_fallback(text)
|
| 732 |
-
if not TRANSFORMERS_AVAILABLE:
|
| 733 |
-
return basic_sentiment_fallback(text)
|
| 734 |
-
|
| 735 |
-
for key in ["social_sent_0", "social_sent_1"]:
|
| 736 |
-
if key not in MODEL_SPECS:
|
| 737 |
-
continue
|
| 738 |
-
try:
|
| 739 |
-
pipe = _registry.get_pipeline(key)
|
| 740 |
-
res = pipe(text[:512])
|
| 741 |
-
if isinstance(res, list) and res:
|
| 742 |
-
res = res[0]
|
| 743 |
-
|
| 744 |
-
label = res.get("label", "neutral").upper()
|
| 745 |
-
score = res.get("score", 0.5)
|
| 746 |
-
|
| 747 |
-
mapped = "bullish" if "POSITIVE" in label or "LABEL_2" in label else (
|
| 748 |
-
"bearish" if "NEGATIVE" in label or "LABEL_0" in label else "neutral"
|
| 749 |
-
)
|
| 750 |
-
|
| 751 |
-
return {
|
| 752 |
-
"label": mapped, "score": score, "confidence": score,
|
| 753 |
-
"available": True, "engine": "huggingface",
|
| 754 |
-
"model": MODEL_SPECS[key].model_id
|
| 755 |
-
}
|
| 756 |
-
except ModelNotAvailable:
|
| 757 |
-
continue
|
| 758 |
-
except Exception as e:
|
| 759 |
-
logger.warning(f"Social sentiment failed for {key}: {str(e)[:100]}")
|
| 760 |
-
continue
|
| 761 |
-
|
| 762 |
-
return basic_sentiment_fallback(text)
|
| 763 |
-
|
| 764 |
-
def analyze_market_text(text: str):
|
| 765 |
-
return ensemble_crypto_sentiment(text)
|
| 766 |
-
|
| 767 |
-
def analyze_chart_points(data: Sequence[Mapping[str, Any]], indicators: Optional[List[str]] = None):
|
| 768 |
-
if not data:
|
| 769 |
-
return {"trend": "neutral", "strength": 0, "analysis": "No data"}
|
| 770 |
-
|
| 771 |
-
prices = [float(p.get("price", 0)) for p in data if p.get("price")]
|
| 772 |
-
if not prices:
|
| 773 |
-
return {"trend": "neutral", "strength": 0, "analysis": "No price data"}
|
| 774 |
-
|
| 775 |
-
first, last = prices[0], prices[-1]
|
| 776 |
-
change = ((last - first) / first * 100) if first > 0 else 0
|
| 777 |
-
|
| 778 |
-
if change > 5:
|
| 779 |
-
trend, strength = "bullish", min(abs(change) / 10, 1.0)
|
| 780 |
-
elif change < -5:
|
| 781 |
-
trend, strength = "bearish", min(abs(change) / 10, 1.0)
|
| 782 |
-
else:
|
| 783 |
-
trend, strength = "neutral", abs(change) / 5
|
| 784 |
-
|
| 785 |
-
return {
|
| 786 |
-
"trend": trend, "strength": strength, "change_pct": change,
|
| 787 |
-
"support": min(prices), "resistance": max(prices),
|
| 788 |
-
"analysis": f"Price moved {change:.2f}% showing {trend} trend"
|
| 789 |
-
}
|
| 790 |
-
|
| 791 |
-
def analyze_news_item(item: Dict[str, Any]):
|
| 792 |
-
text = item.get("title", "") + " " + item.get("description", "")
|
| 793 |
-
sent = ensemble_crypto_sentiment(text)
|
| 794 |
-
return {
|
| 795 |
-
**item,
|
| 796 |
-
"sentiment": sent["label"],
|
| 797 |
-
"sentiment_confidence": sent["confidence"],
|
| 798 |
-
"sentiment_details": sent
|
| 799 |
-
}
|
| 800 |
-
|
| 801 |
-
def get_model_info():
|
| 802 |
-
return {
|
| 803 |
-
"transformers_available": TRANSFORMERS_AVAILABLE,
|
| 804 |
-
"inference_api_mode": INFERENCE_API_MODE,
|
| 805 |
-
"
|
| 806 |
-
"hf_mode": HF_MODE,
|
| 807 |
-
"models_initialized": _registry._initialized,
|
| 808 |
-
"models_loaded": len(_registry._pipelines) + len(_registry._inference_ready),
|
| 809 |
-
"model_catalog": {
|
| 810 |
-
"crypto_sentiment": CRYPTO_SENTIMENT_MODELS,
|
| 811 |
-
"social_sentiment": SOCIAL_SENTIMENT_MODELS,
|
| 812 |
-
"financial_sentiment": FINANCIAL_SENTIMENT_MODELS,
|
| 813 |
-
"news_sentiment": NEWS_SENTIMENT_MODELS,
|
| 814 |
-
"generation": GENERATION_MODELS,
|
| 815 |
-
"trading_signals": TRADING_SIGNAL_MODELS,
|
| 816 |
-
"summarization": SUMMARIZATION_MODELS
|
| 817 |
-
},
|
| 818 |
-
"total_models": len(MODEL_SPECS)
|
| 819 |
-
}
|
| 820 |
-
|
| 821 |
-
def basic_sentiment_fallback(text: str) -> Dict[str, Any]:
|
| 822 |
-
text_lower = text.lower()
|
| 823 |
-
|
| 824 |
-
bullish_words = ["bullish", "rally", "surge", "pump", "breakout", "skyrocket",
|
| 825 |
-
"uptrend", "buy", "accumulation", "moon", "gain", "profit",
|
| 826 |
-
"up", "high", "rise", "growth", "positive", "strong"]
|
| 827 |
-
bearish_words = ["bearish", "dump", "crash", "selloff", "downtrend", "collapse",
|
| 828 |
-
"sell", "capitulation", "panic", "fear", "drop", "loss",
|
| 829 |
-
"down", "low", "fall", "decline", "negative", "weak"]
|
| 830 |
-
|
| 831 |
-
bullish_count = sum(1 for word in bullish_words if word in text_lower)
|
| 832 |
-
bearish_count = sum(1 for word in bearish_words if word in text_lower)
|
| 833 |
-
|
| 834 |
-
if bullish_count == 0 and bearish_count == 0:
|
| 835 |
-
label, confidence = "neutral", 0.5
|
| 836 |
-
bullish_score, bearish_score, neutral_score = 0.0, 0.0, 1.0
|
| 837 |
-
elif bullish_count > bearish_count:
|
| 838 |
-
label = "bullish"
|
| 839 |
-
diff = bullish_count - bearish_count
|
| 840 |
-
confidence = min(0.6 + (diff * 0.05), 0.9)
|
| 841 |
-
bullish_score, bearish_score, neutral_score = confidence, 0.0, 0.0
|
| 842 |
-
else:
|
| 843 |
-
label = "bearish"
|
| 844 |
-
diff = bearish_count - bullish_count
|
| 845 |
-
confidence = min(0.6 + (diff * 0.05), 0.9)
|
| 846 |
-
bearish_score, bullish_score, neutral_score = confidence, 0.0, 0.0
|
| 847 |
-
|
| 848 |
-
return {
|
| 849 |
-
"label": label,
|
| 850 |
-
"confidence": confidence,
|
| 851 |
-
"score": confidence,
|
| 852 |
-
"scores": {
|
| 853 |
-
"bullish": round(bullish_score, 3),
|
| 854 |
-
"bearish": round(bearish_score, 3),
|
| 855 |
-
"neutral": round(neutral_score, 3)
|
| 856 |
-
},
|
| 857 |
-
"available": True,
|
| 858 |
-
"engine": "fallback_lexical",
|
| 859 |
-
"keyword_matches": {
|
| 860 |
-
"bullish": bullish_count,
|
| 861 |
-
"bearish": bearish_count
|
| 862 |
-
}
|
| 863 |
-
}
|
| 864 |
-
|
| 865 |
-
def registry_status():
|
| 866 |
-
loaded = len(_registry._pipelines) + len(_registry._inference_ready)
|
| 867 |
-
status = {
|
| 868 |
-
"ok": HF_MODE != "off" and (loaded > 0 or INFERENCE_API_MODE),
|
| 869 |
-
"initialized": _registry._initialized,
|
| 870 |
-
"pipelines_loaded": loaded,
|
| 871 |
-
"pipelines_failed": len(_registry._failed_models),
|
| 872 |
-
"available_models": list(_registry._pipelines.keys()) + list(_registry._inference_ready),
|
| 873 |
-
"failed_models": list(_registry._failed_models.keys())[:10],
|
| 874 |
-
"transformers_available": TRANSFORMERS_AVAILABLE,
|
| 875 |
-
"inference_api_mode": INFERENCE_API_MODE,
|
| 876 |
-
"hf_mode": HF_MODE,
|
| 877 |
-
"total_specs": len(MODEL_SPECS)
|
| 878 |
-
}
|
| 879 |
-
|
| 880 |
-
if HF_MODE == "off":
|
| 881 |
-
status["error"] = "HF_MODE=off"
|
| 882 |
-
elif INFERENCE_API_MODE and loaded == 0 and _registry._initialized:
|
| 883 |
-
status["error"] = "Inference API ready but no models warmed"
|
| 884 |
-
elif not TRANSFORMERS_AVAILABLE and not INFERENCE_API_MODE:
|
| 885 |
-
status["error"] = "transformers not installed"
|
| 886 |
-
elif loaded == 0 and _registry._initialized:
|
| 887 |
-
status["error"] = "No models loaded yet (lazy loading)"
|
| 888 |
-
|
| 889 |
-
return status
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Centralized access to Hugging Face models with lazy loading and self-healing."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
import logging
|
| 6 |
+
import os
|
| 7 |
+
import threading
|
| 8 |
+
import time
|
| 9 |
+
from dataclasses import dataclass
|
| 10 |
+
from typing import Any, Dict, List, Mapping, Optional, Sequence
|
| 11 |
+
|
| 12 |
+
try:
|
| 13 |
+
from transformers import pipeline
|
| 14 |
+
TRANSFORMERS_AVAILABLE = True
|
| 15 |
+
except ImportError:
|
| 16 |
+
TRANSFORMERS_AVAILABLE = False
|
| 17 |
+
pipeline = None
|
| 18 |
+
|
| 19 |
+
try:
|
| 20 |
+
from huggingface_hub.errors import RepositoryNotFoundError
|
| 21 |
+
from huggingface_hub import InferenceClient
|
| 22 |
+
HF_HUB_AVAILABLE = True
|
| 23 |
+
INFERENCE_CLIENT_AVAILABLE = True
|
| 24 |
+
except ImportError:
|
| 25 |
+
HF_HUB_AVAILABLE = False
|
| 26 |
+
INFERENCE_CLIENT_AVAILABLE = False
|
| 27 |
+
RepositoryNotFoundError = Exception
|
| 28 |
+
InferenceClient = None # type: ignore
|
| 29 |
+
|
| 30 |
+
logger = logging.getLogger(__name__)
|
| 31 |
+
|
| 32 |
+
# Environment configuration
|
| 33 |
+
HF_TOKEN_ENV = os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACE_TOKEN")
|
| 34 |
+
HF_MAX_STARTUP_MODELS = max(1, int(os.getenv("HF_MAX_STARTUP_MODELS", "6")))
|
| 35 |
+
|
| 36 |
+
if HF_TOKEN_ENV:
|
| 37 |
+
_default_mode = "auth"
|
| 38 |
+
elif TRANSFORMERS_AVAILABLE:
|
| 39 |
+
_default_mode = "public"
|
| 40 |
+
else:
|
| 41 |
+
_default_mode = "off"
|
| 42 |
+
|
| 43 |
+
HF_MODE = os.getenv("HF_MODE", _default_mode).lower()
|
| 44 |
+
|
| 45 |
+
if HF_MODE not in ("off", "public", "auth", "inference"):
|
| 46 |
+
HF_MODE = _default_mode
|
| 47 |
+
logger.warning("Invalid HF_MODE, resetting to %s", _default_mode)
|
| 48 |
+
|
| 49 |
+
# When local torch/transformers unavailable, use HF Inference API with token
|
| 50 |
+
INFERENCE_API_MODE = (
|
| 51 |
+
not TRANSFORMERS_AVAILABLE
|
| 52 |
+
and bool(HF_TOKEN_ENV)
|
| 53 |
+
and INFERENCE_CLIENT_AVAILABLE
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
if TRANSFORMERS_AVAILABLE:
|
| 57 |
+
logger.info("Transformers library available (mode: %s)", HF_MODE)
|
| 58 |
+
if HF_TOKEN_ENV:
|
| 59 |
+
logger.info("HF Token found — gated models enabled")
|
| 60 |
+
else:
|
| 61 |
+
if INFERENCE_API_MODE:
|
| 62 |
+
HF_MODE = "inference" if HF_MODE == "off" else HF_MODE
|
| 63 |
+
logger.info("Local transformers unavailable — HF Inference API mode enabled")
|
| 64 |
+
else:
|
| 65 |
+
logger.warning("Transformers unavailable and no HF token — fallback only")
|
| 66 |
+
HF_MODE = "off"
|
| 67 |
+
|
| 68 |
+
if HF_MODE == "auth" and not HF_TOKEN_ENV:
|
| 69 |
+
HF_MODE = "public" if TRANSFORMERS_AVAILABLE else "off"
|
| 70 |
+
logger.warning("HF_MODE=auth but no token — downgraded to %s", HF_MODE)
|
| 71 |
+
|
| 72 |
+
if HF_MODE == "public" and HF_TOKEN_ENV:
|
| 73 |
+
HF_MODE = "auth"
|
| 74 |
+
|
| 75 |
+
# Model catalog - FIXED: Replaced broken model
|
| 76 |
+
CRYPTO_SENTIMENT_MODELS = [
|
| 77 |
+
"kk08/CryptoBERT",
|
| 78 |
+
"ElKulako/cryptobert",
|
| 79 |
+
"cardiffnlp/twitter-roberta-base-sentiment-latest",
|
| 80 |
+
]
|
| 81 |
+
|
| 82 |
+
SOCIAL_SENTIMENT_MODELS = [
|
| 83 |
+
"ElKulako/cryptobert",
|
| 84 |
+
"cardiffnlp/twitter-roberta-base-sentiment-latest",
|
| 85 |
+
]
|
| 86 |
+
|
| 87 |
+
FINANCIAL_SENTIMENT_MODELS = [
|
| 88 |
+
"StephanAkkerman/FinTwitBERT-sentiment",
|
| 89 |
+
"ProsusAI/finbert",
|
| 90 |
+
"cardiffnlp/twitter-roberta-base-sentiment-latest",
|
| 91 |
+
]
|
| 92 |
+
|
| 93 |
+
NEWS_SENTIMENT_MODELS = [
|
| 94 |
+
"StephanAkkerman/FinTwitBERT-sentiment",
|
| 95 |
+
"cardiffnlp/twitter-roberta-base-sentiment-latest",
|
| 96 |
+
]
|
| 97 |
+
|
| 98 |
+
GENERATION_MODELS = [
|
| 99 |
+
"OpenC/crypto-gpt-o3-mini",
|
| 100 |
+
]
|
| 101 |
+
|
| 102 |
+
# FIXED: Use ElKulako/cryptobert for trading signals (classification-based)
|
| 103 |
+
TRADING_SIGNAL_MODELS = [
|
| 104 |
+
"ElKulako/cryptobert",
|
| 105 |
+
]
|
| 106 |
+
|
| 107 |
+
SUMMARIZATION_MODELS = [
|
| 108 |
+
"FurkanGozukara/Crypto-Financial-News-Summarizer",
|
| 109 |
+
]
|
| 110 |
+
|
| 111 |
+
@dataclass(frozen=True)
|
| 112 |
+
class PipelineSpec:
|
| 113 |
+
key: str
|
| 114 |
+
task: str
|
| 115 |
+
model_id: str
|
| 116 |
+
requires_auth: bool = False
|
| 117 |
+
category: str = "sentiment"
|
| 118 |
+
|
| 119 |
+
# Build MODEL_SPECS
|
| 120 |
+
MODEL_SPECS: Dict[str, PipelineSpec] = {}
|
| 121 |
+
|
| 122 |
+
# Crypto sentiment
|
| 123 |
+
for i, mid in enumerate(CRYPTO_SENTIMENT_MODELS):
|
| 124 |
+
key = f"crypto_sent_{i}"
|
| 125 |
+
MODEL_SPECS[key] = PipelineSpec(
|
| 126 |
+
key=key, task="text-classification", model_id=mid,
|
| 127 |
+
category="sentiment_crypto", requires_auth=("ElKulako" in mid)
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
MODEL_SPECS["crypto_sent_kk08"] = PipelineSpec(
|
| 131 |
+
key="crypto_sent_kk08", task="sentiment-analysis", model_id="kk08/CryptoBERT",
|
| 132 |
+
category="sentiment_crypto", requires_auth=False
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
# Social
|
| 136 |
+
for i, mid in enumerate(SOCIAL_SENTIMENT_MODELS):
|
| 137 |
+
key = f"social_sent_{i}"
|
| 138 |
+
MODEL_SPECS[key] = PipelineSpec(
|
| 139 |
+
key=key, task="text-classification", model_id=mid,
|
| 140 |
+
category="sentiment_social", requires_auth=("ElKulako" in mid)
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
MODEL_SPECS["crypto_sent_social"] = PipelineSpec(
|
| 144 |
+
key="crypto_sent_social", task="text-classification", model_id="ElKulako/cryptobert",
|
| 145 |
+
category="sentiment_social", requires_auth=True
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
# Financial
|
| 149 |
+
for i, mid in enumerate(FINANCIAL_SENTIMENT_MODELS):
|
| 150 |
+
key = f"financial_sent_{i}"
|
| 151 |
+
MODEL_SPECS[key] = PipelineSpec(
|
| 152 |
+
key=key, task="text-classification", model_id=mid, category="sentiment_financial"
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
MODEL_SPECS["crypto_sent_fin"] = PipelineSpec(
|
| 156 |
+
key="crypto_sent_fin", task="sentiment-analysis",
|
| 157 |
+
model_id="StephanAkkerman/FinTwitBERT-sentiment",
|
| 158 |
+
category="sentiment_financial", requires_auth=False
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
# News
|
| 162 |
+
for i, mid in enumerate(NEWS_SENTIMENT_MODELS):
|
| 163 |
+
key = f"news_sent_{i}"
|
| 164 |
+
MODEL_SPECS[key] = PipelineSpec(
|
| 165 |
+
key=key, task="text-classification", model_id=mid, category="sentiment_news"
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
+
# Generation
|
| 169 |
+
for i, mid in enumerate(GENERATION_MODELS):
|
| 170 |
+
key = f"crypto_gen_{i}"
|
| 171 |
+
MODEL_SPECS[key] = PipelineSpec(
|
| 172 |
+
key=key, task="text-generation", model_id=mid, category="analysis_generation"
|
| 173 |
+
)
|
| 174 |
+
|
| 175 |
+
MODEL_SPECS["crypto_ai_analyst"] = PipelineSpec(
|
| 176 |
+
key="crypto_ai_analyst", task="text-generation", model_id="OpenC/crypto-gpt-o3-mini",
|
| 177 |
+
category="analysis_generation", requires_auth=False
|
| 178 |
+
)
|
| 179 |
+
|
| 180 |
+
# FIXED: Trading signals - Use classification model
|
| 181 |
+
for i, mid in enumerate(TRADING_SIGNAL_MODELS):
|
| 182 |
+
key = f"crypto_trade_{i}"
|
| 183 |
+
MODEL_SPECS[key] = PipelineSpec(
|
| 184 |
+
key=key, task="text-classification", model_id=mid, category="trading_signal"
|
| 185 |
+
)
|
| 186 |
+
|
| 187 |
+
# FIXED: Use ElKulako/cryptobert with classification
|
| 188 |
+
MODEL_SPECS["crypto_trading_lm"] = PipelineSpec(
|
| 189 |
+
key="crypto_trading_lm", task="text-classification",
|
| 190 |
+
model_id="ElKulako/cryptobert",
|
| 191 |
+
category="trading_signal", requires_auth=True
|
| 192 |
+
)
|
| 193 |
+
|
| 194 |
+
# Summarization
|
| 195 |
+
for i, mid in enumerate(SUMMARIZATION_MODELS):
|
| 196 |
+
MODEL_SPECS[f"summarization_{i}"] = PipelineSpec(
|
| 197 |
+
key=f"summarization_{i}", task="summarization", model_id=mid,
|
| 198 |
+
category="summarization"
|
| 199 |
+
)
|
| 200 |
+
|
| 201 |
+
class ModelNotAvailable(RuntimeError):
|
| 202 |
+
pass
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
def _map_sentiment_label(label: str) -> str:
|
| 206 |
+
label = (label or "").upper()
|
| 207 |
+
if "POSITIVE" in label or "BULLISH" in label or "LABEL_2" in label:
|
| 208 |
+
return "bullish"
|
| 209 |
+
if "NEGATIVE" in label or "BEARISH" in label or "LABEL_0" in label:
|
| 210 |
+
return "bearish"
|
| 211 |
+
return "neutral"
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
def inference_classify(text: str, model_id: str) -> Dict[str, Any]:
|
| 215 |
+
"""Remote inference via HF Inference API (no local torch)."""
|
| 216 |
+
if not HF_TOKEN_ENV or not INFERENCE_CLIENT_AVAILABLE:
|
| 217 |
+
raise ModelNotAvailable("HF Inference API unavailable (no token or huggingface_hub)")
|
| 218 |
+
client = InferenceClient(token=HF_TOKEN_ENV)
|
| 219 |
+
result = client.text_classification(text[:512], model=model_id)
|
| 220 |
+
if isinstance(result, list) and result:
|
| 221 |
+
item = result[0]
|
| 222 |
+
elif isinstance(result, dict):
|
| 223 |
+
item = result
|
| 224 |
+
else:
|
| 225 |
+
raise ModelNotAvailable(f"Empty inference response from {model_id}")
|
| 226 |
+
label_raw = item.get("label", "neutral")
|
| 227 |
+
score = float(item.get("score", 0.5))
|
| 228 |
+
mapped = _map_sentiment_label(label_raw)
|
| 229 |
+
return {
|
| 230 |
+
"label": mapped,
|
| 231 |
+
"confidence": score,
|
| 232 |
+
"score": score,
|
| 233 |
+
"raw_label": label_raw,
|
| 234 |
+
"available": True,
|
| 235 |
+
"engine": "hf_inference",
|
| 236 |
+
"model": model_id,
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
@dataclass
|
| 241 |
+
class ModelHealthEntry:
|
| 242 |
+
key: str
|
| 243 |
+
name: str
|
| 244 |
+
status: str = "unknown"
|
| 245 |
+
last_success: Optional[float] = None
|
| 246 |
+
last_error: Optional[float] = None
|
| 247 |
+
error_count: int = 0
|
| 248 |
+
success_count: int = 0
|
| 249 |
+
cooldown_until: Optional[float] = None
|
| 250 |
+
last_error_message: Optional[str] = None
|
| 251 |
+
|
| 252 |
+
class ModelRegistry:
|
| 253 |
+
def __init__(self):
|
| 254 |
+
self._pipelines = {}
|
| 255 |
+
self._inference_ready: set = set()
|
| 256 |
+
self._lock = threading.Lock()
|
| 257 |
+
self._initialized = False
|
| 258 |
+
self._failed_models = {}
|
| 259 |
+
self._health_registry = {}
|
| 260 |
+
|
| 261 |
+
# Health settings
|
| 262 |
+
self.health_error_threshold = 3
|
| 263 |
+
self.health_cooldown_seconds = 300
|
| 264 |
+
self.health_success_recovery_count = 2
|
| 265 |
+
self.health_reinit_cooldown_seconds = 60
|
| 266 |
+
|
| 267 |
+
def _get_or_create_health_entry(self, key: str) -> ModelHealthEntry:
|
| 268 |
+
if key not in self._health_registry:
|
| 269 |
+
spec = MODEL_SPECS.get(key)
|
| 270 |
+
self._health_registry[key] = ModelHealthEntry(
|
| 271 |
+
key=key,
|
| 272 |
+
name=spec.model_id if spec else key,
|
| 273 |
+
status="unknown"
|
| 274 |
+
)
|
| 275 |
+
return self._health_registry[key]
|
| 276 |
+
|
| 277 |
+
def _update_health_on_success(self, key: str):
|
| 278 |
+
entry = self._get_or_create_health_entry(key)
|
| 279 |
+
entry.last_success = time.time()
|
| 280 |
+
entry.success_count += 1
|
| 281 |
+
|
| 282 |
+
if entry.error_count > 0:
|
| 283 |
+
entry.error_count = max(0, entry.error_count - 1)
|
| 284 |
+
|
| 285 |
+
if entry.success_count >= self.health_success_recovery_count:
|
| 286 |
+
entry.status = "healthy"
|
| 287 |
+
entry.cooldown_until = None
|
| 288 |
+
if key in self._failed_models:
|
| 289 |
+
del self._failed_models[key]
|
| 290 |
+
|
| 291 |
+
def _update_health_on_failure(self, key: str, error_msg: str):
|
| 292 |
+
entry = self._get_or_create_health_entry(key)
|
| 293 |
+
entry.last_error = time.time()
|
| 294 |
+
entry.error_count += 1
|
| 295 |
+
entry.last_error_message = error_msg[:500]
|
| 296 |
+
entry.success_count = 0
|
| 297 |
+
|
| 298 |
+
if entry.error_count >= self.health_error_threshold:
|
| 299 |
+
entry.status = "unavailable"
|
| 300 |
+
entry.cooldown_until = time.time() + self.health_cooldown_seconds
|
| 301 |
+
elif entry.error_count >= (self.health_error_threshold // 2):
|
| 302 |
+
entry.status = "degraded"
|
| 303 |
+
else:
|
| 304 |
+
entry.status = "healthy"
|
| 305 |
+
|
| 306 |
+
def _is_in_cooldown(self, key: str) -> bool:
|
| 307 |
+
if key not in self._health_registry:
|
| 308 |
+
return False
|
| 309 |
+
entry = self._health_registry[key]
|
| 310 |
+
if entry.cooldown_until is None:
|
| 311 |
+
return False
|
| 312 |
+
return time.time() < entry.cooldown_until
|
| 313 |
+
|
| 314 |
+
def attempt_model_reinit(self, key: str) -> Dict[str, Any]:
|
| 315 |
+
if key not in MODEL_SPECS:
|
| 316 |
+
return {"status": "error", "message": f"Unknown model key: {key}"}
|
| 317 |
+
|
| 318 |
+
entry = self._get_or_create_health_entry(key)
|
| 319 |
+
|
| 320 |
+
if entry.last_error:
|
| 321 |
+
time_since_error = time.time() - entry.last_error
|
| 322 |
+
if time_since_error < self.health_reinit_cooldown_seconds:
|
| 323 |
+
return {
|
| 324 |
+
"status": "cooldown",
|
| 325 |
+
"message": f"Model in cooldown, wait {int(self.health_reinit_cooldown_seconds - time_since_error)}s",
|
| 326 |
+
"cooldown_remaining": int(self.health_reinit_cooldown_seconds - time_since_error)
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
with self._lock:
|
| 330 |
+
if key in self._failed_models:
|
| 331 |
+
del self._failed_models[key]
|
| 332 |
+
if key in self._pipelines:
|
| 333 |
+
del self._pipelines[key]
|
| 334 |
+
|
| 335 |
+
entry.error_count = 0
|
| 336 |
+
entry.status = "unknown"
|
| 337 |
+
entry.cooldown_until = None
|
| 338 |
+
|
| 339 |
+
try:
|
| 340 |
+
pipe = self.get_pipeline(key)
|
| 341 |
+
return {
|
| 342 |
+
"status": "success",
|
| 343 |
+
"message": f"Model {key} successfully reinitialized",
|
| 344 |
+
"model": MODEL_SPECS[key].model_id
|
| 345 |
+
}
|
| 346 |
+
except Exception as e:
|
| 347 |
+
return {
|
| 348 |
+
"status": "failed",
|
| 349 |
+
"message": f"Reinitialization failed: {str(e)[:200]}",
|
| 350 |
+
"error": str(e)[:200]
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
def get_model_health_registry(self) -> List[Dict[str, Any]]:
|
| 354 |
+
result = []
|
| 355 |
+
for key, entry in self._health_registry.items():
|
| 356 |
+
spec = MODEL_SPECS.get(key)
|
| 357 |
+
result.append({
|
| 358 |
+
"key": entry.key,
|
| 359 |
+
"name": entry.name,
|
| 360 |
+
"model_id": spec.model_id if spec else entry.name,
|
| 361 |
+
"category": spec.category if spec else "unknown",
|
| 362 |
+
"status": entry.status,
|
| 363 |
+
"last_success": entry.last_success,
|
| 364 |
+
"last_error": entry.last_error,
|
| 365 |
+
"error_count": entry.error_count,
|
| 366 |
+
"success_count": entry.success_count,
|
| 367 |
+
"cooldown_until": entry.cooldown_until,
|
| 368 |
+
"in_cooldown": self._is_in_cooldown(key),
|
| 369 |
+
"last_error_message": entry.last_error_message,
|
| 370 |
+
"loaded": key in self._pipelines or key in self._inference_ready
|
| 371 |
+
})
|
| 372 |
+
|
| 373 |
+
for key, spec in MODEL_SPECS.items():
|
| 374 |
+
if key not in self._health_registry:
|
| 375 |
+
result.append({
|
| 376 |
+
"key": key,
|
| 377 |
+
"name": spec.model_id,
|
| 378 |
+
"model_id": spec.model_id,
|
| 379 |
+
"category": spec.category,
|
| 380 |
+
"status": "unknown",
|
| 381 |
+
"last_success": None,
|
| 382 |
+
"last_error": None,
|
| 383 |
+
"error_count": 0,
|
| 384 |
+
"success_count": 0,
|
| 385 |
+
"cooldown_until": None,
|
| 386 |
+
"in_cooldown": False,
|
| 387 |
+
"last_error_message": None,
|
| 388 |
+
"loaded": key in self._pipelines or key in self._inference_ready
|
| 389 |
+
})
|
| 390 |
+
|
| 391 |
+
return result
|
| 392 |
+
|
| 393 |
+
def _should_use_token(self, spec: PipelineSpec) -> Optional[str]:
|
| 394 |
+
if HF_MODE == "off":
|
| 395 |
+
return None
|
| 396 |
+
if HF_MODE in ("public", "auth", "inference"):
|
| 397 |
+
return HF_TOKEN_ENV if HF_TOKEN_ENV else None
|
| 398 |
+
return None
|
| 399 |
+
|
| 400 |
+
def get_pipeline(self, key: str):
|
| 401 |
+
"""LAZY LOADING: Load pipeline on first request (or mark inference-ready)."""
|
| 402 |
+
if HF_MODE == "off":
|
| 403 |
+
raise ModelNotAvailable("HF_MODE=off - models disabled")
|
| 404 |
+
if INFERENCE_API_MODE and key in self._inference_ready:
|
| 405 |
+
return key # sentinel — callers use inference_classify
|
| 406 |
+
if not TRANSFORMERS_AVAILABLE:
|
| 407 |
+
if INFERENCE_API_MODE and key in MODEL_SPECS:
|
| 408 |
+
self._inference_ready.add(key)
|
| 409 |
+
return key
|
| 410 |
+
raise ModelNotAvailable("transformers library not installed")
|
| 411 |
+
if key not in MODEL_SPECS:
|
| 412 |
+
raise ModelNotAvailable(f"Unknown model key: {key}")
|
| 413 |
+
|
| 414 |
+
spec = MODEL_SPECS[key]
|
| 415 |
+
|
| 416 |
+
if self._is_in_cooldown(key):
|
| 417 |
+
entry = self._health_registry[key]
|
| 418 |
+
cooldown_remaining = int(entry.cooldown_until - time.time())
|
| 419 |
+
raise ModelNotAvailable(
|
| 420 |
+
f"Model in cooldown for {cooldown_remaining}s: {entry.last_error_message or 'previous failures'}"
|
| 421 |
+
)
|
| 422 |
+
|
| 423 |
+
# Return cached pipeline if available
|
| 424 |
+
if key in self._pipelines:
|
| 425 |
+
return self._pipelines[key]
|
| 426 |
+
|
| 427 |
+
if key in self._failed_models:
|
| 428 |
+
raise ModelNotAvailable(f"Model failed previously: {self._failed_models[key]}")
|
| 429 |
+
|
| 430 |
+
with self._lock:
|
| 431 |
+
if key in self._pipelines:
|
| 432 |
+
return self._pipelines[key]
|
| 433 |
+
if key in self._failed_models:
|
| 434 |
+
raise ModelNotAvailable(f"Model failed previously: {self._failed_models[key]}")
|
| 435 |
+
|
| 436 |
+
auth_token = self._should_use_token(spec)
|
| 437 |
+
logger.info(f"🔄 Loading model: {spec.model_id} (mode={HF_MODE})")
|
| 438 |
+
|
| 439 |
+
try:
|
| 440 |
+
pipeline_kwargs = {
|
| 441 |
+
"task": spec.task,
|
| 442 |
+
"model": spec.model_id,
|
| 443 |
+
}
|
| 444 |
+
|
| 445 |
+
if auth_token:
|
| 446 |
+
pipeline_kwargs["token"] = auth_token
|
| 447 |
+
else:
|
| 448 |
+
pipeline_kwargs["token"] = None
|
| 449 |
+
|
| 450 |
+
self._pipelines[key] = pipeline(**pipeline_kwargs)
|
| 451 |
+
logger.info(f"✅ Successfully loaded model: {spec.model_id}")
|
| 452 |
+
self._update_health_on_success(key)
|
| 453 |
+
return self._pipelines[key]
|
| 454 |
+
|
| 455 |
+
except RepositoryNotFoundError as e:
|
| 456 |
+
error_msg = f"Repository not found: {spec.model_id}"
|
| 457 |
+
logger.warning(f"{error_msg} - {str(e)}")
|
| 458 |
+
self._failed_models[key] = error_msg
|
| 459 |
+
self._update_health_on_failure(key, error_msg)
|
| 460 |
+
raise ModelNotAvailable(error_msg) from e
|
| 461 |
+
|
| 462 |
+
except Exception as e:
|
| 463 |
+
error_msg = f"{type(e).__name__}: {str(e)[:100]}"
|
| 464 |
+
logger.warning(f"❌ Failed to load {spec.model_id}: {error_msg}")
|
| 465 |
+
self._failed_models[key] = error_msg
|
| 466 |
+
self._update_health_on_failure(key, error_msg)
|
| 467 |
+
raise ModelNotAvailable(error_msg) from e
|
| 468 |
+
|
| 469 |
+
def call_model_safe(self, key: str, text: str, **kwargs) -> Dict[str, Any]:
|
| 470 |
+
try:
|
| 471 |
+
pipe = self.get_pipeline(key)
|
| 472 |
+
result = pipe(text[:512], **kwargs)
|
| 473 |
+
self._update_health_on_success(key)
|
| 474 |
+
return {
|
| 475 |
+
"status": "success",
|
| 476 |
+
"data": result,
|
| 477 |
+
"model_key": key,
|
| 478 |
+
"model_id": MODEL_SPECS[key].model_id if key in MODEL_SPECS else key
|
| 479 |
+
}
|
| 480 |
+
except ModelNotAvailable as e:
|
| 481 |
+
return {
|
| 482 |
+
"status": "unavailable",
|
| 483 |
+
"error": str(e),
|
| 484 |
+
"model_key": key
|
| 485 |
+
}
|
| 486 |
+
except Exception as e:
|
| 487 |
+
error_msg = f"{type(e).__name__}: {str(e)[:200]}"
|
| 488 |
+
self._update_health_on_failure(key, error_msg)
|
| 489 |
+
return {
|
| 490 |
+
"status": "error",
|
| 491 |
+
"error": error_msg,
|
| 492 |
+
"model_key": key
|
| 493 |
+
}
|
| 494 |
+
|
| 495 |
+
def get_registry_status(self) -> Dict[str, Any]:
|
| 496 |
+
items = []
|
| 497 |
+
for key, spec in MODEL_SPECS.items():
|
| 498 |
+
loaded = key in self._pipelines or key in self._inference_ready
|
| 499 |
+
error = self._failed_models.get(key) if key in self._failed_models else None
|
| 500 |
+
|
| 501 |
+
items.append({
|
| 502 |
+
"key": key,
|
| 503 |
+
"name": spec.model_id,
|
| 504 |
+
"task": spec.task,
|
| 505 |
+
"category": spec.category,
|
| 506 |
+
"loaded": loaded,
|
| 507 |
+
"error": error,
|
| 508 |
+
"requires_auth": spec.requires_auth,
|
| 509 |
+
"backend": "inference_api" if key in self._inference_ready else (
|
| 510 |
+
"local" if key in self._pipelines else "pending"
|
| 511 |
+
),
|
| 512 |
+
})
|
| 513 |
+
|
| 514 |
+
loaded_count = len(set(self._pipelines.keys()) | self._inference_ready)
|
| 515 |
+
return {
|
| 516 |
+
"models_total": len(MODEL_SPECS),
|
| 517 |
+
"models_loaded": loaded_count,
|
| 518 |
+
"models_failed": len(self._failed_models),
|
| 519 |
+
"items": items,
|
| 520 |
+
"hf_mode": HF_MODE,
|
| 521 |
+
"transformers_available": TRANSFORMERS_AVAILABLE,
|
| 522 |
+
"inference_api_mode": INFERENCE_API_MODE,
|
| 523 |
+
"initialized": self._initialized
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
+
def initialize_models(self, max_models: Optional[int] = None):
|
| 527 |
+
"""Initialize registry; warm inference API models or lazy-load local pipelines."""
|
| 528 |
+
max_models = max_models if max_models is not None else HF_MAX_STARTUP_MODELS
|
| 529 |
+
|
| 530 |
+
if self._initialized:
|
| 531 |
+
return {
|
| 532 |
+
"status": "already_initialized",
|
| 533 |
+
"mode": HF_MODE,
|
| 534 |
+
"models_loaded": len(self._pipelines) + len(self._inference_ready),
|
| 535 |
+
"failed_count": len(self._failed_models),
|
| 536 |
+
"lazy_loading": not INFERENCE_API_MODE,
|
| 537 |
+
}
|
| 538 |
+
|
| 539 |
+
self._initialized = True
|
| 540 |
+
|
| 541 |
+
if HF_MODE == "off":
|
| 542 |
+
logger.info("HF_MODE=off, using fallback-only mode")
|
| 543 |
+
return {
|
| 544 |
+
"status": "fallback_only",
|
| 545 |
+
"mode": HF_MODE,
|
| 546 |
+
"models_loaded": 0,
|
| 547 |
+
"error": "HF_MODE=off",
|
| 548 |
+
}
|
| 549 |
+
|
| 550 |
+
if INFERENCE_API_MODE:
|
| 551 |
+
priority_keys = [
|
| 552 |
+
"crypto_sent_kk08", "crypto_sent_0", "crypto_sent_1",
|
| 553 |
+
"financial_sent_0", "social_sent_0", "news_sent_0",
|
| 554 |
+
]
|
| 555 |
+
warmed = 0
|
| 556 |
+
for key in priority_keys:
|
| 557 |
+
if warmed >= max_models:
|
| 558 |
+
break
|
| 559 |
+
if key in MODEL_SPECS:
|
| 560 |
+
self._inference_ready.add(key)
|
| 561 |
+
warmed += 1
|
| 562 |
+
logger.info("Inference API mode: %d models ready (max=%d)", warmed, max_models)
|
| 563 |
+
return {
|
| 564 |
+
"status": "ok",
|
| 565 |
+
"mode": HF_MODE,
|
| 566 |
+
"models_loaded": warmed,
|
| 567 |
+
"models_available": len(MODEL_SPECS),
|
| 568 |
+
"inference_api": True,
|
| 569 |
+
"token_available": bool(HF_TOKEN_ENV),
|
| 570 |
+
}
|
| 571 |
+
|
| 572 |
+
if not TRANSFORMERS_AVAILABLE:
|
| 573 |
+
logger.warning("Transformers not available, using fallback")
|
| 574 |
+
return {
|
| 575 |
+
"status": "fallback_only",
|
| 576 |
+
"mode": HF_MODE,
|
| 577 |
+
"models_loaded": 0,
|
| 578 |
+
"error": "transformers not installed",
|
| 579 |
+
}
|
| 580 |
+
|
| 581 |
+
loaded = 0
|
| 582 |
+
for key in list(MODEL_SPECS.keys())[:max_models]:
|
| 583 |
+
try:
|
| 584 |
+
self.get_pipeline(key)
|
| 585 |
+
loaded += 1
|
| 586 |
+
except Exception as exc:
|
| 587 |
+
logger.warning("Startup load skipped %s: %s", key, str(exc)[:80])
|
| 588 |
+
|
| 589 |
+
logger.info("Local model init: %d/%d loaded (mode=%s)", loaded, max_models, HF_MODE)
|
| 590 |
+
return {
|
| 591 |
+
"status": "ok",
|
| 592 |
+
"mode": HF_MODE,
|
| 593 |
+
"models_loaded": loaded,
|
| 594 |
+
"models_available": len(MODEL_SPECS),
|
| 595 |
+
"lazy_loading": loaded < len(MODEL_SPECS),
|
| 596 |
+
"token_available": bool(HF_TOKEN_ENV),
|
| 597 |
+
}
|
| 598 |
+
|
| 599 |
+
_registry = ModelRegistry()
|
| 600 |
+
|
| 601 |
+
def initialize_models(max_models: Optional[int] = None):
|
| 602 |
+
return _registry.initialize_models(max_models=max_models)
|
| 603 |
+
|
| 604 |
+
def get_model_health_registry() -> List[Dict[str, Any]]:
|
| 605 |
+
return _registry.get_model_health_registry()
|
| 606 |
+
|
| 607 |
+
def attempt_model_reinit(model_key: str) -> Dict[str, Any]:
|
| 608 |
+
return _registry.attempt_model_reinit(model_key)
|
| 609 |
+
|
| 610 |
+
def call_model_safe(model_key: str, text: str, **kwargs) -> Dict[str, Any]:
|
| 611 |
+
return _registry.call_model_safe(model_key, text, **kwargs)
|
| 612 |
+
|
| 613 |
+
def ensemble_crypto_sentiment(text: str) -> Dict[str, Any]:
|
| 614 |
+
if HF_MODE == "off":
|
| 615 |
+
return basic_sentiment_fallback(text)
|
| 616 |
+
|
| 617 |
+
if INFERENCE_API_MODE:
|
| 618 |
+
for model_id in CRYPTO_SENTIMENT_MODELS:
|
| 619 |
+
try:
|
| 620 |
+
return inference_classify(text, model_id)
|
| 621 |
+
except Exception as exc:
|
| 622 |
+
logger.warning("Inference API failed for %s: %s", model_id, str(exc)[:80])
|
| 623 |
+
return basic_sentiment_fallback(text)
|
| 624 |
+
|
| 625 |
+
if not TRANSFORMERS_AVAILABLE:
|
| 626 |
+
return basic_sentiment_fallback(text)
|
| 627 |
+
|
| 628 |
+
results, labels_count, total_conf = {}, {"bullish": 0, "bearish": 0, "neutral": 0}, 0.0
|
| 629 |
+
candidate_keys = ["crypto_sent_0", "crypto_sent_kk08", "crypto_sent_1"]
|
| 630 |
+
|
| 631 |
+
loaded_keys = [key for key in candidate_keys if key in _registry._pipelines]
|
| 632 |
+
if loaded_keys:
|
| 633 |
+
candidate_keys = loaded_keys + [k for k in candidate_keys if k not in loaded_keys]
|
| 634 |
+
|
| 635 |
+
for key in candidate_keys:
|
| 636 |
+
if key not in MODEL_SPECS:
|
| 637 |
+
continue
|
| 638 |
+
try:
|
| 639 |
+
pipe = _registry.get_pipeline(key)
|
| 640 |
+
res = pipe(text[:512])
|
| 641 |
+
if isinstance(res, list) and res:
|
| 642 |
+
res = res[0]
|
| 643 |
+
|
| 644 |
+
label = res.get("label", "NEUTRAL").upper()
|
| 645 |
+
score = res.get("score", 0.5)
|
| 646 |
+
mapped = _map_sentiment_label(label)
|
| 647 |
+
|
| 648 |
+
spec = MODEL_SPECS[key]
|
| 649 |
+
results[spec.model_id] = {"label": mapped, "score": score}
|
| 650 |
+
labels_count[mapped] += 1
|
| 651 |
+
total_conf += score
|
| 652 |
+
|
| 653 |
+
if len(results) >= 1:
|
| 654 |
+
break
|
| 655 |
+
|
| 656 |
+
except ModelNotAvailable:
|
| 657 |
+
continue
|
| 658 |
+
except Exception as e:
|
| 659 |
+
logger.warning(f"Ensemble failed for {key}: {str(e)[:100]}")
|
| 660 |
+
continue
|
| 661 |
+
|
| 662 |
+
if not results:
|
| 663 |
+
return basic_sentiment_fallback(text)
|
| 664 |
+
|
| 665 |
+
final = max(labels_count, key=labels_count.get)
|
| 666 |
+
avg_conf = total_conf / len(results)
|
| 667 |
+
|
| 668 |
+
return {
|
| 669 |
+
"label": final,
|
| 670 |
+
"confidence": avg_conf,
|
| 671 |
+
"scores": results,
|
| 672 |
+
"model_count": len(results),
|
| 673 |
+
"available": True,
|
| 674 |
+
"engine": "huggingface"
|
| 675 |
+
}
|
| 676 |
+
|
| 677 |
+
def analyze_crypto_sentiment(text: str):
|
| 678 |
+
return ensemble_crypto_sentiment(text)
|
| 679 |
+
|
| 680 |
+
def analyze_financial_sentiment(text: str):
|
| 681 |
+
if HF_MODE == "off":
|
| 682 |
+
return basic_sentiment_fallback(text)
|
| 683 |
+
if INFERENCE_API_MODE:
|
| 684 |
+
for model_id in FINANCIAL_SENTIMENT_MODELS:
|
| 685 |
+
try:
|
| 686 |
+
return inference_classify(text, model_id)
|
| 687 |
+
except Exception:
|
| 688 |
+
continue
|
| 689 |
+
return basic_sentiment_fallback(text)
|
| 690 |
+
if not TRANSFORMERS_AVAILABLE:
|
| 691 |
+
return basic_sentiment_fallback(text)
|
| 692 |
+
|
| 693 |
+
for key in ["financial_sent_0", "financial_sent_1"]:
|
| 694 |
+
if key not in MODEL_SPECS:
|
| 695 |
+
continue
|
| 696 |
+
try:
|
| 697 |
+
pipe = _registry.get_pipeline(key)
|
| 698 |
+
res = pipe(text[:512])
|
| 699 |
+
if isinstance(res, list) and res:
|
| 700 |
+
res = res[0]
|
| 701 |
+
|
| 702 |
+
label = res.get("label", "neutral").upper()
|
| 703 |
+
score = res.get("score", 0.5)
|
| 704 |
+
|
| 705 |
+
mapped = "bullish" if "POSITIVE" in label or "LABEL_2" in label else (
|
| 706 |
+
"bearish" if "NEGATIVE" in label or "LABEL_0" in label else "neutral"
|
| 707 |
+
)
|
| 708 |
+
|
| 709 |
+
return {
|
| 710 |
+
"label": mapped, "score": score, "confidence": score,
|
| 711 |
+
"available": True, "engine": "huggingface",
|
| 712 |
+
"model": MODEL_SPECS[key].model_id
|
| 713 |
+
}
|
| 714 |
+
except ModelNotAvailable:
|
| 715 |
+
continue
|
| 716 |
+
except Exception as e:
|
| 717 |
+
logger.warning(f"Financial sentiment failed for {key}: {str(e)[:100]}")
|
| 718 |
+
continue
|
| 719 |
+
|
| 720 |
+
return basic_sentiment_fallback(text)
|
| 721 |
+
|
| 722 |
+
def analyze_social_sentiment(text: str):
|
| 723 |
+
if HF_MODE == "off":
|
| 724 |
+
return basic_sentiment_fallback(text)
|
| 725 |
+
if INFERENCE_API_MODE:
|
| 726 |
+
for model_id in SOCIAL_SENTIMENT_MODELS:
|
| 727 |
+
try:
|
| 728 |
+
return inference_classify(text, model_id)
|
| 729 |
+
except Exception:
|
| 730 |
+
continue
|
| 731 |
+
return basic_sentiment_fallback(text)
|
| 732 |
+
if not TRANSFORMERS_AVAILABLE:
|
| 733 |
+
return basic_sentiment_fallback(text)
|
| 734 |
+
|
| 735 |
+
for key in ["social_sent_0", "social_sent_1"]:
|
| 736 |
+
if key not in MODEL_SPECS:
|
| 737 |
+
continue
|
| 738 |
+
try:
|
| 739 |
+
pipe = _registry.get_pipeline(key)
|
| 740 |
+
res = pipe(text[:512])
|
| 741 |
+
if isinstance(res, list) and res:
|
| 742 |
+
res = res[0]
|
| 743 |
+
|
| 744 |
+
label = res.get("label", "neutral").upper()
|
| 745 |
+
score = res.get("score", 0.5)
|
| 746 |
+
|
| 747 |
+
mapped = "bullish" if "POSITIVE" in label or "LABEL_2" in label else (
|
| 748 |
+
"bearish" if "NEGATIVE" in label or "LABEL_0" in label else "neutral"
|
| 749 |
+
)
|
| 750 |
+
|
| 751 |
+
return {
|
| 752 |
+
"label": mapped, "score": score, "confidence": score,
|
| 753 |
+
"available": True, "engine": "huggingface",
|
| 754 |
+
"model": MODEL_SPECS[key].model_id
|
| 755 |
+
}
|
| 756 |
+
except ModelNotAvailable:
|
| 757 |
+
continue
|
| 758 |
+
except Exception as e:
|
| 759 |
+
logger.warning(f"Social sentiment failed for {key}: {str(e)[:100]}")
|
| 760 |
+
continue
|
| 761 |
+
|
| 762 |
+
return basic_sentiment_fallback(text)
|
| 763 |
+
|
| 764 |
+
def analyze_market_text(text: str):
|
| 765 |
+
return ensemble_crypto_sentiment(text)
|
| 766 |
+
|
| 767 |
+
def analyze_chart_points(data: Sequence[Mapping[str, Any]], indicators: Optional[List[str]] = None):
|
| 768 |
+
if not data:
|
| 769 |
+
return {"trend": "neutral", "strength": 0, "analysis": "No data"}
|
| 770 |
+
|
| 771 |
+
prices = [float(p.get("price", 0)) for p in data if p.get("price")]
|
| 772 |
+
if not prices:
|
| 773 |
+
return {"trend": "neutral", "strength": 0, "analysis": "No price data"}
|
| 774 |
+
|
| 775 |
+
first, last = prices[0], prices[-1]
|
| 776 |
+
change = ((last - first) / first * 100) if first > 0 else 0
|
| 777 |
+
|
| 778 |
+
if change > 5:
|
| 779 |
+
trend, strength = "bullish", min(abs(change) / 10, 1.0)
|
| 780 |
+
elif change < -5:
|
| 781 |
+
trend, strength = "bearish", min(abs(change) / 10, 1.0)
|
| 782 |
+
else:
|
| 783 |
+
trend, strength = "neutral", abs(change) / 5
|
| 784 |
+
|
| 785 |
+
return {
|
| 786 |
+
"trend": trend, "strength": strength, "change_pct": change,
|
| 787 |
+
"support": min(prices), "resistance": max(prices),
|
| 788 |
+
"analysis": f"Price moved {change:.2f}% showing {trend} trend"
|
| 789 |
+
}
|
| 790 |
+
|
| 791 |
+
def analyze_news_item(item: Dict[str, Any]):
|
| 792 |
+
text = item.get("title", "") + " " + item.get("description", "")
|
| 793 |
+
sent = ensemble_crypto_sentiment(text)
|
| 794 |
+
return {
|
| 795 |
+
**item,
|
| 796 |
+
"sentiment": sent["label"],
|
| 797 |
+
"sentiment_confidence": sent["confidence"],
|
| 798 |
+
"sentiment_details": sent
|
| 799 |
+
}
|
| 800 |
+
|
| 801 |
+
def get_model_info():
|
| 802 |
+
return {
|
| 803 |
+
"transformers_available": TRANSFORMERS_AVAILABLE,
|
| 804 |
+
"inference_api_mode": INFERENCE_API_MODE,
|
| 805 |
+
"hf_token_configured": bool(HF_TOKEN_ENV),
|
| 806 |
+
"hf_mode": HF_MODE,
|
| 807 |
+
"models_initialized": _registry._initialized,
|
| 808 |
+
"models_loaded": len(_registry._pipelines) + len(_registry._inference_ready),
|
| 809 |
+
"model_catalog": {
|
| 810 |
+
"crypto_sentiment": CRYPTO_SENTIMENT_MODELS,
|
| 811 |
+
"social_sentiment": SOCIAL_SENTIMENT_MODELS,
|
| 812 |
+
"financial_sentiment": FINANCIAL_SENTIMENT_MODELS,
|
| 813 |
+
"news_sentiment": NEWS_SENTIMENT_MODELS,
|
| 814 |
+
"generation": GENERATION_MODELS,
|
| 815 |
+
"trading_signals": TRADING_SIGNAL_MODELS,
|
| 816 |
+
"summarization": SUMMARIZATION_MODELS
|
| 817 |
+
},
|
| 818 |
+
"total_models": len(MODEL_SPECS)
|
| 819 |
+
}
|
| 820 |
+
|
| 821 |
+
def basic_sentiment_fallback(text: str) -> Dict[str, Any]:
|
| 822 |
+
text_lower = text.lower()
|
| 823 |
+
|
| 824 |
+
bullish_words = ["bullish", "rally", "surge", "pump", "breakout", "skyrocket",
|
| 825 |
+
"uptrend", "buy", "accumulation", "moon", "gain", "profit",
|
| 826 |
+
"up", "high", "rise", "growth", "positive", "strong"]
|
| 827 |
+
bearish_words = ["bearish", "dump", "crash", "selloff", "downtrend", "collapse",
|
| 828 |
+
"sell", "capitulation", "panic", "fear", "drop", "loss",
|
| 829 |
+
"down", "low", "fall", "decline", "negative", "weak"]
|
| 830 |
+
|
| 831 |
+
bullish_count = sum(1 for word in bullish_words if word in text_lower)
|
| 832 |
+
bearish_count = sum(1 for word in bearish_words if word in text_lower)
|
| 833 |
+
|
| 834 |
+
if bullish_count == 0 and bearish_count == 0:
|
| 835 |
+
label, confidence = "neutral", 0.5
|
| 836 |
+
bullish_score, bearish_score, neutral_score = 0.0, 0.0, 1.0
|
| 837 |
+
elif bullish_count > bearish_count:
|
| 838 |
+
label = "bullish"
|
| 839 |
+
diff = bullish_count - bearish_count
|
| 840 |
+
confidence = min(0.6 + (diff * 0.05), 0.9)
|
| 841 |
+
bullish_score, bearish_score, neutral_score = confidence, 0.0, 0.0
|
| 842 |
+
else:
|
| 843 |
+
label = "bearish"
|
| 844 |
+
diff = bearish_count - bullish_count
|
| 845 |
+
confidence = min(0.6 + (diff * 0.05), 0.9)
|
| 846 |
+
bearish_score, bullish_score, neutral_score = confidence, 0.0, 0.0
|
| 847 |
+
|
| 848 |
+
return {
|
| 849 |
+
"label": label,
|
| 850 |
+
"confidence": confidence,
|
| 851 |
+
"score": confidence,
|
| 852 |
+
"scores": {
|
| 853 |
+
"bullish": round(bullish_score, 3),
|
| 854 |
+
"bearish": round(bearish_score, 3),
|
| 855 |
+
"neutral": round(neutral_score, 3)
|
| 856 |
+
},
|
| 857 |
+
"available": True,
|
| 858 |
+
"engine": "fallback_lexical",
|
| 859 |
+
"keyword_matches": {
|
| 860 |
+
"bullish": bullish_count,
|
| 861 |
+
"bearish": bearish_count
|
| 862 |
+
}
|
| 863 |
+
}
|
| 864 |
+
|
| 865 |
+
def registry_status():
|
| 866 |
+
loaded = len(_registry._pipelines) + len(_registry._inference_ready)
|
| 867 |
+
status = {
|
| 868 |
+
"ok": HF_MODE != "off" and (loaded > 0 or INFERENCE_API_MODE),
|
| 869 |
+
"initialized": _registry._initialized,
|
| 870 |
+
"pipelines_loaded": loaded,
|
| 871 |
+
"pipelines_failed": len(_registry._failed_models),
|
| 872 |
+
"available_models": list(_registry._pipelines.keys()) + list(_registry._inference_ready),
|
| 873 |
+
"failed_models": list(_registry._failed_models.keys())[:10],
|
| 874 |
+
"transformers_available": TRANSFORMERS_AVAILABLE,
|
| 875 |
+
"inference_api_mode": INFERENCE_API_MODE,
|
| 876 |
+
"hf_mode": HF_MODE,
|
| 877 |
+
"total_specs": len(MODEL_SPECS)
|
| 878 |
+
}
|
| 879 |
+
|
| 880 |
+
if HF_MODE == "off":
|
| 881 |
+
status["error"] = "HF_MODE=off"
|
| 882 |
+
elif INFERENCE_API_MODE and loaded == 0 and _registry._initialized:
|
| 883 |
+
status["error"] = "Inference API ready but no models warmed"
|
| 884 |
+
elif not TRANSFORMERS_AVAILABLE and not INFERENCE_API_MODE:
|
| 885 |
+
status["error"] = "transformers not installed"
|
| 886 |
+
elif loaded == 0 and _registry._initialized:
|
| 887 |
+
status["error"] = "No models loaded yet (lazy loading)"
|
| 888 |
+
|
| 889 |
+
return status
|
all_apis_merged_2025.json
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
api-resources/crypto_resources_unified_2025-11-11.json
CHANGED
|
@@ -349,7 +349,7 @@
|
|
| 349 |
"base_url": "https://api.etherscan.io/api",
|
| 350 |
"auth": {
|
| 351 |
"type": "apiKeyQuery",
|
| 352 |
-
"key": "
|
| 353 |
"param_name": "apikey"
|
| 354 |
},
|
| 355 |
"docs_url": "https://docs.etherscan.io",
|
|
@@ -369,7 +369,7 @@
|
|
| 369 |
"base_url": "https://api.etherscan.io/api",
|
| 370 |
"auth": {
|
| 371 |
"type": "apiKeyQuery",
|
| 372 |
-
"key": "
|
| 373 |
"param_name": "apikey"
|
| 374 |
},
|
| 375 |
"docs_url": "https://docs.etherscan.io",
|
|
@@ -464,7 +464,7 @@
|
|
| 464 |
"base_url": "https://api.bscscan.com/api",
|
| 465 |
"auth": {
|
| 466 |
"type": "apiKeyQuery",
|
| 467 |
-
"key": "
|
| 468 |
"param_name": "apikey"
|
| 469 |
},
|
| 470 |
"docs_url": "https://docs.bscscan.com",
|
|
@@ -553,7 +553,7 @@
|
|
| 553 |
"base_url": "https://apilist.tronscanapi.com/api",
|
| 554 |
"auth": {
|
| 555 |
"type": "apiKeyQuery",
|
| 556 |
-
"key": "
|
| 557 |
"param_name": "apiKey"
|
| 558 |
},
|
| 559 |
"docs_url": "https://github.com/tronscan/tronscan-frontend/blob/dev2019/document/api.md",
|
|
@@ -651,7 +651,7 @@
|
|
| 651 |
"base_url": "https://pro-api.coinmarketcap.com/v1",
|
| 652 |
"auth": {
|
| 653 |
"type": "apiKeyHeader",
|
| 654 |
-
"key": "
|
| 655 |
"header_name": "X-CMC_PRO_API_KEY"
|
| 656 |
},
|
| 657 |
"docs_url": "https://coinmarketcap.com/api/documentation/v1/",
|
|
@@ -669,7 +669,7 @@
|
|
| 669 |
"base_url": "https://pro-api.coinmarketcap.com/v1",
|
| 670 |
"auth": {
|
| 671 |
"type": "apiKeyHeader",
|
| 672 |
-
"key": "
|
| 673 |
"header_name": "X-CMC_PRO_API_KEY"
|
| 674 |
},
|
| 675 |
"docs_url": "https://coinmarketcap.com/api/documentation/v1/",
|
|
@@ -687,7 +687,7 @@
|
|
| 687 |
"base_url": "https://min-api.cryptocompare.com/data",
|
| 688 |
"auth": {
|
| 689 |
"type": "apiKeyQuery",
|
| 690 |
-
"key": "
|
| 691 |
"param_name": "api_key"
|
| 692 |
},
|
| 693 |
"docs_url": "https://min-api.cryptocompare.com/documentation",
|
|
@@ -884,7 +884,7 @@
|
|
| 884 |
"base_url": "https://min-api.cryptocompare.com",
|
| 885 |
"auth": {
|
| 886 |
"type": "apiKeyQuery",
|
| 887 |
-
"key": "
|
| 888 |
"param_name": "api_key"
|
| 889 |
},
|
| 890 |
"docs_url": null,
|
|
@@ -982,7 +982,7 @@
|
|
| 982 |
"base_url": "https://newsapi.org/v2",
|
| 983 |
"auth": {
|
| 984 |
"type": "apiKeyQuery",
|
| 985 |
-
"key": "
|
| 986 |
"param_name": "apiKey"
|
| 987 |
},
|
| 988 |
"docs_url": "https://newsapi.org/docs",
|
|
@@ -1694,13 +1694,13 @@
|
|
| 1694 |
],
|
| 1695 |
"hf_resources": [
|
| 1696 |
{
|
| 1697 |
-
"id": "HF_TOKEN_FROM_SPACE_SECRET",
|
| 1698 |
"type": "model",
|
| 1699 |
"name": "ElKulako/CryptoBERT",
|
| 1700 |
"base_url": "https://api-inference.huggingface.co/models/ElKulako/cryptobert",
|
| 1701 |
"auth": {
|
| 1702 |
"type": "apiKeyHeaderOptional",
|
| 1703 |
-
"key": "HF_TOKEN_FROM_SPACE_SECRET",
|
| 1704 |
"header_name": "Authorization"
|
| 1705 |
},
|
| 1706 |
"docs_url": "https://huggingface.co/ElKulako/cryptobert",
|
|
@@ -1710,13 +1710,13 @@
|
|
| 1710 |
"notes": "For sentiment analysis"
|
| 1711 |
},
|
| 1712 |
{
|
| 1713 |
-
"id": "HF_TOKEN_FROM_SPACE_SECRET",
|
| 1714 |
"type": "model",
|
| 1715 |
"name": "kk08/CryptoBERT",
|
| 1716 |
"base_url": "https://api-inference.huggingface.co/models/kk08/CryptoBERT",
|
| 1717 |
"auth": {
|
| 1718 |
"type": "apiKeyHeaderOptional",
|
| 1719 |
-
"key": "HF_TOKEN_FROM_SPACE_SECRET",
|
| 1720 |
"header_name": "Authorization"
|
| 1721 |
},
|
| 1722 |
"docs_url": "https://huggingface.co/kk08/CryptoBERT",
|
|
@@ -1726,7 +1726,7 @@
|
|
| 1726 |
"notes": "For sentiment analysis"
|
| 1727 |
},
|
| 1728 |
{
|
| 1729 |
-
"id": "
|
| 1730 |
"type": "dataset",
|
| 1731 |
"name": "linxy/CryptoCoin",
|
| 1732 |
"base_url": "https://huggingface.co/datasets/linxy/CryptoCoin/resolve/main",
|
|
@@ -1740,7 +1740,7 @@
|
|
| 1740 |
"notes": "26 symbols x 7 timeframes = 182 CSVs"
|
| 1741 |
},
|
| 1742 |
{
|
| 1743 |
-
"id": "
|
| 1744 |
"type": "dataset",
|
| 1745 |
"name": "WinkingFace/CryptoLM-Bitcoin-BTC-USDT",
|
| 1746 |
"base_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Bitcoin-BTC-USDT/resolve/main",
|
|
@@ -1755,7 +1755,7 @@
|
|
| 1755 |
"notes": null
|
| 1756 |
},
|
| 1757 |
{
|
| 1758 |
-
"id": "
|
| 1759 |
"type": "dataset",
|
| 1760 |
"name": "WinkingFace/CryptoLM-Ethereum-ETH-USDT",
|
| 1761 |
"base_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Ethereum-ETH-USDT/resolve/main",
|
|
@@ -1770,7 +1770,7 @@
|
|
| 1770 |
"notes": null
|
| 1771 |
},
|
| 1772 |
{
|
| 1773 |
-
"id": "
|
| 1774 |
"type": "dataset",
|
| 1775 |
"name": "WinkingFace/CryptoLM-Solana-SOL-USDT",
|
| 1776 |
"base_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Solana-SOL-USDT/resolve/main",
|
|
@@ -1782,7 +1782,7 @@
|
|
| 1782 |
"notes": null
|
| 1783 |
},
|
| 1784 |
{
|
| 1785 |
-
"id": "
|
| 1786 |
"type": "dataset",
|
| 1787 |
"name": "WinkingFace/CryptoLM-Ripple-XRP-USDT",
|
| 1788 |
"base_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Ripple-XRP-USDT/resolve/main",
|
|
@@ -1862,7 +1862,7 @@
|
|
| 1862 |
"notes": null
|
| 1863 |
},
|
| 1864 |
{
|
| 1865 |
-
"id": "HF_TOKEN_FROM_SPACE_SECRET",
|
| 1866 |
"category": "hf-model",
|
| 1867 |
"name": "HF Model: ElKulako/CryptoBERT",
|
| 1868 |
"base_url": "https://huggingface.co/ElKulako/cryptobert",
|
|
@@ -1873,7 +1873,7 @@
|
|
| 1873 |
"notes": null
|
| 1874 |
},
|
| 1875 |
{
|
| 1876 |
-
"id": "HF_TOKEN_FROM_SPACE_SECRET",
|
| 1877 |
"category": "hf-model",
|
| 1878 |
"name": "HF Model: kk08/CryptoBERT",
|
| 1879 |
"base_url": "https://huggingface.co/kk08/CryptoBERT",
|
|
@@ -1884,7 +1884,7 @@
|
|
| 1884 |
"notes": null
|
| 1885 |
},
|
| 1886 |
{
|
| 1887 |
-
"id": "
|
| 1888 |
"category": "hf-dataset",
|
| 1889 |
"name": "HF Dataset: linxy/CryptoCoin",
|
| 1890 |
"base_url": "https://huggingface.co/datasets/linxy/CryptoCoin",
|
|
@@ -3183,15 +3183,15 @@
|
|
| 3183 |
"source_files": [
|
| 3184 |
{
|
| 3185 |
"path": "/mnt/data/api - Copy.txt",
|
| 3186 |
-
"sha256": "
|
| 3187 |
},
|
| 3188 |
{
|
| 3189 |
"path": "/mnt/data/api-config-complete (1).txt",
|
| 3190 |
-
"sha256": "
|
| 3191 |
},
|
| 3192 |
{
|
| 3193 |
"path": "/mnt/data/crypto_resources_ultimate_2025.zip",
|
| 3194 |
-
"sha256": "
|
| 3195 |
"note": "contains crypto_resources.ts and more"
|
| 3196 |
}
|
| 3197 |
]
|
|
|
|
| 349 |
"base_url": "https://api.etherscan.io/api",
|
| 350 |
"auth": {
|
| 351 |
"type": "apiKeyQuery",
|
| 352 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 353 |
"param_name": "apikey"
|
| 354 |
},
|
| 355 |
"docs_url": "https://docs.etherscan.io",
|
|
|
|
| 369 |
"base_url": "https://api.etherscan.io/api",
|
| 370 |
"auth": {
|
| 371 |
"type": "apiKeyQuery",
|
| 372 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 373 |
"param_name": "apikey"
|
| 374 |
},
|
| 375 |
"docs_url": "https://docs.etherscan.io",
|
|
|
|
| 464 |
"base_url": "https://api.bscscan.com/api",
|
| 465 |
"auth": {
|
| 466 |
"type": "apiKeyQuery",
|
| 467 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 468 |
"param_name": "apikey"
|
| 469 |
},
|
| 470 |
"docs_url": "https://docs.bscscan.com",
|
|
|
|
| 553 |
"base_url": "https://apilist.tronscanapi.com/api",
|
| 554 |
"auth": {
|
| 555 |
"type": "apiKeyQuery",
|
| 556 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 557 |
"param_name": "apiKey"
|
| 558 |
},
|
| 559 |
"docs_url": "https://github.com/tronscan/tronscan-frontend/blob/dev2019/document/api.md",
|
|
|
|
| 651 |
"base_url": "https://pro-api.coinmarketcap.com/v1",
|
| 652 |
"auth": {
|
| 653 |
"type": "apiKeyHeader",
|
| 654 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 655 |
"header_name": "X-CMC_PRO_API_KEY"
|
| 656 |
},
|
| 657 |
"docs_url": "https://coinmarketcap.com/api/documentation/v1/",
|
|
|
|
| 669 |
"base_url": "https://pro-api.coinmarketcap.com/v1",
|
| 670 |
"auth": {
|
| 671 |
"type": "apiKeyHeader",
|
| 672 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 673 |
"header_name": "X-CMC_PRO_API_KEY"
|
| 674 |
},
|
| 675 |
"docs_url": "https://coinmarketcap.com/api/documentation/v1/",
|
|
|
|
| 687 |
"base_url": "https://min-api.cryptocompare.com/data",
|
| 688 |
"auth": {
|
| 689 |
"type": "apiKeyQuery",
|
| 690 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 691 |
"param_name": "api_key"
|
| 692 |
},
|
| 693 |
"docs_url": "https://min-api.cryptocompare.com/documentation",
|
|
|
|
| 884 |
"base_url": "https://min-api.cryptocompare.com",
|
| 885 |
"auth": {
|
| 886 |
"type": "apiKeyQuery",
|
| 887 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 888 |
"param_name": "api_key"
|
| 889 |
},
|
| 890 |
"docs_url": null,
|
|
|
|
| 982 |
"base_url": "https://newsapi.org/v2",
|
| 983 |
"auth": {
|
| 984 |
"type": "apiKeyQuery",
|
| 985 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 986 |
"param_name": "apiKey"
|
| 987 |
},
|
| 988 |
"docs_url": "https://newsapi.org/docs",
|
|
|
|
| 1694 |
],
|
| 1695 |
"hf_resources": [
|
| 1696 |
{
|
| 1697 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1698 |
"type": "model",
|
| 1699 |
"name": "ElKulako/CryptoBERT",
|
| 1700 |
"base_url": "https://api-inference.huggingface.co/models/ElKulako/cryptobert",
|
| 1701 |
"auth": {
|
| 1702 |
"type": "apiKeyHeaderOptional",
|
| 1703 |
+
"key": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1704 |
"header_name": "Authorization"
|
| 1705 |
},
|
| 1706 |
"docs_url": "https://huggingface.co/ElKulako/cryptobert",
|
|
|
|
| 1710 |
"notes": "For sentiment analysis"
|
| 1711 |
},
|
| 1712 |
{
|
| 1713 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1714 |
"type": "model",
|
| 1715 |
"name": "kk08/CryptoBERT",
|
| 1716 |
"base_url": "https://api-inference.huggingface.co/models/kk08/CryptoBERT",
|
| 1717 |
"auth": {
|
| 1718 |
"type": "apiKeyHeaderOptional",
|
| 1719 |
+
"key": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1720 |
"header_name": "Authorization"
|
| 1721 |
},
|
| 1722 |
"docs_url": "https://huggingface.co/kk08/CryptoBERT",
|
|
|
|
| 1726 |
"notes": "For sentiment analysis"
|
| 1727 |
},
|
| 1728 |
{
|
| 1729 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1730 |
"type": "dataset",
|
| 1731 |
"name": "linxy/CryptoCoin",
|
| 1732 |
"base_url": "https://huggingface.co/datasets/linxy/CryptoCoin/resolve/main",
|
|
|
|
| 1740 |
"notes": "26 symbols x 7 timeframes = 182 CSVs"
|
| 1741 |
},
|
| 1742 |
{
|
| 1743 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1744 |
"type": "dataset",
|
| 1745 |
"name": "WinkingFace/CryptoLM-Bitcoin-BTC-USDT",
|
| 1746 |
"base_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Bitcoin-BTC-USDT/resolve/main",
|
|
|
|
| 1755 |
"notes": null
|
| 1756 |
},
|
| 1757 |
{
|
| 1758 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1759 |
"type": "dataset",
|
| 1760 |
"name": "WinkingFace/CryptoLM-Ethereum-ETH-USDT",
|
| 1761 |
"base_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Ethereum-ETH-USDT/resolve/main",
|
|
|
|
| 1770 |
"notes": null
|
| 1771 |
},
|
| 1772 |
{
|
| 1773 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1774 |
"type": "dataset",
|
| 1775 |
"name": "WinkingFace/CryptoLM-Solana-SOL-USDT",
|
| 1776 |
"base_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Solana-SOL-USDT/resolve/main",
|
|
|
|
| 1782 |
"notes": null
|
| 1783 |
},
|
| 1784 |
{
|
| 1785 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1786 |
"type": "dataset",
|
| 1787 |
"name": "WinkingFace/CryptoLM-Ripple-XRP-USDT",
|
| 1788 |
"base_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Ripple-XRP-USDT/resolve/main",
|
|
|
|
| 1862 |
"notes": null
|
| 1863 |
},
|
| 1864 |
{
|
| 1865 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1866 |
"category": "hf-model",
|
| 1867 |
"name": "HF Model: ElKulako/CryptoBERT",
|
| 1868 |
"base_url": "https://huggingface.co/ElKulako/cryptobert",
|
|
|
|
| 1873 |
"notes": null
|
| 1874 |
},
|
| 1875 |
{
|
| 1876 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1877 |
"category": "hf-model",
|
| 1878 |
"name": "HF Model: kk08/CryptoBERT",
|
| 1879 |
"base_url": "https://huggingface.co/kk08/CryptoBERT",
|
|
|
|
| 1884 |
"notes": null
|
| 1885 |
},
|
| 1886 |
{
|
| 1887 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1888 |
"category": "hf-dataset",
|
| 1889 |
"name": "HF Dataset: linxy/CryptoCoin",
|
| 1890 |
"base_url": "https://huggingface.co/datasets/linxy/CryptoCoin",
|
|
|
|
| 3183 |
"source_files": [
|
| 3184 |
{
|
| 3185 |
"path": "/mnt/data/api - Copy.txt",
|
| 3186 |
+
"sha256": "20f9a3357a65c28a691990f89ad57f0de978600e65405fafe2c8b3c3502f6b77"
|
| 3187 |
},
|
| 3188 |
{
|
| 3189 |
"path": "/mnt/data/api-config-complete (1).txt",
|
| 3190 |
+
"sha256": "cb9f4c746f5b8a1d70824340425557e4483ad7a8e5396e0be67d68d671b23697"
|
| 3191 |
},
|
| 3192 |
{
|
| 3193 |
"path": "/mnt/data/crypto_resources_ultimate_2025.zip",
|
| 3194 |
+
"sha256": "5bb6f0ef790f09e23a88adbf4a4c0bc225183e896c3aa63416e53b1eec36ea87",
|
| 3195 |
"note": "contains crypto_resources.ts and more"
|
| 3196 |
}
|
| 3197 |
]
|
api-resources/ultimate_crypto_pipeline_2025_NZasinich.json
CHANGED
|
@@ -1,4 +1,3 @@
|
|
| 1 |
-
ultimate_crypto_pipeline_2025_NZasinich.json
|
| 2 |
{
|
| 3 |
"user": {
|
| 4 |
"handle": "@NZasinich",
|
|
@@ -62,7 +61,7 @@ ultimate_crypto_pipeline_2025_NZasinich.json
|
|
| 62 |
"category": "Block Explorer",
|
| 63 |
"name": "TronScan",
|
| 64 |
"url": "https://api.tronscan.org/api",
|
| 65 |
-
"key": "
|
| 66 |
"free": false,
|
| 67 |
"desc": "TRON accounts."
|
| 68 |
},
|
|
@@ -87,7 +86,7 @@ ultimate_crypto_pipeline_2025_NZasinich.json
|
|
| 87 |
"category": "Block Explorer",
|
| 88 |
"name": "BscScan",
|
| 89 |
"url": "https://api.bscscan.com/api",
|
| 90 |
-
"key": "
|
| 91 |
"free": false,
|
| 92 |
"desc": "BSC balances."
|
| 93 |
},
|
|
@@ -111,7 +110,7 @@ ultimate_crypto_pipeline_2025_NZasinich.json
|
|
| 111 |
"category": "Block Explorer",
|
| 112 |
"name": "Etherscan",
|
| 113 |
"url": "https://api.etherscan.io/api",
|
| 114 |
-
"key": "
|
| 115 |
"free": false,
|
| 116 |
"desc": "ETH explorer."
|
| 117 |
},
|
|
@@ -119,7 +118,7 @@ ultimate_crypto_pipeline_2025_NZasinich.json
|
|
| 119 |
"category": "Block Explorer",
|
| 120 |
"name": "Etherscan Backup",
|
| 121 |
"url": "https://api.etherscan.io/api",
|
| 122 |
-
"key": "
|
| 123 |
"free": false,
|
| 124 |
"desc": "ETH backup."
|
| 125 |
},
|
|
@@ -252,7 +251,7 @@ ultimate_crypto_pipeline_2025_NZasinich.json
|
|
| 252 |
"category": "Market Data",
|
| 253 |
"name": "CoinMarketCap (User key)",
|
| 254 |
"url": "https://pro-api.coinmarketcap.com/v1",
|
| 255 |
-
"key": "
|
| 256 |
"free": false,
|
| 257 |
"rateLimit": "333/day"
|
| 258 |
},
|
|
@@ -483,7 +482,7 @@ ultimate_crypto_pipeline_2025_NZasinich.json
|
|
| 483 |
"content": "export interface CryptoResource { category: string; name: string; url: string; key: string; free: boolean; rateLimit?: string; desc: string; endpoint?: string; example?: string; params?: Record<string, any>; }\n\nexport const resources: CryptoResource[] = [ /* 162 items above */ ];\n\nexport async function callResource(resource: CryptoResource, customEndpoint?: string, params: Record<string, any> = {}): Promise<any> { let url = resource.url + (customEndpoint || resource.endpoint || ''); const query = new URLSearchParams(params).toString(); url += query ? `?${query}` : ''; const headers: HeadersInit = resource.key ? { Authorization: `Bearer ${resource.key}` } : {}; const res = await fetch(url, { headers }); if (!res.ok) throw new Error(`Failed: ${res.status}`); const data = await res.json(); if (!data || Object.keys(data).length === 0) throw new Error('Empty data'); return data; }\n\nexport function getResourcesByCategory(category: string): CryptoResource[] { return resources.filter(r => r.category === category); }"
|
| 484 |
},
|
| 485 |
{
|
| 486 |
-
"filename": "
|
| 487 |
"description": "Complete FastAPI + Hugging Face free data & sentiment pipeline (additive)",
|
| 488 |
"content": "from fastapi import FastAPI, APIRouter; from datasets import load_dataset; import pandas as pd; from transformers import pipeline; app = FastAPI(); router = APIRouter(prefix=\"/api/hf\"); # Full code from previous Cursor Agent prompt..."
|
| 489 |
},
|
|
@@ -500,4 +499,4 @@ ultimate_crypto_pipeline_2025_NZasinich.json
|
|
| 500 |
],
|
| 501 |
"total_files": 5,
|
| 502 |
"download_instructions": "Copy this entire JSON and save as `ultimate_crypto_pipeline_2025.json`. All code is ready to use. For TypeScript: `import { resources, callResource } from './crypto_resources_typescript.ts';`"
|
| 503 |
-
}
|
|
|
|
|
|
|
| 1 |
{
|
| 2 |
"user": {
|
| 3 |
"handle": "@NZasinich",
|
|
|
|
| 61 |
"category": "Block Explorer",
|
| 62 |
"name": "TronScan",
|
| 63 |
"url": "https://api.tronscan.org/api",
|
| 64 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 65 |
"free": false,
|
| 66 |
"desc": "TRON accounts."
|
| 67 |
},
|
|
|
|
| 86 |
"category": "Block Explorer",
|
| 87 |
"name": "BscScan",
|
| 88 |
"url": "https://api.bscscan.com/api",
|
| 89 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 90 |
"free": false,
|
| 91 |
"desc": "BSC balances."
|
| 92 |
},
|
|
|
|
| 110 |
"category": "Block Explorer",
|
| 111 |
"name": "Etherscan",
|
| 112 |
"url": "https://api.etherscan.io/api",
|
| 113 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 114 |
"free": false,
|
| 115 |
"desc": "ETH explorer."
|
| 116 |
},
|
|
|
|
| 118 |
"category": "Block Explorer",
|
| 119 |
"name": "Etherscan Backup",
|
| 120 |
"url": "https://api.etherscan.io/api",
|
| 121 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 122 |
"free": false,
|
| 123 |
"desc": "ETH backup."
|
| 124 |
},
|
|
|
|
| 251 |
"category": "Market Data",
|
| 252 |
"name": "CoinMarketCap (User key)",
|
| 253 |
"url": "https://pro-api.coinmarketcap.com/v1",
|
| 254 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 255 |
"free": false,
|
| 256 |
"rateLimit": "333/day"
|
| 257 |
},
|
|
|
|
| 482 |
"content": "export interface CryptoResource { category: string; name: string; url: string; key: string; free: boolean; rateLimit?: string; desc: string; endpoint?: string; example?: string; params?: Record<string, any>; }\n\nexport const resources: CryptoResource[] = [ /* 162 items above */ ];\n\nexport async function callResource(resource: CryptoResource, customEndpoint?: string, params: Record<string, any> = {}): Promise<any> { let url = resource.url + (customEndpoint || resource.endpoint || ''); const query = new URLSearchParams(params).toString(); url += query ? `?${query}` : ''; const headers: HeadersInit = resource.key ? { Authorization: `Bearer ${resource.key}` } : {}; const res = await fetch(url, { headers }); if (!res.ok) throw new Error(`Failed: ${res.status}`); const data = await res.json(); if (!data || Object.keys(data).length === 0) throw new Error('Empty data'); return data; }\n\nexport function getResourcesByCategory(category: string): CryptoResource[] { return resources.filter(r => r.category === category); }"
|
| 483 |
},
|
| 484 |
{
|
| 485 |
+
"filename": "hf_unified_server.py",
|
| 486 |
"description": "Complete FastAPI + Hugging Face free data & sentiment pipeline (additive)",
|
| 487 |
"content": "from fastapi import FastAPI, APIRouter; from datasets import load_dataset; import pandas as pd; from transformers import pipeline; app = FastAPI(); router = APIRouter(prefix=\"/api/hf\"); # Full code from previous Cursor Agent prompt..."
|
| 488 |
},
|
|
|
|
| 499 |
],
|
| 500 |
"total_files": 5,
|
| 501 |
"download_instructions": "Copy this entire JSON and save as `ultimate_crypto_pipeline_2025.json`. All code is ready to use. For TypeScript: `import { resources, callResource } from './crypto_resources_typescript.ts';`"
|
| 502 |
+
}
|
api/HF_IMPLEMENTATION_COMPLETE.md
CHANGED
|
@@ -1,237 +1,237 @@
|
|
| 1 |
-
# ✅ HuggingFace Integration - Implementation Complete
|
| 2 |
-
|
| 3 |
-
## 🎯 What Was Implemented
|
| 4 |
-
|
| 5 |
-
### Backend Components
|
| 6 |
-
|
| 7 |
-
#### 1. **HF Registry Service** (`backend/services/hf_registry.py`)
|
| 8 |
-
- Auto-discovery of crypto-related models and datasets from HuggingFace Hub
|
| 9 |
-
- Seed models and datasets (always available)
|
| 10 |
-
- Background auto-refresh every 6 hours
|
| 11 |
-
- Health monitoring with age tracking
|
| 12 |
-
- Configurable via environment variables
|
| 13 |
-
|
| 14 |
-
#### 2. **HF Client Service** (`backend/services/hf_client.py`)
|
| 15 |
-
- Local sentiment analysis using transformers
|
| 16 |
-
- Supports multiple models (ElKulako/cryptobert, kk08/CryptoBERT)
|
| 17 |
-
- Label-to-score conversion for crypto sentiment
|
| 18 |
-
- Caching for performance
|
| 19 |
-
- Enable/disable via environment variable
|
| 20 |
-
|
| 21 |
-
#### 3. **HF API Router** (`backend/routers/hf_connect.py`)
|
| 22 |
-
- `GET /api/hf/health` - Health status and registry info
|
| 23 |
-
- `POST /api/hf/refresh` - Force registry refresh
|
| 24 |
-
- `GET /api/hf/registry` - Get models or datasets list
|
| 25 |
-
- `GET /api/hf/search` - Search local snapshot
|
| 26 |
-
- `POST /api/hf/run-sentiment` - Run sentiment analysis
|
| 27 |
-
|
| 28 |
-
### Frontend Components
|
| 29 |
-
|
| 30 |
-
#### 1. **Main Dashboard Integration** (`index.html`)
|
| 31 |
-
- New "🤗 HuggingFace" tab added
|
| 32 |
-
- Health status display
|
| 33 |
-
- Models registry browser (with count badge)
|
| 34 |
-
- Datasets registry browser (with count badge)
|
| 35 |
-
- Search functionality (local snapshot)
|
| 36 |
-
- Sentiment analysis interface with vote display
|
| 37 |
-
- Real-time updates
|
| 38 |
-
- Responsive design matching existing UI
|
| 39 |
-
|
| 40 |
-
#### 2. **Standalone HF Console** (`hf_console.html`)
|
| 41 |
-
- Clean, focused interface for HF features
|
| 42 |
-
- RTL-compatible design
|
| 43 |
-
- All HF functionality in one page
|
| 44 |
-
- Perfect for testing and development
|
| 45 |
-
|
| 46 |
-
### Configuration Files
|
| 47 |
-
|
| 48 |
-
#### 1. **Environment Configuration** (`.env`)
|
| 49 |
-
```env
|
| 50 |
-
HUGGINGFACE_TOKEN=HF_TOKEN_FROM_SPACE_SECRET
|
| 51 |
-
ENABLE_SENTIMENT=true
|
| 52 |
-
SENTIMENT_SOCIAL_MODEL=ElKulako/cryptobert
|
| 53 |
-
SENTIMENT_NEWS_MODEL=kk08/CryptoBERT
|
| 54 |
-
HF_REGISTRY_REFRESH_SEC=21600
|
| 55 |
-
HF_HTTP_TIMEOUT=8.0
|
| 56 |
-
```
|
| 57 |
-
|
| 58 |
-
#### 2. **Dependencies** (`requirements.txt`)
|
| 59 |
-
```
|
| 60 |
-
httpx>=0.24
|
| 61 |
-
transformers>=4.44.0
|
| 62 |
-
datasets>=3.0.0
|
| 63 |
-
huggingface_hub>=0.24.0
|
| 64 |
-
torch>=2.0.0
|
| 65 |
-
```
|
| 66 |
-
|
| 67 |
-
### Testing & Deployment
|
| 68 |
-
|
| 69 |
-
#### 1. **Self-Test Script** (`free_resources_selftest.mjs`)
|
| 70 |
-
- Tests all free API endpoints
|
| 71 |
-
- Tests HF health, registry, and endpoints
|
| 72 |
-
- Validates backend connectivity
|
| 73 |
-
- Exit code 0 on success
|
| 74 |
-
|
| 75 |
-
#### 2. **PowerShell Test Script** (`test_free_endpoints.ps1`)
|
| 76 |
-
- Windows-native testing
|
| 77 |
-
- Same functionality as Node.js version
|
| 78 |
-
- Color-coded output
|
| 79 |
-
|
| 80 |
-
#### 3. **Simple Server** (`simple_server.py`)
|
| 81 |
-
- Lightweight FastAPI server
|
| 82 |
-
- HF integration without complex dependencies
|
| 83 |
-
- Serves static files (index.html, hf_console.html)
|
| 84 |
-
- Background registry refresh
|
| 85 |
-
- Easy to start and stop
|
| 86 |
-
|
| 87 |
-
### Package Scripts
|
| 88 |
-
|
| 89 |
-
Added to `package.json`:
|
| 90 |
-
```json
|
| 91 |
-
{
|
| 92 |
-
"scripts": {
|
| 93 |
-
"test:free-resources": "node free_resources_selftest.mjs",
|
| 94 |
-
"test:free-resources:win": "powershell -NoProfile -ExecutionPolicy Bypass -File test_free_endpoints.ps1"
|
| 95 |
-
}
|
| 96 |
-
}
|
| 97 |
-
```
|
| 98 |
-
|
| 99 |
-
## ✅ Acceptance Criteria - ALL PASSED
|
| 100 |
-
|
| 101 |
-
### 1. Registry Updater ✓
|
| 102 |
-
- `POST /api/hf/refresh` returns `{ok: true, models >= 2, datasets >= 4}`
|
| 103 |
-
- `GET /api/hf/health` includes all required fields
|
| 104 |
-
- Auto-refresh works in background
|
| 105 |
-
|
| 106 |
-
### 2. Snapshot Search ✓
|
| 107 |
-
- `GET /api/hf/registry?kind=models` includes seed models
|
| 108 |
-
- `GET /api/hf/registry?kind=datasets` includes seed datasets
|
| 109 |
-
- `GET /api/hf/search?q=crypto&kind=models` returns results
|
| 110 |
-
|
| 111 |
-
### 3. Local Sentiment Pipeline ✓
|
| 112 |
-
- `POST /api/hf/run-sentiment` with texts returns vote and samples
|
| 113 |
-
- Enabled/disabled via environment variable
|
| 114 |
-
- Model selection configurable
|
| 115 |
-
|
| 116 |
-
### 4. Background Auto-Refresh ✓
|
| 117 |
-
- Starts on server startup
|
| 118 |
-
- Refreshes every 6 hours (configurable)
|
| 119 |
-
- Age tracking in health endpoint
|
| 120 |
-
|
| 121 |
-
### 5. Self-Test ✓
|
| 122 |
-
- `node free_resources_selftest.mjs` exits with code 0
|
| 123 |
-
- Tests all required endpoints
|
| 124 |
-
- Windows PowerShell version available
|
| 125 |
-
|
| 126 |
-
### 6. UI Console ✓
|
| 127 |
-
- New HF tab in main dashboard
|
| 128 |
-
- Standalone HF console page
|
| 129 |
-
- RTL-compatible
|
| 130 |
-
- No breaking changes to existing UI
|
| 131 |
-
|
| 132 |
-
## 🚀 How to Run
|
| 133 |
-
|
| 134 |
-
### Start Server
|
| 135 |
-
```powershell
|
| 136 |
-
python simple_server.py
|
| 137 |
-
```
|
| 138 |
-
|
| 139 |
-
### Access Points
|
| 140 |
-
- **Main Dashboard:** http://localhost:7860/index.html
|
| 141 |
-
- **HF Console:** http://localhost:7860/hf_console.html
|
| 142 |
-
- **API Docs:** http://localhost:7860/docs
|
| 143 |
-
|
| 144 |
-
### Run Tests
|
| 145 |
-
```powershell
|
| 146 |
-
# Node.js version
|
| 147 |
-
npm run test:free-resources
|
| 148 |
-
|
| 149 |
-
# PowerShell version
|
| 150 |
-
npm run test:free-resources:win
|
| 151 |
-
```
|
| 152 |
-
|
| 153 |
-
## 📊 Current Status
|
| 154 |
-
|
| 155 |
-
### Server Status: ✅ RUNNING
|
| 156 |
-
- Process ID: 6
|
| 157 |
-
- Port: 7860
|
| 158 |
-
- Health: http://localhost:7860/health
|
| 159 |
-
- HF Health: http://localhost:7860/api/hf/health
|
| 160 |
-
|
| 161 |
-
### Registry Status: ✅ ACTIVE
|
| 162 |
-
- Models: 2 (seed) + auto-discovered
|
| 163 |
-
- Datasets: 5 (seed) + auto-discovered
|
| 164 |
-
- Last Refresh: Active
|
| 165 |
-
- Auto-Refresh: Every 6 hours
|
| 166 |
-
|
| 167 |
-
### Features Status: ✅ ALL WORKING
|
| 168 |
-
- ✅ Health monitoring
|
| 169 |
-
- ✅ Registry browsing
|
| 170 |
-
- ✅ Search functionality
|
| 171 |
-
- ✅ Sentiment analysis
|
| 172 |
-
- ✅ Background refresh
|
| 173 |
-
- ✅ API documentation
|
| 174 |
-
- ✅ Frontend integration
|
| 175 |
-
|
| 176 |
-
## 🎯 Key Features
|
| 177 |
-
|
| 178 |
-
### Free Resources Only
|
| 179 |
-
- No paid APIs required
|
| 180 |
-
- Uses public HuggingFace Hub API
|
| 181 |
-
- Local transformers for sentiment
|
| 182 |
-
- Free tier rate limits respected
|
| 183 |
-
|
| 184 |
-
### Auto-Refresh
|
| 185 |
-
- Background task runs every 6 hours
|
| 186 |
-
- Configurable interval
|
| 187 |
-
- Manual refresh available via UI or API
|
| 188 |
-
|
| 189 |
-
### Minimal & Additive
|
| 190 |
-
- No changes to existing architecture
|
| 191 |
-
- No breaking changes to current UI
|
| 192 |
-
- Graceful fallback if HF unavailable
|
| 193 |
-
- Optional sentiment analysis
|
| 194 |
-
|
| 195 |
-
### Production Ready
|
| 196 |
-
- Error handling
|
| 197 |
-
- Health monitoring
|
| 198 |
-
- Logging
|
| 199 |
-
- Configuration via environment
|
| 200 |
-
- Self-tests included
|
| 201 |
-
|
| 202 |
-
## 📝 Files Created/Modified
|
| 203 |
-
|
| 204 |
-
### Created:
|
| 205 |
-
- `backend/routers/hf_connect.py`
|
| 206 |
-
- `backend/services/hf_registry.py`
|
| 207 |
-
- `backend/services/hf_client.py`
|
| 208 |
-
- `backend/__init__.py`
|
| 209 |
-
- `backend/routers/__init__.py`
|
| 210 |
-
- `backend/services/__init__.py`
|
| 211 |
-
- `database/__init__.py`
|
| 212 |
-
- `hf_console.html`
|
| 213 |
-
- `free_resources_selftest.mjs`
|
| 214 |
-
- `test_free_endpoints.ps1`
|
| 215 |
-
- `simple_server.py`
|
| 216 |
-
- `start_server.py`
|
| 217 |
-
- `.env`
|
| 218 |
-
- `.env.example`
|
| 219 |
-
- `QUICK_START.md`
|
| 220 |
-
- `HF_IMPLEMENTATION_COMPLETE.md`
|
| 221 |
-
|
| 222 |
-
### Modified:
|
| 223 |
-
- `index.html` (added HF tab and JavaScript functions)
|
| 224 |
-
- `requirements.txt` (added HF dependencies)
|
| 225 |
-
- `package.json` (added test scripts)
|
| 226 |
-
- `app.py` (integrated HF router and background task)
|
| 227 |
-
|
| 228 |
-
## 🎉 Success!
|
| 229 |
-
|
| 230 |
-
The HuggingFace integration is complete and fully functional. All acceptance criteria have been met, and the application is running successfully on port 7860.
|
| 231 |
-
|
| 232 |
-
**Next Steps:**
|
| 233 |
-
1. Open http://localhost:7860/index.html in your browser
|
| 234 |
-
2. Click the "🤗 HuggingFace" tab
|
| 235 |
-
3. Explore the features!
|
| 236 |
-
|
| 237 |
-
Enjoy your new HuggingFace-powered crypto sentiment analysis! 🚀
|
|
|
|
| 1 |
+
# ✅ HuggingFace Integration - Implementation Complete
|
| 2 |
+
|
| 3 |
+
## 🎯 What Was Implemented
|
| 4 |
+
|
| 5 |
+
### Backend Components
|
| 6 |
+
|
| 7 |
+
#### 1. **HF Registry Service** (`backend/services/hf_registry.py`)
|
| 8 |
+
- Auto-discovery of crypto-related models and datasets from HuggingFace Hub
|
| 9 |
+
- Seed models and datasets (always available)
|
| 10 |
+
- Background auto-refresh every 6 hours
|
| 11 |
+
- Health monitoring with age tracking
|
| 12 |
+
- Configurable via environment variables
|
| 13 |
+
|
| 14 |
+
#### 2. **HF Client Service** (`backend/services/hf_client.py`)
|
| 15 |
+
- Local sentiment analysis using transformers
|
| 16 |
+
- Supports multiple models (ElKulako/cryptobert, kk08/CryptoBERT)
|
| 17 |
+
- Label-to-score conversion for crypto sentiment
|
| 18 |
+
- Caching for performance
|
| 19 |
+
- Enable/disable via environment variable
|
| 20 |
+
|
| 21 |
+
#### 3. **HF API Router** (`backend/routers/hf_connect.py`)
|
| 22 |
+
- `GET /api/hf/health` - Health status and registry info
|
| 23 |
+
- `POST /api/hf/refresh` - Force registry refresh
|
| 24 |
+
- `GET /api/hf/registry` - Get models or datasets list
|
| 25 |
+
- `GET /api/hf/search` - Search local snapshot
|
| 26 |
+
- `POST /api/hf/run-sentiment` - Run sentiment analysis
|
| 27 |
+
|
| 28 |
+
### Frontend Components
|
| 29 |
+
|
| 30 |
+
#### 1. **Main Dashboard Integration** (`index.html`)
|
| 31 |
+
- New "🤗 HuggingFace" tab added
|
| 32 |
+
- Health status display
|
| 33 |
+
- Models registry browser (with count badge)
|
| 34 |
+
- Datasets registry browser (with count badge)
|
| 35 |
+
- Search functionality (local snapshot)
|
| 36 |
+
- Sentiment analysis interface with vote display
|
| 37 |
+
- Real-time updates
|
| 38 |
+
- Responsive design matching existing UI
|
| 39 |
+
|
| 40 |
+
#### 2. **Standalone HF Console** (`hf_console.html`)
|
| 41 |
+
- Clean, focused interface for HF features
|
| 42 |
+
- RTL-compatible design
|
| 43 |
+
- All HF functionality in one page
|
| 44 |
+
- Perfect for testing and development
|
| 45 |
+
|
| 46 |
+
### Configuration Files
|
| 47 |
+
|
| 48 |
+
#### 1. **Environment Configuration** (`.env`)
|
| 49 |
+
```env
|
| 50 |
+
HUGGINGFACE_TOKEN=<HF_TOKEN_FROM_SPACE_SECRET>
|
| 51 |
+
ENABLE_SENTIMENT=true
|
| 52 |
+
SENTIMENT_SOCIAL_MODEL=ElKulako/cryptobert
|
| 53 |
+
SENTIMENT_NEWS_MODEL=kk08/CryptoBERT
|
| 54 |
+
HF_REGISTRY_REFRESH_SEC=21600
|
| 55 |
+
HF_HTTP_TIMEOUT=8.0
|
| 56 |
+
```
|
| 57 |
+
|
| 58 |
+
#### 2. **Dependencies** (`requirements.txt`)
|
| 59 |
+
```
|
| 60 |
+
httpx>=0.24
|
| 61 |
+
transformers>=4.44.0
|
| 62 |
+
datasets>=3.0.0
|
| 63 |
+
huggingface_hub>=0.24.0
|
| 64 |
+
torch>=2.0.0
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
### Testing & Deployment
|
| 68 |
+
|
| 69 |
+
#### 1. **Self-Test Script** (`free_resources_selftest.mjs`)
|
| 70 |
+
- Tests all free API endpoints
|
| 71 |
+
- Tests HF health, registry, and endpoints
|
| 72 |
+
- Validates backend connectivity
|
| 73 |
+
- Exit code 0 on success
|
| 74 |
+
|
| 75 |
+
#### 2. **PowerShell Test Script** (`test_free_endpoints.ps1`)
|
| 76 |
+
- Windows-native testing
|
| 77 |
+
- Same functionality as Node.js version
|
| 78 |
+
- Color-coded output
|
| 79 |
+
|
| 80 |
+
#### 3. **Simple Server** (`simple_server.py`)
|
| 81 |
+
- Lightweight FastAPI server
|
| 82 |
+
- HF integration without complex dependencies
|
| 83 |
+
- Serves static files (index.html, hf_console.html)
|
| 84 |
+
- Background registry refresh
|
| 85 |
+
- Easy to start and stop
|
| 86 |
+
|
| 87 |
+
### Package Scripts
|
| 88 |
+
|
| 89 |
+
Added to `package.json`:
|
| 90 |
+
```json
|
| 91 |
+
{
|
| 92 |
+
"scripts": {
|
| 93 |
+
"test:free-resources": "node free_resources_selftest.mjs",
|
| 94 |
+
"test:free-resources:win": "powershell -NoProfile -ExecutionPolicy Bypass -File test_free_endpoints.ps1"
|
| 95 |
+
}
|
| 96 |
+
}
|
| 97 |
+
```
|
| 98 |
+
|
| 99 |
+
## ✅ Acceptance Criteria - ALL PASSED
|
| 100 |
+
|
| 101 |
+
### 1. Registry Updater ✓
|
| 102 |
+
- `POST /api/hf/refresh` returns `{ok: true, models >= 2, datasets >= 4}`
|
| 103 |
+
- `GET /api/hf/health` includes all required fields
|
| 104 |
+
- Auto-refresh works in background
|
| 105 |
+
|
| 106 |
+
### 2. Snapshot Search ✓
|
| 107 |
+
- `GET /api/hf/registry?kind=models` includes seed models
|
| 108 |
+
- `GET /api/hf/registry?kind=datasets` includes seed datasets
|
| 109 |
+
- `GET /api/hf/search?q=crypto&kind=models` returns results
|
| 110 |
+
|
| 111 |
+
### 3. Local Sentiment Pipeline ✓
|
| 112 |
+
- `POST /api/hf/run-sentiment` with texts returns vote and samples
|
| 113 |
+
- Enabled/disabled via environment variable
|
| 114 |
+
- Model selection configurable
|
| 115 |
+
|
| 116 |
+
### 4. Background Auto-Refresh ✓
|
| 117 |
+
- Starts on server startup
|
| 118 |
+
- Refreshes every 6 hours (configurable)
|
| 119 |
+
- Age tracking in health endpoint
|
| 120 |
+
|
| 121 |
+
### 5. Self-Test ✓
|
| 122 |
+
- `node free_resources_selftest.mjs` exits with code 0
|
| 123 |
+
- Tests all required endpoints
|
| 124 |
+
- Windows PowerShell version available
|
| 125 |
+
|
| 126 |
+
### 6. UI Console ✓
|
| 127 |
+
- New HF tab in main dashboard
|
| 128 |
+
- Standalone HF console page
|
| 129 |
+
- RTL-compatible
|
| 130 |
+
- No breaking changes to existing UI
|
| 131 |
+
|
| 132 |
+
## 🚀 How to Run
|
| 133 |
+
|
| 134 |
+
### Start Server
|
| 135 |
+
```powershell
|
| 136 |
+
python simple_server.py
|
| 137 |
+
```
|
| 138 |
+
|
| 139 |
+
### Access Points
|
| 140 |
+
- **Main Dashboard:** http://localhost:7860/index.html
|
| 141 |
+
- **HF Console:** http://localhost:7860/hf_console.html
|
| 142 |
+
- **API Docs:** http://localhost:7860/docs
|
| 143 |
+
|
| 144 |
+
### Run Tests
|
| 145 |
+
```powershell
|
| 146 |
+
# Node.js version
|
| 147 |
+
npm run test:free-resources
|
| 148 |
+
|
| 149 |
+
# PowerShell version
|
| 150 |
+
npm run test:free-resources:win
|
| 151 |
+
```
|
| 152 |
+
|
| 153 |
+
## 📊 Current Status
|
| 154 |
+
|
| 155 |
+
### Server Status: ✅ RUNNING
|
| 156 |
+
- Process ID: 6
|
| 157 |
+
- Port: 7860
|
| 158 |
+
- Health: http://localhost:7860/health
|
| 159 |
+
- HF Health: http://localhost:7860/api/hf/health
|
| 160 |
+
|
| 161 |
+
### Registry Status: ✅ ACTIVE
|
| 162 |
+
- Models: 2 (seed) + auto-discovered
|
| 163 |
+
- Datasets: 5 (seed) + auto-discovered
|
| 164 |
+
- Last Refresh: Active
|
| 165 |
+
- Auto-Refresh: Every 6 hours
|
| 166 |
+
|
| 167 |
+
### Features Status: ✅ ALL WORKING
|
| 168 |
+
- ✅ Health monitoring
|
| 169 |
+
- ✅ Registry browsing
|
| 170 |
+
- ✅ Search functionality
|
| 171 |
+
- ✅ Sentiment analysis
|
| 172 |
+
- ✅ Background refresh
|
| 173 |
+
- ✅ API documentation
|
| 174 |
+
- ✅ Frontend integration
|
| 175 |
+
|
| 176 |
+
## 🎯 Key Features
|
| 177 |
+
|
| 178 |
+
### Free Resources Only
|
| 179 |
+
- No paid APIs required
|
| 180 |
+
- Uses public HuggingFace Hub API
|
| 181 |
+
- Local transformers for sentiment
|
| 182 |
+
- Free tier rate limits respected
|
| 183 |
+
|
| 184 |
+
### Auto-Refresh
|
| 185 |
+
- Background task runs every 6 hours
|
| 186 |
+
- Configurable interval
|
| 187 |
+
- Manual refresh available via UI or API
|
| 188 |
+
|
| 189 |
+
### Minimal & Additive
|
| 190 |
+
- No changes to existing architecture
|
| 191 |
+
- No breaking changes to current UI
|
| 192 |
+
- Graceful fallback if HF unavailable
|
| 193 |
+
- Optional sentiment analysis
|
| 194 |
+
|
| 195 |
+
### Production Ready
|
| 196 |
+
- Error handling
|
| 197 |
+
- Health monitoring
|
| 198 |
+
- Logging
|
| 199 |
+
- Configuration via environment
|
| 200 |
+
- Self-tests included
|
| 201 |
+
|
| 202 |
+
## 📝 Files Created/Modified
|
| 203 |
+
|
| 204 |
+
### Created:
|
| 205 |
+
- `backend/routers/hf_connect.py`
|
| 206 |
+
- `backend/services/hf_registry.py`
|
| 207 |
+
- `backend/services/hf_client.py`
|
| 208 |
+
- `backend/__init__.py`
|
| 209 |
+
- `backend/routers/__init__.py`
|
| 210 |
+
- `backend/services/__init__.py`
|
| 211 |
+
- `database/__init__.py`
|
| 212 |
+
- `hf_console.html`
|
| 213 |
+
- `free_resources_selftest.mjs`
|
| 214 |
+
- `test_free_endpoints.ps1`
|
| 215 |
+
- `simple_server.py`
|
| 216 |
+
- `start_server.py`
|
| 217 |
+
- `.env`
|
| 218 |
+
- `.env.example`
|
| 219 |
+
- `QUICK_START.md`
|
| 220 |
+
- `HF_IMPLEMENTATION_COMPLETE.md`
|
| 221 |
+
|
| 222 |
+
### Modified:
|
| 223 |
+
- `index.html` (added HF tab and JavaScript functions)
|
| 224 |
+
- `requirements.txt` (added HF dependencies)
|
| 225 |
+
- `package.json` (added test scripts)
|
| 226 |
+
- `app.py` (integrated HF router and background task)
|
| 227 |
+
|
| 228 |
+
## 🎉 Success!
|
| 229 |
+
|
| 230 |
+
The HuggingFace integration is complete and fully functional. All acceptance criteria have been met, and the application is running successfully on port 7860.
|
| 231 |
+
|
| 232 |
+
**Next Steps:**
|
| 233 |
+
1. Open http://localhost:7860/index.html in your browser
|
| 234 |
+
2. Click the "🤗 HuggingFace" tab
|
| 235 |
+
3. Explore the features!
|
| 236 |
+
|
| 237 |
+
Enjoy your new HuggingFace-powered crypto sentiment analysis! 🚀
|
api/PRODUCTION_AUDIT_COMPREHENSIVE.md
CHANGED
|
@@ -127,7 +127,7 @@ crypto-dt-source/
|
|
| 127 |
|
| 128 |
1. **Etherscan** (Ethereum)
|
| 129 |
- Endpoint: `https://api.etherscan.io/api`
|
| 130 |
-
- Keys Available: 2 (
|
| 131 |
- Rate Limit: 5 calls/sec
|
| 132 |
- Implemented: ✅ `get_etherscan_gas_price()`
|
| 133 |
- Data: Gas prices, account balances, transactions, token balances
|
|
@@ -135,14 +135,14 @@ crypto-dt-source/
|
|
| 135 |
|
| 136 |
2. **BscScan** (Binance Smart Chain)
|
| 137 |
- Endpoint: `https://api.bscscan.com/api`
|
| 138 |
-
- Key Available:
|
| 139 |
- Rate Limit: 5 calls/sec
|
| 140 |
- Implemented: ✅ `get_bscscan_bnb_price()`
|
| 141 |
- **Real Data:** Yes
|
| 142 |
|
| 143 |
3. **TronScan** (TRON Network)
|
| 144 |
- Endpoint: `https://apilist.tronscanapi.com/api`
|
| 145 |
-
- Key Available:
|
| 146 |
- Implemented: ✅ `get_tronscan_stats()`
|
| 147 |
- **Real Data:** Yes
|
| 148 |
|
|
@@ -170,7 +170,7 @@ crypto-dt-source/
|
|
| 170 |
|
| 171 |
2. **NewsAPI.org** (REQUIRES KEY)
|
| 172 |
- Endpoint: `https://newsdata.io/api/1`
|
| 173 |
-
- Key Available: `NEWSAPI_KEY_FROM_SPACE_SECRET`
|
| 174 |
- Free tier: 100 req/day
|
| 175 |
- Implemented: ✅ `get_newsapi_headlines()`
|
| 176 |
- **Real Data:** Yes (API key required)
|
|
@@ -1034,18 +1034,18 @@ ALCHEMY_KEY= # Alchemy RPC
|
|
| 1034 |
**Available in Code:**
|
| 1035 |
```python
|
| 1036 |
# Blockchain Explorers - KEYS PROVIDED
|
| 1037 |
-
ETHERSCAN_KEY_1 = "
|
| 1038 |
-
ETHERSCAN_KEY_2 = "
|
| 1039 |
-
BSCSCAN_KEY = "
|
| 1040 |
-
TRONSCAN_KEY = "
|
| 1041 |
|
| 1042 |
# Market Data - KEYS PROVIDED
|
| 1043 |
-
COINMARKETCAP_KEY_1 = "
|
| 1044 |
-
COINMARKETCAP_KEY_2 = "
|
| 1045 |
-
CRYPTOCOMPARE_KEY = "
|
| 1046 |
|
| 1047 |
# News - KEY PROVIDED
|
| 1048 |
-
NEWSAPI_KEY = "NEWSAPI_KEY_FROM_SPACE_SECRET"
|
| 1049 |
```
|
| 1050 |
|
| 1051 |
**Status:** ✅ KEYS ARE EMBEDDED IN CONFIG
|
|
|
|
| 127 |
|
| 128 |
1. **Etherscan** (Ethereum)
|
| 129 |
- Endpoint: `https://api.etherscan.io/api`
|
| 130 |
+
- Keys Available: 2 (<REDACTED_API_KEY>, T6IR8VJHX2NE...)
|
| 131 |
- Rate Limit: 5 calls/sec
|
| 132 |
- Implemented: ✅ `get_etherscan_gas_price()`
|
| 133 |
- Data: Gas prices, account balances, transactions, token balances
|
|
|
|
| 135 |
|
| 136 |
2. **BscScan** (Binance Smart Chain)
|
| 137 |
- Endpoint: `https://api.bscscan.com/api`
|
| 138 |
+
- Key Available: <REDACTED_API_KEY>
|
| 139 |
- Rate Limit: 5 calls/sec
|
| 140 |
- Implemented: ✅ `get_bscscan_bnb_price()`
|
| 141 |
- **Real Data:** Yes
|
| 142 |
|
| 143 |
3. **TronScan** (TRON Network)
|
| 144 |
- Endpoint: `https://apilist.tronscanapi.com/api`
|
| 145 |
+
- Key Available: <REDACTED_API_KEY>
|
| 146 |
- Implemented: ✅ `get_tronscan_stats()`
|
| 147 |
- **Real Data:** Yes
|
| 148 |
|
|
|
|
| 170 |
|
| 171 |
2. **NewsAPI.org** (REQUIRES KEY)
|
| 172 |
- Endpoint: `https://newsdata.io/api/1`
|
| 173 |
+
- Key Available: `<NEWSAPI_KEY_FROM_SPACE_SECRET>`
|
| 174 |
- Free tier: 100 req/day
|
| 175 |
- Implemented: ✅ `get_newsapi_headlines()`
|
| 176 |
- **Real Data:** Yes (API key required)
|
|
|
|
| 1034 |
**Available in Code:**
|
| 1035 |
```python
|
| 1036 |
# Blockchain Explorers - KEYS PROVIDED
|
| 1037 |
+
ETHERSCAN_KEY_1 = "<REDACTED_API_KEY>"
|
| 1038 |
+
ETHERSCAN_KEY_2 = "<REDACTED_API_KEY>"
|
| 1039 |
+
BSCSCAN_KEY = "<REDACTED_API_KEY>"
|
| 1040 |
+
TRONSCAN_KEY = "<REDACTED_API_KEY>"
|
| 1041 |
|
| 1042 |
# Market Data - KEYS PROVIDED
|
| 1043 |
+
COINMARKETCAP_KEY_1 = "<REDACTED_API_KEY>"
|
| 1044 |
+
COINMARKETCAP_KEY_2 = "<REDACTED_API_KEY>"
|
| 1045 |
+
CRYPTOCOMPARE_KEY = "<REDACTED_API_KEY>"
|
| 1046 |
|
| 1047 |
# News - KEY PROVIDED
|
| 1048 |
+
NEWSAPI_KEY = "<NEWSAPI_KEY_FROM_SPACE_SECRET>"
|
| 1049 |
```
|
| 1050 |
|
| 1051 |
**Status:** ✅ KEYS ARE EMBEDDED IN CONFIG
|
api/PRODUCTION_READY.md
CHANGED
|
@@ -16,9 +16,9 @@ Your production crypto API monitoring system is now running with:
|
|
| 16 |
- And more...
|
| 17 |
|
| 18 |
2. **Your API Keys Integrated**
|
| 19 |
-
- Etherscan:
|
| 20 |
-
- BscScan:
|
| 21 |
-
- TronScan:
|
| 22 |
- CoinMarketCap: 2 keys loaded
|
| 23 |
- CryptoCompare: Key loaded
|
| 24 |
|
|
|
|
| 16 |
- And more...
|
| 17 |
|
| 18 |
2. **Your API Keys Integrated**
|
| 19 |
+
- Etherscan: <ETHERSCAN_API_KEY_FROM_SPACE_SECRET>
|
| 20 |
+
- BscScan: <BSCSCAN_API_KEY_FROM_SPACE_SECRET>
|
| 21 |
+
- TronScan: <REDACTED_API_KEY>
|
| 22 |
- CoinMarketCap: 2 keys loaded
|
| 23 |
- CryptoCompare: Key loaded
|
| 24 |
|
api/QUICK_START.md
CHANGED
|
@@ -1,182 +1,182 @@
|
|
| 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=HF_TOKEN_FROM_SPACE_SECRET
|
| 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
|
|
|
|
| 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=<HF_TOKEN_FROM_SPACE_SECRET>
|
| 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
|
api/api/ws_integration_services.py
CHANGED
|
@@ -1,334 +1,334 @@
|
|
| 1 |
-
"""
|
| 2 |
-
WebSocket API for Integration Services
|
| 3 |
-
|
| 4 |
-
This module provides WebSocket endpoints for integration services
|
| 5 |
-
including HuggingFace AI models and persistence operations.
|
| 6 |
-
"""
|
| 7 |
-
|
| 8 |
-
import asyncio
|
| 9 |
-
from datetime import datetime
|
| 10 |
-
from typing import Any, Dict
|
| 11 |
-
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
| 12 |
-
import logging
|
| 13 |
-
|
| 14 |
-
from backend.services.ws_service_manager import ws_manager, ServiceType
|
| 15 |
-
from backend.services.hf_registry import HFRegistry
|
| 16 |
-
from backend.services.hf_client import HFClient
|
| 17 |
-
from backend.services.persistence_service import PersistenceService
|
| 18 |
-
from config import Config
|
| 19 |
-
|
| 20 |
-
logger = logging.getLogger(__name__)
|
| 21 |
-
|
| 22 |
-
router = APIRouter()
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
# ============================================================================
|
| 26 |
-
# Integration Service Handlers
|
| 27 |
-
# ============================================================================
|
| 28 |
-
|
| 29 |
-
class IntegrationStreamers:
|
| 30 |
-
"""Handles data streaming for integration services"""
|
| 31 |
-
|
| 32 |
-
def __init__(self):
|
| 33 |
-
self.config = Config()
|
| 34 |
-
try:
|
| 35 |
-
self.hf_registry = HFRegistry()
|
| 36 |
-
except:
|
| 37 |
-
self.hf_registry = None
|
| 38 |
-
logger.warning("HFRegistry not available")
|
| 39 |
-
|
| 40 |
-
try:
|
| 41 |
-
self.hf_client = HFClient()
|
| 42 |
-
except:
|
| 43 |
-
self.hf_client = None
|
| 44 |
-
logger.warning("HFClient not available")
|
| 45 |
-
|
| 46 |
-
try:
|
| 47 |
-
self.persistence_service = PersistenceService()
|
| 48 |
-
except:
|
| 49 |
-
self.persistence_service = None
|
| 50 |
-
logger.warning("PersistenceService not available")
|
| 51 |
-
|
| 52 |
-
# ========================================================================
|
| 53 |
-
# HuggingFace Streaming
|
| 54 |
-
# ========================================================================
|
| 55 |
-
|
| 56 |
-
async def
|
| 57 |
-
"""Stream HuggingFace registry status"""
|
| 58 |
-
if not self.hf_registry:
|
| 59 |
-
return None
|
| 60 |
-
|
| 61 |
-
try:
|
| 62 |
-
status = self.hf_registry.get_status()
|
| 63 |
-
if status:
|
| 64 |
-
return {
|
| 65 |
-
"total_models": status.get("total_models", 0),
|
| 66 |
-
"total_datasets": status.get("total_datasets", 0),
|
| 67 |
-
"available_models": status.get("available_models", []),
|
| 68 |
-
"available_datasets": status.get("available_datasets", []),
|
| 69 |
-
"last_refresh": status.get("last_refresh"),
|
| 70 |
-
"timestamp": datetime.utcnow().isoformat()
|
| 71 |
-
}
|
| 72 |
-
except Exception as e:
|
| 73 |
-
logger.error(f"Error streaming HF registry status: {e}")
|
| 74 |
-
return None
|
| 75 |
-
|
| 76 |
-
async def stream_hf_model_usage(self):
|
| 77 |
-
"""Stream HuggingFace model usage statistics"""
|
| 78 |
-
if not self.hf_client:
|
| 79 |
-
return None
|
| 80 |
-
|
| 81 |
-
try:
|
| 82 |
-
usage = self.hf_client.get_usage_stats()
|
| 83 |
-
if usage:
|
| 84 |
-
return {
|
| 85 |
-
"total_requests": usage.get("total_requests", 0),
|
| 86 |
-
"successful_requests": usage.get("successful_requests", 0),
|
| 87 |
-
"failed_requests": usage.get("failed_requests", 0),
|
| 88 |
-
"average_latency": usage.get("average_latency"),
|
| 89 |
-
"model_usage": usage.get("model_usage", {}),
|
| 90 |
-
"timestamp": datetime.utcnow().isoformat()
|
| 91 |
-
}
|
| 92 |
-
except Exception as e:
|
| 93 |
-
logger.error(f"Error streaming HF model usage: {e}")
|
| 94 |
-
return None
|
| 95 |
-
|
| 96 |
-
async def stream_sentiment_results(self):
|
| 97 |
-
"""Stream real-time sentiment analysis results"""
|
| 98 |
-
if not self.hf_client:
|
| 99 |
-
return None
|
| 100 |
-
|
| 101 |
-
try:
|
| 102 |
-
# This would stream sentiment results as they're processed
|
| 103 |
-
results = self.hf_client.get_recent_results()
|
| 104 |
-
if results:
|
| 105 |
-
return {
|
| 106 |
-
"sentiment_results": results,
|
| 107 |
-
"timestamp": datetime.utcnow().isoformat()
|
| 108 |
-
}
|
| 109 |
-
except Exception as e:
|
| 110 |
-
logger.error(f"Error streaming sentiment results: {e}")
|
| 111 |
-
return None
|
| 112 |
-
|
| 113 |
-
async def stream_model_events(self):
|
| 114 |
-
"""Stream model loading and unloading events"""
|
| 115 |
-
if not self.hf_registry:
|
| 116 |
-
return None
|
| 117 |
-
|
| 118 |
-
try:
|
| 119 |
-
events = self.hf_registry.get_recent_events()
|
| 120 |
-
if events:
|
| 121 |
-
return {
|
| 122 |
-
"model_events": events,
|
| 123 |
-
"timestamp": datetime.utcnow().isoformat()
|
| 124 |
-
}
|
| 125 |
-
except Exception as e:
|
| 126 |
-
logger.error(f"Error streaming model events: {e}")
|
| 127 |
-
return None
|
| 128 |
-
|
| 129 |
-
# ========================================================================
|
| 130 |
-
# Persistence Service Streaming
|
| 131 |
-
# ========================================================================
|
| 132 |
-
|
| 133 |
-
async def stream_persistence_status(self):
|
| 134 |
-
"""Stream persistence service status"""
|
| 135 |
-
if not self.persistence_service:
|
| 136 |
-
return None
|
| 137 |
-
|
| 138 |
-
try:
|
| 139 |
-
status = self.persistence_service.get_status()
|
| 140 |
-
if status:
|
| 141 |
-
return {
|
| 142 |
-
"storage_location": status.get("storage_location"),
|
| 143 |
-
"total_records": status.get("total_records", 0),
|
| 144 |
-
"storage_size": status.get("storage_size"),
|
| 145 |
-
"last_save": status.get("last_save"),
|
| 146 |
-
"active_writers": status.get("active_writers", 0),
|
| 147 |
-
"timestamp": datetime.utcnow().isoformat()
|
| 148 |
-
}
|
| 149 |
-
except Exception as e:
|
| 150 |
-
logger.error(f"Error streaming persistence status: {e}")
|
| 151 |
-
return None
|
| 152 |
-
|
| 153 |
-
async def stream_save_events(self):
|
| 154 |
-
"""Stream data save events"""
|
| 155 |
-
if not self.persistence_service:
|
| 156 |
-
return None
|
| 157 |
-
|
| 158 |
-
try:
|
| 159 |
-
events = self.persistence_service.get_recent_saves()
|
| 160 |
-
if events:
|
| 161 |
-
return {
|
| 162 |
-
"save_events": events,
|
| 163 |
-
"timestamp": datetime.utcnow().isoformat()
|
| 164 |
-
}
|
| 165 |
-
except Exception as e:
|
| 166 |
-
logger.error(f"Error streaming save events: {e}")
|
| 167 |
-
return None
|
| 168 |
-
|
| 169 |
-
async def stream_export_progress(self):
|
| 170 |
-
"""Stream export operation progress"""
|
| 171 |
-
if not self.persistence_service:
|
| 172 |
-
return None
|
| 173 |
-
|
| 174 |
-
try:
|
| 175 |
-
progress = self.persistence_service.get_export_progress()
|
| 176 |
-
if progress:
|
| 177 |
-
return {
|
| 178 |
-
"export_operations": progress,
|
| 179 |
-
"timestamp": datetime.utcnow().isoformat()
|
| 180 |
-
}
|
| 181 |
-
except Exception as e:
|
| 182 |
-
logger.error(f"Error streaming export progress: {e}")
|
| 183 |
-
return None
|
| 184 |
-
|
| 185 |
-
async def stream_backup_events(self):
|
| 186 |
-
"""Stream backup creation events"""
|
| 187 |
-
if not self.persistence_service:
|
| 188 |
-
return None
|
| 189 |
-
|
| 190 |
-
try:
|
| 191 |
-
backups = self.persistence_service.get_recent_backups()
|
| 192 |
-
if backups:
|
| 193 |
-
return {
|
| 194 |
-
"backup_events": backups,
|
| 195 |
-
"timestamp": datetime.utcnow().isoformat()
|
| 196 |
-
}
|
| 197 |
-
except Exception as e:
|
| 198 |
-
logger.error(f"Error streaming backup events: {e}")
|
| 199 |
-
return None
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
# Global instance
|
| 203 |
-
integration_streamers = IntegrationStreamers()
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
# ============================================================================
|
| 207 |
-
# Background Streaming Tasks
|
| 208 |
-
# ============================================================================
|
| 209 |
-
|
| 210 |
-
async def start_integration_streams():
|
| 211 |
-
"""Start all integration stream tasks"""
|
| 212 |
-
logger.info("Starting integration WebSocket streams")
|
| 213 |
-
|
| 214 |
-
tasks = [
|
| 215 |
-
# HuggingFace Registry
|
| 216 |
-
asyncio.create_task(ws_manager.start_service_stream(
|
| 217 |
-
ServiceType.HUGGINGFACE,
|
| 218 |
-
integration_streamers.
|
| 219 |
-
interval=60.0 # 1 minute updates
|
| 220 |
-
)),
|
| 221 |
-
|
| 222 |
-
# Persistence Service
|
| 223 |
-
asyncio.create_task(ws_manager.start_service_stream(
|
| 224 |
-
ServiceType.PERSISTENCE,
|
| 225 |
-
integration_streamers.stream_persistence_status,
|
| 226 |
-
interval=30.0 # 30 second updates
|
| 227 |
-
)),
|
| 228 |
-
]
|
| 229 |
-
|
| 230 |
-
await asyncio.gather(*tasks, return_exceptions=True)
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
# ============================================================================
|
| 234 |
-
# WebSocket Endpoints
|
| 235 |
-
# ============================================================================
|
| 236 |
-
|
| 237 |
-
@router.websocket("/ws/integration")
|
| 238 |
-
async def websocket_integration_endpoint(websocket: WebSocket):
|
| 239 |
-
"""
|
| 240 |
-
Unified WebSocket endpoint for all integration services
|
| 241 |
-
|
| 242 |
-
Connection URL: ws://host:port/ws/integration
|
| 243 |
-
|
| 244 |
-
After connecting, send subscription messages:
|
| 245 |
-
{
|
| 246 |
-
"action": "subscribe",
|
| 247 |
-
"service": "huggingface" | "persistence" | "all"
|
| 248 |
-
}
|
| 249 |
-
|
| 250 |
-
To unsubscribe:
|
| 251 |
-
{
|
| 252 |
-
"action": "unsubscribe",
|
| 253 |
-
"service": "service_name"
|
| 254 |
-
}
|
| 255 |
-
"""
|
| 256 |
-
connection = await ws_manager.connect(websocket)
|
| 257 |
-
|
| 258 |
-
try:
|
| 259 |
-
while True:
|
| 260 |
-
data = await websocket.receive_json()
|
| 261 |
-
await ws_manager.handle_client_message(connection, data)
|
| 262 |
-
|
| 263 |
-
except WebSocketDisconnect:
|
| 264 |
-
logger.info(f"Integration client disconnected: {connection.client_id}")
|
| 265 |
-
except Exception as e:
|
| 266 |
-
logger.error(f"Integration WebSocket error: {e}")
|
| 267 |
-
finally:
|
| 268 |
-
await ws_manager.disconnect(connection.client_id)
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
@router.websocket("/ws/huggingface")
|
| 272 |
-
async def websocket_huggingface(websocket: WebSocket):
|
| 273 |
-
"""
|
| 274 |
-
Dedicated WebSocket endpoint for HuggingFace services
|
| 275 |
-
|
| 276 |
-
Auto-subscribes to huggingface service
|
| 277 |
-
"""
|
| 278 |
-
connection = await ws_manager.connect(websocket)
|
| 279 |
-
connection.subscribe(ServiceType.HUGGINGFACE)
|
| 280 |
-
|
| 281 |
-
try:
|
| 282 |
-
while True:
|
| 283 |
-
data = await websocket.receive_json()
|
| 284 |
-
await ws_manager.handle_client_message(connection, data)
|
| 285 |
-
except WebSocketDisconnect:
|
| 286 |
-
logger.info(f"HuggingFace client disconnected: {connection.client_id}")
|
| 287 |
-
except Exception as e:
|
| 288 |
-
logger.error(f"HuggingFace WebSocket error: {e}")
|
| 289 |
-
finally:
|
| 290 |
-
await ws_manager.disconnect(connection.client_id)
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
@router.websocket("/ws/persistence")
|
| 294 |
-
async def websocket_persistence(websocket: WebSocket):
|
| 295 |
-
"""
|
| 296 |
-
Dedicated WebSocket endpoint for persistence service
|
| 297 |
-
|
| 298 |
-
Auto-subscribes to persistence service
|
| 299 |
-
"""
|
| 300 |
-
connection = await ws_manager.connect(websocket)
|
| 301 |
-
connection.subscribe(ServiceType.PERSISTENCE)
|
| 302 |
-
|
| 303 |
-
try:
|
| 304 |
-
while True:
|
| 305 |
-
data = await websocket.receive_json()
|
| 306 |
-
await ws_manager.handle_client_message(connection, data)
|
| 307 |
-
except WebSocketDisconnect:
|
| 308 |
-
logger.info(f"Persistence client disconnected: {connection.client_id}")
|
| 309 |
-
except Exception as e:
|
| 310 |
-
logger.error(f"Persistence WebSocket error: {e}")
|
| 311 |
-
finally:
|
| 312 |
-
await ws_manager.disconnect(connection.client_id)
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
@router.websocket("/ws/ai")
|
| 316 |
-
async def websocket_ai(websocket: WebSocket):
|
| 317 |
-
"""
|
| 318 |
-
Dedicated WebSocket endpoint for AI/ML operations (alias for HuggingFace)
|
| 319 |
-
|
| 320 |
-
Auto-subscribes to huggingface service
|
| 321 |
-
"""
|
| 322 |
-
connection = await ws_manager.connect(websocket)
|
| 323 |
-
connection.subscribe(ServiceType.HUGGINGFACE)
|
| 324 |
-
|
| 325 |
-
try:
|
| 326 |
-
while True:
|
| 327 |
-
data = await websocket.receive_json()
|
| 328 |
-
await ws_manager.handle_client_message(connection, data)
|
| 329 |
-
except WebSocketDisconnect:
|
| 330 |
-
logger.info(f"AI client disconnected: {connection.client_id}")
|
| 331 |
-
except Exception as e:
|
| 332 |
-
logger.error(f"AI WebSocket error: {e}")
|
| 333 |
-
finally:
|
| 334 |
-
await ws_manager.disconnect(connection.client_id)
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
WebSocket API for Integration Services
|
| 3 |
+
|
| 4 |
+
This module provides WebSocket endpoints for integration services
|
| 5 |
+
including HuggingFace AI models and persistence operations.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import asyncio
|
| 9 |
+
from datetime import datetime
|
| 10 |
+
from typing import Any, Dict
|
| 11 |
+
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
| 12 |
+
import logging
|
| 13 |
+
|
| 14 |
+
from backend.services.ws_service_manager import ws_manager, ServiceType
|
| 15 |
+
from backend.services.hf_registry import HFRegistry
|
| 16 |
+
from backend.services.hf_client import HFClient
|
| 17 |
+
from backend.services.persistence_service import PersistenceService
|
| 18 |
+
from config import Config
|
| 19 |
+
|
| 20 |
+
logger = logging.getLogger(__name__)
|
| 21 |
+
|
| 22 |
+
router = APIRouter()
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
# ============================================================================
|
| 26 |
+
# Integration Service Handlers
|
| 27 |
+
# ============================================================================
|
| 28 |
+
|
| 29 |
+
class IntegrationStreamers:
|
| 30 |
+
"""Handles data streaming for integration services"""
|
| 31 |
+
|
| 32 |
+
def __init__(self):
|
| 33 |
+
self.config = Config()
|
| 34 |
+
try:
|
| 35 |
+
self.hf_registry = HFRegistry()
|
| 36 |
+
except:
|
| 37 |
+
self.hf_registry = None
|
| 38 |
+
logger.warning("HFRegistry not available")
|
| 39 |
+
|
| 40 |
+
try:
|
| 41 |
+
self.hf_client = HFClient()
|
| 42 |
+
except:
|
| 43 |
+
self.hf_client = None
|
| 44 |
+
logger.warning("HFClient not available")
|
| 45 |
+
|
| 46 |
+
try:
|
| 47 |
+
self.persistence_service = PersistenceService()
|
| 48 |
+
except:
|
| 49 |
+
self.persistence_service = None
|
| 50 |
+
logger.warning("PersistenceService not available")
|
| 51 |
+
|
| 52 |
+
# ========================================================================
|
| 53 |
+
# HuggingFace Streaming
|
| 54 |
+
# ========================================================================
|
| 55 |
+
|
| 56 |
+
async def stream_hf(self):
|
| 57 |
+
"""Stream HuggingFace registry status"""
|
| 58 |
+
if not self.hf_registry:
|
| 59 |
+
return None
|
| 60 |
+
|
| 61 |
+
try:
|
| 62 |
+
status = self.hf_registry.get_status()
|
| 63 |
+
if status:
|
| 64 |
+
return {
|
| 65 |
+
"total_models": status.get("total_models", 0),
|
| 66 |
+
"total_datasets": status.get("total_datasets", 0),
|
| 67 |
+
"available_models": status.get("available_models", []),
|
| 68 |
+
"available_datasets": status.get("available_datasets", []),
|
| 69 |
+
"last_refresh": status.get("last_refresh"),
|
| 70 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 71 |
+
}
|
| 72 |
+
except Exception as e:
|
| 73 |
+
logger.error(f"Error streaming HF registry status: {e}")
|
| 74 |
+
return None
|
| 75 |
+
|
| 76 |
+
async def stream_hf_model_usage(self):
|
| 77 |
+
"""Stream HuggingFace model usage statistics"""
|
| 78 |
+
if not self.hf_client:
|
| 79 |
+
return None
|
| 80 |
+
|
| 81 |
+
try:
|
| 82 |
+
usage = self.hf_client.get_usage_stats()
|
| 83 |
+
if usage:
|
| 84 |
+
return {
|
| 85 |
+
"total_requests": usage.get("total_requests", 0),
|
| 86 |
+
"successful_requests": usage.get("successful_requests", 0),
|
| 87 |
+
"failed_requests": usage.get("failed_requests", 0),
|
| 88 |
+
"average_latency": usage.get("average_latency"),
|
| 89 |
+
"model_usage": usage.get("model_usage", {}),
|
| 90 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 91 |
+
}
|
| 92 |
+
except Exception as e:
|
| 93 |
+
logger.error(f"Error streaming HF model usage: {e}")
|
| 94 |
+
return None
|
| 95 |
+
|
| 96 |
+
async def stream_sentiment_results(self):
|
| 97 |
+
"""Stream real-time sentiment analysis results"""
|
| 98 |
+
if not self.hf_client:
|
| 99 |
+
return None
|
| 100 |
+
|
| 101 |
+
try:
|
| 102 |
+
# This would stream sentiment results as they're processed
|
| 103 |
+
results = self.hf_client.get_recent_results()
|
| 104 |
+
if results:
|
| 105 |
+
return {
|
| 106 |
+
"sentiment_results": results,
|
| 107 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 108 |
+
}
|
| 109 |
+
except Exception as e:
|
| 110 |
+
logger.error(f"Error streaming sentiment results: {e}")
|
| 111 |
+
return None
|
| 112 |
+
|
| 113 |
+
async def stream_model_events(self):
|
| 114 |
+
"""Stream model loading and unloading events"""
|
| 115 |
+
if not self.hf_registry:
|
| 116 |
+
return None
|
| 117 |
+
|
| 118 |
+
try:
|
| 119 |
+
events = self.hf_registry.get_recent_events()
|
| 120 |
+
if events:
|
| 121 |
+
return {
|
| 122 |
+
"model_events": events,
|
| 123 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 124 |
+
}
|
| 125 |
+
except Exception as e:
|
| 126 |
+
logger.error(f"Error streaming model events: {e}")
|
| 127 |
+
return None
|
| 128 |
+
|
| 129 |
+
# ========================================================================
|
| 130 |
+
# Persistence Service Streaming
|
| 131 |
+
# ========================================================================
|
| 132 |
+
|
| 133 |
+
async def stream_persistence_status(self):
|
| 134 |
+
"""Stream persistence service status"""
|
| 135 |
+
if not self.persistence_service:
|
| 136 |
+
return None
|
| 137 |
+
|
| 138 |
+
try:
|
| 139 |
+
status = self.persistence_service.get_status()
|
| 140 |
+
if status:
|
| 141 |
+
return {
|
| 142 |
+
"storage_location": status.get("storage_location"),
|
| 143 |
+
"total_records": status.get("total_records", 0),
|
| 144 |
+
"storage_size": status.get("storage_size"),
|
| 145 |
+
"last_save": status.get("last_save"),
|
| 146 |
+
"active_writers": status.get("active_writers", 0),
|
| 147 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 148 |
+
}
|
| 149 |
+
except Exception as e:
|
| 150 |
+
logger.error(f"Error streaming persistence status: {e}")
|
| 151 |
+
return None
|
| 152 |
+
|
| 153 |
+
async def stream_save_events(self):
|
| 154 |
+
"""Stream data save events"""
|
| 155 |
+
if not self.persistence_service:
|
| 156 |
+
return None
|
| 157 |
+
|
| 158 |
+
try:
|
| 159 |
+
events = self.persistence_service.get_recent_saves()
|
| 160 |
+
if events:
|
| 161 |
+
return {
|
| 162 |
+
"save_events": events,
|
| 163 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 164 |
+
}
|
| 165 |
+
except Exception as e:
|
| 166 |
+
logger.error(f"Error streaming save events: {e}")
|
| 167 |
+
return None
|
| 168 |
+
|
| 169 |
+
async def stream_export_progress(self):
|
| 170 |
+
"""Stream export operation progress"""
|
| 171 |
+
if not self.persistence_service:
|
| 172 |
+
return None
|
| 173 |
+
|
| 174 |
+
try:
|
| 175 |
+
progress = self.persistence_service.get_export_progress()
|
| 176 |
+
if progress:
|
| 177 |
+
return {
|
| 178 |
+
"export_operations": progress,
|
| 179 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 180 |
+
}
|
| 181 |
+
except Exception as e:
|
| 182 |
+
logger.error(f"Error streaming export progress: {e}")
|
| 183 |
+
return None
|
| 184 |
+
|
| 185 |
+
async def stream_backup_events(self):
|
| 186 |
+
"""Stream backup creation events"""
|
| 187 |
+
if not self.persistence_service:
|
| 188 |
+
return None
|
| 189 |
+
|
| 190 |
+
try:
|
| 191 |
+
backups = self.persistence_service.get_recent_backups()
|
| 192 |
+
if backups:
|
| 193 |
+
return {
|
| 194 |
+
"backup_events": backups,
|
| 195 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 196 |
+
}
|
| 197 |
+
except Exception as e:
|
| 198 |
+
logger.error(f"Error streaming backup events: {e}")
|
| 199 |
+
return None
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
# Global instance
|
| 203 |
+
integration_streamers = IntegrationStreamers()
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
# ============================================================================
|
| 207 |
+
# Background Streaming Tasks
|
| 208 |
+
# ============================================================================
|
| 209 |
+
|
| 210 |
+
async def start_integration_streams():
|
| 211 |
+
"""Start all integration stream tasks"""
|
| 212 |
+
logger.info("Starting integration WebSocket streams")
|
| 213 |
+
|
| 214 |
+
tasks = [
|
| 215 |
+
# HuggingFace Registry
|
| 216 |
+
asyncio.create_task(ws_manager.start_service_stream(
|
| 217 |
+
ServiceType.HUGGINGFACE,
|
| 218 |
+
integration_streamers.stream_hf,
|
| 219 |
+
interval=60.0 # 1 minute updates
|
| 220 |
+
)),
|
| 221 |
+
|
| 222 |
+
# Persistence Service
|
| 223 |
+
asyncio.create_task(ws_manager.start_service_stream(
|
| 224 |
+
ServiceType.PERSISTENCE,
|
| 225 |
+
integration_streamers.stream_persistence_status,
|
| 226 |
+
interval=30.0 # 30 second updates
|
| 227 |
+
)),
|
| 228 |
+
]
|
| 229 |
+
|
| 230 |
+
await asyncio.gather(*tasks, return_exceptions=True)
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
# ============================================================================
|
| 234 |
+
# WebSocket Endpoints
|
| 235 |
+
# ============================================================================
|
| 236 |
+
|
| 237 |
+
@router.websocket("/ws/integration")
|
| 238 |
+
async def websocket_integration_endpoint(websocket: WebSocket):
|
| 239 |
+
"""
|
| 240 |
+
Unified WebSocket endpoint for all integration services
|
| 241 |
+
|
| 242 |
+
Connection URL: ws://host:port/ws/integration
|
| 243 |
+
|
| 244 |
+
After connecting, send subscription messages:
|
| 245 |
+
{
|
| 246 |
+
"action": "subscribe",
|
| 247 |
+
"service": "huggingface" | "persistence" | "all"
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
To unsubscribe:
|
| 251 |
+
{
|
| 252 |
+
"action": "unsubscribe",
|
| 253 |
+
"service": "service_name"
|
| 254 |
+
}
|
| 255 |
+
"""
|
| 256 |
+
connection = await ws_manager.connect(websocket)
|
| 257 |
+
|
| 258 |
+
try:
|
| 259 |
+
while True:
|
| 260 |
+
data = await websocket.receive_json()
|
| 261 |
+
await ws_manager.handle_client_message(connection, data)
|
| 262 |
+
|
| 263 |
+
except WebSocketDisconnect:
|
| 264 |
+
logger.info(f"Integration client disconnected: {connection.client_id}")
|
| 265 |
+
except Exception as e:
|
| 266 |
+
logger.error(f"Integration WebSocket error: {e}")
|
| 267 |
+
finally:
|
| 268 |
+
await ws_manager.disconnect(connection.client_id)
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
@router.websocket("/ws/huggingface")
|
| 272 |
+
async def websocket_huggingface(websocket: WebSocket):
|
| 273 |
+
"""
|
| 274 |
+
Dedicated WebSocket endpoint for HuggingFace services
|
| 275 |
+
|
| 276 |
+
Auto-subscribes to huggingface service
|
| 277 |
+
"""
|
| 278 |
+
connection = await ws_manager.connect(websocket)
|
| 279 |
+
connection.subscribe(ServiceType.HUGGINGFACE)
|
| 280 |
+
|
| 281 |
+
try:
|
| 282 |
+
while True:
|
| 283 |
+
data = await websocket.receive_json()
|
| 284 |
+
await ws_manager.handle_client_message(connection, data)
|
| 285 |
+
except WebSocketDisconnect:
|
| 286 |
+
logger.info(f"HuggingFace client disconnected: {connection.client_id}")
|
| 287 |
+
except Exception as e:
|
| 288 |
+
logger.error(f"HuggingFace WebSocket error: {e}")
|
| 289 |
+
finally:
|
| 290 |
+
await ws_manager.disconnect(connection.client_id)
|
| 291 |
+
|
| 292 |
+
|
| 293 |
+
@router.websocket("/ws/persistence")
|
| 294 |
+
async def websocket_persistence(websocket: WebSocket):
|
| 295 |
+
"""
|
| 296 |
+
Dedicated WebSocket endpoint for persistence service
|
| 297 |
+
|
| 298 |
+
Auto-subscribes to persistence service
|
| 299 |
+
"""
|
| 300 |
+
connection = await ws_manager.connect(websocket)
|
| 301 |
+
connection.subscribe(ServiceType.PERSISTENCE)
|
| 302 |
+
|
| 303 |
+
try:
|
| 304 |
+
while True:
|
| 305 |
+
data = await websocket.receive_json()
|
| 306 |
+
await ws_manager.handle_client_message(connection, data)
|
| 307 |
+
except WebSocketDisconnect:
|
| 308 |
+
logger.info(f"Persistence client disconnected: {connection.client_id}")
|
| 309 |
+
except Exception as e:
|
| 310 |
+
logger.error(f"Persistence WebSocket error: {e}")
|
| 311 |
+
finally:
|
| 312 |
+
await ws_manager.disconnect(connection.client_id)
|
| 313 |
+
|
| 314 |
+
|
| 315 |
+
@router.websocket("/ws/ai")
|
| 316 |
+
async def websocket_ai(websocket: WebSocket):
|
| 317 |
+
"""
|
| 318 |
+
Dedicated WebSocket endpoint for AI/ML operations (alias for HuggingFace)
|
| 319 |
+
|
| 320 |
+
Auto-subscribes to huggingface service
|
| 321 |
+
"""
|
| 322 |
+
connection = await ws_manager.connect(websocket)
|
| 323 |
+
connection.subscribe(ServiceType.HUGGINGFACE)
|
| 324 |
+
|
| 325 |
+
try:
|
| 326 |
+
while True:
|
| 327 |
+
data = await websocket.receive_json()
|
| 328 |
+
await ws_manager.handle_client_message(connection, data)
|
| 329 |
+
except WebSocketDisconnect:
|
| 330 |
+
logger.info(f"AI client disconnected: {connection.client_id}")
|
| 331 |
+
except Exception as e:
|
| 332 |
+
logger.error(f"AI WebSocket error: {e}")
|
| 333 |
+
finally:
|
| 334 |
+
await ws_manager.disconnect(connection.client_id)
|
api/backend/routers/hf_connect.py
CHANGED
|
@@ -1,35 +1,35 @@
|
|
| 1 |
-
from __future__ import annotations
|
| 2 |
-
from fastapi import APIRouter, Query, Body
|
| 3 |
-
from typing import Literal, List
|
| 4 |
-
from backend.services.hf_registry import REGISTRY
|
| 5 |
-
from backend.services.hf_client import run_sentiment
|
| 6 |
-
|
| 7 |
-
router = APIRouter(prefix="/api/hf", tags=["huggingface"])
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
@router.get("/health")
|
| 11 |
-
async def hf_health():
|
| 12 |
-
return REGISTRY.health()
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
@router.post("/refresh")
|
| 16 |
-
async def hf_refresh():
|
| 17 |
-
return await REGISTRY.refresh()
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
@router.get("/registry")
|
| 21 |
-
async def hf_registry(kind: Literal["models","datasets"]="models"):
|
| 22 |
-
return {"kind": kind, "items": REGISTRY.list(kind)}
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
@router.get("/search")
|
| 26 |
-
async def hf_search(q: str = Query("crypto"), kind: Literal["models","datasets"]="models"):
|
| 27 |
-
hay = REGISTRY.list(kind)
|
| 28 |
-
ql = q.lower()
|
| 29 |
-
res = [x for x in hay if ql in (x.get("id","").lower() + " " + " ".join([str(t) for t in x.get("tags",[])]).lower())]
|
| 30 |
-
return {"query": q, "kind": kind, "count": len(res), "items": res[:50]}
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
@router.post("/run-sentiment")
|
| 34 |
-
async def
|
| 35 |
-
return run_sentiment(texts, model=model)
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
from fastapi import APIRouter, Query, Body
|
| 3 |
+
from typing import Literal, List
|
| 4 |
+
from backend.services.hf_registry import REGISTRY
|
| 5 |
+
from backend.services.hf_client import run_sentiment
|
| 6 |
+
|
| 7 |
+
router = APIRouter(prefix="/api/hf", tags=["huggingface"])
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
@router.get("/health")
|
| 11 |
+
async def hf_health():
|
| 12 |
+
return REGISTRY.health()
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@router.post("/refresh")
|
| 16 |
+
async def hf_refresh():
|
| 17 |
+
return await REGISTRY.refresh()
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@router.get("/registry")
|
| 21 |
+
async def hf_registry(kind: Literal["models","datasets"]="models"):
|
| 22 |
+
return {"kind": kind, "items": REGISTRY.list(kind)}
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
@router.get("/search")
|
| 26 |
+
async def hf_search(q: str = Query("crypto"), kind: Literal["models","datasets"]="models"):
|
| 27 |
+
hay = REGISTRY.list(kind)
|
| 28 |
+
ql = q.lower()
|
| 29 |
+
res = [x for x in hay if ql in (x.get("id","").lower() + " " + " ".join([str(t) for t in x.get("tags",[])]).lower())]
|
| 30 |
+
return {"query": q, "kind": kind, "count": len(res), "items": res[:50]}
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
@router.post("/run-sentiment")
|
| 34 |
+
async def hf_batch(texts: List[str] = Body(..., embed=True), model: str | None = Body(default=None)):
|
| 35 |
+
return run_sentiment(texts, model=model)
|
api/crypto_resources_unified_2025-11-11.json
CHANGED
|
@@ -348,7 +348,7 @@
|
|
| 348 |
"base_url": "https://api.etherscan.io/api",
|
| 349 |
"auth": {
|
| 350 |
"type": "apiKeyQuery",
|
| 351 |
-
"key": "
|
| 352 |
"param_name": "apikey"
|
| 353 |
},
|
| 354 |
"docs_url": "https://docs.etherscan.io",
|
|
@@ -368,7 +368,7 @@
|
|
| 368 |
"base_url": "https://api.etherscan.io/api",
|
| 369 |
"auth": {
|
| 370 |
"type": "apiKeyQuery",
|
| 371 |
-
"key": "
|
| 372 |
"param_name": "apikey"
|
| 373 |
},
|
| 374 |
"docs_url": "https://docs.etherscan.io",
|
|
@@ -463,7 +463,7 @@
|
|
| 463 |
"base_url": "https://api.bscscan.com/api",
|
| 464 |
"auth": {
|
| 465 |
"type": "apiKeyQuery",
|
| 466 |
-
"key": "
|
| 467 |
"param_name": "apikey"
|
| 468 |
},
|
| 469 |
"docs_url": "https://docs.bscscan.com",
|
|
@@ -552,7 +552,7 @@
|
|
| 552 |
"base_url": "https://apilist.tronscanapi.com/api",
|
| 553 |
"auth": {
|
| 554 |
"type": "apiKeyQuery",
|
| 555 |
-
"key": "
|
| 556 |
"param_name": "apiKey"
|
| 557 |
},
|
| 558 |
"docs_url": "https://github.com/tronscan/tronscan-frontend/blob/dev2019/document/api.md",
|
|
@@ -650,7 +650,7 @@
|
|
| 650 |
"base_url": "https://pro-api.coinmarketcap.com/v1",
|
| 651 |
"auth": {
|
| 652 |
"type": "apiKeyHeader",
|
| 653 |
-
"key": "
|
| 654 |
"header_name": "X-CMC_PRO_API_KEY"
|
| 655 |
},
|
| 656 |
"docs_url": "https://coinmarketcap.com/api/documentation/v1/",
|
|
@@ -668,7 +668,7 @@
|
|
| 668 |
"base_url": "https://pro-api.coinmarketcap.com/v1",
|
| 669 |
"auth": {
|
| 670 |
"type": "apiKeyHeader",
|
| 671 |
-
"key": "
|
| 672 |
"header_name": "X-CMC_PRO_API_KEY"
|
| 673 |
},
|
| 674 |
"docs_url": "https://coinmarketcap.com/api/documentation/v1/",
|
|
@@ -686,7 +686,7 @@
|
|
| 686 |
"base_url": "https://min-api.cryptocompare.com/data",
|
| 687 |
"auth": {
|
| 688 |
"type": "apiKeyQuery",
|
| 689 |
-
"key": "
|
| 690 |
"param_name": "api_key"
|
| 691 |
},
|
| 692 |
"docs_url": "https://min-api.cryptocompare.com/documentation",
|
|
@@ -883,7 +883,7 @@
|
|
| 883 |
"base_url": "https://min-api.cryptocompare.com",
|
| 884 |
"auth": {
|
| 885 |
"type": "apiKeyQuery",
|
| 886 |
-
"key": "
|
| 887 |
"param_name": "api_key"
|
| 888 |
},
|
| 889 |
"docs_url": null,
|
|
@@ -981,7 +981,7 @@
|
|
| 981 |
"base_url": "https://newsapi.org/v2",
|
| 982 |
"auth": {
|
| 983 |
"type": "apiKeyQuery",
|
| 984 |
-
"key": "
|
| 985 |
"param_name": "apiKey"
|
| 986 |
},
|
| 987 |
"docs_url": "https://newsapi.org/docs",
|
|
@@ -1693,13 +1693,13 @@
|
|
| 1693 |
],
|
| 1694 |
"hf_resources": [
|
| 1695 |
{
|
| 1696 |
-
"id": "HF_TOKEN_FROM_SPACE_SECRET",
|
| 1697 |
"type": "model",
|
| 1698 |
"name": "ElKulako/CryptoBERT",
|
| 1699 |
"base_url": "https://api-inference.huggingface.co/models/ElKulako/cryptobert",
|
| 1700 |
"auth": {
|
| 1701 |
"type": "apiKeyHeaderOptional",
|
| 1702 |
-
"key": "HF_TOKEN_FROM_SPACE_SECRET",
|
| 1703 |
"header_name": "Authorization"
|
| 1704 |
},
|
| 1705 |
"docs_url": "https://huggingface.co/ElKulako/cryptobert",
|
|
@@ -1709,13 +1709,13 @@
|
|
| 1709 |
"notes": "For sentiment analysis"
|
| 1710 |
},
|
| 1711 |
{
|
| 1712 |
-
"id": "HF_TOKEN_FROM_SPACE_SECRET",
|
| 1713 |
"type": "model",
|
| 1714 |
"name": "kk08/CryptoBERT",
|
| 1715 |
"base_url": "https://api-inference.huggingface.co/models/kk08/CryptoBERT",
|
| 1716 |
"auth": {
|
| 1717 |
"type": "apiKeyHeaderOptional",
|
| 1718 |
-
"key": "HF_TOKEN_FROM_SPACE_SECRET",
|
| 1719 |
"header_name": "Authorization"
|
| 1720 |
},
|
| 1721 |
"docs_url": "https://huggingface.co/kk08/CryptoBERT",
|
|
@@ -1725,7 +1725,7 @@
|
|
| 1725 |
"notes": "For sentiment analysis"
|
| 1726 |
},
|
| 1727 |
{
|
| 1728 |
-
"id": "
|
| 1729 |
"type": "dataset",
|
| 1730 |
"name": "linxy/CryptoCoin",
|
| 1731 |
"base_url": "https://huggingface.co/datasets/linxy/CryptoCoin/resolve/main",
|
|
@@ -1739,7 +1739,7 @@
|
|
| 1739 |
"notes": "26 symbols x 7 timeframes = 182 CSVs"
|
| 1740 |
},
|
| 1741 |
{
|
| 1742 |
-
"id": "
|
| 1743 |
"type": "dataset",
|
| 1744 |
"name": "WinkingFace/CryptoLM-Bitcoin-BTC-USDT",
|
| 1745 |
"base_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Bitcoin-BTC-USDT/resolve/main",
|
|
@@ -1754,7 +1754,7 @@
|
|
| 1754 |
"notes": null
|
| 1755 |
},
|
| 1756 |
{
|
| 1757 |
-
"id": "
|
| 1758 |
"type": "dataset",
|
| 1759 |
"name": "WinkingFace/CryptoLM-Ethereum-ETH-USDT",
|
| 1760 |
"base_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Ethereum-ETH-USDT/resolve/main",
|
|
@@ -1769,7 +1769,7 @@
|
|
| 1769 |
"notes": null
|
| 1770 |
},
|
| 1771 |
{
|
| 1772 |
-
"id": "
|
| 1773 |
"type": "dataset",
|
| 1774 |
"name": "WinkingFace/CryptoLM-Solana-SOL-USDT",
|
| 1775 |
"base_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Solana-SOL-USDT/resolve/main",
|
|
@@ -1781,7 +1781,7 @@
|
|
| 1781 |
"notes": null
|
| 1782 |
},
|
| 1783 |
{
|
| 1784 |
-
"id": "
|
| 1785 |
"type": "dataset",
|
| 1786 |
"name": "WinkingFace/CryptoLM-Ripple-XRP-USDT",
|
| 1787 |
"base_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Ripple-XRP-USDT/resolve/main",
|
|
@@ -1861,7 +1861,7 @@
|
|
| 1861 |
"notes": null
|
| 1862 |
},
|
| 1863 |
{
|
| 1864 |
-
"id": "HF_TOKEN_FROM_SPACE_SECRET",
|
| 1865 |
"category": "hf-model",
|
| 1866 |
"name": "HF Model: ElKulako/CryptoBERT",
|
| 1867 |
"base_url": "https://huggingface.co/ElKulako/cryptobert",
|
|
@@ -1872,7 +1872,7 @@
|
|
| 1872 |
"notes": null
|
| 1873 |
},
|
| 1874 |
{
|
| 1875 |
-
"id": "HF_TOKEN_FROM_SPACE_SECRET",
|
| 1876 |
"category": "hf-model",
|
| 1877 |
"name": "HF Model: kk08/CryptoBERT",
|
| 1878 |
"base_url": "https://huggingface.co/kk08/CryptoBERT",
|
|
@@ -1883,7 +1883,7 @@
|
|
| 1883 |
"notes": null
|
| 1884 |
},
|
| 1885 |
{
|
| 1886 |
-
"id": "
|
| 1887 |
"category": "hf-dataset",
|
| 1888 |
"name": "HF Dataset: linxy/CryptoCoin",
|
| 1889 |
"base_url": "https://huggingface.co/datasets/linxy/CryptoCoin",
|
|
@@ -2082,15 +2082,15 @@
|
|
| 2082 |
"source_files": [
|
| 2083 |
{
|
| 2084 |
"path": "/mnt/data/api - Copy.txt",
|
| 2085 |
-
"sha256": "
|
| 2086 |
},
|
| 2087 |
{
|
| 2088 |
"path": "/mnt/data/api-config-complete (1).txt",
|
| 2089 |
-
"sha256": "
|
| 2090 |
},
|
| 2091 |
{
|
| 2092 |
"path": "/mnt/data/crypto_resources_ultimate_2025.zip",
|
| 2093 |
-
"sha256": "
|
| 2094 |
"note": "contains crypto_resources.ts and more"
|
| 2095 |
}
|
| 2096 |
]
|
|
|
|
| 348 |
"base_url": "https://api.etherscan.io/api",
|
| 349 |
"auth": {
|
| 350 |
"type": "apiKeyQuery",
|
| 351 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 352 |
"param_name": "apikey"
|
| 353 |
},
|
| 354 |
"docs_url": "https://docs.etherscan.io",
|
|
|
|
| 368 |
"base_url": "https://api.etherscan.io/api",
|
| 369 |
"auth": {
|
| 370 |
"type": "apiKeyQuery",
|
| 371 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 372 |
"param_name": "apikey"
|
| 373 |
},
|
| 374 |
"docs_url": "https://docs.etherscan.io",
|
|
|
|
| 463 |
"base_url": "https://api.bscscan.com/api",
|
| 464 |
"auth": {
|
| 465 |
"type": "apiKeyQuery",
|
| 466 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 467 |
"param_name": "apikey"
|
| 468 |
},
|
| 469 |
"docs_url": "https://docs.bscscan.com",
|
|
|
|
| 552 |
"base_url": "https://apilist.tronscanapi.com/api",
|
| 553 |
"auth": {
|
| 554 |
"type": "apiKeyQuery",
|
| 555 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 556 |
"param_name": "apiKey"
|
| 557 |
},
|
| 558 |
"docs_url": "https://github.com/tronscan/tronscan-frontend/blob/dev2019/document/api.md",
|
|
|
|
| 650 |
"base_url": "https://pro-api.coinmarketcap.com/v1",
|
| 651 |
"auth": {
|
| 652 |
"type": "apiKeyHeader",
|
| 653 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 654 |
"header_name": "X-CMC_PRO_API_KEY"
|
| 655 |
},
|
| 656 |
"docs_url": "https://coinmarketcap.com/api/documentation/v1/",
|
|
|
|
| 668 |
"base_url": "https://pro-api.coinmarketcap.com/v1",
|
| 669 |
"auth": {
|
| 670 |
"type": "apiKeyHeader",
|
| 671 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 672 |
"header_name": "X-CMC_PRO_API_KEY"
|
| 673 |
},
|
| 674 |
"docs_url": "https://coinmarketcap.com/api/documentation/v1/",
|
|
|
|
| 686 |
"base_url": "https://min-api.cryptocompare.com/data",
|
| 687 |
"auth": {
|
| 688 |
"type": "apiKeyQuery",
|
| 689 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 690 |
"param_name": "api_key"
|
| 691 |
},
|
| 692 |
"docs_url": "https://min-api.cryptocompare.com/documentation",
|
|
|
|
| 883 |
"base_url": "https://min-api.cryptocompare.com",
|
| 884 |
"auth": {
|
| 885 |
"type": "apiKeyQuery",
|
| 886 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 887 |
"param_name": "api_key"
|
| 888 |
},
|
| 889 |
"docs_url": null,
|
|
|
|
| 981 |
"base_url": "https://newsapi.org/v2",
|
| 982 |
"auth": {
|
| 983 |
"type": "apiKeyQuery",
|
| 984 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 985 |
"param_name": "apiKey"
|
| 986 |
},
|
| 987 |
"docs_url": "https://newsapi.org/docs",
|
|
|
|
| 1693 |
],
|
| 1694 |
"hf_resources": [
|
| 1695 |
{
|
| 1696 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1697 |
"type": "model",
|
| 1698 |
"name": "ElKulako/CryptoBERT",
|
| 1699 |
"base_url": "https://api-inference.huggingface.co/models/ElKulako/cryptobert",
|
| 1700 |
"auth": {
|
| 1701 |
"type": "apiKeyHeaderOptional",
|
| 1702 |
+
"key": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1703 |
"header_name": "Authorization"
|
| 1704 |
},
|
| 1705 |
"docs_url": "https://huggingface.co/ElKulako/cryptobert",
|
|
|
|
| 1709 |
"notes": "For sentiment analysis"
|
| 1710 |
},
|
| 1711 |
{
|
| 1712 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1713 |
"type": "model",
|
| 1714 |
"name": "kk08/CryptoBERT",
|
| 1715 |
"base_url": "https://api-inference.huggingface.co/models/kk08/CryptoBERT",
|
| 1716 |
"auth": {
|
| 1717 |
"type": "apiKeyHeaderOptional",
|
| 1718 |
+
"key": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1719 |
"header_name": "Authorization"
|
| 1720 |
},
|
| 1721 |
"docs_url": "https://huggingface.co/kk08/CryptoBERT",
|
|
|
|
| 1725 |
"notes": "For sentiment analysis"
|
| 1726 |
},
|
| 1727 |
{
|
| 1728 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1729 |
"type": "dataset",
|
| 1730 |
"name": "linxy/CryptoCoin",
|
| 1731 |
"base_url": "https://huggingface.co/datasets/linxy/CryptoCoin/resolve/main",
|
|
|
|
| 1739 |
"notes": "26 symbols x 7 timeframes = 182 CSVs"
|
| 1740 |
},
|
| 1741 |
{
|
| 1742 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1743 |
"type": "dataset",
|
| 1744 |
"name": "WinkingFace/CryptoLM-Bitcoin-BTC-USDT",
|
| 1745 |
"base_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Bitcoin-BTC-USDT/resolve/main",
|
|
|
|
| 1754 |
"notes": null
|
| 1755 |
},
|
| 1756 |
{
|
| 1757 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1758 |
"type": "dataset",
|
| 1759 |
"name": "WinkingFace/CryptoLM-Ethereum-ETH-USDT",
|
| 1760 |
"base_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Ethereum-ETH-USDT/resolve/main",
|
|
|
|
| 1769 |
"notes": null
|
| 1770 |
},
|
| 1771 |
{
|
| 1772 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1773 |
"type": "dataset",
|
| 1774 |
"name": "WinkingFace/CryptoLM-Solana-SOL-USDT",
|
| 1775 |
"base_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Solana-SOL-USDT/resolve/main",
|
|
|
|
| 1781 |
"notes": null
|
| 1782 |
},
|
| 1783 |
{
|
| 1784 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1785 |
"type": "dataset",
|
| 1786 |
"name": "WinkingFace/CryptoLM-Ripple-XRP-USDT",
|
| 1787 |
"base_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Ripple-XRP-USDT/resolve/main",
|
|
|
|
| 1861 |
"notes": null
|
| 1862 |
},
|
| 1863 |
{
|
| 1864 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1865 |
"category": "hf-model",
|
| 1866 |
"name": "HF Model: ElKulako/CryptoBERT",
|
| 1867 |
"base_url": "https://huggingface.co/ElKulako/cryptobert",
|
|
|
|
| 1872 |
"notes": null
|
| 1873 |
},
|
| 1874 |
{
|
| 1875 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1876 |
"category": "hf-model",
|
| 1877 |
"name": "HF Model: kk08/CryptoBERT",
|
| 1878 |
"base_url": "https://huggingface.co/kk08/CryptoBERT",
|
|
|
|
| 1883 |
"notes": null
|
| 1884 |
},
|
| 1885 |
{
|
| 1886 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1887 |
"category": "hf-dataset",
|
| 1888 |
"name": "HF Dataset: linxy/CryptoCoin",
|
| 1889 |
"base_url": "https://huggingface.co/datasets/linxy/CryptoCoin",
|
|
|
|
| 2082 |
"source_files": [
|
| 2083 |
{
|
| 2084 |
"path": "/mnt/data/api - Copy.txt",
|
| 2085 |
+
"sha256": "20f9a3357a65c28a691990f89ad57f0de978600e65405fafe2c8b3c3502f6b77"
|
| 2086 |
},
|
| 2087 |
{
|
| 2088 |
"path": "/mnt/data/api-config-complete (1).txt",
|
| 2089 |
+
"sha256": "cb9f4c746f5b8a1d70824340425557e4483ad7a8e5396e0be67d68d671b23697"
|
| 2090 |
},
|
| 2091 |
{
|
| 2092 |
"path": "/mnt/data/crypto_resources_ultimate_2025.zip",
|
| 2093 |
+
"sha256": "5bb6f0ef790f09e23a88adbf4a4c0bc225183e896c3aa63416e53b1eec36ea87",
|
| 2094 |
"note": "contains crypto_resources.ts and more"
|
| 2095 |
}
|
| 2096 |
]
|
api/ultimate_crypto_pipeline_2025_NZasinich.json
CHANGED
|
@@ -1,4 +1,3 @@
|
|
| 1 |
-
ultimate_crypto_pipeline_2025_NZasinich.json
|
| 2 |
{
|
| 3 |
"user": {
|
| 4 |
"handle": "@NZasinich",
|
|
@@ -62,7 +61,7 @@ ultimate_crypto_pipeline_2025_NZasinich.json
|
|
| 62 |
"category": "Block Explorer",
|
| 63 |
"name": "TronScan",
|
| 64 |
"url": "https://api.tronscan.org/api",
|
| 65 |
-
"key": "
|
| 66 |
"free": false,
|
| 67 |
"desc": "TRON accounts."
|
| 68 |
},
|
|
@@ -87,7 +86,7 @@ ultimate_crypto_pipeline_2025_NZasinich.json
|
|
| 87 |
"category": "Block Explorer",
|
| 88 |
"name": "BscScan",
|
| 89 |
"url": "https://api.bscscan.com/api",
|
| 90 |
-
"key": "
|
| 91 |
"free": false,
|
| 92 |
"desc": "BSC balances."
|
| 93 |
},
|
|
@@ -111,7 +110,7 @@ ultimate_crypto_pipeline_2025_NZasinich.json
|
|
| 111 |
"category": "Block Explorer",
|
| 112 |
"name": "Etherscan",
|
| 113 |
"url": "https://api.etherscan.io/api",
|
| 114 |
-
"key": "
|
| 115 |
"free": false,
|
| 116 |
"desc": "ETH explorer."
|
| 117 |
},
|
|
@@ -119,7 +118,7 @@ ultimate_crypto_pipeline_2025_NZasinich.json
|
|
| 119 |
"category": "Block Explorer",
|
| 120 |
"name": "Etherscan Backup",
|
| 121 |
"url": "https://api.etherscan.io/api",
|
| 122 |
-
"key": "
|
| 123 |
"free": false,
|
| 124 |
"desc": "ETH backup."
|
| 125 |
},
|
|
@@ -252,7 +251,7 @@ ultimate_crypto_pipeline_2025_NZasinich.json
|
|
| 252 |
"category": "Market Data",
|
| 253 |
"name": "CoinMarketCap (User key)",
|
| 254 |
"url": "https://pro-api.coinmarketcap.com/v1",
|
| 255 |
-
"key": "
|
| 256 |
"free": false,
|
| 257 |
"rateLimit": "333/day"
|
| 258 |
},
|
|
@@ -483,7 +482,7 @@ ultimate_crypto_pipeline_2025_NZasinich.json
|
|
| 483 |
"content": "export interface CryptoResource { category: string; name: string; url: string; key: string; free: boolean; rateLimit?: string; desc: string; endpoint?: string; example?: string; params?: Record<string, any>; }\n\nexport const resources: CryptoResource[] = [ /* 162 items above */ ];\n\nexport async function callResource(resource: CryptoResource, customEndpoint?: string, params: Record<string, any> = {}): Promise<any> { let url = resource.url + (customEndpoint || resource.endpoint || ''); const query = new URLSearchParams(params).toString(); url += query ? `?${query}` : ''; const headers: HeadersInit = resource.key ? { Authorization: `Bearer ${resource.key}` } : {}; const res = await fetch(url, { headers }); if (!res.ok) throw new Error(`Failed: ${res.status}`); const data = await res.json(); if (!data || Object.keys(data).length === 0) throw new Error('Empty data'); return data; }\n\nexport function getResourcesByCategory(category: string): CryptoResource[] { return resources.filter(r => r.category === category); }"
|
| 484 |
},
|
| 485 |
{
|
| 486 |
-
"filename": "
|
| 487 |
"description": "Complete FastAPI + Hugging Face free data & sentiment pipeline (additive)",
|
| 488 |
"content": "from fastapi import FastAPI, APIRouter; from datasets import load_dataset; import pandas as pd; from transformers import pipeline; app = FastAPI(); router = APIRouter(prefix=\"/api/hf\"); # Full code from previous Cursor Agent prompt..."
|
| 489 |
},
|
|
@@ -500,4 +499,4 @@ ultimate_crypto_pipeline_2025_NZasinich.json
|
|
| 500 |
],
|
| 501 |
"total_files": 5,
|
| 502 |
"download_instructions": "Copy this entire JSON and save as `ultimate_crypto_pipeline_2025.json`. All code is ready to use. For TypeScript: `import { resources, callResource } from './crypto_resources_typescript.ts';`"
|
| 503 |
-
}
|
|
|
|
|
|
|
| 1 |
{
|
| 2 |
"user": {
|
| 3 |
"handle": "@NZasinich",
|
|
|
|
| 61 |
"category": "Block Explorer",
|
| 62 |
"name": "TronScan",
|
| 63 |
"url": "https://api.tronscan.org/api",
|
| 64 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 65 |
"free": false,
|
| 66 |
"desc": "TRON accounts."
|
| 67 |
},
|
|
|
|
| 86 |
"category": "Block Explorer",
|
| 87 |
"name": "BscScan",
|
| 88 |
"url": "https://api.bscscan.com/api",
|
| 89 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 90 |
"free": false,
|
| 91 |
"desc": "BSC balances."
|
| 92 |
},
|
|
|
|
| 110 |
"category": "Block Explorer",
|
| 111 |
"name": "Etherscan",
|
| 112 |
"url": "https://api.etherscan.io/api",
|
| 113 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 114 |
"free": false,
|
| 115 |
"desc": "ETH explorer."
|
| 116 |
},
|
|
|
|
| 118 |
"category": "Block Explorer",
|
| 119 |
"name": "Etherscan Backup",
|
| 120 |
"url": "https://api.etherscan.io/api",
|
| 121 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 122 |
"free": false,
|
| 123 |
"desc": "ETH backup."
|
| 124 |
},
|
|
|
|
| 251 |
"category": "Market Data",
|
| 252 |
"name": "CoinMarketCap (User key)",
|
| 253 |
"url": "https://pro-api.coinmarketcap.com/v1",
|
| 254 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 255 |
"free": false,
|
| 256 |
"rateLimit": "333/day"
|
| 257 |
},
|
|
|
|
| 482 |
"content": "export interface CryptoResource { category: string; name: string; url: string; key: string; free: boolean; rateLimit?: string; desc: string; endpoint?: string; example?: string; params?: Record<string, any>; }\n\nexport const resources: CryptoResource[] = [ /* 162 items above */ ];\n\nexport async function callResource(resource: CryptoResource, customEndpoint?: string, params: Record<string, any> = {}): Promise<any> { let url = resource.url + (customEndpoint || resource.endpoint || ''); const query = new URLSearchParams(params).toString(); url += query ? `?${query}` : ''; const headers: HeadersInit = resource.key ? { Authorization: `Bearer ${resource.key}` } : {}; const res = await fetch(url, { headers }); if (!res.ok) throw new Error(`Failed: ${res.status}`); const data = await res.json(); if (!data || Object.keys(data).length === 0) throw new Error('Empty data'); return data; }\n\nexport function getResourcesByCategory(category: string): CryptoResource[] { return resources.filter(r => r.category === category); }"
|
| 483 |
},
|
| 484 |
{
|
| 485 |
+
"filename": "hf_unified_server.py",
|
| 486 |
"description": "Complete FastAPI + Hugging Face free data & sentiment pipeline (additive)",
|
| 487 |
"content": "from fastapi import FastAPI, APIRouter; from datasets import load_dataset; import pandas as pd; from transformers import pipeline; app = FastAPI(); router = APIRouter(prefix=\"/api/hf\"); # Full code from previous Cursor Agent prompt..."
|
| 488 |
},
|
|
|
|
| 499 |
],
|
| 500 |
"total_files": 5,
|
| 501 |
"download_instructions": "Copy this entire JSON and save as `ultimate_crypto_pipeline_2025.json`. All code is ready to use. For TypeScript: `import { resources, callResource } from './crypto_resources_typescript.ts';`"
|
| 502 |
+
}
|
api/ws_integration_services.py
CHANGED
|
@@ -1,334 +1,334 @@
|
|
| 1 |
-
"""
|
| 2 |
-
WebSocket API for Integration Services
|
| 3 |
-
|
| 4 |
-
This module provides WebSocket endpoints for integration services
|
| 5 |
-
including HuggingFace AI models and persistence operations.
|
| 6 |
-
"""
|
| 7 |
-
|
| 8 |
-
import asyncio
|
| 9 |
-
from datetime import datetime
|
| 10 |
-
from typing import Any, Dict
|
| 11 |
-
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
| 12 |
-
import logging
|
| 13 |
-
|
| 14 |
-
from backend.services.ws_service_manager import ws_manager, ServiceType
|
| 15 |
-
from backend.services.hf_registry import HFRegistry
|
| 16 |
-
from backend.services.hf_client import HFClient
|
| 17 |
-
from backend.services.persistence_service import PersistenceService
|
| 18 |
-
from config import Config
|
| 19 |
-
|
| 20 |
-
logger = logging.getLogger(__name__)
|
| 21 |
-
|
| 22 |
-
router = APIRouter()
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
# ============================================================================
|
| 26 |
-
# Integration Service Handlers
|
| 27 |
-
# ============================================================================
|
| 28 |
-
|
| 29 |
-
class IntegrationStreamers:
|
| 30 |
-
"""Handles data streaming for integration services"""
|
| 31 |
-
|
| 32 |
-
def __init__(self):
|
| 33 |
-
self.config = Config()
|
| 34 |
-
try:
|
| 35 |
-
self.hf_registry = HFRegistry()
|
| 36 |
-
except:
|
| 37 |
-
self.hf_registry = None
|
| 38 |
-
logger.warning("HFRegistry not available")
|
| 39 |
-
|
| 40 |
-
try:
|
| 41 |
-
self.hf_client = HFClient()
|
| 42 |
-
except:
|
| 43 |
-
self.hf_client = None
|
| 44 |
-
logger.warning("HFClient not available")
|
| 45 |
-
|
| 46 |
-
try:
|
| 47 |
-
self.persistence_service = PersistenceService()
|
| 48 |
-
except:
|
| 49 |
-
self.persistence_service = None
|
| 50 |
-
logger.warning("PersistenceService not available")
|
| 51 |
-
|
| 52 |
-
# ========================================================================
|
| 53 |
-
# HuggingFace Streaming
|
| 54 |
-
# ========================================================================
|
| 55 |
-
|
| 56 |
-
async def
|
| 57 |
-
"""Stream HuggingFace registry status"""
|
| 58 |
-
if not self.hf_registry:
|
| 59 |
-
return None
|
| 60 |
-
|
| 61 |
-
try:
|
| 62 |
-
status = self.hf_registry.get_status()
|
| 63 |
-
if status:
|
| 64 |
-
return {
|
| 65 |
-
"total_models": status.get("total_models", 0),
|
| 66 |
-
"total_datasets": status.get("total_datasets", 0),
|
| 67 |
-
"available_models": status.get("available_models", []),
|
| 68 |
-
"available_datasets": status.get("available_datasets", []),
|
| 69 |
-
"last_refresh": status.get("last_refresh"),
|
| 70 |
-
"timestamp": datetime.utcnow().isoformat()
|
| 71 |
-
}
|
| 72 |
-
except Exception as e:
|
| 73 |
-
logger.error(f"Error streaming HF registry status: {e}")
|
| 74 |
-
return None
|
| 75 |
-
|
| 76 |
-
async def stream_hf_model_usage(self):
|
| 77 |
-
"""Stream HuggingFace model usage statistics"""
|
| 78 |
-
if not self.hf_client:
|
| 79 |
-
return None
|
| 80 |
-
|
| 81 |
-
try:
|
| 82 |
-
usage = self.hf_client.get_usage_stats()
|
| 83 |
-
if usage:
|
| 84 |
-
return {
|
| 85 |
-
"total_requests": usage.get("total_requests", 0),
|
| 86 |
-
"successful_requests": usage.get("successful_requests", 0),
|
| 87 |
-
"failed_requests": usage.get("failed_requests", 0),
|
| 88 |
-
"average_latency": usage.get("average_latency"),
|
| 89 |
-
"model_usage": usage.get("model_usage", {}),
|
| 90 |
-
"timestamp": datetime.utcnow().isoformat()
|
| 91 |
-
}
|
| 92 |
-
except Exception as e:
|
| 93 |
-
logger.error(f"Error streaming HF model usage: {e}")
|
| 94 |
-
return None
|
| 95 |
-
|
| 96 |
-
async def stream_sentiment_results(self):
|
| 97 |
-
"""Stream real-time sentiment analysis results"""
|
| 98 |
-
if not self.hf_client:
|
| 99 |
-
return None
|
| 100 |
-
|
| 101 |
-
try:
|
| 102 |
-
# This would stream sentiment results as they're processed
|
| 103 |
-
results = self.hf_client.get_recent_results()
|
| 104 |
-
if results:
|
| 105 |
-
return {
|
| 106 |
-
"sentiment_results": results,
|
| 107 |
-
"timestamp": datetime.utcnow().isoformat()
|
| 108 |
-
}
|
| 109 |
-
except Exception as e:
|
| 110 |
-
logger.error(f"Error streaming sentiment results: {e}")
|
| 111 |
-
return None
|
| 112 |
-
|
| 113 |
-
async def stream_model_events(self):
|
| 114 |
-
"""Stream model loading and unloading events"""
|
| 115 |
-
if not self.hf_registry:
|
| 116 |
-
return None
|
| 117 |
-
|
| 118 |
-
try:
|
| 119 |
-
events = self.hf_registry.get_recent_events()
|
| 120 |
-
if events:
|
| 121 |
-
return {
|
| 122 |
-
"model_events": events,
|
| 123 |
-
"timestamp": datetime.utcnow().isoformat()
|
| 124 |
-
}
|
| 125 |
-
except Exception as e:
|
| 126 |
-
logger.error(f"Error streaming model events: {e}")
|
| 127 |
-
return None
|
| 128 |
-
|
| 129 |
-
# ========================================================================
|
| 130 |
-
# Persistence Service Streaming
|
| 131 |
-
# ========================================================================
|
| 132 |
-
|
| 133 |
-
async def stream_persistence_status(self):
|
| 134 |
-
"""Stream persistence service status"""
|
| 135 |
-
if not self.persistence_service:
|
| 136 |
-
return None
|
| 137 |
-
|
| 138 |
-
try:
|
| 139 |
-
status = self.persistence_service.get_status()
|
| 140 |
-
if status:
|
| 141 |
-
return {
|
| 142 |
-
"storage_location": status.get("storage_location"),
|
| 143 |
-
"total_records": status.get("total_records", 0),
|
| 144 |
-
"storage_size": status.get("storage_size"),
|
| 145 |
-
"last_save": status.get("last_save"),
|
| 146 |
-
"active_writers": status.get("active_writers", 0),
|
| 147 |
-
"timestamp": datetime.utcnow().isoformat()
|
| 148 |
-
}
|
| 149 |
-
except Exception as e:
|
| 150 |
-
logger.error(f"Error streaming persistence status: {e}")
|
| 151 |
-
return None
|
| 152 |
-
|
| 153 |
-
async def stream_save_events(self):
|
| 154 |
-
"""Stream data save events"""
|
| 155 |
-
if not self.persistence_service:
|
| 156 |
-
return None
|
| 157 |
-
|
| 158 |
-
try:
|
| 159 |
-
events = self.persistence_service.get_recent_saves()
|
| 160 |
-
if events:
|
| 161 |
-
return {
|
| 162 |
-
"save_events": events,
|
| 163 |
-
"timestamp": datetime.utcnow().isoformat()
|
| 164 |
-
}
|
| 165 |
-
except Exception as e:
|
| 166 |
-
logger.error(f"Error streaming save events: {e}")
|
| 167 |
-
return None
|
| 168 |
-
|
| 169 |
-
async def stream_export_progress(self):
|
| 170 |
-
"""Stream export operation progress"""
|
| 171 |
-
if not self.persistence_service:
|
| 172 |
-
return None
|
| 173 |
-
|
| 174 |
-
try:
|
| 175 |
-
progress = self.persistence_service.get_export_progress()
|
| 176 |
-
if progress:
|
| 177 |
-
return {
|
| 178 |
-
"export_operations": progress,
|
| 179 |
-
"timestamp": datetime.utcnow().isoformat()
|
| 180 |
-
}
|
| 181 |
-
except Exception as e:
|
| 182 |
-
logger.error(f"Error streaming export progress: {e}")
|
| 183 |
-
return None
|
| 184 |
-
|
| 185 |
-
async def stream_backup_events(self):
|
| 186 |
-
"""Stream backup creation events"""
|
| 187 |
-
if not self.persistence_service:
|
| 188 |
-
return None
|
| 189 |
-
|
| 190 |
-
try:
|
| 191 |
-
backups = self.persistence_service.get_recent_backups()
|
| 192 |
-
if backups:
|
| 193 |
-
return {
|
| 194 |
-
"backup_events": backups,
|
| 195 |
-
"timestamp": datetime.utcnow().isoformat()
|
| 196 |
-
}
|
| 197 |
-
except Exception as e:
|
| 198 |
-
logger.error(f"Error streaming backup events: {e}")
|
| 199 |
-
return None
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
# Global instance
|
| 203 |
-
integration_streamers = IntegrationStreamers()
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
# ============================================================================
|
| 207 |
-
# Background Streaming Tasks
|
| 208 |
-
# ============================================================================
|
| 209 |
-
|
| 210 |
-
async def start_integration_streams():
|
| 211 |
-
"""Start all integration stream tasks"""
|
| 212 |
-
logger.info("Starting integration WebSocket streams")
|
| 213 |
-
|
| 214 |
-
tasks = [
|
| 215 |
-
# HuggingFace Registry
|
| 216 |
-
asyncio.create_task(ws_manager.start_service_stream(
|
| 217 |
-
ServiceType.HUGGINGFACE,
|
| 218 |
-
integration_streamers.
|
| 219 |
-
interval=60.0 # 1 minute updates
|
| 220 |
-
)),
|
| 221 |
-
|
| 222 |
-
# Persistence Service
|
| 223 |
-
asyncio.create_task(ws_manager.start_service_stream(
|
| 224 |
-
ServiceType.PERSISTENCE,
|
| 225 |
-
integration_streamers.stream_persistence_status,
|
| 226 |
-
interval=30.0 # 30 second updates
|
| 227 |
-
)),
|
| 228 |
-
]
|
| 229 |
-
|
| 230 |
-
await asyncio.gather(*tasks, return_exceptions=True)
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
# ============================================================================
|
| 234 |
-
# WebSocket Endpoints
|
| 235 |
-
# ============================================================================
|
| 236 |
-
|
| 237 |
-
@router.websocket("/ws/integration")
|
| 238 |
-
async def websocket_integration_endpoint(websocket: WebSocket):
|
| 239 |
-
"""
|
| 240 |
-
Unified WebSocket endpoint for all integration services
|
| 241 |
-
|
| 242 |
-
Connection URL: ws://host:port/ws/integration
|
| 243 |
-
|
| 244 |
-
After connecting, send subscription messages:
|
| 245 |
-
{
|
| 246 |
-
"action": "subscribe",
|
| 247 |
-
"service": "huggingface" | "persistence" | "all"
|
| 248 |
-
}
|
| 249 |
-
|
| 250 |
-
To unsubscribe:
|
| 251 |
-
{
|
| 252 |
-
"action": "unsubscribe",
|
| 253 |
-
"service": "service_name"
|
| 254 |
-
}
|
| 255 |
-
"""
|
| 256 |
-
connection = await ws_manager.connect(websocket)
|
| 257 |
-
|
| 258 |
-
try:
|
| 259 |
-
while True:
|
| 260 |
-
data = await websocket.receive_json()
|
| 261 |
-
await ws_manager.handle_client_message(connection, data)
|
| 262 |
-
|
| 263 |
-
except WebSocketDisconnect:
|
| 264 |
-
logger.info(f"Integration client disconnected: {connection.client_id}")
|
| 265 |
-
except Exception as e:
|
| 266 |
-
logger.error(f"Integration WebSocket error: {e}")
|
| 267 |
-
finally:
|
| 268 |
-
await ws_manager.disconnect(connection.client_id)
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
@router.websocket("/ws/huggingface")
|
| 272 |
-
async def websocket_huggingface(websocket: WebSocket):
|
| 273 |
-
"""
|
| 274 |
-
Dedicated WebSocket endpoint for HuggingFace services
|
| 275 |
-
|
| 276 |
-
Auto-subscribes to huggingface service
|
| 277 |
-
"""
|
| 278 |
-
connection = await ws_manager.connect(websocket)
|
| 279 |
-
connection.subscribe(ServiceType.HUGGINGFACE)
|
| 280 |
-
|
| 281 |
-
try:
|
| 282 |
-
while True:
|
| 283 |
-
data = await websocket.receive_json()
|
| 284 |
-
await ws_manager.handle_client_message(connection, data)
|
| 285 |
-
except WebSocketDisconnect:
|
| 286 |
-
logger.info(f"HuggingFace client disconnected: {connection.client_id}")
|
| 287 |
-
except Exception as e:
|
| 288 |
-
logger.error(f"HuggingFace WebSocket error: {e}")
|
| 289 |
-
finally:
|
| 290 |
-
await ws_manager.disconnect(connection.client_id)
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
@router.websocket("/ws/persistence")
|
| 294 |
-
async def websocket_persistence(websocket: WebSocket):
|
| 295 |
-
"""
|
| 296 |
-
Dedicated WebSocket endpoint for persistence service
|
| 297 |
-
|
| 298 |
-
Auto-subscribes to persistence service
|
| 299 |
-
"""
|
| 300 |
-
connection = await ws_manager.connect(websocket)
|
| 301 |
-
connection.subscribe(ServiceType.PERSISTENCE)
|
| 302 |
-
|
| 303 |
-
try:
|
| 304 |
-
while True:
|
| 305 |
-
data = await websocket.receive_json()
|
| 306 |
-
await ws_manager.handle_client_message(connection, data)
|
| 307 |
-
except WebSocketDisconnect:
|
| 308 |
-
logger.info(f"Persistence client disconnected: {connection.client_id}")
|
| 309 |
-
except Exception as e:
|
| 310 |
-
logger.error(f"Persistence WebSocket error: {e}")
|
| 311 |
-
finally:
|
| 312 |
-
await ws_manager.disconnect(connection.client_id)
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
@router.websocket("/ws/ai")
|
| 316 |
-
async def websocket_ai(websocket: WebSocket):
|
| 317 |
-
"""
|
| 318 |
-
Dedicated WebSocket endpoint for AI/ML operations (alias for HuggingFace)
|
| 319 |
-
|
| 320 |
-
Auto-subscribes to huggingface service
|
| 321 |
-
"""
|
| 322 |
-
connection = await ws_manager.connect(websocket)
|
| 323 |
-
connection.subscribe(ServiceType.HUGGINGFACE)
|
| 324 |
-
|
| 325 |
-
try:
|
| 326 |
-
while True:
|
| 327 |
-
data = await websocket.receive_json()
|
| 328 |
-
await ws_manager.handle_client_message(connection, data)
|
| 329 |
-
except WebSocketDisconnect:
|
| 330 |
-
logger.info(f"AI client disconnected: {connection.client_id}")
|
| 331 |
-
except Exception as e:
|
| 332 |
-
logger.error(f"AI WebSocket error: {e}")
|
| 333 |
-
finally:
|
| 334 |
-
await ws_manager.disconnect(connection.client_id)
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
WebSocket API for Integration Services
|
| 3 |
+
|
| 4 |
+
This module provides WebSocket endpoints for integration services
|
| 5 |
+
including HuggingFace AI models and persistence operations.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import asyncio
|
| 9 |
+
from datetime import datetime
|
| 10 |
+
from typing import Any, Dict
|
| 11 |
+
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
| 12 |
+
import logging
|
| 13 |
+
|
| 14 |
+
from backend.services.ws_service_manager import ws_manager, ServiceType
|
| 15 |
+
from backend.services.hf_registry import HFRegistry
|
| 16 |
+
from backend.services.hf_client import HFClient
|
| 17 |
+
from backend.services.persistence_service import PersistenceService
|
| 18 |
+
from config import Config
|
| 19 |
+
|
| 20 |
+
logger = logging.getLogger(__name__)
|
| 21 |
+
|
| 22 |
+
router = APIRouter()
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
# ============================================================================
|
| 26 |
+
# Integration Service Handlers
|
| 27 |
+
# ============================================================================
|
| 28 |
+
|
| 29 |
+
class IntegrationStreamers:
|
| 30 |
+
"""Handles data streaming for integration services"""
|
| 31 |
+
|
| 32 |
+
def __init__(self):
|
| 33 |
+
self.config = Config()
|
| 34 |
+
try:
|
| 35 |
+
self.hf_registry = HFRegistry()
|
| 36 |
+
except:
|
| 37 |
+
self.hf_registry = None
|
| 38 |
+
logger.warning("HFRegistry not available")
|
| 39 |
+
|
| 40 |
+
try:
|
| 41 |
+
self.hf_client = HFClient()
|
| 42 |
+
except:
|
| 43 |
+
self.hf_client = None
|
| 44 |
+
logger.warning("HFClient not available")
|
| 45 |
+
|
| 46 |
+
try:
|
| 47 |
+
self.persistence_service = PersistenceService()
|
| 48 |
+
except:
|
| 49 |
+
self.persistence_service = None
|
| 50 |
+
logger.warning("PersistenceService not available")
|
| 51 |
+
|
| 52 |
+
# ========================================================================
|
| 53 |
+
# HuggingFace Streaming
|
| 54 |
+
# ========================================================================
|
| 55 |
+
|
| 56 |
+
async def stream_hf(self):
|
| 57 |
+
"""Stream HuggingFace registry status"""
|
| 58 |
+
if not self.hf_registry:
|
| 59 |
+
return None
|
| 60 |
+
|
| 61 |
+
try:
|
| 62 |
+
status = self.hf_registry.get_status()
|
| 63 |
+
if status:
|
| 64 |
+
return {
|
| 65 |
+
"total_models": status.get("total_models", 0),
|
| 66 |
+
"total_datasets": status.get("total_datasets", 0),
|
| 67 |
+
"available_models": status.get("available_models", []),
|
| 68 |
+
"available_datasets": status.get("available_datasets", []),
|
| 69 |
+
"last_refresh": status.get("last_refresh"),
|
| 70 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 71 |
+
}
|
| 72 |
+
except Exception as e:
|
| 73 |
+
logger.error(f"Error streaming HF registry status: {e}")
|
| 74 |
+
return None
|
| 75 |
+
|
| 76 |
+
async def stream_hf_model_usage(self):
|
| 77 |
+
"""Stream HuggingFace model usage statistics"""
|
| 78 |
+
if not self.hf_client:
|
| 79 |
+
return None
|
| 80 |
+
|
| 81 |
+
try:
|
| 82 |
+
usage = self.hf_client.get_usage_stats()
|
| 83 |
+
if usage:
|
| 84 |
+
return {
|
| 85 |
+
"total_requests": usage.get("total_requests", 0),
|
| 86 |
+
"successful_requests": usage.get("successful_requests", 0),
|
| 87 |
+
"failed_requests": usage.get("failed_requests", 0),
|
| 88 |
+
"average_latency": usage.get("average_latency"),
|
| 89 |
+
"model_usage": usage.get("model_usage", {}),
|
| 90 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 91 |
+
}
|
| 92 |
+
except Exception as e:
|
| 93 |
+
logger.error(f"Error streaming HF model usage: {e}")
|
| 94 |
+
return None
|
| 95 |
+
|
| 96 |
+
async def stream_sentiment_results(self):
|
| 97 |
+
"""Stream real-time sentiment analysis results"""
|
| 98 |
+
if not self.hf_client:
|
| 99 |
+
return None
|
| 100 |
+
|
| 101 |
+
try:
|
| 102 |
+
# This would stream sentiment results as they're processed
|
| 103 |
+
results = self.hf_client.get_recent_results()
|
| 104 |
+
if results:
|
| 105 |
+
return {
|
| 106 |
+
"sentiment_results": results,
|
| 107 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 108 |
+
}
|
| 109 |
+
except Exception as e:
|
| 110 |
+
logger.error(f"Error streaming sentiment results: {e}")
|
| 111 |
+
return None
|
| 112 |
+
|
| 113 |
+
async def stream_model_events(self):
|
| 114 |
+
"""Stream model loading and unloading events"""
|
| 115 |
+
if not self.hf_registry:
|
| 116 |
+
return None
|
| 117 |
+
|
| 118 |
+
try:
|
| 119 |
+
events = self.hf_registry.get_recent_events()
|
| 120 |
+
if events:
|
| 121 |
+
return {
|
| 122 |
+
"model_events": events,
|
| 123 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 124 |
+
}
|
| 125 |
+
except Exception as e:
|
| 126 |
+
logger.error(f"Error streaming model events: {e}")
|
| 127 |
+
return None
|
| 128 |
+
|
| 129 |
+
# ========================================================================
|
| 130 |
+
# Persistence Service Streaming
|
| 131 |
+
# ========================================================================
|
| 132 |
+
|
| 133 |
+
async def stream_persistence_status(self):
|
| 134 |
+
"""Stream persistence service status"""
|
| 135 |
+
if not self.persistence_service:
|
| 136 |
+
return None
|
| 137 |
+
|
| 138 |
+
try:
|
| 139 |
+
status = self.persistence_service.get_status()
|
| 140 |
+
if status:
|
| 141 |
+
return {
|
| 142 |
+
"storage_location": status.get("storage_location"),
|
| 143 |
+
"total_records": status.get("total_records", 0),
|
| 144 |
+
"storage_size": status.get("storage_size"),
|
| 145 |
+
"last_save": status.get("last_save"),
|
| 146 |
+
"active_writers": status.get("active_writers", 0),
|
| 147 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 148 |
+
}
|
| 149 |
+
except Exception as e:
|
| 150 |
+
logger.error(f"Error streaming persistence status: {e}")
|
| 151 |
+
return None
|
| 152 |
+
|
| 153 |
+
async def stream_save_events(self):
|
| 154 |
+
"""Stream data save events"""
|
| 155 |
+
if not self.persistence_service:
|
| 156 |
+
return None
|
| 157 |
+
|
| 158 |
+
try:
|
| 159 |
+
events = self.persistence_service.get_recent_saves()
|
| 160 |
+
if events:
|
| 161 |
+
return {
|
| 162 |
+
"save_events": events,
|
| 163 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 164 |
+
}
|
| 165 |
+
except Exception as e:
|
| 166 |
+
logger.error(f"Error streaming save events: {e}")
|
| 167 |
+
return None
|
| 168 |
+
|
| 169 |
+
async def stream_export_progress(self):
|
| 170 |
+
"""Stream export operation progress"""
|
| 171 |
+
if not self.persistence_service:
|
| 172 |
+
return None
|
| 173 |
+
|
| 174 |
+
try:
|
| 175 |
+
progress = self.persistence_service.get_export_progress()
|
| 176 |
+
if progress:
|
| 177 |
+
return {
|
| 178 |
+
"export_operations": progress,
|
| 179 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 180 |
+
}
|
| 181 |
+
except Exception as e:
|
| 182 |
+
logger.error(f"Error streaming export progress: {e}")
|
| 183 |
+
return None
|
| 184 |
+
|
| 185 |
+
async def stream_backup_events(self):
|
| 186 |
+
"""Stream backup creation events"""
|
| 187 |
+
if not self.persistence_service:
|
| 188 |
+
return None
|
| 189 |
+
|
| 190 |
+
try:
|
| 191 |
+
backups = self.persistence_service.get_recent_backups()
|
| 192 |
+
if backups:
|
| 193 |
+
return {
|
| 194 |
+
"backup_events": backups,
|
| 195 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 196 |
+
}
|
| 197 |
+
except Exception as e:
|
| 198 |
+
logger.error(f"Error streaming backup events: {e}")
|
| 199 |
+
return None
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
# Global instance
|
| 203 |
+
integration_streamers = IntegrationStreamers()
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
# ============================================================================
|
| 207 |
+
# Background Streaming Tasks
|
| 208 |
+
# ============================================================================
|
| 209 |
+
|
| 210 |
+
async def start_integration_streams():
|
| 211 |
+
"""Start all integration stream tasks"""
|
| 212 |
+
logger.info("Starting integration WebSocket streams")
|
| 213 |
+
|
| 214 |
+
tasks = [
|
| 215 |
+
# HuggingFace Registry
|
| 216 |
+
asyncio.create_task(ws_manager.start_service_stream(
|
| 217 |
+
ServiceType.HUGGINGFACE,
|
| 218 |
+
integration_streamers.stream_hf,
|
| 219 |
+
interval=60.0 # 1 minute updates
|
| 220 |
+
)),
|
| 221 |
+
|
| 222 |
+
# Persistence Service
|
| 223 |
+
asyncio.create_task(ws_manager.start_service_stream(
|
| 224 |
+
ServiceType.PERSISTENCE,
|
| 225 |
+
integration_streamers.stream_persistence_status,
|
| 226 |
+
interval=30.0 # 30 second updates
|
| 227 |
+
)),
|
| 228 |
+
]
|
| 229 |
+
|
| 230 |
+
await asyncio.gather(*tasks, return_exceptions=True)
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
# ============================================================================
|
| 234 |
+
# WebSocket Endpoints
|
| 235 |
+
# ============================================================================
|
| 236 |
+
|
| 237 |
+
@router.websocket("/ws/integration")
|
| 238 |
+
async def websocket_integration_endpoint(websocket: WebSocket):
|
| 239 |
+
"""
|
| 240 |
+
Unified WebSocket endpoint for all integration services
|
| 241 |
+
|
| 242 |
+
Connection URL: ws://host:port/ws/integration
|
| 243 |
+
|
| 244 |
+
After connecting, send subscription messages:
|
| 245 |
+
{
|
| 246 |
+
"action": "subscribe",
|
| 247 |
+
"service": "huggingface" | "persistence" | "all"
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
To unsubscribe:
|
| 251 |
+
{
|
| 252 |
+
"action": "unsubscribe",
|
| 253 |
+
"service": "service_name"
|
| 254 |
+
}
|
| 255 |
+
"""
|
| 256 |
+
connection = await ws_manager.connect(websocket)
|
| 257 |
+
|
| 258 |
+
try:
|
| 259 |
+
while True:
|
| 260 |
+
data = await websocket.receive_json()
|
| 261 |
+
await ws_manager.handle_client_message(connection, data)
|
| 262 |
+
|
| 263 |
+
except WebSocketDisconnect:
|
| 264 |
+
logger.info(f"Integration client disconnected: {connection.client_id}")
|
| 265 |
+
except Exception as e:
|
| 266 |
+
logger.error(f"Integration WebSocket error: {e}")
|
| 267 |
+
finally:
|
| 268 |
+
await ws_manager.disconnect(connection.client_id)
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
@router.websocket("/ws/huggingface")
|
| 272 |
+
async def websocket_huggingface(websocket: WebSocket):
|
| 273 |
+
"""
|
| 274 |
+
Dedicated WebSocket endpoint for HuggingFace services
|
| 275 |
+
|
| 276 |
+
Auto-subscribes to huggingface service
|
| 277 |
+
"""
|
| 278 |
+
connection = await ws_manager.connect(websocket)
|
| 279 |
+
connection.subscribe(ServiceType.HUGGINGFACE)
|
| 280 |
+
|
| 281 |
+
try:
|
| 282 |
+
while True:
|
| 283 |
+
data = await websocket.receive_json()
|
| 284 |
+
await ws_manager.handle_client_message(connection, data)
|
| 285 |
+
except WebSocketDisconnect:
|
| 286 |
+
logger.info(f"HuggingFace client disconnected: {connection.client_id}")
|
| 287 |
+
except Exception as e:
|
| 288 |
+
logger.error(f"HuggingFace WebSocket error: {e}")
|
| 289 |
+
finally:
|
| 290 |
+
await ws_manager.disconnect(connection.client_id)
|
| 291 |
+
|
| 292 |
+
|
| 293 |
+
@router.websocket("/ws/persistence")
|
| 294 |
+
async def websocket_persistence(websocket: WebSocket):
|
| 295 |
+
"""
|
| 296 |
+
Dedicated WebSocket endpoint for persistence service
|
| 297 |
+
|
| 298 |
+
Auto-subscribes to persistence service
|
| 299 |
+
"""
|
| 300 |
+
connection = await ws_manager.connect(websocket)
|
| 301 |
+
connection.subscribe(ServiceType.PERSISTENCE)
|
| 302 |
+
|
| 303 |
+
try:
|
| 304 |
+
while True:
|
| 305 |
+
data = await websocket.receive_json()
|
| 306 |
+
await ws_manager.handle_client_message(connection, data)
|
| 307 |
+
except WebSocketDisconnect:
|
| 308 |
+
logger.info(f"Persistence client disconnected: {connection.client_id}")
|
| 309 |
+
except Exception as e:
|
| 310 |
+
logger.error(f"Persistence WebSocket error: {e}")
|
| 311 |
+
finally:
|
| 312 |
+
await ws_manager.disconnect(connection.client_id)
|
| 313 |
+
|
| 314 |
+
|
| 315 |
+
@router.websocket("/ws/ai")
|
| 316 |
+
async def websocket_ai(websocket: WebSocket):
|
| 317 |
+
"""
|
| 318 |
+
Dedicated WebSocket endpoint for AI/ML operations (alias for HuggingFace)
|
| 319 |
+
|
| 320 |
+
Auto-subscribes to huggingface service
|
| 321 |
+
"""
|
| 322 |
+
connection = await ws_manager.connect(websocket)
|
| 323 |
+
connection.subscribe(ServiceType.HUGGINGFACE)
|
| 324 |
+
|
| 325 |
+
try:
|
| 326 |
+
while True:
|
| 327 |
+
data = await websocket.receive_json()
|
| 328 |
+
await ws_manager.handle_client_message(connection, data)
|
| 329 |
+
except WebSocketDisconnect:
|
| 330 |
+
logger.info(f"AI client disconnected: {connection.client_id}")
|
| 331 |
+
except Exception as e:
|
| 332 |
+
logger.error(f"AI WebSocket error: {e}")
|
| 333 |
+
finally:
|
| 334 |
+
await ws_manager.disconnect(connection.client_id)
|
api_compat_routes.py
CHANGED
|
@@ -1,161 +1,240 @@
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
"""
|
| 3 |
-
Short Hunter /
|
| 4 |
|
| 5 |
-
|
| 6 |
-
-
|
| 7 |
-
This file intentionally keeps the Space as a multi-source data hub. It does not
|
| 8 |
-
remove JSON registries, provider managers, HF model files, or existing resources.
|
| 9 |
-
It only repairs compatibility routes that Short Hunter was calling incorrectly:
|
| 10 |
|
| 11 |
-
|
| 12 |
-
- /
|
| 13 |
-
-
|
|
|
|
|
|
|
| 14 |
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
Failures return structured JSON instead of crashing the whole datasource.
|
| 18 |
"""
|
| 19 |
|
| 20 |
from __future__ import annotations
|
| 21 |
|
| 22 |
import logging
|
|
|
|
|
|
|
| 23 |
from datetime import datetime, timezone
|
| 24 |
from typing import Any, Dict, List, Optional, Tuple
|
| 25 |
|
| 26 |
import httpx
|
| 27 |
from fastapi import APIRouter, Query
|
| 28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
logger = logging.getLogger(__name__)
|
| 30 |
|
|
|
|
|
|
|
| 31 |
HEADERS = {
|
| 32 |
-
"User-Agent": "Mozilla/5.0 (compatible; CryptoDataHub/4.0;
|
| 33 |
"Accept": "application/json",
|
| 34 |
}
|
| 35 |
|
| 36 |
BINANCE_INTERVALS = {
|
| 37 |
"1m": "1m",
|
|
|
|
| 38 |
"5m": "5m",
|
| 39 |
"15m": "15m",
|
| 40 |
"30m": "30m",
|
| 41 |
"1h": "1h",
|
|
|
|
| 42 |
"4h": "4h",
|
|
|
|
|
|
|
|
|
|
| 43 |
"1d": "1d",
|
|
|
|
| 44 |
"1w": "1w",
|
|
|
|
| 45 |
}
|
| 46 |
|
| 47 |
-
|
| 48 |
"1m": "1min",
|
|
|
|
| 49 |
"5m": "5min",
|
| 50 |
"15m": "15min",
|
| 51 |
"30m": "30min",
|
| 52 |
"1h": "1hour",
|
|
|
|
| 53 |
"4h": "4hour",
|
|
|
|
|
|
|
| 54 |
"1d": "1day",
|
| 55 |
"1w": "1week",
|
| 56 |
}
|
| 57 |
|
| 58 |
-
CRYPTOCOMPARE_HIST = {
|
| 59 |
-
"1m": "histominute",
|
| 60 |
-
"5m": "histominute",
|
| 61 |
-
"15m": "histominute",
|
| 62 |
-
"30m": "histominute",
|
| 63 |
-
"1h": "histohour",
|
| 64 |
-
"4h": "histohour",
|
| 65 |
-
"1d": "histoday",
|
| 66 |
-
"1w": "histoday",
|
| 67 |
-
}
|
| 68 |
-
|
| 69 |
-
# Common aliases for CoinGecko community/social fallback. This is deliberately
|
| 70 |
-
# small and safe; unknown coins simply skip CoinGecko-specific fallbacks.
|
| 71 |
-
COINGECKO_IDS = {
|
| 72 |
-
"BTC": "bitcoin",
|
| 73 |
-
"ETH": "ethereum",
|
| 74 |
-
"BNB": "binancecoin",
|
| 75 |
-
"SOL": "solana",
|
| 76 |
-
"XRP": "ripple",
|
| 77 |
-
"ADA": "cardano",
|
| 78 |
-
"DOGE": "dogecoin",
|
| 79 |
-
"AVAX": "avalanche-2",
|
| 80 |
-
"LINK": "chainlink",
|
| 81 |
-
"DOT": "polkadot",
|
| 82 |
-
"TRX": "tron",
|
| 83 |
-
"MATIC": "matic-network",
|
| 84 |
-
"POL": "polygon-ecosystem-token",
|
| 85 |
-
"LTC": "litecoin",
|
| 86 |
-
"BCH": "bitcoin-cash",
|
| 87 |
-
"UNI": "uniswap",
|
| 88 |
-
"ATOM": "cosmos",
|
| 89 |
-
"NEAR": "near",
|
| 90 |
-
"ICP": "internet-computer",
|
| 91 |
-
"ETC": "ethereum-classic",
|
| 92 |
-
"XMR": "monero",
|
| 93 |
-
"APT": "aptos",
|
| 94 |
-
"ARB": "arbitrum",
|
| 95 |
-
"OP": "optimism",
|
| 96 |
-
"PEPE": "pepe",
|
| 97 |
-
"SHIB": "shiba-inu",
|
| 98 |
-
"HBAR": "hedera-hashgraph",
|
| 99 |
-
"CRO": "crypto-com-chain",
|
| 100 |
-
"TAO": "bittensor",
|
| 101 |
-
"ONDO": "ondo-finance",
|
| 102 |
-
"PAXG": "pax-gold",
|
| 103 |
-
"XAUT": "tether-gold",
|
| 104 |
-
}
|
| 105 |
-
|
| 106 |
-
router = APIRouter(tags=["Compat API"])
|
| 107 |
-
|
| 108 |
|
| 109 |
def _now() -> str:
|
| 110 |
return datetime.now(timezone.utc).isoformat()
|
| 111 |
|
| 112 |
|
| 113 |
-
def
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
|
| 120 |
|
| 121 |
def _normalize_symbol(symbol: str) -> str:
|
| 122 |
-
|
| 123 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
|
| 125 |
|
| 126 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
return f"{_base_asset(symbol)}-USDT"
|
| 128 |
|
| 129 |
|
| 130 |
-
def
|
| 131 |
-
|
| 132 |
-
"
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
|
| 143 |
|
| 144 |
-
async def _get_json(url: str, params: Optional[Dict[str, Any]] = None, timeout: float =
|
| 145 |
try:
|
| 146 |
-
|
|
|
|
| 147 |
response = await client.get(url, params=params)
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
return None, f"HTTP {status} from {url}", status
|
| 151 |
try:
|
| 152 |
-
return response.json(), None,
|
| 153 |
-
except Exception as exc:
|
| 154 |
-
return None, f"JSON
|
| 155 |
-
except Exception as exc:
|
| 156 |
-
return None, f"
|
|
|
|
| 157 |
|
| 158 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
async def _fetch_coingecko_markets(limit: int = 100) -> Tuple[List[Dict[str, Any]], List[str]]:
|
| 160 |
url = "https://api.coingecko.com/api/v3/coins/markets"
|
| 161 |
params = {
|
|
@@ -168,271 +247,230 @@ async def _fetch_coingecko_markets(limit: int = 100) -> Tuple[List[Dict[str, Any
|
|
| 168 |
}
|
| 169 |
payload, error, _ = await _get_json(url, params=params, timeout=20.0)
|
| 170 |
if error or not isinstance(payload, list):
|
| 171 |
-
return [], [error or "CoinGecko returned non-list
|
| 172 |
return payload, []
|
| 173 |
|
| 174 |
|
| 175 |
-
async def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
pair = _normalize_symbol(symbol)
|
| 177 |
mapped = BINANCE_INTERVALS.get(interval, interval)
|
| 178 |
payload, error, _ = await _get_json(
|
| 179 |
"https://api.binance.com/api/v3/klines",
|
| 180 |
params={"symbol": pair, "interval": mapped, "limit": min(max(limit, 1), 1000)},
|
| 181 |
-
timeout=
|
| 182 |
)
|
| 183 |
-
if error:
|
| 184 |
-
return [], f"
|
| 185 |
-
if not isinstance(payload, list):
|
| 186 |
-
return [], "binance_klines: non-list payload"
|
| 187 |
candles = []
|
| 188 |
for row in payload:
|
| 189 |
try:
|
| 190 |
-
candles.append(
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
"volume": float(row[5]),
|
| 199 |
-
}
|
| 200 |
-
)
|
| 201 |
except Exception:
|
| 202 |
continue
|
| 203 |
-
return candles,
|
| 204 |
|
| 205 |
|
| 206 |
-
async def _fetch_kucoin_klines(symbol: str,
|
| 207 |
-
|
| 208 |
-
|
|
|
|
| 209 |
payload, error, _ = await _get_json(
|
| 210 |
"https://api.kucoin.com/api/v1/market/candles",
|
| 211 |
-
params={"symbol":
|
| 212 |
-
timeout=
|
| 213 |
)
|
| 214 |
-
if error:
|
| 215 |
-
return [], f"
|
| 216 |
-
|
| 217 |
-
if not isinstance(rows, list):
|
| 218 |
-
return [], "kucoin_klines: missing data[]"
|
| 219 |
candles = []
|
| 220 |
-
|
| 221 |
-
for row in rows[: min(max(limit, 1), 1500)]:
|
| 222 |
try:
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
"low": float(row[4]),
|
| 232 |
-
"volume": float(row[5]),
|
| 233 |
-
}
|
| 234 |
-
)
|
| 235 |
except Exception:
|
| 236 |
continue
|
| 237 |
candles.sort(key=lambda c: c["timestamp"])
|
| 238 |
-
return candles[-limit:],
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
aggregate =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 254 |
payload, error, _ = await _get_json(
|
| 255 |
f"https://min-api.cryptocompare.com/data/v2/{endpoint}",
|
| 256 |
-
params=
|
| 257 |
-
timeout=
|
| 258 |
)
|
| 259 |
-
if error:
|
| 260 |
-
return [], f"
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
candles = []
|
| 265 |
-
for row in data:
|
| 266 |
try:
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
"close": float(row.get("close", 0)),
|
| 276 |
-
"volume": float(row.get("volumefrom", row.get("volumeto", 0)) or 0),
|
| 277 |
-
}
|
| 278 |
-
)
|
| 279 |
except Exception:
|
| 280 |
continue
|
| 281 |
-
return candles,
|
| 282 |
|
|
|
|
|
|
|
| 283 |
|
| 284 |
-
|
|
|
|
|
|
|
|
|
|
| 285 |
errors: List[str] = []
|
| 286 |
-
providers = [
|
| 287 |
-
("binance_public", _fetch_binance_klines),
|
| 288 |
-
("kucoin_public", _fetch_kucoin_klines),
|
| 289 |
-
("cryptocompare_public", _fetch_cryptocompare_klines),
|
| 290 |
-
]
|
| 291 |
-
for name, fn in providers:
|
| 292 |
-
candles, error = await fn(symbol, interval, limit)
|
| 293 |
-
if candles:
|
| 294 |
-
return candles, name, errors
|
| 295 |
-
if error:
|
| 296 |
-
errors.append(error)
|
| 297 |
-
return [], "none", errors or ["all OHLCV providers returned empty data"]
|
| 298 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 299 |
|
| 300 |
-
|
|
|
|
| 301 |
pair = _normalize_symbol(symbol)
|
| 302 |
payload, error, _ = await _get_json(
|
| 303 |
"https://api.binance.com/api/v3/ticker/24hr",
|
| 304 |
params={"symbol": pair},
|
| 305 |
timeout=15.0,
|
| 306 |
)
|
| 307 |
-
if error:
|
| 308 |
-
return
|
| 309 |
-
return payload
|
| 310 |
|
| 311 |
|
| 312 |
-
async def
|
| 313 |
-
|
|
|
|
|
|
|
| 314 |
payload, error, _ = await _get_json(
|
| 315 |
-
"https://api.kucoin.com/api/v1/market/
|
| 316 |
-
params={"symbol":
|
| 317 |
timeout=15.0,
|
| 318 |
)
|
| 319 |
-
if error:
|
| 320 |
-
return
|
| 321 |
-
|
| 322 |
-
return data if isinstance(data, dict) else None, None
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
async def _fetch_volume_multi(symbol: str) -> Dict[str, Any]:
|
| 326 |
-
errors: List[str] = []
|
| 327 |
-
ticker, err = await _fetch_binance_ticker(symbol)
|
| 328 |
-
if ticker:
|
| 329 |
-
return {
|
| 330 |
-
"success": True,
|
| 331 |
-
"errors": [],
|
| 332 |
-
"symbol": _normalize_symbol(symbol),
|
| 333 |
-
"price": float(ticker.get("lastPrice", 0) or 0),
|
| 334 |
-
"volume": float(ticker.get("volume", 0) or 0),
|
| 335 |
-
"base_volume": float(ticker.get("volume", 0) or 0),
|
| 336 |
-
"quote_volume": float(ticker.get("quoteVolume", 0) or 0),
|
| 337 |
-
"change_percent": float(ticker.get("priceChangePercent", 0) or 0),
|
| 338 |
-
"high": float(ticker.get("highPrice", 0) or 0),
|
| 339 |
-
"low": float(ticker.get("lowPrice", 0) or 0),
|
| 340 |
-
"source": "binance_public_24hr",
|
| 341 |
-
"timestamp": _now(),
|
| 342 |
-
}
|
| 343 |
-
if err:
|
| 344 |
-
errors.append(err)
|
| 345 |
-
|
| 346 |
-
stats, err = await _fetch_kucoin_stats(symbol)
|
| 347 |
-
if stats:
|
| 348 |
-
return {
|
| 349 |
-
"success": True,
|
| 350 |
-
"errors": [],
|
| 351 |
-
"symbol": _normalize_symbol(symbol),
|
| 352 |
-
"price": float(stats.get("last", 0) or 0),
|
| 353 |
-
"volume": float(stats.get("vol", 0) or 0),
|
| 354 |
-
"base_volume": float(stats.get("vol", 0) or 0),
|
| 355 |
-
"quote_volume": float(stats.get("volValue", 0) or 0),
|
| 356 |
-
"change_percent": float(stats.get("changeRate", 0) or 0) * 100,
|
| 357 |
-
"high": float(stats.get("high", 0) or 0),
|
| 358 |
-
"low": float(stats.get("low", 0) or 0),
|
| 359 |
-
"source": "kucoin_public_stats",
|
| 360 |
-
"timestamp": _now(),
|
| 361 |
-
}
|
| 362 |
-
if err:
|
| 363 |
-
errors.append(err)
|
| 364 |
-
|
| 365 |
-
return _soft_error("volume", symbol, errors or ["all volume providers failed"])
|
| 366 |
|
| 367 |
|
| 368 |
-
async def _fetch_binance_orderbook(symbol: str, limit: int) -> Tuple[
|
| 369 |
pair = _normalize_symbol(symbol)
|
| 370 |
payload, error, _ = await _get_json(
|
| 371 |
"https://api.binance.com/api/v3/depth",
|
| 372 |
params={"symbol": pair, "limit": min(max(limit, 5), 100)},
|
| 373 |
timeout=15.0,
|
| 374 |
)
|
| 375 |
-
if error:
|
| 376 |
-
return
|
| 377 |
-
return payload
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
async def _fetch_kucoin_orderbook(symbol: str, limit: int) -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
|
| 381 |
-
pair = _kucoin_pair(symbol)
|
| 382 |
-
# KuCoin level2_20 ignores custom depth but is stable/free.
|
| 383 |
-
payload, error, _ = await _get_json(
|
| 384 |
-
"https://api.kucoin.com/api/v1/market/orderbook/level2_20",
|
| 385 |
-
params={"symbol": pair},
|
| 386 |
-
timeout=15.0,
|
| 387 |
-
)
|
| 388 |
-
if error:
|
| 389 |
-
return None, f"kucoin_orderbook: {error}"
|
| 390 |
-
data = (payload or {}).get("data") if isinstance(payload, dict) else None
|
| 391 |
-
return data if isinstance(data, dict) else None, None
|
| 392 |
|
| 393 |
|
| 394 |
-
async def
|
| 395 |
errors: List[str] = []
|
| 396 |
-
book, err = await _fetch_binance_orderbook(symbol, limit)
|
| 397 |
-
if book and (book.get("bids") or book.get("asks")):
|
| 398 |
-
return {
|
| 399 |
-
"success": True,
|
| 400 |
-
"errors": [],
|
| 401 |
-
"symbol": _normalize_symbol(symbol),
|
| 402 |
-
"bids": [[float(p), float(q)] for p, q in (book.get("bids") or [])[:limit]],
|
| 403 |
-
"asks": [[float(p), float(q)] for p, q in (book.get("asks") or [])[:limit]],
|
| 404 |
-
"source": "binance_public_depth",
|
| 405 |
-
"timestamp": _now(),
|
| 406 |
-
}
|
| 407 |
-
if err:
|
| 408 |
-
errors.append(err)
|
| 409 |
-
|
| 410 |
book, err = await _fetch_kucoin_orderbook(symbol, limit)
|
| 411 |
-
if book
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
"bids": [[float(p), float(q)] for p, q in (book.get("bids") or [])[:limit]],
|
| 417 |
-
"asks": [[float(p), float(q)] for p, q in (book.get("asks") or [])[:limit]],
|
| 418 |
-
"source": "kucoin_public_level2_20",
|
| 419 |
-
"timestamp": _now(),
|
| 420 |
-
}
|
| 421 |
-
if err:
|
| 422 |
-
errors.append(err)
|
| 423 |
|
| 424 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 425 |
|
| 426 |
|
| 427 |
-
async def _fetch_fear_greed() -> Tuple[
|
| 428 |
payload, error, _ = await _get_json(
|
| 429 |
"https://api.alternative.me/fng/",
|
| 430 |
params={"limit": 1, "format": "json"},
|
| 431 |
timeout=15.0,
|
| 432 |
)
|
| 433 |
-
if error:
|
| 434 |
-
return
|
| 435 |
-
return payload
|
| 436 |
|
| 437 |
|
| 438 |
def _rsi(closes: List[float], period: int = 14) -> Optional[float]:
|
|
@@ -453,43 +491,75 @@ def _rsi(closes: List[float], period: int = 14) -> Optional[float]:
|
|
| 453 |
|
| 454 |
|
| 455 |
def _ema(values: List[float], period: int) -> Optional[float]:
|
| 456 |
-
if
|
| 457 |
return None
|
| 458 |
-
|
| 459 |
-
ema =
|
| 460 |
-
for
|
| 461 |
-
ema =
|
| 462 |
-
return round(ema,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 463 |
|
| 464 |
|
| 465 |
@router.get("/api/coins/top")
|
| 466 |
async def coins_top(limit: int = Query(100, ge=1, le=250)):
|
| 467 |
markets, errors = await _fetch_coingecko_markets(limit)
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
"name": item.get("name"),
|
| 473 |
-
"current_price": item.get("current_price"),
|
| 474 |
-
"price": item.get("current_price"),
|
| 475 |
-
"market_cap": item.get("market_cap"),
|
| 476 |
-
"volume_24h": item.get("total_volume"),
|
| 477 |
-
"price_change_percentage_24h": item.get("price_change_percentage_24h"),
|
| 478 |
-
"change_24h": item.get("price_change_percentage_24h"),
|
| 479 |
-
"market_cap_rank": item.get("market_cap_rank"),
|
| 480 |
-
"image": item.get("image"),
|
| 481 |
-
}
|
| 482 |
-
for item in markets
|
| 483 |
-
]
|
| 484 |
-
return {
|
| 485 |
-
"success": bool(coins),
|
| 486 |
-
"errors": errors,
|
| 487 |
-
"coins": coins,
|
| 488 |
-
"data": coins,
|
| 489 |
-
"count": len(coins),
|
| 490 |
-
"source": "coingecko_markets_free",
|
| 491 |
-
"timestamp": _now(),
|
| 492 |
-
}
|
| 493 |
|
| 494 |
|
| 495 |
@router.get("/api/top-coins")
|
|
@@ -498,71 +568,16 @@ async def coins_top_alias(limit: int = Query(100, ge=1, le=250)):
|
|
| 498 |
return await coins_top(limit=limit)
|
| 499 |
|
| 500 |
|
| 501 |
-
@router.get("/api/market/gainers")
|
| 502 |
-
async def market_gainers(limit: int = Query(50, ge=1, le=100)):
|
| 503 |
-
markets, errors = await _fetch_coingecko_markets(max(limit * 2, 50))
|
| 504 |
-
gainers = sorted(
|
| 505 |
-
[m for m in markets if (m.get("price_change_percentage_24h") or 0) > 0],
|
| 506 |
-
key=lambda x: x.get("price_change_percentage_24h") or 0,
|
| 507 |
-
reverse=True,
|
| 508 |
-
)[:limit]
|
| 509 |
-
rows = [
|
| 510 |
-
{
|
| 511 |
-
"symbol": str(g.get("symbol", "")).upper(),
|
| 512 |
-
"name": g.get("name"),
|
| 513 |
-
"price": g.get("current_price"),
|
| 514 |
-
"change_24h": g.get("price_change_percentage_24h"),
|
| 515 |
-
"volume_24h": g.get("total_volume"),
|
| 516 |
-
}
|
| 517 |
-
for g in gainers
|
| 518 |
-
]
|
| 519 |
-
return {"success": bool(rows), "errors": errors, "gainers": rows, "data": rows, "count": len(rows), "source": "coingecko_markets_free", "timestamp": _now()}
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
@router.get("/api/market/losers")
|
| 523 |
-
async def market_losers(limit: int = Query(50, ge=1, le=100)):
|
| 524 |
-
markets, errors = await _fetch_coingecko_markets(max(limit * 2, 50))
|
| 525 |
-
losers = sorted(
|
| 526 |
-
[m for m in markets if (m.get("price_change_percentage_24h") or 0) < 0],
|
| 527 |
-
key=lambda x: x.get("price_change_percentage_24h") or 0,
|
| 528 |
-
)[:limit]
|
| 529 |
-
rows = [
|
| 530 |
-
{
|
| 531 |
-
"symbol": str(l.get("symbol", "")).upper(),
|
| 532 |
-
"name": l.get("name"),
|
| 533 |
-
"price": l.get("current_price"),
|
| 534 |
-
"change_24h": l.get("price_change_percentage_24h"),
|
| 535 |
-
"volume_24h": l.get("total_volume"),
|
| 536 |
-
}
|
| 537 |
-
for l in losers
|
| 538 |
-
]
|
| 539 |
-
return {"success": bool(rows), "errors": errors, "losers": rows, "data": rows, "count": len(rows), "source": "coingecko_markets_free", "timestamp": _now()}
|
| 540 |
-
|
| 541 |
-
|
| 542 |
@router.get("/api/trading/ohlcv/{symbol}")
|
| 543 |
async def trading_ohlcv(symbol: str, timeframe: str = "1h", limit: int = 100):
|
| 544 |
-
candles, source, errors = await
|
| 545 |
if not candles:
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
return payload
|
| 549 |
-
return {
|
| 550 |
-
"success": True,
|
| 551 |
-
"errors": errors,
|
| 552 |
-
"symbol": _normalize_symbol(symbol),
|
| 553 |
-
"timeframe": timeframe,
|
| 554 |
-
"candles": candles,
|
| 555 |
-
"ohlcv": candles,
|
| 556 |
-
"data": candles,
|
| 557 |
-
"count": len(candles),
|
| 558 |
-
"source": source,
|
| 559 |
-
"timestamp": _now(),
|
| 560 |
-
}
|
| 561 |
|
| 562 |
|
| 563 |
@router.get("/api/ohlcv")
|
| 564 |
@router.get("/api/klines")
|
| 565 |
-
@router.get("/api/history")
|
| 566 |
async def trading_ohlcv_alias(
|
| 567 |
symbol: str = Query("BTCUSDT"),
|
| 568 |
interval: str = Query("1h"),
|
|
@@ -572,176 +587,227 @@ async def trading_ohlcv_alias(
|
|
| 572 |
return await trading_ohlcv(symbol=symbol, timeframe=(timeframe or interval), limit=limit)
|
| 573 |
|
| 574 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 575 |
@router.get("/api/trading/stats/24h/{symbol}")
|
| 576 |
async def trading_stats_24h(symbol: str):
|
| 577 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 578 |
|
| 579 |
|
| 580 |
-
@router.get("/api/trading/
|
| 581 |
-
async def
|
| 582 |
-
|
|
|
|
|
|
|
|
|
|
| 583 |
|
| 584 |
|
| 585 |
-
@router.get("/api/
|
| 586 |
-
async def
|
| 587 |
-
return await
|
| 588 |
|
| 589 |
|
| 590 |
-
@router.get("/api/
|
| 591 |
-
async def
|
| 592 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 593 |
|
| 594 |
|
| 595 |
-
@router.get("/api/
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
):
|
| 602 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 603 |
|
| 604 |
|
| 605 |
@router.get("/api/social/sentiment")
|
| 606 |
async def social_sentiment(coin: str = "BTC", timeframe: str = "24h"):
|
| 607 |
-
|
| 608 |
-
if not
|
| 609 |
-
return
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
"coin": coin.upper(),
|
| 613 |
-
"timeframe": timeframe,
|
| 614 |
-
"sentiment_score": 0,
|
| 615 |
-
"fear_greed_index": None,
|
| 616 |
-
"label": "Neutral",
|
| 617 |
-
"source": "alternative_me_failed",
|
| 618 |
-
"timestamp": _now(),
|
| 619 |
-
}
|
| 620 |
-
entry = (fng.get("data") or [{}])[0]
|
| 621 |
-
score = int(entry.get("value", 50))
|
| 622 |
normalized = round((score - 50) / 50, 4)
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
"
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
"
|
| 634 |
-
|
| 635 |
-
"timestamp": _now(),
|
| 636 |
-
}
|
| 637 |
|
| 638 |
|
| 639 |
@router.get("/api/sentiment/global")
|
| 640 |
async def sentiment_global():
|
| 641 |
-
return await social_sentiment(coin="GLOBAL"
|
| 642 |
|
| 643 |
|
| 644 |
@router.get("/api/sentiment/asset/{symbol}")
|
| 645 |
async def sentiment_asset(symbol: str):
|
| 646 |
-
return await social_sentiment(coin=_base_asset(symbol)
|
| 647 |
|
| 648 |
|
| 649 |
@router.get("/api/ai/sentiment")
|
| 650 |
-
async def
|
| 651 |
return await sentiment_asset(symbol)
|
| 652 |
|
| 653 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 654 |
@router.get("/api/news/{coin_id}")
|
| 655 |
async def coin_news(coin_id: str, limit: int = Query(5, ge=1, le=20)):
|
|
|
|
| 656 |
payload, error, _ = await _get_json(
|
| 657 |
"https://min-api.cryptocompare.com/data/v2/news/",
|
| 658 |
params={"lang": "EN", "categories": coin_id.upper(), "excludeCategories": "Sponsored"},
|
| 659 |
timeout=15.0,
|
| 660 |
)
|
| 661 |
if error or not isinstance(payload, dict):
|
| 662 |
-
return
|
| 663 |
articles = (payload.get("Data") or [])[:limit]
|
| 664 |
news = [
|
| 665 |
{
|
| 666 |
"title": a.get("title"),
|
| 667 |
"url": a.get("url"),
|
| 668 |
"source": a.get("source"),
|
| 669 |
-
"published_at": datetime.fromtimestamp(a.get("published_on", 0),
|
| 670 |
}
|
| 671 |
for a in articles
|
| 672 |
]
|
| 673 |
-
return
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
@router.get("/api/
|
| 677 |
-
async def
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
"success": True,
|
| 695 |
-
"errors": errors,
|
| 696 |
-
"symbol": _normalize_symbol(symbol),
|
| 697 |
-
"timeframe": timeframe,
|
| 698 |
-
"current_price": current,
|
| 699 |
-
"indicators": {
|
| 700 |
-
"rsi": {"value": _rsi(closes)},
|
| 701 |
-
"macd": {"value": round(macd, 6) if macd is not None else None, "ema12": ema12, "ema26": ema26},
|
| 702 |
-
"bollinger_bands": {"upper": sma20 + 2 * std, "middle": sma20, "lower": sma20 - 2 * std},
|
| 703 |
-
"ema": {"ema12": ema12, "ema26": ema26},
|
| 704 |
-
},
|
| 705 |
-
"source": f"{source}_derived_indicators",
|
| 706 |
-
"timestamp": _now(),
|
| 707 |
-
}
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
@router.get("/api/indicators")
|
| 711 |
-
@router.get("/api/indicators/rsi")
|
| 712 |
-
@router.get("/api/indicators/macd")
|
| 713 |
-
async def indicators_alias(
|
| 714 |
-
symbol: str = Query("BTCUSDT"),
|
| 715 |
-
interval: str = Query("1h"),
|
| 716 |
-
timeframe: Optional[str] = Query(None),
|
| 717 |
-
):
|
| 718 |
-
return await indicators_comprehensive(symbol=symbol, timeframe=(timeframe or interval))
|
| 719 |
|
| 720 |
|
| 721 |
-
@router.get("/api/debug/capabilities
|
| 722 |
-
async def
|
| 723 |
-
|
| 724 |
-
"
|
| 725 |
-
"
|
| 726 |
-
"
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
"
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 742 |
}
|
|
|
|
| 743 |
|
| 744 |
|
| 745 |
def register_compat_routes(app) -> None:
|
| 746 |
app.include_router(router)
|
| 747 |
-
logger.info("Registered Short Hunter compatibility routes
|
|
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
"""
|
| 3 |
+
Short Hunter / V4-compatible API routes for Datasourceforcryptocurrency-4.
|
| 4 |
|
| 5 |
+
This module preserves the Space as a multi-source free crypto data hub and
|
| 6 |
+
adds stable machine-readable compatibility routes for external clients.
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
+
Providers used:
|
| 9 |
+
- CoinGecko public API for market/top/trending discovery.
|
| 10 |
+
- Binance public API for OHLCV/ticker fallback.
|
| 11 |
+
- KuCoin public API for orderbook primary and OHLCV/ticker fallback where useful.
|
| 12 |
+
- Alternative.me public API for global fear/greed sentiment.
|
| 13 |
|
| 14 |
+
No private/write exchange endpoints are used.
|
| 15 |
+
No secrets are required.
|
|
|
|
| 16 |
"""
|
| 17 |
|
| 18 |
from __future__ import annotations
|
| 19 |
|
| 20 |
import logging
|
| 21 |
+
import os
|
| 22 |
+
import json
|
| 23 |
from datetime import datetime, timezone
|
| 24 |
from typing import Any, Dict, List, Optional, Tuple
|
| 25 |
|
| 26 |
import httpx
|
| 27 |
from fastapi import APIRouter, Query
|
| 28 |
|
| 29 |
+
try:
|
| 30 |
+
from api_hub_registry import load_provider_catalog, provider_runtime_summary, get_secret, rotation_plan
|
| 31 |
+
except Exception: # safe fallback for legacy runtime
|
| 32 |
+
def load_provider_catalog():
|
| 33 |
+
return {"metadata": {"name": "provider catalog unavailable"}, "categories": {}}
|
| 34 |
+
def provider_runtime_summary():
|
| 35 |
+
return {"totalProviders": 0, "categories": {}}
|
| 36 |
+
def get_secret(name):
|
| 37 |
+
return None
|
| 38 |
+
def rotation_plan(capability):
|
| 39 |
+
return {"capability": capability, "primaryOrder": [], "allCandidates": []}
|
| 40 |
+
|
| 41 |
logger = logging.getLogger(__name__)
|
| 42 |
|
| 43 |
+
router = APIRouter(tags=["Compat API"])
|
| 44 |
+
|
| 45 |
HEADERS = {
|
| 46 |
+
"User-Agent": "Mozilla/5.0 (compatible; CryptoDataHub/4.0; ShortHunterCompat)",
|
| 47 |
"Accept": "application/json",
|
| 48 |
}
|
| 49 |
|
| 50 |
BINANCE_INTERVALS = {
|
| 51 |
"1m": "1m",
|
| 52 |
+
"3m": "3m",
|
| 53 |
"5m": "5m",
|
| 54 |
"15m": "15m",
|
| 55 |
"30m": "30m",
|
| 56 |
"1h": "1h",
|
| 57 |
+
"2h": "2h",
|
| 58 |
"4h": "4h",
|
| 59 |
+
"6h": "6h",
|
| 60 |
+
"8h": "8h",
|
| 61 |
+
"12h": "12h",
|
| 62 |
"1d": "1d",
|
| 63 |
+
"3d": "3d",
|
| 64 |
"1w": "1w",
|
| 65 |
+
"1M": "1M",
|
| 66 |
}
|
| 67 |
|
| 68 |
+
KUCOIN_TYPES = {
|
| 69 |
"1m": "1min",
|
| 70 |
+
"3m": "3min",
|
| 71 |
"5m": "5min",
|
| 72 |
"15m": "15min",
|
| 73 |
"30m": "30min",
|
| 74 |
"1h": "1hour",
|
| 75 |
+
"2h": "2hour",
|
| 76 |
"4h": "4hour",
|
| 77 |
+
"8h": "8hour",
|
| 78 |
+
"12h": "12hour",
|
| 79 |
"1d": "1day",
|
| 80 |
"1w": "1week",
|
| 81 |
}
|
| 82 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
|
| 84 |
def _now() -> str:
|
| 85 |
return datetime.now(timezone.utc).isoformat()
|
| 86 |
|
| 87 |
|
| 88 |
+
def _ok(data: Any = None, **extra: Any) -> Dict[str, Any]:
|
| 89 |
+
payload = {
|
| 90 |
+
"success": True,
|
| 91 |
+
"errors": [],
|
| 92 |
+
"timestamp": _now(),
|
| 93 |
+
}
|
| 94 |
+
if data is not None:
|
| 95 |
+
payload["data"] = data
|
| 96 |
+
payload.update(extra)
|
| 97 |
+
return payload
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def _fail(message: str, *, status: str = "unavailable", source: str = "compat", **extra: Any) -> Dict[str, Any]:
|
| 101 |
+
payload = {
|
| 102 |
+
"success": False,
|
| 103 |
+
"status": status,
|
| 104 |
+
"errors": [str(message)],
|
| 105 |
+
"source": source,
|
| 106 |
+
"timestamp": _now(),
|
| 107 |
+
}
|
| 108 |
+
payload.update(extra)
|
| 109 |
+
return payload
|
| 110 |
|
| 111 |
|
| 112 |
def _normalize_symbol(symbol: str) -> str:
|
| 113 |
+
raw = (symbol or "BTCUSDT").upper().strip().replace("-", "").replace("/", "")
|
| 114 |
+
if raw.endswith("USDT"):
|
| 115 |
+
return raw
|
| 116 |
+
if raw.endswith("USD") and not raw.endswith("USDT"):
|
| 117 |
+
raw = raw[:-3]
|
| 118 |
+
return f"{raw}USDT"
|
| 119 |
|
| 120 |
|
| 121 |
+
def _base_asset(symbol: str) -> str:
|
| 122 |
+
s = _normalize_symbol(symbol)
|
| 123 |
+
return s[:-4] if s.endswith("USDT") else s
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def _kucoin_symbol(symbol: str) -> str:
|
| 127 |
return f"{_base_asset(symbol)}-USDT"
|
| 128 |
|
| 129 |
|
| 130 |
+
def _float(value: Any, default: float = 0.0) -> float:
|
| 131 |
+
try:
|
| 132 |
+
if value is None or value == "":
|
| 133 |
+
return default
|
| 134 |
+
return float(value)
|
| 135 |
+
except Exception:
|
| 136 |
+
return default
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def _int(value: Any, default: int = 0) -> int:
|
| 140 |
+
try:
|
| 141 |
+
if value is None or value == "":
|
| 142 |
+
return default
|
| 143 |
+
return int(float(value))
|
| 144 |
+
except Exception:
|
| 145 |
+
return default
|
| 146 |
|
| 147 |
|
| 148 |
+
async def _get_json(url: str, *, params: Optional[Dict[str, Any]] = None, timeout: float = 15.0) -> Tuple[Optional[Any], Optional[str], int]:
|
| 149 |
try:
|
| 150 |
+
timeout_value = min(float(timeout or 8.0), 8.0)
|
| 151 |
+
async with httpx.AsyncClient(timeout=httpx.Timeout(timeout_value, connect=3.0), headers=HEADERS) as client:
|
| 152 |
response = await client.get(url, params=params)
|
| 153 |
+
if response.status_code < 200 or response.status_code >= 300:
|
| 154 |
+
return None, f"HTTP {response.status_code} from {url}", response.status_code
|
|
|
|
| 155 |
try:
|
| 156 |
+
return response.json(), None, response.status_code
|
| 157 |
+
except Exception as exc:
|
| 158 |
+
return None, f"Invalid JSON from {url}: {exc}", response.status_code
|
| 159 |
+
except Exception as exc:
|
| 160 |
+
return None, f"Request failed for {url}: {exc}", 0
|
| 161 |
+
|
| 162 |
|
| 163 |
|
| 164 |
+
async def _fetch_coinmarketcap_quotes(limit: int = 100) -> Tuple[List[Dict[str, Any]], List[str]]:
|
| 165 |
+
"""Optional CoinMarketCap provider. Requires COINMARKETCAP_KEY/CMC_API_KEY in Space secrets."""
|
| 166 |
+
key = get_secret("COINMARKETCAP_KEY")
|
| 167 |
+
if not key:
|
| 168 |
+
return [], ["CoinMarketCap key not configured"]
|
| 169 |
+
symbols = "BTC,ETH,BNB,SOL,XRP,DOGE,ADA,TRX,AVAX,LINK,DOT,MATIC,TON,LTC,BCH,UNI,ATOM,ETC,APT,ARB,OP,NEAR,FIL,INJ,SUI,SEI"
|
| 170 |
+
payload, error, _ = await _get_json(
|
| 171 |
+
"https://pro-api.coinmarketcap.com/v1/cryptocurrency/quotes/latest",
|
| 172 |
+
params={"symbol": symbols, "convert": "USD"},
|
| 173 |
+
timeout=15.0,
|
| 174 |
+
)
|
| 175 |
+
# _get_json cannot add dynamic CMC header, so use a custom request here.
|
| 176 |
+
if payload is None:
|
| 177 |
+
try:
|
| 178 |
+
async with httpx.AsyncClient(timeout=httpx.Timeout(8.0, connect=3.0), headers={**HEADERS, "X-CMC_PRO_API_KEY": key}) as client:
|
| 179 |
+
response = await client.get(
|
| 180 |
+
"https://pro-api.coinmarketcap.com/v1/cryptocurrency/quotes/latest",
|
| 181 |
+
params={"symbol": symbols, "convert": "USD"},
|
| 182 |
+
)
|
| 183 |
+
if response.status_code < 200 or response.status_code >= 300:
|
| 184 |
+
return [], [f"CoinMarketCap HTTP {response.status_code}"]
|
| 185 |
+
payload = response.json()
|
| 186 |
+
except Exception as exc:
|
| 187 |
+
return [], [f"CoinMarketCap request failed: {exc}"]
|
| 188 |
+
data = payload.get("data") if isinstance(payload, dict) else None
|
| 189 |
+
if not isinstance(data, dict):
|
| 190 |
+
return [], ["CoinMarketCap returned invalid payload"]
|
| 191 |
+
rows=[]
|
| 192 |
+
for sym, item in list(data.items())[:limit]:
|
| 193 |
+
quote = ((item.get("quote") or {}).get("USD") or {}) if isinstance(item, dict) else {}
|
| 194 |
+
rows.append({
|
| 195 |
+
"symbol": f"{sym.upper()}USDT",
|
| 196 |
+
"baseSymbol": sym.upper(),
|
| 197 |
+
"name": item.get("name"),
|
| 198 |
+
"price": quote.get("price"),
|
| 199 |
+
"change24h": quote.get("percent_change_24h"),
|
| 200 |
+
"change_24h": quote.get("percent_change_24h"),
|
| 201 |
+
"volume24h": quote.get("volume_24h"),
|
| 202 |
+
"volume_24h": quote.get("volume_24h"),
|
| 203 |
+
"marketCap": quote.get("market_cap"),
|
| 204 |
+
"market_cap": quote.get("market_cap"),
|
| 205 |
+
"marketCapRank": item.get("cmc_rank"),
|
| 206 |
+
"market_cap_rank": item.get("cmc_rank"),
|
| 207 |
+
"source": "coinmarketcap_quotes",
|
| 208 |
+
})
|
| 209 |
+
return rows, []
|
| 210 |
+
|
| 211 |
+
async def _fetch_cryptocompare_prices(symbols: str = "BTC,ETH,BNB,SOL,XRP") -> Tuple[List[Dict[str, Any]], List[str]]:
|
| 212 |
+
key = get_secret("CRYPTOCOMPARE_KEY")
|
| 213 |
+
params={"fsyms": symbols, "tsyms": "USD"}
|
| 214 |
+
if key:
|
| 215 |
+
params["api_key"] = key
|
| 216 |
+
payload, error, _ = await _get_json("https://min-api.cryptocompare.com/data/pricemultifull", params=params, timeout=12.0)
|
| 217 |
+
if error or not isinstance(payload, dict):
|
| 218 |
+
return [], [error or "CryptoCompare price payload invalid"]
|
| 219 |
+
raw = ((payload.get("RAW") or {}))
|
| 220 |
+
rows=[]
|
| 221 |
+
for sym, data in raw.items():
|
| 222 |
+
usd = (data or {}).get("USD") or {}
|
| 223 |
+
rows.append({
|
| 224 |
+
"symbol": f"{sym.upper()}USDT",
|
| 225 |
+
"baseSymbol": sym.upper(),
|
| 226 |
+
"name": sym.upper(),
|
| 227 |
+
"price": usd.get("PRICE"),
|
| 228 |
+
"change24h": usd.get("CHANGEPCT24HOUR"),
|
| 229 |
+
"change_24h": usd.get("CHANGEPCT24HOUR"),
|
| 230 |
+
"volume24h": usd.get("VOLUME24HOURTO"),
|
| 231 |
+
"volume_24h": usd.get("VOLUME24HOURTO"),
|
| 232 |
+
"marketCap": usd.get("MKTCAP"),
|
| 233 |
+
"market_cap": usd.get("MKTCAP"),
|
| 234 |
+
"source": "cryptocompare_pricemultifull",
|
| 235 |
+
})
|
| 236 |
+
return rows, []
|
| 237 |
+
|
| 238 |
async def _fetch_coingecko_markets(limit: int = 100) -> Tuple[List[Dict[str, Any]], List[str]]:
|
| 239 |
url = "https://api.coingecko.com/api/v3/coins/markets"
|
| 240 |
params = {
|
|
|
|
| 247 |
}
|
| 248 |
payload, error, _ = await _get_json(url, params=params, timeout=20.0)
|
| 249 |
if error or not isinstance(payload, list):
|
| 250 |
+
return [], [error or "CoinGecko markets returned non-list payload"]
|
| 251 |
return payload, []
|
| 252 |
|
| 253 |
|
| 254 |
+
async def _fetch_coingecko_trending(limit: int = 10) -> Tuple[List[Dict[str, Any]], List[str]]:
|
| 255 |
+
payload, error, _ = await _get_json("https://api.coingecko.com/api/v3/search/trending", timeout=15.0)
|
| 256 |
+
if error or not isinstance(payload, dict):
|
| 257 |
+
return [], [error or "CoinGecko trending returned invalid payload"]
|
| 258 |
+
rows: List[Dict[str, Any]] = []
|
| 259 |
+
for item in (payload.get("coins") or [])[:limit]:
|
| 260 |
+
coin = item.get("item") or {}
|
| 261 |
+
rows.append({
|
| 262 |
+
"id": coin.get("id"),
|
| 263 |
+
"symbol": str(coin.get("symbol", "")).upper(),
|
| 264 |
+
"name": coin.get("name"),
|
| 265 |
+
"price": _float(coin.get("data", {}).get("price"), 0.0) if isinstance(coin.get("data"), dict) else 0.0,
|
| 266 |
+
"marketCapRank": coin.get("market_cap_rank"),
|
| 267 |
+
"score": coin.get("score", 0),
|
| 268 |
+
"source": "coingecko_trending",
|
| 269 |
+
})
|
| 270 |
+
return rows, []
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
async def _fetch_binance_klines(symbol: str, interval: str, limit: int) -> Tuple[List[Dict[str, Any]], List[str]]:
|
| 274 |
pair = _normalize_symbol(symbol)
|
| 275 |
mapped = BINANCE_INTERVALS.get(interval, interval)
|
| 276 |
payload, error, _ = await _get_json(
|
| 277 |
"https://api.binance.com/api/v3/klines",
|
| 278 |
params={"symbol": pair, "interval": mapped, "limit": min(max(limit, 1), 1000)},
|
| 279 |
+
timeout=20.0,
|
| 280 |
)
|
| 281 |
+
if error or not isinstance(payload, list):
|
| 282 |
+
return [], [error or f"Binance klines invalid payload for {pair}"]
|
|
|
|
|
|
|
| 283 |
candles = []
|
| 284 |
for row in payload:
|
| 285 |
try:
|
| 286 |
+
candles.append({
|
| 287 |
+
"timestamp": _int(row[0]),
|
| 288 |
+
"open": _float(row[1]),
|
| 289 |
+
"high": _float(row[2]),
|
| 290 |
+
"low": _float(row[3]),
|
| 291 |
+
"close": _float(row[4]),
|
| 292 |
+
"volume": _float(row[5]),
|
| 293 |
+
})
|
|
|
|
|
|
|
|
|
|
| 294 |
except Exception:
|
| 295 |
continue
|
| 296 |
+
return candles, []
|
| 297 |
|
| 298 |
|
| 299 |
+
async def _fetch_kucoin_klines(symbol: str, timeframe: str, limit: int) -> Tuple[List[Dict[str, Any]], List[str]]:
|
| 300 |
+
# KuCoin returns reverse chronological rows like [time, open, close, high, low, volume, turnover].
|
| 301 |
+
ksymbol = _kucoin_symbol(symbol)
|
| 302 |
+
ktype = KUCOIN_TYPES.get(timeframe, "1hour")
|
| 303 |
payload, error, _ = await _get_json(
|
| 304 |
"https://api.kucoin.com/api/v1/market/candles",
|
| 305 |
+
params={"symbol": ksymbol, "type": ktype},
|
| 306 |
+
timeout=20.0,
|
| 307 |
)
|
| 308 |
+
if error or not isinstance(payload, dict):
|
| 309 |
+
return [], [error or f"KuCoin candles invalid payload for {ksymbol}"]
|
| 310 |
+
data = payload.get("data") or []
|
|
|
|
|
|
|
| 311 |
candles = []
|
| 312 |
+
for row in data[: min(max(limit, 1), 1500)]:
|
|
|
|
| 313 |
try:
|
| 314 |
+
candles.append({
|
| 315 |
+
"timestamp": _int(row[0]) * 1000,
|
| 316 |
+
"open": _float(row[1]),
|
| 317 |
+
"high": _float(row[3]),
|
| 318 |
+
"low": _float(row[4]),
|
| 319 |
+
"close": _float(row[2]),
|
| 320 |
+
"volume": _float(row[5]),
|
| 321 |
+
})
|
|
|
|
|
|
|
|
|
|
|
|
|
| 322 |
except Exception:
|
| 323 |
continue
|
| 324 |
candles.sort(key=lambda c: c["timestamp"])
|
| 325 |
+
return candles[-limit:], []
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
|
| 329 |
+
async def _fetch_cryptocompare_ohlcv(symbol: str, timeframe: str, limit: int) -> Tuple[List[Dict[str, Any]], List[str]]:
|
| 330 |
+
"""Optional OHLCV fallback via CryptoCompare.
|
| 331 |
+
|
| 332 |
+
Uses env CRYPTOCOMPARE_KEY when configured, but can still work on some
|
| 333 |
+
public/free endpoints without a key. This reduces pressure on exchange
|
| 334 |
+
endpoints and preserves the original multi-source hub design.
|
| 335 |
+
"""
|
| 336 |
+
base = _base_asset(symbol)
|
| 337 |
+
tf = (timeframe or "1h").lower()
|
| 338 |
+
if tf.endswith("m"):
|
| 339 |
+
endpoint = "histominute"
|
| 340 |
+
aggregate = max(1, _int(tf[:-1], 1))
|
| 341 |
+
elif tf.endswith("h"):
|
| 342 |
+
endpoint = "histohour"
|
| 343 |
+
aggregate = max(1, _int(tf[:-1], 1))
|
| 344 |
+
elif tf.endswith("d"):
|
| 345 |
+
endpoint = "histoday"
|
| 346 |
+
aggregate = max(1, _int(tf[:-1], 1))
|
| 347 |
+
else:
|
| 348 |
+
endpoint = "histohour"
|
| 349 |
+
aggregate = 1
|
| 350 |
+
params: Dict[str, Any] = {
|
| 351 |
+
"fsym": base,
|
| 352 |
+
"tsym": "USD",
|
| 353 |
+
"limit": min(max(limit, 1), 2000),
|
| 354 |
+
"aggregate": aggregate,
|
| 355 |
+
}
|
| 356 |
+
key = get_secret("CRYPTOCOMPARE_KEY")
|
| 357 |
+
if key:
|
| 358 |
+
params["api_key"] = key
|
| 359 |
payload, error, _ = await _get_json(
|
| 360 |
f"https://min-api.cryptocompare.com/data/v2/{endpoint}",
|
| 361 |
+
params=params,
|
| 362 |
+
timeout=20.0,
|
| 363 |
)
|
| 364 |
+
if error or not isinstance(payload, dict):
|
| 365 |
+
return [], [error or f"CryptoCompare {endpoint} invalid payload for {base}"]
|
| 366 |
+
rows = (((payload.get("Data") or {}).get("Data")) or [])[-limit:]
|
| 367 |
+
candles: List[Dict[str, Any]] = []
|
| 368 |
+
for row in rows:
|
|
|
|
|
|
|
| 369 |
try:
|
| 370 |
+
candles.append({
|
| 371 |
+
"timestamp": _int(row.get("time")) * 1000,
|
| 372 |
+
"open": _float(row.get("open")),
|
| 373 |
+
"high": _float(row.get("high")),
|
| 374 |
+
"low": _float(row.get("low")),
|
| 375 |
+
"close": _float(row.get("close")),
|
| 376 |
+
"volume": _float(row.get("volumefrom")),
|
| 377 |
+
})
|
|
|
|
|
|
|
|
|
|
|
|
|
| 378 |
except Exception:
|
| 379 |
continue
|
| 380 |
+
return candles, []
|
| 381 |
|
| 382 |
+
async def _fetch_ohlcv(symbol: str, timeframe: str, limit: int) -> Tuple[List[Dict[str, Any]], str, List[str]]:
|
| 383 |
+
"""Smart OHLCV rotation.
|
| 384 |
|
| 385 |
+
Raw OHLCV must come from market/exchange providers. HF models are used for
|
| 386 |
+
enrichment, classification, quality scoring, and derived analysis — never as
|
| 387 |
+
the source of truth for historical candles.
|
| 388 |
+
"""
|
| 389 |
errors: List[str] = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 390 |
|
| 391 |
+
candles, err = await _fetch_binance_klines(symbol, timeframe, limit)
|
| 392 |
+
if candles:
|
| 393 |
+
return candles, "binance_public", errors + err
|
| 394 |
+
errors.extend(err)
|
| 395 |
+
|
| 396 |
+
candles, err = await _fetch_kucoin_klines(symbol, timeframe, limit)
|
| 397 |
+
if candles:
|
| 398 |
+
return candles, "kucoin_public", errors + err
|
| 399 |
+
errors.extend(err)
|
| 400 |
+
|
| 401 |
+
candles, err = await _fetch_cryptocompare_ohlcv(symbol, timeframe, limit)
|
| 402 |
+
if candles:
|
| 403 |
+
return candles, "cryptocompare_ohlcv", errors + err
|
| 404 |
+
errors.extend(err)
|
| 405 |
+
|
| 406 |
+
return [], "none", errors or ["No OHLCV provider returned data"]
|
| 407 |
|
| 408 |
+
|
| 409 |
+
async def _fetch_binance_ticker(symbol: str) -> Tuple[Dict[str, Any], List[str]]:
|
| 410 |
pair = _normalize_symbol(symbol)
|
| 411 |
payload, error, _ = await _get_json(
|
| 412 |
"https://api.binance.com/api/v3/ticker/24hr",
|
| 413 |
params={"symbol": pair},
|
| 414 |
timeout=15.0,
|
| 415 |
)
|
| 416 |
+
if error or not isinstance(payload, dict):
|
| 417 |
+
return {}, [error or f"Binance ticker invalid payload for {pair}"]
|
| 418 |
+
return payload, []
|
| 419 |
|
| 420 |
|
| 421 |
+
async def _fetch_kucoin_orderbook(symbol: str, limit: int) -> Tuple[Dict[str, Any], List[str]]:
|
| 422 |
+
ksymbol = _kucoin_symbol(symbol)
|
| 423 |
+
# level2_20 is free public and ignores custom large limits; still stable for Short Hunter guards.
|
| 424 |
+
endpoint = "level2_100" if limit > 20 else "level2_20"
|
| 425 |
payload, error, _ = await _get_json(
|
| 426 |
+
f"https://api.kucoin.com/api/v1/market/orderbook/{endpoint}",
|
| 427 |
+
params={"symbol": ksymbol},
|
| 428 |
timeout=15.0,
|
| 429 |
)
|
| 430 |
+
if error or not isinstance(payload, dict) or not isinstance(payload.get("data"), dict):
|
| 431 |
+
return {}, [error or f"KuCoin orderbook invalid payload for {ksymbol}"]
|
| 432 |
+
return payload["data"], []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 433 |
|
| 434 |
|
| 435 |
+
async def _fetch_binance_orderbook(symbol: str, limit: int) -> Tuple[Dict[str, Any], List[str]]:
|
| 436 |
pair = _normalize_symbol(symbol)
|
| 437 |
payload, error, _ = await _get_json(
|
| 438 |
"https://api.binance.com/api/v3/depth",
|
| 439 |
params={"symbol": pair, "limit": min(max(limit, 5), 100)},
|
| 440 |
timeout=15.0,
|
| 441 |
)
|
| 442 |
+
if error or not isinstance(payload, dict):
|
| 443 |
+
return {}, [error or f"Binance depth invalid payload for {pair}"]
|
| 444 |
+
return payload, []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 445 |
|
| 446 |
|
| 447 |
+
async def _fetch_orderbook(symbol: str, limit: int) -> Tuple[List[List[float]], List[List[float]], str, List[str]]:
|
| 448 |
errors: List[str] = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 449 |
book, err = await _fetch_kucoin_orderbook(symbol, limit)
|
| 450 |
+
if book.get("bids") or book.get("asks"):
|
| 451 |
+
bids = [[_float(p), _float(q)] for p, q in (book.get("bids") or [])[:limit]]
|
| 452 |
+
asks = [[_float(p), _float(q)] for p, q in (book.get("asks") or [])[:limit]]
|
| 453 |
+
return bids, asks, "kucoin_public", errors + err
|
| 454 |
+
errors.extend(err)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 455 |
|
| 456 |
+
book, err = await _fetch_binance_orderbook(symbol, limit)
|
| 457 |
+
if book.get("bids") or book.get("asks"):
|
| 458 |
+
bids = [[_float(p), _float(q)] for p, q in (book.get("bids") or [])[:limit]]
|
| 459 |
+
asks = [[_float(p), _float(q)] for p, q in (book.get("asks") or [])[:limit]]
|
| 460 |
+
return bids, asks, "binance_public", errors + err
|
| 461 |
+
errors.extend(err)
|
| 462 |
+
return [], [], "none", errors or ["No orderbook provider returned data"]
|
| 463 |
|
| 464 |
|
| 465 |
+
async def _fetch_fear_greed() -> Tuple[Dict[str, Any], List[str]]:
|
| 466 |
payload, error, _ = await _get_json(
|
| 467 |
"https://api.alternative.me/fng/",
|
| 468 |
params={"limit": 1, "format": "json"},
|
| 469 |
timeout=15.0,
|
| 470 |
)
|
| 471 |
+
if error or not isinstance(payload, dict):
|
| 472 |
+
return {}, [error or "Alternative.me fear/greed invalid payload"]
|
| 473 |
+
return payload, []
|
| 474 |
|
| 475 |
|
| 476 |
def _rsi(closes: List[float], period: int = 14) -> Optional[float]:
|
|
|
|
| 491 |
|
| 492 |
|
| 493 |
def _ema(values: List[float], period: int) -> Optional[float]:
|
| 494 |
+
if not values:
|
| 495 |
return None
|
| 496 |
+
alpha = 2 / (period + 1)
|
| 497 |
+
ema = values[0]
|
| 498 |
+
for value in values[1:]:
|
| 499 |
+
ema = (value * alpha) + (ema * (1 - alpha))
|
| 500 |
+
return round(ema, 8)
|
| 501 |
+
|
| 502 |
+
|
| 503 |
+
def _macd(closes: List[float]) -> Dict[str, Optional[float]]:
|
| 504 |
+
if len(closes) < 26:
|
| 505 |
+
return {"macd": None, "signal": None, "histogram": None}
|
| 506 |
+
macd_series = []
|
| 507 |
+
for i in range(26, len(closes) + 1):
|
| 508 |
+
fast = _ema(closes[:i], 12)
|
| 509 |
+
slow = _ema(closes[:i], 26)
|
| 510 |
+
if fast is not None and slow is not None:
|
| 511 |
+
macd_series.append(fast - slow)
|
| 512 |
+
macd_value = macd_series[-1] if macd_series else None
|
| 513 |
+
signal = _ema(macd_series, 9) if len(macd_series) >= 9 else None
|
| 514 |
+
histogram = (macd_value - signal) if macd_value is not None and signal is not None else None
|
| 515 |
+
return {
|
| 516 |
+
"macd": round(macd_value, 8) if macd_value is not None else None,
|
| 517 |
+
"signal": round(signal, 8) if signal is not None else None,
|
| 518 |
+
"histogram": round(histogram, 8) if histogram is not None else None,
|
| 519 |
+
}
|
| 520 |
+
|
| 521 |
+
|
| 522 |
+
def _bollinger(closes: List[float], period: int = 20) -> Dict[str, Optional[float]]:
|
| 523 |
+
if len(closes) < period:
|
| 524 |
+
return {"upper": None, "middle": None, "lower": None}
|
| 525 |
+
window = closes[-period:]
|
| 526 |
+
mean = sum(window) / period
|
| 527 |
+
std = (sum((c - mean) ** 2 for c in window) / period) ** 0.5
|
| 528 |
+
return {
|
| 529 |
+
"upper": round(mean + 2 * std, 8),
|
| 530 |
+
"middle": round(mean, 8),
|
| 531 |
+
"lower": round(mean - 2 * std, 8),
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
+
|
| 535 |
+
def _market_row_from_coingecko(item: Dict[str, Any]) -> Dict[str, Any]:
|
| 536 |
+
sym = str(item.get("symbol", "")).upper()
|
| 537 |
+
normalized = _normalize_symbol(sym) if sym else ""
|
| 538 |
+
return {
|
| 539 |
+
"symbol": normalized,
|
| 540 |
+
"baseSymbol": sym,
|
| 541 |
+
"name": item.get("name"),
|
| 542 |
+
"price": item.get("current_price"),
|
| 543 |
+
"change24h": item.get("price_change_percentage_24h"),
|
| 544 |
+
"change_24h": item.get("price_change_percentage_24h"),
|
| 545 |
+
"volume24h": item.get("total_volume"),
|
| 546 |
+
"volume_24h": item.get("total_volume"),
|
| 547 |
+
"marketCap": item.get("market_cap"),
|
| 548 |
+
"market_cap": item.get("market_cap"),
|
| 549 |
+
"marketCapRank": item.get("market_cap_rank"),
|
| 550 |
+
"market_cap_rank": item.get("market_cap_rank"),
|
| 551 |
+
"image": item.get("image"),
|
| 552 |
+
"source": "coingecko_markets",
|
| 553 |
+
}
|
| 554 |
|
| 555 |
|
| 556 |
@router.get("/api/coins/top")
|
| 557 |
async def coins_top(limit: int = Query(100, ge=1, le=250)):
|
| 558 |
markets, errors = await _fetch_coingecko_markets(limit)
|
| 559 |
+
data = [_market_row_from_coingecko(item) for item in markets]
|
| 560 |
+
if not data:
|
| 561 |
+
return _fail("No top coins data available", source="coingecko_markets", data=[], missingCapabilities=["coinsTop"], upstreamErrors=errors)
|
| 562 |
+
return _ok(data=data, coins=data, count=len(data), source="coingecko_markets")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 563 |
|
| 564 |
|
| 565 |
@router.get("/api/top-coins")
|
|
|
|
| 568 |
return await coins_top(limit=limit)
|
| 569 |
|
| 570 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 571 |
@router.get("/api/trading/ohlcv/{symbol}")
|
| 572 |
async def trading_ohlcv(symbol: str, timeframe: str = "1h", limit: int = 100):
|
| 573 |
+
candles, source, errors = await _fetch_ohlcv(symbol, timeframe, limit)
|
| 574 |
if not candles:
|
| 575 |
+
return _fail("OHLCV unavailable", source=source, symbol=_normalize_symbol(symbol), timeframe=timeframe, data=[], missingCapabilities=["ohlcv"], upstreamErrors=errors)
|
| 576 |
+
return _ok(data=candles, symbol=_normalize_symbol(symbol), timeframe=timeframe, candles=candles, source=source)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 577 |
|
| 578 |
|
| 579 |
@router.get("/api/ohlcv")
|
| 580 |
@router.get("/api/klines")
|
|
|
|
| 581 |
async def trading_ohlcv_alias(
|
| 582 |
symbol: str = Query("BTCUSDT"),
|
| 583 |
interval: str = Query("1h"),
|
|
|
|
| 587 |
return await trading_ohlcv(symbol=symbol, timeframe=(timeframe or interval), limit=limit)
|
| 588 |
|
| 589 |
|
| 590 |
+
@router.get("/api/history")
|
| 591 |
+
@router.get("/api/trading/history/{symbol}")
|
| 592 |
+
async def history_alias(symbol: str = "BTCUSDT", interval: str = "1h", timeframe: Optional[str] = None, limit: int = Query(100, ge=1, le=1000)):
|
| 593 |
+
return await trading_ohlcv(symbol=symbol, timeframe=(timeframe or interval), limit=limit)
|
| 594 |
+
|
| 595 |
+
|
| 596 |
@router.get("/api/trading/stats/24h/{symbol}")
|
| 597 |
async def trading_stats_24h(symbol: str):
|
| 598 |
+
ticker, errors = await _fetch_binance_ticker(symbol)
|
| 599 |
+
if not ticker:
|
| 600 |
+
return _fail("24h ticker unavailable", source="binance_public", symbol=_normalize_symbol(symbol), missingCapabilities=["ticker"], upstreamErrors=errors)
|
| 601 |
+
return _ok(
|
| 602 |
+
symbol=_normalize_symbol(symbol),
|
| 603 |
+
price=_float(ticker.get("lastPrice")),
|
| 604 |
+
volume=_float(ticker.get("volume")),
|
| 605 |
+
quote_volume=_float(ticker.get("quoteVolume")),
|
| 606 |
+
change_percent=_float(ticker.get("priceChangePercent")),
|
| 607 |
+
high=_float(ticker.get("highPrice")),
|
| 608 |
+
low=_float(ticker.get("lowPrice")),
|
| 609 |
+
source="binance_public",
|
| 610 |
+
)
|
| 611 |
|
| 612 |
|
| 613 |
+
@router.get("/api/trading/orderbook/{symbol}")
|
| 614 |
+
async def trading_orderbook(symbol: str, limit: int = 20):
|
| 615 |
+
bids, asks, source, errors = await _fetch_orderbook(symbol, limit)
|
| 616 |
+
if not bids and not asks:
|
| 617 |
+
return _fail("Orderbook unavailable", source=source, symbol=_normalize_symbol(symbol), bids=[], asks=[], missingCapabilities=["orderbook"], upstreamErrors=errors)
|
| 618 |
+
return _ok(symbol=_normalize_symbol(symbol), bids=bids, asks=asks, source=source)
|
| 619 |
|
| 620 |
|
| 621 |
+
@router.get("/api/orderbook")
|
| 622 |
+
async def trading_orderbook_alias(symbol: str = Query("BTCUSDT"), limit: int = Query(20, ge=5, le=100)):
|
| 623 |
+
return await trading_orderbook(symbol=symbol, limit=limit)
|
| 624 |
|
| 625 |
|
| 626 |
+
@router.get("/api/indicators/comprehensive")
|
| 627 |
+
async def indicators_comprehensive(symbol: str = "BTCUSDT", timeframe: str = "1h", limit: int = 120):
|
| 628 |
+
candles, source, errors = await _fetch_ohlcv(symbol, timeframe, max(limit, 60))
|
| 629 |
+
closes = [_float(c.get("close")) for c in candles if c.get("close") is not None]
|
| 630 |
+
if not closes:
|
| 631 |
+
return _fail("Indicators unavailable because OHLCV is missing", source=source, symbol=_normalize_symbol(symbol), data={}, missingCapabilities=["indicators", "ohlcv"], upstreamErrors=errors)
|
| 632 |
+
|
| 633 |
+
rsi = _rsi(closes)
|
| 634 |
+
macd = _macd(closes)
|
| 635 |
+
bb = _bollinger(closes)
|
| 636 |
+
ema20 = _ema(closes[-60:], 20) if closes else None
|
| 637 |
+
ema50 = _ema(closes[-120:], 50) if closes else None
|
| 638 |
+
indicators = {
|
| 639 |
+
"rsi": rsi,
|
| 640 |
+
"macd": macd,
|
| 641 |
+
"bb": bb,
|
| 642 |
+
"bollinger_bands": bb,
|
| 643 |
+
"ema": {"ema20": ema20, "ema50": ema50},
|
| 644 |
+
"currentPrice": closes[-1],
|
| 645 |
+
}
|
| 646 |
+
return _ok(symbol=_normalize_symbol(symbol), timeframe=timeframe, data=indicators, indicators=indicators, source=f"{source}_derived")
|
| 647 |
|
| 648 |
|
| 649 |
+
@router.get("/api/indicators")
|
| 650 |
+
async def indicators_alias(symbol: str = Query("BTCUSDT"), interval: str = Query("1h"), timeframe: Optional[str] = Query(None), limit: int = Query(120, ge=20, le=1000)):
|
| 651 |
+
return await indicators_comprehensive(symbol=symbol, timeframe=(timeframe or interval), limit=limit)
|
| 652 |
+
|
| 653 |
+
|
| 654 |
+
@router.get("/api/indicators/rsi")
|
| 655 |
+
async def indicators_rsi(symbol: str = Query("BTCUSDT"), interval: str = Query("1h"), timeframe: Optional[str] = Query(None), limit: int = Query(120, ge=20, le=1000)):
|
| 656 |
+
result = await indicators_comprehensive(symbol=symbol, timeframe=(timeframe or interval), limit=limit)
|
| 657 |
+
if not result.get("success"):
|
| 658 |
+
return result
|
| 659 |
+
value = (result.get("data") or {}).get("rsi")
|
| 660 |
+
return _ok(symbol=_normalize_symbol(symbol), timeframe=(timeframe or interval), data={"rsi": value}, rsi=value, source=result.get("source"))
|
| 661 |
+
|
| 662 |
+
|
| 663 |
+
@router.get("/api/indicators/macd")
|
| 664 |
+
async def indicators_macd(symbol: str = Query("BTCUSDT"), interval: str = Query("1h"), timeframe: Optional[str] = Query(None), limit: int = Query(120, ge=30, le=1000)):
|
| 665 |
+
result = await indicators_comprehensive(symbol=symbol, timeframe=(timeframe or interval), limit=limit)
|
| 666 |
+
if not result.get("success"):
|
| 667 |
+
return result
|
| 668 |
+
value = (result.get("data") or {}).get("macd")
|
| 669 |
+
return _ok(symbol=_normalize_symbol(symbol), timeframe=(timeframe or interval), data=value, macd=value, source=result.get("source"))
|
| 670 |
|
| 671 |
|
| 672 |
@router.get("/api/social/sentiment")
|
| 673 |
async def social_sentiment(coin: str = "BTC", timeframe: str = "24h"):
|
| 674 |
+
payload, errors = await _fetch_fear_greed()
|
| 675 |
+
if not payload:
|
| 676 |
+
return _fail("Global sentiment unavailable", source="alternative_me", asset=coin.upper(), missingCapabilities=["sentiment"], upstreamErrors=errors)
|
| 677 |
+
entry = (payload.get("data") or [{}])[0]
|
| 678 |
+
score = _int(entry.get("value"), 50)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 679 |
normalized = round((score - 50) / 50, 4)
|
| 680 |
+
label = entry.get("value_classification", "Neutral")
|
| 681 |
+
return _ok(
|
| 682 |
+
asset=coin.upper(),
|
| 683 |
+
coin=coin.upper(),
|
| 684 |
+
timeframe=timeframe,
|
| 685 |
+
sentiment="bullish" if score > 55 else ("bearish" if score < 45 else "neutral"),
|
| 686 |
+
score=normalized,
|
| 687 |
+
confidence=abs(normalized),
|
| 688 |
+
fear_greed_index=score,
|
| 689 |
+
label=label,
|
| 690 |
+
source="alternative_me_fear_greed",
|
| 691 |
+
)
|
|
|
|
|
|
|
| 692 |
|
| 693 |
|
| 694 |
@router.get("/api/sentiment/global")
|
| 695 |
async def sentiment_global():
|
| 696 |
+
return await social_sentiment(coin="GLOBAL")
|
| 697 |
|
| 698 |
|
| 699 |
@router.get("/api/sentiment/asset/{symbol}")
|
| 700 |
async def sentiment_asset(symbol: str):
|
| 701 |
+
return await social_sentiment(coin=_base_asset(symbol))
|
| 702 |
|
| 703 |
|
| 704 |
@router.get("/api/ai/sentiment")
|
| 705 |
+
async def ai_sentiment_get(symbol: str = Query("BTC")):
|
| 706 |
return await sentiment_asset(symbol)
|
| 707 |
|
| 708 |
|
| 709 |
+
@router.post("/api/ai/sentiment")
|
| 710 |
+
async def ai_sentiment_post(request: Dict[str, Any]):
|
| 711 |
+
text = str(request.get("text") or request.get("query") or "").strip()
|
| 712 |
+
symbol = str(request.get("symbol") or request.get("asset") or "BTC")
|
| 713 |
+
# Compatibility fallback: combine model-independent global score with the requested text metadata.
|
| 714 |
+
result = await sentiment_asset(symbol)
|
| 715 |
+
result["textProvided"] = bool(text)
|
| 716 |
+
result["route"] = "/api/ai/sentiment"
|
| 717 |
+
return result
|
| 718 |
+
|
| 719 |
+
|
| 720 |
@router.get("/api/news/{coin_id}")
|
| 721 |
async def coin_news(coin_id: str, limit: int = Query(5, ge=1, le=20)):
|
| 722 |
+
# Public CryptoCompare news often works without a key; failure is a partial capability, not fatal.
|
| 723 |
payload, error, _ = await _get_json(
|
| 724 |
"https://min-api.cryptocompare.com/data/v2/news/",
|
| 725 |
params={"lang": "EN", "categories": coin_id.upper(), "excludeCategories": "Sponsored"},
|
| 726 |
timeout=15.0,
|
| 727 |
)
|
| 728 |
if error or not isinstance(payload, dict):
|
| 729 |
+
return _ok(data=[], news=[], count=0, status="empty", source="cryptocompare_news", upstreamErrors=[error] if error else [])
|
| 730 |
articles = (payload.get("Data") or [])[:limit]
|
| 731 |
news = [
|
| 732 |
{
|
| 733 |
"title": a.get("title"),
|
| 734 |
"url": a.get("url"),
|
| 735 |
"source": a.get("source"),
|
| 736 |
+
"published_at": datetime.fromtimestamp(a.get("published_on", 0), timezone.utc).isoformat() if a.get("published_on") else None,
|
| 737 |
}
|
| 738 |
for a in articles
|
| 739 |
]
|
| 740 |
+
return _ok(data=news, news=news, count=len(news), status="available" if news else "empty", source="cryptocompare_news")
|
| 741 |
+
|
| 742 |
+
|
| 743 |
+
@router.get("/api/providers/catalog")
|
| 744 |
+
async def providers_catalog():
|
| 745 |
+
catalog = load_provider_catalog()
|
| 746 |
+
runtime = provider_runtime_summary()
|
| 747 |
+
return _ok(catalog=catalog, runtime=runtime, secretPolicy="Use HuggingFace Space secrets/env vars only; no keys are stored in source.")
|
| 748 |
+
|
| 749 |
+
|
| 750 |
+
@router.get("/api/providers/status")
|
| 751 |
+
async def providers_status():
|
| 752 |
+
runtime = provider_runtime_summary()
|
| 753 |
+
return _ok(
|
| 754 |
+
data=runtime,
|
| 755 |
+
runtime=runtime,
|
| 756 |
+
ohlcvRotation=rotation_plan("ohlcv"),
|
| 757 |
+
orderbookRotation=rotation_plan("orderbook"),
|
| 758 |
+
sentimentRotation=rotation_plan("sentiment"),
|
| 759 |
+
source="sanitized_provider_catalog",
|
| 760 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 761 |
|
| 762 |
|
| 763 |
+
@router.get("/api/debug/capabilities")
|
| 764 |
+
async def debug_capabilities():
|
| 765 |
+
routes = [
|
| 766 |
+
"/api/health",
|
| 767 |
+
"/api/status",
|
| 768 |
+
"/api/market",
|
| 769 |
+
"/api/coins/top",
|
| 770 |
+
"/api/top-coins",
|
| 771 |
+
"/api/trending",
|
| 772 |
+
"/api/ohlcv",
|
| 773 |
+
"/api/klines",
|
| 774 |
+
"/api/history",
|
| 775 |
+
"/api/trading/history/{symbol}",
|
| 776 |
+
"/api/indicators",
|
| 777 |
+
"/api/indicators/rsi",
|
| 778 |
+
"/api/indicators/macd",
|
| 779 |
+
"/api/indicators/comprehensive",
|
| 780 |
+
"/api/sentiment/global",
|
| 781 |
+
"/api/sentiment/asset/{symbol}",
|
| 782 |
+
"POST /api/sentiment",
|
| 783 |
+
"POST /api/sentiment/analyze",
|
| 784 |
+
"POST /api/hf/run-sentiment",
|
| 785 |
+
"/api/ai/sentiment",
|
| 786 |
+
"POST /api/ai/sentiment",
|
| 787 |
+
"/api/news",
|
| 788 |
+
"/api/news/latest",
|
| 789 |
+
"/api/news/{coin_id}",
|
| 790 |
+
"/api/orderbook",
|
| 791 |
+
"/api/trading/orderbook/{symbol}",
|
| 792 |
+
"/api/providers/catalog",
|
| 793 |
+
"/api/providers/status",
|
| 794 |
+
]
|
| 795 |
+
runtime = provider_runtime_summary()
|
| 796 |
+
capabilities = {
|
| 797 |
+
"market": "available_via_cmc_if_configured_else_coingecko_cryptocompare",
|
| 798 |
+
"coinsTop": "available_via_coingecko",
|
| 799 |
+
"trending": "available_via_coingecko",
|
| 800 |
+
"ohlcv": "available_via_binance_or_kucoin_public",
|
| 801 |
+
"klines": "available_via_binance_or_kucoin_public",
|
| 802 |
+
"indicators": "available_local_from_ohlcv",
|
| 803 |
+
"sentiment": "available_via_alternative_me_hf_models",
|
| 804 |
+
"news": "available_via_database_newsapi_if_configured_cryptocompare_or_empty_not_fatal",
|
| 805 |
+
"orderbook": "available_via_kucoin_or_binance_public",
|
| 806 |
+
"providersCatalog": "available_sanitized_no_secrets",
|
| 807 |
}
|
| 808 |
+
return _ok(routes=routes, capabilities=capabilities, providers=runtime, dataState="COMPLETE", missingCapabilities=[])
|
| 809 |
|
| 810 |
|
| 811 |
def register_compat_routes(app) -> None:
|
| 812 |
app.include_router(router)
|
| 813 |
+
logger.info("Registered Short Hunter V4 compatibility routes on /api/coins/top, /api/ohlcv, /api/klines, /api/orderbook, /api/indicators/*, /api/sentiment/*, /api/debug/capabilities")
|
api_server_extended.py
CHANGED
|
@@ -18,6 +18,14 @@ from datetime import datetime
|
|
| 18 |
from contextlib import asynccontextmanager
|
| 19 |
from collections import defaultdict
|
| 20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
logger = logging.getLogger(__name__)
|
| 22 |
|
| 23 |
from fastapi import FastAPI, HTTPException, Response, Request
|
|
@@ -679,13 +687,6 @@ app.add_middleware(
|
|
| 679 |
allow_headers=["*"],
|
| 680 |
)
|
| 681 |
|
| 682 |
-
# ===== Zero-Conflict Router v5 (real OHLCV/orderbook/volume/news/sentiment/funding/OI) =====
|
| 683 |
-
try:
|
| 684 |
-
from enterprise_router_v5 import install_zero_conf_router
|
| 685 |
-
install_zero_conf_router(app)
|
| 686 |
-
except Exception as _e:
|
| 687 |
-
logger.warning(f"enterprise_router_v5 not installed: {_e}")
|
| 688 |
-
|
| 689 |
# Middleware to ensure HTML responses have correct Content-Type
|
| 690 |
class HTMLContentTypeMiddleware(BaseHTTPMiddleware):
|
| 691 |
async def dispatch(self, request: Request, call_next):
|
|
@@ -872,123 +873,140 @@ async def health():
|
|
| 872 |
|
| 873 |
@app.get("/api/health")
|
| 874 |
async def api_health():
|
| 875 |
-
"""API health check
|
| 876 |
try:
|
| 877 |
-
version = "1.0
|
| 878 |
try:
|
| 879 |
-
# Try to get version from metadata
|
| 880 |
api_registry = load_api_registry()
|
| 881 |
-
metadata = api_registry.get("metadata", {})
|
| 882 |
-
|
| 883 |
-
version = metadata.get("version")
|
| 884 |
except Exception:
|
| 885 |
pass
|
| 886 |
-
|
| 887 |
return {
|
|
|
|
|
|
|
| 888 |
"status": "ok",
|
|
|
|
|
|
|
| 889 |
"timestamp": datetime.now().isoformat(),
|
| 890 |
-
"
|
|
|
|
| 891 |
}
|
| 892 |
except Exception as e:
|
| 893 |
-
# Even if something goes wrong, return a clean response
|
| 894 |
logger.error(f"Health check error: {e}")
|
| 895 |
return JSONResponse(
|
| 896 |
status_code=200,
|
| 897 |
content={
|
|
|
|
|
|
|
| 898 |
"status": "ok",
|
|
|
|
|
|
|
| 899 |
"timestamp": datetime.now().isoformat(),
|
| 900 |
-
"
|
| 901 |
-
}
|
| 902 |
)
|
| 903 |
|
| 904 |
-
|
| 905 |
@app.get("/api/status")
|
| 906 |
async def get_status():
|
| 907 |
-
"""
|
| 908 |
try:
|
| 909 |
-
# Load providers
|
| 910 |
config = load_providers_config()
|
| 911 |
-
|
| 912 |
-
|
| 913 |
-
# Count free vs paid providers
|
| 914 |
-
free_count = sum(1 for p in providers.values()
|
| 915 |
-
if not p.get("requires_auth", False) and p.get("rate_limit"))
|
| 916 |
-
paid_count = sum(1 for p in providers.values()
|
| 917 |
-
if p.get("requires_auth", False))
|
| 918 |
-
|
| 919 |
-
# Load resources from unified file
|
| 920 |
resources_json = WORKSPACE_ROOT / "api-resources" / "crypto_resources_unified_2025-11-11.json"
|
|
|
|
| 921 |
resources_data = {"total": 0, "categories": {}}
|
| 922 |
-
|
| 923 |
if resources_json.exists():
|
| 924 |
try:
|
| 925 |
-
with open(resources_json,
|
| 926 |
unified_data = json.load(f)
|
| 927 |
-
registry = unified_data.get(
|
| 928 |
-
|
| 929 |
for category, items in registry.items():
|
| 930 |
-
if category ==
|
| 931 |
continue
|
| 932 |
if isinstance(items, list):
|
| 933 |
count = len(items)
|
| 934 |
-
resources_data[
|
| 935 |
-
|
| 936 |
-
|
| 937 |
-
|
| 938 |
-
|
| 939 |
-
resources_data['categories'][cat_key] = 0
|
| 940 |
-
resources_data['categories'][cat_key] += count
|
| 941 |
-
except Exception as e:
|
| 942 |
-
logger.error(f"Error loading resources: {e}")
|
| 943 |
-
|
| 944 |
-
# Get model count
|
| 945 |
model_count = 0
|
| 946 |
try:
|
| 947 |
from ai_models import MODEL_SPECS
|
| 948 |
model_count = len(MODEL_SPECS) if MODEL_SPECS else 0
|
| 949 |
-
except Exception:
|
| 950 |
-
|
| 951 |
-
|
| 952 |
-
#
|
| 953 |
-
|
| 954 |
-
|
| 955 |
-
|
| 956 |
-
|
| 957 |
-
|
| 958 |
-
|
| 959 |
-
|
| 960 |
-
|
| 961 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 962 |
return {
|
|
|
|
|
|
|
| 963 |
"status": "ok",
|
| 964 |
-
"
|
|
|
|
| 965 |
"timestamp": datetime.now().isoformat(),
|
| 966 |
"last_update": datetime.now().isoformat(),
|
| 967 |
"providers": {
|
| 968 |
-
"
|
| 969 |
-
"
|
| 970 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 971 |
},
|
| 972 |
-
"
|
| 973 |
-
"
|
| 974 |
-
"
|
| 975 |
-
"
|
| 976 |
"resources": resources_data,
|
| 977 |
-
"models": {
|
| 978 |
-
"total": model_count
|
| 979 |
-
}
|
| 980 |
}
|
| 981 |
except Exception as e:
|
| 982 |
logger.error(f"Status endpoint error: {e}")
|
| 983 |
return {
|
|
|
|
|
|
|
| 984 |
"status": "error",
|
|
|
|
| 985 |
"timestamp": datetime.now().isoformat(),
|
| 986 |
-
"
|
| 987 |
-
"
|
| 988 |
-
"
|
|
|
|
|
|
|
| 989 |
}
|
| 990 |
|
| 991 |
-
|
| 992 |
@app.get("/api/stats")
|
| 993 |
async def get_stats():
|
| 994 |
"""System statistics"""
|
|
@@ -1011,122 +1029,198 @@ async def get_stats():
|
|
| 1011 |
|
| 1012 |
# ===== Market Data Endpoint =====
|
| 1013 |
@app.get("/api/market")
|
| 1014 |
-
async def get_market_data():
|
| 1015 |
-
"""
|
| 1016 |
cryptocurrencies = []
|
| 1017 |
-
|
| 1018 |
-
|
| 1019 |
-
|
| 1020 |
-
|
| 1021 |
-
|
| 1022 |
-
|
| 1023 |
-
|
| 1024 |
-
|
| 1025 |
-
|
| 1026 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1027 |
try:
|
| 1028 |
-
|
| 1029 |
-
|
| 1030 |
-
|
| 1031 |
-
|
| 1032 |
-
|
| 1033 |
-
|
| 1034 |
-
"
|
| 1035 |
-
"
|
| 1036 |
-
"
|
| 1037 |
-
"
|
| 1038 |
-
|
| 1039 |
-
|
| 1040 |
-
|
| 1041 |
-
|
| 1042 |
-
|
| 1043 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1044 |
|
| 1045 |
-
|
| 1046 |
-
|
| 1047 |
-
|
| 1048 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1049 |
"name": coin_info["name"],
|
| 1050 |
-
"
|
| 1051 |
-
"
|
| 1052 |
-
"
|
| 1053 |
-
"
|
| 1054 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1055 |
})
|
| 1056 |
-
|
| 1057 |
-
|
| 1058 |
-
|
| 1059 |
-
|
| 1060 |
-
|
| 1061 |
-
|
| 1062 |
-
|
| 1063 |
-
|
| 1064 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1065 |
latest_prices = get_latest_prices_from_db()
|
| 1066 |
-
|
| 1067 |
-
|
| 1068 |
-
|
| 1069 |
-
|
| 1070 |
-
|
| 1071 |
-
|
| 1072 |
-
|
| 1073 |
-
|
| 1074 |
-
|
| 1075 |
-
|
| 1076 |
-
|
| 1077 |
-
|
| 1078 |
-
|
| 1079 |
-
|
| 1080 |
-
|
| 1081 |
-
|
| 1082 |
-
|
| 1083 |
-
|
| 1084 |
-
|
| 1085 |
-
|
| 1086 |
-
"rank": coin_info["rank"],
|
| 1087 |
-
"name": coin_info["name"],
|
| 1088 |
-
"symbol": coin_info["symbol"],
|
| 1089 |
-
"price": 0,
|
| 1090 |
-
"change_24h": 0,
|
| 1091 |
-
"market_cap": 0,
|
| 1092 |
-
"volume_24h": 0,
|
| 1093 |
-
"image": coin_info["image"]
|
| 1094 |
-
})
|
| 1095 |
-
|
| 1096 |
-
# If still no data, return empty structure with message
|
| 1097 |
if not cryptocurrencies:
|
| 1098 |
-
logger.error("No market data available from API or database")
|
| 1099 |
return {
|
|
|
|
|
|
|
| 1100 |
"cryptocurrencies": [],
|
| 1101 |
-
"
|
| 1102 |
-
"
|
|
|
|
|
|
|
|
|
|
| 1103 |
"timestamp": datetime.now().isoformat(),
|
| 1104 |
-
"source": "No data available",
|
| 1105 |
-
"error": "Unable to fetch market data. Please try again later.",
|
| 1106 |
-
"message": "Market data temporarily unavailable"
|
| 1107 |
}
|
| 1108 |
|
| 1109 |
-
|
| 1110 |
-
|
| 1111 |
-
|
| 1112 |
-
if total_market_cap > 0:
|
| 1113 |
-
btc_entry = next((c for c in cryptocurrencies if c["symbol"] == "BTC"), None)
|
| 1114 |
-
if btc_entry:
|
| 1115 |
-
btc_dominance = (btc_entry["market_cap"] / total_market_cap) * 100
|
| 1116 |
-
|
| 1117 |
-
response = {
|
| 1118 |
"cryptocurrencies": cryptocurrencies,
|
|
|
|
| 1119 |
"total_market_cap": total_market_cap,
|
| 1120 |
"btc_dominance": btc_dominance,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1121 |
"timestamp": datetime.now().isoformat(),
|
| 1122 |
-
"source": data_source
|
| 1123 |
}
|
| 1124 |
-
|
| 1125 |
-
if use_fallback:
|
| 1126 |
-
response["warning"] = "Using cached data from database. API unavailable."
|
| 1127 |
-
|
| 1128 |
-
return response
|
| 1129 |
-
|
| 1130 |
|
| 1131 |
@app.get("/api/market/history")
|
| 1132 |
async def get_market_history(symbol: str = "BTC", limit: int = 10):
|
|
@@ -1630,34 +1724,48 @@ async def get_resources_apis_raw():
|
|
| 1630 |
|
| 1631 |
|
| 1632 |
@app.get("/api/trending")
|
| 1633 |
-
async def get_trending():
|
| 1634 |
-
"""Trending coins from CoinGecko
|
| 1635 |
try:
|
| 1636 |
data = await fetch_coingecko_trending()
|
| 1637 |
-
|
| 1638 |
trending_coins = []
|
| 1639 |
-
if "coins" in data:
|
| 1640 |
-
for item in data["coins"][:10]:
|
| 1641 |
coin = item.get("item", {})
|
|
|
|
| 1642 |
trending_coins.append({
|
| 1643 |
"id": coin.get("id"),
|
| 1644 |
"name": coin.get("name"),
|
| 1645 |
-
"symbol":
|
|
|
|
|
|
|
| 1646 |
"market_cap_rank": coin.get("market_cap_rank"),
|
| 1647 |
"thumb": coin.get("thumb"),
|
| 1648 |
-
"score": coin.get("score", 0)
|
|
|
|
| 1649 |
})
|
| 1650 |
-
|
| 1651 |
return {
|
|
|
|
|
|
|
| 1652 |
"trending": trending_coins,
|
| 1653 |
"count": len(trending_coins),
|
| 1654 |
"timestamp": datetime.now().isoformat(),
|
| 1655 |
-
"source": "CoinGecko API (Real Data)"
|
|
|
|
| 1656 |
}
|
| 1657 |
-
|
| 1658 |
except Exception as e:
|
| 1659 |
-
|
| 1660 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1661 |
|
| 1662 |
# ===== Providers Management Endpoints =====
|
| 1663 |
@app.get("/api/providers")
|
|
@@ -1991,7 +2099,7 @@ async def run_diagnostic_test():
|
|
| 1991 |
"duration_seconds": 0,
|
| 1992 |
"summary": {
|
| 1993 |
"transformers_available": False,
|
| 1994 |
-
"
|
| 1995 |
"models_loaded": 0,
|
| 1996 |
"critical_issues": ["Diagnostic script not found"]
|
| 1997 |
}
|
|
@@ -2016,7 +2124,7 @@ async def run_diagnostic_test():
|
|
| 2016 |
# Parse output for summary information
|
| 2017 |
summary = {
|
| 2018 |
"transformers_available": "✅ transformers:" in full_output and "OK" in full_output,
|
| 2019 |
-
"
|
| 2020 |
"models_loaded": 0, # Would need more parsing to count actual loaded models
|
| 2021 |
"critical_issues": []
|
| 2022 |
}
|
|
@@ -2046,7 +2154,7 @@ async def run_diagnostic_test():
|
|
| 2046 |
"duration_seconds": round(duration, 2),
|
| 2047 |
"summary": {
|
| 2048 |
"transformers_available": False,
|
| 2049 |
-
"
|
| 2050 |
"models_loaded": 0,
|
| 2051 |
"critical_issues": ["Test execution timed out"]
|
| 2052 |
}
|
|
@@ -2061,7 +2169,7 @@ async def run_diagnostic_test():
|
|
| 2061 |
"duration_seconds": round(duration, 2),
|
| 2062 |
"summary": {
|
| 2063 |
"transformers_available": False,
|
| 2064 |
-
"
|
| 2065 |
"models_loaded": 0,
|
| 2066 |
"critical_issues": [f"Execution error: {str(e)}"]
|
| 2067 |
}
|
|
@@ -2352,81 +2460,99 @@ async def get_defi():
|
|
| 2352 |
# ===== News Endpoint (compatible with UI) =====
|
| 2353 |
@app.get("/api/news")
|
| 2354 |
async def get_news_api(limit: int = 20):
|
| 2355 |
-
"""Get news
|
|
|
|
|
|
|
|
|
|
| 2356 |
try:
|
| 2357 |
-
# Try to get news from database first
|
| 2358 |
conn = sqlite3.connect(str(DB_PATH))
|
| 2359 |
cursor = conn.cursor()
|
| 2360 |
cursor.execute("""
|
| 2361 |
-
SELECT * FROM news_articles
|
| 2362 |
-
ORDER BY analyzed_at DESC
|
| 2363 |
LIMIT ?
|
| 2364 |
""", (limit,))
|
| 2365 |
rows = cursor.fetchall()
|
| 2366 |
columns = [desc[0] for desc in cursor.description]
|
| 2367 |
conn.close()
|
| 2368 |
-
|
| 2369 |
-
results = []
|
| 2370 |
for row in rows:
|
| 2371 |
record = dict(zip(columns, row))
|
| 2372 |
if record.get("related_symbols"):
|
| 2373 |
try:
|
| 2374 |
record["related_symbols"] = json.loads(record["related_symbols"])
|
| 2375 |
-
except:
|
| 2376 |
pass
|
| 2377 |
results.append(record)
|
| 2378 |
-
|
| 2379 |
-
|
| 2380 |
-
|
| 2381 |
-
|
| 2382 |
-
|
| 2383 |
-
|
| 2384 |
-
|
| 2385 |
-
|
| 2386 |
-
async with httpx.AsyncClient(timeout=10.0) as client:
|
| 2387 |
-
# Try CryptoCompare News API with API key
|
| 2388 |
response = await client.get(
|
| 2389 |
-
"https://
|
| 2390 |
-
|
| 2391 |
-
"User-Agent": "Mozilla/5.0",
|
| 2392 |
-
"authorization": f"Apikey {cryptocompare_api_key}"
|
| 2393 |
-
}
|
| 2394 |
)
|
| 2395 |
if response.status_code == 200:
|
| 2396 |
-
|
| 2397 |
-
|
| 2398 |
-
|
| 2399 |
-
|
| 2400 |
-
|
| 2401 |
-
|
| 2402 |
-
|
| 2403 |
-
|
| 2404 |
-
|
| 2405 |
-
|
| 2406 |
-
|
| 2407 |
-
|
| 2408 |
-
|
| 2409 |
-
|
| 2410 |
-
|
| 2411 |
-
|
| 2412 |
-
|
| 2413 |
-
logger.warning(f"External news API failed: {api_error}")
|
| 2414 |
-
|
| 2415 |
-
return {
|
| 2416 |
-
"success": True,
|
| 2417 |
-
"news": results,
|
| 2418 |
-
"count": len(results),
|
| 2419 |
-
"source": "database" if len(results) > 0 and rows else "external_api"
|
| 2420 |
-
}
|
| 2421 |
-
except Exception as e:
|
| 2422 |
-
logger.error(f"Error in get_news_api: {e}")
|
| 2423 |
-
return {
|
| 2424 |
-
"success": False,
|
| 2425 |
-
"news": [],
|
| 2426 |
-
"count": 0,
|
| 2427 |
-
"error": str(e)
|
| 2428 |
-
}
|
| 2429 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2430 |
|
| 2431 |
# ===== Logs Endpoints =====
|
| 2432 |
@app.get("/api/logs/summary")
|
|
@@ -2899,7 +3025,7 @@ async def analyze_news(request: Dict[str, Any]):
|
|
| 2899 |
return {
|
| 2900 |
"success": True,
|
| 2901 |
"available": True,
|
| 2902 |
-
"
|
| 2903 |
"news": {
|
| 2904 |
"title": title,
|
| 2905 |
"sentiment": sentiment_label,
|
|
@@ -2980,7 +3106,7 @@ async def get_sentiment_history(
|
|
| 2980 |
async def fetch_and_save_news(limit: int = 50):
|
| 2981 |
"""Fetch news from CryptoCompare API and save to database"""
|
| 2982 |
try:
|
| 2983 |
-
cryptocompare_api_key = os.getenv("CRYPTOCOMPARE_API_KEY", "
|
| 2984 |
|
| 2985 |
async with httpx.AsyncClient(timeout=15.0) as client:
|
| 2986 |
response = await client.get(
|
|
@@ -3065,52 +3191,58 @@ async def fetch_and_save_news(limit: int = 50):
|
|
| 3065 |
|
| 3066 |
|
| 3067 |
@app.get("/api/news/latest")
|
| 3068 |
-
async def get_latest_news(
|
| 3069 |
-
|
| 3070 |
-
sentiment: Optional[str] = None
|
| 3071 |
-
):
|
| 3072 |
-
"""Get latest analyzed news from database"""
|
| 3073 |
try:
|
| 3074 |
conn = sqlite3.connect(str(DB_PATH))
|
| 3075 |
cursor = conn.cursor()
|
| 3076 |
-
|
| 3077 |
if sentiment:
|
| 3078 |
cursor.execute("""
|
| 3079 |
-
SELECT * FROM news_articles
|
| 3080 |
-
WHERE sentiment_label = ?
|
| 3081 |
-
ORDER BY analyzed_at DESC
|
| 3082 |
LIMIT ?
|
| 3083 |
""", (sentiment.lower(), limit))
|
| 3084 |
else:
|
| 3085 |
cursor.execute("""
|
| 3086 |
-
SELECT * FROM news_articles
|
| 3087 |
-
ORDER BY analyzed_at DESC
|
| 3088 |
LIMIT ?
|
| 3089 |
""", (limit,))
|
| 3090 |
-
|
| 3091 |
rows = cursor.fetchall()
|
| 3092 |
columns = [desc[0] for desc in cursor.description]
|
| 3093 |
conn.close()
|
| 3094 |
-
|
| 3095 |
results = []
|
| 3096 |
for row in rows:
|
| 3097 |
record = dict(zip(columns, row))
|
| 3098 |
if record.get("related_symbols"):
|
| 3099 |
try:
|
| 3100 |
record["related_symbols"] = json.loads(record["related_symbols"])
|
| 3101 |
-
except:
|
| 3102 |
pass
|
| 3103 |
results.append(record)
|
| 3104 |
-
|
| 3105 |
return {
|
| 3106 |
"success": True,
|
|
|
|
|
|
|
| 3107 |
"count": len(results),
|
| 3108 |
-
"
|
|
|
|
|
|
|
| 3109 |
}
|
| 3110 |
-
|
| 3111 |
except Exception as e:
|
| 3112 |
-
|
| 3113 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3114 |
|
| 3115 |
@app.post("/api/news/summarize")
|
| 3116 |
async def summarize_news(request: Dict[str, Any]):
|
|
@@ -3920,21 +4052,14 @@ try:
|
|
| 3920 |
except Exception as compat_error:
|
| 3921 |
logger.warning(f"Compat routes not loaded: {compat_error}")
|
| 3922 |
|
| 3923 |
-
|
| 3924 |
-
# ===== Unified Crypto Data Platform v5: zero-conflict, non-destructive install =====
|
| 3925 |
-
# This preserves all existing V4 routes/features and only adds missing Data Brain, Hub,
|
| 3926 |
-
# and legacy compatibility endpoints. It does NOT replace or delete old routes.
|
| 3927 |
try:
|
| 3928 |
-
from
|
| 3929 |
|
| 3930 |
-
|
| 3931 |
-
logger.info(
|
| 3932 |
-
|
| 3933 |
-
|
| 3934 |
-
f"skipped_existing={len(_unified_v5_report.get('routesSkippedExisting', []))}"
|
| 3935 |
-
)
|
| 3936 |
-
except Exception as unified_v5_error:
|
| 3937 |
-
logger.warning(f"Unified Crypto Data Platform v5 not installed: {unified_v5_error}")
|
| 3938 |
|
| 3939 |
# ===== Main Entry Point =====
|
| 3940 |
if __name__ == "__main__":
|
|
|
|
| 18 |
from contextlib import asynccontextmanager
|
| 19 |
from collections import defaultdict
|
| 20 |
|
| 21 |
+
try:
|
| 22 |
+
from api_hub_registry import get_secret, provider_runtime_summary
|
| 23 |
+
except Exception:
|
| 24 |
+
def get_secret(name):
|
| 25 |
+
return os.getenv(name)
|
| 26 |
+
def provider_runtime_summary():
|
| 27 |
+
return {"totalProviders": 0, "categories": {}}
|
| 28 |
+
|
| 29 |
logger = logging.getLogger(__name__)
|
| 30 |
|
| 31 |
from fastapi import FastAPI, HTTPException, Response, Request
|
|
|
|
| 687 |
allow_headers=["*"],
|
| 688 |
)
|
| 689 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 690 |
# Middleware to ensure HTML responses have correct Content-Type
|
| 691 |
class HTMLContentTypeMiddleware(BaseHTTPMiddleware):
|
| 692 |
async def dispatch(self, request: Request, call_next):
|
|
|
|
| 873 |
|
| 874 |
@app.get("/api/health")
|
| 875 |
async def api_health():
|
| 876 |
+
"""Stable API health check for HuggingFace Space consumers."""
|
| 877 |
try:
|
| 878 |
+
version = "4.1.0-short-hunter-compat"
|
| 879 |
try:
|
|
|
|
| 880 |
api_registry = load_api_registry()
|
| 881 |
+
metadata = api_registry.get("metadata", {}) if isinstance(api_registry, dict) else {}
|
| 882 |
+
version = metadata.get("version") or version
|
|
|
|
| 883 |
except Exception:
|
| 884 |
pass
|
| 885 |
+
|
| 886 |
return {
|
| 887 |
+
"ok": True,
|
| 888 |
+
"success": True,
|
| 889 |
"status": "ok",
|
| 890 |
+
"service": "Datasourceforcryptocurrency-4",
|
| 891 |
+
"version": version,
|
| 892 |
"timestamp": datetime.now().isoformat(),
|
| 893 |
+
"uptime": None,
|
| 894 |
+
"errors": [],
|
| 895 |
}
|
| 896 |
except Exception as e:
|
|
|
|
| 897 |
logger.error(f"Health check error: {e}")
|
| 898 |
return JSONResponse(
|
| 899 |
status_code=200,
|
| 900 |
content={
|
| 901 |
+
"ok": True,
|
| 902 |
+
"success": True,
|
| 903 |
"status": "ok",
|
| 904 |
+
"service": "Datasourceforcryptocurrency-4",
|
| 905 |
+
"version": "unknown",
|
| 906 |
"timestamp": datetime.now().isoformat(),
|
| 907 |
+
"errors": [],
|
| 908 |
+
},
|
| 909 |
)
|
| 910 |
|
|
|
|
| 911 |
@app.get("/api/status")
|
| 912 |
async def get_status():
|
| 913 |
+
"""Capability-level status. Missing optional capabilities never mark the whole Space down."""
|
| 914 |
try:
|
|
|
|
| 915 |
config = load_providers_config()
|
| 916 |
+
providers_config = config.get("providers", {}) if isinstance(config, dict) else {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 917 |
resources_json = WORKSPACE_ROOT / "api-resources" / "crypto_resources_unified_2025-11-11.json"
|
| 918 |
+
|
| 919 |
resources_data = {"total": 0, "categories": {}}
|
| 920 |
+
errors = []
|
| 921 |
if resources_json.exists():
|
| 922 |
try:
|
| 923 |
+
with open(resources_json, "r", encoding="utf-8") as f:
|
| 924 |
unified_data = json.load(f)
|
| 925 |
+
registry = unified_data.get("registry", {}) if isinstance(unified_data, dict) else {}
|
|
|
|
| 926 |
for category, items in registry.items():
|
| 927 |
+
if category == "metadata":
|
| 928 |
continue
|
| 929 |
if isinstance(items, list):
|
| 930 |
count = len(items)
|
| 931 |
+
resources_data["total"] += count
|
| 932 |
+
resources_data["categories"][category.replace("_", "-")] = resources_data["categories"].get(category.replace("_", "-"), 0) + count
|
| 933 |
+
except Exception as resource_error:
|
| 934 |
+
errors.append(f"resources_load_failed: {resource_error}")
|
| 935 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 936 |
model_count = 0
|
| 937 |
try:
|
| 938 |
from ai_models import MODEL_SPECS
|
| 939 |
model_count = len(MODEL_SPECS) if MODEL_SPECS else 0
|
| 940 |
+
except Exception as model_error:
|
| 941 |
+
errors.append(f"model_registry_unavailable: {model_error}")
|
| 942 |
+
|
| 943 |
+
# Capability model: this endpoint reports route/provider capability, not a slow live external probe.
|
| 944 |
+
capabilities = {
|
| 945 |
+
"market": {"status": "available", "providers": ["CoinGecko public", "database cache"]},
|
| 946 |
+
"coinsTop": {"status": "available", "providers": ["CoinGecko public"]},
|
| 947 |
+
"trending": {"status": "available", "providers": ["CoinGecko public"]},
|
| 948 |
+
"ohlcv": {"status": "available", "providers": ["Binance public", "KuCoin public"]},
|
| 949 |
+
"klines": {"status": "available", "providers": ["Binance public", "KuCoin public"]},
|
| 950 |
+
"indicators": {"status": "available", "providers": ["local OHLCV computation"]},
|
| 951 |
+
"sentiment": {"status": "available" if model_count > 0 else "partial", "providers": ["Alternative.me", "HuggingFace models"]},
|
| 952 |
+
"news": {"status": "available_or_empty", "providers": ["database", "CryptoCompare public"]},
|
| 953 |
+
"orderbook": {"status": "available", "providers": ["KuCoin public", "Binance public"]},
|
| 954 |
+
}
|
| 955 |
+
|
| 956 |
+
missing_capabilities = [name for name, cap in capabilities.items() if cap.get("status") in {"unavailable", "missing"}]
|
| 957 |
+
degraded_capabilities = [name for name, cap in capabilities.items() if cap.get("status") in {"partial", "degraded", "available_or_empty"}]
|
| 958 |
+
|
| 959 |
+
if missing_capabilities and len(missing_capabilities) >= len(capabilities):
|
| 960 |
+
data_state = "UNAVAILABLE"
|
| 961 |
+
elif missing_capabilities:
|
| 962 |
+
data_state = "DEGRADED"
|
| 963 |
+
elif degraded_capabilities:
|
| 964 |
+
data_state = "PARTIAL"
|
| 965 |
+
else:
|
| 966 |
+
data_state = "COMPLETE"
|
| 967 |
+
|
| 968 |
+
provider_health = health_registry.get_summary() if "health_registry" in globals() else {}
|
| 969 |
+
|
| 970 |
return {
|
| 971 |
+
"ok": True,
|
| 972 |
+
"success": True,
|
| 973 |
"status": "ok",
|
| 974 |
+
"service": "Datasourceforcryptocurrency-4",
|
| 975 |
+
"dataState": data_state,
|
| 976 |
"timestamp": datetime.now().isoformat(),
|
| 977 |
"last_update": datetime.now().isoformat(),
|
| 978 |
"providers": {
|
| 979 |
+
"configuredTotal": len(providers_config),
|
| 980 |
+
"health": provider_health,
|
| 981 |
+
"market": capabilities["market"],
|
| 982 |
+
"ohlcv": capabilities["ohlcv"],
|
| 983 |
+
"indicators": capabilities["indicators"],
|
| 984 |
+
"sentiment": capabilities["sentiment"],
|
| 985 |
+
"news": capabilities["news"],
|
| 986 |
+
"orderbook": capabilities["orderbook"],
|
| 987 |
},
|
| 988 |
+
"capabilities": capabilities,
|
| 989 |
+
"missingCapabilities": missing_capabilities,
|
| 990 |
+
"degradedCapabilities": degraded_capabilities,
|
| 991 |
+
"errors": errors,
|
| 992 |
"resources": resources_data,
|
| 993 |
+
"models": {"total": model_count},
|
|
|
|
|
|
|
| 994 |
}
|
| 995 |
except Exception as e:
|
| 996 |
logger.error(f"Status endpoint error: {e}")
|
| 997 |
return {
|
| 998 |
+
"ok": False,
|
| 999 |
+
"success": False,
|
| 1000 |
"status": "error",
|
| 1001 |
+
"dataState": "DEGRADED",
|
| 1002 |
"timestamp": datetime.now().isoformat(),
|
| 1003 |
+
"providers": {},
|
| 1004 |
+
"capabilities": {},
|
| 1005 |
+
"missingCapabilities": [],
|
| 1006 |
+
"errors": [str(e)],
|
| 1007 |
+
"resources": {"total": 0, "categories": {}},
|
| 1008 |
}
|
| 1009 |
|
|
|
|
| 1010 |
@app.get("/api/stats")
|
| 1011 |
async def get_stats():
|
| 1012 |
"""System statistics"""
|
|
|
|
| 1029 |
|
| 1030 |
# ===== Market Data Endpoint =====
|
| 1031 |
@app.get("/api/market")
|
| 1032 |
+
async def get_market_data(limit: int = 100):
|
| 1033 |
+
"""Normalized market data with backward-compatible fields."""
|
| 1034 |
cryptocurrencies = []
|
| 1035 |
+
errors = []
|
| 1036 |
+
|
| 1037 |
+
# Optional primary: CoinMarketCap if configured in HuggingFace Space secrets.
|
| 1038 |
+
cmc_key = get_secret("COINMARKETCAP_KEY")
|
| 1039 |
+
if cmc_key:
|
| 1040 |
+
try:
|
| 1041 |
+
cmc_symbols = "BTC,ETH,BNB,SOL,XRP,DOGE,ADA,TRX,AVAX,LINK,DOT,MATIC,TON,LTC,BCH,UNI,ATOM,ETC,APT,ARB,OP,NEAR,FIL,INJ,SUI,SEI"
|
| 1042 |
+
async with httpx.AsyncClient(timeout=httpx.Timeout(8.0, connect=3.0), headers={**HEADERS, "X-CMC_PRO_API_KEY": cmc_key}) as client:
|
| 1043 |
+
response = await client.get(
|
| 1044 |
+
"https://pro-api.coinmarketcap.com/v1/cryptocurrency/quotes/latest",
|
| 1045 |
+
params={"symbol": cmc_symbols, "convert": "USD"},
|
| 1046 |
+
)
|
| 1047 |
+
if response.status_code == 200:
|
| 1048 |
+
payload = response.json()
|
| 1049 |
+
data = payload.get("data") if isinstance(payload, dict) else {}
|
| 1050 |
+
if isinstance(data, dict):
|
| 1051 |
+
for sym, item in data.items():
|
| 1052 |
+
quote = ((item.get("quote") or {}).get("USD") or {}) if isinstance(item, dict) else {}
|
| 1053 |
+
cryptocurrencies.append({
|
| 1054 |
+
"rank": item.get("cmc_rank"),
|
| 1055 |
+
"name": item.get("name"),
|
| 1056 |
+
"symbol": f"{sym.upper()}USDT",
|
| 1057 |
+
"baseSymbol": sym.upper(),
|
| 1058 |
+
"price": quote.get("price"),
|
| 1059 |
+
"change24h": quote.get("percent_change_24h"),
|
| 1060 |
+
"change_24h": quote.get("percent_change_24h"),
|
| 1061 |
+
"marketCap": quote.get("market_cap"),
|
| 1062 |
+
"market_cap": quote.get("market_cap"),
|
| 1063 |
+
"volume24h": quote.get("volume_24h"),
|
| 1064 |
+
"volume_24h": quote.get("volume_24h"),
|
| 1065 |
+
"source": "coinmarketcap_quotes",
|
| 1066 |
+
})
|
| 1067 |
+
else:
|
| 1068 |
+
errors.append(f"coinmarketcap_http_{response.status_code}")
|
| 1069 |
+
except Exception as cmc_error:
|
| 1070 |
+
errors.append(f"coinmarketcap_failed: {cmc_error}")
|
| 1071 |
+
|
| 1072 |
+
# Primary public fallback: CoinGecko coins/markets gives richer rows for scanners.
|
| 1073 |
try:
|
| 1074 |
+
async with httpx.AsyncClient(timeout=httpx.Timeout(8.0, connect=3.0), headers=HEADERS) as client:
|
| 1075 |
+
response = await client.get(
|
| 1076 |
+
"https://api.coingecko.com/api/v3/coins/markets",
|
| 1077 |
+
params={
|
| 1078 |
+
"vs_currency": "usd",
|
| 1079 |
+
"order": "market_cap_desc",
|
| 1080 |
+
"per_page": min(max(int(limit or 100), 1), 250),
|
| 1081 |
+
"page": 1,
|
| 1082 |
+
"sparkline": "false",
|
| 1083 |
+
"price_change_percentage": "24h",
|
| 1084 |
+
},
|
| 1085 |
+
)
|
| 1086 |
+
if response.status_code == 200:
|
| 1087 |
+
payload = response.json()
|
| 1088 |
+
if isinstance(payload, list):
|
| 1089 |
+
for item in payload:
|
| 1090 |
+
base_symbol = str(item.get("symbol", "")).upper()
|
| 1091 |
+
normalized_symbol = f"{base_symbol}USDT" if base_symbol and not base_symbol.endswith("USDT") else base_symbol
|
| 1092 |
+
cryptocurrencies.append({
|
| 1093 |
+
"rank": item.get("market_cap_rank"),
|
| 1094 |
+
"name": item.get("name"),
|
| 1095 |
+
"symbol": normalized_symbol,
|
| 1096 |
+
"baseSymbol": base_symbol,
|
| 1097 |
+
"price": item.get("current_price"),
|
| 1098 |
+
"change24h": item.get("price_change_percentage_24h"),
|
| 1099 |
+
"change_24h": item.get("price_change_percentage_24h"),
|
| 1100 |
+
"marketCap": item.get("market_cap"),
|
| 1101 |
+
"market_cap": item.get("market_cap"),
|
| 1102 |
+
"volume24h": item.get("total_volume"),
|
| 1103 |
+
"volume_24h": item.get("total_volume"),
|
| 1104 |
+
"image": item.get("image"),
|
| 1105 |
+
"source": "coingecko_markets",
|
| 1106 |
+
})
|
| 1107 |
+
else:
|
| 1108 |
+
errors.append(f"coingecko_markets_http_{response.status_code}")
|
| 1109 |
+
except Exception as market_error:
|
| 1110 |
+
errors.append(f"coingecko_markets_failed: {market_error}")
|
| 1111 |
|
| 1112 |
+
# Fallback: existing simple-price helper for BTC/ETH/BNB.
|
| 1113 |
+
if not cryptocurrencies:
|
| 1114 |
+
coin_mapping = {
|
| 1115 |
+
"bitcoin": {"name": "Bitcoin", "symbol": "BTCUSDT", "rank": 1, "image": "https://assets.coingecko.com/coins/images/1/small/bitcoin.png"},
|
| 1116 |
+
"ethereum": {"name": "Ethereum", "symbol": "ETHUSDT", "rank": 2, "image": "https://assets.coingecko.com/coins/images/279/small/ethereum.png"},
|
| 1117 |
+
"binancecoin": {"name": "BNB", "symbol": "BNBUSDT", "rank": 3, "image": "https://assets.coingecko.com/coins/images/825/small/bnb-icon2_2x.png"},
|
| 1118 |
+
}
|
| 1119 |
+
try:
|
| 1120 |
+
data = await fetch_coingecko_simple_price()
|
| 1121 |
+
for coin_id, coin_info in coin_mapping.items():
|
| 1122 |
+
coin_data = data.get(coin_id, {}) if isinstance(data, dict) else {}
|
| 1123 |
+
if coin_data:
|
| 1124 |
+
cryptocurrencies.append({
|
| 1125 |
+
"rank": coin_info["rank"],
|
| 1126 |
"name": coin_info["name"],
|
| 1127 |
+
"symbol": coin_info["symbol"],
|
| 1128 |
+
"baseSymbol": coin_info["symbol"].replace("USDT", ""),
|
| 1129 |
+
"price": coin_data.get("usd", 0),
|
| 1130 |
+
"change24h": coin_data.get("usd_24h_change", 0),
|
| 1131 |
+
"change_24h": coin_data.get("usd_24h_change", 0),
|
| 1132 |
+
"marketCap": coin_data.get("usd_market_cap", 0),
|
| 1133 |
+
"market_cap": coin_data.get("usd_market_cap", 0),
|
| 1134 |
+
"volume24h": coin_data.get("usd_24h_vol", 0),
|
| 1135 |
+
"volume_24h": coin_data.get("usd_24h_vol", 0),
|
| 1136 |
+
"image": coin_info["image"],
|
| 1137 |
+
"source": "coingecko_simple_price",
|
| 1138 |
})
|
| 1139 |
+
except Exception as simple_error:
|
| 1140 |
+
errors.append(f"coingecko_simple_price_failed: {simple_error}")
|
| 1141 |
+
|
| 1142 |
+
# Secondary fallback: CryptoCompare prices, with optional key.
|
| 1143 |
+
if not cryptocurrencies:
|
| 1144 |
+
try:
|
| 1145 |
+
cc_key = get_secret("CRYPTOCOMPARE_KEY")
|
| 1146 |
+
params = {"fsyms": "BTC,ETH,BNB,SOL,XRP,DOGE,ADA,TRX,AVAX,LINK", "tsyms": "USD"}
|
| 1147 |
+
if cc_key:
|
| 1148 |
+
params["api_key"] = cc_key
|
| 1149 |
+
async with httpx.AsyncClient(timeout=httpx.Timeout(8.0, connect=3.0), headers=HEADERS) as client:
|
| 1150 |
+
response = await client.get("https://min-api.cryptocompare.com/data/pricemultifull", params=params)
|
| 1151 |
+
if response.status_code == 200:
|
| 1152 |
+
payload = response.json()
|
| 1153 |
+
raw = (payload.get("RAW") or {}) if isinstance(payload, dict) else {}
|
| 1154 |
+
for sym, row in raw.items():
|
| 1155 |
+
usd = (row or {}).get("USD") or {}
|
| 1156 |
+
cryptocurrencies.append({
|
| 1157 |
+
"rank": None,
|
| 1158 |
+
"name": sym.upper(),
|
| 1159 |
+
"symbol": f"{sym.upper()}USDT",
|
| 1160 |
+
"baseSymbol": sym.upper(),
|
| 1161 |
+
"price": usd.get("PRICE"),
|
| 1162 |
+
"change24h": usd.get("CHANGEPCT24HOUR"),
|
| 1163 |
+
"change_24h": usd.get("CHANGEPCT24HOUR"),
|
| 1164 |
+
"marketCap": usd.get("MKTCAP"),
|
| 1165 |
+
"market_cap": usd.get("MKTCAP"),
|
| 1166 |
+
"volume24h": usd.get("VOLUME24HOURTO"),
|
| 1167 |
+
"volume_24h": usd.get("VOLUME24HOURTO"),
|
| 1168 |
+
"source": "cryptocompare_pricemultifull",
|
| 1169 |
+
})
|
| 1170 |
+
else:
|
| 1171 |
+
errors.append(f"cryptocompare_prices_http_{response.status_code}")
|
| 1172 |
+
except Exception as cc_error:
|
| 1173 |
+
errors.append(f"cryptocompare_prices_failed: {cc_error}")
|
| 1174 |
+
|
| 1175 |
+
# Last fallback: cached DB rows. Zero-placeholder rows are no longer advertised as valid data.
|
| 1176 |
+
if not cryptocurrencies:
|
| 1177 |
latest_prices = get_latest_prices_from_db()
|
| 1178 |
+
for symbol, db_data in latest_prices.items():
|
| 1179 |
+
cryptocurrencies.append({
|
| 1180 |
+
"rank": db_data.get("rank"),
|
| 1181 |
+
"name": db_data.get("name"),
|
| 1182 |
+
"symbol": f"{symbol}USDT" if not str(symbol).upper().endswith("USDT") else str(symbol).upper(),
|
| 1183 |
+
"baseSymbol": symbol,
|
| 1184 |
+
"price": db_data.get("price_usd", 0),
|
| 1185 |
+
"change24h": db_data.get("percent_change_24h", 0),
|
| 1186 |
+
"change_24h": db_data.get("percent_change_24h", 0),
|
| 1187 |
+
"marketCap": db_data.get("market_cap", 0),
|
| 1188 |
+
"market_cap": db_data.get("market_cap", 0),
|
| 1189 |
+
"volume24h": db_data.get("volume_24h", 0),
|
| 1190 |
+
"volume_24h": db_data.get("volume_24h", 0),
|
| 1191 |
+
"source": "sqlite_cache",
|
| 1192 |
+
})
|
| 1193 |
+
|
| 1194 |
+
total_market_cap = sum((c.get("marketCap") or c.get("market_cap") or 0) for c in cryptocurrencies)
|
| 1195 |
+
btc_entry = next((c for c in cryptocurrencies if str(c.get("symbol", "")).startswith("BTC")), None)
|
| 1196 |
+
btc_dominance = ((btc_entry.get("marketCap") or btc_entry.get("market_cap") or 0) / total_market_cap * 100) if btc_entry and total_market_cap else 0
|
| 1197 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1198 |
if not cryptocurrencies:
|
|
|
|
| 1199 |
return {
|
| 1200 |
+
"success": False,
|
| 1201 |
+
"data": [],
|
| 1202 |
"cryptocurrencies": [],
|
| 1203 |
+
"count": 0,
|
| 1204 |
+
"dataState": "UNAVAILABLE",
|
| 1205 |
+
"missingCapabilities": ["market"],
|
| 1206 |
+
"errors": errors or ["No market provider returned data"],
|
| 1207 |
+
"source": "none",
|
| 1208 |
"timestamp": datetime.now().isoformat(),
|
|
|
|
|
|
|
|
|
|
| 1209 |
}
|
| 1210 |
|
| 1211 |
+
return {
|
| 1212 |
+
"success": True,
|
| 1213 |
+
"data": cryptocurrencies,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1214 |
"cryptocurrencies": cryptocurrencies,
|
| 1215 |
+
"count": len(cryptocurrencies),
|
| 1216 |
"total_market_cap": total_market_cap,
|
| 1217 |
"btc_dominance": btc_dominance,
|
| 1218 |
+
"dataState": "COMPLETE" if not errors else "PARTIAL",
|
| 1219 |
+
"missingCapabilities": [],
|
| 1220 |
+
"errors": errors,
|
| 1221 |
+
"source": cryptocurrencies[0].get("source", "mixed_public_sources"),
|
| 1222 |
"timestamp": datetime.now().isoformat(),
|
|
|
|
| 1223 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1224 |
|
| 1225 |
@app.get("/api/market/history")
|
| 1226 |
async def get_market_history(symbol: str = "BTC", limit: int = 10):
|
|
|
|
| 1724 |
|
| 1725 |
|
| 1726 |
@app.get("/api/trending")
|
| 1727 |
+
async def get_trending(limit: int = 10):
|
| 1728 |
+
"""Trending coins from CoinGecko, normalized for API clients."""
|
| 1729 |
try:
|
| 1730 |
data = await fetch_coingecko_trending()
|
|
|
|
| 1731 |
trending_coins = []
|
| 1732 |
+
if isinstance(data, dict) and "coins" in data:
|
| 1733 |
+
for item in data["coins"][: min(max(int(limit or 10), 1), 50)]:
|
| 1734 |
coin = item.get("item", {})
|
| 1735 |
+
sym = str(coin.get("symbol", "")).upper()
|
| 1736 |
trending_coins.append({
|
| 1737 |
"id": coin.get("id"),
|
| 1738 |
"name": coin.get("name"),
|
| 1739 |
+
"symbol": f"{sym}USDT" if sym and not sym.endswith("USDT") else sym,
|
| 1740 |
+
"baseSymbol": sym,
|
| 1741 |
+
"marketCapRank": coin.get("market_cap_rank"),
|
| 1742 |
"market_cap_rank": coin.get("market_cap_rank"),
|
| 1743 |
"thumb": coin.get("thumb"),
|
| 1744 |
+
"score": coin.get("score", 0),
|
| 1745 |
+
"source": "coingecko_trending",
|
| 1746 |
})
|
| 1747 |
+
|
| 1748 |
return {
|
| 1749 |
+
"success": True,
|
| 1750 |
+
"data": trending_coins,
|
| 1751 |
"trending": trending_coins,
|
| 1752 |
"count": len(trending_coins),
|
| 1753 |
"timestamp": datetime.now().isoformat(),
|
| 1754 |
+
"source": "CoinGecko API (Real Data)",
|
| 1755 |
+
"errors": [],
|
| 1756 |
}
|
|
|
|
| 1757 |
except Exception as e:
|
| 1758 |
+
logger.warning(f"Trending fetch failed: {e}")
|
| 1759 |
+
return {
|
| 1760 |
+
"success": False,
|
| 1761 |
+
"data": [],
|
| 1762 |
+
"trending": [],
|
| 1763 |
+
"count": 0,
|
| 1764 |
+
"timestamp": datetime.now().isoformat(),
|
| 1765 |
+
"source": "coingecko_trending",
|
| 1766 |
+
"errors": [str(e)],
|
| 1767 |
+
"missingCapabilities": ["trending"],
|
| 1768 |
+
}
|
| 1769 |
|
| 1770 |
# ===== Providers Management Endpoints =====
|
| 1771 |
@app.get("/api/providers")
|
|
|
|
| 2099 |
"duration_seconds": 0,
|
| 2100 |
"summary": {
|
| 2101 |
"transformers_available": False,
|
| 2102 |
+
"hf_hub": False,
|
| 2103 |
"models_loaded": 0,
|
| 2104 |
"critical_issues": ["Diagnostic script not found"]
|
| 2105 |
}
|
|
|
|
| 2124 |
# Parse output for summary information
|
| 2125 |
summary = {
|
| 2126 |
"transformers_available": "✅ transformers:" in full_output and "OK" in full_output,
|
| 2127 |
+
"hf_hub": "✅ Hub connection:" in full_output and "OK" in full_output,
|
| 2128 |
"models_loaded": 0, # Would need more parsing to count actual loaded models
|
| 2129 |
"critical_issues": []
|
| 2130 |
}
|
|
|
|
| 2154 |
"duration_seconds": round(duration, 2),
|
| 2155 |
"summary": {
|
| 2156 |
"transformers_available": False,
|
| 2157 |
+
"hf_hub": False,
|
| 2158 |
"models_loaded": 0,
|
| 2159 |
"critical_issues": ["Test execution timed out"]
|
| 2160 |
}
|
|
|
|
| 2169 |
"duration_seconds": round(duration, 2),
|
| 2170 |
"summary": {
|
| 2171 |
"transformers_available": False,
|
| 2172 |
+
"hf_hub": False,
|
| 2173 |
"models_loaded": 0,
|
| 2174 |
"critical_issues": [f"Execution error: {str(e)}"]
|
| 2175 |
}
|
|
|
|
| 2460 |
# ===== News Endpoint (compatible with UI) =====
|
| 2461 |
@app.get("/api/news")
|
| 2462 |
async def get_news_api(limit: int = 20):
|
| 2463 |
+
"""Get news as structured JSON. Empty news is not fatal."""
|
| 2464 |
+
results = []
|
| 2465 |
+
errors = []
|
| 2466 |
+
db_rows_available = False
|
| 2467 |
try:
|
|
|
|
| 2468 |
conn = sqlite3.connect(str(DB_PATH))
|
| 2469 |
cursor = conn.cursor()
|
| 2470 |
cursor.execute("""
|
| 2471 |
+
SELECT * FROM news_articles
|
| 2472 |
+
ORDER BY analyzed_at DESC
|
| 2473 |
LIMIT ?
|
| 2474 |
""", (limit,))
|
| 2475 |
rows = cursor.fetchall()
|
| 2476 |
columns = [desc[0] for desc in cursor.description]
|
| 2477 |
conn.close()
|
| 2478 |
+
db_rows_available = bool(rows)
|
|
|
|
| 2479 |
for row in rows:
|
| 2480 |
record = dict(zip(columns, row))
|
| 2481 |
if record.get("related_symbols"):
|
| 2482 |
try:
|
| 2483 |
record["related_symbols"] = json.loads(record["related_symbols"])
|
| 2484 |
+
except Exception:
|
| 2485 |
pass
|
| 2486 |
results.append(record)
|
| 2487 |
+
except Exception as db_error:
|
| 2488 |
+
errors.append(f"database_news_failed: {db_error}")
|
| 2489 |
+
|
| 2490 |
+
if not results:
|
| 2491 |
+
try:
|
| 2492 |
+
newsapi_key = get_secret("NEWSAPI_KEY")
|
| 2493 |
+
if newsapi_key:
|
| 2494 |
+
async with httpx.AsyncClient(timeout=httpx.Timeout(8.0, connect=3.0), headers=HEADERS) as client:
|
|
|
|
|
|
|
| 2495 |
response = await client.get(
|
| 2496 |
+
"https://newsapi.org/v2/everything",
|
| 2497 |
+
params={"q": "crypto OR bitcoin OR ethereum", "language": "en", "pageSize": min(max(int(limit or 20), 1), 100), "apiKey": newsapi_key, "sortBy": "publishedAt"},
|
|
|
|
|
|
|
|
|
|
| 2498 |
)
|
| 2499 |
if response.status_code == 200:
|
| 2500 |
+
payload = response.json()
|
| 2501 |
+
for article in (payload.get("articles") or [])[:limit]:
|
| 2502 |
+
results.append({
|
| 2503 |
+
"title": article.get("title", ""),
|
| 2504 |
+
"content": article.get("description") or article.get("content") or "",
|
| 2505 |
+
"url": article.get("url", ""),
|
| 2506 |
+
"source": (article.get("source") or {}).get("name", "NewsAPI"),
|
| 2507 |
+
"sentiment_label": None,
|
| 2508 |
+
"sentiment_confidence": None,
|
| 2509 |
+
"related_symbols": [],
|
| 2510 |
+
"published_date": article.get("publishedAt"),
|
| 2511 |
+
"analyzed_at": datetime.now().isoformat(),
|
| 2512 |
+
})
|
| 2513 |
+
else:
|
| 2514 |
+
errors.append(f"newsapi_http_{response.status_code}")
|
| 2515 |
+
except Exception as newsapi_error:
|
| 2516 |
+
errors.append(f"newsapi_failed: {newsapi_error}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2517 |
|
| 2518 |
+
if not results:
|
| 2519 |
+
try:
|
| 2520 |
+
cryptocompare_api_key = get_secret("CRYPTOCOMPARE_KEY")
|
| 2521 |
+
headers = {"User-Agent": "Mozilla/5.0"}
|
| 2522 |
+
if cryptocompare_api_key:
|
| 2523 |
+
headers["authorization"] = f"Apikey {cryptocompare_api_key}"
|
| 2524 |
+
async with httpx.AsyncClient(timeout=httpx.Timeout(8.0, connect=3.0)) as client:
|
| 2525 |
+
response = await client.get("https://min-api.cryptocompare.com/data/v2/news/?lang=EN", headers=headers)
|
| 2526 |
+
if response.status_code == 200:
|
| 2527 |
+
data = response.json()
|
| 2528 |
+
for article in (data.get("Data") or [])[:limit]:
|
| 2529 |
+
results.append({
|
| 2530 |
+
"id": article.get("id"),
|
| 2531 |
+
"title": article.get("title", ""),
|
| 2532 |
+
"content": article.get("body", "")[:500],
|
| 2533 |
+
"url": article.get("url", ""),
|
| 2534 |
+
"source": article.get("source", "CryptoCompare"),
|
| 2535 |
+
"sentiment_label": None,
|
| 2536 |
+
"sentiment_confidence": None,
|
| 2537 |
+
"related_symbols": article.get("categories", "").split("|") if article.get("categories") else [],
|
| 2538 |
+
"published_date": datetime.fromtimestamp(article.get("published_on", 0)).isoformat() if article.get("published_on") else None,
|
| 2539 |
+
"analyzed_at": datetime.now().isoformat(),
|
| 2540 |
+
})
|
| 2541 |
+
else:
|
| 2542 |
+
errors.append(f"cryptocompare_news_http_{response.status_code}")
|
| 2543 |
+
except Exception as api_error:
|
| 2544 |
+
errors.append(f"external_news_failed: {api_error}")
|
| 2545 |
+
|
| 2546 |
+
return {
|
| 2547 |
+
"success": True,
|
| 2548 |
+
"data": results,
|
| 2549 |
+
"news": results,
|
| 2550 |
+
"count": len(results),
|
| 2551 |
+
"status": "available" if results else "empty",
|
| 2552 |
+
"source": "database" if db_rows_available else "external_or_empty",
|
| 2553 |
+
"errors": errors,
|
| 2554 |
+
"timestamp": datetime.now().isoformat(),
|
| 2555 |
+
}
|
| 2556 |
|
| 2557 |
# ===== Logs Endpoints =====
|
| 2558 |
@app.get("/api/logs/summary")
|
|
|
|
| 3025 |
return {
|
| 3026 |
"success": True,
|
| 3027 |
"available": True,
|
| 3028 |
+
"hf_available": hf_available,
|
| 3029 |
"news": {
|
| 3030 |
"title": title,
|
| 3031 |
"sentiment": sentiment_label,
|
|
|
|
| 3106 |
async def fetch_and_save_news(limit: int = 50):
|
| 3107 |
"""Fetch news from CryptoCompare API and save to database"""
|
| 3108 |
try:
|
| 3109 |
+
cryptocompare_api_key = os.getenv("CRYPTOCOMPARE_API_KEY", "<REDACTED_API_KEY>")
|
| 3110 |
|
| 3111 |
async with httpx.AsyncClient(timeout=15.0) as client:
|
| 3112 |
response = await client.get(
|
|
|
|
| 3191 |
|
| 3192 |
|
| 3193 |
@app.get("/api/news/latest")
|
| 3194 |
+
async def get_latest_news(limit: int = 20, sentiment: Optional[str] = None):
|
| 3195 |
+
"""Get latest analyzed news; empty is a valid non-fatal state."""
|
|
|
|
|
|
|
|
|
|
| 3196 |
try:
|
| 3197 |
conn = sqlite3.connect(str(DB_PATH))
|
| 3198 |
cursor = conn.cursor()
|
|
|
|
| 3199 |
if sentiment:
|
| 3200 |
cursor.execute("""
|
| 3201 |
+
SELECT * FROM news_articles
|
| 3202 |
+
WHERE sentiment_label = ?
|
| 3203 |
+
ORDER BY analyzed_at DESC
|
| 3204 |
LIMIT ?
|
| 3205 |
""", (sentiment.lower(), limit))
|
| 3206 |
else:
|
| 3207 |
cursor.execute("""
|
| 3208 |
+
SELECT * FROM news_articles
|
| 3209 |
+
ORDER BY analyzed_at DESC
|
| 3210 |
LIMIT ?
|
| 3211 |
""", (limit,))
|
|
|
|
| 3212 |
rows = cursor.fetchall()
|
| 3213 |
columns = [desc[0] for desc in cursor.description]
|
| 3214 |
conn.close()
|
| 3215 |
+
|
| 3216 |
results = []
|
| 3217 |
for row in rows:
|
| 3218 |
record = dict(zip(columns, row))
|
| 3219 |
if record.get("related_symbols"):
|
| 3220 |
try:
|
| 3221 |
record["related_symbols"] = json.loads(record["related_symbols"])
|
| 3222 |
+
except Exception:
|
| 3223 |
pass
|
| 3224 |
results.append(record)
|
| 3225 |
+
|
| 3226 |
return {
|
| 3227 |
"success": True,
|
| 3228 |
+
"data": results,
|
| 3229 |
+
"news": results,
|
| 3230 |
"count": len(results),
|
| 3231 |
+
"status": "available" if results else "empty",
|
| 3232 |
+
"errors": [],
|
| 3233 |
+
"timestamp": datetime.now().isoformat(),
|
| 3234 |
}
|
|
|
|
| 3235 |
except Exception as e:
|
| 3236 |
+
logger.warning(f"Latest news fetch failed: {e}")
|
| 3237 |
+
return {
|
| 3238 |
+
"success": True,
|
| 3239 |
+
"data": [],
|
| 3240 |
+
"news": [],
|
| 3241 |
+
"count": 0,
|
| 3242 |
+
"status": "empty",
|
| 3243 |
+
"errors": [str(e)],
|
| 3244 |
+
"timestamp": datetime.now().isoformat(),
|
| 3245 |
+
}
|
| 3246 |
|
| 3247 |
@app.post("/api/news/summarize")
|
| 3248 |
async def summarize_news(request: Dict[str, Any]):
|
|
|
|
| 4052 |
except Exception as compat_error:
|
| 4053 |
logger.warning(f"Compat routes not loaded: {compat_error}")
|
| 4054 |
|
| 4055 |
+
# ===== Short Hunter datasource gateway routes (public-data, multi-provider rotation) =====
|
|
|
|
|
|
|
|
|
|
| 4056 |
try:
|
| 4057 |
+
from short_hunter_routes import register_short_hunter_routes
|
| 4058 |
|
| 4059 |
+
register_short_hunter_routes(app)
|
| 4060 |
+
logger.info("Short Hunter datasource gateway routes loaded")
|
| 4061 |
+
except Exception as short_hunter_error:
|
| 4062 |
+
logger.warning(f"Short Hunter datasource routes not loaded: {short_hunter_error}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4063 |
|
| 4064 |
# ===== Main Entry Point =====
|
| 4065 |
if __name__ == "__main__":
|
backend/routers/hf_connect.py
CHANGED
|
@@ -1,35 +1,35 @@
|
|
| 1 |
-
from __future__ import annotations
|
| 2 |
-
from fastapi import APIRouter, Query, Body
|
| 3 |
-
from typing import Literal, List
|
| 4 |
-
from backend.services.hf_registry import REGISTRY
|
| 5 |
-
from backend.services.hf_client import run_sentiment
|
| 6 |
-
|
| 7 |
-
router = APIRouter(prefix="/api/hf", tags=["huggingface"])
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
@router.get("/health")
|
| 11 |
-
async def hf_health():
|
| 12 |
-
return REGISTRY.health()
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
@router.post("/refresh")
|
| 16 |
-
async def hf_refresh():
|
| 17 |
-
return await REGISTRY.refresh()
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
@router.get("/registry")
|
| 21 |
-
async def hf_registry(kind: Literal["models","datasets"]="models"):
|
| 22 |
-
return {"kind": kind, "items": REGISTRY.list(kind)}
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
@router.get("/search")
|
| 26 |
-
async def hf_search(q: str = Query("crypto"), kind: Literal["models","datasets"]="models"):
|
| 27 |
-
hay = REGISTRY.list(kind)
|
| 28 |
-
ql = q.lower()
|
| 29 |
-
res = [x for x in hay if ql in (x.get("id","").lower() + " " + " ".join([str(t) for t in x.get("tags",[])]).lower())]
|
| 30 |
-
return {"query": q, "kind": kind, "count": len(res), "items": res[:50]}
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
@router.post("/run-sentiment")
|
| 34 |
-
async def
|
| 35 |
-
return run_sentiment(texts, model=model)
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
from fastapi import APIRouter, Query, Body
|
| 3 |
+
from typing import Literal, List
|
| 4 |
+
from backend.services.hf_registry import REGISTRY
|
| 5 |
+
from backend.services.hf_client import run_sentiment
|
| 6 |
+
|
| 7 |
+
router = APIRouter(prefix="/api/hf", tags=["huggingface"])
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
@router.get("/health")
|
| 11 |
+
async def hf_health():
|
| 12 |
+
return REGISTRY.health()
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@router.post("/refresh")
|
| 16 |
+
async def hf_refresh():
|
| 17 |
+
return await REGISTRY.refresh()
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@router.get("/registry")
|
| 21 |
+
async def hf_registry(kind: Literal["models","datasets"]="models"):
|
| 22 |
+
return {"kind": kind, "items": REGISTRY.list(kind)}
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
@router.get("/search")
|
| 26 |
+
async def hf_search(q: str = Query("crypto"), kind: Literal["models","datasets"]="models"):
|
| 27 |
+
hay = REGISTRY.list(kind)
|
| 28 |
+
ql = q.lower()
|
| 29 |
+
res = [x for x in hay if ql in (x.get("id","").lower() + " " + " ".join([str(t) for t in x.get("tags",[])]).lower())]
|
| 30 |
+
return {"query": q, "kind": kind, "count": len(res), "items": res[:50]}
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
@router.post("/run-sentiment")
|
| 34 |
+
async def hf_batch(texts: List[str] = Body(..., embed=True), model: str | None = Body(default=None)):
|
| 35 |
+
return run_sentiment(texts, model=model)
|
config.js
CHANGED
|
@@ -1,389 +1,389 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* ═══════════════════════════════════════════════════════════════════
|
| 3 |
-
* CONFIGURATION FILE
|
| 4 |
-
* Dashboard Settings - Easy Customization
|
| 5 |
-
* ═══════════════════════════════════════════════════════════════════
|
| 6 |
-
*/
|
| 7 |
-
|
| 8 |
-
// 🔧 Main Backend Settings
|
| 9 |
-
window.DASHBOARD_CONFIG = {
|
| 10 |
-
|
| 11 |
-
// ═══════════════════════════════════════════════════════════════
|
| 12 |
-
// API and WebSocket URLs
|
| 13 |
-
// ═══════════════════════════════════════════════════════════════
|
| 14 |
-
|
| 15 |
-
// Auto-detect localhost and use port 7860, otherwise use current origin
|
| 16 |
-
BACKEND_URL: (() => {
|
| 17 |
-
const hostname = window.location.hostname;
|
| 18 |
-
if (hostname === 'localhost' || hostname === '127.0.0.1') {
|
| 19 |
-
return `http://${hostname}:7860`;
|
| 20 |
-
}
|
| 21 |
-
return window.location.origin || 'https://really-amin-datasourceforcryptocurrency.hf.space';
|
| 22 |
-
})(),
|
| 23 |
-
WS_URL: (() => {
|
| 24 |
-
const hostname = window.location.hostname;
|
| 25 |
-
let backendUrl;
|
| 26 |
-
if (hostname === 'localhost' || hostname === '127.0.0.1') {
|
| 27 |
-
backendUrl = `http://${hostname}:7860`;
|
| 28 |
-
} else {
|
| 29 |
-
backendUrl = window.location.origin || 'https://really-amin-datasourceforcryptocurrency.hf.space';
|
| 30 |
-
}
|
| 31 |
-
return backendUrl.replace('http://', 'ws://').replace('https://', 'wss://') + '/ws';
|
| 32 |
-
})(),
|
| 33 |
-
|
| 34 |
-
// ⏱️ Update Timing (milliseconds)
|
| 35 |
-
UPDATE_INTERVAL: 30000, // Every 30 seconds
|
| 36 |
-
CACHE_TTL: 60000, // 1 minute
|
| 37 |
-
HEARTBEAT_INTERVAL: 30000, // 30 seconds
|
| 38 |
-
|
| 39 |
-
// 🔄 Reconnection Settings
|
| 40 |
-
MAX_RECONNECT_ATTEMPTS: 5,
|
| 41 |
-
RECONNECT_DELAY: 3000, // 3 seconds
|
| 42 |
-
|
| 43 |
-
// ═══════════════════════════════════════════════════════════════
|
| 44 |
-
// Display Settings
|
| 45 |
-
// ═══════════════════════════════════════════════════════════════
|
| 46 |
-
|
| 47 |
-
// Number of items to display
|
| 48 |
-
MAX_COINS_DISPLAY: 20, // Number of coins in table
|
| 49 |
-
MAX_NEWS_DISPLAY: 20, // Number of news items
|
| 50 |
-
MAX_TRENDING_DISPLAY: 10, // Number of trending items
|
| 51 |
-
|
| 52 |
-
// Table settings
|
| 53 |
-
TABLE_ROWS_PER_PAGE: 10,
|
| 54 |
-
|
| 55 |
-
// ═══════════════════════════════════════════════════════════════
|
| 56 |
-
// Chart Settings
|
| 57 |
-
// ═══════════════════════════════════════════════════════════════
|
| 58 |
-
|
| 59 |
-
CHART: {
|
| 60 |
-
DEFAULT_SYMBOL: 'BTCUSDT',
|
| 61 |
-
DEFAULT_INTERVAL: '1h',
|
| 62 |
-
AVAILABLE_INTERVALS: ['1m', '5m', '15m', '1h', '4h', '1d'],
|
| 63 |
-
THEME: 'dark',
|
| 64 |
-
},
|
| 65 |
-
|
| 66 |
-
// ═══════════════════════════════════════════════════════════════
|
| 67 |
-
// AI Settings
|
| 68 |
-
// ═══════════════════════════════════════════════════════════════
|
| 69 |
-
|
| 70 |
-
AI: {
|
| 71 |
-
ENABLE_SENTIMENT: true,
|
| 72 |
-
ENABLE_NEWS_SUMMARY: true,
|
| 73 |
-
ENABLE_PRICE_PREDICTION: false, // Currently disabled
|
| 74 |
-
ENABLE_PATTERN_DETECTION: false, // Currently disabled
|
| 75 |
-
},
|
| 76 |
-
|
| 77 |
-
// ═══════════════════════════════════════════════════════════════
|
| 78 |
-
// Notification Settings
|
| 79 |
-
// ═══════════════════════════════════════════════════════════════
|
| 80 |
-
|
| 81 |
-
NOTIFICATIONS: {
|
| 82 |
-
ENABLE: true,
|
| 83 |
-
SHOW_PRICE_ALERTS: true,
|
| 84 |
-
SHOW_NEWS_ALERTS: true,
|
| 85 |
-
AUTO_DISMISS_TIME: 5000, // 5 seconds
|
| 86 |
-
},
|
| 87 |
-
|
| 88 |
-
// ═══════════════════════════════════════════════════════════════
|
| 89 |
-
// UI Settings
|
| 90 |
-
// ═══════════════════════════════════════════
|
| 91 |
-
|
| 92 |
-
UI: {
|
| 93 |
-
DEFAULT_THEME: 'dark', // 'dark' or 'light'
|
| 94 |
-
ENABLE_ANIMATIONS: true,
|
| 95 |
-
ENABLE_SOUNDS: false,
|
| 96 |
-
LANGUAGE: 'en', // 'en' or 'fa'
|
| 97 |
-
RTL: false,
|
| 98 |
-
},
|
| 99 |
-
|
| 100 |
-
// ═══════════════════════════════════════════════════════════════
|
| 101 |
-
// Debug Settings
|
| 102 |
-
// ═══════════════════════════════════════════════════════════════
|
| 103 |
-
|
| 104 |
-
DEBUG: {
|
| 105 |
-
ENABLE_CONSOLE_LOGS: true,
|
| 106 |
-
ENABLE_PERFORMANCE_MONITORING: true,
|
| 107 |
-
SHOW_API_REQUESTS: true,
|
| 108 |
-
SHOW_WS_MESSAGES: false,
|
| 109 |
-
},
|
| 110 |
-
|
| 111 |
-
// ═══════════════════════════════════════════════════════════════
|
| 112 |
-
// Default Filters and Sorting
|
| 113 |
-
// ═══════════════════════════════════════════════════════════════
|
| 114 |
-
|
| 115 |
-
FILTERS: {
|
| 116 |
-
DEFAULT_MARKET_FILTER: 'all', // 'all', 'gainers', 'losers', 'trending'
|
| 117 |
-
DEFAULT_NEWS_FILTER: 'all', // 'all', 'bitcoin', 'ethereum', 'defi', 'nft'
|
| 118 |
-
DEFAULT_SORT: 'market_cap', // 'market_cap', 'volume', 'price', 'change'
|
| 119 |
-
SORT_ORDER: 'desc', // 'asc' or 'desc'
|
| 120 |
-
},
|
| 121 |
-
|
| 122 |
-
// ═══════════════════════════════════════════════════════════════
|
| 123 |
-
// HuggingFace Configuration
|
| 124 |
-
// ═══════════════════════════════════════════════════════════════
|
| 125 |
-
|
| 126 |
-
HF_TOKEN: 'HF_TOKEN_FROM_SPACE_SECRET',
|
| 127 |
-
HF_API_BASE: 'https://api-inference.huggingface.co/models',
|
| 128 |
-
|
| 129 |
-
// ═══════════════════════════════════════════════════════════════
|
| 130 |
-
// API Endpoints (Optional - if your backend differs)
|
| 131 |
-
// ═══════════════════════════════════════════════════════════════
|
| 132 |
-
|
| 133 |
-
ENDPOINTS: {
|
| 134 |
-
HEALTH: '/api/health',
|
| 135 |
-
MARKET: '/api/market/stats',
|
| 136 |
-
MARKET_PRICES: '/api/market/prices',
|
| 137 |
-
COINS_TOP: '/api/coins/top',
|
| 138 |
-
COIN_DETAILS: '/api/coins',
|
| 139 |
-
TRENDING: '/api/trending',
|
| 140 |
-
SENTIMENT: '/api/sentiment',
|
| 141 |
-
SENTIMENT_ANALYZE: '/api/sentiment/analyze',
|
| 142 |
-
NEWS: '/api/news/latest',
|
| 143 |
-
NEWS_SUMMARIZE: '/api/news/summarize',
|
| 144 |
-
STATS: '/api/stats',
|
| 145 |
-
PROVIDERS: '/api/providers',
|
| 146 |
-
PROVIDER_STATUS: '/api/providers/status',
|
| 147 |
-
CHART_HISTORY: '/api/charts/price',
|
| 148 |
-
CHART_ANALYZE: '/api/charts/analyze',
|
| 149 |
-
OHLCV: '/api/ohlcv',
|
| 150 |
-
QUERY: '/api/query',
|
| 151 |
-
DATASETS: '/api/datasets/list',
|
| 152 |
-
MODELS: '/api/models/list',
|
| 153 |
-
HF_HEALTH: '/api/hf/health',
|
| 154 |
-
HF_REGISTRY: '/api/hf/registry',
|
| 155 |
-
SYSTEM_STATUS: '/api/system/status',
|
| 156 |
-
SYSTEM_CONFIG: '/api/system/config',
|
| 157 |
-
CATEGORIES: '/api/categories',
|
| 158 |
-
RATE_LIMITS: '/api/rate-limits',
|
| 159 |
-
LOGS: '/api/logs',
|
| 160 |
-
ALERTS: '/api/alerts',
|
| 161 |
-
},
|
| 162 |
-
|
| 163 |
-
// ═══════════════════════════════════════════════════════════════
|
| 164 |
-
// WebSocket Events
|
| 165 |
-
// ═══════════════════════════════════════════════════════════════
|
| 166 |
-
|
| 167 |
-
WS_EVENTS: {
|
| 168 |
-
MARKET_UPDATE: 'market_update',
|
| 169 |
-
SENTIMENT_UPDATE: 'sentiment_update',
|
| 170 |
-
NEWS_UPDATE: 'news_update',
|
| 171 |
-
STATS_UPDATE: 'stats_update',
|
| 172 |
-
PRICE_UPDATE: 'price_update',
|
| 173 |
-
API_UPDATE: 'api_update',
|
| 174 |
-
STATUS_UPDATE: 'status_update',
|
| 175 |
-
SCHEDULE_UPDATE: 'schedule_update',
|
| 176 |
-
CONNECTED: 'connected',
|
| 177 |
-
DISCONNECTED: 'disconnected',
|
| 178 |
-
},
|
| 179 |
-
|
| 180 |
-
// ═══════════════════════════════════════════════════════════════
|
| 181 |
-
// Display Formats
|
| 182 |
-
// ═══════════════════════════════════════════════════════════════
|
| 183 |
-
|
| 184 |
-
FORMATS: {
|
| 185 |
-
CURRENCY: {
|
| 186 |
-
LOCALE: 'en-US',
|
| 187 |
-
STYLE: 'currency',
|
| 188 |
-
CURRENCY: 'USD',
|
| 189 |
-
},
|
| 190 |
-
DATE: {
|
| 191 |
-
LOCALE: 'en-US',
|
| 192 |
-
OPTIONS: {
|
| 193 |
-
year: 'numeric',
|
| 194 |
-
month: 'long',
|
| 195 |
-
day: 'numeric',
|
| 196 |
-
hour: '2-digit',
|
| 197 |
-
minute: '2-digit',
|
| 198 |
-
},
|
| 199 |
-
},
|
| 200 |
-
},
|
| 201 |
-
|
| 202 |
-
// ═══════════════════════════════════════════════════════════════
|
| 203 |
-
// Rate Limiting
|
| 204 |
-
// ═══════════════════════════════════════════════════════════════
|
| 205 |
-
|
| 206 |
-
RATE_LIMITS: {
|
| 207 |
-
API_REQUESTS_PER_MINUTE: 60,
|
| 208 |
-
SEARCH_DEBOUNCE_MS: 300,
|
| 209 |
-
},
|
| 210 |
-
|
| 211 |
-
// ═══════════════════════════════════════════════════════════════
|
| 212 |
-
// Storage Settings
|
| 213 |
-
// ═══════════════════════════════════════════════════════════════
|
| 214 |
-
|
| 215 |
-
STORAGE: {
|
| 216 |
-
USE_LOCAL_STORAGE: true,
|
| 217 |
-
SAVE_PREFERENCES: true,
|
| 218 |
-
STORAGE_PREFIX: 'hts_dashboard_',
|
| 219 |
-
},
|
| 220 |
-
};
|
| 221 |
-
|
| 222 |
-
// ═══════════════════════════════════════════════════════════════════
|
| 223 |
-
// Predefined Profiles
|
| 224 |
-
// ═══════════════════════════════════════════════════════════════════
|
| 225 |
-
|
| 226 |
-
window.DASHBOARD_PROFILES = {
|
| 227 |
-
|
| 228 |
-
// High Performance Profile
|
| 229 |
-
HIGH_PERFORMANCE: {
|
| 230 |
-
UPDATE_INTERVAL: 15000, // Faster updates
|
| 231 |
-
CACHE_TTL: 30000, // Shorter cache
|
| 232 |
-
ENABLE_ANIMATIONS: false, // No animations
|
| 233 |
-
MAX_COINS_DISPLAY: 50,
|
| 234 |
-
},
|
| 235 |
-
|
| 236 |
-
// Data Saver Profile
|
| 237 |
-
DATA_SAVER: {
|
| 238 |
-
UPDATE_INTERVAL: 60000, // Less frequent updates
|
| 239 |
-
CACHE_TTL: 300000, // Longer cache (5 minutes)
|
| 240 |
-
MAX_COINS_DISPLAY: 10,
|
| 241 |
-
MAX_NEWS_DISPLAY: 10,
|
| 242 |
-
},
|
| 243 |
-
|
| 244 |
-
// Presentation Profile
|
| 245 |
-
PRESENTATION: {
|
| 246 |
-
ENABLE_ANIMATIONS: true,
|
| 247 |
-
UPDATE_INTERVAL: 20000,
|
| 248 |
-
SHOW_API_REQUESTS: false,
|
| 249 |
-
ENABLE_CONSOLE_LOGS: false,
|
| 250 |
-
},
|
| 251 |
-
|
| 252 |
-
// Development Profile
|
| 253 |
-
DEVELOPMENT: {
|
| 254 |
-
DEBUG: {
|
| 255 |
-
ENABLE_CONSOLE_LOGS: true,
|
| 256 |
-
ENABLE_PERFORMANCE_MONITORING: true,
|
| 257 |
-
SHOW_API_REQUESTS: true,
|
| 258 |
-
SHOW_WS_MESSAGES: true,
|
| 259 |
-
},
|
| 260 |
-
UPDATE_INTERVAL: 10000,
|
| 261 |
-
},
|
| 262 |
-
};
|
| 263 |
-
|
| 264 |
-
// ═══════════════════════════════════════════════════════════════════
|
| 265 |
-
// Helper Function to Change Profile
|
| 266 |
-
// ═══════════════════════════════════════════════════════════════════
|
| 267 |
-
|
| 268 |
-
window.applyDashboardProfile = function (profileName) {
|
| 269 |
-
if (window.DASHBOARD_PROFILES[profileName]) {
|
| 270 |
-
const profile = window.DASHBOARD_PROFILES[profileName];
|
| 271 |
-
Object.assign(window.DASHBOARD_CONFIG, profile);
|
| 272 |
-
console.log(`✅ Profile "${profileName}" applied`);
|
| 273 |
-
|
| 274 |
-
// Reload application with new settings
|
| 275 |
-
if (window.app) {
|
| 276 |
-
window.app.destroy();
|
| 277 |
-
window.app = new DashboardApp();
|
| 278 |
-
window.app.init();
|
| 279 |
-
}
|
| 280 |
-
} else {
|
| 281 |
-
console.error(`❌ Profile "${profileName}" not found`);
|
| 282 |
-
}
|
| 283 |
-
};
|
| 284 |
-
|
| 285 |
-
// ═══════════════════════════════════════════════════════════════════
|
| 286 |
-
// Helper Function to Change Backend URL
|
| 287 |
-
// ═══════════════════════════════════════════════════════════════════
|
| 288 |
-
|
| 289 |
-
window.changeBackendURL = function (httpUrl, wsUrl) {
|
| 290 |
-
window.DASHBOARD_CONFIG.BACKEND_URL = httpUrl;
|
| 291 |
-
window.DASHBOARD_CONFIG.WS_URL = wsUrl || httpUrl.replace('https://', 'wss://').replace('http://', 'ws://') + '/ws';
|
| 292 |
-
|
| 293 |
-
console.log('✅ Backend URL changed:');
|
| 294 |
-
console.log(' HTTP:', window.DASHBOARD_CONFIG.BACKEND_URL);
|
| 295 |
-
console.log(' WS:', window.DASHBOARD_CONFIG.WS_URL);
|
| 296 |
-
|
| 297 |
-
// Reload application
|
| 298 |
-
if (window.app) {
|
| 299 |
-
window.app.destroy();
|
| 300 |
-
window.app = new DashboardApp();
|
| 301 |
-
window.app.init();
|
| 302 |
-
}
|
| 303 |
-
};
|
| 304 |
-
|
| 305 |
-
// ═══════════════════════════════════════════════════════════════════
|
| 306 |
-
// Save Settings to LocalStorage
|
| 307 |
-
// ═══════════════════════════════════════════════════════════════════
|
| 308 |
-
|
| 309 |
-
window.saveConfig = function () {
|
| 310 |
-
if (window.DASHBOARD_CONFIG.STORAGE.USE_LOCAL_STORAGE) {
|
| 311 |
-
try {
|
| 312 |
-
const configString = JSON.stringify(window.DASHBOARD_CONFIG);
|
| 313 |
-
localStorage.setItem(
|
| 314 |
-
window.DASHBOARD_CONFIG.STORAGE.STORAGE_PREFIX + 'config',
|
| 315 |
-
configString
|
| 316 |
-
);
|
| 317 |
-
console.log('
|
| 318 |
-
} catch (error) {
|
| 319 |
-
console.error('❌ Error saving settings:', error);
|
| 320 |
-
}
|
| 321 |
-
}
|
| 322 |
-
};
|
| 323 |
-
|
| 324 |
-
// ═══════════════════════════════════════════════════════════════════
|
| 325 |
-
// Load Settings from LocalStorage
|
| 326 |
-
// ═══════════════════════════════════════════════════════════════════
|
| 327 |
-
|
| 328 |
-
window.loadConfig = function () {
|
| 329 |
-
if (window.DASHBOARD_CONFIG.STORAGE.USE_LOCAL_STORAGE) {
|
| 330 |
-
try {
|
| 331 |
-
const configString = localStorage.getItem(
|
| 332 |
-
window.DASHBOARD_CONFIG.STORAGE.STORAGE_PREFIX + 'config'
|
| 333 |
-
);
|
| 334 |
-
if (configString) {
|
| 335 |
-
const savedConfig = JSON.parse(configString);
|
| 336 |
-
Object.assign(window.DASHBOARD_CONFIG, savedConfig);
|
| 337 |
-
console.log('✅ Settings loaded');
|
| 338 |
-
}
|
| 339 |
-
} catch (error) {
|
| 340 |
-
console.error('❌ Error loading settings:', error);
|
| 341 |
-
}
|
| 342 |
-
}
|
| 343 |
-
};
|
| 344 |
-
|
| 345 |
-
// ═══════════════════════════════════════════════════════════════════
|
| 346 |
-
// Auto-load Settings on Page Load
|
| 347 |
-
// ═══════════════════════════════════════════════════════════════════
|
| 348 |
-
|
| 349 |
-
if (document.readyState === 'loading') {
|
| 350 |
-
document.addEventListener('DOMContentLoaded', () => {
|
| 351 |
-
window.loadConfig();
|
| 352 |
-
});
|
| 353 |
-
} else {
|
| 354 |
-
window.loadConfig();
|
| 355 |
-
}
|
| 356 |
-
|
| 357 |
-
// ═══════════════════════════════════════════════════════════════════
|
| 358 |
-
// Console Usage Guide
|
| 359 |
-
// ═══════════════════════════════════════════════════════════════════
|
| 360 |
-
|
| 361 |
-
console.log(`
|
| 362 |
-
╔═══════════════════════════════════════════════════════════════╗
|
| 363 |
-
║ HTS CRYPTO DASHBOARD - CONFIGURATION ║
|
| 364 |
-
╚═══════════════════════════════════════════════════════════════╝
|
| 365 |
-
|
| 366 |
-
📋 Available Commands:
|
| 367 |
-
|
| 368 |
-
1. Change Profile:
|
| 369 |
-
applyDashboardProfile('HIGH_PERFORMANCE')
|
| 370 |
-
applyDashboardProfile('DATA_SAVER')
|
| 371 |
-
applyDashboardProfile('PRESENTATION')
|
| 372 |
-
applyDashboardProfile('DEVELOPMENT')
|
| 373 |
-
|
| 374 |
-
2. Change Backend:
|
| 375 |
-
changeBackendURL('https://your-backend.com')
|
| 376 |
-
|
| 377 |
-
3. Save/Load Settings:
|
| 378 |
-
saveConfig()
|
| 379 |
-
loadConfig()
|
| 380 |
-
|
| 381 |
-
4. View Current Settings:
|
| 382 |
-
console.log(DASHBOARD_CONFIG)
|
| 383 |
-
|
| 384 |
-
5. Manual Settings Change:
|
| 385 |
-
DASHBOARD_CONFIG.UPDATE_INTERVAL = 20000
|
| 386 |
-
saveConfig()
|
| 387 |
-
|
| 388 |
-
═══════════════════════════════════════════════════════════════════
|
| 389 |
-
`);
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* ═══════════════════════════════════════════════════════════════════
|
| 3 |
+
* CONFIGURATION FILE
|
| 4 |
+
* Dashboard Settings - Easy Customization
|
| 5 |
+
* ═══════════════════════════════════════════════════════════════════
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
// 🔧 Main Backend Settings
|
| 9 |
+
window.DASHBOARD_CONFIG = {
|
| 10 |
+
|
| 11 |
+
// ═══════════════════════════════════════════════════════════════
|
| 12 |
+
// API and WebSocket URLs
|
| 13 |
+
// ═══════════════════════════════════════════════════════════════
|
| 14 |
+
|
| 15 |
+
// Auto-detect localhost and use port 7860, otherwise use current origin
|
| 16 |
+
BACKEND_URL: (() => {
|
| 17 |
+
const hostname = window.location.hostname;
|
| 18 |
+
if (hostname === 'localhost' || hostname === '127.0.0.1') {
|
| 19 |
+
return `http://${hostname}:7860`;
|
| 20 |
+
}
|
| 21 |
+
return window.location.origin || 'https://really-amin-datasourceforcryptocurrency.hf.space';
|
| 22 |
+
})(),
|
| 23 |
+
WS_URL: (() => {
|
| 24 |
+
const hostname = window.location.hostname;
|
| 25 |
+
let backendUrl;
|
| 26 |
+
if (hostname === 'localhost' || hostname === '127.0.0.1') {
|
| 27 |
+
backendUrl = `http://${hostname}:7860`;
|
| 28 |
+
} else {
|
| 29 |
+
backendUrl = window.location.origin || 'https://really-amin-datasourceforcryptocurrency.hf.space';
|
| 30 |
+
}
|
| 31 |
+
return backendUrl.replace('http://', 'ws://').replace('https://', 'wss://') + '/ws';
|
| 32 |
+
})(),
|
| 33 |
+
|
| 34 |
+
// ⏱️ Update Timing (milliseconds)
|
| 35 |
+
UPDATE_INTERVAL: 30000, // Every 30 seconds
|
| 36 |
+
CACHE_TTL: 60000, // 1 minute
|
| 37 |
+
HEARTBEAT_INTERVAL: 30000, // 30 seconds
|
| 38 |
+
|
| 39 |
+
// 🔄 Reconnection Settings
|
| 40 |
+
MAX_RECONNECT_ATTEMPTS: 5,
|
| 41 |
+
RECONNECT_DELAY: 3000, // 3 seconds
|
| 42 |
+
|
| 43 |
+
// ═══════════════════════════════════════════════════════════════
|
| 44 |
+
// Display Settings
|
| 45 |
+
// ═══════════════════════════════════════════════════════════════
|
| 46 |
+
|
| 47 |
+
// Number of items to display
|
| 48 |
+
MAX_COINS_DISPLAY: 20, // Number of coins in table
|
| 49 |
+
MAX_NEWS_DISPLAY: 20, // Number of news items
|
| 50 |
+
MAX_TRENDING_DISPLAY: 10, // Number of trending items
|
| 51 |
+
|
| 52 |
+
// Table settings
|
| 53 |
+
TABLE_ROWS_PER_PAGE: 10,
|
| 54 |
+
|
| 55 |
+
// ═══════════════════════════════════════════════════════════════
|
| 56 |
+
// Chart Settings
|
| 57 |
+
// ═══════════════════════════════════════════════════════════════
|
| 58 |
+
|
| 59 |
+
CHART: {
|
| 60 |
+
DEFAULT_SYMBOL: 'BTCUSDT',
|
| 61 |
+
DEFAULT_INTERVAL: '1h',
|
| 62 |
+
AVAILABLE_INTERVALS: ['1m', '5m', '15m', '1h', '4h', '1d'],
|
| 63 |
+
THEME: 'dark',
|
| 64 |
+
},
|
| 65 |
+
|
| 66 |
+
// ═══════════════════════════════════════════════════════════════
|
| 67 |
+
// AI Settings
|
| 68 |
+
// ═══════════════════════════════════════════════════════════════
|
| 69 |
+
|
| 70 |
+
AI: {
|
| 71 |
+
ENABLE_SENTIMENT: true,
|
| 72 |
+
ENABLE_NEWS_SUMMARY: true,
|
| 73 |
+
ENABLE_PRICE_PREDICTION: false, // Currently disabled
|
| 74 |
+
ENABLE_PATTERN_DETECTION: false, // Currently disabled
|
| 75 |
+
},
|
| 76 |
+
|
| 77 |
+
// ═══════════════════════════════════════════════════════════════
|
| 78 |
+
// Notification Settings
|
| 79 |
+
// ═══════════════════════════════════════════════════════════════
|
| 80 |
+
|
| 81 |
+
NOTIFICATIONS: {
|
| 82 |
+
ENABLE: true,
|
| 83 |
+
SHOW_PRICE_ALERTS: true,
|
| 84 |
+
SHOW_NEWS_ALERTS: true,
|
| 85 |
+
AUTO_DISMISS_TIME: 5000, // 5 seconds
|
| 86 |
+
},
|
| 87 |
+
|
| 88 |
+
// ═══════════════════════════════════════════════════════════════
|
| 89 |
+
// UI Settings
|
| 90 |
+
// ═══════════════════════════════════════════════════════════════
|
| 91 |
+
|
| 92 |
+
UI: {
|
| 93 |
+
DEFAULT_THEME: 'dark', // 'dark' or 'light'
|
| 94 |
+
ENABLE_ANIMATIONS: true,
|
| 95 |
+
ENABLE_SOUNDS: false,
|
| 96 |
+
LANGUAGE: 'en', // 'en' or 'fa'
|
| 97 |
+
RTL: false,
|
| 98 |
+
},
|
| 99 |
+
|
| 100 |
+
// ═══════════════════════════════════════════════════════════════
|
| 101 |
+
// Debug Settings
|
| 102 |
+
// ═══════════════════════════════════════════════════════════════
|
| 103 |
+
|
| 104 |
+
DEBUG: {
|
| 105 |
+
ENABLE_CONSOLE_LOGS: true,
|
| 106 |
+
ENABLE_PERFORMANCE_MONITORING: true,
|
| 107 |
+
SHOW_API_REQUESTS: true,
|
| 108 |
+
SHOW_WS_MESSAGES: false,
|
| 109 |
+
},
|
| 110 |
+
|
| 111 |
+
// ═══════════════════════════════════════════════════════════════
|
| 112 |
+
// Default Filters and Sorting
|
| 113 |
+
// ═══════════════════════════════════════════════════════════════
|
| 114 |
+
|
| 115 |
+
FILTERS: {
|
| 116 |
+
DEFAULT_MARKET_FILTER: 'all', // 'all', 'gainers', 'losers', 'trending'
|
| 117 |
+
DEFAULT_NEWS_FILTER: 'all', // 'all', 'bitcoin', 'ethereum', 'defi', 'nft'
|
| 118 |
+
DEFAULT_SORT: 'market_cap', // 'market_cap', 'volume', 'price', 'change'
|
| 119 |
+
SORT_ORDER: 'desc', // 'asc' or 'desc'
|
| 120 |
+
},
|
| 121 |
+
|
| 122 |
+
// ═══════════════════════════════════════════════════════════════
|
| 123 |
+
// HuggingFace Configuration
|
| 124 |
+
// ═══════════════════════════════════════════════════════════════
|
| 125 |
+
|
| 126 |
+
HF_TOKEN: '<HF_TOKEN_FROM_SPACE_SECRET>',
|
| 127 |
+
HF_API_BASE: 'https://api-inference.huggingface.co/models',
|
| 128 |
+
|
| 129 |
+
// ═══════════════════════════════════════════════════════════════
|
| 130 |
+
// API Endpoints (Optional - if your backend differs)
|
| 131 |
+
// ═══════════════════════════════════════════════════════════════
|
| 132 |
+
|
| 133 |
+
ENDPOINTS: {
|
| 134 |
+
HEALTH: '/api/health',
|
| 135 |
+
MARKET: '/api/market/stats',
|
| 136 |
+
MARKET_PRICES: '/api/market/prices',
|
| 137 |
+
COINS_TOP: '/api/coins/top',
|
| 138 |
+
COIN_DETAILS: '/api/coins',
|
| 139 |
+
TRENDING: '/api/trending',
|
| 140 |
+
SENTIMENT: '/api/sentiment',
|
| 141 |
+
SENTIMENT_ANALYZE: '/api/sentiment/analyze',
|
| 142 |
+
NEWS: '/api/news/latest',
|
| 143 |
+
NEWS_SUMMARIZE: '/api/news/summarize',
|
| 144 |
+
STATS: '/api/stats',
|
| 145 |
+
PROVIDERS: '/api/providers',
|
| 146 |
+
PROVIDER_STATUS: '/api/providers/status',
|
| 147 |
+
CHART_HISTORY: '/api/charts/price',
|
| 148 |
+
CHART_ANALYZE: '/api/charts/analyze',
|
| 149 |
+
OHLCV: '/api/ohlcv',
|
| 150 |
+
QUERY: '/api/query',
|
| 151 |
+
DATASETS: '/api/datasets/list',
|
| 152 |
+
MODELS: '/api/models/list',
|
| 153 |
+
HF_HEALTH: '/api/hf/health',
|
| 154 |
+
HF_REGISTRY: '/api/hf/registry',
|
| 155 |
+
SYSTEM_STATUS: '/api/system/status',
|
| 156 |
+
SYSTEM_CONFIG: '/api/system/config',
|
| 157 |
+
CATEGORIES: '/api/categories',
|
| 158 |
+
RATE_LIMITS: '/api/rate-limits',
|
| 159 |
+
LOGS: '/api/logs',
|
| 160 |
+
ALERTS: '/api/alerts',
|
| 161 |
+
},
|
| 162 |
+
|
| 163 |
+
// ═══════════════════════════════════════════════════════════════
|
| 164 |
+
// WebSocket Events
|
| 165 |
+
// ═══════════════════════════════════════════════════════════════
|
| 166 |
+
|
| 167 |
+
WS_EVENTS: {
|
| 168 |
+
MARKET_UPDATE: 'market_update',
|
| 169 |
+
SENTIMENT_UPDATE: 'sentiment_update',
|
| 170 |
+
NEWS_UPDATE: 'news_update',
|
| 171 |
+
STATS_UPDATE: 'stats_update',
|
| 172 |
+
PRICE_UPDATE: 'price_update',
|
| 173 |
+
API_UPDATE: 'api_update',
|
| 174 |
+
STATUS_UPDATE: 'status_update',
|
| 175 |
+
SCHEDULE_UPDATE: 'schedule_update',
|
| 176 |
+
CONNECTED: 'connected',
|
| 177 |
+
DISCONNECTED: 'disconnected',
|
| 178 |
+
},
|
| 179 |
+
|
| 180 |
+
// ═══════════════════════════════════════════════════════════════
|
| 181 |
+
// Display Formats
|
| 182 |
+
// ═══════════════════════════════════════════════════════════════
|
| 183 |
+
|
| 184 |
+
FORMATS: {
|
| 185 |
+
CURRENCY: {
|
| 186 |
+
LOCALE: 'en-US',
|
| 187 |
+
STYLE: 'currency',
|
| 188 |
+
CURRENCY: 'USD',
|
| 189 |
+
},
|
| 190 |
+
DATE: {
|
| 191 |
+
LOCALE: 'en-US',
|
| 192 |
+
OPTIONS: {
|
| 193 |
+
year: 'numeric',
|
| 194 |
+
month: 'long',
|
| 195 |
+
day: 'numeric',
|
| 196 |
+
hour: '2-digit',
|
| 197 |
+
minute: '2-digit',
|
| 198 |
+
},
|
| 199 |
+
},
|
| 200 |
+
},
|
| 201 |
+
|
| 202 |
+
// ═══════════════════════════════════════════════════════════════
|
| 203 |
+
// Rate Limiting
|
| 204 |
+
// ═══════════════════════════════════════════════════════════════
|
| 205 |
+
|
| 206 |
+
RATE_LIMITS: {
|
| 207 |
+
API_REQUESTS_PER_MINUTE: 60,
|
| 208 |
+
SEARCH_DEBOUNCE_MS: 300,
|
| 209 |
+
},
|
| 210 |
+
|
| 211 |
+
// ═══════════════════════════════════════════════════════════════
|
| 212 |
+
// Storage Settings
|
| 213 |
+
// ═══════════════════════════════════════════════════════════════
|
| 214 |
+
|
| 215 |
+
STORAGE: {
|
| 216 |
+
USE_LOCAL_STORAGE: true,
|
| 217 |
+
SAVE_PREFERENCES: true,
|
| 218 |
+
STORAGE_PREFIX: 'hts_dashboard_',
|
| 219 |
+
},
|
| 220 |
+
};
|
| 221 |
+
|
| 222 |
+
// ═══════════════════════════════════════════════════════════════════
|
| 223 |
+
// Predefined Profiles
|
| 224 |
+
// ═══════════════════════════════════════════════════════════════════
|
| 225 |
+
|
| 226 |
+
window.DASHBOARD_PROFILES = {
|
| 227 |
+
|
| 228 |
+
// High Performance Profile
|
| 229 |
+
HIGH_PERFORMANCE: {
|
| 230 |
+
UPDATE_INTERVAL: 15000, // Faster updates
|
| 231 |
+
CACHE_TTL: 30000, // Shorter cache
|
| 232 |
+
ENABLE_ANIMATIONS: false, // No animations
|
| 233 |
+
MAX_COINS_DISPLAY: 50,
|
| 234 |
+
},
|
| 235 |
+
|
| 236 |
+
// Data Saver Profile
|
| 237 |
+
DATA_SAVER: {
|
| 238 |
+
UPDATE_INTERVAL: 60000, // Less frequent updates
|
| 239 |
+
CACHE_TTL: 300000, // Longer cache (5 minutes)
|
| 240 |
+
MAX_COINS_DISPLAY: 10,
|
| 241 |
+
MAX_NEWS_DISPLAY: 10,
|
| 242 |
+
},
|
| 243 |
+
|
| 244 |
+
// Presentation Profile
|
| 245 |
+
PRESENTATION: {
|
| 246 |
+
ENABLE_ANIMATIONS: true,
|
| 247 |
+
UPDATE_INTERVAL: 20000,
|
| 248 |
+
SHOW_API_REQUESTS: false,
|
| 249 |
+
ENABLE_CONSOLE_LOGS: false,
|
| 250 |
+
},
|
| 251 |
+
|
| 252 |
+
// Development Profile
|
| 253 |
+
DEVELOPMENT: {
|
| 254 |
+
DEBUG: {
|
| 255 |
+
ENABLE_CONSOLE_LOGS: true,
|
| 256 |
+
ENABLE_PERFORMANCE_MONITORING: true,
|
| 257 |
+
SHOW_API_REQUESTS: true,
|
| 258 |
+
SHOW_WS_MESSAGES: true,
|
| 259 |
+
},
|
| 260 |
+
UPDATE_INTERVAL: 10000,
|
| 261 |
+
},
|
| 262 |
+
};
|
| 263 |
+
|
| 264 |
+
// ═══════════════════════════════════════════════════════════════════
|
| 265 |
+
// Helper Function to Change Profile
|
| 266 |
+
// ═══════════════════════════════════════════════════════════════════
|
| 267 |
+
|
| 268 |
+
window.applyDashboardProfile = function (profileName) {
|
| 269 |
+
if (window.DASHBOARD_PROFILES[profileName]) {
|
| 270 |
+
const profile = window.DASHBOARD_PROFILES[profileName];
|
| 271 |
+
Object.assign(window.DASHBOARD_CONFIG, profile);
|
| 272 |
+
console.log(`✅ Profile "${profileName}" applied`);
|
| 273 |
+
|
| 274 |
+
// Reload application with new settings
|
| 275 |
+
if (window.app) {
|
| 276 |
+
window.app.destroy();
|
| 277 |
+
window.app = new DashboardApp();
|
| 278 |
+
window.app.init();
|
| 279 |
+
}
|
| 280 |
+
} else {
|
| 281 |
+
console.error(`❌ Profile "${profileName}" not found`);
|
| 282 |
+
}
|
| 283 |
+
};
|
| 284 |
+
|
| 285 |
+
// ═══════════════════════════════════════════════════════════════════
|
| 286 |
+
// Helper Function to Change Backend URL
|
| 287 |
+
// ═══════════════════════════════════════════════════════════════════
|
| 288 |
+
|
| 289 |
+
window.changeBackendURL = function (httpUrl, wsUrl) {
|
| 290 |
+
window.DASHBOARD_CONFIG.BACKEND_URL = httpUrl;
|
| 291 |
+
window.DASHBOARD_CONFIG.WS_URL = wsUrl || httpUrl.replace('https://', 'wss://').replace('http://', 'ws://') + '/ws';
|
| 292 |
+
|
| 293 |
+
console.log('✅ Backend URL changed:');
|
| 294 |
+
console.log(' HTTP:', window.DASHBOARD_CONFIG.BACKEND_URL);
|
| 295 |
+
console.log(' WS:', window.DASHBOARD_CONFIG.WS_URL);
|
| 296 |
+
|
| 297 |
+
// Reload application
|
| 298 |
+
if (window.app) {
|
| 299 |
+
window.app.destroy();
|
| 300 |
+
window.app = new DashboardApp();
|
| 301 |
+
window.app.init();
|
| 302 |
+
}
|
| 303 |
+
};
|
| 304 |
+
|
| 305 |
+
// ═══════════════════════════════════════════════════════════════════
|
| 306 |
+
// Save Settings to LocalStorage
|
| 307 |
+
// ═══════════════════════════════════════════════════════════════════
|
| 308 |
+
|
| 309 |
+
window.saveConfig = function () {
|
| 310 |
+
if (window.DASHBOARD_CONFIG.STORAGE.USE_LOCAL_STORAGE) {
|
| 311 |
+
try {
|
| 312 |
+
const configString = JSON.stringify(window.DASHBOARD_CONFIG);
|
| 313 |
+
localStorage.setItem(
|
| 314 |
+
window.DASHBOARD_CONFIG.STORAGE.STORAGE_PREFIX + 'config',
|
| 315 |
+
configString
|
| 316 |
+
);
|
| 317 |
+
console.log('�� Settings saved');
|
| 318 |
+
} catch (error) {
|
| 319 |
+
console.error('❌ Error saving settings:', error);
|
| 320 |
+
}
|
| 321 |
+
}
|
| 322 |
+
};
|
| 323 |
+
|
| 324 |
+
// ═══════════════════════════════════════════════════════════════════
|
| 325 |
+
// Load Settings from LocalStorage
|
| 326 |
+
// ═══════════════════════════════════════════════════════════════════
|
| 327 |
+
|
| 328 |
+
window.loadConfig = function () {
|
| 329 |
+
if (window.DASHBOARD_CONFIG.STORAGE.USE_LOCAL_STORAGE) {
|
| 330 |
+
try {
|
| 331 |
+
const configString = localStorage.getItem(
|
| 332 |
+
window.DASHBOARD_CONFIG.STORAGE.STORAGE_PREFIX + 'config'
|
| 333 |
+
);
|
| 334 |
+
if (configString) {
|
| 335 |
+
const savedConfig = JSON.parse(configString);
|
| 336 |
+
Object.assign(window.DASHBOARD_CONFIG, savedConfig);
|
| 337 |
+
console.log('✅ Settings loaded');
|
| 338 |
+
}
|
| 339 |
+
} catch (error) {
|
| 340 |
+
console.error('❌ Error loading settings:', error);
|
| 341 |
+
}
|
| 342 |
+
}
|
| 343 |
+
};
|
| 344 |
+
|
| 345 |
+
// ═══════════════════════════════════════════════════════════════════
|
| 346 |
+
// Auto-load Settings on Page Load
|
| 347 |
+
// ═══════════════════════════════════════════════════════════════════
|
| 348 |
+
|
| 349 |
+
if (document.readyState === 'loading') {
|
| 350 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 351 |
+
window.loadConfig();
|
| 352 |
+
});
|
| 353 |
+
} else {
|
| 354 |
+
window.loadConfig();
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
// ═══════════════════════════════════════════════════════════════════
|
| 358 |
+
// Console Usage Guide
|
| 359 |
+
// ═══════════════════════════════════════════════════════════════════
|
| 360 |
+
|
| 361 |
+
console.log(`
|
| 362 |
+
╔═══════════════════════════════════════════════════════════════╗
|
| 363 |
+
║ HTS CRYPTO DASHBOARD - CONFIGURATION ║
|
| 364 |
+
╚═══════════════════════════════════════════════════════════════╝
|
| 365 |
+
|
| 366 |
+
📋 Available Commands:
|
| 367 |
+
|
| 368 |
+
1. Change Profile:
|
| 369 |
+
applyDashboardProfile('HIGH_PERFORMANCE')
|
| 370 |
+
applyDashboardProfile('DATA_SAVER')
|
| 371 |
+
applyDashboardProfile('PRESENTATION')
|
| 372 |
+
applyDashboardProfile('DEVELOPMENT')
|
| 373 |
+
|
| 374 |
+
2. Change Backend:
|
| 375 |
+
changeBackendURL('https://your-backend.com')
|
| 376 |
+
|
| 377 |
+
3. Save/Load Settings:
|
| 378 |
+
saveConfig()
|
| 379 |
+
loadConfig()
|
| 380 |
+
|
| 381 |
+
4. View Current Settings:
|
| 382 |
+
console.log(DASHBOARD_CONFIG)
|
| 383 |
+
|
| 384 |
+
5. Manual Settings Change:
|
| 385 |
+
DASHBOARD_CONFIG.UPDATE_INTERVAL = 20000
|
| 386 |
+
saveConfig()
|
| 387 |
+
|
| 388 |
+
═══════════════════════════════════════════════════════════════════
|
| 389 |
+
`);
|
crypto_resources_unified_2025-11-11.json
CHANGED
|
@@ -348,7 +348,7 @@
|
|
| 348 |
"base_url": "https://api.etherscan.io/api",
|
| 349 |
"auth": {
|
| 350 |
"type": "apiKeyQuery",
|
| 351 |
-
"key": "
|
| 352 |
"param_name": "apikey"
|
| 353 |
},
|
| 354 |
"docs_url": "https://docs.etherscan.io",
|
|
@@ -368,7 +368,7 @@
|
|
| 368 |
"base_url": "https://api.etherscan.io/api",
|
| 369 |
"auth": {
|
| 370 |
"type": "apiKeyQuery",
|
| 371 |
-
"key": "
|
| 372 |
"param_name": "apikey"
|
| 373 |
},
|
| 374 |
"docs_url": "https://docs.etherscan.io",
|
|
@@ -463,7 +463,7 @@
|
|
| 463 |
"base_url": "https://api.bscscan.com/api",
|
| 464 |
"auth": {
|
| 465 |
"type": "apiKeyQuery",
|
| 466 |
-
"key": "
|
| 467 |
"param_name": "apikey"
|
| 468 |
},
|
| 469 |
"docs_url": "https://docs.bscscan.com",
|
|
@@ -552,7 +552,7 @@
|
|
| 552 |
"base_url": "https://apilist.tronscanapi.com/api",
|
| 553 |
"auth": {
|
| 554 |
"type": "apiKeyQuery",
|
| 555 |
-
"key": "
|
| 556 |
"param_name": "apiKey"
|
| 557 |
},
|
| 558 |
"docs_url": "https://github.com/tronscan/tronscan-frontend/blob/dev2019/document/api.md",
|
|
@@ -650,7 +650,7 @@
|
|
| 650 |
"base_url": "https://pro-api.coinmarketcap.com/v1",
|
| 651 |
"auth": {
|
| 652 |
"type": "apiKeyHeader",
|
| 653 |
-
"key": "
|
| 654 |
"header_name": "X-CMC_PRO_API_KEY"
|
| 655 |
},
|
| 656 |
"docs_url": "https://coinmarketcap.com/api/documentation/v1/",
|
|
@@ -668,7 +668,7 @@
|
|
| 668 |
"base_url": "https://pro-api.coinmarketcap.com/v1",
|
| 669 |
"auth": {
|
| 670 |
"type": "apiKeyHeader",
|
| 671 |
-
"key": "
|
| 672 |
"header_name": "X-CMC_PRO_API_KEY"
|
| 673 |
},
|
| 674 |
"docs_url": "https://coinmarketcap.com/api/documentation/v1/",
|
|
@@ -686,7 +686,7 @@
|
|
| 686 |
"base_url": "https://min-api.cryptocompare.com/data",
|
| 687 |
"auth": {
|
| 688 |
"type": "apiKeyQuery",
|
| 689 |
-
"key": "
|
| 690 |
"param_name": "api_key"
|
| 691 |
},
|
| 692 |
"docs_url": "https://min-api.cryptocompare.com/documentation",
|
|
@@ -883,7 +883,7 @@
|
|
| 883 |
"base_url": "https://min-api.cryptocompare.com",
|
| 884 |
"auth": {
|
| 885 |
"type": "apiKeyQuery",
|
| 886 |
-
"key": "
|
| 887 |
"param_name": "api_key"
|
| 888 |
},
|
| 889 |
"docs_url": null,
|
|
@@ -981,7 +981,7 @@
|
|
| 981 |
"base_url": "https://newsapi.org/v2",
|
| 982 |
"auth": {
|
| 983 |
"type": "apiKeyQuery",
|
| 984 |
-
"key": "
|
| 985 |
"param_name": "apiKey"
|
| 986 |
},
|
| 987 |
"docs_url": "https://newsapi.org/docs",
|
|
@@ -1693,13 +1693,13 @@
|
|
| 1693 |
],
|
| 1694 |
"hf_resources": [
|
| 1695 |
{
|
| 1696 |
-
"id": "HF_TOKEN_FROM_SPACE_SECRET",
|
| 1697 |
"type": "model",
|
| 1698 |
"name": "ElKulako/CryptoBERT",
|
| 1699 |
"base_url": "https://api-inference.huggingface.co/models/ElKulako/cryptobert",
|
| 1700 |
"auth": {
|
| 1701 |
"type": "apiKeyHeaderOptional",
|
| 1702 |
-
"key": "HF_TOKEN_FROM_SPACE_SECRET",
|
| 1703 |
"header_name": "Authorization"
|
| 1704 |
},
|
| 1705 |
"docs_url": "https://huggingface.co/ElKulako/cryptobert",
|
|
@@ -1709,13 +1709,13 @@
|
|
| 1709 |
"notes": "For sentiment analysis"
|
| 1710 |
},
|
| 1711 |
{
|
| 1712 |
-
"id": "HF_TOKEN_FROM_SPACE_SECRET",
|
| 1713 |
"type": "model",
|
| 1714 |
"name": "kk08/CryptoBERT",
|
| 1715 |
"base_url": "https://api-inference.huggingface.co/models/kk08/CryptoBERT",
|
| 1716 |
"auth": {
|
| 1717 |
"type": "apiKeyHeaderOptional",
|
| 1718 |
-
"key": "HF_TOKEN_FROM_SPACE_SECRET",
|
| 1719 |
"header_name": "Authorization"
|
| 1720 |
},
|
| 1721 |
"docs_url": "https://huggingface.co/kk08/CryptoBERT",
|
|
@@ -1725,7 +1725,7 @@
|
|
| 1725 |
"notes": "For sentiment analysis"
|
| 1726 |
},
|
| 1727 |
{
|
| 1728 |
-
"id": "
|
| 1729 |
"type": "dataset",
|
| 1730 |
"name": "linxy/CryptoCoin",
|
| 1731 |
"base_url": "https://huggingface.co/datasets/linxy/CryptoCoin/resolve/main",
|
|
@@ -1739,7 +1739,7 @@
|
|
| 1739 |
"notes": "26 symbols x 7 timeframes = 182 CSVs"
|
| 1740 |
},
|
| 1741 |
{
|
| 1742 |
-
"id": "
|
| 1743 |
"type": "dataset",
|
| 1744 |
"name": "WinkingFace/CryptoLM-Bitcoin-BTC-USDT",
|
| 1745 |
"base_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Bitcoin-BTC-USDT/resolve/main",
|
|
@@ -1754,7 +1754,7 @@
|
|
| 1754 |
"notes": null
|
| 1755 |
},
|
| 1756 |
{
|
| 1757 |
-
"id": "
|
| 1758 |
"type": "dataset",
|
| 1759 |
"name": "WinkingFace/CryptoLM-Ethereum-ETH-USDT",
|
| 1760 |
"base_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Ethereum-ETH-USDT/resolve/main",
|
|
@@ -1769,7 +1769,7 @@
|
|
| 1769 |
"notes": null
|
| 1770 |
},
|
| 1771 |
{
|
| 1772 |
-
"id": "
|
| 1773 |
"type": "dataset",
|
| 1774 |
"name": "WinkingFace/CryptoLM-Solana-SOL-USDT",
|
| 1775 |
"base_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Solana-SOL-USDT/resolve/main",
|
|
@@ -1781,7 +1781,7 @@
|
|
| 1781 |
"notes": null
|
| 1782 |
},
|
| 1783 |
{
|
| 1784 |
-
"id": "
|
| 1785 |
"type": "dataset",
|
| 1786 |
"name": "WinkingFace/CryptoLM-Ripple-XRP-USDT",
|
| 1787 |
"base_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Ripple-XRP-USDT/resolve/main",
|
|
@@ -1861,7 +1861,7 @@
|
|
| 1861 |
"notes": null
|
| 1862 |
},
|
| 1863 |
{
|
| 1864 |
-
"id": "HF_TOKEN_FROM_SPACE_SECRET",
|
| 1865 |
"category": "hf-model",
|
| 1866 |
"name": "HF Model: ElKulako/CryptoBERT",
|
| 1867 |
"base_url": "https://huggingface.co/ElKulako/cryptobert",
|
|
@@ -1872,7 +1872,7 @@
|
|
| 1872 |
"notes": null
|
| 1873 |
},
|
| 1874 |
{
|
| 1875 |
-
"id": "HF_TOKEN_FROM_SPACE_SECRET",
|
| 1876 |
"category": "hf-model",
|
| 1877 |
"name": "HF Model: kk08/CryptoBERT",
|
| 1878 |
"base_url": "https://huggingface.co/kk08/CryptoBERT",
|
|
@@ -1883,7 +1883,7 @@
|
|
| 1883 |
"notes": null
|
| 1884 |
},
|
| 1885 |
{
|
| 1886 |
-
"id": "
|
| 1887 |
"category": "hf-dataset",
|
| 1888 |
"name": "HF Dataset: linxy/CryptoCoin",
|
| 1889 |
"base_url": "https://huggingface.co/datasets/linxy/CryptoCoin",
|
|
@@ -2082,15 +2082,15 @@
|
|
| 2082 |
"source_files": [
|
| 2083 |
{
|
| 2084 |
"path": "/mnt/data/api - Copy.txt",
|
| 2085 |
-
"sha256": "
|
| 2086 |
},
|
| 2087 |
{
|
| 2088 |
"path": "/mnt/data/api-config-complete (1).txt",
|
| 2089 |
-
"sha256": "
|
| 2090 |
},
|
| 2091 |
{
|
| 2092 |
"path": "/mnt/data/crypto_resources_ultimate_2025.zip",
|
| 2093 |
-
"sha256": "
|
| 2094 |
"note": "contains crypto_resources.ts and more"
|
| 2095 |
}
|
| 2096 |
]
|
|
|
|
| 348 |
"base_url": "https://api.etherscan.io/api",
|
| 349 |
"auth": {
|
| 350 |
"type": "apiKeyQuery",
|
| 351 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 352 |
"param_name": "apikey"
|
| 353 |
},
|
| 354 |
"docs_url": "https://docs.etherscan.io",
|
|
|
|
| 368 |
"base_url": "https://api.etherscan.io/api",
|
| 369 |
"auth": {
|
| 370 |
"type": "apiKeyQuery",
|
| 371 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 372 |
"param_name": "apikey"
|
| 373 |
},
|
| 374 |
"docs_url": "https://docs.etherscan.io",
|
|
|
|
| 463 |
"base_url": "https://api.bscscan.com/api",
|
| 464 |
"auth": {
|
| 465 |
"type": "apiKeyQuery",
|
| 466 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 467 |
"param_name": "apikey"
|
| 468 |
},
|
| 469 |
"docs_url": "https://docs.bscscan.com",
|
|
|
|
| 552 |
"base_url": "https://apilist.tronscanapi.com/api",
|
| 553 |
"auth": {
|
| 554 |
"type": "apiKeyQuery",
|
| 555 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 556 |
"param_name": "apiKey"
|
| 557 |
},
|
| 558 |
"docs_url": "https://github.com/tronscan/tronscan-frontend/blob/dev2019/document/api.md",
|
|
|
|
| 650 |
"base_url": "https://pro-api.coinmarketcap.com/v1",
|
| 651 |
"auth": {
|
| 652 |
"type": "apiKeyHeader",
|
| 653 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 654 |
"header_name": "X-CMC_PRO_API_KEY"
|
| 655 |
},
|
| 656 |
"docs_url": "https://coinmarketcap.com/api/documentation/v1/",
|
|
|
|
| 668 |
"base_url": "https://pro-api.coinmarketcap.com/v1",
|
| 669 |
"auth": {
|
| 670 |
"type": "apiKeyHeader",
|
| 671 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 672 |
"header_name": "X-CMC_PRO_API_KEY"
|
| 673 |
},
|
| 674 |
"docs_url": "https://coinmarketcap.com/api/documentation/v1/",
|
|
|
|
| 686 |
"base_url": "https://min-api.cryptocompare.com/data",
|
| 687 |
"auth": {
|
| 688 |
"type": "apiKeyQuery",
|
| 689 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 690 |
"param_name": "api_key"
|
| 691 |
},
|
| 692 |
"docs_url": "https://min-api.cryptocompare.com/documentation",
|
|
|
|
| 883 |
"base_url": "https://min-api.cryptocompare.com",
|
| 884 |
"auth": {
|
| 885 |
"type": "apiKeyQuery",
|
| 886 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 887 |
"param_name": "api_key"
|
| 888 |
},
|
| 889 |
"docs_url": null,
|
|
|
|
| 981 |
"base_url": "https://newsapi.org/v2",
|
| 982 |
"auth": {
|
| 983 |
"type": "apiKeyQuery",
|
| 984 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 985 |
"param_name": "apiKey"
|
| 986 |
},
|
| 987 |
"docs_url": "https://newsapi.org/docs",
|
|
|
|
| 1693 |
],
|
| 1694 |
"hf_resources": [
|
| 1695 |
{
|
| 1696 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1697 |
"type": "model",
|
| 1698 |
"name": "ElKulako/CryptoBERT",
|
| 1699 |
"base_url": "https://api-inference.huggingface.co/models/ElKulako/cryptobert",
|
| 1700 |
"auth": {
|
| 1701 |
"type": "apiKeyHeaderOptional",
|
| 1702 |
+
"key": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1703 |
"header_name": "Authorization"
|
| 1704 |
},
|
| 1705 |
"docs_url": "https://huggingface.co/ElKulako/cryptobert",
|
|
|
|
| 1709 |
"notes": "For sentiment analysis"
|
| 1710 |
},
|
| 1711 |
{
|
| 1712 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1713 |
"type": "model",
|
| 1714 |
"name": "kk08/CryptoBERT",
|
| 1715 |
"base_url": "https://api-inference.huggingface.co/models/kk08/CryptoBERT",
|
| 1716 |
"auth": {
|
| 1717 |
"type": "apiKeyHeaderOptional",
|
| 1718 |
+
"key": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1719 |
"header_name": "Authorization"
|
| 1720 |
},
|
| 1721 |
"docs_url": "https://huggingface.co/kk08/CryptoBERT",
|
|
|
|
| 1725 |
"notes": "For sentiment analysis"
|
| 1726 |
},
|
| 1727 |
{
|
| 1728 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1729 |
"type": "dataset",
|
| 1730 |
"name": "linxy/CryptoCoin",
|
| 1731 |
"base_url": "https://huggingface.co/datasets/linxy/CryptoCoin/resolve/main",
|
|
|
|
| 1739 |
"notes": "26 symbols x 7 timeframes = 182 CSVs"
|
| 1740 |
},
|
| 1741 |
{
|
| 1742 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1743 |
"type": "dataset",
|
| 1744 |
"name": "WinkingFace/CryptoLM-Bitcoin-BTC-USDT",
|
| 1745 |
"base_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Bitcoin-BTC-USDT/resolve/main",
|
|
|
|
| 1754 |
"notes": null
|
| 1755 |
},
|
| 1756 |
{
|
| 1757 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1758 |
"type": "dataset",
|
| 1759 |
"name": "WinkingFace/CryptoLM-Ethereum-ETH-USDT",
|
| 1760 |
"base_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Ethereum-ETH-USDT/resolve/main",
|
|
|
|
| 1769 |
"notes": null
|
| 1770 |
},
|
| 1771 |
{
|
| 1772 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1773 |
"type": "dataset",
|
| 1774 |
"name": "WinkingFace/CryptoLM-Solana-SOL-USDT",
|
| 1775 |
"base_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Solana-SOL-USDT/resolve/main",
|
|
|
|
| 1781 |
"notes": null
|
| 1782 |
},
|
| 1783 |
{
|
| 1784 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1785 |
"type": "dataset",
|
| 1786 |
"name": "WinkingFace/CryptoLM-Ripple-XRP-USDT",
|
| 1787 |
"base_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Ripple-XRP-USDT/resolve/main",
|
|
|
|
| 1861 |
"notes": null
|
| 1862 |
},
|
| 1863 |
{
|
| 1864 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1865 |
"category": "hf-model",
|
| 1866 |
"name": "HF Model: ElKulako/CryptoBERT",
|
| 1867 |
"base_url": "https://huggingface.co/ElKulako/cryptobert",
|
|
|
|
| 1872 |
"notes": null
|
| 1873 |
},
|
| 1874 |
{
|
| 1875 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1876 |
"category": "hf-model",
|
| 1877 |
"name": "HF Model: kk08/CryptoBERT",
|
| 1878 |
"base_url": "https://huggingface.co/kk08/CryptoBERT",
|
|
|
|
| 1883 |
"notes": null
|
| 1884 |
},
|
| 1885 |
{
|
| 1886 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1887 |
"category": "hf-dataset",
|
| 1888 |
"name": "HF Dataset: linxy/CryptoCoin",
|
| 1889 |
"base_url": "https://huggingface.co/datasets/linxy/CryptoCoin",
|
|
|
|
| 2082 |
"source_files": [
|
| 2083 |
{
|
| 2084 |
"path": "/mnt/data/api - Copy.txt",
|
| 2085 |
+
"sha256": "20f9a3357a65c28a691990f89ad57f0de978600e65405fafe2c8b3c3502f6b77"
|
| 2086 |
},
|
| 2087 |
{
|
| 2088 |
"path": "/mnt/data/api-config-complete (1).txt",
|
| 2089 |
+
"sha256": "cb9f4c746f5b8a1d70824340425557e4483ad7a8e5396e0be67d68d671b23697"
|
| 2090 |
},
|
| 2091 |
{
|
| 2092 |
"path": "/mnt/data/crypto_resources_ultimate_2025.zip",
|
| 2093 |
+
"sha256": "5bb6f0ef790f09e23a88adbf4a4c0bc225183e896c3aa63416e53b1eec36ea87",
|
| 2094 |
"note": "contains crypto_resources.ts and more"
|
| 2095 |
}
|
| 2096 |
]
|
docs/CRYPTOBERT_INTEGRATION.md
CHANGED
|
@@ -35,7 +35,7 @@ This document describes the integration of the **ElKulako/CryptoBERT** model int
|
|
| 35 |
|
| 36 |
```bash
|
| 37 |
# Set HF_TOKEN for authenticated access
|
| 38 |
-
export HF_TOKEN="HF_TOKEN_FROM_SPACE_SECRET"
|
| 39 |
```
|
| 40 |
|
| 41 |
### Python Configuration (config.py)
|
|
@@ -50,7 +50,7 @@ HUGGINGFACE_MODELS = {
|
|
| 50 |
}
|
| 51 |
|
| 52 |
# Hugging Face Authentication
|
| 53 |
-
HF_TOKEN = os.environ.get("HF_TOKEN", "HF_TOKEN_FROM_SPACE_SECRET")
|
| 54 |
HF_USE_AUTH_TOKEN = bool(HF_TOKEN)
|
| 55 |
```
|
| 56 |
|
|
@@ -68,14 +68,14 @@ Run the provided setup script:
|
|
| 68 |
|
| 69 |
1. **Set environment variable (temporary)**:
|
| 70 |
```bash
|
| 71 |
-
export HF_TOKEN="HF_TOKEN_FROM_SPACE_SECRET"
|
| 72 |
```
|
| 73 |
|
| 74 |
2. **Set environment variable (persistent)**:
|
| 75 |
|
| 76 |
Add to `~/.bashrc` or `~/.zshrc`:
|
| 77 |
```bash
|
| 78 |
-
echo 'export HF_TOKEN="HF_TOKEN_FROM_SPACE_SECRET"' >> ~/.bashrc
|
| 79 |
source ~/.bashrc
|
| 80 |
```
|
| 81 |
|
|
@@ -147,7 +147,7 @@ info = ai_models.get_model_info()
|
|
| 147 |
|
| 148 |
print(f"Transformers available: {info['transformers_available']}")
|
| 149 |
print(f"Models initialized: {info['models_initialized']}")
|
| 150 |
-
print(f"HF auth configured: {info['
|
| 151 |
print(f"Device: {info['device']}")
|
| 152 |
|
| 153 |
print("\nLoaded models:")
|
|
|
|
| 35 |
|
| 36 |
```bash
|
| 37 |
# Set HF_TOKEN for authenticated access
|
| 38 |
+
export HF_TOKEN="<HF_TOKEN_FROM_SPACE_SECRET>"
|
| 39 |
```
|
| 40 |
|
| 41 |
### Python Configuration (config.py)
|
|
|
|
| 50 |
}
|
| 51 |
|
| 52 |
# Hugging Face Authentication
|
| 53 |
+
HF_TOKEN = os.environ.get("HF_TOKEN", "<HF_TOKEN_FROM_SPACE_SECRET>")
|
| 54 |
HF_USE_AUTH_TOKEN = bool(HF_TOKEN)
|
| 55 |
```
|
| 56 |
|
|
|
|
| 68 |
|
| 69 |
1. **Set environment variable (temporary)**:
|
| 70 |
```bash
|
| 71 |
+
export HF_TOKEN="<HF_TOKEN_FROM_SPACE_SECRET>"
|
| 72 |
```
|
| 73 |
|
| 74 |
2. **Set environment variable (persistent)**:
|
| 75 |
|
| 76 |
Add to `~/.bashrc` or `~/.zshrc`:
|
| 77 |
```bash
|
| 78 |
+
echo 'export HF_TOKEN="<HF_TOKEN_FROM_SPACE_SECRET>"' >> ~/.bashrc
|
| 79 |
source ~/.bashrc
|
| 80 |
```
|
| 81 |
|
|
|
|
| 147 |
|
| 148 |
print(f"Transformers available: {info['transformers_available']}")
|
| 149 |
print(f"Models initialized: {info['models_initialized']}")
|
| 150 |
+
print(f"HF auth configured: {info['<HF_TOKEN_FROM_SPACE_SECRET>']}")
|
| 151 |
print(f"Device: {info['device']}")
|
| 152 |
|
| 153 |
print("\nLoaded models:")
|
docs/DOCUMENTATION_MANIFEST.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"from": "API_DOCS.md",
|
| 4 |
+
"to": "docs/api/API_DOCS.md",
|
| 5 |
+
"status": "moved"
|
| 6 |
+
},
|
| 7 |
+
{
|
| 8 |
+
"from": "DEPLOYMENT.md",
|
| 9 |
+
"to": "docs/deployment/DEPLOYMENT.md",
|
| 10 |
+
"status": "moved"
|
| 11 |
+
},
|
| 12 |
+
{
|
| 13 |
+
"from": "SET_HF_TOKEN.md",
|
| 14 |
+
"to": "docs/deployment/SET_HF_TOKEN.md",
|
| 15 |
+
"status": "moved"
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
"from": "HF_SPACE_HUB_PRESERVATION_REPORT.md",
|
| 19 |
+
"to": "docs/reports/HF_SPACE_HUB_PRESERVATION_REPORT.md",
|
| 20 |
+
"status": "moved"
|
| 21 |
+
},
|
| 22 |
+
{
|
| 23 |
+
"from": "HF_SPACE_HUB_UPGRADE_REPORT.md",
|
| 24 |
+
"to": "docs/reports/HF_SPACE_HUB_UPGRADE_REPORT.md",
|
| 25 |
+
"status": "moved"
|
| 26 |
+
},
|
| 27 |
+
{
|
| 28 |
+
"from": "HF_SPACE_REPAIR_REPORT.md",
|
| 29 |
+
"to": "docs/reports/HF_SPACE_REPAIR_REPORT.md",
|
| 30 |
+
"status": "moved"
|
| 31 |
+
},
|
| 32 |
+
{
|
| 33 |
+
"from": "PROVIDER_AUTO_DISCOVERY_REPORT.json",
|
| 34 |
+
"to": "docs/reports/PROVIDER_AUTO_DISCOVERY_REPORT.json",
|
| 35 |
+
"status": "moved"
|
| 36 |
+
},
|
| 37 |
+
{
|
| 38 |
+
"from": "VALIDATION_REPORT.txt",
|
| 39 |
+
"to": "docs/reports/VALIDATION_REPORT.txt",
|
| 40 |
+
"status": "moved"
|
| 41 |
+
},
|
| 42 |
+
{
|
| 43 |
+
"from": "INPUT_API_FILES_SECURITY_NOTE.md",
|
| 44 |
+
"to": "docs/security/INPUT_API_FILES_SECURITY_NOTE.md",
|
| 45 |
+
"status": "moved"
|
| 46 |
+
},
|
| 47 |
+
{
|
| 48 |
+
"from": "CHANGELOG.md",
|
| 49 |
+
"to": "docs/archive/CHANGELOG.md",
|
| 50 |
+
"status": "moved"
|
| 51 |
+
}
|
| 52 |
+
]
|
docs/active/API_RESOURCES_RUNTIME_PLAN.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# API Resources Runtime Plan
|
| 2 |
+
|
| 3 |
+
`api-resources/provider_capabilities_v4.json` is the primary provider capability catalog.
|
| 4 |
+
|
| 5 |
+
`api-resources/crypto_resources_unified_2025-11-11.json` is retained as the extended registry/reference.
|
| 6 |
+
|
| 7 |
+
`api-resources/ultimate_crypto_pipeline_2025_NZasinich.json` has been normalized to valid JSON by removing the preserved filename prefix line.
|
| 8 |
+
|
| 9 |
+
Runtime logic uses provider adapters under `providers/` and exposes catalog validation through:
|
| 10 |
+
|
| 11 |
+
```bash
|
| 12 |
+
GET /api/short-hunter/catalog
|
| 13 |
+
GET /api/short-hunter/health
|
| 14 |
+
```
|
| 15 |
+
|
| 16 |
+
Provider rotation can be tuned with environment variables:
|
| 17 |
+
|
| 18 |
+
- `SH_PROVIDER_ORDER_UNIVERSE`
|
| 19 |
+
- `SH_PROVIDER_ORDER_CONTRACT`
|
| 20 |
+
- `SH_PROVIDER_ORDER_TICKER`
|
| 21 |
+
- `SH_PROVIDER_ORDER_OHLCV`
|
| 22 |
+
- `SH_PROVIDER_ORDER_ORDERBOOK`
|
| 23 |
+
- `SH_PROVIDER_ORDER_FUNDING`
|
| 24 |
+
- `SH_PROVIDER_ORDER_OPEN_INTEREST`
|
| 25 |
+
- `SH_PROVIDER_ORDER_MARK_INDEX`
|
| 26 |
+
- `SH_PROVIDER_ORDER_SENTIMENT`
|
| 27 |
+
|
| 28 |
+
Example:
|
| 29 |
+
|
| 30 |
+
```bash
|
| 31 |
+
SH_PROVIDER_ORDER_TICKER=kucoin,binance,coingecko,cryptocompare
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
For exchange region/DNS issues, use provider base URL overrides or proxy variables instead of trying to mutate host DNS from the Space:
|
| 35 |
+
|
| 36 |
+
- `KUCOIN_FUTURES_BASE_URL`
|
| 37 |
+
- `BINANCE_FUTURES_BASE_URL`
|
| 38 |
+
- `HTTP_PROXY`
|
| 39 |
+
- `HTTPS_PROXY`
|
| 40 |
+
- `ALL_PROXY`
|
docs/active/DOCUMENTATION_ORGANIZATION.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Documentation Organization
|
| 2 |
+
|
| 3 |
+
The project root was release/archive heavy. The current recommended documentation map is:
|
| 4 |
+
|
| 5 |
+
- `docs/active/` — active production docs and Short Hunter contracts
|
| 6 |
+
- `docs/deployment/` — HuggingFace/Docker/deployment notes
|
| 7 |
+
- `docs/api/` — API and websocket references
|
| 8 |
+
- `docs/reports/` — repair/audit/preservation reports
|
| 9 |
+
- `docs/archive/` — legacy solution notes and old server docs
|
| 10 |
+
- `docs/security/` — security notes and secret-handling guidance
|
| 11 |
+
|
| 12 |
+
Root should remain focused on active runtime files: `README.md`, `START_HERE.md`, `Dockerfile`, `.env.example`, `requirements.txt`, `api_server_extended.py`, `api_compat_routes.py`, `short_hunter_routes.py`, `providers/`, and `api-resources/`.
|
docs/active/PRODUCTION_ENTRYPOINT.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Production Entrypoint
|
| 2 |
+
|
| 3 |
+
Active production runtime for HuggingFace Space deployment:
|
| 4 |
+
|
| 5 |
+
```bash
|
| 6 |
+
uvicorn api_server_extended:app --host 0.0.0.0 --port 7860
|
| 7 |
+
```
|
| 8 |
+
|
| 9 |
+
`api_server_extended.py` remains the single production FastAPI app. It preserves the existing compatibility routes and now registers the Short Hunter datasource gateway routes from `short_hunter_routes.py`.
|
| 10 |
+
|
| 11 |
+
Legacy or alternate servers such as `main.py`, `hf_unified_server.py`, `api/main.py`, `api/app.py`, `api/production_server.py`, and `api/real_server.py` are preserved as historical/reference implementations. They are not the production entrypoint.
|
docs/active/SHORT_HUNTER_DATASOURCE_CONTRACT.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Short Hunter Datasource Contract
|
| 2 |
+
|
| 3 |
+
This HuggingFace Space is a public-data datasource gateway for `SHORT HUNTER | FUTURES DESK`.
|
| 4 |
+
|
| 5 |
+
It is not a trading bot and never places orders.
|
| 6 |
+
|
| 7 |
+
## Core endpoints
|
| 8 |
+
|
| 9 |
+
- `GET /api/short-hunter/health`
|
| 10 |
+
- `GET /api/short-hunter/universe`
|
| 11 |
+
- `GET /api/short-hunter/market/{symbol}`
|
| 12 |
+
- `GET /api/short-hunter/ohlcv/{symbol}`
|
| 13 |
+
- `GET /api/short-hunter/orderbook/{symbol}`
|
| 14 |
+
- `GET /api/short-hunter/funding/{symbol}`
|
| 15 |
+
- `GET /api/short-hunter/open-interest/{symbol}`
|
| 16 |
+
- `GET /api/short-hunter/indicators/{symbol}`
|
| 17 |
+
- `GET /api/short-hunter/sentiment/{symbol}`
|
| 18 |
+
- `GET /api/short-hunter/snapshot/{symbol}`
|
| 19 |
+
- `POST /api/short-hunter/batch-snapshot`
|
| 20 |
+
- `GET /api/short-hunter/network/diagnostics`
|
| 21 |
+
|
| 22 |
+
## Normalized provider response
|
| 23 |
+
|
| 24 |
+
Every provider/capability component returns:
|
| 25 |
+
|
| 26 |
+
```json
|
| 27 |
+
{
|
| 28 |
+
"success": true,
|
| 29 |
+
"source": "kucoin_futures",
|
| 30 |
+
"capability": "funding",
|
| 31 |
+
"sourceMode": "LIVE",
|
| 32 |
+
"dataState": "REAL",
|
| 33 |
+
"latencyMs": 123,
|
| 34 |
+
"timestamp": "...",
|
| 35 |
+
"data": {},
|
| 36 |
+
"errors": [],
|
| 37 |
+
"warnings": [],
|
| 38 |
+
"missingCapabilities": [],
|
| 39 |
+
"noTradeGuard": false,
|
| 40 |
+
"noTradeGuardReason": null
|
| 41 |
+
}
|
| 42 |
+
```
|
| 43 |
+
|
| 44 |
+
## Truthfulness rule
|
| 45 |
+
|
| 46 |
+
The gateway must never fabricate live data. If data is missing, the response must expose `sourceMode=UNAVAILABLE` or `sourceMode=DEGRADED`, `dataState=UNAVAILABLE` or `PARTIAL`, `missingCapabilities`, and `noTradeGuard=true` for critical Short Hunter capabilities.
|
| 47 |
+
|
| 48 |
+
## Critical capabilities
|
| 49 |
+
|
| 50 |
+
- contract validation
|
| 51 |
+
- ticker
|
| 52 |
+
- OHLCV
|
| 53 |
+
- orderbook
|
| 54 |
+
- funding
|
| 55 |
+
- open interest
|
| 56 |
+
|
| 57 |
+
If any critical capability is missing from a snapshot, Short Hunter must treat the result as paper/manual only with no-trade guard active.
|
docs/api/API_DOCS.md
ADDED
|
@@ -0,0 +1,527 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 📡 API Documentation
|
| 2 |
+
|
| 3 |
+
## Base URL
|
| 4 |
+
```
|
| 5 |
+
http://localhost:8000
|
| 6 |
+
```
|
| 7 |
+
|
| 8 |
+
## Authentication
|
| 9 |
+
No authentication required for this demo version.
|
| 10 |
+
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
## 🏥 Health & Status Endpoints
|
| 14 |
+
|
| 15 |
+
### GET /health
|
| 16 |
+
Get system health status
|
| 17 |
+
|
| 18 |
+
**Request:**
|
| 19 |
+
```bash
|
| 20 |
+
curl http://localhost:8000/health
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
**Response:**
|
| 24 |
+
```json
|
| 25 |
+
{
|
| 26 |
+
"status": "healthy",
|
| 27 |
+
"timestamp": "2025-01-15T10:30:00",
|
| 28 |
+
"components": [
|
| 29 |
+
{
|
| 30 |
+
"name": "API Server 1",
|
| 31 |
+
"status": "healthy",
|
| 32 |
+
"uptime": 99.99,
|
| 33 |
+
"response_time": 120
|
| 34 |
+
}
|
| 35 |
+
],
|
| 36 |
+
"summary": {
|
| 37 |
+
"total_components": 8,
|
| 38 |
+
"healthy": 8,
|
| 39 |
+
"degraded": 0,
|
| 40 |
+
"critical": 0
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
---
|
| 46 |
+
|
| 47 |
+
### GET /info
|
| 48 |
+
Get system information
|
| 49 |
+
|
| 50 |
+
**Request:**
|
| 51 |
+
```bash
|
| 52 |
+
curl http://localhost:8000/info
|
| 53 |
+
```
|
| 54 |
+
|
| 55 |
+
**Response:**
|
| 56 |
+
```json
|
| 57 |
+
{
|
| 58 |
+
"name": "Crypto API Monitor",
|
| 59 |
+
"version": "1.0.0",
|
| 60 |
+
"environment": "production",
|
| 61 |
+
"uptime_seconds": 86400,
|
| 62 |
+
"memory_usage_mb": 450,
|
| 63 |
+
"cpu_usage_percent": 25.5,
|
| 64 |
+
"active_connections": 3,
|
| 65 |
+
"timestamp": "2025-01-15T10:30:00"
|
| 66 |
+
}
|
| 67 |
+
```
|
| 68 |
+
|
| 69 |
+
---
|
| 70 |
+
|
| 71 |
+
## 📊 Provider Endpoints
|
| 72 |
+
|
| 73 |
+
### GET /api/providers
|
| 74 |
+
Get all data providers status
|
| 75 |
+
|
| 76 |
+
**Request:**
|
| 77 |
+
```bash
|
| 78 |
+
curl http://localhost:8000/api/providers
|
| 79 |
+
```
|
| 80 |
+
|
| 81 |
+
**Response:**
|
| 82 |
+
```json
|
| 83 |
+
[
|
| 84 |
+
{
|
| 85 |
+
"name": "Binance",
|
| 86 |
+
"type": "Exchange",
|
| 87 |
+
"status": "operational",
|
| 88 |
+
"uptime": 99.95,
|
| 89 |
+
"response_time_ms": 85,
|
| 90 |
+
"requests_today": 150000,
|
| 91 |
+
"last_check": "2025-01-15T10:30:00",
|
| 92 |
+
"endpoint": "https://api.binance.com"
|
| 93 |
+
},
|
| 94 |
+
{
|
| 95 |
+
"name": "CoinGecko",
|
| 96 |
+
"type": "Data Provider",
|
| 97 |
+
"status": "operational",
|
| 98 |
+
"uptime": 99.87,
|
| 99 |
+
"response_time_ms": 120,
|
| 100 |
+
"requests_today": 89000,
|
| 101 |
+
"last_check": "2025-01-15T10:30:00",
|
| 102 |
+
"endpoint": "https://api.coingecko.com"
|
| 103 |
+
}
|
| 104 |
+
]
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
---
|
| 108 |
+
|
| 109 |
+
## 💰 Cryptocurrency Data
|
| 110 |
+
|
| 111 |
+
### GET /api/crypto/prices/top
|
| 112 |
+
Get top cryptocurrency prices
|
| 113 |
+
|
| 114 |
+
**Parameters:**
|
| 115 |
+
- `limit` (optional): Number of results (default: 10)
|
| 116 |
+
|
| 117 |
+
**Request:**
|
| 118 |
+
```bash
|
| 119 |
+
curl http://localhost:8000/api/crypto/prices/top?limit=5
|
| 120 |
+
```
|
| 121 |
+
|
| 122 |
+
**Response:**
|
| 123 |
+
```json
|
| 124 |
+
[
|
| 125 |
+
{
|
| 126 |
+
"symbol": "BTC",
|
| 127 |
+
"name": "Bitcoin",
|
| 128 |
+
"price": 42150.50,
|
| 129 |
+
"change_24h": 3.25,
|
| 130 |
+
"volume_24h": 28000000000,
|
| 131 |
+
"market_cap": 825000000000,
|
| 132 |
+
"last_updated": "2025-01-15T10:30:00"
|
| 133 |
+
},
|
| 134 |
+
{
|
| 135 |
+
"symbol": "ETH",
|
| 136 |
+
"name": "Ethereum",
|
| 137 |
+
"price": 2215.80,
|
| 138 |
+
"change_24h": 2.15,
|
| 139 |
+
"volume_24h": 12000000000,
|
| 140 |
+
"market_cap": 265000000000,
|
| 141 |
+
"last_updated": "2025-01-15T10:30:00"
|
| 142 |
+
}
|
| 143 |
+
]
|
| 144 |
+
```
|
| 145 |
+
|
| 146 |
+
---
|
| 147 |
+
|
| 148 |
+
### GET /api/crypto/market-overview
|
| 149 |
+
Get market overview and statistics
|
| 150 |
+
|
| 151 |
+
**Request:**
|
| 152 |
+
```bash
|
| 153 |
+
curl http://localhost:8000/api/crypto/market-overview
|
| 154 |
+
```
|
| 155 |
+
|
| 156 |
+
**Response:**
|
| 157 |
+
```json
|
| 158 |
+
{
|
| 159 |
+
"total_market_cap": 1750000000000,
|
| 160 |
+
"total_volume_24h": 95000000000,
|
| 161 |
+
"average_change_24h": 2.45,
|
| 162 |
+
"top_gainers": [
|
| 163 |
+
{
|
| 164 |
+
"symbol": "SOL",
|
| 165 |
+
"name": "Solana",
|
| 166 |
+
"price": 98.50,
|
| 167 |
+
"change_24h": 12.30
|
| 168 |
+
}
|
| 169 |
+
],
|
| 170 |
+
"top_losers": [
|
| 171 |
+
{
|
| 172 |
+
"symbol": "XRP",
|
| 173 |
+
"name": "Ripple",
|
| 174 |
+
"price": 0.51,
|
| 175 |
+
"change_24h": -5.20
|
| 176 |
+
}
|
| 177 |
+
],
|
| 178 |
+
"timestamp": "2025-01-15T10:30:00"
|
| 179 |
+
}
|
| 180 |
+
```
|
| 181 |
+
|
| 182 |
+
---
|
| 183 |
+
|
| 184 |
+
## 📁 Categories
|
| 185 |
+
|
| 186 |
+
### GET /api/categories
|
| 187 |
+
Get cryptocurrency categories
|
| 188 |
+
|
| 189 |
+
**Request:**
|
| 190 |
+
```bash
|
| 191 |
+
curl http://localhost:8000/api/categories
|
| 192 |
+
```
|
| 193 |
+
|
| 194 |
+
**Response:**
|
| 195 |
+
```json
|
| 196 |
+
[
|
| 197 |
+
{
|
| 198 |
+
"id": 1,
|
| 199 |
+
"name": "DeFi",
|
| 200 |
+
"market_cap": 45000000000,
|
| 201 |
+
"change_24h": 5.2
|
| 202 |
+
},
|
| 203 |
+
{
|
| 204 |
+
"id": 2,
|
| 205 |
+
"name": "Smart Contract Platform",
|
| 206 |
+
"market_cap": 120000000000,
|
| 207 |
+
"change_24h": 3.1
|
| 208 |
+
}
|
| 209 |
+
]
|
| 210 |
+
```
|
| 211 |
+
|
| 212 |
+
---
|
| 213 |
+
|
| 214 |
+
## ⏱️ Rate Limits
|
| 215 |
+
|
| 216 |
+
### GET /api/rate-limits
|
| 217 |
+
Get API rate limit information
|
| 218 |
+
|
| 219 |
+
**Request:**
|
| 220 |
+
```bash
|
| 221 |
+
curl http://localhost:8000/api/rate-limits
|
| 222 |
+
```
|
| 223 |
+
|
| 224 |
+
**Response:**
|
| 225 |
+
```json
|
| 226 |
+
[
|
| 227 |
+
{
|
| 228 |
+
"provider": "Binance",
|
| 229 |
+
"limit_per_minute": 1200,
|
| 230 |
+
"limit_per_hour": 60000,
|
| 231 |
+
"remaining": 850,
|
| 232 |
+
"reset_time": "2025-01-15T10:31:00"
|
| 233 |
+
}
|
| 234 |
+
]
|
| 235 |
+
```
|
| 236 |
+
|
| 237 |
+
---
|
| 238 |
+
|
| 239 |
+
## 📋 Logs
|
| 240 |
+
|
| 241 |
+
### GET /api/logs
|
| 242 |
+
Get system logs
|
| 243 |
+
|
| 244 |
+
**Parameters:**
|
| 245 |
+
- `limit` (optional): Number of logs (default: 50)
|
| 246 |
+
|
| 247 |
+
**Request:**
|
| 248 |
+
```bash
|
| 249 |
+
curl http://localhost:8000/api/logs?limit=10
|
| 250 |
+
```
|
| 251 |
+
|
| 252 |
+
**Response:**
|
| 253 |
+
```json
|
| 254 |
+
[
|
| 255 |
+
{
|
| 256 |
+
"id": 1,
|
| 257 |
+
"timestamp": "2025-01-15T10:30:00",
|
| 258 |
+
"level": "INFO",
|
| 259 |
+
"message": "API request processed successfully",
|
| 260 |
+
"provider": "Binance"
|
| 261 |
+
},
|
| 262 |
+
{
|
| 263 |
+
"id": 2,
|
| 264 |
+
"timestamp": "2025-01-15T10:29:45",
|
| 265 |
+
"level": "WARNING",
|
| 266 |
+
"message": "Rate limit approaching",
|
| 267 |
+
"provider": "CoinGecko"
|
| 268 |
+
}
|
| 269 |
+
]
|
| 270 |
+
```
|
| 271 |
+
|
| 272 |
+
---
|
| 273 |
+
|
| 274 |
+
## 🔔 Alerts
|
| 275 |
+
|
| 276 |
+
### GET /api/alerts
|
| 277 |
+
Get active system alerts
|
| 278 |
+
|
| 279 |
+
**Request:**
|
| 280 |
+
```bash
|
| 281 |
+
curl http://localhost:8000/api/alerts
|
| 282 |
+
```
|
| 283 |
+
|
| 284 |
+
**Response:**
|
| 285 |
+
```json
|
| 286 |
+
[
|
| 287 |
+
{
|
| 288 |
+
"id": 1,
|
| 289 |
+
"severity": "warning",
|
| 290 |
+
"title": "High API Usage",
|
| 291 |
+
"message": "API usage is at 85% of limit",
|
| 292 |
+
"timestamp": "2025-01-15T10:30:00"
|
| 293 |
+
}
|
| 294 |
+
]
|
| 295 |
+
```
|
| 296 |
+
|
| 297 |
+
---
|
| 298 |
+
|
| 299 |
+
## 🤗 Hugging Face Integration
|
| 300 |
+
|
| 301 |
+
### GET /api/hf/health
|
| 302 |
+
Check Hugging Face integration health
|
| 303 |
+
|
| 304 |
+
**Request:**
|
| 305 |
+
```bash
|
| 306 |
+
curl http://localhost:8000/api/hf/health
|
| 307 |
+
```
|
| 308 |
+
|
| 309 |
+
**Response:**
|
| 310 |
+
```json
|
| 311 |
+
{
|
| 312 |
+
"status": "operational",
|
| 313 |
+
"models_available": 12,
|
| 314 |
+
"last_sync": "2025-01-15T10:30:00"
|
| 315 |
+
}
|
| 316 |
+
```
|
| 317 |
+
|
| 318 |
+
---
|
| 319 |
+
|
| 320 |
+
### POST /api/hf/refresh
|
| 321 |
+
Refresh Hugging Face data
|
| 322 |
+
|
| 323 |
+
**Request:**
|
| 324 |
+
```bash
|
| 325 |
+
curl -X POST http://localhost:8000/api/hf/refresh
|
| 326 |
+
```
|
| 327 |
+
|
| 328 |
+
**Response:**
|
| 329 |
+
```json
|
| 330 |
+
{
|
| 331 |
+
"status": "success",
|
| 332 |
+
"message": "Data refresh initiated",
|
| 333 |
+
"timestamp": "2025-01-15T10:30:00"
|
| 334 |
+
}
|
| 335 |
+
```
|
| 336 |
+
|
| 337 |
+
---
|
| 338 |
+
|
| 339 |
+
### GET /api/hf/registry
|
| 340 |
+
Get Hugging Face model registry
|
| 341 |
+
|
| 342 |
+
**Request:**
|
| 343 |
+
```bash
|
| 344 |
+
curl http://localhost:8000/api/hf/registry
|
| 345 |
+
```
|
| 346 |
+
|
| 347 |
+
**Response:**
|
| 348 |
+
```json
|
| 349 |
+
{
|
| 350 |
+
"models": [
|
| 351 |
+
{
|
| 352 |
+
"name": "sentiment-analysis",
|
| 353 |
+
"status": "active"
|
| 354 |
+
},
|
| 355 |
+
{
|
| 356 |
+
"name": "price-prediction",
|
| 357 |
+
"status": "active"
|
| 358 |
+
}
|
| 359 |
+
]
|
| 360 |
+
}
|
| 361 |
+
```
|
| 362 |
+
|
| 363 |
+
---
|
| 364 |
+
|
| 365 |
+
### POST /api/hf/run-sentiment
|
| 366 |
+
Run sentiment analysis
|
| 367 |
+
|
| 368 |
+
**Request:**
|
| 369 |
+
```bash
|
| 370 |
+
curl -X POST http://localhost:8000/api/hf/run-sentiment \
|
| 371 |
+
-H "Content-Type: application/json" \
|
| 372 |
+
-d '{"text": "Bitcoin is going to the moon!"}'
|
| 373 |
+
```
|
| 374 |
+
|
| 375 |
+
**Response:**
|
| 376 |
+
```json
|
| 377 |
+
{
|
| 378 |
+
"sentiment": "positive",
|
| 379 |
+
"score": 0.95,
|
| 380 |
+
"timestamp": "2025-01-15T10:30:00"
|
| 381 |
+
}
|
| 382 |
+
```
|
| 383 |
+
|
| 384 |
+
---
|
| 385 |
+
|
| 386 |
+
## 🔌 WebSocket
|
| 387 |
+
|
| 388 |
+
### WS /ws/live
|
| 389 |
+
Real-time updates via WebSocket
|
| 390 |
+
|
| 391 |
+
**Connection:**
|
| 392 |
+
```javascript
|
| 393 |
+
const ws = new WebSocket('ws://localhost:8000/ws/live');
|
| 394 |
+
|
| 395 |
+
ws.onopen = () => {
|
| 396 |
+
console.log('Connected');
|
| 397 |
+
};
|
| 398 |
+
|
| 399 |
+
ws.onmessage = (event) => {
|
| 400 |
+
const data = JSON.parse(event.data);
|
| 401 |
+
console.log('Message:', data);
|
| 402 |
+
};
|
| 403 |
+
```
|
| 404 |
+
|
| 405 |
+
**Message Types:**
|
| 406 |
+
|
| 407 |
+
#### Connection Established
|
| 408 |
+
```json
|
| 409 |
+
{
|
| 410 |
+
"type": "connection_established",
|
| 411 |
+
"timestamp": "2025-01-15T10:30:00"
|
| 412 |
+
}
|
| 413 |
+
```
|
| 414 |
+
|
| 415 |
+
#### Status Update
|
| 416 |
+
```json
|
| 417 |
+
{
|
| 418 |
+
"type": "status_update",
|
| 419 |
+
"data": {
|
| 420 |
+
"status": "healthy",
|
| 421 |
+
"components": [...]
|
| 422 |
+
},
|
| 423 |
+
"timestamp": "2025-01-15T10:30:00"
|
| 424 |
+
}
|
| 425 |
+
```
|
| 426 |
+
|
| 427 |
+
#### Provider Status Change
|
| 428 |
+
```json
|
| 429 |
+
{
|
| 430 |
+
"type": "provider_status_change",
|
| 431 |
+
"data": {
|
| 432 |
+
"provider": "Binance",
|
| 433 |
+
"status": "operational"
|
| 434 |
+
},
|
| 435 |
+
"timestamp": "2025-01-15T10:30:00"
|
| 436 |
+
}
|
| 437 |
+
```
|
| 438 |
+
|
| 439 |
+
#### New Alert
|
| 440 |
+
```json
|
| 441 |
+
{
|
| 442 |
+
"type": "new_alert",
|
| 443 |
+
"data": {
|
| 444 |
+
"severity": "info",
|
| 445 |
+
"title": "System Update",
|
| 446 |
+
"message": "Cache refreshed successfully"
|
| 447 |
+
},
|
| 448 |
+
"timestamp": "2025-01-15T10:30:00"
|
| 449 |
+
}
|
| 450 |
+
```
|
| 451 |
+
|
| 452 |
+
---
|
| 453 |
+
|
| 454 |
+
## 📊 Status Codes
|
| 455 |
+
|
| 456 |
+
- `200` - Success
|
| 457 |
+
- `404` - Endpoint not found
|
| 458 |
+
- `500` - Internal server error
|
| 459 |
+
|
| 460 |
+
---
|
| 461 |
+
|
| 462 |
+
## 🔄 Update Frequency
|
| 463 |
+
|
| 464 |
+
- **WebSocket**: Real-time (every 5 seconds)
|
| 465 |
+
- **Health**: On-demand
|
| 466 |
+
- **Providers**: On-demand
|
| 467 |
+
- **Crypto Prices**: On-demand (recommended: every 30s)
|
| 468 |
+
|
| 469 |
+
---
|
| 470 |
+
|
| 471 |
+
## 💡 Best Practices
|
| 472 |
+
|
| 473 |
+
1. **Use WebSocket** for real-time data instead of polling
|
| 474 |
+
2. **Cache responses** when appropriate
|
| 475 |
+
3. **Respect rate limits** to avoid throttling
|
| 476 |
+
4. **Handle errors** gracefully with retry logic
|
| 477 |
+
5. **Monitor health** endpoint regularly
|
| 478 |
+
|
| 479 |
+
---
|
| 480 |
+
|
| 481 |
+
## 🧪 Testing Endpoints
|
| 482 |
+
|
| 483 |
+
### Using curl:
|
| 484 |
+
```bash
|
| 485 |
+
# Test health
|
| 486 |
+
curl http://localhost:8000/health
|
| 487 |
+
|
| 488 |
+
# Test with formatting
|
| 489 |
+
curl http://localhost:8000/api/providers | python -m json.tool
|
| 490 |
+
```
|
| 491 |
+
|
| 492 |
+
### Using Python:
|
| 493 |
+
```python
|
| 494 |
+
import requests
|
| 495 |
+
|
| 496 |
+
# Get health status
|
| 497 |
+
response = requests.get('http://localhost:8000/health')
|
| 498 |
+
print(response.json())
|
| 499 |
+
|
| 500 |
+
# Get crypto prices
|
| 501 |
+
response = requests.get('http://localhost:8000/api/crypto/prices/top')
|
| 502 |
+
prices = response.json()
|
| 503 |
+
for crypto in prices:
|
| 504 |
+
print(f"{crypto['symbol']}: ${crypto['price']}")
|
| 505 |
+
```
|
| 506 |
+
|
| 507 |
+
### Using JavaScript:
|
| 508 |
+
```javascript
|
| 509 |
+
// Fetch crypto prices
|
| 510 |
+
fetch('http://localhost:8000/api/crypto/prices/top')
|
| 511 |
+
.then(response => response.json())
|
| 512 |
+
.then(data => console.log(data));
|
| 513 |
+
|
| 514 |
+
// WebSocket connection
|
| 515 |
+
const ws = new WebSocket('ws://localhost:8000/ws/live');
|
| 516 |
+
ws.onmessage = (event) => {
|
| 517 |
+
console.log('Update:', JSON.parse(event.data));
|
| 518 |
+
};
|
| 519 |
+
```
|
| 520 |
+
|
| 521 |
+
---
|
| 522 |
+
|
| 523 |
+
## 📞 Support
|
| 524 |
+
|
| 525 |
+
برای سوالات بیشتر، به `README.md` مراجعه کنید.
|
| 526 |
+
|
| 527 |
+
For more questions, refer to `README.md`.
|
docs/archive/CHANGELOG.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 📋 Changelog - نسخه 3.0.0
|
| 2 |
+
|
| 3 |
+
## ✨ ویژگیهای جدید
|
| 4 |
+
|
| 5 |
+
### 🎯 Log Management System
|
| 6 |
+
- ✅ سیستم کامل مدیریت لاگها
|
| 7 |
+
- ✅ فیلتر پیشرفته (Level, Category, Provider, Time Range)
|
| 8 |
+
- ✅ جستجو در لاگها
|
| 9 |
+
- ✅ Export به JSON و CSV
|
| 10 |
+
- ✅ Import از JSON
|
| 11 |
+
- ✅ آمار تفصیلی لاگها
|
| 12 |
+
- ✅ Log Rotation خودکار
|
| 13 |
+
- ✅ نمایش Real-time در داشبورد
|
| 14 |
+
|
| 15 |
+
### 📦 Resource Management System
|
| 16 |
+
- ✅ مدیریت کامل منابع API
|
| 17 |
+
- ✅ Import از فایلهای JSON مختلف
|
| 18 |
+
- ✅ Export به JSON و CSV
|
| 19 |
+
- ✅ Backup خودکار
|
| 20 |
+
- ✅ اعتبارسنجی Provider
|
| 21 |
+
- ✅ فیلتر بر اساس Category
|
| 22 |
+
- ✅ آمار تفصیلی منابع
|
| 23 |
+
|
| 24 |
+
### 🎨 UI/UX Enhancements
|
| 25 |
+
- ✅ تب جدید Logs با فیلتر پیشرفته
|
| 26 |
+
- ✅ تب جدید Resources با مدیریت کامل
|
| 27 |
+
- ✅ Modal برای Import منابع
|
| 28 |
+
- ✅ بهبود طراحی و رنگبندی
|
| 29 |
+
- ✅ Toast Notifications
|
| 30 |
+
- ✅ Responsive Design
|
| 31 |
+
|
| 32 |
+
### 🔧 API Enhancements
|
| 33 |
+
- ✅ 20+ Endpoint جدید برای Log Management
|
| 34 |
+
- ✅ 10+ Endpoint جدید برای Resource Management
|
| 35 |
+
- ✅ یکپارچهسازی Log Manager با Provider Manager
|
| 36 |
+
- ✅ یکپارچهسازی Resource Manager
|
| 37 |
+
|
| 38 |
+
### 📊 Provider Management
|
| 39 |
+
- ✅ ادغام 200+ منبع از فایلهای JSON
|
| 40 |
+
- ✅ پشتیبانی از فرمتهای مختلف JSON
|
| 41 |
+
- ✅ تبدیل خودکار فرمتهای مختلف
|
| 42 |
+
- ✅ مدیریت API Keys
|
| 43 |
+
|
| 44 |
+
## 📁 فایلهای جدید
|
| 45 |
+
|
| 46 |
+
1. **log_manager.py** - سیستم مدیریت لاگها
|
| 47 |
+
2. **resource_manager.py** - سیستم مدیریت منابع
|
| 48 |
+
3. **import_resources.py** - اسکریپت import خودکار
|
| 49 |
+
4. **providers_config_ultimate.json** - پیکربندی کامل با 200+ منبع
|
| 50 |
+
5. **QUICK_START.md** - راهنمای سریع شروع
|
| 51 |
+
|
| 52 |
+
## 🔄 تغییرات در فایلهای موجود
|
| 53 |
+
|
| 54 |
+
### unified_dashboard.html
|
| 55 |
+
- ✅ افزودن تب Logs
|
| 56 |
+
- ✅ افزودن تب Resources
|
| 57 |
+
- ✅ افزودن Modal Import
|
| 58 |
+
- ✅ توابع JavaScript برای Logs و Resources
|
| 59 |
+
- ✅ بهبود UI/UX
|
| 60 |
+
|
| 61 |
+
### api_server_extended.py
|
| 62 |
+
- ✅ یکپارچهسازی Log Manager
|
| 63 |
+
- ✅ یکپارچهسازی Resource Manager
|
| 64 |
+
- ✅ Endpointهای جدید برای Logs
|
| 65 |
+
- ✅ Endpointهای جدید برای Resources
|
| 66 |
+
- ✅ بهبود Error Handling
|
| 67 |
+
|
| 68 |
+
## 📈 آمار
|
| 69 |
+
|
| 70 |
+
- **کل منابع**: 200+
|
| 71 |
+
- **دستهبندیها**: 9 دسته مختلف
|
| 72 |
+
- **API Endpoints**: 50+
|
| 73 |
+
- **تبهای داشبورد**: 8 تب
|
| 74 |
+
- **قابلیت Export**: JSON, CSV
|
| 75 |
+
- **قابلیت Import**: JSON
|
| 76 |
+
|
| 77 |
+
## 🐛 رفع مشکلات
|
| 78 |
+
|
| 79 |
+
- ✅ بهبود Error Handling
|
| 80 |
+
- ✅ بهبود Performance
|
| 81 |
+
- ✅ بهبود Memory Management
|
| 82 |
+
- ✅ بهبود Log Rotation
|
| 83 |
+
|
| 84 |
+
## 🔮 ویژگیهای آینده
|
| 85 |
+
|
| 86 |
+
- [ ] Real-time WebSocket برای لاگها
|
| 87 |
+
- [ ] Dashboard Analytics پیشرفته
|
| 88 |
+
- [ ] Alert System (Email, Telegram)
|
| 89 |
+
- [ ] Auto-scaling برای Providers
|
| 90 |
+
- [ ] Machine Learning برای انتخاب بهترین Provider
|
| 91 |
+
|
| 92 |
+
---
|
| 93 |
+
|
| 94 |
+
**نسخه 3.0.0 - 13 نوامبر 2025**
|
| 95 |
+
|
docs/components/WEBSOCKET_GUIDE.md
CHANGED
|
@@ -1,446 +1,446 @@
|
|
| 1 |
-
# 📡 راهنمای استفاده از WebSocket API
|
| 2 |
-
|
| 3 |
-
## 🎯 مقدمه
|
| 4 |
-
|
| 5 |
-
این سیستم از WebSocket برای ارتباط بلادرنگ (Real-time) بین سرور و کلاینت استفاده میکند که سرعت و کارایی بسیار بالاتری نسبت به HTTP polling دارد.
|
| 6 |
-
|
| 7 |
-
## 🚀 مزایای WebSocket نسبت به HTTP
|
| 8 |
-
|
| 9 |
-
| ویژگی | HTTP Polling | WebSocket |
|
| 10 |
-
|-------|--------------|-----------|
|
| 11 |
-
| سرعت | کند (1-5 ثانیه تاخیر) | فوری (< 100ms) |
|
| 12 |
-
| منابع سرور | بالا | پایین |
|
| 13 |
-
| پهنای باند | زیاد | کم |
|
| 14 |
-
| اتصال | Multiple | Single (دائمی) |
|
| 15 |
-
| Overhead | بالا (headers هر بار) | خیلی کم |
|
| 16 |
-
|
| 17 |
-
## 📦 فایلهای اضافه شده
|
| 18 |
-
|
| 19 |
-
### Backend:
|
| 20 |
-
- `backend/services/connection_manager.py` - مدیریت اتصالات WebSocket
|
| 21 |
-
- تغییرات در `api_server_extended.py` - اضافه شدن endpointهای WebSocket
|
| 22 |
-
|
| 23 |
-
### Frontend:
|
| 24 |
-
- `static/js/websocket-client.js` - کلاینت JavaScript
|
| 25 |
-
- `static/css/connection-status.css` - استایلهای بصری
|
| 26 |
-
- `test_websocket.html` - صفحه تست
|
| 27 |
-
|
| 28 |
-
## 🔌 اتصال به WebSocket
|
| 29 |
-
|
| 30 |
-
### از JavaScript:
|
| 31 |
-
|
| 32 |
-
```javascript
|
| 33 |
-
// استفاده از کلاینت آماده
|
| 34 |
-
const wsClient = new CryptoWebSocketClient();
|
| 35 |
-
|
| 36 |
-
// یا اتصال دستی
|
| 37 |
-
const ws = new WebSocket('ws://localhost:8000/ws');
|
| 38 |
-
|
| 39 |
-
ws.onopen = () => {
|
| 40 |
-
console.log('متصل شد!');
|
| 41 |
-
};
|
| 42 |
-
|
| 43 |
-
ws.onmessage = (event) => {
|
| 44 |
-
const data = JSON.parse(event.data);
|
| 45 |
-
console.log('پیام دریافت شد:', data);
|
| 46 |
-
};
|
| 47 |
-
```
|
| 48 |
-
|
| 49 |
-
### از Python:
|
| 50 |
-
|
| 51 |
-
```python
|
| 52 |
-
import asyncio
|
| 53 |
-
import websockets
|
| 54 |
-
import json
|
| 55 |
-
|
| 56 |
-
async def connect():
|
| 57 |
-
uri = "ws://localhost:8000/ws"
|
| 58 |
-
async with websockets.connect(uri) as websocket:
|
| 59 |
-
# دریافت پیام welcome
|
| 60 |
-
welcome = await websocket.recv()
|
| 61 |
-
print(f"دریافت: {welcome}")
|
| 62 |
-
|
| 63 |
-
# ارسال پیام
|
| 64 |
-
await websocket.send(json.dumps({
|
| 65 |
-
"type": "subscribe",
|
| 66 |
-
"group": "market"
|
| 67 |
-
}))
|
| 68 |
-
|
| 69 |
-
# دریافت پیامها
|
| 70 |
-
async for message in websocket:
|
| 71 |
-
data = json.loads(message)
|
| 72 |
-
print(f"داده جدید: {data}")
|
| 73 |
-
|
| 74 |
-
asyncio.run(connect())
|
| 75 |
-
```
|
| 76 |
-
|
| 77 |
-
## 📨 انواع پیامها
|
| 78 |
-
|
| 79 |
-
### 1. پیامهای سیستمی (Server → Client)
|
| 80 |
-
|
| 81 |
-
#### Welcome Message
|
| 82 |
-
```json
|
| 83 |
-
{
|
| 84 |
-
"type": "welcome",
|
| 85 |
-
"session_id": "
|
| 86 |
-
"message": "به سیستم مانیتورینگ کریپتو خوش آمدید",
|
| 87 |
-
"timestamp": "2024-01-15T10:30:00"
|
| 88 |
-
}
|
| 89 |
-
```
|
| 90 |
-
|
| 91 |
-
#### Stats Update (هر 30 ثانیه)
|
| 92 |
-
```json
|
| 93 |
-
{
|
| 94 |
-
"type": "stats_update",
|
| 95 |
-
"data": {
|
| 96 |
-
"active_connections": 15,
|
| 97 |
-
"total_sessions": 23,
|
| 98 |
-
"messages_sent": 1250,
|
| 99 |
-
"messages_received": 450,
|
| 100 |
-
"client_types": {
|
| 101 |
-
"browser": 12,
|
| 102 |
-
"api": 2,
|
| 103 |
-
"mobile": 1
|
| 104 |
-
},
|
| 105 |
-
"subscriptions": {
|
| 106 |
-
"market": 8,
|
| 107 |
-
"prices": 10,
|
| 108 |
-
"all": 15
|
| 109 |
-
}
|
| 110 |
-
},
|
| 111 |
-
"timestamp": "2024-01-15T10:30:30"
|
| 112 |
-
}
|
| 113 |
-
```
|
| 114 |
-
|
| 115 |
-
#### Provider Stats
|
| 116 |
-
```json
|
| 117 |
-
{
|
| 118 |
-
"type": "provider_stats",
|
| 119 |
-
"data": {
|
| 120 |
-
"summary": {
|
| 121 |
-
"total_providers": 150,
|
| 122 |
-
"online": 142,
|
| 123 |
-
"offline": 8,
|
| 124 |
-
"overall_success_rate": 95.5
|
| 125 |
-
}
|
| 126 |
-
},
|
| 127 |
-
"timestamp": "2024-01-15T10:30:30"
|
| 128 |
-
}
|
| 129 |
-
```
|
| 130 |
-
|
| 131 |
-
#### Market Update
|
| 132 |
-
```json
|
| 133 |
-
{
|
| 134 |
-
"type": "market_update",
|
| 135 |
-
"data": {
|
| 136 |
-
"btc": { "price": 43250, "change_24h": 2.5 },
|
| 137 |
-
"eth": { "price": 2280, "change_24h": -1.2 }
|
| 138 |
-
},
|
| 139 |
-
"timestamp": "2024-01-15T10:30:45"
|
| 140 |
-
}
|
| 141 |
-
```
|
| 142 |
-
|
| 143 |
-
#### Price Update
|
| 144 |
-
```json
|
| 145 |
-
{
|
| 146 |
-
"type": "price_update",
|
| 147 |
-
"data": {
|
| 148 |
-
"symbol": "BTC",
|
| 149 |
-
"price": 43250.50,
|
| 150 |
-
"change_24h": 2.35
|
| 151 |
-
},
|
| 152 |
-
"timestamp": "2024-01-15T10:30:50"
|
| 153 |
-
}
|
| 154 |
-
```
|
| 155 |
-
|
| 156 |
-
#### Alert
|
| 157 |
-
```json
|
| 158 |
-
{
|
| 159 |
-
"type": "alert",
|
| 160 |
-
"data": {
|
| 161 |
-
"alert_type": "price_threshold",
|
| 162 |
-
"message": "قیمت بیتکوین از ۴۵۰۰۰ دلار عبور کرد",
|
| 163 |
-
"severity": "info"
|
| 164 |
-
},
|
| 165 |
-
"timestamp": "2024-01-15T10:31:00"
|
| 166 |
-
}
|
| 167 |
-
```
|
| 168 |
-
|
| 169 |
-
#### Heartbeat
|
| 170 |
-
```json
|
| 171 |
-
{
|
| 172 |
-
"type": "heartbeat",
|
| 173 |
-
"timestamp": "2024-01-15T10:31:10"
|
| 174 |
-
}
|
| 175 |
-
```
|
| 176 |
-
|
| 177 |
-
### 2. پیامهای کلاینت (Client → Server)
|
| 178 |
-
|
| 179 |
-
#### Subscribe
|
| 180 |
-
```json
|
| 181 |
-
{
|
| 182 |
-
"type": "subscribe",
|
| 183 |
-
"group": "market"
|
| 184 |
-
}
|
| 185 |
-
```
|
| 186 |
-
|
| 187 |
-
گروههای موجود:
|
| 188 |
-
- `market` - بهروزرسانیهای بازار
|
| 189 |
-
- `prices` - تغییرات قیمت
|
| 190 |
-
- `news` - اخبار
|
| 191 |
-
- `alerts` - هشدارها
|
| 192 |
-
- `all` - همه
|
| 193 |
-
|
| 194 |
-
#### Unsubscribe
|
| 195 |
-
```json
|
| 196 |
-
{
|
| 197 |
-
"type": "unsubscribe",
|
| 198 |
-
"group": "market"
|
| 199 |
-
}
|
| 200 |
-
```
|
| 201 |
-
|
| 202 |
-
#### Request Stats
|
| 203 |
-
```json
|
| 204 |
-
{
|
| 205 |
-
"type": "get_stats"
|
| 206 |
-
}
|
| 207 |
-
```
|
| 208 |
-
|
| 209 |
-
#### Ping
|
| 210 |
-
```json
|
| 211 |
-
{
|
| 212 |
-
"type": "ping"
|
| 213 |
-
}
|
| 214 |
-
```
|
| 215 |
-
|
| 216 |
-
## 🎨 استفاده از کامپوننتهای بصری
|
| 217 |
-
|
| 218 |
-
### 1. نوار وضعیت اتصال
|
| 219 |
-
|
| 220 |
-
```html
|
| 221 |
-
<!-- اضافه کردن به صفحه -->
|
| 222 |
-
<div class="connection-status-bar" id="ws-connection-status">
|
| 223 |
-
<div class="ws-connection-info">
|
| 224 |
-
<span class="status-dot status-dot-offline" id="ws-status-dot"></span>
|
| 225 |
-
<span class="ws-status-text" id="ws-status-text">در حال اتصال...</span>
|
| 226 |
-
</div>
|
| 227 |
-
|
| 228 |
-
<div class="online-users-widget">
|
| 229 |
-
<div class="online-users-count">
|
| 230 |
-
<span class="users-icon">👥</span>
|
| 231 |
-
<span class="count-number" id="active-users-count">0</span>
|
| 232 |
-
<span class="count-label">کاربر آنلاین</span>
|
| 233 |
-
</div>
|
| 234 |
-
</div>
|
| 235 |
-
</div>
|
| 236 |
-
```
|
| 237 |
-
|
| 238 |
-
### 2. اضافه کردن CSS و JS
|
| 239 |
-
|
| 240 |
-
```html
|
| 241 |
-
<head>
|
| 242 |
-
<link rel="stylesheet" href="/static/css/connection-status.css">
|
| 243 |
-
</head>
|
| 244 |
-
<body>
|
| 245 |
-
<!-- محتوا -->
|
| 246 |
-
|
| 247 |
-
<script src="/static/js/websocket-client.js"></script>
|
| 248 |
-
</body>
|
| 249 |
-
```
|
| 250 |
-
|
| 251 |
-
### 3. استفاده از Client
|
| 252 |
-
|
| 253 |
-
```javascript
|
| 254 |
-
// کلاینت به صورت خودکار متصل میشود
|
| 255 |
-
// در دسترس از طریق window.wsClient
|
| 256 |
-
|
| 257 |
-
// ثبت handler سفارشی
|
| 258 |
-
window.wsClient.on('custom_event', (message) => {
|
| 259 |
-
console.log('رویداد سفارشی:', message);
|
| 260 |
-
});
|
| 261 |
-
|
| 262 |
-
// اتصال به وضعیت اتصال
|
| 263 |
-
window.wsClient.onConnection((isConnected) => {
|
| 264 |
-
if (isConnected) {
|
| 265 |
-
console.log('✅ متصل شد');
|
| 266 |
-
} else {
|
| 267 |
-
console.log('❌ قطع شد');
|
| 268 |
-
}
|
| 269 |
-
});
|
| 270 |
-
|
| 271 |
-
// ارسال پیام
|
| 272 |
-
window.wsClient.send({
|
| 273 |
-
type: 'custom_action',
|
| 274 |
-
data: { value: 123 }
|
| 275 |
-
});
|
| 276 |
-
```
|
| 277 |
-
|
| 278 |
-
## 🔧 API Endpoints
|
| 279 |
-
|
| 280 |
-
### GET `/api/sessions`
|
| 281 |
-
دریافت لیست sessionهای فعال
|
| 282 |
-
|
| 283 |
-
**Response:**
|
| 284 |
-
```json
|
| 285 |
-
{
|
| 286 |
-
"sessions": {
|
| 287 |
-
"550e8400-...": {
|
| 288 |
-
"session_id": "550e8400-...",
|
| 289 |
-
"client_type": "browser",
|
| 290 |
-
"connected_at": "2024-01-15T10:00:00",
|
| 291 |
-
"last_activity": "2024-01-15T10:30:00"
|
| 292 |
-
}
|
| 293 |
-
},
|
| 294 |
-
"stats": {
|
| 295 |
-
"active_connections": 15,
|
| 296 |
-
"total_sessions": 23
|
| 297 |
-
}
|
| 298 |
-
}
|
| 299 |
-
```
|
| 300 |
-
|
| 301 |
-
### GET `/api/sessions/stats`
|
| 302 |
-
دریافت آمار اتصالات
|
| 303 |
-
|
| 304 |
-
**Response:**
|
| 305 |
-
```json
|
| 306 |
-
{
|
| 307 |
-
"active_connections": 15,
|
| 308 |
-
"total_sessions": 23,
|
| 309 |
-
"messages_sent": 1250,
|
| 310 |
-
"messages_received": 450,
|
| 311 |
-
"client_types": {
|
| 312 |
-
"browser": 12,
|
| 313 |
-
"api": 2
|
| 314 |
-
}
|
| 315 |
-
}
|
| 316 |
-
```
|
| 317 |
-
|
| 318 |
-
### POST `/api/broadcast`
|
| 319 |
-
ارسال پیام به همه کلاینتها
|
| 320 |
-
|
| 321 |
-
**Request:**
|
| 322 |
-
```json
|
| 323 |
-
{
|
| 324 |
-
"message": {
|
| 325 |
-
"type": "notification",
|
| 326 |
-
"text": "سیستم بهروز شد"
|
| 327 |
-
},
|
| 328 |
-
"group": "all"
|
| 329 |
-
}
|
| 330 |
-
```
|
| 331 |
-
|
| 332 |
-
## 🧪 تست
|
| 333 |
-
|
| 334 |
-
### 1. باز کردن صفحه تست:
|
| 335 |
-
```
|
| 336 |
-
http://localhost:8000/test_websocket.html
|
| 337 |
-
```
|
| 338 |
-
|
| 339 |
-
### 2. چک کردن اتصال:
|
| 340 |
-
- نوار بالای صفحه باید سبز شود (متصل)
|
| 341 |
-
- تعداد کاربران آنلاین باید نمایش داده شود
|
| 342 |
-
|
| 343 |
-
### 3. تست دستورات:
|
| 344 |
-
- کلیک روی دکمههای مختلف
|
| 345 |
-
- مشاهده لاگ پیامها در پنل پایین
|
| 346 |
-
|
| 347 |
-
### 4. تست چند تب:
|
| 348 |
-
- باز کردن چند تب مرورگر
|
| 349 |
-
- تعداد کاربران آنلاین باید افزایش یابد
|
| 350 |
-
|
| 351 |
-
## 📊 مانیتورینگ
|
| 352 |
-
|
| 353 |
-
### لاگهای سرور:
|
| 354 |
-
```bash
|
| 355 |
-
# مشاهده لاگهای WebSocket
|
| 356 |
-
tail -f logs/app.log | grep "WebSocket"
|
| 357 |
-
```
|
| 358 |
-
|
| 359 |
-
### متریکها:
|
| 360 |
-
- تعداد اتصالات فعال
|
| 361 |
-
- تعداد کل sessionها
|
| 362 |
-
- پیامهای ارسالی/دریافتی
|
| 363 |
-
- توزیع انواع کلاینت
|
| 364 |
-
|
| 365 |
-
## 🔒 امنیت
|
| 366 |
-
|
| 367 |
-
### توصیهها:
|
| 368 |
-
1. برای production از `wss://` (WebSocket Secure) استفاده کنید
|
| 369 |
-
2. محدودیت تعداد اتصال برای هر IP
|
| 370 |
-
3. Rate limiting برای پیامها
|
| 371 |
-
4. اعتبارسنجی token برای authentication
|
| 372 |
-
|
| 373 |
-
### مثال با Token:
|
| 374 |
-
```javascript
|
| 375 |
-
const ws = new WebSocket('ws://localhost:8000/ws');
|
| 376 |
-
ws.onopen = () => {
|
| 377 |
-
ws.send(JSON.stringify({
|
| 378 |
-
type: 'auth',
|
| 379 |
-
token: 'YOUR_JWT_TOKEN'
|
| 380 |
-
}));
|
| 381 |
-
};
|
| 382 |
-
```
|
| 383 |
-
|
| 384 |
-
## 🐛 عیبیابی
|
| 385 |
-
|
| 386 |
-
### مشکل: اتصال برقرار نمیشود
|
| 387 |
-
```bash
|
| 388 |
-
# چک کردن اجرای سرور
|
| 389 |
-
curl http://localhost:8000/health
|
| 390 |
-
|
| 391 |
-
# بررسی پورت
|
| 392 |
-
netstat -an | grep 8000
|
| 393 |
-
```
|
| 394 |
-
|
| 395 |
-
### مشکل: اتصال قطع میشود
|
| 396 |
-
- Heartbeat فعال است؟
|
| 397 |
-
- Proxy یا Firewall مشکل ندارد؟
|
| 398 |
-
- Logهای سرور را بررسی کنید
|
| 399 |
-
|
| 400 |
-
### مشکل: پیامها دریافت نمیشوند
|
| 401 |
-
- Subscribe کردهاید؟
|
| 402 |
-
- نوع پیام صحیح است؟
|
| 403 |
-
- کنسول مرورگر را بررسی کنید
|
| 404 |
-
|
| 405 |
-
## 📚 منابع بیشتر
|
| 406 |
-
|
| 407 |
-
- [WebSocket API - MDN](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket)
|
| 408 |
-
- [FastAPI WebSockets](https://fastapi.tiangolo.com/advanced/websockets/)
|
| 409 |
-
- [websockets Python library](https://websockets.readthedocs.io/)
|
| 410 |
-
|
| 411 |
-
## 🎓 مثال کامل Integration
|
| 412 |
-
|
| 413 |
-
```html
|
| 414 |
-
<!DOCTYPE html>
|
| 415 |
-
<html lang="fa" dir="rtl">
|
| 416 |
-
<head>
|
| 417 |
-
<link rel="stylesheet" href="/static/css/connection-status.css">
|
| 418 |
-
</head>
|
| 419 |
-
<body>
|
| 420 |
-
<!-- UI Components -->
|
| 421 |
-
<div class="connection-status-bar" id="ws-connection-status">
|
| 422 |
-
<!-- ... -->
|
| 423 |
-
</div>
|
| 424 |
-
|
| 425 |
-
<div class="dashboard">
|
| 426 |
-
<h1>تعداد کاربران: <span id="user-count">0</span></h1>
|
| 427 |
-
</div>
|
| 428 |
-
|
| 429 |
-
<script src="/static/js/websocket-client.js"></script>
|
| 430 |
-
<script>
|
| 431 |
-
// Custom logic
|
| 432 |
-
if (window.wsClient) {
|
| 433 |
-
window.wsClient.on('stats_update', (msg) => {
|
| 434 |
-
document.getElementById('user-count').textContent =
|
| 435 |
-
msg.data.active_connections;
|
| 436 |
-
});
|
| 437 |
-
}
|
| 438 |
-
</script>
|
| 439 |
-
</body>
|
| 440 |
-
</html>
|
| 441 |
-
```
|
| 442 |
-
|
| 443 |
-
---
|
| 444 |
-
|
| 445 |
-
**نکته مهم:** این سیستم به صورت خودکار reconnect میکند و نیازی به مدیریت دستی ندارید!
|
| 446 |
-
|
|
|
|
| 1 |
+
# 📡 راهنمای استفاده از WebSocket API
|
| 2 |
+
|
| 3 |
+
## 🎯 مقدمه
|
| 4 |
+
|
| 5 |
+
این سیستم از WebSocket برای ارتباط بلادرنگ (Real-time) بین سرور و کلاینت استفاده میکند که سرعت و کارایی بسیار بالاتری نسبت به HTTP polling دارد.
|
| 6 |
+
|
| 7 |
+
## 🚀 مزایای WebSocket نسبت به HTTP
|
| 8 |
+
|
| 9 |
+
| ویژگی | HTTP Polling | WebSocket |
|
| 10 |
+
|-------|--------------|-----------|
|
| 11 |
+
| سرعت | کند (1-5 ثانیه تاخیر) | فوری (< 100ms) |
|
| 12 |
+
| منابع سرور | بالا | پایین |
|
| 13 |
+
| پهنای باند | زیاد | کم |
|
| 14 |
+
| اتصال | Multiple | Single (دائمی) |
|
| 15 |
+
| Overhead | بالا (headers هر بار) | خیلی کم |
|
| 16 |
+
|
| 17 |
+
## 📦 فایلهای اضافه شده
|
| 18 |
+
|
| 19 |
+
### Backend:
|
| 20 |
+
- `backend/services/connection_manager.py` - مدیریت اتصالات WebSocket
|
| 21 |
+
- تغییرات در `api_server_extended.py` - اضافه شدن endpointهای WebSocket
|
| 22 |
+
|
| 23 |
+
### Frontend:
|
| 24 |
+
- `static/js/websocket-client.js` - کلاینت JavaScript
|
| 25 |
+
- `static/css/connection-status.css` - استایلهای بصری
|
| 26 |
+
- `test_websocket.html` - صفحه تست
|
| 27 |
+
|
| 28 |
+
## 🔌 اتصال به WebSocket
|
| 29 |
+
|
| 30 |
+
### از JavaScript:
|
| 31 |
+
|
| 32 |
+
```javascript
|
| 33 |
+
// استفاده از کلاینت آماده
|
| 34 |
+
const wsClient = new CryptoWebSocketClient();
|
| 35 |
+
|
| 36 |
+
// یا اتصال دستی
|
| 37 |
+
const ws = new WebSocket('ws://localhost:8000/ws');
|
| 38 |
+
|
| 39 |
+
ws.onopen = () => {
|
| 40 |
+
console.log('متصل شد!');
|
| 41 |
+
};
|
| 42 |
+
|
| 43 |
+
ws.onmessage = (event) => {
|
| 44 |
+
const data = JSON.parse(event.data);
|
| 45 |
+
console.log('پیام دریافت شد:', data);
|
| 46 |
+
};
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
### از Python:
|
| 50 |
+
|
| 51 |
+
```python
|
| 52 |
+
import asyncio
|
| 53 |
+
import websockets
|
| 54 |
+
import json
|
| 55 |
+
|
| 56 |
+
async def connect():
|
| 57 |
+
uri = "ws://localhost:8000/ws"
|
| 58 |
+
async with websockets.connect(uri) as websocket:
|
| 59 |
+
# دریافت پیام welcome
|
| 60 |
+
welcome = await websocket.recv()
|
| 61 |
+
print(f"دریافت: {welcome}")
|
| 62 |
+
|
| 63 |
+
# ارسال پیام
|
| 64 |
+
await websocket.send(json.dumps({
|
| 65 |
+
"type": "subscribe",
|
| 66 |
+
"group": "market"
|
| 67 |
+
}))
|
| 68 |
+
|
| 69 |
+
# دریافت پیامها
|
| 70 |
+
async for message in websocket:
|
| 71 |
+
data = json.loads(message)
|
| 72 |
+
print(f"داده جدید: {data}")
|
| 73 |
+
|
| 74 |
+
asyncio.run(connect())
|
| 75 |
+
```
|
| 76 |
+
|
| 77 |
+
## 📨 انواع پیامها
|
| 78 |
+
|
| 79 |
+
### 1. پیامهای سیستمی (Server → Client)
|
| 80 |
+
|
| 81 |
+
#### Welcome Message
|
| 82 |
+
```json
|
| 83 |
+
{
|
| 84 |
+
"type": "welcome",
|
| 85 |
+
"session_id": "550e8400-e29b-41d4-a716-446655440000",
|
| 86 |
+
"message": "به سیستم مانیتورینگ کریپتو خوش آمدید",
|
| 87 |
+
"timestamp": "2024-01-15T10:30:00"
|
| 88 |
+
}
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
#### Stats Update (هر 30 ثانیه)
|
| 92 |
+
```json
|
| 93 |
+
{
|
| 94 |
+
"type": "stats_update",
|
| 95 |
+
"data": {
|
| 96 |
+
"active_connections": 15,
|
| 97 |
+
"total_sessions": 23,
|
| 98 |
+
"messages_sent": 1250,
|
| 99 |
+
"messages_received": 450,
|
| 100 |
+
"client_types": {
|
| 101 |
+
"browser": 12,
|
| 102 |
+
"api": 2,
|
| 103 |
+
"mobile": 1
|
| 104 |
+
},
|
| 105 |
+
"subscriptions": {
|
| 106 |
+
"market": 8,
|
| 107 |
+
"prices": 10,
|
| 108 |
+
"all": 15
|
| 109 |
+
}
|
| 110 |
+
},
|
| 111 |
+
"timestamp": "2024-01-15T10:30:30"
|
| 112 |
+
}
|
| 113 |
+
```
|
| 114 |
+
|
| 115 |
+
#### Provider Stats
|
| 116 |
+
```json
|
| 117 |
+
{
|
| 118 |
+
"type": "provider_stats",
|
| 119 |
+
"data": {
|
| 120 |
+
"summary": {
|
| 121 |
+
"total_providers": 150,
|
| 122 |
+
"online": 142,
|
| 123 |
+
"offline": 8,
|
| 124 |
+
"overall_success_rate": 95.5
|
| 125 |
+
}
|
| 126 |
+
},
|
| 127 |
+
"timestamp": "2024-01-15T10:30:30"
|
| 128 |
+
}
|
| 129 |
+
```
|
| 130 |
+
|
| 131 |
+
#### Market Update
|
| 132 |
+
```json
|
| 133 |
+
{
|
| 134 |
+
"type": "market_update",
|
| 135 |
+
"data": {
|
| 136 |
+
"btc": { "price": 43250, "change_24h": 2.5 },
|
| 137 |
+
"eth": { "price": 2280, "change_24h": -1.2 }
|
| 138 |
+
},
|
| 139 |
+
"timestamp": "2024-01-15T10:30:45"
|
| 140 |
+
}
|
| 141 |
+
```
|
| 142 |
+
|
| 143 |
+
#### Price Update
|
| 144 |
+
```json
|
| 145 |
+
{
|
| 146 |
+
"type": "price_update",
|
| 147 |
+
"data": {
|
| 148 |
+
"symbol": "BTC",
|
| 149 |
+
"price": 43250.50,
|
| 150 |
+
"change_24h": 2.35
|
| 151 |
+
},
|
| 152 |
+
"timestamp": "2024-01-15T10:30:50"
|
| 153 |
+
}
|
| 154 |
+
```
|
| 155 |
+
|
| 156 |
+
#### Alert
|
| 157 |
+
```json
|
| 158 |
+
{
|
| 159 |
+
"type": "alert",
|
| 160 |
+
"data": {
|
| 161 |
+
"alert_type": "price_threshold",
|
| 162 |
+
"message": "قیمت بیتکوین از ۴۵۰۰۰ دلار عبور کرد",
|
| 163 |
+
"severity": "info"
|
| 164 |
+
},
|
| 165 |
+
"timestamp": "2024-01-15T10:31:00"
|
| 166 |
+
}
|
| 167 |
+
```
|
| 168 |
+
|
| 169 |
+
#### Heartbeat
|
| 170 |
+
```json
|
| 171 |
+
{
|
| 172 |
+
"type": "heartbeat",
|
| 173 |
+
"timestamp": "2024-01-15T10:31:10"
|
| 174 |
+
}
|
| 175 |
+
```
|
| 176 |
+
|
| 177 |
+
### 2. پیامهای کلاینت (Client → Server)
|
| 178 |
+
|
| 179 |
+
#### Subscribe
|
| 180 |
+
```json
|
| 181 |
+
{
|
| 182 |
+
"type": "subscribe",
|
| 183 |
+
"group": "market"
|
| 184 |
+
}
|
| 185 |
+
```
|
| 186 |
+
|
| 187 |
+
گروههای موجود:
|
| 188 |
+
- `market` - بهروزرسانیهای بازار
|
| 189 |
+
- `prices` - تغییرات قیمت
|
| 190 |
+
- `news` - اخبار
|
| 191 |
+
- `alerts` - هشدارها
|
| 192 |
+
- `all` - همه
|
| 193 |
+
|
| 194 |
+
#### Unsubscribe
|
| 195 |
+
```json
|
| 196 |
+
{
|
| 197 |
+
"type": "unsubscribe",
|
| 198 |
+
"group": "market"
|
| 199 |
+
}
|
| 200 |
+
```
|
| 201 |
+
|
| 202 |
+
#### Request Stats
|
| 203 |
+
```json
|
| 204 |
+
{
|
| 205 |
+
"type": "get_stats"
|
| 206 |
+
}
|
| 207 |
+
```
|
| 208 |
+
|
| 209 |
+
#### Ping
|
| 210 |
+
```json
|
| 211 |
+
{
|
| 212 |
+
"type": "ping"
|
| 213 |
+
}
|
| 214 |
+
```
|
| 215 |
+
|
| 216 |
+
## 🎨 استفاده از کامپوننتهای بصری
|
| 217 |
+
|
| 218 |
+
### 1. نوار وضعیت اتصال
|
| 219 |
+
|
| 220 |
+
```html
|
| 221 |
+
<!-- اضافه کردن به صفحه -->
|
| 222 |
+
<div class="connection-status-bar" id="ws-connection-status">
|
| 223 |
+
<div class="ws-connection-info">
|
| 224 |
+
<span class="status-dot status-dot-offline" id="ws-status-dot"></span>
|
| 225 |
+
<span class="ws-status-text" id="ws-status-text">در حال اتصال...</span>
|
| 226 |
+
</div>
|
| 227 |
+
|
| 228 |
+
<div class="online-users-widget">
|
| 229 |
+
<div class="online-users-count">
|
| 230 |
+
<span class="users-icon">👥</span>
|
| 231 |
+
<span class="count-number" id="active-users-count">0</span>
|
| 232 |
+
<span class="count-label">کاربر آنلاین</span>
|
| 233 |
+
</div>
|
| 234 |
+
</div>
|
| 235 |
+
</div>
|
| 236 |
+
```
|
| 237 |
+
|
| 238 |
+
### 2. اضافه کردن CSS و JS
|
| 239 |
+
|
| 240 |
+
```html
|
| 241 |
+
<head>
|
| 242 |
+
<link rel="stylesheet" href="/static/css/connection-status.css">
|
| 243 |
+
</head>
|
| 244 |
+
<body>
|
| 245 |
+
<!-- محتوا -->
|
| 246 |
+
|
| 247 |
+
<script src="/static/js/websocket-client.js"></script>
|
| 248 |
+
</body>
|
| 249 |
+
```
|
| 250 |
+
|
| 251 |
+
### 3. استفاده از Client
|
| 252 |
+
|
| 253 |
+
```javascript
|
| 254 |
+
// کلاینت به صورت خودکار متصل میشود
|
| 255 |
+
// در دسترس از طریق window.wsClient
|
| 256 |
+
|
| 257 |
+
// ثبت handler سفارشی
|
| 258 |
+
window.wsClient.on('custom_event', (message) => {
|
| 259 |
+
console.log('رویداد سفارشی:', message);
|
| 260 |
+
});
|
| 261 |
+
|
| 262 |
+
// اتصال به وضعیت اتصال
|
| 263 |
+
window.wsClient.onConnection((isConnected) => {
|
| 264 |
+
if (isConnected) {
|
| 265 |
+
console.log('✅ متصل شد');
|
| 266 |
+
} else {
|
| 267 |
+
console.log('❌ قطع شد');
|
| 268 |
+
}
|
| 269 |
+
});
|
| 270 |
+
|
| 271 |
+
// ارسال پیام
|
| 272 |
+
window.wsClient.send({
|
| 273 |
+
type: 'custom_action',
|
| 274 |
+
data: { value: 123 }
|
| 275 |
+
});
|
| 276 |
+
```
|
| 277 |
+
|
| 278 |
+
## 🔧 API Endpoints
|
| 279 |
+
|
| 280 |
+
### GET `/api/sessions`
|
| 281 |
+
دریافت لیست sessionهای فعال
|
| 282 |
+
|
| 283 |
+
**Response:**
|
| 284 |
+
```json
|
| 285 |
+
{
|
| 286 |
+
"sessions": {
|
| 287 |
+
"550e8400-...": {
|
| 288 |
+
"session_id": "550e8400-...",
|
| 289 |
+
"client_type": "browser",
|
| 290 |
+
"connected_at": "2024-01-15T10:00:00",
|
| 291 |
+
"last_activity": "2024-01-15T10:30:00"
|
| 292 |
+
}
|
| 293 |
+
},
|
| 294 |
+
"stats": {
|
| 295 |
+
"active_connections": 15,
|
| 296 |
+
"total_sessions": 23
|
| 297 |
+
}
|
| 298 |
+
}
|
| 299 |
+
```
|
| 300 |
+
|
| 301 |
+
### GET `/api/sessions/stats`
|
| 302 |
+
دریافت آمار اتصالات
|
| 303 |
+
|
| 304 |
+
**Response:**
|
| 305 |
+
```json
|
| 306 |
+
{
|
| 307 |
+
"active_connections": 15,
|
| 308 |
+
"total_sessions": 23,
|
| 309 |
+
"messages_sent": 1250,
|
| 310 |
+
"messages_received": 450,
|
| 311 |
+
"client_types": {
|
| 312 |
+
"browser": 12,
|
| 313 |
+
"api": 2
|
| 314 |
+
}
|
| 315 |
+
}
|
| 316 |
+
```
|
| 317 |
+
|
| 318 |
+
### POST `/api/broadcast`
|
| 319 |
+
ارسال پیام به همه کلاینتها
|
| 320 |
+
|
| 321 |
+
**Request:**
|
| 322 |
+
```json
|
| 323 |
+
{
|
| 324 |
+
"message": {
|
| 325 |
+
"type": "notification",
|
| 326 |
+
"text": "سیستم بهروز شد"
|
| 327 |
+
},
|
| 328 |
+
"group": "all"
|
| 329 |
+
}
|
| 330 |
+
```
|
| 331 |
+
|
| 332 |
+
## 🧪 تست
|
| 333 |
+
|
| 334 |
+
### 1. باز کردن صفحه تست:
|
| 335 |
+
```
|
| 336 |
+
http://localhost:8000/test_websocket.html
|
| 337 |
+
```
|
| 338 |
+
|
| 339 |
+
### 2. چک کردن اتصال:
|
| 340 |
+
- نوار بالای صفحه باید سبز شود (متصل)
|
| 341 |
+
- تعداد کاربران آنلاین باید نمایش داده شود
|
| 342 |
+
|
| 343 |
+
### 3. تست دستورات:
|
| 344 |
+
- کلیک روی دکمههای مختلف
|
| 345 |
+
- مشاهده لاگ پیامها در پنل پایین
|
| 346 |
+
|
| 347 |
+
### 4. تست چند تب:
|
| 348 |
+
- باز کردن چند تب مرورگر
|
| 349 |
+
- تعداد کاربران آنلاین باید افزایش یابد
|
| 350 |
+
|
| 351 |
+
## 📊 مانیتورینگ
|
| 352 |
+
|
| 353 |
+
### لاگهای سرور:
|
| 354 |
+
```bash
|
| 355 |
+
# مشاهده لاگهای WebSocket
|
| 356 |
+
tail -f logs/app.log | grep "WebSocket"
|
| 357 |
+
```
|
| 358 |
+
|
| 359 |
+
### متریکها:
|
| 360 |
+
- تعداد اتصالات فعال
|
| 361 |
+
- تعداد کل sessionها
|
| 362 |
+
- پیامهای ارسالی/دریافتی
|
| 363 |
+
- توزیع انواع کلاینت
|
| 364 |
+
|
| 365 |
+
## 🔒 امنیت
|
| 366 |
+
|
| 367 |
+
### توصیهها:
|
| 368 |
+
1. برای production از `wss://` (WebSocket Secure) استفاده کنید
|
| 369 |
+
2. محدودیت تعداد اتصال برای هر IP
|
| 370 |
+
3. Rate limiting برای پیامها
|
| 371 |
+
4. اعتبارسنجی token برای authentication
|
| 372 |
+
|
| 373 |
+
### مثال با Token:
|
| 374 |
+
```javascript
|
| 375 |
+
const ws = new WebSocket('ws://localhost:8000/ws');
|
| 376 |
+
ws.onopen = () => {
|
| 377 |
+
ws.send(JSON.stringify({
|
| 378 |
+
type: 'auth',
|
| 379 |
+
token: 'YOUR_JWT_TOKEN'
|
| 380 |
+
}));
|
| 381 |
+
};
|
| 382 |
+
```
|
| 383 |
+
|
| 384 |
+
## 🐛 عیبیابی
|
| 385 |
+
|
| 386 |
+
### مشکل: اتصال برقرار نمیشود
|
| 387 |
+
```bash
|
| 388 |
+
# چک کردن اجرای سرور
|
| 389 |
+
curl http://localhost:8000/health
|
| 390 |
+
|
| 391 |
+
# بررسی پورت
|
| 392 |
+
netstat -an | grep 8000
|
| 393 |
+
```
|
| 394 |
+
|
| 395 |
+
### مشکل: اتصال قطع میشود
|
| 396 |
+
- Heartbeat فعال است؟
|
| 397 |
+
- Proxy یا Firewall مشکل ندارد؟
|
| 398 |
+
- Logهای سرور را بررسی کنید
|
| 399 |
+
|
| 400 |
+
### مشکل: پیامها دریافت نمیشوند
|
| 401 |
+
- Subscribe کردهاید؟
|
| 402 |
+
- نوع پیام صحیح است؟
|
| 403 |
+
- کنسول مرورگر را بررسی کنید
|
| 404 |
+
|
| 405 |
+
## 📚 منابع بیشتر
|
| 406 |
+
|
| 407 |
+
- [WebSocket API - MDN](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket)
|
| 408 |
+
- [FastAPI WebSockets](https://fastapi.tiangolo.com/advanced/websockets/)
|
| 409 |
+
- [websockets Python library](https://websockets.readthedocs.io/)
|
| 410 |
+
|
| 411 |
+
## 🎓 مثال کامل Integration
|
| 412 |
+
|
| 413 |
+
```html
|
| 414 |
+
<!DOCTYPE html>
|
| 415 |
+
<html lang="fa" dir="rtl">
|
| 416 |
+
<head>
|
| 417 |
+
<link rel="stylesheet" href="/static/css/connection-status.css">
|
| 418 |
+
</head>
|
| 419 |
+
<body>
|
| 420 |
+
<!-- UI Components -->
|
| 421 |
+
<div class="connection-status-bar" id="ws-connection-status">
|
| 422 |
+
<!-- ... -->
|
| 423 |
+
</div>
|
| 424 |
+
|
| 425 |
+
<div class="dashboard">
|
| 426 |
+
<h1>تعداد کاربران: <span id="user-count">0</span></h1>
|
| 427 |
+
</div>
|
| 428 |
+
|
| 429 |
+
<script src="/static/js/websocket-client.js"></script>
|
| 430 |
+
<script>
|
| 431 |
+
// Custom logic
|
| 432 |
+
if (window.wsClient) {
|
| 433 |
+
window.wsClient.on('stats_update', (msg) => {
|
| 434 |
+
document.getElementById('user-count').textContent =
|
| 435 |
+
msg.data.active_connections;
|
| 436 |
+
});
|
| 437 |
+
}
|
| 438 |
+
</script>
|
| 439 |
+
</body>
|
| 440 |
+
</html>
|
| 441 |
+
```
|
| 442 |
+
|
| 443 |
+
---
|
| 444 |
+
|
| 445 |
+
**نکته مهم:** این سیستم به صورت خودکار reconnect میکند و نیازی به مدیریت دستی ندارید!
|
| 446 |
+
|
docs/deployment/DEPLOYMENT.md
ADDED
|
@@ -0,0 +1,438 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🌐 راهنمای استقرار (Deployment Guide)
|
| 2 |
+
|
| 3 |
+
این فایل شامل دستورالعمل کامل برای استقرار داشبورد کریپتو در پلتفرمهای مختلف است.
|
| 4 |
+
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
## 📋 فهرست
|
| 8 |
+
|
| 9 |
+
1. [Hugging Face Spaces](#1-hugging-face-spaces)
|
| 10 |
+
2. [Railway.app](#2-railwayapp)
|
| 11 |
+
3. [Render.com](#3-rendercom)
|
| 12 |
+
4. [Oracle Cloud (رایگان)](#4-oracle-cloud-رایگان)
|
| 13 |
+
5. [Vercel](#5-vercel)
|
| 14 |
+
6. [Docker (محلی)](#6-docker-محلی)
|
| 15 |
+
7. [VPS / سرور اختصاصی](#7-vps--سرور-اختصاصی)
|
| 16 |
+
|
| 17 |
+
---
|
| 18 |
+
|
| 19 |
+
## 1. Hugging Face Spaces
|
| 20 |
+
|
| 21 |
+
### 🎯 مزایا
|
| 22 |
+
- ✅ رایگان
|
| 23 |
+
- ✅ راهاندازی سریع
|
| 24 |
+
- ✅ URL عمومی
|
| 25 |
+
- ✅ مناسب برای demo
|
| 26 |
+
|
| 27 |
+
### 📝 مراحل استقرار
|
| 28 |
+
|
| 29 |
+
#### روش 1: استفاده از Docker (توصیه میشود)
|
| 30 |
+
|
| 31 |
+
1. **ایجاد Space جدید**
|
| 32 |
+
- به [huggingface.co/spaces](https://huggingface.co/spaces) بروید
|
| 33 |
+
- روی "Create new Space" کلیک کنید
|
| 34 |
+
- نام Space را وارد کنید
|
| 35 |
+
- SDK را روی **Docker** تنظیم کنید
|
| 36 |
+
|
| 37 |
+
2. **آپلود فایلها**
|
| 38 |
+
```bash
|
| 39 |
+
git clone https://huggingface.co/spaces/YOUR_USERNAME/YOUR_SPACE
|
| 40 |
+
cd YOUR_SPACE
|
| 41 |
+
|
| 42 |
+
# کپی فایلهای پروژه
|
| 43 |
+
cp -r crypto_dashboard/* .
|
| 44 |
+
|
| 45 |
+
git add .
|
| 46 |
+
git commit -m "Initial commit"
|
| 47 |
+
git push
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
3. **تنظیم Port**
|
| 51 |
+
در فایل `Dockerfile` مطمئن شوید که port 7860 استفاده میشود:
|
| 52 |
+
```dockerfile
|
| 53 |
+
CMD ["python", "app.py"]
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
#### روش 2: بدون Docker
|
| 57 |
+
|
| 58 |
+
1. ایجاد فایل `app.py` در روت
|
| 59 |
+
2. ایجاد پوشه `templates/` و قرار دادن `index.html`
|
| 60 |
+
3. ایجاد `requirements.txt`
|
| 61 |
+
4. Push به repository
|
| 62 |
+
|
| 63 |
+
### ⚙️ تنظیمات
|
| 64 |
+
|
| 65 |
+
در تب Settings:
|
| 66 |
+
- **Hardware**: CPU basic (رایگان)
|
| 67 |
+
- **Port**: 7860
|
| 68 |
+
- **Sleep Time**: 48 hours (برای free tier)
|
| 69 |
+
|
| 70 |
+
### 🔗 نتیجه
|
| 71 |
+
Space شما در آدرس زیر در دسترس خواهد بود:
|
| 72 |
+
```
|
| 73 |
+
https://huggingface.co/spaces/YOUR_USERNAME/YOUR_SPACE
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
---
|
| 77 |
+
|
| 78 |
+
## 2. Railway.app
|
| 79 |
+
|
| 80 |
+
### 🎯 مزایا
|
| 81 |
+
- ✅ Free tier سخاوتمندانه ($5 credit/month)
|
| 82 |
+
- ✅ Deploy خودکار از Git
|
| 83 |
+
- ✅ Custom domain رایگان
|
| 84 |
+
- ✅ Logs و Monitoring
|
| 85 |
+
|
| 86 |
+
### 📝 مراحل استقرار
|
| 87 |
+
|
| 88 |
+
1. **ثبت نام**
|
| 89 |
+
- به [railway.app](https://railway.app) بروید
|
| 90 |
+
- Sign up با GitHub
|
| 91 |
+
|
| 92 |
+
2. **Deploy از GitHub**
|
| 93 |
+
```bash
|
| 94 |
+
# Push پروژه به GitHub
|
| 95 |
+
git init
|
| 96 |
+
git add .
|
| 97 |
+
git commit -m "Initial commit"
|
| 98 |
+
git push origin main
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
3. **ایجاد Project در Railway**
|
| 102 |
+
- New Project
|
| 103 |
+
- Deploy from GitHub repo
|
| 104 |
+
- انتخاب repository
|
| 105 |
+
|
| 106 |
+
4. **تنظیمات (اختیاری)**
|
| 107 |
+
```bash
|
| 108 |
+
# متغیرهای محیطی
|
| 109 |
+
PORT=7860
|
| 110 |
+
HOST=0.0.0.0
|
| 111 |
+
```
|
| 112 |
+
|
| 113 |
+
5. **Deploy**
|
| 114 |
+
- Railway به صورت خودکار deploy میکند
|
| 115 |
+
- URL عمومی دریافت میکنید
|
| 116 |
+
|
| 117 |
+
### 💰 هزینه
|
| 118 |
+
- Free tier: $5 credit/month (کافی برای این پروژه)
|
| 119 |
+
- پس از اتمام: $5-10/month
|
| 120 |
+
|
| 121 |
+
---
|
| 122 |
+
|
| 123 |
+
## 3. Render.com
|
| 124 |
+
|
| 125 |
+
### 🎯 مزایا
|
| 126 |
+
- ✅ Free tier
|
| 127 |
+
- ✅ راهاندازی ساده
|
| 128 |
+
- ✅ SSL رایگان
|
| 129 |
+
- ✅ Auto-deploy
|
| 130 |
+
|
| 131 |
+
### 📝 مراحل استقرار
|
| 132 |
+
|
| 133 |
+
1. **ثبت نام**
|
| 134 |
+
- [render.com](https://render.com)
|
| 135 |
+
|
| 136 |
+
2. **New Web Service**
|
| 137 |
+
- Connect GitHub repository
|
| 138 |
+
- یا Manual Deploy
|
| 139 |
+
|
| 140 |
+
3. **تنظیمات**
|
| 141 |
+
```yaml
|
| 142 |
+
Name: crypto-dashboard
|
| 143 |
+
Environment: Python 3
|
| 144 |
+
Build Command: pip install -r requirements.txt
|
| 145 |
+
Start Command: python app.py
|
| 146 |
+
```
|
| 147 |
+
|
| 148 |
+
4. **Environment Variables**
|
| 149 |
+
```
|
| 150 |
+
PORT=7860
|
| 151 |
+
```
|
| 152 |
+
|
| 153 |
+
5. **Deploy**
|
| 154 |
+
- Create Web Service
|
| 155 |
+
|
| 156 |
+
### ⚠️ نکته
|
| 157 |
+
Free tier ممکن است پس از مدتی inactive شود (sleep mode)
|
| 158 |
+
|
| 159 |
+
---
|
| 160 |
+
|
| 161 |
+
## 4. Oracle Cloud (رایگان)
|
| 162 |
+
|
| 163 |
+
### 🎯 مزایا
|
| 164 |
+
- ✅ رایگان برای همیشه
|
| 165 |
+
- ✅ 2 VM instances
|
| 166 |
+
- ✅ 1GB RAM هر کدام
|
| 167 |
+
- ✅ 100GB storage
|
| 168 |
+
|
| 169 |
+
### 📝 مراحل استقرار
|
| 170 |
+
|
| 171 |
+
1. **ثبت نام در Oracle Cloud**
|
| 172 |
+
- [cloud.oracle.com](https://cloud.oracle.com)
|
| 173 |
+
- نیاز به کارت اعتباری (شارژ نمیشود)
|
| 174 |
+
|
| 175 |
+
2. **ایجاد VM Instance**
|
| 176 |
+
- Compute > Instances > Create Instance
|
| 177 |
+
- Shape: VM.Standard.E2.1.Micro (Free)
|
| 178 |
+
- Image: Ubuntu 22.04
|
| 179 |
+
|
| 180 |
+
3. **نصب Python**
|
| 181 |
+
```bash
|
| 182 |
+
ssh ubuntu@YOUR_VM_IP
|
| 183 |
+
|
| 184 |
+
sudo apt update
|
| 185 |
+
sudo apt install python3 python3-pip -y
|
| 186 |
+
```
|
| 187 |
+
|
| 188 |
+
4. **Deploy پروژه**
|
| 189 |
+
```bash
|
| 190 |
+
# آپلود فایلها
|
| 191 |
+
scp -r crypto_dashboard ubuntu@YOUR_VM_IP:~/
|
| 192 |
+
|
| 193 |
+
# SSH به سرور
|
| 194 |
+
ssh ubuntu@YOUR_VM_IP
|
| 195 |
+
|
| 196 |
+
cd crypto_dashboard
|
| 197 |
+
pip3 install -r requirements.txt
|
| 198 |
+
|
| 199 |
+
# اجرا
|
| 200 |
+
python3 app.py
|
| 201 |
+
```
|
| 202 |
+
|
| 203 |
+
5. **نصب به عنوان Service**
|
| 204 |
+
```bash
|
| 205 |
+
sudo nano /etc/systemd/system/crypto-dashboard.service
|
| 206 |
+
```
|
| 207 |
+
|
| 208 |
+
محتوا:
|
| 209 |
+
```ini
|
| 210 |
+
[Unit]
|
| 211 |
+
Description=Crypto Dashboard
|
| 212 |
+
After=network.target
|
| 213 |
+
|
| 214 |
+
[Service]
|
| 215 |
+
User=ubuntu
|
| 216 |
+
WorkingDirectory=/home/ubuntu/crypto_dashboard
|
| 217 |
+
ExecStart=/usr/bin/python3 /home/ubuntu/crypto_dashboard/app.py
|
| 218 |
+
Restart=always
|
| 219 |
+
|
| 220 |
+
[Install]
|
| 221 |
+
WantedBy=multi-user.target
|
| 222 |
+
```
|
| 223 |
+
|
| 224 |
+
فعالسازی:
|
| 225 |
+
```bash
|
| 226 |
+
sudo systemctl enable crypto-dashboard
|
| 227 |
+
sudo systemctl start crypto-dashboard
|
| 228 |
+
```
|
| 229 |
+
|
| 230 |
+
6. **باز کردن Port**
|
| 231 |
+
- Networking > Virtual Cloud Networks
|
| 232 |
+
- Security Lists > Add Ingress Rule
|
| 233 |
+
- Port: 7860
|
| 234 |
+
|
| 235 |
+
### 🔗 دسترسی
|
| 236 |
+
```
|
| 237 |
+
http://YOUR_VM_IP:7860
|
| 238 |
+
```
|
| 239 |
+
|
| 240 |
+
---
|
| 241 |
+
|
| 242 |
+
## 5. Vercel
|
| 243 |
+
|
| 244 |
+
### 🎯 مزایا
|
| 245 |
+
- ✅ رایگان
|
| 246 |
+
- ✅ سریع
|
| 247 |
+
- ✅ Custom domain
|
| 248 |
+
|
| 249 |
+
### ⚠️ محدودیت
|
| 250 |
+
Vercel برای Serverless Functions طراحی شده، برای FastAPI نیاز به تنظیمات اضافی دارد.
|
| 251 |
+
|
| 252 |
+
### 📝 نیاز به:
|
| 253 |
+
1. ایجاد `vercel.json`
|
| 254 |
+
2. استفاده از `@vercel/python`
|
| 255 |
+
3. تبدیل به Serverless Functions
|
| 256 |
+
|
| 257 |
+
**توصیه**: برای این پروژه از Railway یا Render استفاده کنید.
|
| 258 |
+
|
| 259 |
+
---
|
| 260 |
+
|
| 261 |
+
## 6. Docker (محلی)
|
| 262 |
+
|
| 263 |
+
### 📝 مراحل
|
| 264 |
+
|
| 265 |
+
1. **Build Image**
|
| 266 |
+
```bash
|
| 267 |
+
docker build -t crypto-dashboard .
|
| 268 |
+
```
|
| 269 |
+
|
| 270 |
+
2. **Run Container**
|
| 271 |
+
```bash
|
| 272 |
+
docker run -p 7860:7860 crypto-dashboard
|
| 273 |
+
```
|
| 274 |
+
|
| 275 |
+
3. **با Docker Compose**
|
| 276 |
+
|
| 277 |
+
ایجاد `docker-compose.yml`:
|
| 278 |
+
```yaml
|
| 279 |
+
version: '3.8'
|
| 280 |
+
services:
|
| 281 |
+
crypto-dashboard:
|
| 282 |
+
build: .
|
| 283 |
+
ports:
|
| 284 |
+
- "7860:7860"
|
| 285 |
+
restart: always
|
| 286 |
+
```
|
| 287 |
+
|
| 288 |
+
اجرا:
|
| 289 |
+
```bash
|
| 290 |
+
docker-compose up -d
|
| 291 |
+
```
|
| 292 |
+
|
| 293 |
+
---
|
| 294 |
+
|
| 295 |
+
## 7. VPS / سرور اختصاصی
|
| 296 |
+
|
| 297 |
+
### 📝 مراحل (Ubuntu/Debian)
|
| 298 |
+
|
| 299 |
+
1. **نصب Dependencies**
|
| 300 |
+
```bash
|
| 301 |
+
sudo apt update
|
| 302 |
+
sudo apt install python3 python3-pip nginx -y
|
| 303 |
+
```
|
| 304 |
+
|
| 305 |
+
2. **آپلود پروژه**
|
| 306 |
+
```bash
|
| 307 |
+
cd /opt
|
| 308 |
+
sudo git clone YOUR_REPO
|
| 309 |
+
cd crypto_dashboard
|
| 310 |
+
sudo pip3 install -r requirements.txt
|
| 311 |
+
```
|
| 312 |
+
|
| 313 |
+
3. **ایجاد Systemd Service**
|
| 314 |
+
```bash
|
| 315 |
+
sudo nano /etc/systemd/system/crypto-dashboard.service
|
| 316 |
+
```
|
| 317 |
+
|
| 318 |
+
محتوا:
|
| 319 |
+
```ini
|
| 320 |
+
[Unit]
|
| 321 |
+
Description=Crypto Dashboard API
|
| 322 |
+
After=network.target
|
| 323 |
+
|
| 324 |
+
[Service]
|
| 325 |
+
Type=simple
|
| 326 |
+
User=www-data
|
| 327 |
+
WorkingDirectory=/opt/crypto_dashboard
|
| 328 |
+
ExecStart=/usr/bin/python3 /opt/crypto_dashboard/app.py
|
| 329 |
+
Restart=always
|
| 330 |
+
|
| 331 |
+
[Install]
|
| 332 |
+
WantedBy=multi-user.target
|
| 333 |
+
```
|
| 334 |
+
|
| 335 |
+
4. **تنظیم Nginx (اختیاری)**
|
| 336 |
+
```nginx
|
| 337 |
+
server {
|
| 338 |
+
listen 80;
|
| 339 |
+
server_name yourdomain.com;
|
| 340 |
+
|
| 341 |
+
location / {
|
| 342 |
+
proxy_pass http://127.0.0.1:7860;
|
| 343 |
+
proxy_set_header Host $host;
|
| 344 |
+
proxy_set_header X-Real-IP $remote_addr;
|
| 345 |
+
}
|
| 346 |
+
}
|
| 347 |
+
```
|
| 348 |
+
|
| 349 |
+
5. **فعالسازی**
|
| 350 |
+
```bash
|
| 351 |
+
sudo systemctl enable crypto-dashboard
|
| 352 |
+
sudo systemctl start crypto-dashboard
|
| 353 |
+
sudo systemctl enable nginx
|
| 354 |
+
sudo systemctl restart nginx
|
| 355 |
+
```
|
| 356 |
+
|
| 357 |
+
---
|
| 358 |
+
|
| 359 |
+
## 📊 مقایسه پلتفرمها
|
| 360 |
+
|
| 361 |
+
| پلتفرم | رایگان | راحتی | سرعت | Custom Domain | مناسب برای |
|
| 362 |
+
|--------|--------|-------|------|---------------|-----------|
|
| 363 |
+
| **Hugging Face** | ✅ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ❌ | Demo, Testing |
|
| 364 |
+
| **Railway** | 💵 Limited | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ✅ | Production |
|
| 365 |
+
| **Render** | ✅ Limited | ⭐⭐⭐⭐ | ⭐⭐⭐ | ✅ | Production |
|
| 366 |
+
| **Oracle Cloud** | ✅ | ⭐⭐⭐ | ⭐⭐⭐⭐ | ✅ | Production |
|
| 367 |
+
| **VPS** | 💵 | ⭐⭐ | ⭐⭐⭐⭐⭐ | ✅ | Production |
|
| 368 |
+
|
| 369 |
+
---
|
| 370 |
+
|
| 371 |
+
## 🎯 توصیه بر اساس نیاز
|
| 372 |
+
|
| 373 |
+
### برای Demo و Testing
|
| 374 |
+
→ **Hugging Face Spaces** 🏆
|
| 375 |
+
|
| 376 |
+
### برای Production با بودجه کم
|
| 377 |
+
→ **Oracle Cloud** (رایگان) یا **Render.com**
|
| 378 |
+
|
| 379 |
+
### برای Production حرفهای
|
| 380 |
+
→ **Railway.app** یا **VPS**
|
| 381 |
+
|
| 382 |
+
### برای Maximum Performance
|
| 383 |
+
→ **VPS اختصاصی** با Nginx
|
| 384 |
+
|
| 385 |
+
---
|
| 386 |
+
|
| 387 |
+
## 🔧 نکات عمومی
|
| 388 |
+
|
| 389 |
+
### SSL Certificate (HTTPS)
|
| 390 |
+
```bash
|
| 391 |
+
# با Certbot (Let's Encrypt)
|
| 392 |
+
sudo apt install certbot python3-certbot-nginx
|
| 393 |
+
sudo certbot --nginx -d yourdomain.com
|
| 394 |
+
```
|
| 395 |
+
|
| 396 |
+
### Monitoring
|
| 397 |
+
```bash
|
| 398 |
+
# لاگها
|
| 399 |
+
sudo journalctl -u crypto-dashboard -f
|
| 400 |
+
|
| 401 |
+
# وضعیت سرویس
|
| 402 |
+
sudo systemctl status crypto-dashboard
|
| 403 |
+
```
|
| 404 |
+
|
| 405 |
+
### Updates
|
| 406 |
+
```bash
|
| 407 |
+
cd crypto_dashboard
|
| 408 |
+
git pull
|
| 409 |
+
sudo systemctl restart crypto-dashboard
|
| 410 |
+
```
|
| 411 |
+
|
| 412 |
+
---
|
| 413 |
+
|
| 414 |
+
## ❓ سوالات متداول
|
| 415 |
+
|
| 416 |
+
**Q: چرا پس از deploy سایت کار نمیکند؟**
|
| 417 |
+
A: Port را چک کنید (باید 7860 باشد) و Logs را بررسی کنید
|
| 418 |
+
|
| 419 |
+
**Q: چگونه Custom Domain اضافه کنم؟**
|
| 420 |
+
A: در Settings پلتفرم خود، Custom Domain را تنظیم کنید
|
| 421 |
+
|
| 422 |
+
**Q: چرا سرعت کند است؟**
|
| 423 |
+
A: Cache را فعال کنید و CDN استفاده کنید
|
| 424 |
+
|
| 425 |
+
**Q: چگونه Database اضافه کنم؟**
|
| 426 |
+
A: SQLite (محلی) یا PostgreSQL (cloud) را اضافه کنید
|
| 427 |
+
|
| 428 |
+
---
|
| 429 |
+
|
| 430 |
+
## 📞 پشتیبانی
|
| 431 |
+
|
| 432 |
+
اگر در استقرار مشکل دارید:
|
| 433 |
+
1. Logs را بررسی کنید
|
| 434 |
+
2. Port و Firewall را چک کنید
|
| 435 |
+
3. Dependencies را دوباره نصب کنید
|
| 436 |
+
4. Issue باز کنید
|
| 437 |
+
|
| 438 |
+
**موفق باشید! 🚀**
|
docs/deployment/SET_HF_TOKEN.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# تنظیم توکن Hugging Face
|
| 2 |
+
|
| 3 |
+
## توکن شما:
|
| 4 |
+
```
|
| 5 |
+
<HF_TOKEN_FROM_SPACE_SECRET>
|
| 6 |
+
```
|
| 7 |
+
|
| 8 |
+
## روشهای تنظیم:
|
| 9 |
+
|
| 10 |
+
### 1. روی Hugging Face Space (توصیه شده):
|
| 11 |
+
|
| 12 |
+
1. به Space خود بروید
|
| 13 |
+
2. بروید به **Settings** → **Repository secrets**
|
| 14 |
+
3. دو secret اضافه کنید:
|
| 15 |
+
|
| 16 |
+
**Secret 1:**
|
| 17 |
+
- Name: `HF_TOKEN`
|
| 18 |
+
- Value: `<HF_TOKEN_FROM_SPACE_SECRET>`
|
| 19 |
+
|
| 20 |
+
**Secret 2:**
|
| 21 |
+
- Name: `HF_MODE`
|
| 22 |
+
- Value: `public`
|
| 23 |
+
|
| 24 |
+
4. Space را Restart کنید
|
| 25 |
+
|
| 26 |
+
---
|
| 27 |
+
|
| 28 |
+
### 2. روی Windows (Local):
|
| 29 |
+
|
| 30 |
+
در PowerShell:
|
| 31 |
+
```powershell
|
| 32 |
+
$env:HF_TOKEN="<HF_TOKEN_FROM_SPACE_SECRET>"
|
| 33 |
+
$env:HF_MODE="public"
|
| 34 |
+
python api_server_extended.py
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
یا برای دائمی کردن، در System Environment Variables:
|
| 38 |
+
1. Win + R → `sysdm.cpl` → Advanced → Environment Variables
|
| 39 |
+
2. در User variables، New کنید:
|
| 40 |
+
- Name: `HF_TOKEN`
|
| 41 |
+
- Value: `<HF_TOKEN_FROM_SPACE_SECRET>`
|
| 42 |
+
3. یکی دیگر:
|
| 43 |
+
- Name: `HF_MODE`
|
| 44 |
+
- Value: `public`
|
| 45 |
+
|
| 46 |
+
---
|
| 47 |
+
|
| 48 |
+
### 3. روی Linux/Mac (Local):
|
| 49 |
+
|
| 50 |
+
در terminal:
|
| 51 |
+
```bash
|
| 52 |
+
export HF_TOKEN="<HF_TOKEN_FROM_SPACE_SECRET>"
|
| 53 |
+
export HF_MODE="public"
|
| 54 |
+
python api_server_extended.py
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
یا در `~/.bashrc` یا `~/.zshrc` اضافه کنید:
|
| 58 |
+
```bash
|
| 59 |
+
export HF_TOKEN="<HF_TOKEN_FROM_SPACE_SECRET>"
|
| 60 |
+
export HF_MODE="public"
|
| 61 |
+
```
|
| 62 |
+
|
| 63 |
+
---
|
| 64 |
+
|
| 65 |
+
### 4. با فایل .env:
|
| 66 |
+
|
| 67 |
+
فایل `.env` در root پروژه ایجاد کنید:
|
| 68 |
+
```
|
| 69 |
+
HF_TOKEN=<HF_TOKEN_FROM_SPACE_SECRET>
|
| 70 |
+
HF_MODE=public
|
| 71 |
+
PORT=7860
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
سپس در PowerShell قبل از اجرا:
|
| 75 |
+
```powershell
|
| 76 |
+
Get-Content .env | ForEach-Object {
|
| 77 |
+
if ($_ -match '^([^=]+)=(.*)$') {
|
| 78 |
+
[Environment]::SetEnvironmentVariable($matches[1], $matches[2], 'Process')
|
| 79 |
+
}
|
| 80 |
+
}
|
| 81 |
+
python api_server_extended.py
|
| 82 |
+
```
|
| 83 |
+
|
| 84 |
+
---
|
| 85 |
+
|
| 86 |
+
## بررسی تنظیمات:
|
| 87 |
+
|
| 88 |
+
پس از تنظیم، بررسی کنید:
|
| 89 |
+
```powershell
|
| 90 |
+
python -c "import os; print('HF_TOKEN:', 'SET' if os.getenv('HF_TOKEN') else 'NOT SET'); print('HF_MODE:', os.getenv('HF_MODE', 'not set'))"
|
| 91 |
+
```
|
| 92 |
+
|
| 93 |
+
یا با تست:
|
| 94 |
+
```powershell
|
| 95 |
+
python test_fixes.py
|
| 96 |
+
```
|
| 97 |
+
|
| 98 |
+
---
|
| 99 |
+
|
| 100 |
+
## نکته امنیتی:
|
| 101 |
+
⚠️ **این توکن را public نکنید!**
|
| 102 |
+
- در git commit نکنید
|
| 103 |
+
- در GitHub/GitLab share نکنید
|
| 104 |
+
- فقط در Secrets استفاده کنید
|
| 105 |
+
|
docs/persian/REALTIME_FEATURES_FA.md
CHANGED
|
@@ -1,374 +1,374 @@
|
|
| 1 |
-
# 🚀 ویژگیهای بلادرنگ سیستم مانیتورینگ کریپتو
|
| 2 |
-
|
| 3 |
-
## ✨ چه چیزی اضافه شد؟
|
| 4 |
-
|
| 5 |
-
### 1. 📡 سیستم WebSocket کامل
|
| 6 |
-
|
| 7 |
-
**قبل (HTTP Polling):**
|
| 8 |
-
```
|
| 9 |
-
کلاینت → درخواست HTTP → سرور
|
| 10 |
-
← پاسخ HTTP ←
|
| 11 |
-
(تکرار هر 1-5 ثانیه) ⏱️
|
| 12 |
-
```
|
| 13 |
-
|
| 14 |
-
**الان (WebSocket):**
|
| 15 |
-
```
|
| 16 |
-
کلاینت ⟷ اتصال دائمی ⟷ سرور
|
| 17 |
-
← داده لحظهای ←
|
| 18 |
-
(فوری و بدون تاخیر! ⚡)
|
| 19 |
-
```
|
| 20 |
-
|
| 21 |
-
### 2. 👥 نمایش تعداد کاربران آنلاین
|
| 22 |
-
|
| 23 |
-
برنامه الان میتواند **بلافاصله** به شما نشان دهد:
|
| 24 |
-
- چند نفر الان متصل هستند
|
| 25 |
-
- چند جلسه (session) فعال است
|
| 26 |
-
- چه نوع کلاینتهایی متصلاند (مرورگر، API، موبایل)
|
| 27 |
-
|
| 28 |
-
### 3. 🎨 رابط کاربری زیبا و هوشمند
|
| 29 |
-
|
| 30 |
-
- **نوار وضعیت بالای صفحه** با نمایش:
|
| 31 |
-
- وضعیت اتصال (متصل/قطع شده) با نقطه رنگی
|
| 32 |
-
- تعداد کاربران آنلاین به صورت زنده
|
| 33 |
-
- آمار جلسات کلی
|
| 34 |
-
|
| 35 |
-
- **انیمیشنهای جذاب**:
|
| 36 |
-
- هنگام تغییر تعداد کاربران
|
| 37 |
-
- هنگام اتصال/قطع اتصال
|
| 38 |
-
- پالس نقطه وضعیت
|
| 39 |
-
|
| 40 |
-
- **reconnect خودکار**:
|
| 41 |
-
- اگر اتصال قطع شد، خودکار دوباره وصل میشود
|
| 42 |
-
- نیازی به refresh صفحه نیست!
|
| 43 |
-
|
| 44 |
-
## 🎯 چرا این تغییرات مهم است؟
|
| 45 |
-
|
| 46 |
-
### سرعت 10 برابر بیشتر! ⚡
|
| 47 |
-
|
| 48 |
-
| عملیات | HTTP Polling | WebSocket |
|
| 49 |
-
|--------|--------------|-----------|
|
| 50 |
-
| بهروزرسانی قیمت | 2-5 ثانیه | < 100ms |
|
| 51 |
-
| نمایش کاربران | هر 3 ثانیه | فوری |
|
| 52 |
-
| مصرف سرور | 100% | 10% |
|
| 53 |
-
| پهنای باند | زیاد | خیلی کم |
|
| 54 |
-
|
| 55 |
-
### Session Management حرفهای 🔐
|
| 56 |
-
|
| 57 |
-
هر کاربر یک **Session ID** منحصر به فرد دارد:
|
| 58 |
-
```json
|
| 59 |
-
{
|
| 60 |
-
"session_id": "
|
| 61 |
-
"client_type": "browser",
|
| 62 |
-
"connected_at": "2024-01-15T10:00:00",
|
| 63 |
-
"metadata": { "source": "unified_dashboard" }
|
| 64 |
-
}
|
| 65 |
-
```
|
| 66 |
-
|
| 67 |
-
## 📂 فایلهای جدید
|
| 68 |
-
|
| 69 |
-
### Backend (سرور):
|
| 70 |
-
```
|
| 71 |
-
backend/services/
|
| 72 |
-
├── connection_manager.py ← مدیریت اتصالات WebSocket
|
| 73 |
-
└── auto_discovery_service.py ← کشف خودکار منابع جدید
|
| 74 |
-
|
| 75 |
-
api_server_extended.py ← بهروزرسانی شده با WebSocket
|
| 76 |
-
```
|
| 77 |
-
|
| 78 |
-
### Frontend (رابط کاربری):
|
| 79 |
-
```
|
| 80 |
-
static/
|
| 81 |
-
├── js/
|
| 82 |
-
│ └── websocket-client.js ← کلاینت WebSocket هوشمند
|
| 83 |
-
└── css/
|
| 84 |
-
└── connection-status.css ← استایلهای زیبا
|
| 85 |
-
|
| 86 |
-
test_websocket.html ← صفحه تست کامل
|
| 87 |
-
```
|
| 88 |
-
|
| 89 |
-
### مستندات:
|
| 90 |
-
```
|
| 91 |
-
WEBSOCKET_GUIDE.md ← راهنمای کامل WebSocket
|
| 92 |
-
REALTIME_FEATURES_FA.md ← این فایل!
|
| 93 |
-
```
|
| 94 |
-
|
| 95 |
-
## 🚀 نحوه استفاده
|
| 96 |
-
|
| 97 |
-
### 1. راهاندازی سرور:
|
| 98 |
-
|
| 99 |
-
```bash
|
| 100 |
-
# نصب وابستگیهای جدید
|
| 101 |
-
pip install -r requirements.txt
|
| 102 |
-
|
| 103 |
-
# اجرای سرور
|
| 104 |
-
python api_server_extended.py
|
| 105 |
-
```
|
| 106 |
-
|
| 107 |
-
### 2. باز کردن صفحه تست:
|
| 108 |
-
|
| 109 |
-
```
|
| 110 |
-
http://localhost:8000/test_websocket.html
|
| 111 |
-
```
|
| 112 |
-
|
| 113 |
-
### 3. مشاهده نتایج:
|
| 114 |
-
|
| 115 |
-
- ✅ نوار بالا باید **سبز** شود
|
| 116 |
-
- 👥 تعداد کاربران باید نمایش داده شود
|
| 117 |
-
- 📊 آمار به صورت **لحظهای** آپدیت میشود
|
| 118 |
-
|
| 119 |
-
### 4. تست با چند تب:
|
| 120 |
-
|
| 121 |
-
1. صفحه را در چند تب باز کنید
|
| 122 |
-
2. تعداد کاربران آنلاین **فوراً** افزایش مییابد
|
| 123 |
-
3. یک تب را ببندید → تعداد کاربران کم میشود
|
| 124 |
-
|
| 125 |
-
## 🎮 ویژگیهای پیشرفته
|
| 126 |
-
|
| 127 |
-
### Subscribe به کانالهای مختلف:
|
| 128 |
-
|
| 129 |
-
```javascript
|
| 130 |
-
// فقط اطلاعات بازار
|
| 131 |
-
wsClient.subscribe('market');
|
| 132 |
-
|
| 133 |
-
// فقط قیمتها
|
| 134 |
-
wsClient.subscribe('prices');
|
| 135 |
-
|
| 136 |
-
// فقط اخبار
|
| 137 |
-
wsClient.subscribe('news');
|
| 138 |
-
|
| 139 |
-
// همه چیز
|
| 140 |
-
wsClient.subscribe('all');
|
| 141 |
-
```
|
| 142 |
-
|
| 143 |
-
### دریافت آمار فوری:
|
| 144 |
-
|
| 145 |
-
```javascript
|
| 146 |
-
// درخواست آمار
|
| 147 |
-
wsClient.requestStats();
|
| 148 |
-
|
| 149 |
-
// پاسخ در کمتر از 100ms:
|
| 150 |
-
{
|
| 151 |
-
"active_connections": 15,
|
| 152 |
-
"total_sessions": 23,
|
| 153 |
-
"client_types": {
|
| 154 |
-
"browser": 12,
|
| 155 |
-
"api": 2,
|
| 156 |
-
"mobile": 1
|
| 157 |
-
}
|
| 158 |
-
}
|
| 159 |
-
```
|
| 160 |
-
|
| 161 |
-
### Handler سفارشی:
|
| 162 |
-
|
| 163 |
-
```javascript
|
| 164 |
-
// ثبت handler برای رویداد خاص
|
| 165 |
-
wsClient.on('price_update', (message) => {
|
| 166 |
-
console.log('قیمت جدید:', message.data);
|
| 167 |
-
updateUI(message.data);
|
| 168 |
-
});
|
| 169 |
-
```
|
| 170 |
-
|
| 171 |
-
## 📊 مثال کاربردی
|
| 172 |
-
|
| 173 |
-
### نمایش تعداد کاربران در صفحه خودتان:
|
| 174 |
-
|
| 175 |
-
```html
|
| 176 |
-
<!DOCTYPE html>
|
| 177 |
-
<html lang="fa" dir="rtl">
|
| 178 |
-
<head>
|
| 179 |
-
<link rel="stylesheet" href="/static/css/connection-status.css">
|
| 180 |
-
</head>
|
| 181 |
-
<body>
|
| 182 |
-
<!-- نوار وضعیت -->
|
| 183 |
-
<div class="connection-status-bar" id="ws-connection-status">
|
| 184 |
-
<div class="ws-connection-info">
|
| 185 |
-
<span class="status-dot" id="ws-status-dot"></span>
|
| 186 |
-
<span id="ws-status-text">در حال اتصال...</span>
|
| 187 |
-
</div>
|
| 188 |
-
|
| 189 |
-
<div class="online-users-widget">
|
| 190 |
-
<span class="users-icon">👥</span>
|
| 191 |
-
<span class="count-number" id="active-users-count">0</span>
|
| 192 |
-
<span class="count-label">کاربر آنلاین</span>
|
| 193 |
-
</div>
|
| 194 |
-
</div>
|
| 195 |
-
|
| 196 |
-
<!-- محتوای اصلی شما -->
|
| 197 |
-
<div class="container">
|
| 198 |
-
<h1>داشبورد من</h1>
|
| 199 |
-
<!-- ... -->
|
| 200 |
-
</div>
|
| 201 |
-
|
| 202 |
-
<!-- اضافه کردن WebSocket Client -->
|
| 203 |
-
<script src="/static/js/websocket-client.js"></script>
|
| 204 |
-
<script>
|
| 205 |
-
// همین! دیگر نیازی به کد اضافه نیست
|
| 206 |
-
// کلاینت خودکار متصل میشود و UI را آپدیت میکند
|
| 207 |
-
</script>
|
| 208 |
-
</body>
|
| 209 |
-
</html>
|
| 210 |
-
```
|
| 211 |
-
|
| 212 |
-
## 🔥 کاربردهای واقعی
|
| 213 |
-
|
| 214 |
-
### 1. برنامه موبایل:
|
| 215 |
-
```python
|
| 216 |
-
import asyncio
|
| 217 |
-
import websockets
|
| 218 |
-
import json
|
| 219 |
-
|
| 220 |
-
async def mobile_app():
|
| 221 |
-
uri = "ws://yourserver.com/ws"
|
| 222 |
-
async with websockets.connect(uri) as ws:
|
| 223 |
-
# دریافت لحظهای قیمتها
|
| 224 |
-
async for message in ws:
|
| 225 |
-
data = json.loads(message)
|
| 226 |
-
if data['type'] == 'price_update':
|
| 227 |
-
show_notification(data['data'])
|
| 228 |
-
```
|
| 229 |
-
|
| 230 |
-
### 2. ربات تلگرام:
|
| 231 |
-
```python
|
| 232 |
-
async def telegram_bot():
|
| 233 |
-
async with websockets.connect("ws://server/ws") as ws:
|
| 234 |
-
# Subscribe به alerts
|
| 235 |
-
await ws.send(json.dumps({
|
| 236 |
-
"type": "subscribe",
|
| 237 |
-
"group": "alerts"
|
| 238 |
-
}))
|
| 239 |
-
|
| 240 |
-
async for message in ws:
|
| 241 |
-
data = json.loads(message)
|
| 242 |
-
if data['type'] == 'alert':
|
| 243 |
-
# ارسال به تلگرام
|
| 244 |
-
await bot.send_message(
|
| 245 |
-
chat_id,
|
| 246 |
-
data['data']['message']
|
| 247 |
-
)
|
| 248 |
-
```
|
| 249 |
-
|
| 250 |
-
### 3. صفحه نمایش عمومی:
|
| 251 |
-
```javascript
|
| 252 |
-
// نمایش روی تلویزیون یا نمایشگر
|
| 253 |
-
const ws = new CryptoWebSocketClient();
|
| 254 |
-
|
| 255 |
-
ws.on('market_update', (msg) => {
|
| 256 |
-
// آپدیت نمودارها و قیمتها
|
| 257 |
-
updateCharts(msg.data);
|
| 258 |
-
updatePrices(msg.data);
|
| 259 |
-
});
|
| 260 |
-
|
| 261 |
-
// هر 10 ثانیه یکبار
|
| 262 |
-
setInterval(() => {
|
| 263 |
-
ws.requestStats();
|
| 264 |
-
}, 10000);
|
| 265 |
-
```
|
| 266 |
-
|
| 267 |
-
## 🎨 سفارشیسازی UI
|
| 268 |
-
|
| 269 |
-
### تغییر رنگها:
|
| 270 |
-
|
| 271 |
-
```css
|
| 272 |
-
/* در فایل CSS خودتان */
|
| 273 |
-
.connection-status-bar {
|
| 274 |
-
background: linear-gradient(135deg, #your-color1, #your-color2);
|
| 275 |
-
}
|
| 276 |
-
|
| 277 |
-
.status-dot-online {
|
| 278 |
-
background: #your-green-color;
|
| 279 |
-
}
|
| 280 |
-
```
|
| 281 |
-
|
| 282 |
-
### تغییر موقعیت نوار:
|
| 283 |
-
|
| 284 |
-
```css
|
| 285 |
-
.connection-status-bar {
|
| 286 |
-
/* به جای top */
|
| 287 |
-
bottom: 0;
|
| 288 |
-
}
|
| 289 |
-
```
|
| 290 |
-
|
| 291 |
-
### افزودن اطلاعات بیشتر:
|
| 292 |
-
|
| 293 |
-
```javascript
|
| 294 |
-
wsClient.on('stats_update', (msg) => {
|
| 295 |
-
// نمایش آمار سفارشی
|
| 296 |
-
document.getElementById('my-stat').textContent =
|
| 297 |
-
msg.data.custom_metric;
|
| 298 |
-
});
|
| 299 |
-
```
|
| 300 |
-
|
| 301 |
-
## 🐛 عیبیابی
|
| 302 |
-
|
| 303 |
-
### مشکل: اتصال برقرار نمیشود
|
| 304 |
-
|
| 305 |
-
1. سرور اجرا شده؟
|
| 306 |
-
```bash
|
| 307 |
-
curl http://localhost:8000/health
|
| 308 |
-
```
|
| 309 |
-
|
| 310 |
-
2. پورت باز است؟
|
| 311 |
-
```bash
|
| 312 |
-
netstat -an | grep 8000
|
| 313 |
-
```
|
| 314 |
-
|
| 315 |
-
3. کنسول مرورگر چه میگوید؟
|
| 316 |
-
- F12 → Console
|
| 317 |
-
|
| 318 |
-
### مشکل: تعداد کاربران نمایش نمیشود
|
| 319 |
-
|
| 320 |
-
1. Elementها با ID صحیح وجود دارند؟
|
| 321 |
-
```html
|
| 322 |
-
<span id="active-users-count">0</span>
|
| 323 |
-
```
|
| 324 |
-
|
| 325 |
-
2. JavaScript لود شده؟
|
| 326 |
-
```javascript
|
| 327 |
-
console.log(window.wsClient); // باید object باشد
|
| 328 |
-
```
|
| 329 |
-
|
| 330 |
-
### مشکل: اتصال مدام قطع میشود
|
| 331 |
-
|
| 332 |
-
1. Heartbeat فعال است؟ (باید هر 10 ثانیه یک پیام بیاید)
|
| 333 |
-
2. Firewall یا Proxy مشکل ندارد؟
|
| 334 |
-
3. Timeout سرور کم است؟
|
| 335 |
-
|
| 336 |
-
## 📈 Performance
|
| 337 |
-
|
| 338 |
-
### قبل:
|
| 339 |
-
- 🐌 100 کاربر = 6000 درخواست HTTP در دقیقه
|
| 340 |
-
- 💾 حجم داده: ~300MB در ساعت
|
| 341 |
-
- ⚡ CPU: 60-80%
|
| 342 |
-
|
| 343 |
-
### بعد:
|
| 344 |
-
- ⚡ 100 کاربر = 100 اتصال WebSocket
|
| 345 |
-
- 💾 حجم داده: ~10MB در ساعت
|
| 346 |
-
- ⚡ CPU: 10-15%
|
| 347 |
-
|
| 348 |
-
**30 برابر کارآمدتر!** 🎉
|
| 349 |
-
|
| 350 |
-
## 🎓 آموزش ویدیویی (قریب الوقوع)
|
| 351 |
-
|
| 352 |
-
- [ ] نصب و راهاندازی
|
| 353 |
-
- [ ] استفاده از API
|
| 354 |
-
- [ ] ساخت داشبورد سفارشی
|
| 355 |
-
- [ ] Integration با برنامه موبایل
|
| 356 |
-
|
| 357 |
-
## 💡 ایدههای بیشتر
|
| 358 |
-
|
| 359 |
-
1. **چت بین کاربران** - با همین WebSocket
|
| 360 |
-
2. **Trading Signals** - دریافت لحظهای سیگنالها
|
| 361 |
-
3. **Portfolio Tracker** - بهروزرسانی فوری داراییها
|
| 362 |
-
4. **Price Alerts** - هشدار لحظهای برای تغییر قیمت
|
| 363 |
-
|
| 364 |
-
## 📞 پشتیبانی
|
| 365 |
-
|
| 366 |
-
سوال دارید؟
|
| 367 |
-
- 📖 [راهنمای کامل WebSocket](WEBSOCKET_GUIDE.md)
|
| 368 |
-
- 🧪 [صفحه تست](http://localhost:8000/test_websocket.html)
|
| 369 |
-
- 💬 Issue در GitHub
|
| 370 |
-
|
| 371 |
-
---
|
| 372 |
-
|
| 373 |
-
**ساخته شده با ❤️ برای توسعهدهندگان ایرانی**
|
| 374 |
-
|
|
|
|
| 1 |
+
# 🚀 ویژگیهای بلادرنگ سیستم مانیتورینگ کریپتو
|
| 2 |
+
|
| 3 |
+
## ✨ چه چیزی اضافه شد؟
|
| 4 |
+
|
| 5 |
+
### 1. 📡 سیستم WebSocket کامل
|
| 6 |
+
|
| 7 |
+
**قبل (HTTP Polling):**
|
| 8 |
+
```
|
| 9 |
+
کلاینت → درخواست HTTP → سرور
|
| 10 |
+
← پاسخ HTTP ←
|
| 11 |
+
(تکرار هر 1-5 ثانیه) ⏱️
|
| 12 |
+
```
|
| 13 |
+
|
| 14 |
+
**الان (WebSocket):**
|
| 15 |
+
```
|
| 16 |
+
کلاینت ⟷ اتصال دائمی ⟷ سرور
|
| 17 |
+
← داده لحظهای ←
|
| 18 |
+
(فوری و بدون تاخیر! ⚡)
|
| 19 |
+
```
|
| 20 |
+
|
| 21 |
+
### 2. 👥 نمایش تعداد کاربران آنلاین
|
| 22 |
+
|
| 23 |
+
برنامه الان میتواند **بلافاصله** به شما نشان دهد:
|
| 24 |
+
- چند نفر الان متصل هستند
|
| 25 |
+
- چند جلسه (session) فعال است
|
| 26 |
+
- چه نوع کلاینتهایی متصلاند (مرورگر، API، موبایل)
|
| 27 |
+
|
| 28 |
+
### 3. 🎨 رابط کاربری زیبا و هوشمند
|
| 29 |
+
|
| 30 |
+
- **نوار وضعیت بالای صفحه** با نمایش:
|
| 31 |
+
- وضعیت اتصال (متصل/قطع شده) با نقطه رنگی
|
| 32 |
+
- تعداد کاربران آنلاین به صورت زنده
|
| 33 |
+
- آمار جلسات کلی
|
| 34 |
+
|
| 35 |
+
- **انیمیشنهای جذاب**:
|
| 36 |
+
- هنگام تغییر تعداد کاربران
|
| 37 |
+
- هنگام اتصال/قطع اتصال
|
| 38 |
+
- پالس نقطه وضعیت
|
| 39 |
+
|
| 40 |
+
- **reconnect خودکار**:
|
| 41 |
+
- اگر اتصال قطع شد، خودکار دوباره وصل میشود
|
| 42 |
+
- نیازی به refresh صفحه نیست!
|
| 43 |
+
|
| 44 |
+
## 🎯 چرا این تغییرات مهم است؟
|
| 45 |
+
|
| 46 |
+
### سرعت 10 برابر بیشتر! ⚡
|
| 47 |
+
|
| 48 |
+
| عملیات | HTTP Polling | WebSocket |
|
| 49 |
+
|--------|--------------|-----------|
|
| 50 |
+
| بهروزرسانی قیمت | 2-5 ثانیه | < 100ms |
|
| 51 |
+
| نمایش کاربران | هر 3 ثانیه | فوری |
|
| 52 |
+
| مصرف سرور | 100% | 10% |
|
| 53 |
+
| پهنای باند | زیاد | خیلی کم |
|
| 54 |
+
|
| 55 |
+
### Session Management حرفهای 🔐
|
| 56 |
+
|
| 57 |
+
هر کاربر یک **Session ID** منحصر به فرد دارد:
|
| 58 |
+
```json
|
| 59 |
+
{
|
| 60 |
+
"session_id": "550e8400-e29b-41d4-a716-446655440000",
|
| 61 |
+
"client_type": "browser",
|
| 62 |
+
"connected_at": "2024-01-15T10:00:00",
|
| 63 |
+
"metadata": { "source": "unified_dashboard" }
|
| 64 |
+
}
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
## 📂 فایلهای جدید
|
| 68 |
+
|
| 69 |
+
### Backend (سرور):
|
| 70 |
+
```
|
| 71 |
+
backend/services/
|
| 72 |
+
├── connection_manager.py ← مدیریت اتصالات WebSocket
|
| 73 |
+
└── auto_discovery_service.py ← کشف خودکار منابع جدید
|
| 74 |
+
|
| 75 |
+
api_server_extended.py ← بهروزرسانی شده با WebSocket
|
| 76 |
+
```
|
| 77 |
+
|
| 78 |
+
### Frontend (رابط کاربری):
|
| 79 |
+
```
|
| 80 |
+
static/
|
| 81 |
+
├── js/
|
| 82 |
+
│ └── websocket-client.js ← کلاینت WebSocket هوشمند
|
| 83 |
+
└── css/
|
| 84 |
+
└── connection-status.css ← استایلهای زیبا
|
| 85 |
+
|
| 86 |
+
test_websocket.html ← صفحه تست کامل
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
### مستندات:
|
| 90 |
+
```
|
| 91 |
+
WEBSOCKET_GUIDE.md ← راهنمای کامل WebSocket
|
| 92 |
+
REALTIME_FEATURES_FA.md ← این فایل!
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
## 🚀 نحوه استفاده
|
| 96 |
+
|
| 97 |
+
### 1. راهاندازی سرور:
|
| 98 |
+
|
| 99 |
+
```bash
|
| 100 |
+
# نصب وابستگیهای جدید
|
| 101 |
+
pip install -r requirements.txt
|
| 102 |
+
|
| 103 |
+
# اجرای سرور
|
| 104 |
+
python api_server_extended.py
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
### 2. باز کردن صفحه تست:
|
| 108 |
+
|
| 109 |
+
```
|
| 110 |
+
http://localhost:8000/test_websocket.html
|
| 111 |
+
```
|
| 112 |
+
|
| 113 |
+
### 3. مشاهده نتایج:
|
| 114 |
+
|
| 115 |
+
- ✅ نوار بالا باید **سبز** شود
|
| 116 |
+
- 👥 تعداد کاربران باید نمایش داده شود
|
| 117 |
+
- 📊 آمار به صورت **لحظهای** آپدیت میشود
|
| 118 |
+
|
| 119 |
+
### 4. تست با چند تب:
|
| 120 |
+
|
| 121 |
+
1. صفحه را در چند تب باز کنید
|
| 122 |
+
2. تعداد کاربران آنلاین **فوراً** افزایش مییابد
|
| 123 |
+
3. یک تب را ببندید → تعداد کاربران کم میشود
|
| 124 |
+
|
| 125 |
+
## 🎮 ویژگیهای پیشرفته
|
| 126 |
+
|
| 127 |
+
### Subscribe به کانالهای مختلف:
|
| 128 |
+
|
| 129 |
+
```javascript
|
| 130 |
+
// فقط اطلاعات بازار
|
| 131 |
+
wsClient.subscribe('market');
|
| 132 |
+
|
| 133 |
+
// فقط قیمتها
|
| 134 |
+
wsClient.subscribe('prices');
|
| 135 |
+
|
| 136 |
+
// فقط اخبار
|
| 137 |
+
wsClient.subscribe('news');
|
| 138 |
+
|
| 139 |
+
// همه چیز
|
| 140 |
+
wsClient.subscribe('all');
|
| 141 |
+
```
|
| 142 |
+
|
| 143 |
+
### دریافت آمار فوری:
|
| 144 |
+
|
| 145 |
+
```javascript
|
| 146 |
+
// درخواست آمار
|
| 147 |
+
wsClient.requestStats();
|
| 148 |
+
|
| 149 |
+
// پاسخ در کمتر از 100ms:
|
| 150 |
+
{
|
| 151 |
+
"active_connections": 15,
|
| 152 |
+
"total_sessions": 23,
|
| 153 |
+
"client_types": {
|
| 154 |
+
"browser": 12,
|
| 155 |
+
"api": 2,
|
| 156 |
+
"mobile": 1
|
| 157 |
+
}
|
| 158 |
+
}
|
| 159 |
+
```
|
| 160 |
+
|
| 161 |
+
### Handler سفارشی:
|
| 162 |
+
|
| 163 |
+
```javascript
|
| 164 |
+
// ثبت handler برای رویداد خاص
|
| 165 |
+
wsClient.on('price_update', (message) => {
|
| 166 |
+
console.log('قیمت جدید:', message.data);
|
| 167 |
+
updateUI(message.data);
|
| 168 |
+
});
|
| 169 |
+
```
|
| 170 |
+
|
| 171 |
+
## 📊 مثال کاربردی
|
| 172 |
+
|
| 173 |
+
### نمایش تعداد کاربران در صفحه خودتان:
|
| 174 |
+
|
| 175 |
+
```html
|
| 176 |
+
<!DOCTYPE html>
|
| 177 |
+
<html lang="fa" dir="rtl">
|
| 178 |
+
<head>
|
| 179 |
+
<link rel="stylesheet" href="/static/css/connection-status.css">
|
| 180 |
+
</head>
|
| 181 |
+
<body>
|
| 182 |
+
<!-- نوار وضعیت -->
|
| 183 |
+
<div class="connection-status-bar" id="ws-connection-status">
|
| 184 |
+
<div class="ws-connection-info">
|
| 185 |
+
<span class="status-dot" id="ws-status-dot"></span>
|
| 186 |
+
<span id="ws-status-text">در حال اتصال...</span>
|
| 187 |
+
</div>
|
| 188 |
+
|
| 189 |
+
<div class="online-users-widget">
|
| 190 |
+
<span class="users-icon">👥</span>
|
| 191 |
+
<span class="count-number" id="active-users-count">0</span>
|
| 192 |
+
<span class="count-label">کاربر آنلاین</span>
|
| 193 |
+
</div>
|
| 194 |
+
</div>
|
| 195 |
+
|
| 196 |
+
<!-- محتوای اصلی شما -->
|
| 197 |
+
<div class="container">
|
| 198 |
+
<h1>داشبورد من</h1>
|
| 199 |
+
<!-- ... -->
|
| 200 |
+
</div>
|
| 201 |
+
|
| 202 |
+
<!-- اضافه کردن WebSocket Client -->
|
| 203 |
+
<script src="/static/js/websocket-client.js"></script>
|
| 204 |
+
<script>
|
| 205 |
+
// همین! دیگر نیازی به کد اضافه نیست
|
| 206 |
+
// کلاینت خودکار متصل میشود و UI را آپدیت میکند
|
| 207 |
+
</script>
|
| 208 |
+
</body>
|
| 209 |
+
</html>
|
| 210 |
+
```
|
| 211 |
+
|
| 212 |
+
## 🔥 کاربردهای واقعی
|
| 213 |
+
|
| 214 |
+
### 1. برنامه موبایل:
|
| 215 |
+
```python
|
| 216 |
+
import asyncio
|
| 217 |
+
import websockets
|
| 218 |
+
import json
|
| 219 |
+
|
| 220 |
+
async def mobile_app():
|
| 221 |
+
uri = "ws://yourserver.com/ws"
|
| 222 |
+
async with websockets.connect(uri) as ws:
|
| 223 |
+
# دریافت لحظهای قیمتها
|
| 224 |
+
async for message in ws:
|
| 225 |
+
data = json.loads(message)
|
| 226 |
+
if data['type'] == 'price_update':
|
| 227 |
+
show_notification(data['data'])
|
| 228 |
+
```
|
| 229 |
+
|
| 230 |
+
### 2. ربات تلگرام:
|
| 231 |
+
```python
|
| 232 |
+
async def telegram_bot():
|
| 233 |
+
async with websockets.connect("ws://server/ws") as ws:
|
| 234 |
+
# Subscribe به alerts
|
| 235 |
+
await ws.send(json.dumps({
|
| 236 |
+
"type": "subscribe",
|
| 237 |
+
"group": "alerts"
|
| 238 |
+
}))
|
| 239 |
+
|
| 240 |
+
async for message in ws:
|
| 241 |
+
data = json.loads(message)
|
| 242 |
+
if data['type'] == 'alert':
|
| 243 |
+
# ارسال به تلگرام
|
| 244 |
+
await bot.send_message(
|
| 245 |
+
chat_id,
|
| 246 |
+
data['data']['message']
|
| 247 |
+
)
|
| 248 |
+
```
|
| 249 |
+
|
| 250 |
+
### 3. صفحه نمایش عمومی:
|
| 251 |
+
```javascript
|
| 252 |
+
// نمایش روی تلویزیون یا نمایشگر
|
| 253 |
+
const ws = new CryptoWebSocketClient();
|
| 254 |
+
|
| 255 |
+
ws.on('market_update', (msg) => {
|
| 256 |
+
// آپدیت نمودارها و قیمتها
|
| 257 |
+
updateCharts(msg.data);
|
| 258 |
+
updatePrices(msg.data);
|
| 259 |
+
});
|
| 260 |
+
|
| 261 |
+
// هر 10 ثانیه یکبار
|
| 262 |
+
setInterval(() => {
|
| 263 |
+
ws.requestStats();
|
| 264 |
+
}, 10000);
|
| 265 |
+
```
|
| 266 |
+
|
| 267 |
+
## 🎨 سفارشیسازی UI
|
| 268 |
+
|
| 269 |
+
### تغییر رنگها:
|
| 270 |
+
|
| 271 |
+
```css
|
| 272 |
+
/* در فایل CSS خودتان */
|
| 273 |
+
.connection-status-bar {
|
| 274 |
+
background: linear-gradient(135deg, #your-color1, #your-color2);
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
.status-dot-online {
|
| 278 |
+
background: #your-green-color;
|
| 279 |
+
}
|
| 280 |
+
```
|
| 281 |
+
|
| 282 |
+
### تغییر موقعیت نوار:
|
| 283 |
+
|
| 284 |
+
```css
|
| 285 |
+
.connection-status-bar {
|
| 286 |
+
/* به جای top */
|
| 287 |
+
bottom: 0;
|
| 288 |
+
}
|
| 289 |
+
```
|
| 290 |
+
|
| 291 |
+
### افزودن اطلاعات بیشتر:
|
| 292 |
+
|
| 293 |
+
```javascript
|
| 294 |
+
wsClient.on('stats_update', (msg) => {
|
| 295 |
+
// نمایش آمار سفارشی
|
| 296 |
+
document.getElementById('my-stat').textContent =
|
| 297 |
+
msg.data.custom_metric;
|
| 298 |
+
});
|
| 299 |
+
```
|
| 300 |
+
|
| 301 |
+
## 🐛 عیبیابی
|
| 302 |
+
|
| 303 |
+
### مشکل: اتصال برقرار نمیشود
|
| 304 |
+
|
| 305 |
+
1. سرور اجرا شده؟
|
| 306 |
+
```bash
|
| 307 |
+
curl http://localhost:8000/health
|
| 308 |
+
```
|
| 309 |
+
|
| 310 |
+
2. پورت باز است؟
|
| 311 |
+
```bash
|
| 312 |
+
netstat -an | grep 8000
|
| 313 |
+
```
|
| 314 |
+
|
| 315 |
+
3. کنسول مرورگر چه میگوید؟
|
| 316 |
+
- F12 → Console
|
| 317 |
+
|
| 318 |
+
### مشکل: تعداد کاربران نمایش نمیشود
|
| 319 |
+
|
| 320 |
+
1. Elementها با ID صحیح وجود دارند؟
|
| 321 |
+
```html
|
| 322 |
+
<span id="active-users-count">0</span>
|
| 323 |
+
```
|
| 324 |
+
|
| 325 |
+
2. JavaScript لود شده؟
|
| 326 |
+
```javascript
|
| 327 |
+
console.log(window.wsClient); // باید object باشد
|
| 328 |
+
```
|
| 329 |
+
|
| 330 |
+
### مشکل: اتصال مدام قطع میشود
|
| 331 |
+
|
| 332 |
+
1. Heartbeat فعال است؟ (باید هر 10 ثانیه یک پیام بیاید)
|
| 333 |
+
2. Firewall یا Proxy مشکل ندارد؟
|
| 334 |
+
3. Timeout سرور کم است؟
|
| 335 |
+
|
| 336 |
+
## 📈 Performance
|
| 337 |
+
|
| 338 |
+
### قبل:
|
| 339 |
+
- 🐌 100 کاربر = 6000 درخواست HTTP در دقیقه
|
| 340 |
+
- 💾 حجم داده: ~300MB در ساعت
|
| 341 |
+
- ⚡ CPU: 60-80%
|
| 342 |
+
|
| 343 |
+
### بعد:
|
| 344 |
+
- ⚡ 100 کاربر = 100 اتصال WebSocket
|
| 345 |
+
- 💾 حجم داده: ~10MB در ساعت
|
| 346 |
+
- ⚡ CPU: 10-15%
|
| 347 |
+
|
| 348 |
+
**30 برابر کارآمدتر!** 🎉
|
| 349 |
+
|
| 350 |
+
## 🎓 آموزش ویدیویی (قریب الوقوع)
|
| 351 |
+
|
| 352 |
+
- [ ] نصب و راهاندازی
|
| 353 |
+
- [ ] استفاده از API
|
| 354 |
+
- [ ] ساخت داشبورد سفارشی
|
| 355 |
+
- [ ] Integration با برنامه موبایل
|
| 356 |
+
|
| 357 |
+
## 💡 ایدههای بیشتر
|
| 358 |
+
|
| 359 |
+
1. **چت بین کاربران** - با همین WebSocket
|
| 360 |
+
2. **Trading Signals** - دریافت لحظهای سیگنالها
|
| 361 |
+
3. **Portfolio Tracker** - بهروزرسانی فوری داراییها
|
| 362 |
+
4. **Price Alerts** - هشدار لحظهای برای تغییر قیمت
|
| 363 |
+
|
| 364 |
+
## 📞 پشتیبانی
|
| 365 |
+
|
| 366 |
+
سوال دارید؟
|
| 367 |
+
- 📖 [راهنمای کامل WebSocket](WEBSOCKET_GUIDE.md)
|
| 368 |
+
- 🧪 [صفحه تست](http://localhost:8000/test_websocket.html)
|
| 369 |
+
- 💬 Issue در GitHub
|
| 370 |
+
|
| 371 |
+
---
|
| 372 |
+
|
| 373 |
+
**ساخته شده با ❤️ برای توسعهدهندگان ایرانی**
|
| 374 |
+
|
docs/reports/HF_SPACE_HUB_PRESERVATION_REPORT.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# HF Space V4 Hub Preservation Report
|
| 2 |
+
|
| 3 |
+
This package is the non-destructive source-preserved hub build.
|
| 4 |
+
|
| 5 |
+
## What changed
|
| 6 |
+
|
| 7 |
+
- Preserved the large JSON/API catalogs and provider config files.
|
| 8 |
+
- Preserved `api/`, `hf-data-engine/`, `api-resources/`, model code, provider manager, resource manager, and rotation/circuit-breaker logic.
|
| 9 |
+
- Cleaned only the project surface: moved secondary dashboard HTML files to `ui-gallery/`, removed backup/optimized duplicates and Python cache files.
|
| 10 |
+
- Added env-aware provider rotation helpers in `api_hub_registry.py`.
|
| 11 |
+
- Extended OHLCV fallback order in `api_compat_routes.py`: Binance Public -> KuCoin Public -> CryptoCompare.
|
| 12 |
+
- Added `/api/providers/status` fields for OHLCV/orderbook/sentiment rotation plans.
|
| 13 |
+
|
| 14 |
+
## Preservation rule
|
| 15 |
+
|
| 16 |
+
The old deploy-clean package was too aggressive for a development/source hub. This version keeps the project as a multi-source hub. Do not delete JSON registries just to reduce file count.
|
| 17 |
+
|
| 18 |
+
## OHLCV rule
|
| 19 |
+
|
| 20 |
+
Raw OHLCV is never hallucinated by models. Raw OHLCV comes from public market/exchange providers. Hugging Face models may be used for:
|
| 21 |
+
|
| 22 |
+
- sentiment and news classification
|
| 23 |
+
- anomaly/quality scoring
|
| 24 |
+
- regime/context labeling
|
| 25 |
+
- forecasting/enrichment
|
| 26 |
+
- fallback explanations
|
| 27 |
+
|
| 28 |
+
Models must not replace real candle data as source of truth.
|
| 29 |
+
|
| 30 |
+
## Secret policy
|
| 31 |
+
|
| 32 |
+
No raw API keys are stored in source. Configure keys in HuggingFace Space Secrets / env vars.
|
| 33 |
+
|
| 34 |
+
## Important preserved files/folders
|
| 35 |
+
|
| 36 |
+
- `all_apis_merged_2025.json`
|
| 37 |
+
- `crypto_resources_unified_2025-11-11.json`
|
| 38 |
+
- `ultimate_crypto_pipeline_2025_NZasinich.json`
|
| 39 |
+
- `providers_config_extended.json`
|
| 40 |
+
- `providers_config_ultimate.json`
|
| 41 |
+
- `api-resources/`
|
| 42 |
+
- `api/`
|
| 43 |
+
- `hf-data-engine/`
|
| 44 |
+
- `api_hub_registry.py`
|
| 45 |
+
- `provider_manager.py`
|
| 46 |
+
- `resource_manager.py`
|
| 47 |
+
- `ai_models.py`
|
| 48 |
+
- `crypto_data_bank/ai/huggingface_models.py`
|
| 49 |
+
|
| 50 |
+
## Validation
|
| 51 |
+
|
| 52 |
+
Run:
|
| 53 |
+
|
| 54 |
+
```bash
|
| 55 |
+
python -m py_compile api_server_extended.py api_compat_routes.py api_hub_registry.py ai_models.py provider_manager.py resource_manager.py
|
| 56 |
+
```
|
| 57 |
+
|
| 58 |
+
Smoke routes after deploy:
|
| 59 |
+
|
| 60 |
+
```bash
|
| 61 |
+
curl /api/health
|
| 62 |
+
curl /api/status
|
| 63 |
+
curl /api/providers/catalog
|
| 64 |
+
curl /api/providers/status
|
| 65 |
+
curl /api/debug/capabilities
|
| 66 |
+
curl "/api/ohlcv?symbol=BTCUSDT&timeframe=1h&limit=50"
|
| 67 |
+
curl "/api/orderbook?symbol=BTCUSDT&limit=20"
|
| 68 |
+
```
|
docs/reports/HF_SPACE_HUB_UPGRADE_REPORT.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Datasourceforcryptocurrency-4 Hub Upgrade + Surface Cleanup Report
|
| 2 |
+
|
| 3 |
+
## Scope
|
| 4 |
+
Worked only on the HuggingFace Space package. Short Hunter was not modified.
|
| 5 |
+
|
| 6 |
+
## Uploaded API files
|
| 7 |
+
Two uploaded API text files were reviewed as provider/source lists. They contain raw credentials, so this package does **not** hardcode those values.
|
| 8 |
+
|
| 9 |
+
## Security decision
|
| 10 |
+
All provider keys must be configured through HuggingFace Space secrets/environment variables. A sanitized provider catalog was created instead of committing keys.
|
| 11 |
+
|
| 12 |
+
## Added / changed
|
| 13 |
+
- `api_hub_registry.py` — sanitized runtime provider registry and env-secret detection.
|
| 14 |
+
- `api-resources/provider_capabilities_v4.json` — sanitized provider/capability catalog generated from the uploaded source lists.
|
| 15 |
+
- `.env.example` — expanded optional secret names without values.
|
| 16 |
+
- `api_compat_routes.py` — added `/api/providers/catalog`, `/api/providers/status`, and enhanced `/api/debug/capabilities`.
|
| 17 |
+
- `api_server_extended.py` — market/news provider chain improved with optional CoinMarketCap, CryptoCompare, and NewsAPI via env secrets.
|
| 18 |
+
- `INPUT_API_FILES_SECURITY_NOTE.md` — explains secret handling.
|
| 19 |
+
|
| 20 |
+
## Provider hub model
|
| 21 |
+
The Space now documents and exposes categories for:
|
| 22 |
+
- market data
|
| 23 |
+
- exchange public data
|
| 24 |
+
- news
|
| 25 |
+
- sentiment
|
| 26 |
+
- block explorers
|
| 27 |
+
- on-chain analytics
|
| 28 |
+
- whale tracking
|
| 29 |
+
|
| 30 |
+
## Project surface cleanup
|
| 31 |
+
Archived duplicate/legacy root index files and noisy historical docs into:
|
| 32 |
+
|
| 33 |
+
`_archive/surface_cleanup_legacy/`
|
| 34 |
+
|
| 35 |
+
Active index files preserved:
|
| 36 |
+
- `index.html`
|
| 37 |
+
- `api/index.html`
|
| 38 |
+
- `hf-data-engine/index.html`
|
| 39 |
+
- `templates/index.html`
|
| 40 |
+
|
| 41 |
+
## Runtime rules preserved
|
| 42 |
+
- No live trading.
|
| 43 |
+
- No private exchange write endpoints.
|
| 44 |
+
- No frontend secrets.
|
| 45 |
+
- Missing optional providers degrade gracefully.
|
| 46 |
+
- News/orderbook/sentiment absence must not mark the whole datasource unavailable.
|
| 47 |
+
|
| 48 |
+
## Validation
|
| 49 |
+
Run:
|
| 50 |
+
|
| 51 |
+
```bash
|
| 52 |
+
python -m py_compile api_server_extended.py api_compat_routes.py api_hub_registry.py <HF_TOKEN_FROM_SPACE_SECRET>.py main.py
|
| 53 |
+
```
|
| 54 |
+
|
| 55 |
+
Smoke routes:
|
| 56 |
+
|
| 57 |
+
```bash
|
| 58 |
+
curl /api/health
|
| 59 |
+
curl /api/status
|
| 60 |
+
curl /api/debug/capabilities
|
| 61 |
+
curl /api/providers/catalog
|
| 62 |
+
curl /api/providers/status
|
| 63 |
+
curl /api/market
|
| 64 |
+
curl /api/news
|
| 65 |
+
curl /api/news/latest
|
| 66 |
+
```
|
docs/reports/HF_SPACE_REPAIR_REPORT.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Datasourceforcryptocurrency-4 Repair Report
|
| 2 |
+
|
| 3 |
+
## Scope
|
| 4 |
+
Worked only inside the HuggingFace Space repository. Short Hunter project files were not modified.
|
| 5 |
+
|
| 6 |
+
## Main files changed
|
| 7 |
+
- `api_server_extended.py`
|
| 8 |
+
- `api_compat_routes.py`
|
| 9 |
+
|
| 10 |
+
## What was fixed
|
| 11 |
+
- Added/strengthened stable JSON API contracts for external clients.
|
| 12 |
+
- Preserved the existing FastAPI app, dashboard, model routes, provider/resource registry, and HF model endpoints.
|
| 13 |
+
- Kept V4 as an enhanced multi-source hub, not a clone of V2.
|
| 14 |
+
- Added/confirmed compatibility routes for Short Hunter and other clients:
|
| 15 |
+
- `GET /api/health`
|
| 16 |
+
- `GET /api/status`
|
| 17 |
+
- `GET /api/market`
|
| 18 |
+
- `GET /api/coins/top`
|
| 19 |
+
- `GET /api/trending`
|
| 20 |
+
- `GET /api/ohlcv`
|
| 21 |
+
- `GET /api/klines`
|
| 22 |
+
- `GET /api/history`
|
| 23 |
+
- `GET /api/trading/history/{symbol}`
|
| 24 |
+
- `GET /api/indicators`
|
| 25 |
+
- `GET /api/indicators/rsi`
|
| 26 |
+
- `GET /api/indicators/macd`
|
| 27 |
+
- `GET /api/indicators/comprehensive`
|
| 28 |
+
- `GET /api/sentiment/global`
|
| 29 |
+
- `GET /api/sentiment/asset/{symbol}`
|
| 30 |
+
- `GET/POST /api/ai/sentiment`
|
| 31 |
+
- `GET /api/orderbook`
|
| 32 |
+
- `GET /api/debug/capabilities`
|
| 33 |
+
|
| 34 |
+
## Provider model
|
| 35 |
+
- Market/top/trending: CoinGecko public API.
|
| 36 |
+
- OHLCV/klines: Binance public primary with KuCoin public fallback.
|
| 37 |
+
- Orderbook: KuCoin public primary with Binance public fallback.
|
| 38 |
+
- Sentiment: Alternative.me global fear/greed plus existing HF sentiment endpoints.
|
| 39 |
+
- Indicators: local computation from OHLCV when external indicator provider is absent.
|
| 40 |
+
- News: database first, public CryptoCompare fallback, empty news is non-fatal.
|
| 41 |
+
|
| 42 |
+
## Behavior changes
|
| 43 |
+
- Missing optional data no longer marks the whole Space unavailable.
|
| 44 |
+
- `/api/status` now reports capability-level state using `COMPLETE`, `PARTIAL`, `DEGRADED`, or `UNAVAILABLE`.
|
| 45 |
+
- Every compatibility failure returns structured JSON with `success:false`, `errors`, `source`, and `timestamp` instead of raw 404/HTML errors.
|
| 46 |
+
- Empty news returns `success:true`, `data:[]`, `status:"empty"`.
|
| 47 |
+
- No private/write exchange endpoints were added.
|
| 48 |
+
- No trading execution was added.
|
| 49 |
+
|
| 50 |
+
## Local validation in this sandbox
|
| 51 |
+
Internet/DNS is disabled in the execution sandbox, so public provider calls cannot return live market data here. The important validation result is that endpoints respond with structured JSON instead of crashing.
|
| 52 |
+
|
| 53 |
+
Checked successfully with FastAPI TestClient:
|
| 54 |
+
- `/api/health` -> 200, structured success JSON
|
| 55 |
+
- `/api/status` -> 200, capability-level JSON
|
| 56 |
+
- `/api/debug/capabilities` -> 200
|
| 57 |
+
- `/api/news` -> 200, empty/non-fatal structured JSON when DB/provider unavailable
|
| 58 |
+
- `/api/news/latest` -> 200, empty/non-fatal structured JSON when DB table absent
|
| 59 |
+
- `/api/ohlcv` -> 200, structured `success:false` when sandbox DNS blocks providers
|
| 60 |
+
- `/api/orderbook` -> 200, structured `success:false` when sandbox DNS blocks providers
|
| 61 |
+
- `/api/sentiment/global` -> 200, structured `success:false` when sandbox DNS blocks providers
|
| 62 |
+
|
| 63 |
+
## Compile validation
|
| 64 |
+
- `python -m py_compile api_server_extended.py api_compat_routes.py` passed.
|
| 65 |
+
|
| 66 |
+
## Secrets
|
| 67 |
+
- Final packaged ZIP excludes `.git`, `.env`, caches, and bytecode.
|
| 68 |
+
- Sensitive token-like values are sanitized in the packaged copy.
|
| 69 |
+
- Use HuggingFace Space Repository Secrets for real tokens/API keys.
|
docs/reports/PRODUCTION_AUDIT_COMPREHENSIVE.md
CHANGED
|
@@ -127,7 +127,7 @@ crypto-dt-source/
|
|
| 127 |
|
| 128 |
1. **Etherscan** (Ethereum)
|
| 129 |
- Endpoint: `https://api.etherscan.io/api`
|
| 130 |
-
- Keys Available: 2 (
|
| 131 |
- Rate Limit: 5 calls/sec
|
| 132 |
- Implemented: ✅ `get_etherscan_gas_price()`
|
| 133 |
- Data: Gas prices, account balances, transactions, token balances
|
|
@@ -135,14 +135,14 @@ crypto-dt-source/
|
|
| 135 |
|
| 136 |
2. **BscScan** (Binance Smart Chain)
|
| 137 |
- Endpoint: `https://api.bscscan.com/api`
|
| 138 |
-
- Key Available:
|
| 139 |
- Rate Limit: 5 calls/sec
|
| 140 |
- Implemented: ✅ `get_bscscan_bnb_price()`
|
| 141 |
- **Real Data:** Yes
|
| 142 |
|
| 143 |
3. **TronScan** (TRON Network)
|
| 144 |
- Endpoint: `https://apilist.tronscanapi.com/api`
|
| 145 |
-
- Key Available:
|
| 146 |
- Implemented: ✅ `get_tronscan_stats()`
|
| 147 |
- **Real Data:** Yes
|
| 148 |
|
|
@@ -170,7 +170,7 @@ crypto-dt-source/
|
|
| 170 |
|
| 171 |
2. **NewsAPI.org** (REQUIRES KEY)
|
| 172 |
- Endpoint: `https://newsdata.io/api/1`
|
| 173 |
-
- Key Available: `NEWSAPI_KEY_FROM_SPACE_SECRET`
|
| 174 |
- Free tier: 100 req/day
|
| 175 |
- Implemented: ✅ `get_newsapi_headlines()`
|
| 176 |
- **Real Data:** Yes (API key required)
|
|
@@ -1034,18 +1034,18 @@ ALCHEMY_KEY= # Alchemy RPC
|
|
| 1034 |
**Available in Code:**
|
| 1035 |
```python
|
| 1036 |
# Blockchain Explorers - KEYS PROVIDED
|
| 1037 |
-
ETHERSCAN_KEY_1 = "
|
| 1038 |
-
ETHERSCAN_KEY_2 = "
|
| 1039 |
-
BSCSCAN_KEY = "
|
| 1040 |
-
TRONSCAN_KEY = "
|
| 1041 |
|
| 1042 |
# Market Data - KEYS PROVIDED
|
| 1043 |
-
COINMARKETCAP_KEY_1 = "
|
| 1044 |
-
COINMARKETCAP_KEY_2 = "
|
| 1045 |
-
CRYPTOCOMPARE_KEY = "
|
| 1046 |
|
| 1047 |
# News - KEY PROVIDED
|
| 1048 |
-
NEWSAPI_KEY = "NEWSAPI_KEY_FROM_SPACE_SECRET"
|
| 1049 |
```
|
| 1050 |
|
| 1051 |
**Status:** ✅ KEYS ARE EMBEDDED IN CONFIG
|
|
|
|
| 127 |
|
| 128 |
1. **Etherscan** (Ethereum)
|
| 129 |
- Endpoint: `https://api.etherscan.io/api`
|
| 130 |
+
- Keys Available: 2 (<REDACTED_API_KEY>, T6IR8VJHX2NE...)
|
| 131 |
- Rate Limit: 5 calls/sec
|
| 132 |
- Implemented: ✅ `get_etherscan_gas_price()`
|
| 133 |
- Data: Gas prices, account balances, transactions, token balances
|
|
|
|
| 135 |
|
| 136 |
2. **BscScan** (Binance Smart Chain)
|
| 137 |
- Endpoint: `https://api.bscscan.com/api`
|
| 138 |
+
- Key Available: <REDACTED_API_KEY>
|
| 139 |
- Rate Limit: 5 calls/sec
|
| 140 |
- Implemented: ✅ `get_bscscan_bnb_price()`
|
| 141 |
- **Real Data:** Yes
|
| 142 |
|
| 143 |
3. **TronScan** (TRON Network)
|
| 144 |
- Endpoint: `https://apilist.tronscanapi.com/api`
|
| 145 |
+
- Key Available: <REDACTED_API_KEY>
|
| 146 |
- Implemented: ✅ `get_tronscan_stats()`
|
| 147 |
- **Real Data:** Yes
|
| 148 |
|
|
|
|
| 170 |
|
| 171 |
2. **NewsAPI.org** (REQUIRES KEY)
|
| 172 |
- Endpoint: `https://newsdata.io/api/1`
|
| 173 |
+
- Key Available: `<NEWSAPI_KEY_FROM_SPACE_SECRET>`
|
| 174 |
- Free tier: 100 req/day
|
| 175 |
- Implemented: ✅ `get_newsapi_headlines()`
|
| 176 |
- **Real Data:** Yes (API key required)
|
|
|
|
| 1034 |
**Available in Code:**
|
| 1035 |
```python
|
| 1036 |
# Blockchain Explorers - KEYS PROVIDED
|
| 1037 |
+
ETHERSCAN_KEY_1 = "<REDACTED_API_KEY>"
|
| 1038 |
+
ETHERSCAN_KEY_2 = "<REDACTED_API_KEY>"
|
| 1039 |
+
BSCSCAN_KEY = "<REDACTED_API_KEY>"
|
| 1040 |
+
TRONSCAN_KEY = "<REDACTED_API_KEY>"
|
| 1041 |
|
| 1042 |
# Market Data - KEYS PROVIDED
|
| 1043 |
+
COINMARKETCAP_KEY_1 = "<REDACTED_API_KEY>"
|
| 1044 |
+
COINMARKETCAP_KEY_2 = "<REDACTED_API_KEY>"
|
| 1045 |
+
CRYPTOCOMPARE_KEY = "<REDACTED_API_KEY>"
|
| 1046 |
|
| 1047 |
# News - KEY PROVIDED
|
| 1048 |
+
NEWSAPI_KEY = "<NEWSAPI_KEY_FROM_SPACE_SECRET>"
|
| 1049 |
```
|
| 1050 |
|
| 1051 |
**Status:** ✅ KEYS ARE EMBEDDED IN CONFIG
|
docs/reports/PROVIDER_AUTO_DISCOVERY_REPORT.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
docs/reports/VALIDATION_REPORT.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
py_compile return code: 0
|
| 2 |
+
|
docs/security/INPUT_API_FILES_SECURITY_NOTE.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Input API Files Security Note
|
| 2 |
+
|
| 3 |
+
The user-provided API text files contained provider names and raw key-like values.
|
| 4 |
+
|
| 5 |
+
Security policy used in this package:
|
| 6 |
+
|
| 7 |
+
- Raw keys were not copied into source.
|
| 8 |
+
- Provider names/capabilities were preserved.
|
| 9 |
+
- Runtime reads secrets only from environment variables / HuggingFace Space Secrets.
|
| 10 |
+
- Rotate any keys that were pasted into chat or local logs.
|
gradio_dashboard.py
CHANGED
|
@@ -1,476 +1,476 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
Comprehensive Gradio Dashboard for Crypto Data Sources
|
| 4 |
-
Monitors health, accessibility, and functionality of all data sources
|
| 5 |
-
"""
|
| 6 |
-
|
| 7 |
-
import gradio as gr
|
| 8 |
-
import httpx
|
| 9 |
-
import asyncio
|
| 10 |
-
import json
|
| 11 |
-
import time
|
| 12 |
-
from datetime import datetime
|
| 13 |
-
from typing import Dict, List, Tuple, Optional
|
| 14 |
-
import pandas as pd
|
| 15 |
-
from pathlib import Path
|
| 16 |
-
import sys
|
| 17 |
-
import os
|
| 18 |
-
|
| 19 |
-
# Add project root to path
|
| 20 |
-
sys.path.insert(0, os.path.dirname(__file__))
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
class CryptoResourceMonitor:
|
| 24 |
-
"""Monitor and test all crypto data sources"""
|
| 25 |
-
|
| 26 |
-
def __init__(self):
|
| 27 |
-
self.api_resources = self.load_api_resources()
|
| 28 |
-
self.health_cache = {}
|
| 29 |
-
self.last_check_time = None
|
| 30 |
-
self.fastapi_url = "http://localhost:7860"
|
| 31 |
-
self.hf_engine_url = "http://localhost:8000"
|
| 32 |
-
|
| 33 |
-
def load_api_resources(self) -> Dict:
|
| 34 |
-
"""Load all API resources from api-resources folder"""
|
| 35 |
-
resources = {
|
| 36 |
-
"unified": {},
|
| 37 |
-
"pipeline": {},
|
| 38 |
-
"merged": {}
|
| 39 |
-
}
|
| 40 |
-
|
| 41 |
-
try:
|
| 42 |
-
# Load unified resources
|
| 43 |
-
unified_path = Path("api-resources/crypto_resources_unified_2025-11-11.json")
|
| 44 |
-
if unified_path.exists():
|
| 45 |
-
with open(unified_path) as f:
|
| 46 |
-
resources["unified"] = json.load(f)
|
| 47 |
-
|
| 48 |
-
# Load pipeline
|
| 49 |
-
pipeline_path = Path("api-resources/ultimate_crypto_pipeline_2025_NZasinich.json")
|
| 50 |
-
if pipeline_path.exists():
|
| 51 |
-
with open(pipeline_path) as f:
|
| 52 |
-
resources["pipeline"] = json.load(f)
|
| 53 |
-
|
| 54 |
-
# Load merged APIs
|
| 55 |
-
merged_path = Path("all_apis_merged_2025.json")
|
| 56 |
-
if merged_path.exists():
|
| 57 |
-
with open(merged_path) as f:
|
| 58 |
-
resources["merged"] = json.load(f)
|
| 59 |
-
|
| 60 |
-
except Exception as e:
|
| 61 |
-
print(f"Error loading resources: {e}")
|
| 62 |
-
|
| 63 |
-
return resources
|
| 64 |
-
|
| 65 |
-
async def check_endpoint_health(self, url: str, timeout: int = 5) -> Tuple[bool, float, str]:
|
| 66 |
-
"""Check if an endpoint is accessible"""
|
| 67 |
-
start_time = time.time()
|
| 68 |
-
try:
|
| 69 |
-
async with httpx.AsyncClient(timeout=timeout) as client:
|
| 70 |
-
response = await client.get(url)
|
| 71 |
-
latency = (time.time() - start_time) * 1000
|
| 72 |
-
return response.status_code < 400, latency, f"Status: {response.status_code}"
|
| 73 |
-
except httpx.TimeoutException:
|
| 74 |
-
return False, timeout * 1000, "Timeout"
|
| 75 |
-
except Exception as e:
|
| 76 |
-
return False, 0, str(e)[:100]
|
| 77 |
-
|
| 78 |
-
def check_fastapi_server(self) -> Tuple[bool, str]:
|
| 79 |
-
"""Check if main FastAPI server is running"""
|
| 80 |
-
try:
|
| 81 |
-
response = httpx.get(f"{self.fastapi_url}/health", timeout=5)
|
| 82 |
-
return True, f"✅ Online (Status: {response.status_code})"
|
| 83 |
-
except:
|
| 84 |
-
return False, "❌ Offline"
|
| 85 |
-
|
| 86 |
-
def check_hf_data_engine(self) -> Tuple[bool, str]:
|
| 87 |
-
"""Check if HF Data Engine is running"""
|
| 88 |
-
try:
|
| 89 |
-
response = httpx.get(f"{self.hf_engine_url}/api/health", timeout=5)
|
| 90 |
-
data = response.json()
|
| 91 |
-
providers = len(data.get("providers", []))
|
| 92 |
-
uptime = data.get("uptime", 0)
|
| 93 |
-
return True, f"✅ Online ({providers} providers, uptime: {uptime}s)"
|
| 94 |
-
except:
|
| 95 |
-
return False, "❌ Offline"
|
| 96 |
-
|
| 97 |
-
def get_system_overview(self) -> str:
|
| 98 |
-
"""Get overview of all systems"""
|
| 99 |
-
fastapi_ok, fastapi_msg = self.check_fastapi_server()
|
| 100 |
-
hf_ok, hf_msg = self.check_hf_data_engine()
|
| 101 |
-
|
| 102 |
-
overview = f"""
|
| 103 |
-
# 🚀 Crypto Data Sources - System Overview
|
| 104 |
-
|
| 105 |
-
**Last Updated:** {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
|
| 106 |
-
|
| 107 |
-
## 🖥️ Main Systems
|
| 108 |
-
|
| 109 |
-
### FastAPI Backend ({self.fastapi_url})
|
| 110 |
-
{fastapi_msg}
|
| 111 |
-
|
| 112 |
-
### HF Data Engine ({self.hf_engine_url})
|
| 113 |
-
{hf_msg}
|
| 114 |
-
|
| 115 |
-
## 📊 Loaded Resources
|
| 116 |
-
|
| 117 |
-
- **Unified Resources:** {len(self.api_resources.get('unified', {}).get('registry', {}))} sources
|
| 118 |
-
- **Pipeline Resources:** {len(self.api_resources.get('pipeline', {}))} sources
|
| 119 |
-
- **Merged APIs:** {len(self.api_resources.get('merged', {}))} sources
|
| 120 |
-
|
| 121 |
-
## 📁 Resource Categories
|
| 122 |
-
|
| 123 |
-
"""
|
| 124 |
-
|
| 125 |
-
# Count categories from unified resources
|
| 126 |
-
if 'registry' in self.api_resources.get('unified', {}):
|
| 127 |
-
categories = {}
|
| 128 |
-
for source in self.api_resources['unified']['registry'].values():
|
| 129 |
-
for item in source:
|
| 130 |
-
cat = item.get('category', item.get('chain', item.get('role', 'unknown')))
|
| 131 |
-
categories[cat] = categories.get(cat, 0) + 1
|
| 132 |
-
|
| 133 |
-
for cat, count in sorted(categories.items()):
|
| 134 |
-
overview += f"- **{cat}:** {count} sources\n"
|
| 135 |
-
|
| 136 |
-
return overview
|
| 137 |
-
|
| 138 |
-
async def test_all_sources(self, progress=gr.Progress()) -> Tuple[str, pd.DataFrame]:
|
| 139 |
-
"""Test all data sources for accessibility"""
|
| 140 |
-
results = []
|
| 141 |
-
|
| 142 |
-
progress(0, desc="Loading resources...")
|
| 143 |
-
|
| 144 |
-
# Test unified resources
|
| 145 |
-
if 'registry' in self.api_resources.get('unified', {}):
|
| 146 |
-
registry = self.api_resources['unified']['registry']
|
| 147 |
-
total = sum(len(sources) for sources in registry.values())
|
| 148 |
-
current = 0
|
| 149 |
-
|
| 150 |
-
for source_type, sources in registry.items():
|
| 151 |
-
for source in sources:
|
| 152 |
-
current += 1
|
| 153 |
-
progress(current / total, desc=f"Testing {source.get('name', 'Unknown')}...")
|
| 154 |
-
|
| 155 |
-
name = source.get('name', 'Unknown')
|
| 156 |
-
base_url = source.get('base_url', '')
|
| 157 |
-
category = source.get('category', source.get('chain', source.get('role', 'unknown')))
|
| 158 |
-
|
| 159 |
-
if base_url:
|
| 160 |
-
is_healthy, latency, message = await self.check_endpoint_health(base_url)
|
| 161 |
-
status = "✅ Online" if is_healthy else "❌ Offline"
|
| 162 |
-
results.append({
|
| 163 |
-
"Name": name,
|
| 164 |
-
"Category": category,
|
| 165 |
-
"Status": status,
|
| 166 |
-
"Latency (ms)": f"{latency:.0f}" if is_healthy else "-",
|
| 167 |
-
"URL": base_url[:50] + "..." if len(base_url) > 50 else base_url,
|
| 168 |
-
"Message": message
|
| 169 |
-
})
|
| 170 |
-
|
| 171 |
-
await asyncio.sleep(0.1) # Rate limiting
|
| 172 |
-
|
| 173 |
-
df = pd.DataFrame(results) if results else pd.DataFrame()
|
| 174 |
-
|
| 175 |
-
summary = f"""
|
| 176 |
-
# ✅ Health Check Complete
|
| 177 |
-
|
| 178 |
-
**Total Sources Tested:** {len(results)}
|
| 179 |
-
**Online:** {len([r for r in results if '✅' in r['Status']])}
|
| 180 |
-
**Offline:** {len([r for r in results if '❌' in r['Status']])}
|
| 181 |
-
**Average Latency:** {sum(float(r['Latency (ms)']) for r in results if r['Latency (ms)'] != '-') / max(1, len([r for r in results if r['Latency (ms)'] != '-'])):.0f} ms
|
| 182 |
-
**Completed:** {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
|
| 183 |
-
"""
|
| 184 |
-
|
| 185 |
-
return summary, df
|
| 186 |
-
|
| 187 |
-
def test_fastapi_endpoints(self) -> Tuple[str, pd.DataFrame]:
|
| 188 |
-
"""Test all FastAPI endpoints"""
|
| 189 |
-
endpoints = [
|
| 190 |
-
("/health", "GET", "Health Check"),
|
| 191 |
-
("/api/status", "GET", "System Status"),
|
| 192 |
-
("/api/providers", "GET", "Provider List"),
|
| 193 |
-
("/api/pools", "GET", "Pool Management"),
|
| 194 |
-
("/api/hf/health", "GET", "HuggingFace Health"),
|
| 195 |
-
("/api/feature-flags", "GET", "Feature Flags"),
|
| 196 |
-
]
|
| 197 |
-
|
| 198 |
-
results = []
|
| 199 |
-
for endpoint, method, description in endpoints:
|
| 200 |
-
try:
|
| 201 |
-
url = f"{self.fastapi_url}{endpoint}"
|
| 202 |
-
response = httpx.get(url, timeout=5)
|
| 203 |
-
status = "✅ Working" if response.status_code < 400 else "⚠️ Error"
|
| 204 |
-
results.append({
|
| 205 |
-
"Endpoint": endpoint,
|
| 206 |
-
"Method": method,
|
| 207 |
-
"Description": description,
|
| 208 |
-
"Status": status,
|
| 209 |
-
"Status Code": response.status_code,
|
| 210 |
-
"Response Time": f"{response.elapsed.total_seconds() * 1000:.0f} ms"
|
| 211 |
-
})
|
| 212 |
-
except Exception as e:
|
| 213 |
-
results.append({
|
| 214 |
-
"Endpoint": endpoint,
|
| 215 |
-
"Method": method,
|
| 216 |
-
"Description": description,
|
| 217 |
-
"Status": "❌ Failed",
|
| 218 |
-
"Status Code": "-",
|
| 219 |
-
"Response Time": str(e)[:50]
|
| 220 |
-
})
|
| 221 |
-
|
| 222 |
-
df = pd.DataFrame(results)
|
| 223 |
-
summary = f"**Tested {len(results)} endpoints** - {len([r for r in results if '✅' in r['Status']])} working"
|
| 224 |
-
return summary, df
|
| 225 |
-
|
| 226 |
-
def
|
| 227 |
-
"""Test HF Data Engine endpoints"""
|
| 228 |
-
endpoints = [
|
| 229 |
-
("/api/health", "Health Check"),
|
| 230 |
-
("/api/prices?symbols=BTC,ETH", "Prices"),
|
| 231 |
-
("/api/ohlcv?symbol=BTC&interval=1h&limit=10", "OHLCV Data"),
|
| 232 |
-
("/api/sentiment", "Sentiment"),
|
| 233 |
-
("/api/market/overview", "Market Overview"),
|
| 234 |
-
]
|
| 235 |
-
|
| 236 |
-
results = []
|
| 237 |
-
for endpoint, description in endpoints:
|
| 238 |
-
try:
|
| 239 |
-
url = f"{self.hf_engine_url}{endpoint}"
|
| 240 |
-
start = time.time()
|
| 241 |
-
response = httpx.get(url, timeout=30)
|
| 242 |
-
latency = (time.time() - start) * 1000
|
| 243 |
-
|
| 244 |
-
status = "✅ Working" if response.status_code < 400 else "⚠️ Error"
|
| 245 |
-
|
| 246 |
-
# Get data preview
|
| 247 |
-
try:
|
| 248 |
-
data = response.json()
|
| 249 |
-
preview = str(data)[:100] + "..." if len(str(data)) > 100 else str(data)
|
| 250 |
-
except:
|
| 251 |
-
preview = "N/A"
|
| 252 |
-
|
| 253 |
-
results.append({
|
| 254 |
-
"Endpoint": endpoint.split("?")[0],
|
| 255 |
-
"Description": description,
|
| 256 |
-
"Status": status,
|
| 257 |
-
"Latency": f"{latency:.0f} ms",
|
| 258 |
-
"Preview": preview
|
| 259 |
-
})
|
| 260 |
-
except Exception as e:
|
| 261 |
-
results.append({
|
| 262 |
-
"Endpoint": endpoint.split("?")[0],
|
| 263 |
-
"Description": description,
|
| 264 |
-
"Status": "❌ Failed",
|
| 265 |
-
"Latency": "-",
|
| 266 |
-
"Preview": str(e)[:100]
|
| 267 |
-
})
|
| 268 |
-
|
| 269 |
-
df = pd.DataFrame(results)
|
| 270 |
-
working = len([r for r in results if '✅' in r['Status']])
|
| 271 |
-
summary = f"**Tested {len(results)} endpoints** - {working}/{len(results)} working"
|
| 272 |
-
return summary, df
|
| 273 |
-
|
| 274 |
-
def get_resource_details(self, resource_name: str) -> str:
|
| 275 |
-
"""Get detailed information about a specific resource"""
|
| 276 |
-
details = f"# 📋 Resource Details: {resource_name}\n\n"
|
| 277 |
-
|
| 278 |
-
# Search in all resource files
|
| 279 |
-
if 'registry' in self.api_resources.get('unified', {}):
|
| 280 |
-
for source_type, sources in self.api_resources['unified']['registry'].items():
|
| 281 |
-
for source in sources:
|
| 282 |
-
if source.get('name') == resource_name:
|
| 283 |
-
details += f"## Source Type: {source_type}\n\n"
|
| 284 |
-
details += f"```json\n{json.dumps(source, indent=2)}\n```\n"
|
| 285 |
-
return details
|
| 286 |
-
|
| 287 |
-
return f"Resource '{resource_name}' not found"
|
| 288 |
-
|
| 289 |
-
def get_statistics(self) -> str:
|
| 290 |
-
"""Get comprehensive statistics"""
|
| 291 |
-
stats = "# 📊 Comprehensive Statistics\n\n"
|
| 292 |
-
|
| 293 |
-
# Count all resources
|
| 294 |
-
total_unified = 0
|
| 295 |
-
if 'registry' in self.api_resources.get('unified', {}):
|
| 296 |
-
for sources in self.api_resources['unified']['registry'].values():
|
| 297 |
-
total_unified += len(sources)
|
| 298 |
-
|
| 299 |
-
total_pipeline = len(self.api_resources.get('pipeline', {}))
|
| 300 |
-
total_merged = len(self.api_resources.get('merged', {}))
|
| 301 |
-
|
| 302 |
-
stats += f"""
|
| 303 |
-
## Total Resources
|
| 304 |
-
- **Unified Resources:** {total_unified}
|
| 305 |
-
- **Pipeline Resources:** {total_pipeline}
|
| 306 |
-
- **Merged APIs:** {total_merged}
|
| 307 |
-
- **Grand Total:** {total_unified + total_pipeline + total_merged}
|
| 308 |
-
|
| 309 |
-
## By Category (Unified Resources)
|
| 310 |
-
"""
|
| 311 |
-
|
| 312 |
-
# Count by category
|
| 313 |
-
if 'registry' in self.api_resources.get('unified', {}):
|
| 314 |
-
categories = {}
|
| 315 |
-
for sources in self.api_resources['unified']['registry'].values():
|
| 316 |
-
for source in sources:
|
| 317 |
-
cat = source.get('category', source.get('chain', source.get('role', 'unknown')))
|
| 318 |
-
categories[cat] = categories.get(cat, 0) + 1
|
| 319 |
-
|
| 320 |
-
for cat, count in sorted(categories.items(), key=lambda x: x[1], reverse=True):
|
| 321 |
-
stats += f"- **{cat}:** {count}\n"
|
| 322 |
-
|
| 323 |
-
return stats
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
# Initialize monitor
|
| 327 |
-
monitor = CryptoResourceMonitor()
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
# Build Gradio Interface
|
| 331 |
-
with gr.Blocks(title="Crypto Data Sources Monitor", theme=gr.themes.Soft()) as demo:
|
| 332 |
-
gr.Markdown("""
|
| 333 |
-
# 🚀 Crypto Data Sources - Comprehensive Monitor
|
| 334 |
-
|
| 335 |
-
**Monitor health, accessibility, and functionality of all data sources**
|
| 336 |
-
|
| 337 |
-
This dashboard provides real-time monitoring and testing of:
|
| 338 |
-
- 200+ Free Crypto APIs and Data Sources
|
| 339 |
-
- FastAPI Backend Server
|
| 340 |
-
- HuggingFace Data Engine
|
| 341 |
-
- All endpoints and providers
|
| 342 |
-
""")
|
| 343 |
-
|
| 344 |
-
# Tab 1: System Overview
|
| 345 |
-
with gr.Tab("🏠 System Overview"):
|
| 346 |
-
overview_md = gr.Markdown(monitor.get_system_overview())
|
| 347 |
-
refresh_overview_btn = gr.Button("🔄 Refresh Overview", variant="primary")
|
| 348 |
-
refresh_overview_btn.click(
|
| 349 |
-
fn=lambda: monitor.get_system_overview(),
|
| 350 |
-
outputs=[overview_md]
|
| 351 |
-
)
|
| 352 |
-
|
| 353 |
-
# Tab 2: Health Check
|
| 354 |
-
with gr.Tab("🏥 Health Check"):
|
| 355 |
-
gr.Markdown("### Test all data sources for accessibility")
|
| 356 |
-
test_all_btn = gr.Button("🧪 Test All Sources", variant="primary", size="lg")
|
| 357 |
-
health_summary = gr.Markdown()
|
| 358 |
-
health_table = gr.Dataframe(
|
| 359 |
-
headers=["Name", "Category", "Status", "Latency (ms)", "URL", "Message"],
|
| 360 |
-
wrap=True
|
| 361 |
-
)
|
| 362 |
-
test_all_btn.click(
|
| 363 |
-
fn=monitor.test_all_sources,
|
| 364 |
-
outputs=[health_summary, health_table]
|
| 365 |
-
)
|
| 366 |
-
|
| 367 |
-
# Tab 3: FastAPI Endpoints
|
| 368 |
-
with gr.Tab("⚡ FastAPI Endpoints"):
|
| 369 |
-
gr.Markdown("### Test main application endpoints")
|
| 370 |
-
test_fastapi_btn = gr.Button("🧪 Test FastAPI Endpoints", variant="primary")
|
| 371 |
-
fastapi_summary = gr.Markdown()
|
| 372 |
-
fastapi_table = gr.Dataframe(wrap=True)
|
| 373 |
-
test_fastapi_btn.click(
|
| 374 |
-
fn=monitor.test_fastapi_endpoints,
|
| 375 |
-
outputs=[fastapi_summary, fastapi_table]
|
| 376 |
-
)
|
| 377 |
-
|
| 378 |
-
# Tab 4: HF Data Engine
|
| 379 |
-
with gr.Tab("🤗 HF Data Engine"):
|
| 380 |
-
gr.Markdown("### Test HuggingFace Data Engine")
|
| 381 |
-
test_hf_btn = gr.Button("🧪 Test HF Engine", variant="primary")
|
| 382 |
-
hf_summary = gr.Markdown()
|
| 383 |
-
hf_table = gr.Dataframe(wrap=True)
|
| 384 |
-
test_hf_btn.click(
|
| 385 |
-
fn=monitor.
|
| 386 |
-
outputs=[hf_summary, hf_table]
|
| 387 |
-
)
|
| 388 |
-
|
| 389 |
-
# Tab 5: Resource Explorer
|
| 390 |
-
with gr.Tab("🔍 Resource Explorer"):
|
| 391 |
-
gr.Markdown("### Explore API resources")
|
| 392 |
-
|
| 393 |
-
# Get list of all resource names
|
| 394 |
-
resource_names = []
|
| 395 |
-
if 'registry' in monitor.api_resources.get('unified', {}):
|
| 396 |
-
for sources in monitor.api_resources['unified']['registry'].values():
|
| 397 |
-
for source in sources:
|
| 398 |
-
resource_names.append(source.get('name', 'Unknown'))
|
| 399 |
-
|
| 400 |
-
resource_dropdown = gr.Dropdown(
|
| 401 |
-
choices=sorted(resource_names),
|
| 402 |
-
label="Select Resource",
|
| 403 |
-
interactive=True
|
| 404 |
-
)
|
| 405 |
-
resource_details = gr.Markdown()
|
| 406 |
-
resource_dropdown.change(
|
| 407 |
-
fn=monitor.get_resource_details,
|
| 408 |
-
inputs=[resource_dropdown],
|
| 409 |
-
outputs=[resource_details]
|
| 410 |
-
)
|
| 411 |
-
|
| 412 |
-
# Tab 6: Statistics
|
| 413 |
-
with gr.Tab("📊 Statistics"):
|
| 414 |
-
stats_md = gr.Markdown(monitor.get_statistics())
|
| 415 |
-
refresh_stats_btn = gr.Button("🔄 Refresh Statistics", variant="primary")
|
| 416 |
-
refresh_stats_btn.click(
|
| 417 |
-
fn=lambda: monitor.get_statistics(),
|
| 418 |
-
outputs=[stats_md]
|
| 419 |
-
)
|
| 420 |
-
|
| 421 |
-
# Tab 7: API Testing
|
| 422 |
-
with gr.Tab("🧪 API Testing"):
|
| 423 |
-
gr.Markdown("### Interactive API Testing")
|
| 424 |
-
|
| 425 |
-
with gr.Row():
|
| 426 |
-
with gr.Column():
|
| 427 |
-
api_url = gr.Textbox(
|
| 428 |
-
label="API URL",
|
| 429 |
-
placeholder="http://localhost:7860/api/status",
|
| 430 |
-
value="http://localhost:7860/api/status"
|
| 431 |
-
)
|
| 432 |
-
api_method = gr.Radio(
|
| 433 |
-
choices=["GET", "POST"],
|
| 434 |
-
label="Method",
|
| 435 |
-
value="GET"
|
| 436 |
-
)
|
| 437 |
-
test_api_btn = gr.Button("🚀 Test API", variant="primary")
|
| 438 |
-
|
| 439 |
-
with gr.Column():
|
| 440 |
-
api_response = gr.JSON(label="Response")
|
| 441 |
-
|
| 442 |
-
def test_custom_api(url: str, method: str):
|
| 443 |
-
try:
|
| 444 |
-
if method == "GET":
|
| 445 |
-
response = httpx.get(url, timeout=30)
|
| 446 |
-
else:
|
| 447 |
-
response = httpx.post(url, timeout=30)
|
| 448 |
-
|
| 449 |
-
return {
|
| 450 |
-
"status_code": response.status_code,
|
| 451 |
-
"headers": dict(response.headers),
|
| 452 |
-
"body": response.json() if response.headers.get("content-type", "").startswith("application/json") else response.text[:1000]
|
| 453 |
-
}
|
| 454 |
-
except Exception as e:
|
| 455 |
-
return {"error": str(e)}
|
| 456 |
-
|
| 457 |
-
test_api_btn.click(
|
| 458 |
-
fn=test_custom_api,
|
| 459 |
-
inputs=[api_url, api_method],
|
| 460 |
-
outputs=[api_response]
|
| 461 |
-
)
|
| 462 |
-
|
| 463 |
-
# Footer
|
| 464 |
-
gr.Markdown("""
|
| 465 |
-
---
|
| 466 |
-
**Crypto Data Sources Monitor** | Built with Gradio | Last Updated: 2024-11-14
|
| 467 |
-
""")
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
if __name__ == "__main__":
|
| 471 |
-
demo.launch(
|
| 472 |
-
server_name="0.0.0.0",
|
| 473 |
-
server_port=7861,
|
| 474 |
-
share=False,
|
| 475 |
-
show_error=True
|
| 476 |
-
)
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Comprehensive Gradio Dashboard for Crypto Data Sources
|
| 4 |
+
Monitors health, accessibility, and functionality of all data sources
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import gradio as gr
|
| 8 |
+
import httpx
|
| 9 |
+
import asyncio
|
| 10 |
+
import json
|
| 11 |
+
import time
|
| 12 |
+
from datetime import datetime
|
| 13 |
+
from typing import Dict, List, Tuple, Optional
|
| 14 |
+
import pandas as pd
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
import sys
|
| 17 |
+
import os
|
| 18 |
+
|
| 19 |
+
# Add project root to path
|
| 20 |
+
sys.path.insert(0, os.path.dirname(__file__))
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class CryptoResourceMonitor:
|
| 24 |
+
"""Monitor and test all crypto data sources"""
|
| 25 |
+
|
| 26 |
+
def __init__(self):
|
| 27 |
+
self.api_resources = self.load_api_resources()
|
| 28 |
+
self.health_cache = {}
|
| 29 |
+
self.last_check_time = None
|
| 30 |
+
self.fastapi_url = "http://localhost:7860"
|
| 31 |
+
self.hf_engine_url = "http://localhost:8000"
|
| 32 |
+
|
| 33 |
+
def load_api_resources(self) -> Dict:
|
| 34 |
+
"""Load all API resources from api-resources folder"""
|
| 35 |
+
resources = {
|
| 36 |
+
"unified": {},
|
| 37 |
+
"pipeline": {},
|
| 38 |
+
"merged": {}
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
try:
|
| 42 |
+
# Load unified resources
|
| 43 |
+
unified_path = Path("api-resources/crypto_resources_unified_2025-11-11.json")
|
| 44 |
+
if unified_path.exists():
|
| 45 |
+
with open(unified_path) as f:
|
| 46 |
+
resources["unified"] = json.load(f)
|
| 47 |
+
|
| 48 |
+
# Load pipeline
|
| 49 |
+
pipeline_path = Path("api-resources/ultimate_crypto_pipeline_2025_NZasinich.json")
|
| 50 |
+
if pipeline_path.exists():
|
| 51 |
+
with open(pipeline_path) as f:
|
| 52 |
+
resources["pipeline"] = json.load(f)
|
| 53 |
+
|
| 54 |
+
# Load merged APIs
|
| 55 |
+
merged_path = Path("all_apis_merged_2025.json")
|
| 56 |
+
if merged_path.exists():
|
| 57 |
+
with open(merged_path) as f:
|
| 58 |
+
resources["merged"] = json.load(f)
|
| 59 |
+
|
| 60 |
+
except Exception as e:
|
| 61 |
+
print(f"Error loading resources: {e}")
|
| 62 |
+
|
| 63 |
+
return resources
|
| 64 |
+
|
| 65 |
+
async def check_endpoint_health(self, url: str, timeout: int = 5) -> Tuple[bool, float, str]:
|
| 66 |
+
"""Check if an endpoint is accessible"""
|
| 67 |
+
start_time = time.time()
|
| 68 |
+
try:
|
| 69 |
+
async with httpx.AsyncClient(timeout=timeout) as client:
|
| 70 |
+
response = await client.get(url)
|
| 71 |
+
latency = (time.time() - start_time) * 1000
|
| 72 |
+
return response.status_code < 400, latency, f"Status: {response.status_code}"
|
| 73 |
+
except httpx.TimeoutException:
|
| 74 |
+
return False, timeout * 1000, "Timeout"
|
| 75 |
+
except Exception as e:
|
| 76 |
+
return False, 0, str(e)[:100]
|
| 77 |
+
|
| 78 |
+
def check_fastapi_server(self) -> Tuple[bool, str]:
|
| 79 |
+
"""Check if main FastAPI server is running"""
|
| 80 |
+
try:
|
| 81 |
+
response = httpx.get(f"{self.fastapi_url}/health", timeout=5)
|
| 82 |
+
return True, f"✅ Online (Status: {response.status_code})"
|
| 83 |
+
except:
|
| 84 |
+
return False, "❌ Offline"
|
| 85 |
+
|
| 86 |
+
def check_hf_data_engine(self) -> Tuple[bool, str]:
|
| 87 |
+
"""Check if HF Data Engine is running"""
|
| 88 |
+
try:
|
| 89 |
+
response = httpx.get(f"{self.hf_engine_url}/api/health", timeout=5)
|
| 90 |
+
data = response.json()
|
| 91 |
+
providers = len(data.get("providers", []))
|
| 92 |
+
uptime = data.get("uptime", 0)
|
| 93 |
+
return True, f"✅ Online ({providers} providers, uptime: {uptime}s)"
|
| 94 |
+
except:
|
| 95 |
+
return False, "❌ Offline"
|
| 96 |
+
|
| 97 |
+
def get_system_overview(self) -> str:
|
| 98 |
+
"""Get overview of all systems"""
|
| 99 |
+
fastapi_ok, fastapi_msg = self.check_fastapi_server()
|
| 100 |
+
hf_ok, hf_msg = self.check_hf_data_engine()
|
| 101 |
+
|
| 102 |
+
overview = f"""
|
| 103 |
+
# 🚀 Crypto Data Sources - System Overview
|
| 104 |
+
|
| 105 |
+
**Last Updated:** {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
|
| 106 |
+
|
| 107 |
+
## 🖥️ Main Systems
|
| 108 |
+
|
| 109 |
+
### FastAPI Backend ({self.fastapi_url})
|
| 110 |
+
{fastapi_msg}
|
| 111 |
+
|
| 112 |
+
### HF Data Engine ({self.hf_engine_url})
|
| 113 |
+
{hf_msg}
|
| 114 |
+
|
| 115 |
+
## 📊 Loaded Resources
|
| 116 |
+
|
| 117 |
+
- **Unified Resources:** {len(self.api_resources.get('unified', {}).get('registry', {}))} sources
|
| 118 |
+
- **Pipeline Resources:** {len(self.api_resources.get('pipeline', {}))} sources
|
| 119 |
+
- **Merged APIs:** {len(self.api_resources.get('merged', {}))} sources
|
| 120 |
+
|
| 121 |
+
## 📁 Resource Categories
|
| 122 |
+
|
| 123 |
+
"""
|
| 124 |
+
|
| 125 |
+
# Count categories from unified resources
|
| 126 |
+
if 'registry' in self.api_resources.get('unified', {}):
|
| 127 |
+
categories = {}
|
| 128 |
+
for source in self.api_resources['unified']['registry'].values():
|
| 129 |
+
for item in source:
|
| 130 |
+
cat = item.get('category', item.get('chain', item.get('role', 'unknown')))
|
| 131 |
+
categories[cat] = categories.get(cat, 0) + 1
|
| 132 |
+
|
| 133 |
+
for cat, count in sorted(categories.items()):
|
| 134 |
+
overview += f"- **{cat}:** {count} sources\n"
|
| 135 |
+
|
| 136 |
+
return overview
|
| 137 |
+
|
| 138 |
+
async def test_all_sources(self, progress=gr.Progress()) -> Tuple[str, pd.DataFrame]:
|
| 139 |
+
"""Test all data sources for accessibility"""
|
| 140 |
+
results = []
|
| 141 |
+
|
| 142 |
+
progress(0, desc="Loading resources...")
|
| 143 |
+
|
| 144 |
+
# Test unified resources
|
| 145 |
+
if 'registry' in self.api_resources.get('unified', {}):
|
| 146 |
+
registry = self.api_resources['unified']['registry']
|
| 147 |
+
total = sum(len(sources) for sources in registry.values())
|
| 148 |
+
current = 0
|
| 149 |
+
|
| 150 |
+
for source_type, sources in registry.items():
|
| 151 |
+
for source in sources:
|
| 152 |
+
current += 1
|
| 153 |
+
progress(current / total, desc=f"Testing {source.get('name', 'Unknown')}...")
|
| 154 |
+
|
| 155 |
+
name = source.get('name', 'Unknown')
|
| 156 |
+
base_url = source.get('base_url', '')
|
| 157 |
+
category = source.get('category', source.get('chain', source.get('role', 'unknown')))
|
| 158 |
+
|
| 159 |
+
if base_url:
|
| 160 |
+
is_healthy, latency, message = await self.check_endpoint_health(base_url)
|
| 161 |
+
status = "✅ Online" if is_healthy else "❌ Offline"
|
| 162 |
+
results.append({
|
| 163 |
+
"Name": name,
|
| 164 |
+
"Category": category,
|
| 165 |
+
"Status": status,
|
| 166 |
+
"Latency (ms)": f"{latency:.0f}" if is_healthy else "-",
|
| 167 |
+
"URL": base_url[:50] + "..." if len(base_url) > 50 else base_url,
|
| 168 |
+
"Message": message
|
| 169 |
+
})
|
| 170 |
+
|
| 171 |
+
await asyncio.sleep(0.1) # Rate limiting
|
| 172 |
+
|
| 173 |
+
df = pd.DataFrame(results) if results else pd.DataFrame()
|
| 174 |
+
|
| 175 |
+
summary = f"""
|
| 176 |
+
# ✅ Health Check Complete
|
| 177 |
+
|
| 178 |
+
**Total Sources Tested:** {len(results)}
|
| 179 |
+
**Online:** {len([r for r in results if '✅' in r['Status']])}
|
| 180 |
+
**Offline:** {len([r for r in results if '❌' in r['Status']])}
|
| 181 |
+
**Average Latency:** {sum(float(r['Latency (ms)']) for r in results if r['Latency (ms)'] != '-') / max(1, len([r for r in results if r['Latency (ms)'] != '-'])):.0f} ms
|
| 182 |
+
**Completed:** {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
|
| 183 |
+
"""
|
| 184 |
+
|
| 185 |
+
return summary, df
|
| 186 |
+
|
| 187 |
+
def test_fastapi_endpoints(self) -> Tuple[str, pd.DataFrame]:
|
| 188 |
+
"""Test all FastAPI endpoints"""
|
| 189 |
+
endpoints = [
|
| 190 |
+
("/health", "GET", "Health Check"),
|
| 191 |
+
("/api/status", "GET", "System Status"),
|
| 192 |
+
("/api/providers", "GET", "Provider List"),
|
| 193 |
+
("/api/pools", "GET", "Pool Management"),
|
| 194 |
+
("/api/hf/health", "GET", "HuggingFace Health"),
|
| 195 |
+
("/api/feature-flags", "GET", "Feature Flags"),
|
| 196 |
+
]
|
| 197 |
+
|
| 198 |
+
results = []
|
| 199 |
+
for endpoint, method, description in endpoints:
|
| 200 |
+
try:
|
| 201 |
+
url = f"{self.fastapi_url}{endpoint}"
|
| 202 |
+
response = httpx.get(url, timeout=5)
|
| 203 |
+
status = "✅ Working" if response.status_code < 400 else "⚠️ Error"
|
| 204 |
+
results.append({
|
| 205 |
+
"Endpoint": endpoint,
|
| 206 |
+
"Method": method,
|
| 207 |
+
"Description": description,
|
| 208 |
+
"Status": status,
|
| 209 |
+
"Status Code": response.status_code,
|
| 210 |
+
"Response Time": f"{response.elapsed.total_seconds() * 1000:.0f} ms"
|
| 211 |
+
})
|
| 212 |
+
except Exception as e:
|
| 213 |
+
results.append({
|
| 214 |
+
"Endpoint": endpoint,
|
| 215 |
+
"Method": method,
|
| 216 |
+
"Description": description,
|
| 217 |
+
"Status": "❌ Failed",
|
| 218 |
+
"Status Code": "-",
|
| 219 |
+
"Response Time": str(e)[:50]
|
| 220 |
+
})
|
| 221 |
+
|
| 222 |
+
df = pd.DataFrame(results)
|
| 223 |
+
summary = f"**Tested {len(results)} endpoints** - {len([r for r in results if '✅' in r['Status']])} working"
|
| 224 |
+
return summary, df
|
| 225 |
+
|
| 226 |
+
def test_hf_models(self) -> Tuple[str, pd.DataFrame]:
|
| 227 |
+
"""Test HF Data Engine endpoints"""
|
| 228 |
+
endpoints = [
|
| 229 |
+
("/api/health", "Health Check"),
|
| 230 |
+
("/api/prices?symbols=BTC,ETH", "Prices"),
|
| 231 |
+
("/api/ohlcv?symbol=BTC&interval=1h&limit=10", "OHLCV Data"),
|
| 232 |
+
("/api/sentiment", "Sentiment"),
|
| 233 |
+
("/api/market/overview", "Market Overview"),
|
| 234 |
+
]
|
| 235 |
+
|
| 236 |
+
results = []
|
| 237 |
+
for endpoint, description in endpoints:
|
| 238 |
+
try:
|
| 239 |
+
url = f"{self.hf_engine_url}{endpoint}"
|
| 240 |
+
start = time.time()
|
| 241 |
+
response = httpx.get(url, timeout=30)
|
| 242 |
+
latency = (time.time() - start) * 1000
|
| 243 |
+
|
| 244 |
+
status = "✅ Working" if response.status_code < 400 else "⚠️ Error"
|
| 245 |
+
|
| 246 |
+
# Get data preview
|
| 247 |
+
try:
|
| 248 |
+
data = response.json()
|
| 249 |
+
preview = str(data)[:100] + "..." if len(str(data)) > 100 else str(data)
|
| 250 |
+
except:
|
| 251 |
+
preview = "N/A"
|
| 252 |
+
|
| 253 |
+
results.append({
|
| 254 |
+
"Endpoint": endpoint.split("?")[0],
|
| 255 |
+
"Description": description,
|
| 256 |
+
"Status": status,
|
| 257 |
+
"Latency": f"{latency:.0f} ms",
|
| 258 |
+
"Preview": preview
|
| 259 |
+
})
|
| 260 |
+
except Exception as e:
|
| 261 |
+
results.append({
|
| 262 |
+
"Endpoint": endpoint.split("?")[0],
|
| 263 |
+
"Description": description,
|
| 264 |
+
"Status": "❌ Failed",
|
| 265 |
+
"Latency": "-",
|
| 266 |
+
"Preview": str(e)[:100]
|
| 267 |
+
})
|
| 268 |
+
|
| 269 |
+
df = pd.DataFrame(results)
|
| 270 |
+
working = len([r for r in results if '✅' in r['Status']])
|
| 271 |
+
summary = f"**Tested {len(results)} endpoints** - {working}/{len(results)} working"
|
| 272 |
+
return summary, df
|
| 273 |
+
|
| 274 |
+
def get_resource_details(self, resource_name: str) -> str:
|
| 275 |
+
"""Get detailed information about a specific resource"""
|
| 276 |
+
details = f"# 📋 Resource Details: {resource_name}\n\n"
|
| 277 |
+
|
| 278 |
+
# Search in all resource files
|
| 279 |
+
if 'registry' in self.api_resources.get('unified', {}):
|
| 280 |
+
for source_type, sources in self.api_resources['unified']['registry'].items():
|
| 281 |
+
for source in sources:
|
| 282 |
+
if source.get('name') == resource_name:
|
| 283 |
+
details += f"## Source Type: {source_type}\n\n"
|
| 284 |
+
details += f"```json\n{json.dumps(source, indent=2)}\n```\n"
|
| 285 |
+
return details
|
| 286 |
+
|
| 287 |
+
return f"Resource '{resource_name}' not found"
|
| 288 |
+
|
| 289 |
+
def get_statistics(self) -> str:
|
| 290 |
+
"""Get comprehensive statistics"""
|
| 291 |
+
stats = "# 📊 Comprehensive Statistics\n\n"
|
| 292 |
+
|
| 293 |
+
# Count all resources
|
| 294 |
+
total_unified = 0
|
| 295 |
+
if 'registry' in self.api_resources.get('unified', {}):
|
| 296 |
+
for sources in self.api_resources['unified']['registry'].values():
|
| 297 |
+
total_unified += len(sources)
|
| 298 |
+
|
| 299 |
+
total_pipeline = len(self.api_resources.get('pipeline', {}))
|
| 300 |
+
total_merged = len(self.api_resources.get('merged', {}))
|
| 301 |
+
|
| 302 |
+
stats += f"""
|
| 303 |
+
## Total Resources
|
| 304 |
+
- **Unified Resources:** {total_unified}
|
| 305 |
+
- **Pipeline Resources:** {total_pipeline}
|
| 306 |
+
- **Merged APIs:** {total_merged}
|
| 307 |
+
- **Grand Total:** {total_unified + total_pipeline + total_merged}
|
| 308 |
+
|
| 309 |
+
## By Category (Unified Resources)
|
| 310 |
+
"""
|
| 311 |
+
|
| 312 |
+
# Count by category
|
| 313 |
+
if 'registry' in self.api_resources.get('unified', {}):
|
| 314 |
+
categories = {}
|
| 315 |
+
for sources in self.api_resources['unified']['registry'].values():
|
| 316 |
+
for source in sources:
|
| 317 |
+
cat = source.get('category', source.get('chain', source.get('role', 'unknown')))
|
| 318 |
+
categories[cat] = categories.get(cat, 0) + 1
|
| 319 |
+
|
| 320 |
+
for cat, count in sorted(categories.items(), key=lambda x: x[1], reverse=True):
|
| 321 |
+
stats += f"- **{cat}:** {count}\n"
|
| 322 |
+
|
| 323 |
+
return stats
|
| 324 |
+
|
| 325 |
+
|
| 326 |
+
# Initialize monitor
|
| 327 |
+
monitor = CryptoResourceMonitor()
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
# Build Gradio Interface
|
| 331 |
+
with gr.Blocks(title="Crypto Data Sources Monitor", theme=gr.themes.Soft()) as demo:
|
| 332 |
+
gr.Markdown("""
|
| 333 |
+
# 🚀 Crypto Data Sources - Comprehensive Monitor
|
| 334 |
+
|
| 335 |
+
**Monitor health, accessibility, and functionality of all data sources**
|
| 336 |
+
|
| 337 |
+
This dashboard provides real-time monitoring and testing of:
|
| 338 |
+
- 200+ Free Crypto APIs and Data Sources
|
| 339 |
+
- FastAPI Backend Server
|
| 340 |
+
- HuggingFace Data Engine
|
| 341 |
+
- All endpoints and providers
|
| 342 |
+
""")
|
| 343 |
+
|
| 344 |
+
# Tab 1: System Overview
|
| 345 |
+
with gr.Tab("🏠 System Overview"):
|
| 346 |
+
overview_md = gr.Markdown(monitor.get_system_overview())
|
| 347 |
+
refresh_overview_btn = gr.Button("🔄 Refresh Overview", variant="primary")
|
| 348 |
+
refresh_overview_btn.click(
|
| 349 |
+
fn=lambda: monitor.get_system_overview(),
|
| 350 |
+
outputs=[overview_md]
|
| 351 |
+
)
|
| 352 |
+
|
| 353 |
+
# Tab 2: Health Check
|
| 354 |
+
with gr.Tab("🏥 Health Check"):
|
| 355 |
+
gr.Markdown("### Test all data sources for accessibility")
|
| 356 |
+
test_all_btn = gr.Button("🧪 Test All Sources", variant="primary", size="lg")
|
| 357 |
+
health_summary = gr.Markdown()
|
| 358 |
+
health_table = gr.Dataframe(
|
| 359 |
+
headers=["Name", "Category", "Status", "Latency (ms)", "URL", "Message"],
|
| 360 |
+
wrap=True
|
| 361 |
+
)
|
| 362 |
+
test_all_btn.click(
|
| 363 |
+
fn=monitor.test_all_sources,
|
| 364 |
+
outputs=[health_summary, health_table]
|
| 365 |
+
)
|
| 366 |
+
|
| 367 |
+
# Tab 3: FastAPI Endpoints
|
| 368 |
+
with gr.Tab("⚡ FastAPI Endpoints"):
|
| 369 |
+
gr.Markdown("### Test main application endpoints")
|
| 370 |
+
test_fastapi_btn = gr.Button("🧪 Test FastAPI Endpoints", variant="primary")
|
| 371 |
+
fastapi_summary = gr.Markdown()
|
| 372 |
+
fastapi_table = gr.Dataframe(wrap=True)
|
| 373 |
+
test_fastapi_btn.click(
|
| 374 |
+
fn=monitor.test_fastapi_endpoints,
|
| 375 |
+
outputs=[fastapi_summary, fastapi_table]
|
| 376 |
+
)
|
| 377 |
+
|
| 378 |
+
# Tab 4: HF Data Engine
|
| 379 |
+
with gr.Tab("🤗 HF Data Engine"):
|
| 380 |
+
gr.Markdown("### Test HuggingFace Data Engine")
|
| 381 |
+
test_hf_btn = gr.Button("🧪 Test HF Engine", variant="primary")
|
| 382 |
+
hf_summary = gr.Markdown()
|
| 383 |
+
hf_table = gr.Dataframe(wrap=True)
|
| 384 |
+
test_hf_btn.click(
|
| 385 |
+
fn=monitor.test_hf_models,
|
| 386 |
+
outputs=[hf_summary, hf_table]
|
| 387 |
+
)
|
| 388 |
+
|
| 389 |
+
# Tab 5: Resource Explorer
|
| 390 |
+
with gr.Tab("🔍 Resource Explorer"):
|
| 391 |
+
gr.Markdown("### Explore API resources")
|
| 392 |
+
|
| 393 |
+
# Get list of all resource names
|
| 394 |
+
resource_names = []
|
| 395 |
+
if 'registry' in monitor.api_resources.get('unified', {}):
|
| 396 |
+
for sources in monitor.api_resources['unified']['registry'].values():
|
| 397 |
+
for source in sources:
|
| 398 |
+
resource_names.append(source.get('name', 'Unknown'))
|
| 399 |
+
|
| 400 |
+
resource_dropdown = gr.Dropdown(
|
| 401 |
+
choices=sorted(resource_names),
|
| 402 |
+
label="Select Resource",
|
| 403 |
+
interactive=True
|
| 404 |
+
)
|
| 405 |
+
resource_details = gr.Markdown()
|
| 406 |
+
resource_dropdown.change(
|
| 407 |
+
fn=monitor.get_resource_details,
|
| 408 |
+
inputs=[resource_dropdown],
|
| 409 |
+
outputs=[resource_details]
|
| 410 |
+
)
|
| 411 |
+
|
| 412 |
+
# Tab 6: Statistics
|
| 413 |
+
with gr.Tab("📊 Statistics"):
|
| 414 |
+
stats_md = gr.Markdown(monitor.get_statistics())
|
| 415 |
+
refresh_stats_btn = gr.Button("🔄 Refresh Statistics", variant="primary")
|
| 416 |
+
refresh_stats_btn.click(
|
| 417 |
+
fn=lambda: monitor.get_statistics(),
|
| 418 |
+
outputs=[stats_md]
|
| 419 |
+
)
|
| 420 |
+
|
| 421 |
+
# Tab 7: API Testing
|
| 422 |
+
with gr.Tab("🧪 API Testing"):
|
| 423 |
+
gr.Markdown("### Interactive API Testing")
|
| 424 |
+
|
| 425 |
+
with gr.Row():
|
| 426 |
+
with gr.Column():
|
| 427 |
+
api_url = gr.Textbox(
|
| 428 |
+
label="API URL",
|
| 429 |
+
placeholder="http://localhost:7860/api/status",
|
| 430 |
+
value="http://localhost:7860/api/status"
|
| 431 |
+
)
|
| 432 |
+
api_method = gr.Radio(
|
| 433 |
+
choices=["GET", "POST"],
|
| 434 |
+
label="Method",
|
| 435 |
+
value="GET"
|
| 436 |
+
)
|
| 437 |
+
test_api_btn = gr.Button("🚀 Test API", variant="primary")
|
| 438 |
+
|
| 439 |
+
with gr.Column():
|
| 440 |
+
api_response = gr.JSON(label="Response")
|
| 441 |
+
|
| 442 |
+
def test_custom_api(url: str, method: str):
|
| 443 |
+
try:
|
| 444 |
+
if method == "GET":
|
| 445 |
+
response = httpx.get(url, timeout=30)
|
| 446 |
+
else:
|
| 447 |
+
response = httpx.post(url, timeout=30)
|
| 448 |
+
|
| 449 |
+
return {
|
| 450 |
+
"status_code": response.status_code,
|
| 451 |
+
"headers": dict(response.headers),
|
| 452 |
+
"body": response.json() if response.headers.get("content-type", "").startswith("application/json") else response.text[:1000]
|
| 453 |
+
}
|
| 454 |
+
except Exception as e:
|
| 455 |
+
return {"error": str(e)}
|
| 456 |
+
|
| 457 |
+
test_api_btn.click(
|
| 458 |
+
fn=test_custom_api,
|
| 459 |
+
inputs=[api_url, api_method],
|
| 460 |
+
outputs=[api_response]
|
| 461 |
+
)
|
| 462 |
+
|
| 463 |
+
# Footer
|
| 464 |
+
gr.Markdown("""
|
| 465 |
+
---
|
| 466 |
+
**Crypto Data Sources Monitor** | Built with Gradio | Last Updated: 2024-11-14
|
| 467 |
+
""")
|
| 468 |
+
|
| 469 |
+
|
| 470 |
+
if __name__ == "__main__":
|
| 471 |
+
demo.launch(
|
| 472 |
+
server_name="0.0.0.0",
|
| 473 |
+
server_port=7861,
|
| 474 |
+
share=False,
|
| 475 |
+
show_error=True
|
| 476 |
+
)
|
hf-data-engine/HUGGINGFACE_DIAGNOSTIC_GUIDE.md
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
hf-data-engine/api-resources/crypto_resources_unified_2025-11-11.json
CHANGED
|
@@ -348,7 +348,7 @@
|
|
| 348 |
"base_url": "https://api.etherscan.io/api",
|
| 349 |
"auth": {
|
| 350 |
"type": "apiKeyQuery",
|
| 351 |
-
"key": "
|
| 352 |
"param_name": "apikey"
|
| 353 |
},
|
| 354 |
"docs_url": "https://docs.etherscan.io",
|
|
@@ -368,7 +368,7 @@
|
|
| 368 |
"base_url": "https://api.etherscan.io/api",
|
| 369 |
"auth": {
|
| 370 |
"type": "apiKeyQuery",
|
| 371 |
-
"key": "
|
| 372 |
"param_name": "apikey"
|
| 373 |
},
|
| 374 |
"docs_url": "https://docs.etherscan.io",
|
|
@@ -463,7 +463,7 @@
|
|
| 463 |
"base_url": "https://api.bscscan.com/api",
|
| 464 |
"auth": {
|
| 465 |
"type": "apiKeyQuery",
|
| 466 |
-
"key": "
|
| 467 |
"param_name": "apikey"
|
| 468 |
},
|
| 469 |
"docs_url": "https://docs.bscscan.com",
|
|
@@ -552,7 +552,7 @@
|
|
| 552 |
"base_url": "https://apilist.tronscanapi.com/api",
|
| 553 |
"auth": {
|
| 554 |
"type": "apiKeyQuery",
|
| 555 |
-
"key": "
|
| 556 |
"param_name": "apiKey"
|
| 557 |
},
|
| 558 |
"docs_url": "https://github.com/tronscan/tronscan-frontend/blob/dev2019/document/api.md",
|
|
@@ -650,7 +650,7 @@
|
|
| 650 |
"base_url": "https://pro-api.coinmarketcap.com/v1",
|
| 651 |
"auth": {
|
| 652 |
"type": "apiKeyHeader",
|
| 653 |
-
"key": "
|
| 654 |
"header_name": "X-CMC_PRO_API_KEY"
|
| 655 |
},
|
| 656 |
"docs_url": "https://coinmarketcap.com/api/documentation/v1/",
|
|
@@ -668,7 +668,7 @@
|
|
| 668 |
"base_url": "https://pro-api.coinmarketcap.com/v1",
|
| 669 |
"auth": {
|
| 670 |
"type": "apiKeyHeader",
|
| 671 |
-
"key": "
|
| 672 |
"header_name": "X-CMC_PRO_API_KEY"
|
| 673 |
},
|
| 674 |
"docs_url": "https://coinmarketcap.com/api/documentation/v1/",
|
|
@@ -686,7 +686,7 @@
|
|
| 686 |
"base_url": "https://min-api.cryptocompare.com/data",
|
| 687 |
"auth": {
|
| 688 |
"type": "apiKeyQuery",
|
| 689 |
-
"key": "
|
| 690 |
"param_name": "api_key"
|
| 691 |
},
|
| 692 |
"docs_url": "https://min-api.cryptocompare.com/documentation",
|
|
@@ -883,7 +883,7 @@
|
|
| 883 |
"base_url": "https://min-api.cryptocompare.com",
|
| 884 |
"auth": {
|
| 885 |
"type": "apiKeyQuery",
|
| 886 |
-
"key": "
|
| 887 |
"param_name": "api_key"
|
| 888 |
},
|
| 889 |
"docs_url": null,
|
|
@@ -981,7 +981,7 @@
|
|
| 981 |
"base_url": "https://newsapi.org/v2",
|
| 982 |
"auth": {
|
| 983 |
"type": "apiKeyQuery",
|
| 984 |
-
"key": "
|
| 985 |
"param_name": "apiKey"
|
| 986 |
},
|
| 987 |
"docs_url": "https://newsapi.org/docs",
|
|
@@ -1693,13 +1693,13 @@
|
|
| 1693 |
],
|
| 1694 |
"hf_resources": [
|
| 1695 |
{
|
| 1696 |
-
"id": "HF_TOKEN_FROM_SPACE_SECRET",
|
| 1697 |
"type": "model",
|
| 1698 |
"name": "ElKulako/CryptoBERT",
|
| 1699 |
"base_url": "https://api-inference.huggingface.co/models/ElKulako/cryptobert",
|
| 1700 |
"auth": {
|
| 1701 |
"type": "apiKeyHeaderOptional",
|
| 1702 |
-
"key": "HF_TOKEN_FROM_SPACE_SECRET",
|
| 1703 |
"header_name": "Authorization"
|
| 1704 |
},
|
| 1705 |
"docs_url": "https://huggingface.co/ElKulako/cryptobert",
|
|
@@ -1709,13 +1709,13 @@
|
|
| 1709 |
"notes": "For sentiment analysis"
|
| 1710 |
},
|
| 1711 |
{
|
| 1712 |
-
"id": "HF_TOKEN_FROM_SPACE_SECRET",
|
| 1713 |
"type": "model",
|
| 1714 |
"name": "kk08/CryptoBERT",
|
| 1715 |
"base_url": "https://api-inference.huggingface.co/models/kk08/CryptoBERT",
|
| 1716 |
"auth": {
|
| 1717 |
"type": "apiKeyHeaderOptional",
|
| 1718 |
-
"key": "HF_TOKEN_FROM_SPACE_SECRET",
|
| 1719 |
"header_name": "Authorization"
|
| 1720 |
},
|
| 1721 |
"docs_url": "https://huggingface.co/kk08/CryptoBERT",
|
|
@@ -1725,7 +1725,7 @@
|
|
| 1725 |
"notes": "For sentiment analysis"
|
| 1726 |
},
|
| 1727 |
{
|
| 1728 |
-
"id": "
|
| 1729 |
"type": "dataset",
|
| 1730 |
"name": "linxy/CryptoCoin",
|
| 1731 |
"base_url": "https://huggingface.co/datasets/linxy/CryptoCoin/resolve/main",
|
|
@@ -1739,7 +1739,7 @@
|
|
| 1739 |
"notes": "26 symbols x 7 timeframes = 182 CSVs"
|
| 1740 |
},
|
| 1741 |
{
|
| 1742 |
-
"id": "
|
| 1743 |
"type": "dataset",
|
| 1744 |
"name": "WinkingFace/CryptoLM-Bitcoin-BTC-USDT",
|
| 1745 |
"base_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Bitcoin-BTC-USDT/resolve/main",
|
|
@@ -1754,7 +1754,7 @@
|
|
| 1754 |
"notes": null
|
| 1755 |
},
|
| 1756 |
{
|
| 1757 |
-
"id": "
|
| 1758 |
"type": "dataset",
|
| 1759 |
"name": "WinkingFace/CryptoLM-Ethereum-ETH-USDT",
|
| 1760 |
"base_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Ethereum-ETH-USDT/resolve/main",
|
|
@@ -1769,7 +1769,7 @@
|
|
| 1769 |
"notes": null
|
| 1770 |
},
|
| 1771 |
{
|
| 1772 |
-
"id": "
|
| 1773 |
"type": "dataset",
|
| 1774 |
"name": "WinkingFace/CryptoLM-Solana-SOL-USDT",
|
| 1775 |
"base_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Solana-SOL-USDT/resolve/main",
|
|
@@ -1781,7 +1781,7 @@
|
|
| 1781 |
"notes": null
|
| 1782 |
},
|
| 1783 |
{
|
| 1784 |
-
"id": "
|
| 1785 |
"type": "dataset",
|
| 1786 |
"name": "WinkingFace/CryptoLM-Ripple-XRP-USDT",
|
| 1787 |
"base_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Ripple-XRP-USDT/resolve/main",
|
|
@@ -1861,7 +1861,7 @@
|
|
| 1861 |
"notes": null
|
| 1862 |
},
|
| 1863 |
{
|
| 1864 |
-
"id": "HF_TOKEN_FROM_SPACE_SECRET",
|
| 1865 |
"category": "hf-model",
|
| 1866 |
"name": "HF Model: ElKulako/CryptoBERT",
|
| 1867 |
"base_url": "https://huggingface.co/ElKulako/cryptobert",
|
|
@@ -1872,7 +1872,7 @@
|
|
| 1872 |
"notes": null
|
| 1873 |
},
|
| 1874 |
{
|
| 1875 |
-
"id": "HF_TOKEN_FROM_SPACE_SECRET",
|
| 1876 |
"category": "hf-model",
|
| 1877 |
"name": "HF Model: kk08/CryptoBERT",
|
| 1878 |
"base_url": "https://huggingface.co/kk08/CryptoBERT",
|
|
@@ -1883,7 +1883,7 @@
|
|
| 1883 |
"notes": null
|
| 1884 |
},
|
| 1885 |
{
|
| 1886 |
-
"id": "
|
| 1887 |
"category": "hf-dataset",
|
| 1888 |
"name": "HF Dataset: linxy/CryptoCoin",
|
| 1889 |
"base_url": "https://huggingface.co/datasets/linxy/CryptoCoin",
|
|
@@ -2082,15 +2082,15 @@
|
|
| 2082 |
"source_files": [
|
| 2083 |
{
|
| 2084 |
"path": "/mnt/data/api - Copy.txt",
|
| 2085 |
-
"sha256": "
|
| 2086 |
},
|
| 2087 |
{
|
| 2088 |
"path": "/mnt/data/api-config-complete (1).txt",
|
| 2089 |
-
"sha256": "
|
| 2090 |
},
|
| 2091 |
{
|
| 2092 |
"path": "/mnt/data/crypto_resources_ultimate_2025.zip",
|
| 2093 |
-
"sha256": "
|
| 2094 |
"note": "contains crypto_resources.ts and more"
|
| 2095 |
}
|
| 2096 |
]
|
|
|
|
| 348 |
"base_url": "https://api.etherscan.io/api",
|
| 349 |
"auth": {
|
| 350 |
"type": "apiKeyQuery",
|
| 351 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 352 |
"param_name": "apikey"
|
| 353 |
},
|
| 354 |
"docs_url": "https://docs.etherscan.io",
|
|
|
|
| 368 |
"base_url": "https://api.etherscan.io/api",
|
| 369 |
"auth": {
|
| 370 |
"type": "apiKeyQuery",
|
| 371 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 372 |
"param_name": "apikey"
|
| 373 |
},
|
| 374 |
"docs_url": "https://docs.etherscan.io",
|
|
|
|
| 463 |
"base_url": "https://api.bscscan.com/api",
|
| 464 |
"auth": {
|
| 465 |
"type": "apiKeyQuery",
|
| 466 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 467 |
"param_name": "apikey"
|
| 468 |
},
|
| 469 |
"docs_url": "https://docs.bscscan.com",
|
|
|
|
| 552 |
"base_url": "https://apilist.tronscanapi.com/api",
|
| 553 |
"auth": {
|
| 554 |
"type": "apiKeyQuery",
|
| 555 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 556 |
"param_name": "apiKey"
|
| 557 |
},
|
| 558 |
"docs_url": "https://github.com/tronscan/tronscan-frontend/blob/dev2019/document/api.md",
|
|
|
|
| 650 |
"base_url": "https://pro-api.coinmarketcap.com/v1",
|
| 651 |
"auth": {
|
| 652 |
"type": "apiKeyHeader",
|
| 653 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 654 |
"header_name": "X-CMC_PRO_API_KEY"
|
| 655 |
},
|
| 656 |
"docs_url": "https://coinmarketcap.com/api/documentation/v1/",
|
|
|
|
| 668 |
"base_url": "https://pro-api.coinmarketcap.com/v1",
|
| 669 |
"auth": {
|
| 670 |
"type": "apiKeyHeader",
|
| 671 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 672 |
"header_name": "X-CMC_PRO_API_KEY"
|
| 673 |
},
|
| 674 |
"docs_url": "https://coinmarketcap.com/api/documentation/v1/",
|
|
|
|
| 686 |
"base_url": "https://min-api.cryptocompare.com/data",
|
| 687 |
"auth": {
|
| 688 |
"type": "apiKeyQuery",
|
| 689 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 690 |
"param_name": "api_key"
|
| 691 |
},
|
| 692 |
"docs_url": "https://min-api.cryptocompare.com/documentation",
|
|
|
|
| 883 |
"base_url": "https://min-api.cryptocompare.com",
|
| 884 |
"auth": {
|
| 885 |
"type": "apiKeyQuery",
|
| 886 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 887 |
"param_name": "api_key"
|
| 888 |
},
|
| 889 |
"docs_url": null,
|
|
|
|
| 981 |
"base_url": "https://newsapi.org/v2",
|
| 982 |
"auth": {
|
| 983 |
"type": "apiKeyQuery",
|
| 984 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 985 |
"param_name": "apiKey"
|
| 986 |
},
|
| 987 |
"docs_url": "https://newsapi.org/docs",
|
|
|
|
| 1693 |
],
|
| 1694 |
"hf_resources": [
|
| 1695 |
{
|
| 1696 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1697 |
"type": "model",
|
| 1698 |
"name": "ElKulako/CryptoBERT",
|
| 1699 |
"base_url": "https://api-inference.huggingface.co/models/ElKulako/cryptobert",
|
| 1700 |
"auth": {
|
| 1701 |
"type": "apiKeyHeaderOptional",
|
| 1702 |
+
"key": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1703 |
"header_name": "Authorization"
|
| 1704 |
},
|
| 1705 |
"docs_url": "https://huggingface.co/ElKulako/cryptobert",
|
|
|
|
| 1709 |
"notes": "For sentiment analysis"
|
| 1710 |
},
|
| 1711 |
{
|
| 1712 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1713 |
"type": "model",
|
| 1714 |
"name": "kk08/CryptoBERT",
|
| 1715 |
"base_url": "https://api-inference.huggingface.co/models/kk08/CryptoBERT",
|
| 1716 |
"auth": {
|
| 1717 |
"type": "apiKeyHeaderOptional",
|
| 1718 |
+
"key": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1719 |
"header_name": "Authorization"
|
| 1720 |
},
|
| 1721 |
"docs_url": "https://huggingface.co/kk08/CryptoBERT",
|
|
|
|
| 1725 |
"notes": "For sentiment analysis"
|
| 1726 |
},
|
| 1727 |
{
|
| 1728 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1729 |
"type": "dataset",
|
| 1730 |
"name": "linxy/CryptoCoin",
|
| 1731 |
"base_url": "https://huggingface.co/datasets/linxy/CryptoCoin/resolve/main",
|
|
|
|
| 1739 |
"notes": "26 symbols x 7 timeframes = 182 CSVs"
|
| 1740 |
},
|
| 1741 |
{
|
| 1742 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1743 |
"type": "dataset",
|
| 1744 |
"name": "WinkingFace/CryptoLM-Bitcoin-BTC-USDT",
|
| 1745 |
"base_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Bitcoin-BTC-USDT/resolve/main",
|
|
|
|
| 1754 |
"notes": null
|
| 1755 |
},
|
| 1756 |
{
|
| 1757 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1758 |
"type": "dataset",
|
| 1759 |
"name": "WinkingFace/CryptoLM-Ethereum-ETH-USDT",
|
| 1760 |
"base_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Ethereum-ETH-USDT/resolve/main",
|
|
|
|
| 1769 |
"notes": null
|
| 1770 |
},
|
| 1771 |
{
|
| 1772 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1773 |
"type": "dataset",
|
| 1774 |
"name": "WinkingFace/CryptoLM-Solana-SOL-USDT",
|
| 1775 |
"base_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Solana-SOL-USDT/resolve/main",
|
|
|
|
| 1781 |
"notes": null
|
| 1782 |
},
|
| 1783 |
{
|
| 1784 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1785 |
"type": "dataset",
|
| 1786 |
"name": "WinkingFace/CryptoLM-Ripple-XRP-USDT",
|
| 1787 |
"base_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Ripple-XRP-USDT/resolve/main",
|
|
|
|
| 1861 |
"notes": null
|
| 1862 |
},
|
| 1863 |
{
|
| 1864 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1865 |
"category": "hf-model",
|
| 1866 |
"name": "HF Model: ElKulako/CryptoBERT",
|
| 1867 |
"base_url": "https://huggingface.co/ElKulako/cryptobert",
|
|
|
|
| 1872 |
"notes": null
|
| 1873 |
},
|
| 1874 |
{
|
| 1875 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1876 |
"category": "hf-model",
|
| 1877 |
"name": "HF Model: kk08/CryptoBERT",
|
| 1878 |
"base_url": "https://huggingface.co/kk08/CryptoBERT",
|
|
|
|
| 1883 |
"notes": null
|
| 1884 |
},
|
| 1885 |
{
|
| 1886 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1887 |
"category": "hf-dataset",
|
| 1888 |
"name": "HF Dataset: linxy/CryptoCoin",
|
| 1889 |
"base_url": "https://huggingface.co/datasets/linxy/CryptoCoin",
|
|
|
|
| 2082 |
"source_files": [
|
| 2083 |
{
|
| 2084 |
"path": "/mnt/data/api - Copy.txt",
|
| 2085 |
+
"sha256": "20f9a3357a65c28a691990f89ad57f0de978600e65405fafe2c8b3c3502f6b77"
|
| 2086 |
},
|
| 2087 |
{
|
| 2088 |
"path": "/mnt/data/api-config-complete (1).txt",
|
| 2089 |
+
"sha256": "cb9f4c746f5b8a1d70824340425557e4483ad7a8e5396e0be67d68d671b23697"
|
| 2090 |
},
|
| 2091 |
{
|
| 2092 |
"path": "/mnt/data/crypto_resources_ultimate_2025.zip",
|
| 2093 |
+
"sha256": "5bb6f0ef790f09e23a88adbf4a4c0bc225183e896c3aa63416e53b1eec36ea87",
|
| 2094 |
"note": "contains crypto_resources.ts and more"
|
| 2095 |
}
|
| 2096 |
]
|
hf-data-engine/api-resources/ultimate_crypto_pipeline_2025_NZasinich.json
CHANGED
|
@@ -62,7 +62,7 @@ ultimate_crypto_pipeline_2025_NZasinich.json
|
|
| 62 |
"category": "Block Explorer",
|
| 63 |
"name": "TronScan",
|
| 64 |
"url": "https://api.tronscan.org/api",
|
| 65 |
-
"key": "
|
| 66 |
"free": false,
|
| 67 |
"desc": "TRON accounts."
|
| 68 |
},
|
|
@@ -87,7 +87,7 @@ ultimate_crypto_pipeline_2025_NZasinich.json
|
|
| 87 |
"category": "Block Explorer",
|
| 88 |
"name": "BscScan",
|
| 89 |
"url": "https://api.bscscan.com/api",
|
| 90 |
-
"key": "
|
| 91 |
"free": false,
|
| 92 |
"desc": "BSC balances."
|
| 93 |
},
|
|
@@ -111,7 +111,7 @@ ultimate_crypto_pipeline_2025_NZasinich.json
|
|
| 111 |
"category": "Block Explorer",
|
| 112 |
"name": "Etherscan",
|
| 113 |
"url": "https://api.etherscan.io/api",
|
| 114 |
-
"key": "
|
| 115 |
"free": false,
|
| 116 |
"desc": "ETH explorer."
|
| 117 |
},
|
|
@@ -119,7 +119,7 @@ ultimate_crypto_pipeline_2025_NZasinich.json
|
|
| 119 |
"category": "Block Explorer",
|
| 120 |
"name": "Etherscan Backup",
|
| 121 |
"url": "https://api.etherscan.io/api",
|
| 122 |
-
"key": "
|
| 123 |
"free": false,
|
| 124 |
"desc": "ETH backup."
|
| 125 |
},
|
|
@@ -252,7 +252,7 @@ ultimate_crypto_pipeline_2025_NZasinich.json
|
|
| 252 |
"category": "Market Data",
|
| 253 |
"name": "CoinMarketCap (User key)",
|
| 254 |
"url": "https://pro-api.coinmarketcap.com/v1",
|
| 255 |
-
"key": "
|
| 256 |
"free": false,
|
| 257 |
"rateLimit": "333/day"
|
| 258 |
},
|
|
@@ -483,7 +483,7 @@ ultimate_crypto_pipeline_2025_NZasinich.json
|
|
| 483 |
"content": "export interface CryptoResource { category: string; name: string; url: string; key: string; free: boolean; rateLimit?: string; desc: string; endpoint?: string; example?: string; params?: Record<string, any>; }\n\nexport const resources: CryptoResource[] = [ /* 162 items above */ ];\n\nexport async function callResource(resource: CryptoResource, customEndpoint?: string, params: Record<string, any> = {}): Promise<any> { let url = resource.url + (customEndpoint || resource.endpoint || ''); const query = new URLSearchParams(params).toString(); url += query ? `?${query}` : ''; const headers: HeadersInit = resource.key ? { Authorization: `Bearer ${resource.key}` } : {}; const res = await fetch(url, { headers }); if (!res.ok) throw new Error(`Failed: ${res.status}`); const data = await res.json(); if (!data || Object.keys(data).length === 0) throw new Error('Empty data'); return data; }\n\nexport function getResourcesByCategory(category: string): CryptoResource[] { return resources.filter(r => r.category === category); }"
|
| 484 |
},
|
| 485 |
{
|
| 486 |
-
"filename": "
|
| 487 |
"description": "Complete FastAPI + Hugging Face free data & sentiment pipeline (additive)",
|
| 488 |
"content": "from fastapi import FastAPI, APIRouter; from datasets import load_dataset; import pandas as pd; from transformers import pipeline; app = FastAPI(); router = APIRouter(prefix=\"/api/hf\"); # Full code from previous Cursor Agent prompt..."
|
| 489 |
},
|
|
|
|
| 62 |
"category": "Block Explorer",
|
| 63 |
"name": "TronScan",
|
| 64 |
"url": "https://api.tronscan.org/api",
|
| 65 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 66 |
"free": false,
|
| 67 |
"desc": "TRON accounts."
|
| 68 |
},
|
|
|
|
| 87 |
"category": "Block Explorer",
|
| 88 |
"name": "BscScan",
|
| 89 |
"url": "https://api.bscscan.com/api",
|
| 90 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 91 |
"free": false,
|
| 92 |
"desc": "BSC balances."
|
| 93 |
},
|
|
|
|
| 111 |
"category": "Block Explorer",
|
| 112 |
"name": "Etherscan",
|
| 113 |
"url": "https://api.etherscan.io/api",
|
| 114 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 115 |
"free": false,
|
| 116 |
"desc": "ETH explorer."
|
| 117 |
},
|
|
|
|
| 119 |
"category": "Block Explorer",
|
| 120 |
"name": "Etherscan Backup",
|
| 121 |
"url": "https://api.etherscan.io/api",
|
| 122 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 123 |
"free": false,
|
| 124 |
"desc": "ETH backup."
|
| 125 |
},
|
|
|
|
| 252 |
"category": "Market Data",
|
| 253 |
"name": "CoinMarketCap (User key)",
|
| 254 |
"url": "https://pro-api.coinmarketcap.com/v1",
|
| 255 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 256 |
"free": false,
|
| 257 |
"rateLimit": "333/day"
|
| 258 |
},
|
|
|
|
| 483 |
"content": "export interface CryptoResource { category: string; name: string; url: string; key: string; free: boolean; rateLimit?: string; desc: string; endpoint?: string; example?: string; params?: Record<string, any>; }\n\nexport const resources: CryptoResource[] = [ /* 162 items above */ ];\n\nexport async function callResource(resource: CryptoResource, customEndpoint?: string, params: Record<string, any> = {}): Promise<any> { let url = resource.url + (customEndpoint || resource.endpoint || ''); const query = new URLSearchParams(params).toString(); url += query ? `?${query}` : ''; const headers: HeadersInit = resource.key ? { Authorization: `Bearer ${resource.key}` } : {}; const res = await fetch(url, { headers }); if (!res.ok) throw new Error(`Failed: ${res.status}`); const data = await res.json(); if (!data || Object.keys(data).length === 0) throw new Error('Empty data'); return data; }\n\nexport function getResourcesByCategory(category: string): CryptoResource[] { return resources.filter(r => r.category === category); }"
|
| 484 |
},
|
| 485 |
{
|
| 486 |
+
"filename": "hf_unified_server.py",
|
| 487 |
"description": "Complete FastAPI + Hugging Face free data & sentiment pipeline (additive)",
|
| 488 |
"content": "from fastapi import FastAPI, APIRouter; from datasets import load_dataset; import pandas as pd; from transformers import pipeline; app = FastAPI(); router = APIRouter(prefix=\"/api/hf\"); # Full code from previous Cursor Agent prompt..."
|
| 489 |
},
|
hf-data-engine/api/ws_integration_services.py
CHANGED
|
@@ -1,334 +1,334 @@
|
|
| 1 |
-
"""
|
| 2 |
-
WebSocket API for Integration Services
|
| 3 |
-
|
| 4 |
-
This module provides WebSocket endpoints for integration services
|
| 5 |
-
including HuggingFace AI models and persistence operations.
|
| 6 |
-
"""
|
| 7 |
-
|
| 8 |
-
import asyncio
|
| 9 |
-
from datetime import datetime
|
| 10 |
-
from typing import Any, Dict
|
| 11 |
-
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
| 12 |
-
import logging
|
| 13 |
-
|
| 14 |
-
from backend.services.ws_service_manager import ws_manager, ServiceType
|
| 15 |
-
from backend.services.hf_registry import HFRegistry
|
| 16 |
-
from backend.services.hf_client import HFClient
|
| 17 |
-
from backend.services.persistence_service import PersistenceService
|
| 18 |
-
from config import Config
|
| 19 |
-
|
| 20 |
-
logger = logging.getLogger(__name__)
|
| 21 |
-
|
| 22 |
-
router = APIRouter()
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
# ============================================================================
|
| 26 |
-
# Integration Service Handlers
|
| 27 |
-
# ============================================================================
|
| 28 |
-
|
| 29 |
-
class IntegrationStreamers:
|
| 30 |
-
"""Handles data streaming for integration services"""
|
| 31 |
-
|
| 32 |
-
def __init__(self):
|
| 33 |
-
self.config = Config()
|
| 34 |
-
try:
|
| 35 |
-
self.hf_registry = HFRegistry()
|
| 36 |
-
except:
|
| 37 |
-
self.hf_registry = None
|
| 38 |
-
logger.warning("HFRegistry not available")
|
| 39 |
-
|
| 40 |
-
try:
|
| 41 |
-
self.hf_client = HFClient()
|
| 42 |
-
except:
|
| 43 |
-
self.hf_client = None
|
| 44 |
-
logger.warning("HFClient not available")
|
| 45 |
-
|
| 46 |
-
try:
|
| 47 |
-
self.persistence_service = PersistenceService()
|
| 48 |
-
except:
|
| 49 |
-
self.persistence_service = None
|
| 50 |
-
logger.warning("PersistenceService not available")
|
| 51 |
-
|
| 52 |
-
# ========================================================================
|
| 53 |
-
# HuggingFace Streaming
|
| 54 |
-
# ========================================================================
|
| 55 |
-
|
| 56 |
-
async def
|
| 57 |
-
"""Stream HuggingFace registry status"""
|
| 58 |
-
if not self.hf_registry:
|
| 59 |
-
return None
|
| 60 |
-
|
| 61 |
-
try:
|
| 62 |
-
status = self.hf_registry.get_status()
|
| 63 |
-
if status:
|
| 64 |
-
return {
|
| 65 |
-
"total_models": status.get("total_models", 0),
|
| 66 |
-
"total_datasets": status.get("total_datasets", 0),
|
| 67 |
-
"available_models": status.get("available_models", []),
|
| 68 |
-
"available_datasets": status.get("available_datasets", []),
|
| 69 |
-
"last_refresh": status.get("last_refresh"),
|
| 70 |
-
"timestamp": datetime.utcnow().isoformat()
|
| 71 |
-
}
|
| 72 |
-
except Exception as e:
|
| 73 |
-
logger.error(f"Error streaming HF registry status: {e}")
|
| 74 |
-
return None
|
| 75 |
-
|
| 76 |
-
async def stream_hf_model_usage(self):
|
| 77 |
-
"""Stream HuggingFace model usage statistics"""
|
| 78 |
-
if not self.hf_client:
|
| 79 |
-
return None
|
| 80 |
-
|
| 81 |
-
try:
|
| 82 |
-
usage = self.hf_client.get_usage_stats()
|
| 83 |
-
if usage:
|
| 84 |
-
return {
|
| 85 |
-
"total_requests": usage.get("total_requests", 0),
|
| 86 |
-
"successful_requests": usage.get("successful_requests", 0),
|
| 87 |
-
"failed_requests": usage.get("failed_requests", 0),
|
| 88 |
-
"average_latency": usage.get("average_latency"),
|
| 89 |
-
"model_usage": usage.get("model_usage", {}),
|
| 90 |
-
"timestamp": datetime.utcnow().isoformat()
|
| 91 |
-
}
|
| 92 |
-
except Exception as e:
|
| 93 |
-
logger.error(f"Error streaming HF model usage: {e}")
|
| 94 |
-
return None
|
| 95 |
-
|
| 96 |
-
async def stream_sentiment_results(self):
|
| 97 |
-
"""Stream real-time sentiment analysis results"""
|
| 98 |
-
if not self.hf_client:
|
| 99 |
-
return None
|
| 100 |
-
|
| 101 |
-
try:
|
| 102 |
-
# This would stream sentiment results as they're processed
|
| 103 |
-
results = self.hf_client.get_recent_results()
|
| 104 |
-
if results:
|
| 105 |
-
return {
|
| 106 |
-
"sentiment_results": results,
|
| 107 |
-
"timestamp": datetime.utcnow().isoformat()
|
| 108 |
-
}
|
| 109 |
-
except Exception as e:
|
| 110 |
-
logger.error(f"Error streaming sentiment results: {e}")
|
| 111 |
-
return None
|
| 112 |
-
|
| 113 |
-
async def stream_model_events(self):
|
| 114 |
-
"""Stream model loading and unloading events"""
|
| 115 |
-
if not self.hf_registry:
|
| 116 |
-
return None
|
| 117 |
-
|
| 118 |
-
try:
|
| 119 |
-
events = self.hf_registry.get_recent_events()
|
| 120 |
-
if events:
|
| 121 |
-
return {
|
| 122 |
-
"model_events": events,
|
| 123 |
-
"timestamp": datetime.utcnow().isoformat()
|
| 124 |
-
}
|
| 125 |
-
except Exception as e:
|
| 126 |
-
logger.error(f"Error streaming model events: {e}")
|
| 127 |
-
return None
|
| 128 |
-
|
| 129 |
-
# ========================================================================
|
| 130 |
-
# Persistence Service Streaming
|
| 131 |
-
# ========================================================================
|
| 132 |
-
|
| 133 |
-
async def stream_persistence_status(self):
|
| 134 |
-
"""Stream persistence service status"""
|
| 135 |
-
if not self.persistence_service:
|
| 136 |
-
return None
|
| 137 |
-
|
| 138 |
-
try:
|
| 139 |
-
status = self.persistence_service.get_status()
|
| 140 |
-
if status:
|
| 141 |
-
return {
|
| 142 |
-
"storage_location": status.get("storage_location"),
|
| 143 |
-
"total_records": status.get("total_records", 0),
|
| 144 |
-
"storage_size": status.get("storage_size"),
|
| 145 |
-
"last_save": status.get("last_save"),
|
| 146 |
-
"active_writers": status.get("active_writers", 0),
|
| 147 |
-
"timestamp": datetime.utcnow().isoformat()
|
| 148 |
-
}
|
| 149 |
-
except Exception as e:
|
| 150 |
-
logger.error(f"Error streaming persistence status: {e}")
|
| 151 |
-
return None
|
| 152 |
-
|
| 153 |
-
async def stream_save_events(self):
|
| 154 |
-
"""Stream data save events"""
|
| 155 |
-
if not self.persistence_service:
|
| 156 |
-
return None
|
| 157 |
-
|
| 158 |
-
try:
|
| 159 |
-
events = self.persistence_service.get_recent_saves()
|
| 160 |
-
if events:
|
| 161 |
-
return {
|
| 162 |
-
"save_events": events,
|
| 163 |
-
"timestamp": datetime.utcnow().isoformat()
|
| 164 |
-
}
|
| 165 |
-
except Exception as e:
|
| 166 |
-
logger.error(f"Error streaming save events: {e}")
|
| 167 |
-
return None
|
| 168 |
-
|
| 169 |
-
async def stream_export_progress(self):
|
| 170 |
-
"""Stream export operation progress"""
|
| 171 |
-
if not self.persistence_service:
|
| 172 |
-
return None
|
| 173 |
-
|
| 174 |
-
try:
|
| 175 |
-
progress = self.persistence_service.get_export_progress()
|
| 176 |
-
if progress:
|
| 177 |
-
return {
|
| 178 |
-
"export_operations": progress,
|
| 179 |
-
"timestamp": datetime.utcnow().isoformat()
|
| 180 |
-
}
|
| 181 |
-
except Exception as e:
|
| 182 |
-
logger.error(f"Error streaming export progress: {e}")
|
| 183 |
-
return None
|
| 184 |
-
|
| 185 |
-
async def stream_backup_events(self):
|
| 186 |
-
"""Stream backup creation events"""
|
| 187 |
-
if not self.persistence_service:
|
| 188 |
-
return None
|
| 189 |
-
|
| 190 |
-
try:
|
| 191 |
-
backups = self.persistence_service.get_recent_backups()
|
| 192 |
-
if backups:
|
| 193 |
-
return {
|
| 194 |
-
"backup_events": backups,
|
| 195 |
-
"timestamp": datetime.utcnow().isoformat()
|
| 196 |
-
}
|
| 197 |
-
except Exception as e:
|
| 198 |
-
logger.error(f"Error streaming backup events: {e}")
|
| 199 |
-
return None
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
# Global instance
|
| 203 |
-
integration_streamers = IntegrationStreamers()
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
# ============================================================================
|
| 207 |
-
# Background Streaming Tasks
|
| 208 |
-
# ============================================================================
|
| 209 |
-
|
| 210 |
-
async def start_integration_streams():
|
| 211 |
-
"""Start all integration stream tasks"""
|
| 212 |
-
logger.info("Starting integration WebSocket streams")
|
| 213 |
-
|
| 214 |
-
tasks = [
|
| 215 |
-
# HuggingFace Registry
|
| 216 |
-
asyncio.create_task(ws_manager.start_service_stream(
|
| 217 |
-
ServiceType.HUGGINGFACE,
|
| 218 |
-
integration_streamers.
|
| 219 |
-
interval=60.0 # 1 minute updates
|
| 220 |
-
)),
|
| 221 |
-
|
| 222 |
-
# Persistence Service
|
| 223 |
-
asyncio.create_task(ws_manager.start_service_stream(
|
| 224 |
-
ServiceType.PERSISTENCE,
|
| 225 |
-
integration_streamers.stream_persistence_status,
|
| 226 |
-
interval=30.0 # 30 second updates
|
| 227 |
-
)),
|
| 228 |
-
]
|
| 229 |
-
|
| 230 |
-
await asyncio.gather(*tasks, return_exceptions=True)
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
# ============================================================================
|
| 234 |
-
# WebSocket Endpoints
|
| 235 |
-
# ============================================================================
|
| 236 |
-
|
| 237 |
-
@router.websocket("/ws/integration")
|
| 238 |
-
async def websocket_integration_endpoint(websocket: WebSocket):
|
| 239 |
-
"""
|
| 240 |
-
Unified WebSocket endpoint for all integration services
|
| 241 |
-
|
| 242 |
-
Connection URL: ws://host:port/ws/integration
|
| 243 |
-
|
| 244 |
-
After connecting, send subscription messages:
|
| 245 |
-
{
|
| 246 |
-
"action": "subscribe",
|
| 247 |
-
"service": "huggingface" | "persistence" | "all"
|
| 248 |
-
}
|
| 249 |
-
|
| 250 |
-
To unsubscribe:
|
| 251 |
-
{
|
| 252 |
-
"action": "unsubscribe",
|
| 253 |
-
"service": "service_name"
|
| 254 |
-
}
|
| 255 |
-
"""
|
| 256 |
-
connection = await ws_manager.connect(websocket)
|
| 257 |
-
|
| 258 |
-
try:
|
| 259 |
-
while True:
|
| 260 |
-
data = await websocket.receive_json()
|
| 261 |
-
await ws_manager.handle_client_message(connection, data)
|
| 262 |
-
|
| 263 |
-
except WebSocketDisconnect:
|
| 264 |
-
logger.info(f"Integration client disconnected: {connection.client_id}")
|
| 265 |
-
except Exception as e:
|
| 266 |
-
logger.error(f"Integration WebSocket error: {e}")
|
| 267 |
-
finally:
|
| 268 |
-
await ws_manager.disconnect(connection.client_id)
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
@router.websocket("/ws/huggingface")
|
| 272 |
-
async def websocket_huggingface(websocket: WebSocket):
|
| 273 |
-
"""
|
| 274 |
-
Dedicated WebSocket endpoint for HuggingFace services
|
| 275 |
-
|
| 276 |
-
Auto-subscribes to huggingface service
|
| 277 |
-
"""
|
| 278 |
-
connection = await ws_manager.connect(websocket)
|
| 279 |
-
connection.subscribe(ServiceType.HUGGINGFACE)
|
| 280 |
-
|
| 281 |
-
try:
|
| 282 |
-
while True:
|
| 283 |
-
data = await websocket.receive_json()
|
| 284 |
-
await ws_manager.handle_client_message(connection, data)
|
| 285 |
-
except WebSocketDisconnect:
|
| 286 |
-
logger.info(f"HuggingFace client disconnected: {connection.client_id}")
|
| 287 |
-
except Exception as e:
|
| 288 |
-
logger.error(f"HuggingFace WebSocket error: {e}")
|
| 289 |
-
finally:
|
| 290 |
-
await ws_manager.disconnect(connection.client_id)
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
@router.websocket("/ws/persistence")
|
| 294 |
-
async def websocket_persistence(websocket: WebSocket):
|
| 295 |
-
"""
|
| 296 |
-
Dedicated WebSocket endpoint for persistence service
|
| 297 |
-
|
| 298 |
-
Auto-subscribes to persistence service
|
| 299 |
-
"""
|
| 300 |
-
connection = await ws_manager.connect(websocket)
|
| 301 |
-
connection.subscribe(ServiceType.PERSISTENCE)
|
| 302 |
-
|
| 303 |
-
try:
|
| 304 |
-
while True:
|
| 305 |
-
data = await websocket.receive_json()
|
| 306 |
-
await ws_manager.handle_client_message(connection, data)
|
| 307 |
-
except WebSocketDisconnect:
|
| 308 |
-
logger.info(f"Persistence client disconnected: {connection.client_id}")
|
| 309 |
-
except Exception as e:
|
| 310 |
-
logger.error(f"Persistence WebSocket error: {e}")
|
| 311 |
-
finally:
|
| 312 |
-
await ws_manager.disconnect(connection.client_id)
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
@router.websocket("/ws/ai")
|
| 316 |
-
async def websocket_ai(websocket: WebSocket):
|
| 317 |
-
"""
|
| 318 |
-
Dedicated WebSocket endpoint for AI/ML operations (alias for HuggingFace)
|
| 319 |
-
|
| 320 |
-
Auto-subscribes to huggingface service
|
| 321 |
-
"""
|
| 322 |
-
connection = await ws_manager.connect(websocket)
|
| 323 |
-
connection.subscribe(ServiceType.HUGGINGFACE)
|
| 324 |
-
|
| 325 |
-
try:
|
| 326 |
-
while True:
|
| 327 |
-
data = await websocket.receive_json()
|
| 328 |
-
await ws_manager.handle_client_message(connection, data)
|
| 329 |
-
except WebSocketDisconnect:
|
| 330 |
-
logger.info(f"AI client disconnected: {connection.client_id}")
|
| 331 |
-
except Exception as e:
|
| 332 |
-
logger.error(f"AI WebSocket error: {e}")
|
| 333 |
-
finally:
|
| 334 |
-
await ws_manager.disconnect(connection.client_id)
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
WebSocket API for Integration Services
|
| 3 |
+
|
| 4 |
+
This module provides WebSocket endpoints for integration services
|
| 5 |
+
including HuggingFace AI models and persistence operations.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import asyncio
|
| 9 |
+
from datetime import datetime
|
| 10 |
+
from typing import Any, Dict
|
| 11 |
+
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
| 12 |
+
import logging
|
| 13 |
+
|
| 14 |
+
from backend.services.ws_service_manager import ws_manager, ServiceType
|
| 15 |
+
from backend.services.hf_registry import HFRegistry
|
| 16 |
+
from backend.services.hf_client import HFClient
|
| 17 |
+
from backend.services.persistence_service import PersistenceService
|
| 18 |
+
from config import Config
|
| 19 |
+
|
| 20 |
+
logger = logging.getLogger(__name__)
|
| 21 |
+
|
| 22 |
+
router = APIRouter()
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
# ============================================================================
|
| 26 |
+
# Integration Service Handlers
|
| 27 |
+
# ============================================================================
|
| 28 |
+
|
| 29 |
+
class IntegrationStreamers:
|
| 30 |
+
"""Handles data streaming for integration services"""
|
| 31 |
+
|
| 32 |
+
def __init__(self):
|
| 33 |
+
self.config = Config()
|
| 34 |
+
try:
|
| 35 |
+
self.hf_registry = HFRegistry()
|
| 36 |
+
except:
|
| 37 |
+
self.hf_registry = None
|
| 38 |
+
logger.warning("HFRegistry not available")
|
| 39 |
+
|
| 40 |
+
try:
|
| 41 |
+
self.hf_client = HFClient()
|
| 42 |
+
except:
|
| 43 |
+
self.hf_client = None
|
| 44 |
+
logger.warning("HFClient not available")
|
| 45 |
+
|
| 46 |
+
try:
|
| 47 |
+
self.persistence_service = PersistenceService()
|
| 48 |
+
except:
|
| 49 |
+
self.persistence_service = None
|
| 50 |
+
logger.warning("PersistenceService not available")
|
| 51 |
+
|
| 52 |
+
# ========================================================================
|
| 53 |
+
# HuggingFace Streaming
|
| 54 |
+
# ========================================================================
|
| 55 |
+
|
| 56 |
+
async def stream_hf(self):
|
| 57 |
+
"""Stream HuggingFace registry status"""
|
| 58 |
+
if not self.hf_registry:
|
| 59 |
+
return None
|
| 60 |
+
|
| 61 |
+
try:
|
| 62 |
+
status = self.hf_registry.get_status()
|
| 63 |
+
if status:
|
| 64 |
+
return {
|
| 65 |
+
"total_models": status.get("total_models", 0),
|
| 66 |
+
"total_datasets": status.get("total_datasets", 0),
|
| 67 |
+
"available_models": status.get("available_models", []),
|
| 68 |
+
"available_datasets": status.get("available_datasets", []),
|
| 69 |
+
"last_refresh": status.get("last_refresh"),
|
| 70 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 71 |
+
}
|
| 72 |
+
except Exception as e:
|
| 73 |
+
logger.error(f"Error streaming HF registry status: {e}")
|
| 74 |
+
return None
|
| 75 |
+
|
| 76 |
+
async def stream_hf_model_usage(self):
|
| 77 |
+
"""Stream HuggingFace model usage statistics"""
|
| 78 |
+
if not self.hf_client:
|
| 79 |
+
return None
|
| 80 |
+
|
| 81 |
+
try:
|
| 82 |
+
usage = self.hf_client.get_usage_stats()
|
| 83 |
+
if usage:
|
| 84 |
+
return {
|
| 85 |
+
"total_requests": usage.get("total_requests", 0),
|
| 86 |
+
"successful_requests": usage.get("successful_requests", 0),
|
| 87 |
+
"failed_requests": usage.get("failed_requests", 0),
|
| 88 |
+
"average_latency": usage.get("average_latency"),
|
| 89 |
+
"model_usage": usage.get("model_usage", {}),
|
| 90 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 91 |
+
}
|
| 92 |
+
except Exception as e:
|
| 93 |
+
logger.error(f"Error streaming HF model usage: {e}")
|
| 94 |
+
return None
|
| 95 |
+
|
| 96 |
+
async def stream_sentiment_results(self):
|
| 97 |
+
"""Stream real-time sentiment analysis results"""
|
| 98 |
+
if not self.hf_client:
|
| 99 |
+
return None
|
| 100 |
+
|
| 101 |
+
try:
|
| 102 |
+
# This would stream sentiment results as they're processed
|
| 103 |
+
results = self.hf_client.get_recent_results()
|
| 104 |
+
if results:
|
| 105 |
+
return {
|
| 106 |
+
"sentiment_results": results,
|
| 107 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 108 |
+
}
|
| 109 |
+
except Exception as e:
|
| 110 |
+
logger.error(f"Error streaming sentiment results: {e}")
|
| 111 |
+
return None
|
| 112 |
+
|
| 113 |
+
async def stream_model_events(self):
|
| 114 |
+
"""Stream model loading and unloading events"""
|
| 115 |
+
if not self.hf_registry:
|
| 116 |
+
return None
|
| 117 |
+
|
| 118 |
+
try:
|
| 119 |
+
events = self.hf_registry.get_recent_events()
|
| 120 |
+
if events:
|
| 121 |
+
return {
|
| 122 |
+
"model_events": events,
|
| 123 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 124 |
+
}
|
| 125 |
+
except Exception as e:
|
| 126 |
+
logger.error(f"Error streaming model events: {e}")
|
| 127 |
+
return None
|
| 128 |
+
|
| 129 |
+
# ========================================================================
|
| 130 |
+
# Persistence Service Streaming
|
| 131 |
+
# ========================================================================
|
| 132 |
+
|
| 133 |
+
async def stream_persistence_status(self):
|
| 134 |
+
"""Stream persistence service status"""
|
| 135 |
+
if not self.persistence_service:
|
| 136 |
+
return None
|
| 137 |
+
|
| 138 |
+
try:
|
| 139 |
+
status = self.persistence_service.get_status()
|
| 140 |
+
if status:
|
| 141 |
+
return {
|
| 142 |
+
"storage_location": status.get("storage_location"),
|
| 143 |
+
"total_records": status.get("total_records", 0),
|
| 144 |
+
"storage_size": status.get("storage_size"),
|
| 145 |
+
"last_save": status.get("last_save"),
|
| 146 |
+
"active_writers": status.get("active_writers", 0),
|
| 147 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 148 |
+
}
|
| 149 |
+
except Exception as e:
|
| 150 |
+
logger.error(f"Error streaming persistence status: {e}")
|
| 151 |
+
return None
|
| 152 |
+
|
| 153 |
+
async def stream_save_events(self):
|
| 154 |
+
"""Stream data save events"""
|
| 155 |
+
if not self.persistence_service:
|
| 156 |
+
return None
|
| 157 |
+
|
| 158 |
+
try:
|
| 159 |
+
events = self.persistence_service.get_recent_saves()
|
| 160 |
+
if events:
|
| 161 |
+
return {
|
| 162 |
+
"save_events": events,
|
| 163 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 164 |
+
}
|
| 165 |
+
except Exception as e:
|
| 166 |
+
logger.error(f"Error streaming save events: {e}")
|
| 167 |
+
return None
|
| 168 |
+
|
| 169 |
+
async def stream_export_progress(self):
|
| 170 |
+
"""Stream export operation progress"""
|
| 171 |
+
if not self.persistence_service:
|
| 172 |
+
return None
|
| 173 |
+
|
| 174 |
+
try:
|
| 175 |
+
progress = self.persistence_service.get_export_progress()
|
| 176 |
+
if progress:
|
| 177 |
+
return {
|
| 178 |
+
"export_operations": progress,
|
| 179 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 180 |
+
}
|
| 181 |
+
except Exception as e:
|
| 182 |
+
logger.error(f"Error streaming export progress: {e}")
|
| 183 |
+
return None
|
| 184 |
+
|
| 185 |
+
async def stream_backup_events(self):
|
| 186 |
+
"""Stream backup creation events"""
|
| 187 |
+
if not self.persistence_service:
|
| 188 |
+
return None
|
| 189 |
+
|
| 190 |
+
try:
|
| 191 |
+
backups = self.persistence_service.get_recent_backups()
|
| 192 |
+
if backups:
|
| 193 |
+
return {
|
| 194 |
+
"backup_events": backups,
|
| 195 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 196 |
+
}
|
| 197 |
+
except Exception as e:
|
| 198 |
+
logger.error(f"Error streaming backup events: {e}")
|
| 199 |
+
return None
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
# Global instance
|
| 203 |
+
integration_streamers = IntegrationStreamers()
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
# ============================================================================
|
| 207 |
+
# Background Streaming Tasks
|
| 208 |
+
# ============================================================================
|
| 209 |
+
|
| 210 |
+
async def start_integration_streams():
|
| 211 |
+
"""Start all integration stream tasks"""
|
| 212 |
+
logger.info("Starting integration WebSocket streams")
|
| 213 |
+
|
| 214 |
+
tasks = [
|
| 215 |
+
# HuggingFace Registry
|
| 216 |
+
asyncio.create_task(ws_manager.start_service_stream(
|
| 217 |
+
ServiceType.HUGGINGFACE,
|
| 218 |
+
integration_streamers.stream_hf,
|
| 219 |
+
interval=60.0 # 1 minute updates
|
| 220 |
+
)),
|
| 221 |
+
|
| 222 |
+
# Persistence Service
|
| 223 |
+
asyncio.create_task(ws_manager.start_service_stream(
|
| 224 |
+
ServiceType.PERSISTENCE,
|
| 225 |
+
integration_streamers.stream_persistence_status,
|
| 226 |
+
interval=30.0 # 30 second updates
|
| 227 |
+
)),
|
| 228 |
+
]
|
| 229 |
+
|
| 230 |
+
await asyncio.gather(*tasks, return_exceptions=True)
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
# ============================================================================
|
| 234 |
+
# WebSocket Endpoints
|
| 235 |
+
# ============================================================================
|
| 236 |
+
|
| 237 |
+
@router.websocket("/ws/integration")
|
| 238 |
+
async def websocket_integration_endpoint(websocket: WebSocket):
|
| 239 |
+
"""
|
| 240 |
+
Unified WebSocket endpoint for all integration services
|
| 241 |
+
|
| 242 |
+
Connection URL: ws://host:port/ws/integration
|
| 243 |
+
|
| 244 |
+
After connecting, send subscription messages:
|
| 245 |
+
{
|
| 246 |
+
"action": "subscribe",
|
| 247 |
+
"service": "huggingface" | "persistence" | "all"
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
To unsubscribe:
|
| 251 |
+
{
|
| 252 |
+
"action": "unsubscribe",
|
| 253 |
+
"service": "service_name"
|
| 254 |
+
}
|
| 255 |
+
"""
|
| 256 |
+
connection = await ws_manager.connect(websocket)
|
| 257 |
+
|
| 258 |
+
try:
|
| 259 |
+
while True:
|
| 260 |
+
data = await websocket.receive_json()
|
| 261 |
+
await ws_manager.handle_client_message(connection, data)
|
| 262 |
+
|
| 263 |
+
except WebSocketDisconnect:
|
| 264 |
+
logger.info(f"Integration client disconnected: {connection.client_id}")
|
| 265 |
+
except Exception as e:
|
| 266 |
+
logger.error(f"Integration WebSocket error: {e}")
|
| 267 |
+
finally:
|
| 268 |
+
await ws_manager.disconnect(connection.client_id)
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
@router.websocket("/ws/huggingface")
|
| 272 |
+
async def websocket_huggingface(websocket: WebSocket):
|
| 273 |
+
"""
|
| 274 |
+
Dedicated WebSocket endpoint for HuggingFace services
|
| 275 |
+
|
| 276 |
+
Auto-subscribes to huggingface service
|
| 277 |
+
"""
|
| 278 |
+
connection = await ws_manager.connect(websocket)
|
| 279 |
+
connection.subscribe(ServiceType.HUGGINGFACE)
|
| 280 |
+
|
| 281 |
+
try:
|
| 282 |
+
while True:
|
| 283 |
+
data = await websocket.receive_json()
|
| 284 |
+
await ws_manager.handle_client_message(connection, data)
|
| 285 |
+
except WebSocketDisconnect:
|
| 286 |
+
logger.info(f"HuggingFace client disconnected: {connection.client_id}")
|
| 287 |
+
except Exception as e:
|
| 288 |
+
logger.error(f"HuggingFace WebSocket error: {e}")
|
| 289 |
+
finally:
|
| 290 |
+
await ws_manager.disconnect(connection.client_id)
|
| 291 |
+
|
| 292 |
+
|
| 293 |
+
@router.websocket("/ws/persistence")
|
| 294 |
+
async def websocket_persistence(websocket: WebSocket):
|
| 295 |
+
"""
|
| 296 |
+
Dedicated WebSocket endpoint for persistence service
|
| 297 |
+
|
| 298 |
+
Auto-subscribes to persistence service
|
| 299 |
+
"""
|
| 300 |
+
connection = await ws_manager.connect(websocket)
|
| 301 |
+
connection.subscribe(ServiceType.PERSISTENCE)
|
| 302 |
+
|
| 303 |
+
try:
|
| 304 |
+
while True:
|
| 305 |
+
data = await websocket.receive_json()
|
| 306 |
+
await ws_manager.handle_client_message(connection, data)
|
| 307 |
+
except WebSocketDisconnect:
|
| 308 |
+
logger.info(f"Persistence client disconnected: {connection.client_id}")
|
| 309 |
+
except Exception as e:
|
| 310 |
+
logger.error(f"Persistence WebSocket error: {e}")
|
| 311 |
+
finally:
|
| 312 |
+
await ws_manager.disconnect(connection.client_id)
|
| 313 |
+
|
| 314 |
+
|
| 315 |
+
@router.websocket("/ws/ai")
|
| 316 |
+
async def websocket_ai(websocket: WebSocket):
|
| 317 |
+
"""
|
| 318 |
+
Dedicated WebSocket endpoint for AI/ML operations (alias for HuggingFace)
|
| 319 |
+
|
| 320 |
+
Auto-subscribes to huggingface service
|
| 321 |
+
"""
|
| 322 |
+
connection = await ws_manager.connect(websocket)
|
| 323 |
+
connection.subscribe(ServiceType.HUGGINGFACE)
|
| 324 |
+
|
| 325 |
+
try:
|
| 326 |
+
while True:
|
| 327 |
+
data = await websocket.receive_json()
|
| 328 |
+
await ws_manager.handle_client_message(connection, data)
|
| 329 |
+
except WebSocketDisconnect:
|
| 330 |
+
logger.info(f"AI client disconnected: {connection.client_id}")
|
| 331 |
+
except Exception as e:
|
| 332 |
+
logger.error(f"AI WebSocket error: {e}")
|
| 333 |
+
finally:
|
| 334 |
+
await ws_manager.disconnect(connection.client_id)
|
hf-data-engine/backend/routers/hf_connect.py
CHANGED
|
@@ -1,35 +1,35 @@
|
|
| 1 |
-
from __future__ import annotations
|
| 2 |
-
from fastapi import APIRouter, Query, Body
|
| 3 |
-
from typing import Literal, List
|
| 4 |
-
from backend.services.hf_registry import REGISTRY
|
| 5 |
-
from backend.services.hf_client import run_sentiment
|
| 6 |
-
|
| 7 |
-
router = APIRouter(prefix="/api/hf", tags=["huggingface"])
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
@router.get("/health")
|
| 11 |
-
async def hf_health():
|
| 12 |
-
return REGISTRY.health()
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
@router.post("/refresh")
|
| 16 |
-
async def hf_refresh():
|
| 17 |
-
return await REGISTRY.refresh()
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
@router.get("/registry")
|
| 21 |
-
async def hf_registry(kind: Literal["models","datasets"]="models"):
|
| 22 |
-
return {"kind": kind, "items": REGISTRY.list(kind)}
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
@router.get("/search")
|
| 26 |
-
async def hf_search(q: str = Query("crypto"), kind: Literal["models","datasets"]="models"):
|
| 27 |
-
hay = REGISTRY.list(kind)
|
| 28 |
-
ql = q.lower()
|
| 29 |
-
res = [x for x in hay if ql in (x.get("id","").lower() + " " + " ".join([str(t) for t in x.get("tags",[])]).lower())]
|
| 30 |
-
return {"query": q, "kind": kind, "count": len(res), "items": res[:50]}
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
@router.post("/run-sentiment")
|
| 34 |
-
async def
|
| 35 |
-
return run_sentiment(texts, model=model)
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
from fastapi import APIRouter, Query, Body
|
| 3 |
+
from typing import Literal, List
|
| 4 |
+
from backend.services.hf_registry import REGISTRY
|
| 5 |
+
from backend.services.hf_client import run_sentiment
|
| 6 |
+
|
| 7 |
+
router = APIRouter(prefix="/api/hf", tags=["huggingface"])
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
@router.get("/health")
|
| 11 |
+
async def hf_health():
|
| 12 |
+
return REGISTRY.health()
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@router.post("/refresh")
|
| 16 |
+
async def hf_refresh():
|
| 17 |
+
return await REGISTRY.refresh()
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@router.get("/registry")
|
| 21 |
+
async def hf_registry(kind: Literal["models","datasets"]="models"):
|
| 22 |
+
return {"kind": kind, "items": REGISTRY.list(kind)}
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
@router.get("/search")
|
| 26 |
+
async def hf_search(q: str = Query("crypto"), kind: Literal["models","datasets"]="models"):
|
| 27 |
+
hay = REGISTRY.list(kind)
|
| 28 |
+
ql = q.lower()
|
| 29 |
+
res = [x for x in hay if ql in (x.get("id","").lower() + " " + " ".join([str(t) for t in x.get("tags",[])]).lower())]
|
| 30 |
+
return {"query": q, "kind": kind, "count": len(res), "items": res[:50]}
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
@router.post("/run-sentiment")
|
| 34 |
+
async def hf_batch(texts: List[str] = Body(..., embed=True), model: str | None = Body(default=None)):
|
| 35 |
+
return run_sentiment(texts, model=model)
|
hf-data-engine/crypto_resources_unified_2025-11-11.json
CHANGED
|
@@ -348,7 +348,7 @@
|
|
| 348 |
"base_url": "https://api.etherscan.io/api",
|
| 349 |
"auth": {
|
| 350 |
"type": "apiKeyQuery",
|
| 351 |
-
"key": "
|
| 352 |
"param_name": "apikey"
|
| 353 |
},
|
| 354 |
"docs_url": "https://docs.etherscan.io",
|
|
@@ -368,7 +368,7 @@
|
|
| 368 |
"base_url": "https://api.etherscan.io/api",
|
| 369 |
"auth": {
|
| 370 |
"type": "apiKeyQuery",
|
| 371 |
-
"key": "
|
| 372 |
"param_name": "apikey"
|
| 373 |
},
|
| 374 |
"docs_url": "https://docs.etherscan.io",
|
|
@@ -463,7 +463,7 @@
|
|
| 463 |
"base_url": "https://api.bscscan.com/api",
|
| 464 |
"auth": {
|
| 465 |
"type": "apiKeyQuery",
|
| 466 |
-
"key": "
|
| 467 |
"param_name": "apikey"
|
| 468 |
},
|
| 469 |
"docs_url": "https://docs.bscscan.com",
|
|
@@ -552,7 +552,7 @@
|
|
| 552 |
"base_url": "https://apilist.tronscanapi.com/api",
|
| 553 |
"auth": {
|
| 554 |
"type": "apiKeyQuery",
|
| 555 |
-
"key": "
|
| 556 |
"param_name": "apiKey"
|
| 557 |
},
|
| 558 |
"docs_url": "https://github.com/tronscan/tronscan-frontend/blob/dev2019/document/api.md",
|
|
@@ -650,7 +650,7 @@
|
|
| 650 |
"base_url": "https://pro-api.coinmarketcap.com/v1",
|
| 651 |
"auth": {
|
| 652 |
"type": "apiKeyHeader",
|
| 653 |
-
"key": "
|
| 654 |
"header_name": "X-CMC_PRO_API_KEY"
|
| 655 |
},
|
| 656 |
"docs_url": "https://coinmarketcap.com/api/documentation/v1/",
|
|
@@ -668,7 +668,7 @@
|
|
| 668 |
"base_url": "https://pro-api.coinmarketcap.com/v1",
|
| 669 |
"auth": {
|
| 670 |
"type": "apiKeyHeader",
|
| 671 |
-
"key": "
|
| 672 |
"header_name": "X-CMC_PRO_API_KEY"
|
| 673 |
},
|
| 674 |
"docs_url": "https://coinmarketcap.com/api/documentation/v1/",
|
|
@@ -686,7 +686,7 @@
|
|
| 686 |
"base_url": "https://min-api.cryptocompare.com/data",
|
| 687 |
"auth": {
|
| 688 |
"type": "apiKeyQuery",
|
| 689 |
-
"key": "
|
| 690 |
"param_name": "api_key"
|
| 691 |
},
|
| 692 |
"docs_url": "https://min-api.cryptocompare.com/documentation",
|
|
@@ -883,7 +883,7 @@
|
|
| 883 |
"base_url": "https://min-api.cryptocompare.com",
|
| 884 |
"auth": {
|
| 885 |
"type": "apiKeyQuery",
|
| 886 |
-
"key": "
|
| 887 |
"param_name": "api_key"
|
| 888 |
},
|
| 889 |
"docs_url": null,
|
|
@@ -981,7 +981,7 @@
|
|
| 981 |
"base_url": "https://newsapi.org/v2",
|
| 982 |
"auth": {
|
| 983 |
"type": "apiKeyQuery",
|
| 984 |
-
"key": "
|
| 985 |
"param_name": "apiKey"
|
| 986 |
},
|
| 987 |
"docs_url": "https://newsapi.org/docs",
|
|
@@ -1693,13 +1693,13 @@
|
|
| 1693 |
],
|
| 1694 |
"hf_resources": [
|
| 1695 |
{
|
| 1696 |
-
"id": "HF_TOKEN_FROM_SPACE_SECRET",
|
| 1697 |
"type": "model",
|
| 1698 |
"name": "ElKulako/CryptoBERT",
|
| 1699 |
"base_url": "https://api-inference.huggingface.co/models/ElKulako/cryptobert",
|
| 1700 |
"auth": {
|
| 1701 |
"type": "apiKeyHeaderOptional",
|
| 1702 |
-
"key": "HF_TOKEN_FROM_SPACE_SECRET",
|
| 1703 |
"header_name": "Authorization"
|
| 1704 |
},
|
| 1705 |
"docs_url": "https://huggingface.co/ElKulako/cryptobert",
|
|
@@ -1709,13 +1709,13 @@
|
|
| 1709 |
"notes": "For sentiment analysis"
|
| 1710 |
},
|
| 1711 |
{
|
| 1712 |
-
"id": "HF_TOKEN_FROM_SPACE_SECRET",
|
| 1713 |
"type": "model",
|
| 1714 |
"name": "kk08/CryptoBERT",
|
| 1715 |
"base_url": "https://api-inference.huggingface.co/models/kk08/CryptoBERT",
|
| 1716 |
"auth": {
|
| 1717 |
"type": "apiKeyHeaderOptional",
|
| 1718 |
-
"key": "HF_TOKEN_FROM_SPACE_SECRET",
|
| 1719 |
"header_name": "Authorization"
|
| 1720 |
},
|
| 1721 |
"docs_url": "https://huggingface.co/kk08/CryptoBERT",
|
|
@@ -1725,7 +1725,7 @@
|
|
| 1725 |
"notes": "For sentiment analysis"
|
| 1726 |
},
|
| 1727 |
{
|
| 1728 |
-
"id": "
|
| 1729 |
"type": "dataset",
|
| 1730 |
"name": "linxy/CryptoCoin",
|
| 1731 |
"base_url": "https://huggingface.co/datasets/linxy/CryptoCoin/resolve/main",
|
|
@@ -1739,7 +1739,7 @@
|
|
| 1739 |
"notes": "26 symbols x 7 timeframes = 182 CSVs"
|
| 1740 |
},
|
| 1741 |
{
|
| 1742 |
-
"id": "
|
| 1743 |
"type": "dataset",
|
| 1744 |
"name": "WinkingFace/CryptoLM-Bitcoin-BTC-USDT",
|
| 1745 |
"base_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Bitcoin-BTC-USDT/resolve/main",
|
|
@@ -1754,7 +1754,7 @@
|
|
| 1754 |
"notes": null
|
| 1755 |
},
|
| 1756 |
{
|
| 1757 |
-
"id": "
|
| 1758 |
"type": "dataset",
|
| 1759 |
"name": "WinkingFace/CryptoLM-Ethereum-ETH-USDT",
|
| 1760 |
"base_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Ethereum-ETH-USDT/resolve/main",
|
|
@@ -1769,7 +1769,7 @@
|
|
| 1769 |
"notes": null
|
| 1770 |
},
|
| 1771 |
{
|
| 1772 |
-
"id": "
|
| 1773 |
"type": "dataset",
|
| 1774 |
"name": "WinkingFace/CryptoLM-Solana-SOL-USDT",
|
| 1775 |
"base_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Solana-SOL-USDT/resolve/main",
|
|
@@ -1781,7 +1781,7 @@
|
|
| 1781 |
"notes": null
|
| 1782 |
},
|
| 1783 |
{
|
| 1784 |
-
"id": "
|
| 1785 |
"type": "dataset",
|
| 1786 |
"name": "WinkingFace/CryptoLM-Ripple-XRP-USDT",
|
| 1787 |
"base_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Ripple-XRP-USDT/resolve/main",
|
|
@@ -1861,7 +1861,7 @@
|
|
| 1861 |
"notes": null
|
| 1862 |
},
|
| 1863 |
{
|
| 1864 |
-
"id": "HF_TOKEN_FROM_SPACE_SECRET",
|
| 1865 |
"category": "hf-model",
|
| 1866 |
"name": "HF Model: ElKulako/CryptoBERT",
|
| 1867 |
"base_url": "https://huggingface.co/ElKulako/cryptobert",
|
|
@@ -1872,7 +1872,7 @@
|
|
| 1872 |
"notes": null
|
| 1873 |
},
|
| 1874 |
{
|
| 1875 |
-
"id": "HF_TOKEN_FROM_SPACE_SECRET",
|
| 1876 |
"category": "hf-model",
|
| 1877 |
"name": "HF Model: kk08/CryptoBERT",
|
| 1878 |
"base_url": "https://huggingface.co/kk08/CryptoBERT",
|
|
@@ -1883,7 +1883,7 @@
|
|
| 1883 |
"notes": null
|
| 1884 |
},
|
| 1885 |
{
|
| 1886 |
-
"id": "
|
| 1887 |
"category": "hf-dataset",
|
| 1888 |
"name": "HF Dataset: linxy/CryptoCoin",
|
| 1889 |
"base_url": "https://huggingface.co/datasets/linxy/CryptoCoin",
|
|
@@ -2082,15 +2082,15 @@
|
|
| 2082 |
"source_files": [
|
| 2083 |
{
|
| 2084 |
"path": "/mnt/data/api - Copy.txt",
|
| 2085 |
-
"sha256": "
|
| 2086 |
},
|
| 2087 |
{
|
| 2088 |
"path": "/mnt/data/api-config-complete (1).txt",
|
| 2089 |
-
"sha256": "
|
| 2090 |
},
|
| 2091 |
{
|
| 2092 |
"path": "/mnt/data/crypto_resources_ultimate_2025.zip",
|
| 2093 |
-
"sha256": "
|
| 2094 |
"note": "contains crypto_resources.ts and more"
|
| 2095 |
}
|
| 2096 |
]
|
|
|
|
| 348 |
"base_url": "https://api.etherscan.io/api",
|
| 349 |
"auth": {
|
| 350 |
"type": "apiKeyQuery",
|
| 351 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 352 |
"param_name": "apikey"
|
| 353 |
},
|
| 354 |
"docs_url": "https://docs.etherscan.io",
|
|
|
|
| 368 |
"base_url": "https://api.etherscan.io/api",
|
| 369 |
"auth": {
|
| 370 |
"type": "apiKeyQuery",
|
| 371 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 372 |
"param_name": "apikey"
|
| 373 |
},
|
| 374 |
"docs_url": "https://docs.etherscan.io",
|
|
|
|
| 463 |
"base_url": "https://api.bscscan.com/api",
|
| 464 |
"auth": {
|
| 465 |
"type": "apiKeyQuery",
|
| 466 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 467 |
"param_name": "apikey"
|
| 468 |
},
|
| 469 |
"docs_url": "https://docs.bscscan.com",
|
|
|
|
| 552 |
"base_url": "https://apilist.tronscanapi.com/api",
|
| 553 |
"auth": {
|
| 554 |
"type": "apiKeyQuery",
|
| 555 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 556 |
"param_name": "apiKey"
|
| 557 |
},
|
| 558 |
"docs_url": "https://github.com/tronscan/tronscan-frontend/blob/dev2019/document/api.md",
|
|
|
|
| 650 |
"base_url": "https://pro-api.coinmarketcap.com/v1",
|
| 651 |
"auth": {
|
| 652 |
"type": "apiKeyHeader",
|
| 653 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 654 |
"header_name": "X-CMC_PRO_API_KEY"
|
| 655 |
},
|
| 656 |
"docs_url": "https://coinmarketcap.com/api/documentation/v1/",
|
|
|
|
| 668 |
"base_url": "https://pro-api.coinmarketcap.com/v1",
|
| 669 |
"auth": {
|
| 670 |
"type": "apiKeyHeader",
|
| 671 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 672 |
"header_name": "X-CMC_PRO_API_KEY"
|
| 673 |
},
|
| 674 |
"docs_url": "https://coinmarketcap.com/api/documentation/v1/",
|
|
|
|
| 686 |
"base_url": "https://min-api.cryptocompare.com/data",
|
| 687 |
"auth": {
|
| 688 |
"type": "apiKeyQuery",
|
| 689 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 690 |
"param_name": "api_key"
|
| 691 |
},
|
| 692 |
"docs_url": "https://min-api.cryptocompare.com/documentation",
|
|
|
|
| 883 |
"base_url": "https://min-api.cryptocompare.com",
|
| 884 |
"auth": {
|
| 885 |
"type": "apiKeyQuery",
|
| 886 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 887 |
"param_name": "api_key"
|
| 888 |
},
|
| 889 |
"docs_url": null,
|
|
|
|
| 981 |
"base_url": "https://newsapi.org/v2",
|
| 982 |
"auth": {
|
| 983 |
"type": "apiKeyQuery",
|
| 984 |
+
"key": "<API_KEY_FROM_SPACE_SECRET>",
|
| 985 |
"param_name": "apiKey"
|
| 986 |
},
|
| 987 |
"docs_url": "https://newsapi.org/docs",
|
|
|
|
| 1693 |
],
|
| 1694 |
"hf_resources": [
|
| 1695 |
{
|
| 1696 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1697 |
"type": "model",
|
| 1698 |
"name": "ElKulako/CryptoBERT",
|
| 1699 |
"base_url": "https://api-inference.huggingface.co/models/ElKulako/cryptobert",
|
| 1700 |
"auth": {
|
| 1701 |
"type": "apiKeyHeaderOptional",
|
| 1702 |
+
"key": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1703 |
"header_name": "Authorization"
|
| 1704 |
},
|
| 1705 |
"docs_url": "https://huggingface.co/ElKulako/cryptobert",
|
|
|
|
| 1709 |
"notes": "For sentiment analysis"
|
| 1710 |
},
|
| 1711 |
{
|
| 1712 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1713 |
"type": "model",
|
| 1714 |
"name": "kk08/CryptoBERT",
|
| 1715 |
"base_url": "https://api-inference.huggingface.co/models/kk08/CryptoBERT",
|
| 1716 |
"auth": {
|
| 1717 |
"type": "apiKeyHeaderOptional",
|
| 1718 |
+
"key": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1719 |
"header_name": "Authorization"
|
| 1720 |
},
|
| 1721 |
"docs_url": "https://huggingface.co/kk08/CryptoBERT",
|
|
|
|
| 1725 |
"notes": "For sentiment analysis"
|
| 1726 |
},
|
| 1727 |
{
|
| 1728 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1729 |
"type": "dataset",
|
| 1730 |
"name": "linxy/CryptoCoin",
|
| 1731 |
"base_url": "https://huggingface.co/datasets/linxy/CryptoCoin/resolve/main",
|
|
|
|
| 1739 |
"notes": "26 symbols x 7 timeframes = 182 CSVs"
|
| 1740 |
},
|
| 1741 |
{
|
| 1742 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1743 |
"type": "dataset",
|
| 1744 |
"name": "WinkingFace/CryptoLM-Bitcoin-BTC-USDT",
|
| 1745 |
"base_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Bitcoin-BTC-USDT/resolve/main",
|
|
|
|
| 1754 |
"notes": null
|
| 1755 |
},
|
| 1756 |
{
|
| 1757 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1758 |
"type": "dataset",
|
| 1759 |
"name": "WinkingFace/CryptoLM-Ethereum-ETH-USDT",
|
| 1760 |
"base_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Ethereum-ETH-USDT/resolve/main",
|
|
|
|
| 1769 |
"notes": null
|
| 1770 |
},
|
| 1771 |
{
|
| 1772 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1773 |
"type": "dataset",
|
| 1774 |
"name": "WinkingFace/CryptoLM-Solana-SOL-USDT",
|
| 1775 |
"base_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Solana-SOL-USDT/resolve/main",
|
|
|
|
| 1781 |
"notes": null
|
| 1782 |
},
|
| 1783 |
{
|
| 1784 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1785 |
"type": "dataset",
|
| 1786 |
"name": "WinkingFace/CryptoLM-Ripple-XRP-USDT",
|
| 1787 |
"base_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Ripple-XRP-USDT/resolve/main",
|
|
|
|
| 1861 |
"notes": null
|
| 1862 |
},
|
| 1863 |
{
|
| 1864 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1865 |
"category": "hf-model",
|
| 1866 |
"name": "HF Model: ElKulako/CryptoBERT",
|
| 1867 |
"base_url": "https://huggingface.co/ElKulako/cryptobert",
|
|
|
|
| 1872 |
"notes": null
|
| 1873 |
},
|
| 1874 |
{
|
| 1875 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1876 |
"category": "hf-model",
|
| 1877 |
"name": "HF Model: kk08/CryptoBERT",
|
| 1878 |
"base_url": "https://huggingface.co/kk08/CryptoBERT",
|
|
|
|
| 1883 |
"notes": null
|
| 1884 |
},
|
| 1885 |
{
|
| 1886 |
+
"id": "<HF_TOKEN_FROM_SPACE_SECRET>",
|
| 1887 |
"category": "hf-dataset",
|
| 1888 |
"name": "HF Dataset: linxy/CryptoCoin",
|
| 1889 |
"base_url": "https://huggingface.co/datasets/linxy/CryptoCoin",
|
|
|
|
| 2082 |
"source_files": [
|
| 2083 |
{
|
| 2084 |
"path": "/mnt/data/api - Copy.txt",
|
| 2085 |
+
"sha256": "20f9a3357a65c28a691990f89ad57f0de978600e65405fafe2c8b3c3502f6b77"
|
| 2086 |
},
|
| 2087 |
{
|
| 2088 |
"path": "/mnt/data/api-config-complete (1).txt",
|
| 2089 |
+
"sha256": "cb9f4c746f5b8a1d70824340425557e4483ad7a8e5396e0be67d68d671b23697"
|
| 2090 |
},
|
| 2091 |
{
|
| 2092 |
"path": "/mnt/data/crypto_resources_ultimate_2025.zip",
|
| 2093 |
+
"sha256": "5bb6f0ef790f09e23a88adbf4a4c0bc225183e896c3aa63416e53b1eec36ea87",
|
| 2094 |
"note": "contains crypto_resources.ts and more"
|
| 2095 |
}
|
| 2096 |
]
|