Market and analysis pages (#114)
Browse files* feat: Add advanced technical indicators and improve data fetching
This commit enhances the technical analysis module by integrating several new indicators, including Bollinger Bands, Stochastic RSI, ATR, and SMAs. It also refines the data fetching from CoinGecko to include more comprehensive coin details and improves the fallback data with accurate image URLs. The analysis and recommendation logic has been updated to leverage these new indicators for more robust insights.
Co-authored-by: bxsfy712 <bxsfy712@outlook.com>
* feat: Add technical indicators API and UI
Integrates a new API for technical indicators and updates the UI to display them.
Co-authored-by: bxsfy712 <bxsfy712@outlook.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: bxsfy712 <bxsfy712@outlook.com>
- backend/routers/indicators_api.py +1131 -0
- backend/services/coingecko_client.py +8 -0
- hf_unified_server.py +41 -23
- static/pages/services/index.html +166 -0
- static/pages/services/services.css +528 -0
- static/pages/services/services.js +522 -0
- static/pages/technical-analysis/technical-analysis-professional.js +379 -15
- static/pages/technical-analysis/technical-analysis.js +1 -0
- static/shared/css/sidebar-enhanced.css +283 -0
- static/shared/layouts/sidebar.html +153 -0
|
@@ -0,0 +1,1131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Technical Indicators API Router
|
| 4 |
+
Provides API endpoints for calculating technical indicators on cryptocurrency data.
|
| 5 |
+
Includes: Bollinger Bands, Stochastic RSI, ATR, SMA, EMA, MACD, RSI
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from fastapi import APIRouter, HTTPException, Query
|
| 9 |
+
from pydantic import BaseModel, Field
|
| 10 |
+
from typing import List, Dict, Any, Optional
|
| 11 |
+
from datetime import datetime
|
| 12 |
+
import logging
|
| 13 |
+
import numpy as np
|
| 14 |
+
|
| 15 |
+
logger = logging.getLogger(__name__)
|
| 16 |
+
|
| 17 |
+
router = APIRouter(prefix="/api/indicators", tags=["Technical Indicators"])
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
# ============================================================================
|
| 21 |
+
# Pydantic Models
|
| 22 |
+
# ============================================================================
|
| 23 |
+
|
| 24 |
+
class OHLCVData(BaseModel):
|
| 25 |
+
"""OHLCV data model"""
|
| 26 |
+
timestamp: int
|
| 27 |
+
open: float
|
| 28 |
+
high: float
|
| 29 |
+
low: float
|
| 30 |
+
close: float
|
| 31 |
+
volume: float
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class IndicatorRequest(BaseModel):
|
| 35 |
+
"""Request model for indicator calculation"""
|
| 36 |
+
symbol: str = Field(default="BTC", description="Cryptocurrency symbol")
|
| 37 |
+
timeframe: str = Field(default="1h", description="Timeframe (1m, 5m, 15m, 1h, 4h, 1d)")
|
| 38 |
+
ohlcv: Optional[List[OHLCVData]] = Field(default=None, description="OHLCV data array")
|
| 39 |
+
period: int = Field(default=14, description="Indicator period")
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class BollingerBandsResponse(BaseModel):
|
| 43 |
+
"""Bollinger Bands response model"""
|
| 44 |
+
upper: float
|
| 45 |
+
middle: float
|
| 46 |
+
lower: float
|
| 47 |
+
bandwidth: float
|
| 48 |
+
percent_b: float
|
| 49 |
+
signal: str
|
| 50 |
+
description: str
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
class StochRSIResponse(BaseModel):
|
| 54 |
+
"""Stochastic RSI response model"""
|
| 55 |
+
value: float
|
| 56 |
+
k_line: float
|
| 57 |
+
d_line: float
|
| 58 |
+
signal: str
|
| 59 |
+
description: str
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
class ATRResponse(BaseModel):
|
| 63 |
+
"""Average True Range response model"""
|
| 64 |
+
value: float
|
| 65 |
+
percent: float
|
| 66 |
+
volatility_level: str
|
| 67 |
+
signal: str
|
| 68 |
+
description: str
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
class SMAResponse(BaseModel):
|
| 72 |
+
"""Simple Moving Average response model"""
|
| 73 |
+
sma20: float
|
| 74 |
+
sma50: float
|
| 75 |
+
sma200: Optional[float]
|
| 76 |
+
price_vs_sma20: str
|
| 77 |
+
price_vs_sma50: str
|
| 78 |
+
trend: str
|
| 79 |
+
signal: str
|
| 80 |
+
description: str
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
class EMAResponse(BaseModel):
|
| 84 |
+
"""Exponential Moving Average response model"""
|
| 85 |
+
ema12: float
|
| 86 |
+
ema26: float
|
| 87 |
+
ema50: Optional[float]
|
| 88 |
+
trend: str
|
| 89 |
+
signal: str
|
| 90 |
+
description: str
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
class MACDResponse(BaseModel):
|
| 94 |
+
"""MACD response model"""
|
| 95 |
+
macd_line: float
|
| 96 |
+
signal_line: float
|
| 97 |
+
histogram: float
|
| 98 |
+
trend: str
|
| 99 |
+
signal: str
|
| 100 |
+
description: str
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
class RSIResponse(BaseModel):
|
| 104 |
+
"""RSI response model"""
|
| 105 |
+
value: float
|
| 106 |
+
signal: str
|
| 107 |
+
description: str
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
class ComprehensiveIndicatorsResponse(BaseModel):
|
| 111 |
+
"""All indicators combined response"""
|
| 112 |
+
symbol: str
|
| 113 |
+
timeframe: str
|
| 114 |
+
timestamp: str
|
| 115 |
+
current_price: float
|
| 116 |
+
bollinger_bands: BollingerBandsResponse
|
| 117 |
+
stoch_rsi: StochRSIResponse
|
| 118 |
+
atr: ATRResponse
|
| 119 |
+
sma: SMAResponse
|
| 120 |
+
ema: EMAResponse
|
| 121 |
+
macd: MACDResponse
|
| 122 |
+
rsi: RSIResponse
|
| 123 |
+
overall_signal: str
|
| 124 |
+
recommendation: str
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
# ============================================================================
|
| 128 |
+
# Helper Functions for Calculations
|
| 129 |
+
# ============================================================================
|
| 130 |
+
|
| 131 |
+
def calculate_sma(prices: List[float], period: int) -> float:
|
| 132 |
+
"""Calculate Simple Moving Average"""
|
| 133 |
+
if len(prices) < period:
|
| 134 |
+
return prices[-1] if prices else 0
|
| 135 |
+
return sum(prices[-period:]) / period
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
def calculate_ema(prices: List[float], period: int) -> float:
|
| 139 |
+
"""Calculate Exponential Moving Average"""
|
| 140 |
+
if len(prices) < period:
|
| 141 |
+
return prices[-1] if prices else 0
|
| 142 |
+
|
| 143 |
+
multiplier = 2 / (period + 1)
|
| 144 |
+
ema = sum(prices[:period]) / period # SMA for first period
|
| 145 |
+
|
| 146 |
+
for price in prices[period:]:
|
| 147 |
+
ema = (price * multiplier) + (ema * (1 - multiplier))
|
| 148 |
+
|
| 149 |
+
return ema
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def calculate_rsi(prices: List[float], period: int = 14) -> float:
|
| 153 |
+
"""Calculate Relative Strength Index"""
|
| 154 |
+
if len(prices) < period + 1:
|
| 155 |
+
return 50.0
|
| 156 |
+
|
| 157 |
+
deltas = [prices[i] - prices[i-1] for i in range(1, len(prices))]
|
| 158 |
+
gains = [d if d > 0 else 0 for d in deltas[-period:]]
|
| 159 |
+
losses = [-d if d < 0 else 0 for d in deltas[-period:]]
|
| 160 |
+
|
| 161 |
+
avg_gain = sum(gains) / period
|
| 162 |
+
avg_loss = sum(losses) / period
|
| 163 |
+
|
| 164 |
+
if avg_loss == 0:
|
| 165 |
+
return 100.0 if avg_gain > 0 else 50.0
|
| 166 |
+
|
| 167 |
+
rs = avg_gain / avg_loss
|
| 168 |
+
return 100 - (100 / (1 + rs))
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def calculate_bollinger_bands(prices: List[float], period: int = 20, std_dev: float = 2) -> Dict[str, float]:
|
| 172 |
+
"""Calculate Bollinger Bands"""
|
| 173 |
+
if len(prices) < period:
|
| 174 |
+
current = prices[-1] if prices else 0
|
| 175 |
+
return {
|
| 176 |
+
"upper": current,
|
| 177 |
+
"middle": current,
|
| 178 |
+
"lower": current,
|
| 179 |
+
"bandwidth": 0,
|
| 180 |
+
"percent_b": 50
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
recent_prices = prices[-period:]
|
| 184 |
+
middle = sum(recent_prices) / period
|
| 185 |
+
|
| 186 |
+
# Calculate standard deviation
|
| 187 |
+
variance = sum((p - middle) ** 2 for p in recent_prices) / period
|
| 188 |
+
std = variance ** 0.5
|
| 189 |
+
|
| 190 |
+
upper = middle + (std_dev * std)
|
| 191 |
+
lower = middle - (std_dev * std)
|
| 192 |
+
|
| 193 |
+
# Bandwidth as percentage
|
| 194 |
+
bandwidth = ((upper - lower) / middle) * 100 if middle > 0 else 0
|
| 195 |
+
|
| 196 |
+
# Percent B (position within bands)
|
| 197 |
+
current_price = prices[-1]
|
| 198 |
+
if upper != lower:
|
| 199 |
+
percent_b = ((current_price - lower) / (upper - lower)) * 100
|
| 200 |
+
else:
|
| 201 |
+
percent_b = 50
|
| 202 |
+
|
| 203 |
+
return {
|
| 204 |
+
"upper": round(upper, 8),
|
| 205 |
+
"middle": round(middle, 8),
|
| 206 |
+
"lower": round(lower, 8),
|
| 207 |
+
"bandwidth": round(bandwidth, 2),
|
| 208 |
+
"percent_b": round(percent_b, 2)
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
def calculate_stoch_rsi(prices: List[float], rsi_period: int = 14, stoch_period: int = 14) -> Dict[str, float]:
|
| 213 |
+
"""Calculate Stochastic RSI"""
|
| 214 |
+
if len(prices) < rsi_period + stoch_period:
|
| 215 |
+
return {"value": 50, "k_line": 50, "d_line": 50}
|
| 216 |
+
|
| 217 |
+
# Calculate RSI values for the stoch period
|
| 218 |
+
rsi_values = []
|
| 219 |
+
for i in range(stoch_period + 3): # Extra for smoothing
|
| 220 |
+
end_idx = len(prices) - stoch_period + i + 1
|
| 221 |
+
if end_idx > rsi_period:
|
| 222 |
+
slice_prices = prices[:end_idx]
|
| 223 |
+
rsi_values.append(calculate_rsi(slice_prices, rsi_period))
|
| 224 |
+
|
| 225 |
+
if len(rsi_values) < stoch_period:
|
| 226 |
+
return {"value": 50, "k_line": 50, "d_line": 50}
|
| 227 |
+
|
| 228 |
+
recent_rsi = rsi_values[-stoch_period:]
|
| 229 |
+
rsi_high = max(recent_rsi)
|
| 230 |
+
rsi_low = min(recent_rsi)
|
| 231 |
+
|
| 232 |
+
current_rsi = rsi_values[-1]
|
| 233 |
+
|
| 234 |
+
if rsi_high == rsi_low:
|
| 235 |
+
stoch_rsi = 50
|
| 236 |
+
else:
|
| 237 |
+
stoch_rsi = ((current_rsi - rsi_low) / (rsi_high - rsi_low)) * 100
|
| 238 |
+
|
| 239 |
+
# K line is the raw Stoch RSI
|
| 240 |
+
k_line = stoch_rsi
|
| 241 |
+
|
| 242 |
+
# D line is 3-period SMA of K
|
| 243 |
+
if len(rsi_values) >= 3:
|
| 244 |
+
k_values = []
|
| 245 |
+
for i in range(3):
|
| 246 |
+
idx = -3 + i
|
| 247 |
+
r_high = max(rsi_values[idx-stoch_period+1:idx+1]) if idx+1 <= 0 else rsi_high
|
| 248 |
+
r_low = min(rsi_values[idx-stoch_period+1:idx+1]) if idx+1 <= 0 else rsi_low
|
| 249 |
+
curr = rsi_values[idx]
|
| 250 |
+
if r_high != r_low:
|
| 251 |
+
k_values.append(((curr - r_low) / (r_high - r_low)) * 100)
|
| 252 |
+
else:
|
| 253 |
+
k_values.append(50)
|
| 254 |
+
d_line = sum(k_values) / 3
|
| 255 |
+
else:
|
| 256 |
+
d_line = k_line
|
| 257 |
+
|
| 258 |
+
return {
|
| 259 |
+
"value": round(stoch_rsi, 2),
|
| 260 |
+
"k_line": round(k_line, 2),
|
| 261 |
+
"d_line": round(d_line, 2)
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
def calculate_atr(highs: List[float], lows: List[float], closes: List[float], period: int = 14) -> float:
|
| 266 |
+
"""Calculate Average True Range"""
|
| 267 |
+
if len(closes) < period + 1:
|
| 268 |
+
if len(highs) > 0 and len(lows) > 0:
|
| 269 |
+
return highs[-1] - lows[-1]
|
| 270 |
+
return 0
|
| 271 |
+
|
| 272 |
+
true_ranges = []
|
| 273 |
+
for i in range(1, len(closes)):
|
| 274 |
+
high = highs[i]
|
| 275 |
+
low = lows[i]
|
| 276 |
+
prev_close = closes[i-1]
|
| 277 |
+
|
| 278 |
+
tr = max(
|
| 279 |
+
high - low,
|
| 280 |
+
abs(high - prev_close),
|
| 281 |
+
abs(low - prev_close)
|
| 282 |
+
)
|
| 283 |
+
true_ranges.append(tr)
|
| 284 |
+
|
| 285 |
+
# ATR is the average of the last 'period' true ranges
|
| 286 |
+
if len(true_ranges) < period:
|
| 287 |
+
return sum(true_ranges) / len(true_ranges) if true_ranges else 0
|
| 288 |
+
|
| 289 |
+
return sum(true_ranges[-period:]) / period
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
def calculate_macd(prices: List[float], fast: int = 12, slow: int = 26, signal: int = 9) -> Dict[str, float]:
|
| 293 |
+
"""Calculate MACD"""
|
| 294 |
+
if len(prices) < slow + signal:
|
| 295 |
+
return {"macd_line": 0, "signal_line": 0, "histogram": 0}
|
| 296 |
+
|
| 297 |
+
ema_fast = calculate_ema(prices, fast)
|
| 298 |
+
ema_slow = calculate_ema(prices, slow)
|
| 299 |
+
macd_line = ema_fast - ema_slow
|
| 300 |
+
|
| 301 |
+
# Calculate signal line (EMA of MACD)
|
| 302 |
+
# We need MACD values history for signal line
|
| 303 |
+
macd_values = []
|
| 304 |
+
for i in range(signal + 5):
|
| 305 |
+
idx = len(prices) - signal - 5 + i
|
| 306 |
+
if idx > slow:
|
| 307 |
+
slice_prices = prices[:idx+1]
|
| 308 |
+
ef = calculate_ema(slice_prices, fast)
|
| 309 |
+
es = calculate_ema(slice_prices, slow)
|
| 310 |
+
macd_values.append(ef - es)
|
| 311 |
+
|
| 312 |
+
if len(macd_values) >= signal:
|
| 313 |
+
signal_line = calculate_ema(macd_values, signal)
|
| 314 |
+
else:
|
| 315 |
+
signal_line = macd_line
|
| 316 |
+
|
| 317 |
+
histogram = macd_line - signal_line
|
| 318 |
+
|
| 319 |
+
return {
|
| 320 |
+
"macd_line": round(macd_line, 8),
|
| 321 |
+
"signal_line": round(signal_line, 8),
|
| 322 |
+
"histogram": round(histogram, 8)
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
|
| 326 |
+
# ============================================================================
|
| 327 |
+
# API Endpoints
|
| 328 |
+
# ============================================================================
|
| 329 |
+
|
| 330 |
+
@router.get("/services")
|
| 331 |
+
async def list_indicator_services():
|
| 332 |
+
"""List all available technical indicator services"""
|
| 333 |
+
return {
|
| 334 |
+
"success": True,
|
| 335 |
+
"services": [
|
| 336 |
+
{
|
| 337 |
+
"id": "bollinger_bands",
|
| 338 |
+
"name": "Bollinger Bands",
|
| 339 |
+
"description": "Volatility bands placed above and below a moving average",
|
| 340 |
+
"endpoint": "/api/indicators/bollinger-bands",
|
| 341 |
+
"parameters": ["symbol", "timeframe", "period", "std_dev"],
|
| 342 |
+
"icon": "π",
|
| 343 |
+
"category": "volatility"
|
| 344 |
+
},
|
| 345 |
+
{
|
| 346 |
+
"id": "stoch_rsi",
|
| 347 |
+
"name": "Stochastic RSI",
|
| 348 |
+
"description": "Combines Stochastic oscillator with RSI for momentum",
|
| 349 |
+
"endpoint": "/api/indicators/stoch-rsi",
|
| 350 |
+
"parameters": ["symbol", "timeframe", "rsi_period", "stoch_period"],
|
| 351 |
+
"icon": "π",
|
| 352 |
+
"category": "momentum"
|
| 353 |
+
},
|
| 354 |
+
{
|
| 355 |
+
"id": "atr",
|
| 356 |
+
"name": "Average True Range (ATR)",
|
| 357 |
+
"description": "Measures market volatility and price movement",
|
| 358 |
+
"endpoint": "/api/indicators/atr",
|
| 359 |
+
"parameters": ["symbol", "timeframe", "period"],
|
| 360 |
+
"icon": "π",
|
| 361 |
+
"category": "volatility"
|
| 362 |
+
},
|
| 363 |
+
{
|
| 364 |
+
"id": "sma",
|
| 365 |
+
"name": "Simple Moving Average (SMA)",
|
| 366 |
+
"description": "Average price over specified periods (20, 50, 200)",
|
| 367 |
+
"endpoint": "/api/indicators/sma",
|
| 368 |
+
"parameters": ["symbol", "timeframe"],
|
| 369 |
+
"icon": "γ°οΈ",
|
| 370 |
+
"category": "trend"
|
| 371 |
+
},
|
| 372 |
+
{
|
| 373 |
+
"id": "ema",
|
| 374 |
+
"name": "Exponential Moving Average (EMA)",
|
| 375 |
+
"description": "Weighted moving average giving more weight to recent prices",
|
| 376 |
+
"endpoint": "/api/indicators/ema",
|
| 377 |
+
"parameters": ["symbol", "timeframe"],
|
| 378 |
+
"icon": "π",
|
| 379 |
+
"category": "trend"
|
| 380 |
+
},
|
| 381 |
+
{
|
| 382 |
+
"id": "macd",
|
| 383 |
+
"name": "MACD",
|
| 384 |
+
"description": "Moving Average Convergence Divergence - trend following momentum",
|
| 385 |
+
"endpoint": "/api/indicators/macd",
|
| 386 |
+
"parameters": ["symbol", "timeframe", "fast", "slow", "signal"],
|
| 387 |
+
"icon": "π",
|
| 388 |
+
"category": "momentum"
|
| 389 |
+
},
|
| 390 |
+
{
|
| 391 |
+
"id": "rsi",
|
| 392 |
+
"name": "RSI",
|
| 393 |
+
"description": "Relative Strength Index - momentum oscillator (0-100)",
|
| 394 |
+
"endpoint": "/api/indicators/rsi",
|
| 395 |
+
"parameters": ["symbol", "timeframe", "period"],
|
| 396 |
+
"icon": "πͺ",
|
| 397 |
+
"category": "momentum"
|
| 398 |
+
},
|
| 399 |
+
{
|
| 400 |
+
"id": "comprehensive",
|
| 401 |
+
"name": "Comprehensive Analysis",
|
| 402 |
+
"description": "All indicators combined with trading signals",
|
| 403 |
+
"endpoint": "/api/indicators/comprehensive",
|
| 404 |
+
"parameters": ["symbol", "timeframe"],
|
| 405 |
+
"icon": "π―",
|
| 406 |
+
"category": "analysis"
|
| 407 |
+
}
|
| 408 |
+
],
|
| 409 |
+
"categories": {
|
| 410 |
+
"volatility": "Measure price volatility and potential breakouts",
|
| 411 |
+
"momentum": "Identify overbought/oversold conditions",
|
| 412 |
+
"trend": "Determine market direction and strength",
|
| 413 |
+
"analysis": "Complete multi-indicator analysis"
|
| 414 |
+
},
|
| 415 |
+
"timestamp": datetime.utcnow().isoformat() + "Z"
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
|
| 419 |
+
@router.get("/bollinger-bands")
|
| 420 |
+
async def get_bollinger_bands(
|
| 421 |
+
symbol: str = Query(default="BTC", description="Cryptocurrency symbol"),
|
| 422 |
+
timeframe: str = Query(default="1h", description="Timeframe"),
|
| 423 |
+
period: int = Query(default=20, description="Period for calculation"),
|
| 424 |
+
std_dev: float = Query(default=2.0, description="Standard deviation multiplier")
|
| 425 |
+
):
|
| 426 |
+
"""Calculate Bollinger Bands for a symbol"""
|
| 427 |
+
try:
|
| 428 |
+
# Get OHLCV data from market API
|
| 429 |
+
from backend.services.coingecko_client import coingecko_client
|
| 430 |
+
|
| 431 |
+
# Map timeframe to days
|
| 432 |
+
timeframe_days = {"1m": 1, "5m": 1, "15m": 1, "1h": 7, "4h": 30, "1d": 90}
|
| 433 |
+
days = timeframe_days.get(timeframe, 7)
|
| 434 |
+
|
| 435 |
+
ohlcv = await coingecko_client.get_ohlcv(symbol, days=days)
|
| 436 |
+
|
| 437 |
+
if not ohlcv or "prices" not in ohlcv:
|
| 438 |
+
# Return demo data if API fails
|
| 439 |
+
current_price = 67500 if symbol.upper() == "BTC" else 3400 if symbol.upper() == "ETH" else 100
|
| 440 |
+
return {
|
| 441 |
+
"success": True,
|
| 442 |
+
"symbol": symbol.upper(),
|
| 443 |
+
"timeframe": timeframe,
|
| 444 |
+
"indicator": "bollinger_bands",
|
| 445 |
+
"data": {
|
| 446 |
+
"upper": round(current_price * 1.05, 2),
|
| 447 |
+
"middle": current_price,
|
| 448 |
+
"lower": round(current_price * 0.95, 2),
|
| 449 |
+
"bandwidth": 10.0,
|
| 450 |
+
"percent_b": 50.0
|
| 451 |
+
},
|
| 452 |
+
"signal": "neutral",
|
| 453 |
+
"description": "Price is within the bands - no extreme conditions detected",
|
| 454 |
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
| 455 |
+
"source": "fallback"
|
| 456 |
+
}
|
| 457 |
+
|
| 458 |
+
prices = [p[1] for p in ohlcv["prices"]]
|
| 459 |
+
bb = calculate_bollinger_bands(prices, period, std_dev)
|
| 460 |
+
|
| 461 |
+
current_price = prices[-1] if prices else 0
|
| 462 |
+
|
| 463 |
+
# Determine signal
|
| 464 |
+
if bb["percent_b"] > 95:
|
| 465 |
+
signal = "overbought"
|
| 466 |
+
description = "Price at upper band - potential reversal or breakout"
|
| 467 |
+
elif bb["percent_b"] < 5:
|
| 468 |
+
signal = "oversold"
|
| 469 |
+
description = "Price at lower band - potential bounce or breakdown"
|
| 470 |
+
elif bb["percent_b"] > 70:
|
| 471 |
+
signal = "bullish_caution"
|
| 472 |
+
description = "Price approaching upper band - watch for resistance"
|
| 473 |
+
elif bb["percent_b"] < 30:
|
| 474 |
+
signal = "bearish_caution"
|
| 475 |
+
description = "Price approaching lower band - watch for support"
|
| 476 |
+
else:
|
| 477 |
+
signal = "neutral"
|
| 478 |
+
description = "Price within normal range - no extreme conditions"
|
| 479 |
+
|
| 480 |
+
return {
|
| 481 |
+
"success": True,
|
| 482 |
+
"symbol": symbol.upper(),
|
| 483 |
+
"timeframe": timeframe,
|
| 484 |
+
"indicator": "bollinger_bands",
|
| 485 |
+
"data": bb,
|
| 486 |
+
"current_price": round(current_price, 8),
|
| 487 |
+
"signal": signal,
|
| 488 |
+
"description": description,
|
| 489 |
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
| 490 |
+
"source": "coingecko"
|
| 491 |
+
}
|
| 492 |
+
|
| 493 |
+
except Exception as e:
|
| 494 |
+
logger.error(f"Bollinger Bands calculation error: {e}")
|
| 495 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 496 |
+
|
| 497 |
+
|
| 498 |
+
@router.get("/stoch-rsi")
|
| 499 |
+
async def get_stoch_rsi(
|
| 500 |
+
symbol: str = Query(default="BTC", description="Cryptocurrency symbol"),
|
| 501 |
+
timeframe: str = Query(default="1h", description="Timeframe"),
|
| 502 |
+
rsi_period: int = Query(default=14, description="RSI period"),
|
| 503 |
+
stoch_period: int = Query(default=14, description="Stochastic period")
|
| 504 |
+
):
|
| 505 |
+
"""Calculate Stochastic RSI for a symbol"""
|
| 506 |
+
try:
|
| 507 |
+
from backend.services.coingecko_client import coingecko_client
|
| 508 |
+
|
| 509 |
+
timeframe_days = {"1m": 1, "5m": 1, "15m": 1, "1h": 7, "4h": 30, "1d": 90}
|
| 510 |
+
days = timeframe_days.get(timeframe, 7)
|
| 511 |
+
|
| 512 |
+
ohlcv = await coingecko_client.get_ohlcv(symbol, days=days)
|
| 513 |
+
|
| 514 |
+
if not ohlcv or "prices" not in ohlcv:
|
| 515 |
+
return {
|
| 516 |
+
"success": True,
|
| 517 |
+
"symbol": symbol.upper(),
|
| 518 |
+
"timeframe": timeframe,
|
| 519 |
+
"indicator": "stoch_rsi",
|
| 520 |
+
"data": {"value": 50.0, "k_line": 50.0, "d_line": 50.0},
|
| 521 |
+
"signal": "neutral",
|
| 522 |
+
"description": "Neutral momentum conditions",
|
| 523 |
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
| 524 |
+
"source": "fallback"
|
| 525 |
+
}
|
| 526 |
+
|
| 527 |
+
prices = [p[1] for p in ohlcv["prices"]]
|
| 528 |
+
stoch = calculate_stoch_rsi(prices, rsi_period, stoch_period)
|
| 529 |
+
|
| 530 |
+
# Determine signal
|
| 531 |
+
if stoch["value"] > 80:
|
| 532 |
+
signal = "overbought"
|
| 533 |
+
description = "Extreme overbought - high probability of pullback"
|
| 534 |
+
elif stoch["value"] < 20:
|
| 535 |
+
signal = "oversold"
|
| 536 |
+
description = "Extreme oversold - high probability of bounce"
|
| 537 |
+
elif stoch["k_line"] > stoch["d_line"] and stoch["value"] < 50:
|
| 538 |
+
signal = "bullish_crossover"
|
| 539 |
+
description = "K crossed above D in oversold territory - bullish signal"
|
| 540 |
+
elif stoch["k_line"] < stoch["d_line"] and stoch["value"] > 50:
|
| 541 |
+
signal = "bearish_crossover"
|
| 542 |
+
description = "K crossed below D in overbought territory - bearish signal"
|
| 543 |
+
else:
|
| 544 |
+
signal = "neutral"
|
| 545 |
+
description = "Normal momentum range - no extreme conditions"
|
| 546 |
+
|
| 547 |
+
return {
|
| 548 |
+
"success": True,
|
| 549 |
+
"symbol": symbol.upper(),
|
| 550 |
+
"timeframe": timeframe,
|
| 551 |
+
"indicator": "stoch_rsi",
|
| 552 |
+
"data": stoch,
|
| 553 |
+
"signal": signal,
|
| 554 |
+
"description": description,
|
| 555 |
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
| 556 |
+
"source": "coingecko"
|
| 557 |
+
}
|
| 558 |
+
|
| 559 |
+
except Exception as e:
|
| 560 |
+
logger.error(f"Stochastic RSI calculation error: {e}")
|
| 561 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 562 |
+
|
| 563 |
+
|
| 564 |
+
@router.get("/atr")
|
| 565 |
+
async def get_atr(
|
| 566 |
+
symbol: str = Query(default="BTC", description="Cryptocurrency symbol"),
|
| 567 |
+
timeframe: str = Query(default="1h", description="Timeframe"),
|
| 568 |
+
period: int = Query(default=14, description="ATR period")
|
| 569 |
+
):
|
| 570 |
+
"""Calculate Average True Range for a symbol"""
|
| 571 |
+
try:
|
| 572 |
+
from backend.services.coingecko_client import coingecko_client
|
| 573 |
+
|
| 574 |
+
timeframe_days = {"1m": 1, "5m": 1, "15m": 1, "1h": 7, "4h": 30, "1d": 90}
|
| 575 |
+
days = timeframe_days.get(timeframe, 7)
|
| 576 |
+
|
| 577 |
+
ohlcv = await coingecko_client.get_ohlcv(symbol, days=days)
|
| 578 |
+
|
| 579 |
+
if not ohlcv or "prices" not in ohlcv:
|
| 580 |
+
current_price = 67500 if symbol.upper() == "BTC" else 3400 if symbol.upper() == "ETH" else 100
|
| 581 |
+
atr_value = current_price * 0.02 # 2% default volatility
|
| 582 |
+
return {
|
| 583 |
+
"success": True,
|
| 584 |
+
"symbol": symbol.upper(),
|
| 585 |
+
"timeframe": timeframe,
|
| 586 |
+
"indicator": "atr",
|
| 587 |
+
"data": {
|
| 588 |
+
"value": round(atr_value, 2),
|
| 589 |
+
"percent": 2.0
|
| 590 |
+
},
|
| 591 |
+
"volatility_level": "medium",
|
| 592 |
+
"signal": "neutral",
|
| 593 |
+
"description": "Normal market volatility",
|
| 594 |
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
| 595 |
+
"source": "fallback"
|
| 596 |
+
}
|
| 597 |
+
|
| 598 |
+
prices = [p[1] for p in ohlcv["prices"]]
|
| 599 |
+
# For ATR we need H/L/C - use price approximation
|
| 600 |
+
highs = [p * 1.005 for p in prices] # Approximate
|
| 601 |
+
lows = [p * 0.995 for p in prices]
|
| 602 |
+
|
| 603 |
+
atr_value = calculate_atr(highs, lows, prices, period)
|
| 604 |
+
current_price = prices[-1] if prices else 1
|
| 605 |
+
atr_percent = (atr_value / current_price) * 100 if current_price > 0 else 0
|
| 606 |
+
|
| 607 |
+
# Determine volatility level
|
| 608 |
+
if atr_percent > 5:
|
| 609 |
+
volatility_level = "very_high"
|
| 610 |
+
signal = "high_risk"
|
| 611 |
+
description = "Very high volatility - increase position sizing caution"
|
| 612 |
+
elif atr_percent > 3:
|
| 613 |
+
volatility_level = "high"
|
| 614 |
+
signal = "caution"
|
| 615 |
+
description = "High volatility - wider stop losses recommended"
|
| 616 |
+
elif atr_percent > 1.5:
|
| 617 |
+
volatility_level = "medium"
|
| 618 |
+
signal = "neutral"
|
| 619 |
+
description = "Normal volatility - standard position sizing"
|
| 620 |
+
else:
|
| 621 |
+
volatility_level = "low"
|
| 622 |
+
signal = "breakout_watch"
|
| 623 |
+
description = "Low volatility - potential breakout forming"
|
| 624 |
+
|
| 625 |
+
return {
|
| 626 |
+
"success": True,
|
| 627 |
+
"symbol": symbol.upper(),
|
| 628 |
+
"timeframe": timeframe,
|
| 629 |
+
"indicator": "atr",
|
| 630 |
+
"data": {
|
| 631 |
+
"value": round(atr_value, 8),
|
| 632 |
+
"percent": round(atr_percent, 2)
|
| 633 |
+
},
|
| 634 |
+
"current_price": round(current_price, 8),
|
| 635 |
+
"volatility_level": volatility_level,
|
| 636 |
+
"signal": signal,
|
| 637 |
+
"description": description,
|
| 638 |
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
| 639 |
+
"source": "coingecko"
|
| 640 |
+
}
|
| 641 |
+
|
| 642 |
+
except Exception as e:
|
| 643 |
+
logger.error(f"ATR calculation error: {e}")
|
| 644 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 645 |
+
|
| 646 |
+
|
| 647 |
+
@router.get("/sma")
|
| 648 |
+
async def get_sma(
|
| 649 |
+
symbol: str = Query(default="BTC", description="Cryptocurrency symbol"),
|
| 650 |
+
timeframe: str = Query(default="1h", description="Timeframe")
|
| 651 |
+
):
|
| 652 |
+
"""Calculate Simple Moving Averages (20, 50, 200) for a symbol"""
|
| 653 |
+
try:
|
| 654 |
+
from backend.services.coingecko_client import coingecko_client
|
| 655 |
+
|
| 656 |
+
# Need more data for SMA 200
|
| 657 |
+
ohlcv = await coingecko_client.get_ohlcv(symbol, days=365)
|
| 658 |
+
|
| 659 |
+
if not ohlcv or "prices" not in ohlcv:
|
| 660 |
+
current_price = 67500 if symbol.upper() == "BTC" else 3400 if symbol.upper() == "ETH" else 100
|
| 661 |
+
return {
|
| 662 |
+
"success": True,
|
| 663 |
+
"symbol": symbol.upper(),
|
| 664 |
+
"timeframe": timeframe,
|
| 665 |
+
"indicator": "sma",
|
| 666 |
+
"data": {
|
| 667 |
+
"sma20": current_price,
|
| 668 |
+
"sma50": current_price * 0.98,
|
| 669 |
+
"sma200": current_price * 0.95
|
| 670 |
+
},
|
| 671 |
+
"current_price": current_price,
|
| 672 |
+
"price_vs_sma20": "above",
|
| 673 |
+
"price_vs_sma50": "above",
|
| 674 |
+
"trend": "bullish",
|
| 675 |
+
"signal": "buy",
|
| 676 |
+
"description": "Price above all major SMAs - bullish trend",
|
| 677 |
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
| 678 |
+
"source": "fallback"
|
| 679 |
+
}
|
| 680 |
+
|
| 681 |
+
prices = [p[1] for p in ohlcv["prices"]]
|
| 682 |
+
current_price = prices[-1] if prices else 0
|
| 683 |
+
|
| 684 |
+
sma20 = calculate_sma(prices, 20)
|
| 685 |
+
sma50 = calculate_sma(prices, 50)
|
| 686 |
+
sma200 = calculate_sma(prices, 200) if len(prices) >= 200 else None
|
| 687 |
+
|
| 688 |
+
price_vs_sma20 = "above" if current_price > sma20 else "below"
|
| 689 |
+
price_vs_sma50 = "above" if current_price > sma50 else "below"
|
| 690 |
+
|
| 691 |
+
# Determine trend
|
| 692 |
+
if current_price > sma20 > sma50:
|
| 693 |
+
trend = "strong_bullish"
|
| 694 |
+
signal = "buy"
|
| 695 |
+
description = "Strong uptrend - price above rising SMAs"
|
| 696 |
+
elif current_price > sma20 and current_price > sma50:
|
| 697 |
+
trend = "bullish"
|
| 698 |
+
signal = "buy"
|
| 699 |
+
description = "Bullish trend - price above major SMAs"
|
| 700 |
+
elif current_price < sma20 < sma50:
|
| 701 |
+
trend = "strong_bearish"
|
| 702 |
+
signal = "sell"
|
| 703 |
+
description = "Strong downtrend - price below falling SMAs"
|
| 704 |
+
elif current_price < sma20 and current_price < sma50:
|
| 705 |
+
trend = "bearish"
|
| 706 |
+
signal = "sell"
|
| 707 |
+
description = "Bearish trend - price below major SMAs"
|
| 708 |
+
else:
|
| 709 |
+
trend = "neutral"
|
| 710 |
+
signal = "hold"
|
| 711 |
+
description = "Mixed signals - waiting for clearer direction"
|
| 712 |
+
|
| 713 |
+
return {
|
| 714 |
+
"success": True,
|
| 715 |
+
"symbol": symbol.upper(),
|
| 716 |
+
"timeframe": timeframe,
|
| 717 |
+
"indicator": "sma",
|
| 718 |
+
"data": {
|
| 719 |
+
"sma20": round(sma20, 8),
|
| 720 |
+
"sma50": round(sma50, 8),
|
| 721 |
+
"sma200": round(sma200, 8) if sma200 else None
|
| 722 |
+
},
|
| 723 |
+
"current_price": round(current_price, 8),
|
| 724 |
+
"price_vs_sma20": price_vs_sma20,
|
| 725 |
+
"price_vs_sma50": price_vs_sma50,
|
| 726 |
+
"trend": trend,
|
| 727 |
+
"signal": signal,
|
| 728 |
+
"description": description,
|
| 729 |
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
| 730 |
+
"source": "coingecko"
|
| 731 |
+
}
|
| 732 |
+
|
| 733 |
+
except Exception as e:
|
| 734 |
+
logger.error(f"SMA calculation error: {e}")
|
| 735 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 736 |
+
|
| 737 |
+
|
| 738 |
+
@router.get("/ema")
|
| 739 |
+
async def get_ema(
|
| 740 |
+
symbol: str = Query(default="BTC", description="Cryptocurrency symbol"),
|
| 741 |
+
timeframe: str = Query(default="1h", description="Timeframe")
|
| 742 |
+
):
|
| 743 |
+
"""Calculate Exponential Moving Averages for a symbol"""
|
| 744 |
+
try:
|
| 745 |
+
from backend.services.coingecko_client import coingecko_client
|
| 746 |
+
|
| 747 |
+
ohlcv = await coingecko_client.get_ohlcv(symbol, days=90)
|
| 748 |
+
|
| 749 |
+
if not ohlcv or "prices" not in ohlcv:
|
| 750 |
+
current_price = 67500 if symbol.upper() == "BTC" else 3400 if symbol.upper() == "ETH" else 100
|
| 751 |
+
return {
|
| 752 |
+
"success": True,
|
| 753 |
+
"symbol": symbol.upper(),
|
| 754 |
+
"timeframe": timeframe,
|
| 755 |
+
"indicator": "ema",
|
| 756 |
+
"data": {
|
| 757 |
+
"ema12": current_price,
|
| 758 |
+
"ema26": current_price * 0.99,
|
| 759 |
+
"ema50": current_price * 0.97
|
| 760 |
+
},
|
| 761 |
+
"current_price": current_price,
|
| 762 |
+
"trend": "bullish",
|
| 763 |
+
"signal": "buy",
|
| 764 |
+
"description": "EMAs aligned bullish",
|
| 765 |
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
| 766 |
+
"source": "fallback"
|
| 767 |
+
}
|
| 768 |
+
|
| 769 |
+
prices = [p[1] for p in ohlcv["prices"]]
|
| 770 |
+
current_price = prices[-1] if prices else 0
|
| 771 |
+
|
| 772 |
+
ema12 = calculate_ema(prices, 12)
|
| 773 |
+
ema26 = calculate_ema(prices, 26)
|
| 774 |
+
ema50 = calculate_ema(prices, 50) if len(prices) >= 50 else None
|
| 775 |
+
|
| 776 |
+
# Determine trend
|
| 777 |
+
if ema12 > ema26:
|
| 778 |
+
if current_price > ema12:
|
| 779 |
+
trend = "strong_bullish"
|
| 780 |
+
signal = "buy"
|
| 781 |
+
description = "Strong bullish - price above rising EMAs"
|
| 782 |
+
else:
|
| 783 |
+
trend = "bullish"
|
| 784 |
+
signal = "buy"
|
| 785 |
+
description = "Bullish EMAs - EMA12 above EMA26"
|
| 786 |
+
else:
|
| 787 |
+
if current_price < ema12:
|
| 788 |
+
trend = "strong_bearish"
|
| 789 |
+
signal = "sell"
|
| 790 |
+
description = "Strong bearish - price below falling EMAs"
|
| 791 |
+
else:
|
| 792 |
+
trend = "bearish"
|
| 793 |
+
signal = "sell"
|
| 794 |
+
description = "Bearish EMAs - EMA12 below EMA26"
|
| 795 |
+
|
| 796 |
+
return {
|
| 797 |
+
"success": True,
|
| 798 |
+
"symbol": symbol.upper(),
|
| 799 |
+
"timeframe": timeframe,
|
| 800 |
+
"indicator": "ema",
|
| 801 |
+
"data": {
|
| 802 |
+
"ema12": round(ema12, 8),
|
| 803 |
+
"ema26": round(ema26, 8),
|
| 804 |
+
"ema50": round(ema50, 8) if ema50 else None
|
| 805 |
+
},
|
| 806 |
+
"current_price": round(current_price, 8),
|
| 807 |
+
"trend": trend,
|
| 808 |
+
"signal": signal,
|
| 809 |
+
"description": description,
|
| 810 |
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
| 811 |
+
"source": "coingecko"
|
| 812 |
+
}
|
| 813 |
+
|
| 814 |
+
except Exception as e:
|
| 815 |
+
logger.error(f"EMA calculation error: {e}")
|
| 816 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 817 |
+
|
| 818 |
+
|
| 819 |
+
@router.get("/macd")
|
| 820 |
+
async def get_macd(
|
| 821 |
+
symbol: str = Query(default="BTC", description="Cryptocurrency symbol"),
|
| 822 |
+
timeframe: str = Query(default="1h", description="Timeframe"),
|
| 823 |
+
fast: int = Query(default=12, description="Fast EMA period"),
|
| 824 |
+
slow: int = Query(default=26, description="Slow EMA period"),
|
| 825 |
+
signal_period: int = Query(default=9, description="Signal line period")
|
| 826 |
+
):
|
| 827 |
+
"""Calculate MACD for a symbol"""
|
| 828 |
+
try:
|
| 829 |
+
from backend.services.coingecko_client import coingecko_client
|
| 830 |
+
|
| 831 |
+
ohlcv = await coingecko_client.get_ohlcv(symbol, days=90)
|
| 832 |
+
|
| 833 |
+
if not ohlcv or "prices" not in ohlcv:
|
| 834 |
+
return {
|
| 835 |
+
"success": True,
|
| 836 |
+
"symbol": symbol.upper(),
|
| 837 |
+
"timeframe": timeframe,
|
| 838 |
+
"indicator": "macd",
|
| 839 |
+
"data": {
|
| 840 |
+
"macd_line": 50.0,
|
| 841 |
+
"signal_line": 45.0,
|
| 842 |
+
"histogram": 5.0
|
| 843 |
+
},
|
| 844 |
+
"trend": "bullish",
|
| 845 |
+
"signal": "buy",
|
| 846 |
+
"description": "MACD above signal line - bullish momentum",
|
| 847 |
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
| 848 |
+
"source": "fallback"
|
| 849 |
+
}
|
| 850 |
+
|
| 851 |
+
prices = [p[1] for p in ohlcv["prices"]]
|
| 852 |
+
macd = calculate_macd(prices, fast, slow, signal_period)
|
| 853 |
+
|
| 854 |
+
# Determine signal
|
| 855 |
+
if macd["histogram"] > 0:
|
| 856 |
+
if macd["macd_line"] > 0:
|
| 857 |
+
trend = "strong_bullish"
|
| 858 |
+
signal = "buy"
|
| 859 |
+
description = "Strong bullish - MACD and histogram positive"
|
| 860 |
+
else:
|
| 861 |
+
trend = "bullish"
|
| 862 |
+
signal = "buy"
|
| 863 |
+
description = "Bullish crossover - MACD above signal"
|
| 864 |
+
else:
|
| 865 |
+
if macd["macd_line"] < 0:
|
| 866 |
+
trend = "strong_bearish"
|
| 867 |
+
signal = "sell"
|
| 868 |
+
description = "Strong bearish - MACD and histogram negative"
|
| 869 |
+
else:
|
| 870 |
+
trend = "bearish"
|
| 871 |
+
signal = "sell"
|
| 872 |
+
description = "Bearish crossover - MACD below signal"
|
| 873 |
+
|
| 874 |
+
return {
|
| 875 |
+
"success": True,
|
| 876 |
+
"symbol": symbol.upper(),
|
| 877 |
+
"timeframe": timeframe,
|
| 878 |
+
"indicator": "macd",
|
| 879 |
+
"data": macd,
|
| 880 |
+
"trend": trend,
|
| 881 |
+
"signal": signal,
|
| 882 |
+
"description": description,
|
| 883 |
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
| 884 |
+
"source": "coingecko"
|
| 885 |
+
}
|
| 886 |
+
|
| 887 |
+
except Exception as e:
|
| 888 |
+
logger.error(f"MACD calculation error: {e}")
|
| 889 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 890 |
+
|
| 891 |
+
|
| 892 |
+
@router.get("/rsi")
|
| 893 |
+
async def get_rsi(
|
| 894 |
+
symbol: str = Query(default="BTC", description="Cryptocurrency symbol"),
|
| 895 |
+
timeframe: str = Query(default="1h", description="Timeframe"),
|
| 896 |
+
period: int = Query(default=14, description="RSI period")
|
| 897 |
+
):
|
| 898 |
+
"""Calculate RSI for a symbol"""
|
| 899 |
+
try:
|
| 900 |
+
from backend.services.coingecko_client import coingecko_client
|
| 901 |
+
|
| 902 |
+
timeframe_days = {"1m": 1, "5m": 1, "15m": 1, "1h": 7, "4h": 30, "1d": 90}
|
| 903 |
+
days = timeframe_days.get(timeframe, 7)
|
| 904 |
+
|
| 905 |
+
ohlcv = await coingecko_client.get_ohlcv(symbol, days=days)
|
| 906 |
+
|
| 907 |
+
if not ohlcv or "prices" not in ohlcv:
|
| 908 |
+
return {
|
| 909 |
+
"success": True,
|
| 910 |
+
"symbol": symbol.upper(),
|
| 911 |
+
"timeframe": timeframe,
|
| 912 |
+
"indicator": "rsi",
|
| 913 |
+
"data": {"value": 55.0},
|
| 914 |
+
"signal": "neutral",
|
| 915 |
+
"description": "RSI in neutral zone - no extreme conditions",
|
| 916 |
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
| 917 |
+
"source": "fallback"
|
| 918 |
+
}
|
| 919 |
+
|
| 920 |
+
prices = [p[1] for p in ohlcv["prices"]]
|
| 921 |
+
rsi = calculate_rsi(prices, period)
|
| 922 |
+
|
| 923 |
+
# Determine signal
|
| 924 |
+
if rsi > 70:
|
| 925 |
+
signal = "overbought"
|
| 926 |
+
description = f"RSI at {rsi:.1f} - overbought conditions, potential pullback"
|
| 927 |
+
elif rsi < 30:
|
| 928 |
+
signal = "oversold"
|
| 929 |
+
description = f"RSI at {rsi:.1f} - oversold conditions, potential bounce"
|
| 930 |
+
elif rsi > 60:
|
| 931 |
+
signal = "bullish"
|
| 932 |
+
description = f"RSI at {rsi:.1f} - bullish momentum"
|
| 933 |
+
elif rsi < 40:
|
| 934 |
+
signal = "bearish"
|
| 935 |
+
description = f"RSI at {rsi:.1f} - bearish momentum"
|
| 936 |
+
else:
|
| 937 |
+
signal = "neutral"
|
| 938 |
+
description = f"RSI at {rsi:.1f} - neutral zone"
|
| 939 |
+
|
| 940 |
+
return {
|
| 941 |
+
"success": True,
|
| 942 |
+
"symbol": symbol.upper(),
|
| 943 |
+
"timeframe": timeframe,
|
| 944 |
+
"indicator": "rsi",
|
| 945 |
+
"data": {"value": round(rsi, 2)},
|
| 946 |
+
"signal": signal,
|
| 947 |
+
"description": description,
|
| 948 |
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
| 949 |
+
"source": "coingecko"
|
| 950 |
+
}
|
| 951 |
+
|
| 952 |
+
except Exception as e:
|
| 953 |
+
logger.error(f"RSI calculation error: {e}")
|
| 954 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 955 |
+
|
| 956 |
+
|
| 957 |
+
@router.get("/comprehensive")
|
| 958 |
+
async def get_comprehensive_analysis(
|
| 959 |
+
symbol: str = Query(default="BTC", description="Cryptocurrency symbol"),
|
| 960 |
+
timeframe: str = Query(default="1h", description="Timeframe")
|
| 961 |
+
):
|
| 962 |
+
"""Get comprehensive analysis with all indicators"""
|
| 963 |
+
try:
|
| 964 |
+
from backend.services.coingecko_client import coingecko_client
|
| 965 |
+
|
| 966 |
+
# Get historical data
|
| 967 |
+
ohlcv = await coingecko_client.get_ohlcv(symbol, days=365)
|
| 968 |
+
|
| 969 |
+
if not ohlcv or "prices" not in ohlcv:
|
| 970 |
+
# Return comprehensive fallback
|
| 971 |
+
current_price = 67500 if symbol.upper() == "BTC" else 3400 if symbol.upper() == "ETH" else 100
|
| 972 |
+
return {
|
| 973 |
+
"success": True,
|
| 974 |
+
"symbol": symbol.upper(),
|
| 975 |
+
"timeframe": timeframe,
|
| 976 |
+
"current_price": current_price,
|
| 977 |
+
"indicators": {
|
| 978 |
+
"bollinger_bands": {"upper": current_price * 1.05, "middle": current_price, "lower": current_price * 0.95, "bandwidth": 10, "percent_b": 50},
|
| 979 |
+
"stoch_rsi": {"value": 50, "k_line": 50, "d_line": 50},
|
| 980 |
+
"atr": {"value": current_price * 0.02, "percent": 2.0},
|
| 981 |
+
"sma": {"sma20": current_price, "sma50": current_price * 0.98, "sma200": current_price * 0.95},
|
| 982 |
+
"ema": {"ema12": current_price, "ema26": current_price * 0.99},
|
| 983 |
+
"macd": {"macd_line": 50, "signal_line": 45, "histogram": 5},
|
| 984 |
+
"rsi": {"value": 55}
|
| 985 |
+
},
|
| 986 |
+
"signals": {
|
| 987 |
+
"bollinger_bands": "neutral",
|
| 988 |
+
"stoch_rsi": "neutral",
|
| 989 |
+
"atr": "medium_volatility",
|
| 990 |
+
"sma": "bullish",
|
| 991 |
+
"ema": "bullish",
|
| 992 |
+
"macd": "bullish",
|
| 993 |
+
"rsi": "neutral"
|
| 994 |
+
},
|
| 995 |
+
"overall_signal": "HOLD",
|
| 996 |
+
"confidence": 60,
|
| 997 |
+
"recommendation": "Mixed signals - wait for clearer direction",
|
| 998 |
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
| 999 |
+
"source": "fallback"
|
| 1000 |
+
}
|
| 1001 |
+
|
| 1002 |
+
prices = [p[1] for p in ohlcv["prices"]]
|
| 1003 |
+
current_price = prices[-1] if prices else 0
|
| 1004 |
+
|
| 1005 |
+
# Calculate all indicators
|
| 1006 |
+
bb = calculate_bollinger_bands(prices, 20, 2)
|
| 1007 |
+
stoch = calculate_stoch_rsi(prices, 14, 14)
|
| 1008 |
+
|
| 1009 |
+
# Approximate H/L for ATR
|
| 1010 |
+
highs = [p * 1.005 for p in prices]
|
| 1011 |
+
lows = [p * 0.995 for p in prices]
|
| 1012 |
+
atr_value = calculate_atr(highs, lows, prices, 14)
|
| 1013 |
+
atr_percent = (atr_value / current_price) * 100 if current_price > 0 else 0
|
| 1014 |
+
|
| 1015 |
+
sma20 = calculate_sma(prices, 20)
|
| 1016 |
+
sma50 = calculate_sma(prices, 50)
|
| 1017 |
+
sma200 = calculate_sma(prices, 200) if len(prices) >= 200 else None
|
| 1018 |
+
|
| 1019 |
+
ema12 = calculate_ema(prices, 12)
|
| 1020 |
+
ema26 = calculate_ema(prices, 26)
|
| 1021 |
+
|
| 1022 |
+
macd = calculate_macd(prices, 12, 26, 9)
|
| 1023 |
+
rsi = calculate_rsi(prices, 14)
|
| 1024 |
+
|
| 1025 |
+
# Determine individual signals
|
| 1026 |
+
signals = {}
|
| 1027 |
+
|
| 1028 |
+
# BB signal
|
| 1029 |
+
if bb["percent_b"] > 80:
|
| 1030 |
+
signals["bollinger_bands"] = "overbought"
|
| 1031 |
+
elif bb["percent_b"] < 20:
|
| 1032 |
+
signals["bollinger_bands"] = "oversold"
|
| 1033 |
+
else:
|
| 1034 |
+
signals["bollinger_bands"] = "neutral"
|
| 1035 |
+
|
| 1036 |
+
# Stoch RSI signal
|
| 1037 |
+
if stoch["value"] > 80:
|
| 1038 |
+
signals["stoch_rsi"] = "overbought"
|
| 1039 |
+
elif stoch["value"] < 20:
|
| 1040 |
+
signals["stoch_rsi"] = "oversold"
|
| 1041 |
+
else:
|
| 1042 |
+
signals["stoch_rsi"] = "neutral"
|
| 1043 |
+
|
| 1044 |
+
# ATR signal
|
| 1045 |
+
if atr_percent > 5:
|
| 1046 |
+
signals["atr"] = "high_volatility"
|
| 1047 |
+
elif atr_percent < 1:
|
| 1048 |
+
signals["atr"] = "low_volatility"
|
| 1049 |
+
else:
|
| 1050 |
+
signals["atr"] = "medium_volatility"
|
| 1051 |
+
|
| 1052 |
+
# SMA signal
|
| 1053 |
+
if current_price > sma20 and current_price > sma50:
|
| 1054 |
+
signals["sma"] = "bullish"
|
| 1055 |
+
elif current_price < sma20 and current_price < sma50:
|
| 1056 |
+
signals["sma"] = "bearish"
|
| 1057 |
+
else:
|
| 1058 |
+
signals["sma"] = "neutral"
|
| 1059 |
+
|
| 1060 |
+
# EMA signal
|
| 1061 |
+
if ema12 > ema26:
|
| 1062 |
+
signals["ema"] = "bullish"
|
| 1063 |
+
else:
|
| 1064 |
+
signals["ema"] = "bearish"
|
| 1065 |
+
|
| 1066 |
+
# MACD signal
|
| 1067 |
+
if macd["histogram"] > 0:
|
| 1068 |
+
signals["macd"] = "bullish"
|
| 1069 |
+
else:
|
| 1070 |
+
signals["macd"] = "bearish"
|
| 1071 |
+
|
| 1072 |
+
# RSI signal
|
| 1073 |
+
if rsi > 70:
|
| 1074 |
+
signals["rsi"] = "overbought"
|
| 1075 |
+
elif rsi < 30:
|
| 1076 |
+
signals["rsi"] = "oversold"
|
| 1077 |
+
elif rsi > 50:
|
| 1078 |
+
signals["rsi"] = "bullish"
|
| 1079 |
+
else:
|
| 1080 |
+
signals["rsi"] = "bearish"
|
| 1081 |
+
|
| 1082 |
+
# Calculate overall signal
|
| 1083 |
+
bullish_count = sum(1 for s in signals.values() if s in ["bullish", "oversold"])
|
| 1084 |
+
bearish_count = sum(1 for s in signals.values() if s in ["bearish", "overbought"])
|
| 1085 |
+
|
| 1086 |
+
if bullish_count >= 5:
|
| 1087 |
+
overall_signal = "STRONG_BUY"
|
| 1088 |
+
confidence = 85
|
| 1089 |
+
recommendation = "Strong bullish signals across multiple indicators - consider buying"
|
| 1090 |
+
elif bullish_count >= 4:
|
| 1091 |
+
overall_signal = "BUY"
|
| 1092 |
+
confidence = 70
|
| 1093 |
+
recommendation = "Majority bullish signals - favorable conditions for entry"
|
| 1094 |
+
elif bearish_count >= 5:
|
| 1095 |
+
overall_signal = "STRONG_SELL"
|
| 1096 |
+
confidence = 85
|
| 1097 |
+
recommendation = "Strong bearish signals across multiple indicators - consider selling"
|
| 1098 |
+
elif bearish_count >= 4:
|
| 1099 |
+
overall_signal = "SELL"
|
| 1100 |
+
confidence = 70
|
| 1101 |
+
recommendation = "Majority bearish signals - unfavorable conditions"
|
| 1102 |
+
else:
|
| 1103 |
+
overall_signal = "HOLD"
|
| 1104 |
+
confidence = 50
|
| 1105 |
+
recommendation = "Mixed signals - wait for clearer direction before taking action"
|
| 1106 |
+
|
| 1107 |
+
return {
|
| 1108 |
+
"success": True,
|
| 1109 |
+
"symbol": symbol.upper(),
|
| 1110 |
+
"timeframe": timeframe,
|
| 1111 |
+
"current_price": round(current_price, 8),
|
| 1112 |
+
"indicators": {
|
| 1113 |
+
"bollinger_bands": bb,
|
| 1114 |
+
"stoch_rsi": stoch,
|
| 1115 |
+
"atr": {"value": round(atr_value, 8), "percent": round(atr_percent, 2)},
|
| 1116 |
+
"sma": {"sma20": round(sma20, 8), "sma50": round(sma50, 8), "sma200": round(sma200, 8) if sma200 else None},
|
| 1117 |
+
"ema": {"ema12": round(ema12, 8), "ema26": round(ema26, 8)},
|
| 1118 |
+
"macd": macd,
|
| 1119 |
+
"rsi": {"value": round(rsi, 2)}
|
| 1120 |
+
},
|
| 1121 |
+
"signals": signals,
|
| 1122 |
+
"overall_signal": overall_signal,
|
| 1123 |
+
"confidence": confidence,
|
| 1124 |
+
"recommendation": recommendation,
|
| 1125 |
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
| 1126 |
+
"source": "coingecko"
|
| 1127 |
+
}
|
| 1128 |
+
|
| 1129 |
+
except Exception as e:
|
| 1130 |
+
logger.error(f"Comprehensive analysis error: {e}")
|
| 1131 |
+
raise HTTPException(status_code=500, detail=str(e))
|
|
@@ -133,13 +133,21 @@ class CoinGeckoClient:
|
|
| 133 |
prices = []
|
| 134 |
for coin in data:
|
| 135 |
prices.append({
|
|
|
|
| 136 |
"symbol": coin.get("symbol", "").upper(),
|
| 137 |
"name": coin.get("name", ""),
|
|
|
|
| 138 |
"price": coin.get("current_price", 0),
|
| 139 |
"change24h": coin.get("price_change_24h", 0),
|
| 140 |
"changePercent24h": coin.get("price_change_percentage_24h", 0),
|
| 141 |
"volume24h": coin.get("total_volume", 0),
|
| 142 |
"marketCap": coin.get("market_cap", 0),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
"source": "coingecko",
|
| 144 |
"timestamp": int(datetime.utcnow().timestamp() * 1000)
|
| 145 |
})
|
|
|
|
| 133 |
prices = []
|
| 134 |
for coin in data:
|
| 135 |
prices.append({
|
| 136 |
+
"id": coin.get("id", ""),
|
| 137 |
"symbol": coin.get("symbol", "").upper(),
|
| 138 |
"name": coin.get("name", ""),
|
| 139 |
+
"image": coin.get("image", ""), # CoinGecko provides real image URLs
|
| 140 |
"price": coin.get("current_price", 0),
|
| 141 |
"change24h": coin.get("price_change_24h", 0),
|
| 142 |
"changePercent24h": coin.get("price_change_percentage_24h", 0),
|
| 143 |
"volume24h": coin.get("total_volume", 0),
|
| 144 |
"marketCap": coin.get("market_cap", 0),
|
| 145 |
+
"market_cap_rank": coin.get("market_cap_rank", 0),
|
| 146 |
+
"circulating_supply": coin.get("circulating_supply", 0),
|
| 147 |
+
"total_supply": coin.get("total_supply", 0),
|
| 148 |
+
"max_supply": coin.get("max_supply", 0),
|
| 149 |
+
"ath": coin.get("ath", 0),
|
| 150 |
+
"atl": coin.get("atl", 0),
|
| 151 |
"source": "coingecko",
|
| 152 |
"timestamp": int(datetime.utcnow().timestamp() * 1000)
|
| 153 |
})
|
|
@@ -366,6 +366,14 @@ try:
|
|
| 366 |
except Exception as e:
|
| 367 |
logger.error(f"Failed to include realtime_monitoring_router: {e}")
|
| 368 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 369 |
# Add routers status endpoint
|
| 370 |
@app.get("/api/routers")
|
| 371 |
async def get_routers_status():
|
|
@@ -383,6 +391,7 @@ async def get_routers_status():
|
|
| 383 |
"trading_backtesting": "loaded" if trading_router else "not_available",
|
| 384 |
"market_api": "loaded",
|
| 385 |
"technical_analysis": "loaded",
|
|
|
|
| 386 |
"dynamic_model_loader": "loaded" if dynamic_model_router else "not_available"
|
| 387 |
}
|
| 388 |
return {
|
|
@@ -1316,13 +1325,19 @@ async def api_coins_top(limit: int = 50):
|
|
| 1316 |
# Transform to expected format with all required fields
|
| 1317 |
coins = []
|
| 1318 |
for idx, coin in enumerate(market_data):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1319 |
coins.append({
|
| 1320 |
-
"id": coin.get("symbol", "").lower(),
|
| 1321 |
-
"rank": idx + 1,
|
| 1322 |
-
"market_cap_rank": idx + 1,
|
| 1323 |
"symbol": coin.get("symbol", ""),
|
| 1324 |
"name": coin.get("name", coin.get("symbol", "")),
|
| 1325 |
-
"image":
|
| 1326 |
"price": coin.get("price", 0),
|
| 1327 |
"current_price": coin.get("price", 0),
|
| 1328 |
"market_cap": coin.get("marketCap", 0),
|
|
@@ -1333,12 +1348,13 @@ async def api_coins_top(limit: int = 50):
|
|
| 1333 |
"price_change_percentage_24h": coin.get("changePercent24h", 0),
|
| 1334 |
"change_7d": 0, # Will be populated if available
|
| 1335 |
"price_change_percentage_7d": 0,
|
|
|
|
| 1336 |
"sparkline": [], # Can be populated from separate API call if needed
|
| 1337 |
-
"circulating_supply": 0,
|
| 1338 |
-
"total_supply": 0,
|
| 1339 |
-
"max_supply": 0,
|
| 1340 |
-
"ath": 0,
|
| 1341 |
-
"atl": 0,
|
| 1342 |
"last_updated": coin.get("timestamp", int(datetime.utcnow().timestamp() * 1000))
|
| 1343 |
})
|
| 1344 |
|
|
@@ -1352,31 +1368,32 @@ async def api_coins_top(limit: int = 50):
|
|
| 1352 |
}
|
| 1353 |
except Exception as e:
|
| 1354 |
logger.error(f"Failed to fetch top coins: {e}")
|
| 1355 |
-
# Return minimal fallback data
|
| 1356 |
import random
|
| 1357 |
fallback_coins = []
|
|
|
|
| 1358 |
coin_data = [
|
| 1359 |
-
("BTC", "Bitcoin", 67850, 1_280_000_000_000),
|
| 1360 |
-
("ETH", "Ethereum", 3420, 410_000_000_000),
|
| 1361 |
-
("BNB", "
|
| 1362 |
-
("SOL", "Solana", 145, 65_000_000_000),
|
| 1363 |
-
("XRP", "
|
| 1364 |
-
("ADA", "Cardano", 0.58, 21_000_000_000),
|
| 1365 |
-
("AVAX", "Avalanche", 38, 14_500_000_000),
|
| 1366 |
-
("DOT", "Polkadot", 7.2, 9_800_000_000),
|
| 1367 |
-
("MATIC", "Polygon", 0.88, 8_200_000_000),
|
| 1368 |
-
("LINK", "Chainlink", 15.4, 8_900_000_000)
|
| 1369 |
]
|
| 1370 |
|
| 1371 |
for i in range(min(limit, len(coin_data) * 5)):
|
| 1372 |
-
symbol, name, price, mcap = coin_data[i % len(coin_data)]
|
| 1373 |
fallback_coins.append({
|
| 1374 |
-
"id":
|
| 1375 |
"rank": i + 1,
|
| 1376 |
"market_cap_rank": i + 1,
|
| 1377 |
"symbol": symbol,
|
| 1378 |
"name": name,
|
| 1379 |
-
"image":
|
| 1380 |
"price": price,
|
| 1381 |
"current_price": price,
|
| 1382 |
"market_cap": mcap,
|
|
@@ -1387,6 +1404,7 @@ async def api_coins_top(limit: int = 50):
|
|
| 1387 |
"price_change_percentage_24h": round(random.uniform(-8, 15), 2),
|
| 1388 |
"change_7d": round(random.uniform(-20, 30), 2),
|
| 1389 |
"price_change_percentage_7d": round(random.uniform(-20, 30), 2),
|
|
|
|
| 1390 |
"sparkline": []
|
| 1391 |
})
|
| 1392 |
|
|
|
|
| 366 |
except Exception as e:
|
| 367 |
logger.error(f"Failed to include realtime_monitoring_router: {e}")
|
| 368 |
|
| 369 |
+
# Technical Indicators Services API
|
| 370 |
+
try:
|
| 371 |
+
from backend.routers.indicators_api import router as indicators_router
|
| 372 |
+
app.include_router(indicators_router) # Technical Indicators API (BB, StochRSI, ATR, SMA, EMA, MACD, RSI)
|
| 373 |
+
logger.info("β β
Technical Indicators Router loaded (Bollinger Bands, StochRSI, ATR, SMA, EMA, MACD, RSI)")
|
| 374 |
+
except Exception as e:
|
| 375 |
+
logger.error(f"Failed to include indicators_router: {e}")
|
| 376 |
+
|
| 377 |
# Add routers status endpoint
|
| 378 |
@app.get("/api/routers")
|
| 379 |
async def get_routers_status():
|
|
|
|
| 391 |
"trading_backtesting": "loaded" if trading_router else "not_available",
|
| 392 |
"market_api": "loaded",
|
| 393 |
"technical_analysis": "loaded",
|
| 394 |
+
"technical_indicators": "loaded", # NEW: BB, StochRSI, ATR, SMA, EMA, MACD, RSI
|
| 395 |
"dynamic_model_loader": "loaded" if dynamic_model_router else "not_available"
|
| 396 |
}
|
| 397 |
return {
|
|
|
|
| 1325 |
# Transform to expected format with all required fields
|
| 1326 |
coins = []
|
| 1327 |
for idx, coin in enumerate(market_data):
|
| 1328 |
+
# Use the real CoinGecko image URL if available
|
| 1329 |
+
image_url = coin.get("image", "")
|
| 1330 |
+
if not image_url:
|
| 1331 |
+
# Fallback to a generated URL
|
| 1332 |
+
image_url = f"https://assets.coingecko.com/coins/images/1/small/{coin.get('id', coin.get('symbol', '').lower())}.png"
|
| 1333 |
+
|
| 1334 |
coins.append({
|
| 1335 |
+
"id": coin.get("id", coin.get("symbol", "").lower()),
|
| 1336 |
+
"rank": coin.get("market_cap_rank", idx + 1),
|
| 1337 |
+
"market_cap_rank": coin.get("market_cap_rank", idx + 1),
|
| 1338 |
"symbol": coin.get("symbol", ""),
|
| 1339 |
"name": coin.get("name", coin.get("symbol", "")),
|
| 1340 |
+
"image": image_url, # Real image URL from CoinGecko
|
| 1341 |
"price": coin.get("price", 0),
|
| 1342 |
"current_price": coin.get("price", 0),
|
| 1343 |
"market_cap": coin.get("marketCap", 0),
|
|
|
|
| 1348 |
"price_change_percentage_24h": coin.get("changePercent24h", 0),
|
| 1349 |
"change_7d": 0, # Will be populated if available
|
| 1350 |
"price_change_percentage_7d": 0,
|
| 1351 |
+
"price_change_percentage_7d_in_currency": 0,
|
| 1352 |
"sparkline": [], # Can be populated from separate API call if needed
|
| 1353 |
+
"circulating_supply": coin.get("circulating_supply", 0),
|
| 1354 |
+
"total_supply": coin.get("total_supply", 0),
|
| 1355 |
+
"max_supply": coin.get("max_supply", 0),
|
| 1356 |
+
"ath": coin.get("ath", 0),
|
| 1357 |
+
"atl": coin.get("atl", 0),
|
| 1358 |
"last_updated": coin.get("timestamp", int(datetime.utcnow().timestamp() * 1000))
|
| 1359 |
})
|
| 1360 |
|
|
|
|
| 1368 |
}
|
| 1369 |
except Exception as e:
|
| 1370 |
logger.error(f"Failed to fetch top coins: {e}")
|
| 1371 |
+
# Return minimal fallback data with proper CoinGecko image URLs
|
| 1372 |
import random
|
| 1373 |
fallback_coins = []
|
| 1374 |
+
# (symbol, name, price, mcap, coingecko_id, image_url)
|
| 1375 |
coin_data = [
|
| 1376 |
+
("BTC", "Bitcoin", 67850, 1_280_000_000_000, "bitcoin", "https://assets.coingecko.com/coins/images/1/small/bitcoin.png"),
|
| 1377 |
+
("ETH", "Ethereum", 3420, 410_000_000_000, "ethereum", "https://assets.coingecko.com/coins/images/279/small/ethereum.png"),
|
| 1378 |
+
("BNB", "BNB", 585, 88_000_000_000, "binancecoin", "https://assets.coingecko.com/coins/images/825/small/bnb-icon2_2x.png"),
|
| 1379 |
+
("SOL", "Solana", 145, 65_000_000_000, "solana", "https://assets.coingecko.com/coins/images/4128/small/solana.png"),
|
| 1380 |
+
("XRP", "XRP", 0.62, 34_000_000_000, "ripple", "https://assets.coingecko.com/coins/images/44/small/xrp-symbol-white-128.png"),
|
| 1381 |
+
("ADA", "Cardano", 0.58, 21_000_000_000, "cardano", "https://assets.coingecko.com/coins/images/975/small/cardano.png"),
|
| 1382 |
+
("AVAX", "Avalanche", 38, 14_500_000_000, "avalanche-2", "https://assets.coingecko.com/coins/images/12559/small/Avalanche_Circle_RedWhite_Trans.png"),
|
| 1383 |
+
("DOT", "Polkadot", 7.2, 9_800_000_000, "polkadot", "https://assets.coingecko.com/coins/images/12171/small/polkadot.png"),
|
| 1384 |
+
("MATIC", "Polygon", 0.88, 8_200_000_000, "matic-network", "https://assets.coingecko.com/coins/images/4713/small/matic-token-icon.png"),
|
| 1385 |
+
("LINK", "Chainlink", 15.4, 8_900_000_000, "chainlink", "https://assets.coingecko.com/coins/images/877/small/chainlink-new-logo.png")
|
| 1386 |
]
|
| 1387 |
|
| 1388 |
for i in range(min(limit, len(coin_data) * 5)):
|
| 1389 |
+
symbol, name, price, mcap, coingecko_id, image = coin_data[i % len(coin_data)]
|
| 1390 |
fallback_coins.append({
|
| 1391 |
+
"id": coingecko_id,
|
| 1392 |
"rank": i + 1,
|
| 1393 |
"market_cap_rank": i + 1,
|
| 1394 |
"symbol": symbol,
|
| 1395 |
"name": name,
|
| 1396 |
+
"image": image, # Correct CoinGecko image URL
|
| 1397 |
"price": price,
|
| 1398 |
"current_price": price,
|
| 1399 |
"market_cap": mcap,
|
|
|
|
| 1404 |
"price_change_percentage_24h": round(random.uniform(-8, 15), 2),
|
| 1405 |
"change_7d": round(random.uniform(-20, 30), 2),
|
| 1406 |
"price_change_percentage_7d": round(random.uniform(-20, 30), 2),
|
| 1407 |
+
"price_change_percentage_7d_in_currency": round(random.uniform(-20, 30), 2),
|
| 1408 |
"sparkline": []
|
| 1409 |
})
|
| 1410 |
|
|
@@ -0,0 +1,166 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en" dir="ltr" data-theme="light">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<meta name="description" content="Technical Indicator Services - Bollinger Bands, Stochastic RSI, ATR, SMA, EMA, MACD, RSI">
|
| 7 |
+
<meta name="theme-color" content="#14b8a6">
|
| 8 |
+
<title>Indicator Services | Crypto Monitor</title>
|
| 9 |
+
|
| 10 |
+
<!-- Favicon -->
|
| 11 |
+
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' stop-color='%232dd4bf'/%3E%3Cstop offset='50%25' stop-color='%2322d3ee'/%3E%3Cstop offset='100%25' stop-color='%233b82f6'/%3E%3C/linearGradient%3E%3C/defs%3E%3Ccircle cx='50' cy='50' r='45' fill='url(%23g)'/%3E%3Cpath d='M50 25 L65 45 L50 40 L35 45 Z M50 75 L35 55 L50 60 L65 55 Z' fill='white'/%3E%3C/svg%3E">
|
| 12 |
+
|
| 13 |
+
<!-- Preconnect -->
|
| 14 |
+
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>
|
| 15 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 16 |
+
|
| 17 |
+
<!-- Critical CSS -->
|
| 18 |
+
<style>
|
| 19 |
+
:root{--teal-dark:#0d7377;--teal:#14b8a6;--teal-light:#2dd4bf;--cyan:#22d3ee;--text-primary:#0f2926;--text-secondary:#2a5f5a;--text-muted:#64748b;--bg-main:#ffffff;--bg-secondary:#f8fdfc;--sidebar-width:260px}
|
| 20 |
+
*,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
|
| 21 |
+
html{font-size:14px;-webkit-font-smoothing:antialiased}
|
| 22 |
+
body{font-family:system-ui,-apple-system,sans-serif;font-size:14px;line-height:1.5;color:var(--text-secondary);background:var(--bg-main);min-height:100vh}
|
| 23 |
+
.app-container{display:flex;min-height:100vh}
|
| 24 |
+
.sidebar{position:fixed;left:0;top:0;bottom:0;width:var(--sidebar-width);background:linear-gradient(180deg,#fff 0%,#f8fdfc 100%);border-right:1px solid rgba(20,184,166,0.12);z-index:100}
|
| 25 |
+
.main-content{flex:1;margin-left:var(--sidebar-width);min-height:100vh}
|
| 26 |
+
.page-content{padding:1.5rem;max-width:1400px;margin:0 auto}
|
| 27 |
+
@media(max-width:768px){.sidebar{transform:translateX(-100%)}.main-content{margin-left:0}}
|
| 28 |
+
</style>
|
| 29 |
+
|
| 30 |
+
<!-- CSS -->
|
| 31 |
+
<link rel="stylesheet" href="/static/shared/css/design-system.css?v=3.0" media="print" onload="this.media='all'">
|
| 32 |
+
<noscript><link rel="stylesheet" href="/static/shared/css/design-system.css?v=3.0"></noscript>
|
| 33 |
+
<link rel="stylesheet" href="/static/shared/css/global.css?v=3.0" media="print" onload="this.media='all'">
|
| 34 |
+
<noscript><link rel="stylesheet" href="/static/shared/css/global.css?v=3.0"></noscript>
|
| 35 |
+
<link rel="stylesheet" href="/static/shared/css/components.css" media="print" onload="this.media='all'">
|
| 36 |
+
<noscript><link rel="stylesheet" href="/static/shared/css/components.css"></noscript>
|
| 37 |
+
<link rel="stylesheet" href="/static/shared/css/layout.css" media="print" onload="this.media='all'">
|
| 38 |
+
<noscript><link rel="stylesheet" href="/static/shared/css/layout.css"></noscript>
|
| 39 |
+
<link rel="stylesheet" href="/static/pages/services/services.css?v=1.0" media="print" onload="this.media='all'">
|
| 40 |
+
<noscript><link rel="stylesheet" href="/static/pages/services/services.css?v=1.0"></noscript>
|
| 41 |
+
|
| 42 |
+
<!-- Scripts -->
|
| 43 |
+
<script src="/static/shared/js/utils/error-suppressor.js"></script>
|
| 44 |
+
<script src="/static/js/api-config.js"></script>
|
| 45 |
+
</head>
|
| 46 |
+
<body>
|
| 47 |
+
<div class="app-container">
|
| 48 |
+
<!-- Sidebar -->
|
| 49 |
+
<aside id="sidebar-container"></aside>
|
| 50 |
+
|
| 51 |
+
<!-- Main Content -->
|
| 52 |
+
<main class="main-content">
|
| 53 |
+
<!-- Header -->
|
| 54 |
+
<header id="header-container"></header>
|
| 55 |
+
|
| 56 |
+
<!-- Services Content -->
|
| 57 |
+
<div class="page-content">
|
| 58 |
+
<!-- Page Header -->
|
| 59 |
+
<div class="page-header">
|
| 60 |
+
<div class="page-title">
|
| 61 |
+
<h1>
|
| 62 |
+
<span class="page-icon">
|
| 63 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="url(#iconGradient)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 64 |
+
<defs>
|
| 65 |
+
<linearGradient id="iconGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
| 66 |
+
<stop offset="0%" stop-color="#2dd4bf"/>
|
| 67 |
+
<stop offset="50%" stop-color="#22d3ee"/>
|
| 68 |
+
<stop offset="100%" stop-color="#3b82f6"/>
|
| 69 |
+
</linearGradient>
|
| 70 |
+
</defs>
|
| 71 |
+
<line x1="12" y1="1" x2="12" y2="23"></line>
|
| 72 |
+
<path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path>
|
| 73 |
+
</svg>
|
| 74 |
+
</span>
|
| 75 |
+
Indicator Services
|
| 76 |
+
</h1>
|
| 77 |
+
<p class="page-subtitle">Technical Analysis Tools & Real-time Indicators</p>
|
| 78 |
+
</div>
|
| 79 |
+
<div class="page-actions">
|
| 80 |
+
<button id="refresh-btn" class="btn-icon" title="Refresh" aria-label="Refresh data">
|
| 81 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 82 |
+
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/>
|
| 83 |
+
<path d="M21 3v5h-5"/>
|
| 84 |
+
</svg>
|
| 85 |
+
</button>
|
| 86 |
+
<span id="last-update" class="last-update">Loading...</span>
|
| 87 |
+
</div>
|
| 88 |
+
</div>
|
| 89 |
+
|
| 90 |
+
<!-- Symbol Selector -->
|
| 91 |
+
<div class="symbol-selector glass-card">
|
| 92 |
+
<div class="form-group">
|
| 93 |
+
<label for="symbol-input">Symbol</label>
|
| 94 |
+
<input type="text" id="symbol-input" class="form-input" value="BTC" placeholder="Enter symbol (e.g., BTC, ETH)">
|
| 95 |
+
</div>
|
| 96 |
+
<div class="form-group">
|
| 97 |
+
<label for="timeframe-select">Timeframe</label>
|
| 98 |
+
<select id="timeframe-select" class="form-select">
|
| 99 |
+
<option value="1h" selected>1 Hour</option>
|
| 100 |
+
<option value="4h">4 Hours</option>
|
| 101 |
+
<option value="1d">1 Day</option>
|
| 102 |
+
</select>
|
| 103 |
+
</div>
|
| 104 |
+
<button id="analyze-all-btn" class="btn btn-gradient">
|
| 105 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 106 |
+
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline>
|
| 107 |
+
</svg>
|
| 108 |
+
Analyze All Indicators
|
| 109 |
+
</button>
|
| 110 |
+
</div>
|
| 111 |
+
|
| 112 |
+
<!-- Services Categories -->
|
| 113 |
+
<div class="services-categories">
|
| 114 |
+
<button class="category-btn active" data-category="all">All Services</button>
|
| 115 |
+
<button class="category-btn" data-category="volatility">Volatility</button>
|
| 116 |
+
<button class="category-btn" data-category="momentum">Momentum</button>
|
| 117 |
+
<button class="category-btn" data-category="trend">Trend</button>
|
| 118 |
+
</div>
|
| 119 |
+
|
| 120 |
+
<!-- Services Grid -->
|
| 121 |
+
<div id="services-grid" class="services-grid">
|
| 122 |
+
<!-- Will be populated by JavaScript -->
|
| 123 |
+
<div class="loading-state">
|
| 124 |
+
<div class="loading-spinner"></div>
|
| 125 |
+
<p>Loading services...</p>
|
| 126 |
+
</div>
|
| 127 |
+
</div>
|
| 128 |
+
|
| 129 |
+
<!-- Results Section -->
|
| 130 |
+
<div id="results-section" class="results-section" style="display: none;">
|
| 131 |
+
<h2 class="section-title">
|
| 132 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 133 |
+
<path d="M12 8V4H8"/>
|
| 134 |
+
<rect x="2" y="14" width="4" height="6" rx="1"/>
|
| 135 |
+
<rect x="10" y="10" width="4" height="10" rx="1"/>
|
| 136 |
+
<rect x="18" y="6" width="4" height="14" rx="1"/>
|
| 137 |
+
<path d="m8 4 4 4"/>
|
| 138 |
+
</svg>
|
| 139 |
+
Analysis Results
|
| 140 |
+
</h2>
|
| 141 |
+
<div id="results-container" class="results-container">
|
| 142 |
+
<!-- Results will be rendered here -->
|
| 143 |
+
</div>
|
| 144 |
+
</div>
|
| 145 |
+
</div>
|
| 146 |
+
</main>
|
| 147 |
+
</div>
|
| 148 |
+
|
| 149 |
+
<!-- Toast Container -->
|
| 150 |
+
<div id="toast-container" aria-live="polite"></div>
|
| 151 |
+
|
| 152 |
+
<script type="module">
|
| 153 |
+
(async function() {
|
| 154 |
+
try {
|
| 155 |
+
const { LayoutManager } = await import('/static/shared/js/core/layout-manager.js?v=3.0');
|
| 156 |
+
await LayoutManager.init('services');
|
| 157 |
+
|
| 158 |
+
const { default: ServicesPage } = await import('/static/pages/services/services.js?v=1.0');
|
| 159 |
+
window.servicesPage = ServicesPage;
|
| 160 |
+
} catch (error) {
|
| 161 |
+
console.error('Failed to initialize services page:', error);
|
| 162 |
+
}
|
| 163 |
+
})();
|
| 164 |
+
</script>
|
| 165 |
+
</body>
|
| 166 |
+
</html>
|
|
@@ -0,0 +1,528 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Services Page Styles
|
| 3 |
+
* Technical Indicator Services
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
/* Symbol Selector */
|
| 7 |
+
.symbol-selector {
|
| 8 |
+
display: flex;
|
| 9 |
+
flex-wrap: wrap;
|
| 10 |
+
align-items: flex-end;
|
| 11 |
+
gap: 1rem;
|
| 12 |
+
padding: 1.5rem;
|
| 13 |
+
margin-bottom: 1.5rem;
|
| 14 |
+
background: linear-gradient(135deg, rgba(20, 184, 166, 0.05), rgba(6, 182, 212, 0.03));
|
| 15 |
+
border: 1px solid rgba(20, 184, 166, 0.15);
|
| 16 |
+
border-radius: 16px;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
.symbol-selector .form-group {
|
| 20 |
+
flex: 1;
|
| 21 |
+
min-width: 150px;
|
| 22 |
+
margin: 0;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
.symbol-selector label {
|
| 26 |
+
display: block;
|
| 27 |
+
font-size: 0.75rem;
|
| 28 |
+
font-weight: 600;
|
| 29 |
+
text-transform: uppercase;
|
| 30 |
+
letter-spacing: 0.05em;
|
| 31 |
+
color: var(--text-muted);
|
| 32 |
+
margin-bottom: 0.5rem;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
.symbol-selector .form-input,
|
| 36 |
+
.symbol-selector .form-select {
|
| 37 |
+
width: 100%;
|
| 38 |
+
padding: 0.75rem 1rem;
|
| 39 |
+
border: 2px solid rgba(20, 184, 166, 0.2);
|
| 40 |
+
border-radius: 10px;
|
| 41 |
+
font-size: 1rem;
|
| 42 |
+
background: white;
|
| 43 |
+
transition: all 0.3s ease;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
.symbol-selector .form-input:focus,
|
| 47 |
+
.symbol-selector .form-select:focus {
|
| 48 |
+
outline: none;
|
| 49 |
+
border-color: #14b8a6;
|
| 50 |
+
box-shadow: 0 0 0 3px rgba(20, 184, 166, 0.1);
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
.symbol-selector .btn {
|
| 54 |
+
padding: 0.75rem 1.5rem;
|
| 55 |
+
display: flex;
|
| 56 |
+
align-items: center;
|
| 57 |
+
gap: 0.5rem;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
/* Services Categories */
|
| 61 |
+
.services-categories {
|
| 62 |
+
display: flex;
|
| 63 |
+
flex-wrap: wrap;
|
| 64 |
+
gap: 0.5rem;
|
| 65 |
+
margin-bottom: 1.5rem;
|
| 66 |
+
padding: 0.5rem;
|
| 67 |
+
background: rgba(248, 250, 252, 0.8);
|
| 68 |
+
border-radius: 12px;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
.category-btn {
|
| 72 |
+
padding: 0.625rem 1.25rem;
|
| 73 |
+
border: 2px solid transparent;
|
| 74 |
+
border-radius: 8px;
|
| 75 |
+
background: white;
|
| 76 |
+
font-size: 0.875rem;
|
| 77 |
+
font-weight: 600;
|
| 78 |
+
color: var(--text-secondary);
|
| 79 |
+
cursor: pointer;
|
| 80 |
+
transition: all 0.2s ease;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
.category-btn:hover {
|
| 84 |
+
background: rgba(20, 184, 166, 0.1);
|
| 85 |
+
color: #14b8a6;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.category-btn.active {
|
| 89 |
+
background: linear-gradient(135deg, #14b8a6, #06b6d4);
|
| 90 |
+
color: white;
|
| 91 |
+
border-color: transparent;
|
| 92 |
+
box-shadow: 0 4px 12px rgba(20, 184, 166, 0.3);
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
/* Services Grid */
|
| 96 |
+
.services-grid {
|
| 97 |
+
display: grid;
|
| 98 |
+
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
| 99 |
+
gap: 1.5rem;
|
| 100 |
+
margin-bottom: 2rem;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
/* Service Card */
|
| 104 |
+
.service-card-large {
|
| 105 |
+
background: white;
|
| 106 |
+
border: 1px solid rgba(20, 184, 166, 0.1);
|
| 107 |
+
border-radius: 16px;
|
| 108 |
+
overflow: hidden;
|
| 109 |
+
transition: all 0.3s ease;
|
| 110 |
+
cursor: pointer;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
.service-card-large:hover {
|
| 114 |
+
transform: translateY(-4px);
|
| 115 |
+
box-shadow: 0 12px 40px rgba(20, 184, 166, 0.15);
|
| 116 |
+
border-color: rgba(20, 184, 166, 0.3);
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
.service-card-header {
|
| 120 |
+
display: flex;
|
| 121 |
+
align-items: center;
|
| 122 |
+
gap: 1rem;
|
| 123 |
+
padding: 1.25rem;
|
| 124 |
+
background: linear-gradient(135deg, rgba(20, 184, 166, 0.08), rgba(6, 182, 212, 0.05));
|
| 125 |
+
border-bottom: 1px solid rgba(20, 184, 166, 0.1);
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
.service-card-icon {
|
| 129 |
+
width: 56px;
|
| 130 |
+
height: 56px;
|
| 131 |
+
display: flex;
|
| 132 |
+
align-items: center;
|
| 133 |
+
justify-content: center;
|
| 134 |
+
font-size: 2rem;
|
| 135 |
+
background: white;
|
| 136 |
+
border-radius: 14px;
|
| 137 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
.service-card-title {
|
| 141 |
+
flex: 1;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
.service-card-title h3 {
|
| 145 |
+
font-size: 1.125rem;
|
| 146 |
+
font-weight: 700;
|
| 147 |
+
color: var(--text-primary);
|
| 148 |
+
margin: 0 0 0.25rem;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
.service-card-title .category-tag {
|
| 152 |
+
font-size: 0.75rem;
|
| 153 |
+
font-weight: 600;
|
| 154 |
+
text-transform: uppercase;
|
| 155 |
+
letter-spacing: 0.05em;
|
| 156 |
+
color: #14b8a6;
|
| 157 |
+
padding: 2px 8px;
|
| 158 |
+
background: rgba(20, 184, 166, 0.1);
|
| 159 |
+
border-radius: 4px;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
.service-card-body {
|
| 163 |
+
padding: 1.25rem;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
.service-card-desc {
|
| 167 |
+
font-size: 0.9rem;
|
| 168 |
+
color: var(--text-secondary);
|
| 169 |
+
margin-bottom: 1rem;
|
| 170 |
+
line-height: 1.6;
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
.service-card-params {
|
| 174 |
+
display: flex;
|
| 175 |
+
flex-wrap: wrap;
|
| 176 |
+
gap: 0.5rem;
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
.param-tag {
|
| 180 |
+
font-size: 0.75rem;
|
| 181 |
+
padding: 4px 10px;
|
| 182 |
+
background: rgba(248, 250, 252, 0.8);
|
| 183 |
+
border: 1px solid rgba(0, 0, 0, 0.05);
|
| 184 |
+
border-radius: 6px;
|
| 185 |
+
color: var(--text-muted);
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
.service-card-footer {
|
| 189 |
+
display: flex;
|
| 190 |
+
align-items: center;
|
| 191 |
+
justify-content: space-between;
|
| 192 |
+
padding: 1rem 1.25rem;
|
| 193 |
+
background: rgba(248, 250, 252, 0.5);
|
| 194 |
+
border-top: 1px solid rgba(0, 0, 0, 0.05);
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
.service-card-footer .btn {
|
| 198 |
+
padding: 0.5rem 1rem;
|
| 199 |
+
font-size: 0.875rem;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
.service-status {
|
| 203 |
+
display: flex;
|
| 204 |
+
align-items: center;
|
| 205 |
+
gap: 0.5rem;
|
| 206 |
+
font-size: 0.8rem;
|
| 207 |
+
color: #10b981;
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
.service-status .status-dot {
|
| 211 |
+
width: 8px;
|
| 212 |
+
height: 8px;
|
| 213 |
+
background: #10b981;
|
| 214 |
+
border-radius: 50%;
|
| 215 |
+
animation: pulse 2s infinite;
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
@keyframes pulse {
|
| 219 |
+
0%, 100% { opacity: 1; transform: scale(1); }
|
| 220 |
+
50% { opacity: 0.7; transform: scale(1.1); }
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
/* Results Section */
|
| 224 |
+
.results-section {
|
| 225 |
+
margin-top: 2rem;
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
.section-title {
|
| 229 |
+
display: flex;
|
| 230 |
+
align-items: center;
|
| 231 |
+
gap: 0.75rem;
|
| 232 |
+
font-size: 1.25rem;
|
| 233 |
+
font-weight: 700;
|
| 234 |
+
color: var(--text-primary);
|
| 235 |
+
margin-bottom: 1.5rem;
|
| 236 |
+
padding-bottom: 0.75rem;
|
| 237 |
+
border-bottom: 2px solid rgba(20, 184, 166, 0.2);
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
.section-title svg {
|
| 241 |
+
color: #14b8a6;
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
.results-container {
|
| 245 |
+
display: grid;
|
| 246 |
+
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
| 247 |
+
gap: 1.5rem;
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
/* Result Card */
|
| 251 |
+
.result-card {
|
| 252 |
+
background: white;
|
| 253 |
+
border: 1px solid rgba(20, 184, 166, 0.1);
|
| 254 |
+
border-radius: 16px;
|
| 255 |
+
overflow: hidden;
|
| 256 |
+
animation: fadeInUp 0.5s ease;
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
@keyframes fadeInUp {
|
| 260 |
+
from {
|
| 261 |
+
opacity: 0;
|
| 262 |
+
transform: translateY(20px);
|
| 263 |
+
}
|
| 264 |
+
to {
|
| 265 |
+
opacity: 1;
|
| 266 |
+
transform: translateY(0);
|
| 267 |
+
}
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
.result-card-header {
|
| 271 |
+
display: flex;
|
| 272 |
+
align-items: center;
|
| 273 |
+
justify-content: space-between;
|
| 274 |
+
padding: 1rem 1.25rem;
|
| 275 |
+
background: linear-gradient(135deg, rgba(20, 184, 166, 0.08), rgba(6, 182, 212, 0.05));
|
| 276 |
+
border-bottom: 1px solid rgba(20, 184, 166, 0.1);
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
.result-card-header h4 {
|
| 280 |
+
display: flex;
|
| 281 |
+
align-items: center;
|
| 282 |
+
gap: 0.5rem;
|
| 283 |
+
font-size: 1rem;
|
| 284 |
+
font-weight: 700;
|
| 285 |
+
color: var(--text-primary);
|
| 286 |
+
margin: 0;
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
.result-card-header .indicator-icon {
|
| 290 |
+
font-size: 1.25rem;
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
.signal-badge {
|
| 294 |
+
padding: 4px 12px;
|
| 295 |
+
border-radius: 20px;
|
| 296 |
+
font-size: 0.75rem;
|
| 297 |
+
font-weight: 700;
|
| 298 |
+
text-transform: uppercase;
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
.signal-badge.bullish,
|
| 302 |
+
.signal-badge.buy,
|
| 303 |
+
.signal-badge.oversold {
|
| 304 |
+
background: rgba(34, 197, 94, 0.15);
|
| 305 |
+
color: #16a34a;
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
.signal-badge.bearish,
|
| 309 |
+
.signal-badge.sell,
|
| 310 |
+
.signal-badge.overbought {
|
| 311 |
+
background: rgba(239, 68, 68, 0.15);
|
| 312 |
+
color: #dc2626;
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
.signal-badge.neutral,
|
| 316 |
+
.signal-badge.hold {
|
| 317 |
+
background: rgba(234, 179, 8, 0.15);
|
| 318 |
+
color: #ca8a04;
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
.result-card-body {
|
| 322 |
+
padding: 1.25rem;
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
.result-values {
|
| 326 |
+
display: grid;
|
| 327 |
+
grid-template-columns: repeat(2, 1fr);
|
| 328 |
+
gap: 1rem;
|
| 329 |
+
margin-bottom: 1rem;
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
.result-value {
|
| 333 |
+
text-align: center;
|
| 334 |
+
padding: 0.75rem;
|
| 335 |
+
background: rgba(248, 250, 252, 0.8);
|
| 336 |
+
border-radius: 10px;
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
.result-value .label {
|
| 340 |
+
display: block;
|
| 341 |
+
font-size: 0.7rem;
|
| 342 |
+
font-weight: 600;
|
| 343 |
+
text-transform: uppercase;
|
| 344 |
+
letter-spacing: 0.05em;
|
| 345 |
+
color: var(--text-muted);
|
| 346 |
+
margin-bottom: 0.25rem;
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
.result-value .value {
|
| 350 |
+
font-size: 1.125rem;
|
| 351 |
+
font-weight: 700;
|
| 352 |
+
color: var(--text-primary);
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
.result-description {
|
| 356 |
+
padding: 0.75rem;
|
| 357 |
+
background: rgba(20, 184, 166, 0.05);
|
| 358 |
+
border-radius: 8px;
|
| 359 |
+
border-left: 4px solid #14b8a6;
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
.result-description p {
|
| 363 |
+
font-size: 0.875rem;
|
| 364 |
+
color: var(--text-secondary);
|
| 365 |
+
margin: 0;
|
| 366 |
+
line-height: 1.5;
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
/* Loading State */
|
| 370 |
+
.loading-state {
|
| 371 |
+
grid-column: 1 / -1;
|
| 372 |
+
display: flex;
|
| 373 |
+
flex-direction: column;
|
| 374 |
+
align-items: center;
|
| 375 |
+
justify-content: center;
|
| 376 |
+
padding: 3rem;
|
| 377 |
+
color: var(--text-muted);
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
.loading-spinner {
|
| 381 |
+
width: 48px;
|
| 382 |
+
height: 48px;
|
| 383 |
+
border: 4px solid rgba(20, 184, 166, 0.2);
|
| 384 |
+
border-top-color: #14b8a6;
|
| 385 |
+
border-radius: 50%;
|
| 386 |
+
animation: spin 1s linear infinite;
|
| 387 |
+
margin-bottom: 1rem;
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
@keyframes spin {
|
| 391 |
+
to { transform: rotate(360deg); }
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
/* Error State */
|
| 395 |
+
.error-state {
|
| 396 |
+
grid-column: 1 / -1;
|
| 397 |
+
display: flex;
|
| 398 |
+
flex-direction: column;
|
| 399 |
+
align-items: center;
|
| 400 |
+
justify-content: center;
|
| 401 |
+
padding: 3rem;
|
| 402 |
+
text-align: center;
|
| 403 |
+
color: var(--text-muted);
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
.error-state svg {
|
| 407 |
+
color: #ef4444;
|
| 408 |
+
margin-bottom: 1rem;
|
| 409 |
+
}
|
| 410 |
+
|
| 411 |
+
/* Responsive */
|
| 412 |
+
@media (max-width: 768px) {
|
| 413 |
+
.symbol-selector {
|
| 414 |
+
flex-direction: column;
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
.symbol-selector .form-group {
|
| 418 |
+
width: 100%;
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
.symbol-selector .btn {
|
| 422 |
+
width: 100%;
|
| 423 |
+
justify-content: center;
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
.services-grid {
|
| 427 |
+
grid-template-columns: 1fr;
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
+
.results-container {
|
| 431 |
+
grid-template-columns: 1fr;
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
.result-values {
|
| 435 |
+
grid-template-columns: 1fr;
|
| 436 |
+
}
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
/* Dark Mode */
|
| 440 |
+
[data-theme="dark"] .symbol-selector {
|
| 441 |
+
background: linear-gradient(135deg, rgba(20, 184, 166, 0.1), rgba(6, 182, 212, 0.05));
|
| 442 |
+
border-color: rgba(20, 184, 166, 0.2);
|
| 443 |
+
}
|
| 444 |
+
|
| 445 |
+
[data-theme="dark"] .symbol-selector .form-input,
|
| 446 |
+
[data-theme="dark"] .symbol-selector .form-select {
|
| 447 |
+
background: rgba(30, 41, 59, 0.8);
|
| 448 |
+
border-color: rgba(20, 184, 166, 0.3);
|
| 449 |
+
color: #f1f5f9;
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
[data-theme="dark"] .services-categories {
|
| 453 |
+
background: rgba(30, 41, 59, 0.5);
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
[data-theme="dark"] .category-btn {
|
| 457 |
+
background: rgba(30, 41, 59, 0.8);
|
| 458 |
+
color: #94a3b8;
|
| 459 |
+
}
|
| 460 |
+
|
| 461 |
+
[data-theme="dark"] .category-btn:hover {
|
| 462 |
+
background: rgba(20, 184, 166, 0.2);
|
| 463 |
+
color: #2dd4bf;
|
| 464 |
+
}
|
| 465 |
+
|
| 466 |
+
[data-theme="dark"] .service-card-large {
|
| 467 |
+
background: #1e293b;
|
| 468 |
+
border-color: rgba(20, 184, 166, 0.2);
|
| 469 |
+
}
|
| 470 |
+
|
| 471 |
+
[data-theme="dark"] .service-card-header {
|
| 472 |
+
background: linear-gradient(135deg, rgba(20, 184, 166, 0.15), rgba(6, 182, 212, 0.1));
|
| 473 |
+
}
|
| 474 |
+
|
| 475 |
+
[data-theme="dark"] .service-card-icon {
|
| 476 |
+
background: rgba(15, 23, 42, 0.8);
|
| 477 |
+
}
|
| 478 |
+
|
| 479 |
+
[data-theme="dark"] .service-card-title h3 {
|
| 480 |
+
color: #f1f5f9;
|
| 481 |
+
}
|
| 482 |
+
|
| 483 |
+
[data-theme="dark"] .service-card-body {
|
| 484 |
+
background: #1e293b;
|
| 485 |
+
}
|
| 486 |
+
|
| 487 |
+
[data-theme="dark"] .service-card-desc {
|
| 488 |
+
color: #94a3b8;
|
| 489 |
+
}
|
| 490 |
+
|
| 491 |
+
[data-theme="dark"] .param-tag {
|
| 492 |
+
background: rgba(15, 23, 42, 0.8);
|
| 493 |
+
border-color: rgba(255, 255, 255, 0.1);
|
| 494 |
+
color: #94a3b8;
|
| 495 |
+
}
|
| 496 |
+
|
| 497 |
+
[data-theme="dark"] .service-card-footer {
|
| 498 |
+
background: rgba(15, 23, 42, 0.5);
|
| 499 |
+
}
|
| 500 |
+
|
| 501 |
+
[data-theme="dark"] .result-card {
|
| 502 |
+
background: #1e293b;
|
| 503 |
+
border-color: rgba(20, 184, 166, 0.2);
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
[data-theme="dark"] .result-card-header {
|
| 507 |
+
background: linear-gradient(135deg, rgba(20, 184, 166, 0.15), rgba(6, 182, 212, 0.1));
|
| 508 |
+
}
|
| 509 |
+
|
| 510 |
+
[data-theme="dark"] .result-card-header h4 {
|
| 511 |
+
color: #f1f5f9;
|
| 512 |
+
}
|
| 513 |
+
|
| 514 |
+
[data-theme="dark"] .result-value {
|
| 515 |
+
background: rgba(15, 23, 42, 0.5);
|
| 516 |
+
}
|
| 517 |
+
|
| 518 |
+
[data-theme="dark"] .result-value .value {
|
| 519 |
+
color: #f1f5f9;
|
| 520 |
+
}
|
| 521 |
+
|
| 522 |
+
[data-theme="dark"] .result-description {
|
| 523 |
+
background: rgba(20, 184, 166, 0.1);
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
+
[data-theme="dark"] .result-description p {
|
| 527 |
+
color: #94a3b8;
|
| 528 |
+
}
|
|
@@ -0,0 +1,522 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Services Page - Technical Indicator Services
|
| 3 |
+
*/
|
| 4 |
+
|
| 5 |
+
class ServicesPage {
|
| 6 |
+
constructor() {
|
| 7 |
+
this.services = [];
|
| 8 |
+
this.currentCategory = 'all';
|
| 9 |
+
this.currentSymbol = 'BTC';
|
| 10 |
+
this.currentTimeframe = '1h';
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
async init() {
|
| 14 |
+
console.log('[Services] Initializing...');
|
| 15 |
+
|
| 16 |
+
this.bindEvents();
|
| 17 |
+
await this.loadServices();
|
| 18 |
+
this.checkUrlParams();
|
| 19 |
+
|
| 20 |
+
console.log('[Services] Ready');
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
bindEvents() {
|
| 24 |
+
// Refresh button
|
| 25 |
+
document.getElementById('refresh-btn')?.addEventListener('click', () => {
|
| 26 |
+
this.loadServices();
|
| 27 |
+
});
|
| 28 |
+
|
| 29 |
+
// Symbol input
|
| 30 |
+
document.getElementById('symbol-input')?.addEventListener('change', (e) => {
|
| 31 |
+
this.currentSymbol = e.target.value.toUpperCase() || 'BTC';
|
| 32 |
+
});
|
| 33 |
+
|
| 34 |
+
// Timeframe select
|
| 35 |
+
document.getElementById('timeframe-select')?.addEventListener('change', (e) => {
|
| 36 |
+
this.currentTimeframe = e.target.value || '1h';
|
| 37 |
+
});
|
| 38 |
+
|
| 39 |
+
// Analyze all button
|
| 40 |
+
document.getElementById('analyze-all-btn')?.addEventListener('click', () => {
|
| 41 |
+
this.analyzeAll();
|
| 42 |
+
});
|
| 43 |
+
|
| 44 |
+
// Category buttons
|
| 45 |
+
document.querySelectorAll('.category-btn').forEach(btn => {
|
| 46 |
+
btn.addEventListener('click', (e) => {
|
| 47 |
+
document.querySelectorAll('.category-btn').forEach(b => b.classList.remove('active'));
|
| 48 |
+
e.target.classList.add('active');
|
| 49 |
+
this.currentCategory = e.target.dataset.category;
|
| 50 |
+
this.filterServices();
|
| 51 |
+
});
|
| 52 |
+
});
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
checkUrlParams() {
|
| 56 |
+
const params = new URLSearchParams(window.location.search);
|
| 57 |
+
const service = params.get('service');
|
| 58 |
+
|
| 59 |
+
if (service) {
|
| 60 |
+
// Auto-analyze the specific service
|
| 61 |
+
setTimeout(() => {
|
| 62 |
+
this.analyzeService(service);
|
| 63 |
+
}, 500);
|
| 64 |
+
}
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
async loadServices() {
|
| 68 |
+
const grid = document.getElementById('services-grid');
|
| 69 |
+
if (!grid) return;
|
| 70 |
+
|
| 71 |
+
grid.innerHTML = `
|
| 72 |
+
<div class="loading-state">
|
| 73 |
+
<div class="loading-spinner"></div>
|
| 74 |
+
<p>Loading indicator services...</p>
|
| 75 |
+
</div>
|
| 76 |
+
`;
|
| 77 |
+
|
| 78 |
+
try {
|
| 79 |
+
const response = await fetch('/api/indicators/services');
|
| 80 |
+
|
| 81 |
+
if (response.ok) {
|
| 82 |
+
const data = await response.json();
|
| 83 |
+
this.services = data.services || [];
|
| 84 |
+
console.log('[Services] Loaded', this.services.length, 'services');
|
| 85 |
+
} else {
|
| 86 |
+
// Use fallback data
|
| 87 |
+
this.services = this.getFallbackServices();
|
| 88 |
+
}
|
| 89 |
+
} catch (error) {
|
| 90 |
+
console.error('[Services] Load error:', error);
|
| 91 |
+
this.services = this.getFallbackServices();
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
this.renderServices();
|
| 95 |
+
this.updateTimestamp();
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
getFallbackServices() {
|
| 99 |
+
return [
|
| 100 |
+
{
|
| 101 |
+
id: 'bollinger_bands',
|
| 102 |
+
name: 'Bollinger Bands',
|
| 103 |
+
description: 'Volatility bands placed above and below a moving average. Identifies overbought/oversold conditions and potential breakouts.',
|
| 104 |
+
endpoint: '/api/indicators/bollinger-bands',
|
| 105 |
+
parameters: ['symbol', 'timeframe', 'period', 'std_dev'],
|
| 106 |
+
icon: 'π',
|
| 107 |
+
category: 'volatility'
|
| 108 |
+
},
|
| 109 |
+
{
|
| 110 |
+
id: 'stoch_rsi',
|
| 111 |
+
name: 'Stochastic RSI',
|
| 112 |
+
description: 'Combines Stochastic oscillator with RSI for enhanced momentum detection. Great for identifying extreme conditions.',
|
| 113 |
+
endpoint: '/api/indicators/stoch-rsi',
|
| 114 |
+
parameters: ['symbol', 'timeframe', 'rsi_period', 'stoch_period'],
|
| 115 |
+
icon: 'π',
|
| 116 |
+
category: 'momentum'
|
| 117 |
+
},
|
| 118 |
+
{
|
| 119 |
+
id: 'atr',
|
| 120 |
+
name: 'Average True Range (ATR)',
|
| 121 |
+
description: 'Measures market volatility by analyzing the range of price movements. Useful for setting stop losses.',
|
| 122 |
+
endpoint: '/api/indicators/atr',
|
| 123 |
+
parameters: ['symbol', 'timeframe', 'period'],
|
| 124 |
+
icon: 'π',
|
| 125 |
+
category: 'volatility'
|
| 126 |
+
},
|
| 127 |
+
{
|
| 128 |
+
id: 'sma',
|
| 129 |
+
name: 'Simple Moving Average (SMA)',
|
| 130 |
+
description: 'Average price over specified periods (20, 50, 200). Identifies trend direction and support/resistance levels.',
|
| 131 |
+
endpoint: '/api/indicators/sma',
|
| 132 |
+
parameters: ['symbol', 'timeframe'],
|
| 133 |
+
icon: 'γ°οΈ',
|
| 134 |
+
category: 'trend'
|
| 135 |
+
},
|
| 136 |
+
{
|
| 137 |
+
id: 'ema',
|
| 138 |
+
name: 'Exponential Moving Average (EMA)',
|
| 139 |
+
description: 'Weighted moving average giving more weight to recent prices. More responsive to current price action.',
|
| 140 |
+
endpoint: '/api/indicators/ema',
|
| 141 |
+
parameters: ['symbol', 'timeframe'],
|
| 142 |
+
icon: 'π',
|
| 143 |
+
category: 'trend'
|
| 144 |
+
},
|
| 145 |
+
{
|
| 146 |
+
id: 'macd',
|
| 147 |
+
name: 'MACD',
|
| 148 |
+
description: 'Moving Average Convergence Divergence. Trend-following momentum indicator showing relationship between EMAs.',
|
| 149 |
+
endpoint: '/api/indicators/macd',
|
| 150 |
+
parameters: ['symbol', 'timeframe', 'fast', 'slow', 'signal'],
|
| 151 |
+
icon: 'π',
|
| 152 |
+
category: 'momentum'
|
| 153 |
+
},
|
| 154 |
+
{
|
| 155 |
+
id: 'rsi',
|
| 156 |
+
name: 'RSI',
|
| 157 |
+
description: 'Relative Strength Index. Momentum oscillator measuring speed and magnitude of price movements (0-100).',
|
| 158 |
+
endpoint: '/api/indicators/rsi',
|
| 159 |
+
parameters: ['symbol', 'timeframe', 'period'],
|
| 160 |
+
icon: 'πͺ',
|
| 161 |
+
category: 'momentum'
|
| 162 |
+
},
|
| 163 |
+
{
|
| 164 |
+
id: 'comprehensive',
|
| 165 |
+
name: 'Comprehensive Analysis',
|
| 166 |
+
description: 'All indicators combined with trading signals. Get a complete market overview with actionable recommendations.',
|
| 167 |
+
endpoint: '/api/indicators/comprehensive',
|
| 168 |
+
parameters: ['symbol', 'timeframe'],
|
| 169 |
+
icon: 'π―',
|
| 170 |
+
category: 'analysis'
|
| 171 |
+
}
|
| 172 |
+
];
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
filterServices() {
|
| 176 |
+
this.renderServices();
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
renderServices() {
|
| 180 |
+
const grid = document.getElementById('services-grid');
|
| 181 |
+
if (!grid) return;
|
| 182 |
+
|
| 183 |
+
const filteredServices = this.currentCategory === 'all'
|
| 184 |
+
? this.services
|
| 185 |
+
: this.services.filter(s => s.category === this.currentCategory);
|
| 186 |
+
|
| 187 |
+
if (filteredServices.length === 0) {
|
| 188 |
+
grid.innerHTML = `
|
| 189 |
+
<div class="error-state">
|
| 190 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 191 |
+
<circle cx="12" cy="12" r="10"></circle>
|
| 192 |
+
<line x1="12" y1="8" x2="12" y2="12"></line>
|
| 193 |
+
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
| 194 |
+
</svg>
|
| 195 |
+
<h3>No services found</h3>
|
| 196 |
+
<p>No indicator services match the selected category.</p>
|
| 197 |
+
</div>
|
| 198 |
+
`;
|
| 199 |
+
return;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
grid.innerHTML = filteredServices.map(service => `
|
| 203 |
+
<div class="service-card-large" data-service="${service.id}">
|
| 204 |
+
<div class="service-card-header">
|
| 205 |
+
<div class="service-card-icon">${service.icon}</div>
|
| 206 |
+
<div class="service-card-title">
|
| 207 |
+
<h3>${service.name}</h3>
|
| 208 |
+
<span class="category-tag">${service.category}</span>
|
| 209 |
+
</div>
|
| 210 |
+
</div>
|
| 211 |
+
<div class="service-card-body">
|
| 212 |
+
<p class="service-card-desc">${service.description}</p>
|
| 213 |
+
<div class="service-card-params">
|
| 214 |
+
${service.parameters.map(p => `<span class="param-tag">${p}</span>`).join('')}
|
| 215 |
+
</div>
|
| 216 |
+
</div>
|
| 217 |
+
<div class="service-card-footer">
|
| 218 |
+
<div class="service-status">
|
| 219 |
+
<span class="status-dot"></span>
|
| 220 |
+
<span>Available</span>
|
| 221 |
+
</div>
|
| 222 |
+
<button class="btn btn-primary" onclick="servicesPage.analyzeService('${service.id}')">
|
| 223 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 224 |
+
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline>
|
| 225 |
+
</svg>
|
| 226 |
+
Analyze
|
| 227 |
+
</button>
|
| 228 |
+
</div>
|
| 229 |
+
</div>
|
| 230 |
+
`).join('');
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
async analyzeService(serviceId) {
|
| 234 |
+
const resultsSection = document.getElementById('results-section');
|
| 235 |
+
const resultsContainer = document.getElementById('results-container');
|
| 236 |
+
|
| 237 |
+
if (!resultsSection || !resultsContainer) return;
|
| 238 |
+
|
| 239 |
+
// Get current values
|
| 240 |
+
const symbolInput = document.getElementById('symbol-input');
|
| 241 |
+
const timeframeSelect = document.getElementById('timeframe-select');
|
| 242 |
+
|
| 243 |
+
this.currentSymbol = symbolInput?.value?.toUpperCase() || 'BTC';
|
| 244 |
+
this.currentTimeframe = timeframeSelect?.value || '1h';
|
| 245 |
+
|
| 246 |
+
// Show results section
|
| 247 |
+
resultsSection.style.display = 'block';
|
| 248 |
+
resultsContainer.innerHTML = `
|
| 249 |
+
<div class="loading-state">
|
| 250 |
+
<div class="loading-spinner"></div>
|
| 251 |
+
<p>Analyzing ${this.currentSymbol} with ${serviceId}...</p>
|
| 252 |
+
</div>
|
| 253 |
+
`;
|
| 254 |
+
|
| 255 |
+
// Scroll to results
|
| 256 |
+
resultsSection.scrollIntoView({ behavior: 'smooth' });
|
| 257 |
+
|
| 258 |
+
try {
|
| 259 |
+
const service = this.services.find(s => s.id === serviceId);
|
| 260 |
+
if (!service) throw new Error('Service not found');
|
| 261 |
+
|
| 262 |
+
const url = `${service.endpoint}?symbol=${encodeURIComponent(this.currentSymbol)}&timeframe=${encodeURIComponent(this.currentTimeframe)}`;
|
| 263 |
+
const response = await fetch(url);
|
| 264 |
+
|
| 265 |
+
if (!response.ok) {
|
| 266 |
+
throw new Error(`HTTP ${response.status}`);
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
const result = await response.json();
|
| 270 |
+
this.renderResult(service, result);
|
| 271 |
+
} catch (error) {
|
| 272 |
+
console.error('[Services] Analysis error:', error);
|
| 273 |
+
resultsContainer.innerHTML = `
|
| 274 |
+
<div class="error-state">
|
| 275 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 276 |
+
<circle cx="12" cy="12" r="10"></circle>
|
| 277 |
+
<line x1="15" y1="9" x2="9" y2="15"></line>
|
| 278 |
+
<line x1="9" y1="9" x2="15" y2="15"></line>
|
| 279 |
+
</svg>
|
| 280 |
+
<h3>Analysis Failed</h3>
|
| 281 |
+
<p>${error.message}</p>
|
| 282 |
+
<button class="btn btn-primary" onclick="servicesPage.analyzeService('${serviceId}')">Retry</button>
|
| 283 |
+
</div>
|
| 284 |
+
`;
|
| 285 |
+
}
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
async analyzeAll() {
|
| 289 |
+
const resultsSection = document.getElementById('results-section');
|
| 290 |
+
const resultsContainer = document.getElementById('results-container');
|
| 291 |
+
|
| 292 |
+
if (!resultsSection || !resultsContainer) return;
|
| 293 |
+
|
| 294 |
+
// Get current values
|
| 295 |
+
const symbolInput = document.getElementById('symbol-input');
|
| 296 |
+
const timeframeSelect = document.getElementById('timeframe-select');
|
| 297 |
+
|
| 298 |
+
this.currentSymbol = symbolInput?.value?.toUpperCase() || 'BTC';
|
| 299 |
+
this.currentTimeframe = timeframeSelect?.value || '1h';
|
| 300 |
+
|
| 301 |
+
// Show loading
|
| 302 |
+
resultsSection.style.display = 'block';
|
| 303 |
+
resultsContainer.innerHTML = `
|
| 304 |
+
<div class="loading-state">
|
| 305 |
+
<div class="loading-spinner"></div>
|
| 306 |
+
<p>Running comprehensive analysis on ${this.currentSymbol}...</p>
|
| 307 |
+
</div>
|
| 308 |
+
`;
|
| 309 |
+
|
| 310 |
+
resultsSection.scrollIntoView({ behavior: 'smooth' });
|
| 311 |
+
|
| 312 |
+
try {
|
| 313 |
+
const url = `/api/indicators/comprehensive?symbol=${encodeURIComponent(this.currentSymbol)}&timeframe=${encodeURIComponent(this.currentTimeframe)}`;
|
| 314 |
+
const response = await fetch(url);
|
| 315 |
+
|
| 316 |
+
if (!response.ok) {
|
| 317 |
+
throw new Error(`HTTP ${response.status}`);
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
const result = await response.json();
|
| 321 |
+
this.renderComprehensiveResult(result);
|
| 322 |
+
} catch (error) {
|
| 323 |
+
console.error('[Services] Comprehensive analysis error:', error);
|
| 324 |
+
resultsContainer.innerHTML = `
|
| 325 |
+
<div class="error-state">
|
| 326 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 327 |
+
<circle cx="12" cy="12" r="10"></circle>
|
| 328 |
+
<line x1="15" y1="9" x2="9" y2="15"></line>
|
| 329 |
+
<line x1="9" y1="9" x2="15" y2="15"></line>
|
| 330 |
+
</svg>
|
| 331 |
+
<h3>Analysis Failed</h3>
|
| 332 |
+
<p>${error.message}</p>
|
| 333 |
+
<button class="btn btn-primary" onclick="servicesPage.analyzeAll()">Retry</button>
|
| 334 |
+
</div>
|
| 335 |
+
`;
|
| 336 |
+
}
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
renderResult(service, result) {
|
| 340 |
+
const resultsContainer = document.getElementById('results-container');
|
| 341 |
+
if (!resultsContainer) return;
|
| 342 |
+
|
| 343 |
+
const signalClass = this.getSignalClass(result.signal);
|
| 344 |
+
const data = result.data || {};
|
| 345 |
+
|
| 346 |
+
let valuesHtml = '';
|
| 347 |
+
for (const [key, value] of Object.entries(data)) {
|
| 348 |
+
if (value !== null && value !== undefined) {
|
| 349 |
+
valuesHtml += `
|
| 350 |
+
<div class="result-value">
|
| 351 |
+
<span class="label">${this.formatLabel(key)}</span>
|
| 352 |
+
<span class="value">${this.formatValue(value)}</span>
|
| 353 |
+
</div>
|
| 354 |
+
`;
|
| 355 |
+
}
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
resultsContainer.innerHTML = `
|
| 359 |
+
<div class="result-card">
|
| 360 |
+
<div class="result-card-header">
|
| 361 |
+
<h4>
|
| 362 |
+
<span class="indicator-icon">${service.icon}</span>
|
| 363 |
+
${service.name}
|
| 364 |
+
</h4>
|
| 365 |
+
<span class="signal-badge ${signalClass}">${result.signal || 'N/A'}</span>
|
| 366 |
+
</div>
|
| 367 |
+
<div class="result-card-body">
|
| 368 |
+
<div class="result-values">
|
| 369 |
+
${valuesHtml}
|
| 370 |
+
</div>
|
| 371 |
+
<div class="result-description">
|
| 372 |
+
<p>${result.description || 'No description available'}</p>
|
| 373 |
+
</div>
|
| 374 |
+
</div>
|
| 375 |
+
</div>
|
| 376 |
+
`;
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
renderComprehensiveResult(result) {
|
| 380 |
+
const resultsContainer = document.getElementById('results-container');
|
| 381 |
+
if (!resultsContainer) return;
|
| 382 |
+
|
| 383 |
+
const indicators = result.indicators || {};
|
| 384 |
+
const signals = result.signals || {};
|
| 385 |
+
|
| 386 |
+
let cardsHtml = '';
|
| 387 |
+
|
| 388 |
+
// Overall signal card
|
| 389 |
+
const overallClass = this.getSignalClass(result.overall_signal?.toLowerCase());
|
| 390 |
+
cardsHtml += `
|
| 391 |
+
<div class="result-card" style="grid-column: 1 / -1;">
|
| 392 |
+
<div class="result-card-header" style="background: linear-gradient(135deg, rgba(20, 184, 166, 0.2), rgba(6, 182, 212, 0.15));">
|
| 393 |
+
<h4>
|
| 394 |
+
<span class="indicator-icon">π―</span>
|
| 395 |
+
Overall Analysis - ${result.symbol || this.currentSymbol}
|
| 396 |
+
</h4>
|
| 397 |
+
<span class="signal-badge ${overallClass}">${result.overall_signal || 'N/A'}</span>
|
| 398 |
+
</div>
|
| 399 |
+
<div class="result-card-body">
|
| 400 |
+
<div class="result-values">
|
| 401 |
+
<div class="result-value">
|
| 402 |
+
<span class="label">Current Price</span>
|
| 403 |
+
<span class="value">${this.formatValue(result.current_price)}</span>
|
| 404 |
+
</div>
|
| 405 |
+
<div class="result-value">
|
| 406 |
+
<span class="label">Confidence</span>
|
| 407 |
+
<span class="value">${result.confidence || 0}%</span>
|
| 408 |
+
</div>
|
| 409 |
+
</div>
|
| 410 |
+
<div class="result-description">
|
| 411 |
+
<p><strong>Recommendation:</strong> ${result.recommendation || 'No recommendation available'}</p>
|
| 412 |
+
</div>
|
| 413 |
+
</div>
|
| 414 |
+
</div>
|
| 415 |
+
`;
|
| 416 |
+
|
| 417 |
+
// Individual indicator cards
|
| 418 |
+
const indicatorMeta = {
|
| 419 |
+
bollinger_bands: { icon: 'π', name: 'Bollinger Bands' },
|
| 420 |
+
stoch_rsi: { icon: 'π', name: 'Stochastic RSI' },
|
| 421 |
+
atr: { icon: 'π', name: 'ATR' },
|
| 422 |
+
sma: { icon: 'γ°οΈ', name: 'SMA' },
|
| 423 |
+
ema: { icon: 'π', name: 'EMA' },
|
| 424 |
+
macd: { icon: 'π', name: 'MACD' },
|
| 425 |
+
rsi: { icon: 'πͺ', name: 'RSI' }
|
| 426 |
+
};
|
| 427 |
+
|
| 428 |
+
for (const [key, data] of Object.entries(indicators)) {
|
| 429 |
+
const meta = indicatorMeta[key] || { icon: 'π', name: key };
|
| 430 |
+
const signal = signals[key] || 'neutral';
|
| 431 |
+
const signalClass = this.getSignalClass(signal);
|
| 432 |
+
|
| 433 |
+
let valuesHtml = '';
|
| 434 |
+
if (typeof data === 'object') {
|
| 435 |
+
for (const [k, v] of Object.entries(data)) {
|
| 436 |
+
if (v !== null && v !== undefined) {
|
| 437 |
+
valuesHtml += `
|
| 438 |
+
<div class="result-value">
|
| 439 |
+
<span class="label">${this.formatLabel(k)}</span>
|
| 440 |
+
<span class="value">${this.formatValue(v)}</span>
|
| 441 |
+
</div>
|
| 442 |
+
`;
|
| 443 |
+
}
|
| 444 |
+
}
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
cardsHtml += `
|
| 448 |
+
<div class="result-card">
|
| 449 |
+
<div class="result-card-header">
|
| 450 |
+
<h4>
|
| 451 |
+
<span class="indicator-icon">${meta.icon}</span>
|
| 452 |
+
${meta.name}
|
| 453 |
+
</h4>
|
| 454 |
+
<span class="signal-badge ${signalClass}">${signal}</span>
|
| 455 |
+
</div>
|
| 456 |
+
<div class="result-card-body">
|
| 457 |
+
<div class="result-values">
|
| 458 |
+
${valuesHtml || '<p style="grid-column: 1/-1; text-align: center; color: var(--text-muted);">No data</p>'}
|
| 459 |
+
</div>
|
| 460 |
+
</div>
|
| 461 |
+
</div>
|
| 462 |
+
`;
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
resultsContainer.innerHTML = cardsHtml;
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
getSignalClass(signal) {
|
| 469 |
+
if (!signal) return 'neutral';
|
| 470 |
+
const s = signal.toLowerCase();
|
| 471 |
+
|
| 472 |
+
if (s.includes('buy') || s.includes('bullish') || s.includes('oversold') || s.includes('strong_buy')) {
|
| 473 |
+
return 'bullish';
|
| 474 |
+
}
|
| 475 |
+
if (s.includes('sell') || s.includes('bearish') || s.includes('overbought') || s.includes('strong_sell')) {
|
| 476 |
+
return 'bearish';
|
| 477 |
+
}
|
| 478 |
+
return 'neutral';
|
| 479 |
+
}
|
| 480 |
+
|
| 481 |
+
formatLabel(key) {
|
| 482 |
+
return key
|
| 483 |
+
.replace(/_/g, ' ')
|
| 484 |
+
.replace(/([A-Z])/g, ' $1')
|
| 485 |
+
.split(' ')
|
| 486 |
+
.map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
|
| 487 |
+
.join(' ');
|
| 488 |
+
}
|
| 489 |
+
|
| 490 |
+
formatValue(value) {
|
| 491 |
+
if (value === null || value === undefined) return 'β';
|
| 492 |
+
if (typeof value === 'number') {
|
| 493 |
+
if (value > 1000000) return (value / 1000000).toFixed(2) + 'M';
|
| 494 |
+
if (value > 1000) return (value / 1000).toFixed(2) + 'K';
|
| 495 |
+
if (value < 0.0001 && value > 0) return value.toExponential(2);
|
| 496 |
+
if (Number.isInteger(value)) return value.toLocaleString();
|
| 497 |
+
return value.toFixed(value < 1 ? 4 : 2);
|
| 498 |
+
}
|
| 499 |
+
return String(value);
|
| 500 |
+
}
|
| 501 |
+
|
| 502 |
+
updateTimestamp() {
|
| 503 |
+
const el = document.getElementById('last-update');
|
| 504 |
+
if (el) {
|
| 505 |
+
el.textContent = `Updated: ${new Date().toLocaleTimeString()}`;
|
| 506 |
+
}
|
| 507 |
+
}
|
| 508 |
+
|
| 509 |
+
showToast(message, type = 'info') {
|
| 510 |
+
console.log(`[Toast ${type}]`, message);
|
| 511 |
+
// Implement toast if needed
|
| 512 |
+
}
|
| 513 |
+
}
|
| 514 |
+
|
| 515 |
+
// Initialize
|
| 516 |
+
const servicesPage = new ServicesPage();
|
| 517 |
+
servicesPage.init();
|
| 518 |
+
|
| 519 |
+
// Expose globally
|
| 520 |
+
window.servicesPage = servicesPage;
|
| 521 |
+
|
| 522 |
+
export default servicesPage;
|
|
@@ -682,10 +682,107 @@ class TechnicalAnalysisProfessional {
|
|
| 682 |
|
| 683 |
// Calculate EMA
|
| 684 |
this.indicators.ema = this.calculateEMA(ohlcvData, 20);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 685 |
|
| 686 |
// Update indicator displays
|
| 687 |
this.updateIndicatorDisplays();
|
| 688 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 689 |
|
| 690 |
/**
|
| 691 |
* Calculate RSI (Relative Strength Index)
|
|
@@ -791,6 +888,129 @@ class TechnicalAnalysisProfessional {
|
|
| 791 |
if (emaElement && this.indicators.ema !== null) {
|
| 792 |
emaElement.textContent = safeFormatCurrency(this.indicators.ema);
|
| 793 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 794 |
}
|
| 795 |
|
| 796 |
/**
|
|
@@ -854,17 +1074,48 @@ class TechnicalAnalysisProfessional {
|
|
| 854 |
const rsi = this.indicators.rsi;
|
| 855 |
const macd = this.indicators.macd;
|
| 856 |
const ema = this.indicators.ema;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 857 |
|
| 858 |
-
// Determine trend
|
| 859 |
let trend = 'neutral';
|
| 860 |
let trendDescription = 'Market is consolidating';
|
|
|
|
| 861 |
|
|
|
|
| 862 |
if (latestCandle.close > ema) {
|
| 863 |
-
|
| 864 |
-
trendDescription = 'Price is above EMA - Bullish trend';
|
| 865 |
} else if (latestCandle.close < ema) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 866 |
trend = 'bearish';
|
| 867 |
-
trendDescription =
|
| 868 |
}
|
| 869 |
|
| 870 |
// Generate indicator analysis
|
|
@@ -908,40 +1159,153 @@ class TechnicalAnalysisProfessional {
|
|
| 908 |
interpretation: emaStatus === 'bullish' ? 'Price above EMA' : 'Price below EMA'
|
| 909 |
});
|
| 910 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 911 |
|
| 912 |
-
// Generate signal
|
| 913 |
let signal = 'hold';
|
| 914 |
let recommendation = 'Wait for clearer signals';
|
| 915 |
|
| 916 |
const bullishSignals = indicators.filter(i => i.status === 'bullish' || i.status === 'oversold').length;
|
| 917 |
const bearishSignals = indicators.filter(i => i.status === 'bearish' || i.status === 'overbought').length;
|
|
|
|
| 918 |
|
| 919 |
-
if (bullishSignals
|
| 920 |
signal = 'buy';
|
| 921 |
-
recommendation =
|
| 922 |
-
} else if (bearishSignals
|
| 923 |
signal = 'sell';
|
| 924 |
-
recommendation =
|
|
|
|
|
|
|
| 925 |
}
|
| 926 |
|
| 927 |
-
// Calculate risk
|
| 928 |
let riskScore = 50;
|
| 929 |
let risk = 'medium';
|
| 930 |
|
|
|
|
| 931 |
if (rsi !== null) {
|
| 932 |
-
if (rsi >
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 933 |
}
|
| 934 |
|
|
|
|
| 935 |
if (trend === 'bullish' && signal === 'buy') {
|
| 936 |
-
riskScore -=
|
| 937 |
} else if (trend === 'bearish' && signal === 'sell') {
|
| 938 |
-
riskScore -=
|
| 939 |
}
|
| 940 |
|
| 941 |
riskScore = Math.max(10, Math.min(90, riskScore));
|
| 942 |
|
| 943 |
-
if (riskScore <
|
| 944 |
-
else if (riskScore >
|
| 945 |
|
| 946 |
return {
|
| 947 |
trend,
|
|
|
|
| 682 |
|
| 683 |
// Calculate EMA
|
| 684 |
this.indicators.ema = this.calculateEMA(ohlcvData, 20);
|
| 685 |
+
|
| 686 |
+
// Calculate Bollinger Bands
|
| 687 |
+
this.indicators.bollingerBands = this.calculateBollingerBands(ohlcvData);
|
| 688 |
+
|
| 689 |
+
// Calculate SMA
|
| 690 |
+
this.indicators.sma20 = this.calculateSMA(ohlcvData, 20);
|
| 691 |
+
this.indicators.sma50 = this.calculateSMA(ohlcvData, 50);
|
| 692 |
+
|
| 693 |
+
// Calculate Stochastic RSI
|
| 694 |
+
this.indicators.stochRsi = this.calculateStochRSI(ohlcvData);
|
| 695 |
+
|
| 696 |
+
// Calculate ATR (Average True Range)
|
| 697 |
+
this.indicators.atr = this.calculateATR(ohlcvData);
|
| 698 |
|
| 699 |
// Update indicator displays
|
| 700 |
this.updateIndicatorDisplays();
|
| 701 |
}
|
| 702 |
+
|
| 703 |
+
/**
|
| 704 |
+
* Calculate Bollinger Bands
|
| 705 |
+
*/
|
| 706 |
+
calculateBollingerBands(data, period = 20, stdDev = 2) {
|
| 707 |
+
if (data.length < period) return null;
|
| 708 |
+
|
| 709 |
+
const closes = data.map(d => d.close);
|
| 710 |
+
const sma = this.calculateSMA(data, period);
|
| 711 |
+
|
| 712 |
+
// Calculate standard deviation
|
| 713 |
+
const slice = closes.slice(-period);
|
| 714 |
+
const mean = slice.reduce((a, b) => a + b, 0) / period;
|
| 715 |
+
const variance = slice.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / period;
|
| 716 |
+
const std = Math.sqrt(variance);
|
| 717 |
+
|
| 718 |
+
return {
|
| 719 |
+
upper: sma + (stdDev * std),
|
| 720 |
+
middle: sma,
|
| 721 |
+
lower: sma - (stdDev * std),
|
| 722 |
+
bandwidth: ((sma + (stdDev * std)) - (sma - (stdDev * std))) / sma * 100
|
| 723 |
+
};
|
| 724 |
+
}
|
| 725 |
+
|
| 726 |
+
/**
|
| 727 |
+
* Calculate Simple Moving Average
|
| 728 |
+
*/
|
| 729 |
+
calculateSMA(data, period) {
|
| 730 |
+
if (data.length < period) return null;
|
| 731 |
+
const closes = data.slice(-period).map(d => d.close);
|
| 732 |
+
return closes.reduce((a, b) => a + b, 0) / period;
|
| 733 |
+
}
|
| 734 |
+
|
| 735 |
+
/**
|
| 736 |
+
* Calculate Stochastic RSI
|
| 737 |
+
*/
|
| 738 |
+
calculateStochRSI(data, rsiPeriod = 14, stochPeriod = 14) {
|
| 739 |
+
if (data.length < rsiPeriod + stochPeriod) return null;
|
| 740 |
+
|
| 741 |
+
// First calculate RSI values for last stochPeriod+1 candles
|
| 742 |
+
const rsiValues = [];
|
| 743 |
+
for (let i = data.length - stochPeriod - 1; i < data.length; i++) {
|
| 744 |
+
const slice = data.slice(Math.max(0, i - rsiPeriod), i + 1);
|
| 745 |
+
if (slice.length >= rsiPeriod) {
|
| 746 |
+
const rsi = this.calculateRSI(slice, rsiPeriod);
|
| 747 |
+
if (rsi !== null) rsiValues.push(rsi);
|
| 748 |
+
}
|
| 749 |
+
}
|
| 750 |
+
|
| 751 |
+
if (rsiValues.length < 2) return null;
|
| 752 |
+
|
| 753 |
+
const minRsi = Math.min(...rsiValues);
|
| 754 |
+
const maxRsi = Math.max(...rsiValues);
|
| 755 |
+
const currentRsi = rsiValues[rsiValues.length - 1];
|
| 756 |
+
|
| 757 |
+
if (maxRsi === minRsi) return 50;
|
| 758 |
+
|
| 759 |
+
return ((currentRsi - minRsi) / (maxRsi - minRsi)) * 100;
|
| 760 |
+
}
|
| 761 |
+
|
| 762 |
+
/**
|
| 763 |
+
* Calculate Average True Range (ATR)
|
| 764 |
+
*/
|
| 765 |
+
calculateATR(data, period = 14) {
|
| 766 |
+
if (data.length < period + 1) return null;
|
| 767 |
+
|
| 768 |
+
const trueRanges = [];
|
| 769 |
+
for (let i = 1; i < data.length; i++) {
|
| 770 |
+
const high = data[i].high;
|
| 771 |
+
const low = data[i].low;
|
| 772 |
+
const prevClose = data[i - 1].close;
|
| 773 |
+
|
| 774 |
+
const tr = Math.max(
|
| 775 |
+
high - low,
|
| 776 |
+
Math.abs(high - prevClose),
|
| 777 |
+
Math.abs(low - prevClose)
|
| 778 |
+
);
|
| 779 |
+
trueRanges.push(tr);
|
| 780 |
+
}
|
| 781 |
+
|
| 782 |
+
// Calculate ATR as SMA of true ranges
|
| 783 |
+
const recentTR = trueRanges.slice(-period);
|
| 784 |
+
return recentTR.reduce((a, b) => a + b, 0) / period;
|
| 785 |
+
}
|
| 786 |
|
| 787 |
/**
|
| 788 |
* Calculate RSI (Relative Strength Index)
|
|
|
|
| 888 |
if (emaElement && this.indicators.ema !== null) {
|
| 889 |
emaElement.textContent = safeFormatCurrency(this.indicators.ema);
|
| 890 |
}
|
| 891 |
+
|
| 892 |
+
// Create or update extended indicators panel
|
| 893 |
+
this.renderExtendedIndicators();
|
| 894 |
+
}
|
| 895 |
+
|
| 896 |
+
/**
|
| 897 |
+
* Render extended indicators panel
|
| 898 |
+
*/
|
| 899 |
+
renderExtendedIndicators() {
|
| 900 |
+
// Find or create extended indicators container
|
| 901 |
+
let container = document.getElementById('extended-indicators');
|
| 902 |
+
if (!container) {
|
| 903 |
+
const indicatorsSection = document.querySelector('.page-content');
|
| 904 |
+
if (indicatorsSection) {
|
| 905 |
+
const analysisResults = document.getElementById('analysis-results');
|
| 906 |
+
if (analysisResults) {
|
| 907 |
+
container = document.createElement('div');
|
| 908 |
+
container.id = 'extended-indicators';
|
| 909 |
+
container.style.cssText = 'background: rgba(0,0,0,0.2); border-radius: 16px; padding: 1.5rem; margin-bottom: 1.5rem;';
|
| 910 |
+
analysisResults.parentNode.insertBefore(container, analysisResults);
|
| 911 |
+
}
|
| 912 |
+
}
|
| 913 |
+
}
|
| 914 |
+
|
| 915 |
+
if (!container) return;
|
| 916 |
+
|
| 917 |
+
const bb = this.indicators.bollingerBands;
|
| 918 |
+
const stochRsi = this.indicators.stochRsi;
|
| 919 |
+
const atr = this.indicators.atr;
|
| 920 |
+
const sma20 = this.indicators.sma20;
|
| 921 |
+
const sma50 = this.indicators.sma50;
|
| 922 |
+
const latestPrice = this.ohlcvData.length > 0 ? this.ohlcvData[this.ohlcvData.length - 1].close : 0;
|
| 923 |
+
|
| 924 |
+
// Determine BB position
|
| 925 |
+
let bbPosition = 'neutral';
|
| 926 |
+
let bbColor = '#fbbf24';
|
| 927 |
+
if (bb && latestPrice) {
|
| 928 |
+
if (latestPrice >= bb.upper) {
|
| 929 |
+
bbPosition = 'Upper Band';
|
| 930 |
+
bbColor = '#ef4444';
|
| 931 |
+
} else if (latestPrice <= bb.lower) {
|
| 932 |
+
bbPosition = 'Lower Band';
|
| 933 |
+
bbColor = '#22c55e';
|
| 934 |
+
} else {
|
| 935 |
+
const midDistance = (latestPrice - bb.lower) / (bb.upper - bb.lower);
|
| 936 |
+
if (midDistance > 0.7) {
|
| 937 |
+
bbPosition = 'Near Upper';
|
| 938 |
+
bbColor = '#f59e0b';
|
| 939 |
+
} else if (midDistance < 0.3) {
|
| 940 |
+
bbPosition = 'Near Lower';
|
| 941 |
+
bbColor = '#10b981';
|
| 942 |
+
} else {
|
| 943 |
+
bbPosition = 'Middle Band';
|
| 944 |
+
bbColor = '#3b82f6';
|
| 945 |
+
}
|
| 946 |
+
}
|
| 947 |
+
}
|
| 948 |
+
|
| 949 |
+
// Determine trend from SMAs
|
| 950 |
+
let smaTrend = 'neutral';
|
| 951 |
+
let smaColor = '#fbbf24';
|
| 952 |
+
if (sma20 && sma50) {
|
| 953 |
+
if (sma20 > sma50) {
|
| 954 |
+
smaTrend = 'Bullish (20 > 50)';
|
| 955 |
+
smaColor = '#22c55e';
|
| 956 |
+
} else {
|
| 957 |
+
smaTrend = 'Bearish (20 < 50)';
|
| 958 |
+
smaColor = '#ef4444';
|
| 959 |
+
}
|
| 960 |
+
}
|
| 961 |
+
|
| 962 |
+
container.innerHTML = `
|
| 963 |
+
<h3 style="margin: 0 0 1rem 0; font-size: 1.125rem; display: flex; align-items: center; gap: 0.5rem;">
|
| 964 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 965 |
+
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline>
|
| 966 |
+
</svg>
|
| 967 |
+
Advanced Indicators
|
| 968 |
+
</h3>
|
| 969 |
+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;">
|
| 970 |
+
<!-- Bollinger Bands -->
|
| 971 |
+
<div style="background: rgba(0,0,0,0.2); border-radius: 8px; padding: 1rem; border-left: 4px solid ${bbColor};">
|
| 972 |
+
<div style="font-size: 0.75rem; text-transform: uppercase; color: var(--text-muted); margin-bottom: 0.5rem;">Bollinger Bands (20,2)</div>
|
| 973 |
+
<div style="font-size: 1.25rem; font-weight: 700; color: ${bbColor};">${bbPosition}</div>
|
| 974 |
+
${bb ? `
|
| 975 |
+
<div style="font-size: 0.75rem; color: var(--text-muted); margin-top: 0.5rem;">
|
| 976 |
+
Upper: ${safeFormatCurrency(bb.upper)}<br>
|
| 977 |
+
Middle: ${safeFormatCurrency(bb.middle)}<br>
|
| 978 |
+
Lower: ${safeFormatCurrency(bb.lower)}<br>
|
| 979 |
+
Bandwidth: ${bb.bandwidth.toFixed(2)}%
|
| 980 |
+
</div>` : ''}
|
| 981 |
+
</div>
|
| 982 |
+
|
| 983 |
+
<!-- Stochastic RSI -->
|
| 984 |
+
<div style="background: rgba(0,0,0,0.2); border-radius: 8px; padding: 1rem; border-left: 4px solid ${stochRsi > 80 ? '#ef4444' : stochRsi < 20 ? '#22c55e' : '#fbbf24'};">
|
| 985 |
+
<div style="font-size: 0.75rem; text-transform: uppercase; color: var(--text-muted); margin-bottom: 0.5rem;">Stochastic RSI</div>
|
| 986 |
+
<div style="font-size: 1.5rem; font-weight: 700; color: ${stochRsi > 80 ? '#ef4444' : stochRsi < 20 ? '#22c55e' : '#fbbf24'};">
|
| 987 |
+
${stochRsi !== null ? stochRsi.toFixed(1) : '--'}
|
| 988 |
+
</div>
|
| 989 |
+
<div style="font-size: 0.75rem; color: var(--text-muted); margin-top: 0.25rem;">
|
| 990 |
+
${stochRsi > 80 ? 'Overbought' : stochRsi < 20 ? 'Oversold' : 'Neutral'}
|
| 991 |
+
</div>
|
| 992 |
+
</div>
|
| 993 |
+
|
| 994 |
+
<!-- ATR -->
|
| 995 |
+
<div style="background: rgba(0,0,0,0.2); border-radius: 8px; padding: 1rem; border-left: 4px solid #3b82f6;">
|
| 996 |
+
<div style="font-size: 0.75rem; text-transform: uppercase; color: var(--text-muted); margin-bottom: 0.5rem;">ATR (14)</div>
|
| 997 |
+
<div style="font-size: 1.5rem; font-weight: 700;">${atr !== null ? safeFormatCurrency(atr) : '--'}</div>
|
| 998 |
+
<div style="font-size: 0.75rem; color: var(--text-muted); margin-top: 0.25rem;">
|
| 999 |
+
Volatility: ${atr && latestPrice ? (atr / latestPrice * 100).toFixed(2) + '%' : '--'}
|
| 1000 |
+
</div>
|
| 1001 |
+
</div>
|
| 1002 |
+
|
| 1003 |
+
<!-- SMA Crossover -->
|
| 1004 |
+
<div style="background: rgba(0,0,0,0.2); border-radius: 8px; padding: 1rem; border-left: 4px solid ${smaColor};">
|
| 1005 |
+
<div style="font-size: 0.75rem; text-transform: uppercase; color: var(--text-muted); margin-bottom: 0.5rem;">SMA Crossover</div>
|
| 1006 |
+
<div style="font-size: 1.25rem; font-weight: 700; color: ${smaColor};">${smaTrend}</div>
|
| 1007 |
+
<div style="font-size: 0.75rem; color: var(--text-muted); margin-top: 0.5rem;">
|
| 1008 |
+
SMA20: ${sma20 ? safeFormatCurrency(sma20) : '--'}<br>
|
| 1009 |
+
SMA50: ${sma50 ? safeFormatCurrency(sma50) : '--'}
|
| 1010 |
+
</div>
|
| 1011 |
+
</div>
|
| 1012 |
+
</div>
|
| 1013 |
+
`;
|
| 1014 |
}
|
| 1015 |
|
| 1016 |
/**
|
|
|
|
| 1074 |
const rsi = this.indicators.rsi;
|
| 1075 |
const macd = this.indicators.macd;
|
| 1076 |
const ema = this.indicators.ema;
|
| 1077 |
+
const bb = this.indicators.bollingerBands;
|
| 1078 |
+
const stochRsi = this.indicators.stochRsi;
|
| 1079 |
+
const atr = this.indicators.atr;
|
| 1080 |
+
const sma20 = this.indicators.sma20;
|
| 1081 |
+
const sma50 = this.indicators.sma50;
|
| 1082 |
|
| 1083 |
+
// Determine trend - use multiple indicators
|
| 1084 |
let trend = 'neutral';
|
| 1085 |
let trendDescription = 'Market is consolidating';
|
| 1086 |
+
let trendSignals = { bullish: 0, bearish: 0 };
|
| 1087 |
|
| 1088 |
+
// EMA trend
|
| 1089 |
if (latestCandle.close > ema) {
|
| 1090 |
+
trendSignals.bullish++;
|
|
|
|
| 1091 |
} else if (latestCandle.close < ema) {
|
| 1092 |
+
trendSignals.bearish++;
|
| 1093 |
+
}
|
| 1094 |
+
|
| 1095 |
+
// SMA crossover trend
|
| 1096 |
+
if (sma20 && sma50) {
|
| 1097 |
+
if (sma20 > sma50) {
|
| 1098 |
+
trendSignals.bullish++;
|
| 1099 |
+
} else {
|
| 1100 |
+
trendSignals.bearish++;
|
| 1101 |
+
}
|
| 1102 |
+
}
|
| 1103 |
+
|
| 1104 |
+
// Bollinger Bands position
|
| 1105 |
+
if (bb) {
|
| 1106 |
+
if (latestCandle.close > bb.middle) {
|
| 1107 |
+
trendSignals.bullish++;
|
| 1108 |
+
} else {
|
| 1109 |
+
trendSignals.bearish++;
|
| 1110 |
+
}
|
| 1111 |
+
}
|
| 1112 |
+
|
| 1113 |
+
if (trendSignals.bullish > trendSignals.bearish) {
|
| 1114 |
+
trend = 'bullish';
|
| 1115 |
+
trendDescription = `Uptrend detected: Price above EMA${sma20 > sma50 ? ', SMA20 > SMA50' : ''}${bb && latestCandle.close > bb.middle ? ', Above BB middle' : ''}`;
|
| 1116 |
+
} else if (trendSignals.bearish > trendSignals.bullish) {
|
| 1117 |
trend = 'bearish';
|
| 1118 |
+
trendDescription = `Downtrend detected: Price below EMA${sma20 < sma50 ? ', SMA20 < SMA50' : ''}${bb && latestCandle.close < bb.middle ? ', Below BB middle' : ''}`;
|
| 1119 |
}
|
| 1120 |
|
| 1121 |
// Generate indicator analysis
|
|
|
|
| 1159 |
interpretation: emaStatus === 'bullish' ? 'Price above EMA' : 'Price below EMA'
|
| 1160 |
});
|
| 1161 |
}
|
| 1162 |
+
|
| 1163 |
+
// Add Bollinger Bands analysis
|
| 1164 |
+
if (bb) {
|
| 1165 |
+
let bbStatus, bbInterpretation;
|
| 1166 |
+
const bbPosition = (latestCandle.close - bb.lower) / (bb.upper - bb.lower);
|
| 1167 |
+
|
| 1168 |
+
if (latestCandle.close >= bb.upper) {
|
| 1169 |
+
bbStatus = 'overbought';
|
| 1170 |
+
bbInterpretation = 'At upper band - possible reversal';
|
| 1171 |
+
} else if (latestCandle.close <= bb.lower) {
|
| 1172 |
+
bbStatus = 'oversold';
|
| 1173 |
+
bbInterpretation = 'At lower band - possible bounce';
|
| 1174 |
+
} else if (bbPosition > 0.7) {
|
| 1175 |
+
bbStatus = 'bearish';
|
| 1176 |
+
bbInterpretation = 'Near upper band - caution';
|
| 1177 |
+
} else if (bbPosition < 0.3) {
|
| 1178 |
+
bbStatus = 'bullish';
|
| 1179 |
+
bbInterpretation = 'Near lower band - potential buy';
|
| 1180 |
+
} else {
|
| 1181 |
+
bbStatus = 'neutral';
|
| 1182 |
+
bbInterpretation = 'Within bands - no signal';
|
| 1183 |
+
}
|
| 1184 |
+
|
| 1185 |
+
indicators.push({
|
| 1186 |
+
name: 'Bollinger Bands',
|
| 1187 |
+
value: `${(bbPosition * 100).toFixed(0)}%`,
|
| 1188 |
+
status: bbStatus,
|
| 1189 |
+
interpretation: bbInterpretation
|
| 1190 |
+
});
|
| 1191 |
+
}
|
| 1192 |
+
|
| 1193 |
+
// Add Stochastic RSI
|
| 1194 |
+
if (stochRsi !== null) {
|
| 1195 |
+
let stochStatus, stochInterpretation;
|
| 1196 |
+
if (stochRsi > 80) {
|
| 1197 |
+
stochStatus = 'overbought';
|
| 1198 |
+
stochInterpretation = 'Extreme overbought';
|
| 1199 |
+
} else if (stochRsi < 20) {
|
| 1200 |
+
stochStatus = 'oversold';
|
| 1201 |
+
stochInterpretation = 'Extreme oversold';
|
| 1202 |
+
} else {
|
| 1203 |
+
stochStatus = 'neutral';
|
| 1204 |
+
stochInterpretation = 'Normal range';
|
| 1205 |
+
}
|
| 1206 |
+
|
| 1207 |
+
indicators.push({
|
| 1208 |
+
name: 'Stoch RSI',
|
| 1209 |
+
value: stochRsi.toFixed(1),
|
| 1210 |
+
status: stochStatus,
|
| 1211 |
+
interpretation: stochInterpretation
|
| 1212 |
+
});
|
| 1213 |
+
}
|
| 1214 |
+
|
| 1215 |
+
// Add SMA crossover
|
| 1216 |
+
if (sma20 && sma50) {
|
| 1217 |
+
const smaStatus = sma20 > sma50 ? 'bullish' : 'bearish';
|
| 1218 |
+
indicators.push({
|
| 1219 |
+
name: 'SMA Cross',
|
| 1220 |
+
value: sma20 > sma50 ? 'Golden' : 'Death',
|
| 1221 |
+
status: smaStatus,
|
| 1222 |
+
interpretation: sma20 > sma50 ? 'Bullish crossover' : 'Bearish crossover'
|
| 1223 |
+
});
|
| 1224 |
+
}
|
| 1225 |
+
|
| 1226 |
+
// Add ATR for volatility
|
| 1227 |
+
if (atr !== null) {
|
| 1228 |
+
const atrPercent = (atr / latestCandle.close) * 100;
|
| 1229 |
+
let atrStatus, atrInterpretation;
|
| 1230 |
+
|
| 1231 |
+
if (atrPercent > 5) {
|
| 1232 |
+
atrStatus = 'high';
|
| 1233 |
+
atrInterpretation = 'High volatility - increase stop loss';
|
| 1234 |
+
} else if (atrPercent < 1) {
|
| 1235 |
+
atrStatus = 'low';
|
| 1236 |
+
atrInterpretation = 'Low volatility - breakout expected';
|
| 1237 |
+
} else {
|
| 1238 |
+
atrStatus = 'neutral';
|
| 1239 |
+
atrInterpretation = 'Normal volatility';
|
| 1240 |
+
}
|
| 1241 |
+
|
| 1242 |
+
indicators.push({
|
| 1243 |
+
name: 'ATR (14)',
|
| 1244 |
+
value: `${atrPercent.toFixed(2)}%`,
|
| 1245 |
+
status: atrStatus,
|
| 1246 |
+
interpretation: atrInterpretation
|
| 1247 |
+
});
|
| 1248 |
+
}
|
| 1249 |
|
| 1250 |
+
// Generate signal - count all indicator signals
|
| 1251 |
let signal = 'hold';
|
| 1252 |
let recommendation = 'Wait for clearer signals';
|
| 1253 |
|
| 1254 |
const bullishSignals = indicators.filter(i => i.status === 'bullish' || i.status === 'oversold').length;
|
| 1255 |
const bearishSignals = indicators.filter(i => i.status === 'bearish' || i.status === 'overbought').length;
|
| 1256 |
+
const totalSignals = indicators.length;
|
| 1257 |
|
| 1258 |
+
if (bullishSignals >= totalSignals * 0.5 && bullishSignals > bearishSignals) {
|
| 1259 |
signal = 'buy';
|
| 1260 |
+
recommendation = `Strong buy signals detected (${bullishSignals}/${totalSignals} indicators bullish). Consider entering a long position with proper risk management. Use ATR for stop loss placement.`;
|
| 1261 |
+
} else if (bearishSignals >= totalSignals * 0.5 && bearishSignals > bullishSignals) {
|
| 1262 |
signal = 'sell';
|
| 1263 |
+
recommendation = `Strong sell signals detected (${bearishSignals}/${totalSignals} indicators bearish). Consider taking profits or shorting with proper risk management.`;
|
| 1264 |
+
} else {
|
| 1265 |
+
recommendation = `Mixed signals (${bullishSignals} bullish, ${bearishSignals} bearish). Wait for clearer direction or trade cautiously.`;
|
| 1266 |
}
|
| 1267 |
|
| 1268 |
+
// Calculate risk based on all indicators
|
| 1269 |
let riskScore = 50;
|
| 1270 |
let risk = 'medium';
|
| 1271 |
|
| 1272 |
+
// RSI extreme adds risk
|
| 1273 |
if (rsi !== null) {
|
| 1274 |
+
if (rsi > 80 || rsi < 20) riskScore += 15;
|
| 1275 |
+
else if (rsi > 70 || rsi < 30) riskScore += 10;
|
| 1276 |
+
}
|
| 1277 |
+
|
| 1278 |
+
// Stoch RSI extreme adds risk
|
| 1279 |
+
if (stochRsi !== null) {
|
| 1280 |
+
if (stochRsi > 90 || stochRsi < 10) riskScore += 15;
|
| 1281 |
+
else if (stochRsi > 80 || stochRsi < 20) riskScore += 10;
|
| 1282 |
+
}
|
| 1283 |
+
|
| 1284 |
+
// High volatility adds risk
|
| 1285 |
+
if (atr !== null) {
|
| 1286 |
+
const atrPercent = (atr / latestCandle.close) * 100;
|
| 1287 |
+
if (atrPercent > 5) riskScore += 15;
|
| 1288 |
+
else if (atrPercent > 3) riskScore += 10;
|
| 1289 |
+
}
|
| 1290 |
+
|
| 1291 |
+
// At Bollinger Band extremes adds risk
|
| 1292 |
+
if (bb) {
|
| 1293 |
+
if (latestCandle.close >= bb.upper || latestCandle.close <= bb.lower) {
|
| 1294 |
+
riskScore += 10;
|
| 1295 |
+
}
|
| 1296 |
}
|
| 1297 |
|
| 1298 |
+
// Aligned signals reduce risk
|
| 1299 |
if (trend === 'bullish' && signal === 'buy') {
|
| 1300 |
+
riskScore -= 15;
|
| 1301 |
} else if (trend === 'bearish' && signal === 'sell') {
|
| 1302 |
+
riskScore -= 15;
|
| 1303 |
}
|
| 1304 |
|
| 1305 |
riskScore = Math.max(10, Math.min(90, riskScore));
|
| 1306 |
|
| 1307 |
+
if (riskScore < 35) risk = 'low';
|
| 1308 |
+
else if (riskScore > 65) risk = 'high';
|
| 1309 |
|
| 1310 |
return {
|
| 1311 |
trend,
|
|
@@ -944,6 +944,7 @@ class TechnicalAnalysisPage {
|
|
| 944 |
} catch (error) {
|
| 945 |
logger.warn('TechnicalAnalysis', 'Failed to draw support/resistance:', error);
|
| 946 |
}
|
|
|
|
| 947 |
|
| 948 |
renderAnalysis() {
|
| 949 |
if (!this.analysisData) return;
|
|
|
|
| 944 |
} catch (error) {
|
| 945 |
logger.warn('TechnicalAnalysis', 'Failed to draw support/resistance:', error);
|
| 946 |
}
|
| 947 |
+
}
|
| 948 |
|
| 949 |
renderAnalysis() {
|
| 950 |
if (!this.analysisData) return;
|
|
@@ -235,3 +235,286 @@
|
|
| 235 |
[data-theme="dark"] .status-text {
|
| 236 |
color: white;
|
| 237 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
[data-theme="dark"] .status-text {
|
| 236 |
color: white;
|
| 237 |
}
|
| 238 |
+
|
| 239 |
+
/* ============================================================================
|
| 240 |
+
Services Menu (Dollar Sign Button)
|
| 241 |
+
============================================================================ */
|
| 242 |
+
|
| 243 |
+
.services-menu-container {
|
| 244 |
+
position: relative;
|
| 245 |
+
padding: 0.75rem 1rem;
|
| 246 |
+
border-top: 1px solid rgba(20, 184, 166, 0.1);
|
| 247 |
+
margin-top: auto;
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
.services-toggle-btn {
|
| 251 |
+
width: 100%;
|
| 252 |
+
display: flex;
|
| 253 |
+
align-items: center;
|
| 254 |
+
justify-content: center;
|
| 255 |
+
gap: 0.5rem;
|
| 256 |
+
padding: 0.875rem 1rem;
|
| 257 |
+
background: linear-gradient(135deg, #14b8a6, #06b6d4);
|
| 258 |
+
border: none;
|
| 259 |
+
border-radius: 12px;
|
| 260 |
+
color: white;
|
| 261 |
+
font-weight: 600;
|
| 262 |
+
cursor: pointer;
|
| 263 |
+
transition: all 0.3s ease;
|
| 264 |
+
box-shadow: 0 4px 15px rgba(20, 184, 166, 0.3);
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
.services-toggle-btn:hover {
|
| 268 |
+
transform: translateY(-2px);
|
| 269 |
+
box-shadow: 0 6px 20px rgba(20, 184, 166, 0.4);
|
| 270 |
+
background: linear-gradient(135deg, #0d9488, #0891b2);
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
.services-toggle-btn.active {
|
| 274 |
+
background: linear-gradient(135deg, #0d9488, #0891b2);
|
| 275 |
+
box-shadow: 0 4px 20px rgba(20, 184, 166, 0.5);
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
.services-toggle-btn svg {
|
| 279 |
+
animation: dollarPulse 2s ease-in-out infinite;
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
@keyframes dollarPulse {
|
| 283 |
+
0%, 100% { transform: scale(1); }
|
| 284 |
+
50% { transform: scale(1.1); }
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
/* Services Popup */
|
| 288 |
+
.services-popup {
|
| 289 |
+
position: absolute;
|
| 290 |
+
bottom: 100%;
|
| 291 |
+
left: 0;
|
| 292 |
+
right: 0;
|
| 293 |
+
margin-bottom: 0.5rem;
|
| 294 |
+
background: white;
|
| 295 |
+
border-radius: 16px;
|
| 296 |
+
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(20, 184, 166, 0.1);
|
| 297 |
+
z-index: 1000;
|
| 298 |
+
overflow: hidden;
|
| 299 |
+
animation: slideUp 0.3s ease;
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
@keyframes slideUp {
|
| 303 |
+
from {
|
| 304 |
+
opacity: 0;
|
| 305 |
+
transform: translateY(10px);
|
| 306 |
+
}
|
| 307 |
+
to {
|
| 308 |
+
opacity: 1;
|
| 309 |
+
transform: translateY(0);
|
| 310 |
+
}
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
.services-popup-header {
|
| 314 |
+
display: flex;
|
| 315 |
+
align-items: center;
|
| 316 |
+
justify-content: space-between;
|
| 317 |
+
padding: 1rem 1.25rem;
|
| 318 |
+
background: linear-gradient(135deg, rgba(20, 184, 166, 0.1), rgba(6, 182, 212, 0.05));
|
| 319 |
+
border-bottom: 1px solid rgba(20, 184, 166, 0.1);
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
.services-popup-header h4 {
|
| 323 |
+
display: flex;
|
| 324 |
+
align-items: center;
|
| 325 |
+
gap: 0.5rem;
|
| 326 |
+
margin: 0;
|
| 327 |
+
font-size: 0.95rem;
|
| 328 |
+
font-weight: 700;
|
| 329 |
+
color: var(--text-primary, #0f172a);
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
.services-popup-header svg {
|
| 333 |
+
color: #14b8a6;
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
.services-close-btn {
|
| 337 |
+
width: 28px;
|
| 338 |
+
height: 28px;
|
| 339 |
+
display: flex;
|
| 340 |
+
align-items: center;
|
| 341 |
+
justify-content: center;
|
| 342 |
+
background: rgba(0, 0, 0, 0.05);
|
| 343 |
+
border: none;
|
| 344 |
+
border-radius: 8px;
|
| 345 |
+
font-size: 1.25rem;
|
| 346 |
+
color: var(--text-secondary, #64748b);
|
| 347 |
+
cursor: pointer;
|
| 348 |
+
transition: all 0.2s ease;
|
| 349 |
+
}
|
| 350 |
+
|
| 351 |
+
.services-close-btn:hover {
|
| 352 |
+
background: rgba(239, 68, 68, 0.1);
|
| 353 |
+
color: #ef4444;
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
.services-popup-body {
|
| 357 |
+
padding: 1rem;
|
| 358 |
+
max-height: 400px;
|
| 359 |
+
overflow-y: auto;
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
.services-grid {
|
| 363 |
+
display: grid;
|
| 364 |
+
grid-template-columns: 1fr;
|
| 365 |
+
gap: 0.5rem;
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
.service-card {
|
| 369 |
+
display: flex;
|
| 370 |
+
align-items: center;
|
| 371 |
+
gap: 0.75rem;
|
| 372 |
+
padding: 0.75rem 1rem;
|
| 373 |
+
background: rgba(248, 250, 252, 0.8);
|
| 374 |
+
border: 1px solid rgba(20, 184, 166, 0.1);
|
| 375 |
+
border-radius: 10px;
|
| 376 |
+
text-decoration: none;
|
| 377 |
+
transition: all 0.2s ease;
|
| 378 |
+
position: relative;
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
.service-card:hover {
|
| 382 |
+
background: linear-gradient(135deg, rgba(20, 184, 166, 0.1), rgba(6, 182, 212, 0.05));
|
| 383 |
+
border-color: rgba(20, 184, 166, 0.3);
|
| 384 |
+
transform: translateX(4px);
|
| 385 |
+
box-shadow: 0 2px 8px rgba(20, 184, 166, 0.15);
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
.service-card.featured {
|
| 389 |
+
background: linear-gradient(135deg, rgba(20, 184, 166, 0.15), rgba(6, 182, 212, 0.1));
|
| 390 |
+
border-color: rgba(20, 184, 166, 0.3);
|
| 391 |
+
}
|
| 392 |
+
|
| 393 |
+
.service-card.featured:hover {
|
| 394 |
+
background: linear-gradient(135deg, rgba(20, 184, 166, 0.2), rgba(6, 182, 212, 0.15));
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
.service-icon {
|
| 398 |
+
font-size: 1.5rem;
|
| 399 |
+
width: 36px;
|
| 400 |
+
height: 36px;
|
| 401 |
+
display: flex;
|
| 402 |
+
align-items: center;
|
| 403 |
+
justify-content: center;
|
| 404 |
+
background: white;
|
| 405 |
+
border-radius: 8px;
|
| 406 |
+
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
.service-info {
|
| 410 |
+
flex: 1;
|
| 411 |
+
display: flex;
|
| 412 |
+
flex-direction: column;
|
| 413 |
+
gap: 2px;
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
.service-name {
|
| 417 |
+
font-size: 0.875rem;
|
| 418 |
+
font-weight: 600;
|
| 419 |
+
color: var(--text-primary, #0f172a);
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
.service-desc {
|
| 423 |
+
font-size: 0.75rem;
|
| 424 |
+
color: var(--text-secondary, #64748b);
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
.service-badge {
|
| 428 |
+
font-size: 0.625rem;
|
| 429 |
+
font-weight: 700;
|
| 430 |
+
padding: 3px 8px;
|
| 431 |
+
background: linear-gradient(135deg, #14b8a6, #06b6d4);
|
| 432 |
+
color: white;
|
| 433 |
+
border-radius: 6px;
|
| 434 |
+
text-transform: uppercase;
|
| 435 |
+
letter-spacing: 0.5px;
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
+
.services-popup-footer {
|
| 439 |
+
padding: 0.75rem 1rem;
|
| 440 |
+
background: rgba(248, 250, 252, 0.8);
|
| 441 |
+
border-top: 1px solid rgba(20, 184, 166, 0.1);
|
| 442 |
+
}
|
| 443 |
+
|
| 444 |
+
.view-all-btn {
|
| 445 |
+
display: flex;
|
| 446 |
+
align-items: center;
|
| 447 |
+
justify-content: center;
|
| 448 |
+
gap: 0.5rem;
|
| 449 |
+
width: 100%;
|
| 450 |
+
padding: 0.625rem;
|
| 451 |
+
background: transparent;
|
| 452 |
+
border: 2px solid rgba(20, 184, 166, 0.3);
|
| 453 |
+
border-radius: 8px;
|
| 454 |
+
font-size: 0.875rem;
|
| 455 |
+
font-weight: 600;
|
| 456 |
+
color: #14b8a6;
|
| 457 |
+
text-decoration: none;
|
| 458 |
+
transition: all 0.2s ease;
|
| 459 |
+
}
|
| 460 |
+
|
| 461 |
+
.view-all-btn:hover {
|
| 462 |
+
background: rgba(20, 184, 166, 0.1);
|
| 463 |
+
border-color: #14b8a6;
|
| 464 |
+
}
|
| 465 |
+
|
| 466 |
+
/* Dark Mode for Services Menu */
|
| 467 |
+
[data-theme="dark"] .services-popup {
|
| 468 |
+
background: #1e293b;
|
| 469 |
+
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(20, 184, 166, 0.2);
|
| 470 |
+
}
|
| 471 |
+
|
| 472 |
+
[data-theme="dark"] .services-popup-header {
|
| 473 |
+
background: linear-gradient(135deg, rgba(20, 184, 166, 0.15), rgba(6, 182, 212, 0.1));
|
| 474 |
+
border-color: rgba(20, 184, 166, 0.2);
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
+
[data-theme="dark"] .services-popup-header h4 {
|
| 478 |
+
color: #f1f5f9;
|
| 479 |
+
}
|
| 480 |
+
|
| 481 |
+
[data-theme="dark"] .services-close-btn {
|
| 482 |
+
background: rgba(255, 255, 255, 0.1);
|
| 483 |
+
color: #94a3b8;
|
| 484 |
+
}
|
| 485 |
+
|
| 486 |
+
[data-theme="dark"] .service-card {
|
| 487 |
+
background: rgba(30, 41, 59, 0.8);
|
| 488 |
+
border-color: rgba(20, 184, 166, 0.2);
|
| 489 |
+
}
|
| 490 |
+
|
| 491 |
+
[data-theme="dark"] .service-card:hover {
|
| 492 |
+
background: linear-gradient(135deg, rgba(20, 184, 166, 0.2), rgba(6, 182, 212, 0.1));
|
| 493 |
+
}
|
| 494 |
+
|
| 495 |
+
[data-theme="dark"] .service-icon {
|
| 496 |
+
background: rgba(15, 23, 42, 0.8);
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
[data-theme="dark"] .service-name {
|
| 500 |
+
color: #f1f5f9;
|
| 501 |
+
}
|
| 502 |
+
|
| 503 |
+
[data-theme="dark"] .service-desc {
|
| 504 |
+
color: #94a3b8;
|
| 505 |
+
}
|
| 506 |
+
|
| 507 |
+
[data-theme="dark"] .services-popup-footer {
|
| 508 |
+
background: rgba(15, 23, 42, 0.8);
|
| 509 |
+
border-color: rgba(20, 184, 166, 0.2);
|
| 510 |
+
}
|
| 511 |
+
|
| 512 |
+
[data-theme="dark"] .view-all-btn {
|
| 513 |
+
color: #2dd4bf;
|
| 514 |
+
border-color: rgba(45, 212, 191, 0.3);
|
| 515 |
+
}
|
| 516 |
+
|
| 517 |
+
[data-theme="dark"] .view-all-btn:hover {
|
| 518 |
+
background: rgba(45, 212, 191, 0.1);
|
| 519 |
+
border-color: #2dd4bf;
|
| 520 |
+
}
|
|
@@ -123,6 +123,20 @@
|
|
| 123 |
</a>
|
| 124 |
</li>
|
| 125 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
<!-- Trading Assistant -->
|
| 127 |
<li class="nav-item">
|
| 128 |
<a href="/static/pages/trading-assistant/index.html" class="nav-link" data-page="trading-assistant">
|
|
@@ -212,6 +226,113 @@
|
|
| 212 |
</ul>
|
| 213 |
</nav>
|
| 214 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
<!-- Sidebar Footer -->
|
| 216 |
<div class="sidebar-footer">
|
| 217 |
<div class="sidebar-status">
|
|
@@ -220,3 +341,35 @@
|
|
| 220 |
</div>
|
| 221 |
</div>
|
| 222 |
</aside>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
</a>
|
| 124 |
</li>
|
| 125 |
|
| 126 |
+
<!-- Indicator Services -->
|
| 127 |
+
<li class="nav-item">
|
| 128 |
+
<a href="/static/pages/services/index.html" class="nav-link" data-page="services">
|
| 129 |
+
<span class="nav-icon">
|
| 130 |
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 131 |
+
<line x1="12" y1="1" x2="12" y2="23"></line>
|
| 132 |
+
<path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path>
|
| 133 |
+
</svg>
|
| 134 |
+
</span>
|
| 135 |
+
<span class="nav-label">Services</span>
|
| 136 |
+
<span class="nav-badge" style="background: linear-gradient(135deg, #14b8a6, #06b6d4); color: white; font-size: 8px; padding: 2px 6px; border-radius: 8px; margin-left: auto;">NEW</span>
|
| 137 |
+
</a>
|
| 138 |
+
</li>
|
| 139 |
+
|
| 140 |
<!-- Trading Assistant -->
|
| 141 |
<li class="nav-item">
|
| 142 |
<a href="/static/pages/trading-assistant/index.html" class="nav-link" data-page="trading-assistant">
|
|
|
|
| 226 |
</ul>
|
| 227 |
</nav>
|
| 228 |
|
| 229 |
+
<!-- Services Menu (Dollar Sign) -->
|
| 230 |
+
<div class="services-menu-container">
|
| 231 |
+
<button id="services-toggle" class="services-toggle-btn" title="Premium Services">
|
| 232 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
| 233 |
+
<line x1="12" y1="1" x2="12" y2="23"></line>
|
| 234 |
+
<path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path>
|
| 235 |
+
</svg>
|
| 236 |
+
</button>
|
| 237 |
+
|
| 238 |
+
<!-- Services Popup Menu -->
|
| 239 |
+
<div id="services-popup" class="services-popup" style="display: none;">
|
| 240 |
+
<div class="services-popup-header">
|
| 241 |
+
<h4>
|
| 242 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 243 |
+
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline>
|
| 244 |
+
</svg>
|
| 245 |
+
Indicator Services
|
| 246 |
+
</h4>
|
| 247 |
+
<button class="services-close-btn" id="services-close">Γ</button>
|
| 248 |
+
</div>
|
| 249 |
+
<div class="services-popup-body">
|
| 250 |
+
<div class="services-grid">
|
| 251 |
+
<!-- Bollinger Bands -->
|
| 252 |
+
<a href="/static/pages/services/index.html?service=bollinger_bands" class="service-card" data-service="bollinger_bands">
|
| 253 |
+
<span class="service-icon">π</span>
|
| 254 |
+
<div class="service-info">
|
| 255 |
+
<span class="service-name">Bollinger Bands</span>
|
| 256 |
+
<span class="service-desc">Volatility bands analysis</span>
|
| 257 |
+
</div>
|
| 258 |
+
</a>
|
| 259 |
+
|
| 260 |
+
<!-- Stochastic RSI -->
|
| 261 |
+
<a href="/static/pages/services/index.html?service=stoch_rsi" class="service-card" data-service="stoch_rsi">
|
| 262 |
+
<span class="service-icon">π</span>
|
| 263 |
+
<div class="service-info">
|
| 264 |
+
<span class="service-name">Stochastic RSI</span>
|
| 265 |
+
<span class="service-desc">Momentum oscillator</span>
|
| 266 |
+
</div>
|
| 267 |
+
</a>
|
| 268 |
+
|
| 269 |
+
<!-- ATR -->
|
| 270 |
+
<a href="/static/pages/services/index.html?service=atr" class="service-card" data-service="atr">
|
| 271 |
+
<span class="service-icon">π</span>
|
| 272 |
+
<div class="service-info">
|
| 273 |
+
<span class="service-name">ATR</span>
|
| 274 |
+
<span class="service-desc">Average True Range</span>
|
| 275 |
+
</div>
|
| 276 |
+
</a>
|
| 277 |
+
|
| 278 |
+
<!-- SMA -->
|
| 279 |
+
<a href="/static/pages/services/index.html?service=sma" class="service-card" data-service="sma">
|
| 280 |
+
<span class="service-icon">γ°οΈ</span>
|
| 281 |
+
<div class="service-info">
|
| 282 |
+
<span class="service-name">SMA</span>
|
| 283 |
+
<span class="service-desc">Simple Moving Average</span>
|
| 284 |
+
</div>
|
| 285 |
+
</a>
|
| 286 |
+
|
| 287 |
+
<!-- EMA -->
|
| 288 |
+
<a href="/static/pages/services/index.html?service=ema" class="service-card" data-service="ema">
|
| 289 |
+
<span class="service-icon">π</span>
|
| 290 |
+
<div class="service-info">
|
| 291 |
+
<span class="service-name">EMA</span>
|
| 292 |
+
<span class="service-desc">Exponential MA</span>
|
| 293 |
+
</div>
|
| 294 |
+
</a>
|
| 295 |
+
|
| 296 |
+
<!-- MACD -->
|
| 297 |
+
<a href="/static/pages/services/index.html?service=macd" class="service-card" data-service="macd">
|
| 298 |
+
<span class="service-icon">π</span>
|
| 299 |
+
<div class="service-info">
|
| 300 |
+
<span class="service-name">MACD</span>
|
| 301 |
+
<span class="service-desc">Trend momentum</span>
|
| 302 |
+
</div>
|
| 303 |
+
</a>
|
| 304 |
+
|
| 305 |
+
<!-- RSI -->
|
| 306 |
+
<a href="/static/pages/services/index.html?service=rsi" class="service-card" data-service="rsi">
|
| 307 |
+
<span class="service-icon">πͺ</span>
|
| 308 |
+
<div class="service-info">
|
| 309 |
+
<span class="service-name">RSI</span>
|
| 310 |
+
<span class="service-desc">Relative Strength</span>
|
| 311 |
+
</div>
|
| 312 |
+
</a>
|
| 313 |
+
|
| 314 |
+
<!-- Comprehensive -->
|
| 315 |
+
<a href="/static/pages/services/index.html?service=comprehensive" class="service-card featured" data-service="comprehensive">
|
| 316 |
+
<span class="service-icon">π―</span>
|
| 317 |
+
<div class="service-info">
|
| 318 |
+
<span class="service-name">Full Analysis</span>
|
| 319 |
+
<span class="service-desc">All indicators combined</span>
|
| 320 |
+
</div>
|
| 321 |
+
<span class="service-badge">PRO</span>
|
| 322 |
+
</a>
|
| 323 |
+
</div>
|
| 324 |
+
</div>
|
| 325 |
+
<div class="services-popup-footer">
|
| 326 |
+
<a href="/static/pages/services/index.html" class="view-all-btn">
|
| 327 |
+
View All Services
|
| 328 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 329 |
+
<polyline points="9 18 15 12 9 6"></polyline>
|
| 330 |
+
</svg>
|
| 331 |
+
</a>
|
| 332 |
+
</div>
|
| 333 |
+
</div>
|
| 334 |
+
</div>
|
| 335 |
+
|
| 336 |
<!-- Sidebar Footer -->
|
| 337 |
<div class="sidebar-footer">
|
| 338 |
<div class="sidebar-status">
|
|
|
|
| 341 |
</div>
|
| 342 |
</div>
|
| 343 |
</aside>
|
| 344 |
+
|
| 345 |
+
<script>
|
| 346 |
+
// Services Menu Toggle
|
| 347 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 348 |
+
const toggleBtn = document.getElementById('services-toggle');
|
| 349 |
+
const popup = document.getElementById('services-popup');
|
| 350 |
+
const closeBtn = document.getElementById('services-close');
|
| 351 |
+
|
| 352 |
+
if (toggleBtn && popup) {
|
| 353 |
+
toggleBtn.addEventListener('click', function(e) {
|
| 354 |
+
e.stopPropagation();
|
| 355 |
+
popup.style.display = popup.style.display === 'none' ? 'block' : 'none';
|
| 356 |
+
toggleBtn.classList.toggle('active');
|
| 357 |
+
});
|
| 358 |
+
|
| 359 |
+
if (closeBtn) {
|
| 360 |
+
closeBtn.addEventListener('click', function() {
|
| 361 |
+
popup.style.display = 'none';
|
| 362 |
+
toggleBtn.classList.remove('active');
|
| 363 |
+
});
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
// Close on outside click
|
| 367 |
+
document.addEventListener('click', function(e) {
|
| 368 |
+
if (!popup.contains(e.target) && !toggleBtn.contains(e.target)) {
|
| 369 |
+
popup.style.display = 'none';
|
| 370 |
+
toggleBtn.classList.remove('active');
|
| 371 |
+
}
|
| 372 |
+
});
|
| 373 |
+
}
|
| 374 |
+
});
|
| 375 |
+
</script>
|