nimazasinich Cursor Agent bxsfy712 commited on
Commit
bb4c54e
·
1 Parent(s): 3fea549

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 ADDED
@@ -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))
backend/services/coingecko_client.py CHANGED
@@ -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
  })
hf_unified_server.py CHANGED
@@ -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": f"https://assets.coingecko.com/coins/images/1/small/{coin.get('symbol', '').lower()}.png",
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", "Binance Coin", 585, 88_000_000_000),
1362
- ("SOL", "Solana", 145, 65_000_000_000),
1363
- ("XRP", "Ripple", 0.62, 34_000_000_000),
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": symbol.lower(),
1375
  "rank": i + 1,
1376
  "market_cap_rank": i + 1,
1377
  "symbol": symbol,
1378
  "name": name,
1379
- "image": f"https://assets.coingecko.com/coins/images/1/small/{symbol.lower()}.png",
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
 
static/pages/services/index.html ADDED
@@ -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>
static/pages/services/services.css ADDED
@@ -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
+ }
static/pages/services/services.js ADDED
@@ -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;
static/pages/technical-analysis/technical-analysis-professional.js CHANGED
@@ -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
- trend = 'bullish';
864
- trendDescription = 'Price is above EMA - Bullish trend';
865
  } else if (latestCandle.close < ema) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
866
  trend = 'bearish';
867
- trendDescription = 'Price is below EMA - Bearish trend';
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 > bearishSignals && bullishSignals >= 2) {
920
  signal = 'buy';
921
- recommendation = 'Strong buy signals detected. Consider entering a long position with proper risk management.';
922
- } else if (bearishSignals > bullishSignals && bearishSignals >= 2) {
923
  signal = 'sell';
924
- recommendation = 'Strong sell signals detected. Consider taking profits or shorting with proper risk management.';
 
 
925
  }
926
 
927
- // Calculate risk
928
  let riskScore = 50;
929
  let risk = 'medium';
930
 
 
931
  if (rsi !== null) {
932
- if (rsi > 70 || rsi < 30) riskScore += 20;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
933
  }
934
 
 
935
  if (trend === 'bullish' && signal === 'buy') {
936
- riskScore -= 10;
937
  } else if (trend === 'bearish' && signal === 'sell') {
938
- riskScore -= 10;
939
  }
940
 
941
  riskScore = Math.max(10, Math.min(90, riskScore));
942
 
943
- if (riskScore < 40) risk = 'low';
944
- else if (riskScore > 60) risk = 'high';
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,
static/pages/technical-analysis/technical-analysis.js CHANGED
@@ -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;
static/shared/css/sidebar-enhanced.css CHANGED
@@ -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
+ }
static/shared/layouts/sidebar.html CHANGED
@@ -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>