Really-amin commited on
Commit
89dbf23
·
verified ·
1 Parent(s): 7ef7962

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +488 -421
app.py CHANGED
@@ -1,20 +1,22 @@
1
- from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Query
2
- from fastapi.responses import FileResponse, JSONResponse
 
 
 
 
 
 
3
  from fastapi.middleware.cors import CORSMiddleware
4
- from pathlib import Path
5
- from datetime import datetime, timedelta, timezone
6
- from typing import Any, Dict, List, Optional
7
-
8
  import asyncio
 
 
9
  import random
10
- import ccxt
11
 
12
- # -------------------------------------------------
13
- # اپلیکیشن FastAPI
14
- # -------------------------------------------------
15
- app = FastAPI(title="Crypto Data Source API")
16
 
17
- # CORS آزاد (در صورت نیاز بعداً محدود کن)
18
  app.add_middleware(
19
  CORSMiddleware,
20
  allow_origins=["*"],
@@ -23,431 +25,496 @@ app.add_middleware(
23
  allow_headers=["*"],
24
  )
25
 
26
- # روت پروژه = همون جایی که app.py هست
27
- BASE_DIR = Path(__file__).resolve().parent
28
- INDEX_FILE = BASE_DIR / "INDEX.html"
29
- START_TIME = datetime.now(timezone.utc)
30
-
31
- # داده‌های نمونه برای داشبورد
32
- BASE_PROVIDERS: List[Dict[str, Any]] = [
33
- {"name": "Binance Spot", "category": "market_data", "has_key": False, "priority": 5},
34
- {"name": "Binance Futures", "category": "market_data", "has_key": False, "priority": 4},
35
- {"name": "CoinGecko", "category": "market_data", "has_key": False, "priority": 4},
36
- {"name": "CoinPaprika", "category": "market_data", "has_key": False, "priority": 3},
37
- {"name": "Etherscan", "category": "blockchain", "has_key": True, "priority": 4},
38
- {"name": "BscScan", "category": "blockchain", "has_key": True, "priority": 3},
39
- {"name": "TronScan", "category": "blockchain", "has_key": False, "priority": 3},
40
- {"name": "CryptoPanic", "category": "news", "has_key": False, "priority": 2},
41
- ]
42
-
43
- CUSTOM_APIS: List[Dict[str, Any]] = []
44
-
45
- API_KEYS: List[Dict[str, str]] = [
46
- {"provider": "Binance", "key_masked": "BINANCE-****-****-1234"},
47
- {"provider": "Etherscan", "key_masked": "ETHERSCAN-****-****-ABCD"},
48
- {"provider": "BscScan", "key_masked": "BSCSCAN-****-****-5678"},
49
- ]
50
-
51
-
52
- def _utc_now() -> datetime:
53
- return datetime.now(timezone.utc)
54
-
55
-
56
- def _generate_providers_snapshot() -> List[Dict[str, Any]]:
57
- """ساخت دیتای فیک برای provider ها (برای داشبورد)."""
58
- now = _utc_now()
59
- providers: List[Dict[str, Any]] = []
60
-
61
- all_providers = BASE_PROVIDERS + CUSTOM_APIS
62
-
63
- for base in all_providers:
64
- rt = random.randint(80, 900) # response time
65
- minutes_ago = random.uniform(0, 30)
66
- last_fetch_dt = now - timedelta(minutes=minutes_ago)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
 
68
- if rt < 300:
69
- status = "online"
70
- elif rt < 700:
71
- status = "degraded"
72
- rt += random.randint(50, 200)
73
- else:
74
- status = "offline"
75
 
76
- providers.append(
77
- {
78
- "name": base["name"],
79
- "category": base["category"],
80
- "status": status,
81
- "response_time_ms": rt,
82
- "last_fetch": last_fetch_dt.isoformat(),
83
- "has_key": bool(base.get("has_key", False)),
84
- "priority": int(base.get("priority", 3)),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  }
86
- )
87
-
88
- return providers
89
-
90
-
91
- # -------------------------------------------------
92
- # APIهای ccxt پایه (health / markets / ticker)
93
- # -------------------------------------------------
94
- @app.get("/api/health")
95
- async def health_check():
96
  return {
97
- "status": "online",
98
- "version": "1.0.0",
99
- "timestamp": _utc_now().isoformat(),
100
- "ccxt_version": ccxt.__version__,
 
 
 
101
  }
102
 
103
-
104
- @app.get("/api/markets")
105
- async def get_markets():
 
 
 
 
 
 
 
106
  try:
107
- exchange = ccxt.binance()
108
- markets = exchange.load_markets()
109
- return {
110
- "success": True,
111
- "total_markets": len(markets),
112
- "markets": list(markets.keys())[:50],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  except Exception as e:
115
- return {"success": False, "error": str(e)}
116
-
117
-
118
- @app.get("/api/ticker/{symbol}")
119
- async def get_ticker(symbol: str):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  try:
121
- exchange = ccxt.binance()
122
- ticker = exchange.fetch_ticker(symbol)
123
- return {"success": True, "data": ticker}
 
 
 
 
 
 
 
 
 
 
 
 
124
  except Exception as e:
125
- return {"success": False, "error": str(e)}
126
-
127
-
128
- # -------------------------------------------------
129
- # /api/status – کارت‌های بالای داشبورد
130
- # -------------------------------------------------
131
- @app.get("/api/status")
132
- async def api_status():
133
- providers = _generate_providers_snapshot()
134
- total = len(providers)
135
- online = sum(1 for p in providers if p["status"] == "online")
136
- offline = sum(1 for p in providers if p["status"] == "offline")
137
- degraded = sum(1 for p in providers if p["status"] == "degraded")
138
- avg_response = (
139
- sum(p["response_time_ms"] for p in providers) / total if total else 0
140
- )
141
-
142
- total_requests_hour = random.randint(200, 1200)
143
- total_failures_hour = random.randint(0, max(20, degraded * 2 + offline * 5))
144
-
145
- system_health = (
146
- "healthy"
147
- if offline == 0 and degraded <= max(1, total // 4)
148
- else "degraded"
149
- )
150
-
151
- now = _utc_now()
152
- uptime_seconds = (now - START_TIME).total_seconds()
153
-
154
  return {
155
- "status": "online",
156
- "service": "crypto-data-source",
157
- "version": "1.0.0",
158
- "timestamp": now.isoformat(),
159
- "uptime_seconds": uptime_seconds,
160
- "total_providers": total,
161
- "online": online,
162
- "offline": offline,
163
- "degraded": degraded,
164
- "avg_response_time_ms": int(avg_response),
165
- "system_health": system_health,
166
- "total_requests_hour": total_requests_hour,
167
- "total_failures_hour": total_failures_hour,
168
  }
169
 
170
-
171
- # -------------------------------------------------
172
- # /api/providers – جدول providerها
173
- # -------------------------------------------------
174
- @app.get("/api/providers")
175
- async def api_providers():
176
- return _generate_providers_snapshot()
177
-
178
-
179
- # -------------------------------------------------
180
- # /api/categories – کارت‌های وسط
181
- # -------------------------------------------------
182
- @app.get("/api/categories")
183
- async def api_categories():
184
- providers = _generate_providers_snapshot()
185
- by_cat: Dict[str, List[Dict[str, Any]]] = {}
186
- for p in providers:
187
- by_cat.setdefault(p["category"], []).append(p)
188
-
189
- categories: List[Dict[str, Any]] = []
190
- for category, items in by_cat.items():
191
- total_sources = len(items)
192
- online_sources = sum(1 for i in items if i["status"] == "online")
193
- avg_rt = (
194
- sum(i["response_time_ms"] for i in items) / total_sources
195
- if total_sources
196
- else 0
197
- )
198
- status = "online" if online_sources > 0 else "offline"
199
- categories.append(
200
- {
201
- "name": category,
202
- "status": status,
203
- "online_sources": online_sources,
204
- "total_sources": total_sources,
205
- "avg_response_time_ms": int(avg_rt),
206
- }
207
- )
208
-
209
- return categories
210
-
211
-
212
- # -------------------------------------------------
213
- # /api/charts/health-history – نمودار خطی
214
- # -------------------------------------------------
215
- @app.get("/api/charts/health-history")
216
- async def api_chart_health_history(hours: int = 24):
217
- now = _utc_now()
218
- points = max(12, hours)
219
- timestamps: List[str] = []
220
- success_rate: List[float] = []
221
-
222
- for i in range(points):
223
- ts = now - timedelta(hours=(hours * (points - 1 - i) / points))
224
- base = 90
225
- noise = random.uniform(-10, 5)
226
- value = max(50.0, min(100.0, base + noise))
227
- timestamps.append(ts.isoformat())
228
- success_rate.append(round(value, 2))
229
-
230
- return {"timestamps": timestamps, "success_rate": success_rate}
231
-
232
-
233
- # -------------------------------------------------
234
- # /api/charts/compliance – نمودار میله‌ای
235
- # -------------------------------------------------
236
- @app.get("/api/charts/compliance")
237
- async def api_chart_compliance(days: int = 7):
238
- today = _utc_now().date()
239
- dates: List[str] = []
240
- compliance_percentage: List[float] = []
241
-
242
- for i in range(days):
243
- d = today - timedelta(days=(days - 1 - i))
244
- base = 88
245
- noise = random.uniform(-8, 5)
246
- value = max(60.0, min(100.0, base + noise))
247
- dates.append(d.isoformat())
248
- compliance_percentage.append(round(value, 2))
249
-
250
- return {"dates": dates, "compliance_percentage": compliance_percentage}
251
-
252
-
253
- # -------------------------------------------------
254
- # /api/freshness – جدول تازگی
255
- # -------------------------------------------------
256
- @app.get("/api/freshness")
257
- async def api_freshness():
258
- providers = _generate_providers_snapshot()
259
- items: List[Dict[str, Any]] = []
260
-
261
- for p in providers:
262
- fetch_time = datetime.fromisoformat(p["last_fetch"])
263
- staleness_minutes = (_utc_now() - fetch_time).total_seconds() / 60.0
264
- ttl_minutes = 30
265
- status = "fresh" if staleness_minutes <= ttl_minutes else "stale"
266
- items.append(
267
- {
268
- "provider": p["name"],
269
- "category": p["category"],
270
- "fetch_time": fetch_time.isoformat(),
271
- "staleness_minutes": round(staleness_minutes, 1),
272
- "ttl_minutes": ttl_minutes,
273
- "status": status,
274
- }
275
- )
276
-
277
- return items
278
-
279
-
280
- # -------------------------------------------------
281
- # /api/logs – لاگ‌ها
282
- # -------------------------------------------------
283
- @app.get("/api/logs")
284
- async def api_logs():
285
- providers = _generate_providers_snapshot()
286
- logs: List[Dict[str, Any]] = []
287
- now = _utc_now()
288
-
289
- for p in providers:
290
- logs.append(
291
- {
292
- "timestamp": (now - timedelta(minutes=random.randint(0, 60))).isoformat(),
293
- "provider": p["name"],
294
- "endpoint": "/api/status",
295
- "status": "success",
296
- "http_code": 200,
297
- "response_time_ms": p["response_time_ms"],
298
- "error_message": None,
299
- }
300
- )
301
- if p["status"] != "online" and random.random() < 0.5:
302
- logs.append(
303
- {
304
- "timestamp": (now - timedelta(minutes=random.randint(0, 60))).isoformat(),
305
- "provider": p["name"],
306
- "endpoint": "/api/markets",
307
- "status": "error",
308
- "http_code": random.choice([429, 500, 504]),
309
- "response_time_ms": random.randint(500, 2000),
310
- "error_message": "Upstream error or timeout",
311
- }
312
- )
313
-
314
- logs.sort(key=lambda x: x["timestamp"], reverse=True)
315
- return logs
316
-
317
-
318
- # -------------------------------------------------
319
- # /api/failures – خطاها
320
- # -------------------------------------------------
321
- @app.get("/api/failures")
322
- async def api_failures():
323
- logs = await api_logs()
324
- failures: List[Dict[str, Any]] = []
325
-
326
- for log in logs:
327
- if log["status"] == "error":
328
- failures.append(
329
- {
330
- "timestamp": log["timestamp"],
331
- "provider": log["provider"],
332
- "error_type": "HTTP " + str(log["http_code"]),
333
- "error_message": log["error_message"] or "Unknown error",
334
- "retry_attempted": random.random() < 0.6,
335
- }
336
- )
337
-
338
- return {"recent_failures": failures}
339
-
340
-
341
- # -------------------------------------------------
342
- # /api/config/keys – کلیدها
343
- # -------------------------------------------------
344
- @app.get("/api/config/keys")
345
- async def api_config_keys():
346
- return API_KEYS
347
-
348
-
349
- # -------------------------------------------------
350
- # /api/custom/add – API سفارشی
351
- # -------------------------------------------------
352
- @app.post("/api/custom/add")
353
- async def api_custom_add(
354
- name: str = Query(...),
355
- url: str = Query(...),
356
- category: str = Query(...),
357
- test_field: Optional[str] = Query(None),
358
- ):
359
- CUSTOM_APIS.append(
360
- {
361
- "name": name,
362
- "url": url,
363
- "category": category or "other",
364
- "test_field": test_field,
365
- "has_key": False,
366
- "priority": 3,
367
- }
368
- )
369
- return {"success": True, "message": "Custom API added successfully"}
370
-
371
-
372
- # -------------------------------------------------
373
- # WebSocket /ws/live
374
- # -------------------------------------------------
375
- class LiveConnections:
376
- def __init__(self) -> None:
377
- self.active: List[WebSocket] = []
378
-
379
- async def connect(self, websocket: WebSocket) -> None:
380
- await websocket.accept()
381
- self.active.append(websocket)
382
-
383
- def disconnect(self, websocket: WebSocket) -> None:
384
- if websocket in self.active:
385
- self.active.remove(websocket)
386
-
387
-
388
- live_manager = LiveConnections()
389
-
390
-
391
- @app.websocket("/ws/live")
392
- async def websocket_live(websocket: WebSocket):
393
- await live_manager.connect(websocket)
394
  try:
395
- await websocket.send_json(
396
- {
397
- "type": "welcome",
398
- "service": "crypto-data-source",
399
- "timestamp": _utc_now().isoformat(),
400
- }
401
- )
402
-
403
- while True:
404
- payload = {
405
- "type": "live_metrics",
406
- "timestamp": _utc_now().isoformat(),
407
- "metrics": {
408
- "cpu_usage": random.randint(5, 40),
409
- "memory_usage": random.randint(20, 75),
410
- "active_requests": random.randint(1, 30),
411
- "error_rate_percent": round(random.uniform(0.0, 3.0), 2),
412
- },
413
  }
414
- await websocket.send_json(payload)
415
- await asyncio.sleep(5)
416
- except WebSocketDisconnect:
417
- live_manager.disconnect(websocket)
418
- except Exception:
419
- live_manager.disconnect(websocket)
420
-
421
-
422
- # -------------------------------------------------
423
- # سرو index.html از همون روت
424
- # -------------------------------------------------
425
- @app.get("/")
426
- async def root():
427
- if INDEX_FILE.exists():
428
- return FileResponse(INDEX_FILE)
429
- return JSONResponse(
430
- {
431
- "message": "index.html not found next to app.py.",
432
- "hint": "Put index.html in the same folder as app.py or use /docs.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
433
  }
434
- )
435
-
436
-
437
- @app.get("/{full_path:path}")
438
- async def spa_catch_all(full_path: str):
439
- # اگر مسیر /api/... باشه و endpoint تعریف نشده باشه
440
- if full_path.startswith("api/"):
441
- return JSONResponse(
442
- {"error": "API endpoint not found", "path": full_path},
443
- status_code=404,
444
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
445
 
446
- # غیر از API، همیشه همون index.html رو برگردون (SPA)
447
- if INDEX_FILE.exists():
448
- return FileResponse(INDEX_FILE)
449
 
450
- return JSONResponse(
451
- {"error": "No frontend available for this path.", "path": full_path},
452
- status_code=404,
453
- )
 
1
+ """
2
+ Crypto Dashboard API - FastAPI Backend
3
+ پیاده‌سازی کامل تمام APIهای مورد نیاز برای داشبورد کریپتو
4
+ """
5
+
6
+ from fastapi import FastAPI, HTTPException
7
+ from fastapi.staticfiles import StaticFiles
8
+ from fastapi.responses import HTMLResponse, FileResponse
9
  from fastapi.middleware.cors import CORSMiddleware
10
+ import httpx
 
 
 
11
  import asyncio
12
+ from datetime import datetime, timedelta
13
+ from typing import List, Dict, Optional
14
  import random
15
+ import os
16
 
17
+ app = FastAPI(title="Crypto Dashboard API", version="1.0.0")
 
 
 
18
 
19
+ # CORS Configuration
20
  app.add_middleware(
21
  CORSMiddleware,
22
  allow_origins=["*"],
 
25
  allow_headers=["*"],
26
  )
27
 
28
+ # ============================================================================
29
+ # Data Caching System
30
+ # ============================================================================
31
+
32
+ class DataCache:
33
+ """سیستم کش ساده برای ذخیره موقت داده‌ها"""
34
+ def __init__(self):
35
+ self.cache = {}
36
+ self.cache_time = {}
37
+
38
+ def get(self, key: str, ttl: int = 60):
39
+ """دریافت داده از کش"""
40
+ if key in self.cache and key in self.cache_time:
41
+ if (datetime.now() - self.cache_time[key]).seconds < ttl:
42
+ return self.cache[key]
43
+ return None
44
+
45
+ def set(self, key: str, value):
46
+ """ذخیره داده در کش"""
47
+ self.cache[key] = value
48
+ self.cache_time[key] = datetime.now()
49
+
50
+ cache = DataCache()
51
+
52
+ # ============================================================================
53
+ # External API Client
54
+ # ============================================================================
55
+
56
+ async def fetch_coingecko(endpoint: str, params: dict = None):
57
+ """دریافت داده از CoinGecko API"""
58
+ base_url = "https://api.coingecko.com/api/v3"
59
+ url = f"{base_url}/{endpoint}"
60
+
61
+ try:
62
+ async with httpx.AsyncClient(timeout=10.0) as client:
63
+ response = await client.get(url, params=params)
64
+ if response.status_code == 200:
65
+ return response.json()
66
+ except Exception as e:
67
+ print(f"Error fetching from CoinGecko: {e}")
68
+
69
+ return None
70
+
71
+ async def fetch_alternative_me(endpoint: str):
72
+ """دریافت داده از Alternative.me API"""
73
+ base_url = "https://api.alternative.me"
74
+ url = f"{base_url}/{endpoint}"
75
+
76
+ try:
77
+ async with httpx.AsyncClient(timeout=10.0) as client:
78
+ response = await client.get(url)
79
+ if response.status_code == 200:
80
+ return response.json()
81
+ except Exception as e:
82
+ print(f"Error fetching from Alternative.me: {e}")
83
+
84
+ return None
85
 
86
+ # ============================================================================
87
+ # API Endpoints
88
+ # ============================================================================
 
 
 
 
89
 
90
+ @app.get("/")
91
+ async def root():
92
+ """صفحه اصلی - بارگذاری فایل HTML"""
93
+ html_path = os.path.join(os.path.dirname(__file__), "templates", "index.html")
94
+ if os.path.exists(html_path):
95
+ return FileResponse(html_path)
96
+ return {"message": "Crypto Dashboard API is running", "version": "1.0.0"}
97
+
98
+ @app.get("/api/crypto/market-overview")
99
+ async def get_market_overview():
100
+ """
101
+ نمای کلی بازار - Market Cap, Volume, BTC Dominance
102
+ """
103
+ cache_key = "market_overview"
104
+ cached = cache.get(cache_key, ttl=60)
105
+ if cached:
106
+ return cached
107
+
108
+ try:
109
+ # دریافت داده‌های کلی بازار
110
+ global_data = await fetch_coingecko("global", {})
111
+
112
+ if global_data and 'data' in global_data:
113
+ data = global_data['data']
114
+
115
+ result = {
116
+ "total_market_cap": data.get('total_market_cap', {}).get('usd', 0),
117
+ "total_volume_24h": data.get('total_volume', {}).get('usd', 0),
118
+ "btc_dominance": data.get('market_cap_percentage', {}).get('btc', 0),
119
+ "active_cryptocurrencies": data.get('active_cryptocurrencies', 0),
120
+ "markets": data.get('markets', 0),
121
+ "market_cap_change_24h": data.get('market_cap_change_percentage_24h_usd', 0),
122
+ "updated_at": datetime.now().isoformat()
123
  }
124
+
125
+ cache.set(cache_key, result)
126
+ return result
127
+
128
+ except Exception as e:
129
+ print(f"Error in market overview: {e}")
130
+
131
+ # داده‌های پیش‌فرض
 
 
132
  return {
133
+ "total_market_cap": 2500000000000,
134
+ "total_volume_24h": 100000000000,
135
+ "btc_dominance": 52.5,
136
+ "active_cryptocurrencies": 12000,
137
+ "markets": 850,
138
+ "market_cap_change_24h": 2.5,
139
+ "updated_at": datetime.now().isoformat()
140
  }
141
 
142
+ @app.get("/api/crypto/prices/trending")
143
+ async def get_trending_coins(limit: int = 10):
144
+ """
145
+ ارزهای ترند - Trending Cryptocurrencies
146
+ """
147
+ cache_key = f"trending_{limit}"
148
+ cached = cache.get(cache_key, ttl=300) # 5 دقیقه
149
+ if cached:
150
+ return cached
151
+
152
  try:
153
+ trending = await fetch_coingecko("search/trending", {})
154
+
155
+ if trending and 'coins' in trending:
156
+ result = []
157
+ for item in trending['coins'][:limit]:
158
+ coin = item.get('item', {})
159
+ result.append({
160
+ "symbol": coin.get('symbol', '').upper(),
161
+ "name": coin.get('name', ''),
162
+ "price": coin.get('data', {}).get('price', 0),
163
+ "price_change_24h": coin.get('data', {}).get('price_change_percentage_24h', {}).get('usd', 0),
164
+ "market_cap": coin.get('data', {}).get('market_cap', 0),
165
+ "volume_24h": coin.get('data', {}).get('total_volume', 0),
166
+ "rank": coin.get('market_cap_rank', 0)
167
+ })
168
+
169
+ cache.set(cache_key, result)
170
+ return result
171
+
172
+ except Exception as e:
173
+ print(f"Error in trending coins: {e}")
174
+
175
+ # داده‌های نمونه
176
+ return generate_sample_coins(limit)
177
+
178
+ @app.get("/api/crypto/prices/top")
179
+ async def get_top_coins(limit: int = 20):
180
+ """
181
+ ارزهای برتر - Top Cryptocurrencies by Market Cap
182
+ """
183
+ cache_key = f"top_coins_{limit}"
184
+ cached = cache.get(cache_key, ttl=60)
185
+ if cached:
186
+ return cached
187
+
188
+ try:
189
+ params = {
190
+ "vs_currency": "usd",
191
+ "order": "market_cap_desc",
192
+ "per_page": limit,
193
+ "page": 1,
194
+ "sparkline": False,
195
+ "price_change_percentage": "24h"
196
  }
197
+
198
+ coins = await fetch_coingecko("coins/markets", params)
199
+
200
+ if coins:
201
+ result = []
202
+ for coin in coins:
203
+ result.append({
204
+ "symbol": coin.get('symbol', '').upper(),
205
+ "name": coin.get('name', ''),
206
+ "price": coin.get('current_price', 0),
207
+ "price_change_24h": coin.get('price_change_percentage_24h', 0),
208
+ "market_cap": coin.get('market_cap', 0),
209
+ "volume_24h": coin.get('total_volume', 0),
210
+ "rank": coin.get('market_cap_rank', 0),
211
+ "circulating_supply": coin.get('circulating_supply', 0),
212
+ "high_24h": coin.get('high_24h', 0),
213
+ "low_24h": coin.get('low_24h', 0)
214
+ })
215
+
216
+ cache.set(cache_key, result)
217
+ return result
218
+
219
  except Exception as e:
220
+ print(f"Error in top coins: {e}")
221
+
222
+ return generate_sample_coins(limit)
223
+
224
+ @app.get("/api/crypto/news/latest")
225
+ async def get_latest_news(limit: int = 20):
226
+ """
227
+ آخرین اخبار کریپتو - Latest Crypto News
228
+ """
229
+ cache_key = f"news_{limit}"
230
+ cached = cache.get(cache_key, ttl=600) # 10 دقیقه
231
+ if cached:
232
+ return cached
233
+
234
+ # شبیه‌سازی اخبار (در نسخه واقعی از یک News API استفاده می‌شود)
235
+ news = []
236
+ sources = ["CoinDesk", "CryptoNews", "Bitcoin Magazine", "The Block", "Decrypt"]
237
+ titles = [
238
+ "Bitcoin Reaches New All-Time High",
239
+ "Ethereum 2.0 Upgrade Successful",
240
+ "Major Institution Announces Crypto Investment",
241
+ "New DeFi Protocol Launches",
242
+ "Regulatory Update on Cryptocurrency",
243
+ "Blockchain Technology Adoption Increases",
244
+ "NFT Market Shows Strong Growth",
245
+ "Layer 2 Solutions Gain Traction"
246
+ ]
247
+
248
+ for i in range(limit):
249
+ news.append({
250
+ "title": random.choice(titles),
251
+ "source": random.choice(sources),
252
+ "url": f"https://example.com/news/{i}",
253
+ "published_at": (datetime.now() - timedelta(hours=random.randint(1, 24))).isoformat(),
254
+ "content": "Latest developments in the cryptocurrency market..."
255
+ })
256
+
257
+ cache.set(cache_key, news)
258
+ return news
259
+
260
+ @app.get("/api/crypto/sentiment/current")
261
+ async def get_current_sentiment():
262
+ """
263
+ احساسات فعلی بازار - Fear & Greed Index
264
+ """
265
+ cache_key = "sentiment_current"
266
+ cached = cache.get(cache_key, ttl=300) # 5 دقیقه
267
+ if cached:
268
+ return cached
269
+
270
  try:
271
+ sentiment = await fetch_alternative_me("fng/")
272
+
273
+ if sentiment and 'data' in sentiment and len(sentiment['data']) > 0:
274
+ data = sentiment['data'][0]
275
+
276
+ result = {
277
+ "fear_greed_index": int(data.get('value', 50)),
278
+ "classification": data.get('value_classification', 'Neutral'),
279
+ "description": get_sentiment_description(int(data.get('value', 50))),
280
+ "timestamp": data.get('timestamp', datetime.now().isoformat())
281
+ }
282
+
283
+ cache.set(cache_key, result)
284
+ return result
285
+
286
  except Exception as e:
287
+ print(f"Error in current sentiment: {e}")
288
+
289
+ # داده پیش‌فرض
290
+ value = 50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
291
  return {
292
+ "fear_greed_index": value,
293
+ "classification": get_sentiment_classification(value),
294
+ "description": get_sentiment_description(value),
295
+ "timestamp": datetime.now().isoformat()
 
 
 
 
 
 
 
 
 
296
  }
297
 
298
+ @app.get("/api/crypto/sentiment/history")
299
+ async def get_sentiment_history(hours: int = 168):
300
+ """
301
+ تاریخچه احساسات - Sentiment History
302
+ """
303
+ cache_key = f"sentiment_history_{hours}"
304
+ cached = cache.get(cache_key, ttl=3600) # 1 ساعت
305
+ if cached:
306
+ return cached
307
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
308
  try:
309
+ # تبدیل ساعات به روزها
310
+ days = max(1, hours // 24)
311
+ sentiment = await fetch_alternative_me(f"fng/?limit={days}")
312
+
313
+ if sentiment and 'data' in sentiment:
314
+ result = {
315
+ "history": [
316
+ {
317
+ "value": int(item.get('value', 50)),
318
+ "classification": item.get('value_classification', 'Neutral'),
319
+ "timestamp": item.get('timestamp', '')
320
+ }
321
+ for item in sentiment['data']
322
+ ]
 
 
 
 
323
  }
324
+
325
+ cache.set(cache_key, result)
326
+ return result
327
+
328
+ except Exception as e:
329
+ print(f"Error in sentiment history: {e}")
330
+
331
+ # داده‌های نمونه
332
+ history = []
333
+ for i in range(hours // 24):
334
+ history.append({
335
+ "value": random.randint(20, 80),
336
+ "classification": "Neutral",
337
+ "timestamp": (datetime.now() - timedelta(days=i)).isoformat()
338
+ })
339
+
340
+ return {"history": history}
341
+
342
+ @app.get("/api/crypto/whales/transactions")
343
+ async def get_whale_transactions(limit: int = 20):
344
+ """
345
+ تراکنش‌های نهنگ‌ها - Whale Transactions
346
+ """
347
+ cache_key = f"whales_{limit}"
348
+ cached = cache.get(cache_key, ttl=300) # 5 دقیقه
349
+ if cached:
350
+ return cached
351
+
352
+ # شبیه‌سازی تراکنش‌های نهنگ
353
+ transactions = []
354
+ blockchains = ["Ethereum", "Bitcoin", "BSC", "Polygon"]
355
+
356
+ for i in range(limit):
357
+ transactions.append({
358
+ "amount_usd": random.randint(1000000, 100000000),
359
+ "blockchain": random.choice(blockchains),
360
+ "from_address": f"0x{''.join(random.choices('0123456789abcdef', k=40))}",
361
+ "to_address": f"0x{''.join(random.choices('0123456789abcdef', k=40))}",
362
+ "timestamp": (datetime.now() - timedelta(minutes=random.randint(1, 1440))).isoformat(),
363
+ "tx_hash": f"0x{''.join(random.choices('0123456789abcdef', k=64))}"
364
+ })
365
+
366
+ cache.set(cache_key, transactions)
367
+ return transactions
368
+
369
+ @app.get("/api/crypto/blockchain/gas")
370
+ async def get_gas_prices():
371
+ """
372
+ قیمت گس شبکه‌ها - Network Gas Prices
373
+ """
374
+ cache_key = "gas_prices"
375
+ cached = cache.get(cache_key, ttl=30) # 30 ثانیه
376
+ if cached:
377
+ return cached
378
+
379
+ # در نسخه واقعی از Etherscan API یا Web3 استفاده می‌شود
380
+ result = {
381
+ "ethereum": {
382
+ "gas_price_gwei": random.randint(20, 100),
383
+ "fast": random.randint(50, 150),
384
+ "standard": random.randint(30, 80),
385
+ "slow": random.randint(15, 40)
386
+ },
387
+ "bsc": {
388
+ "gas_price_gwei": random.randint(3, 10),
389
+ "fast": random.randint(5, 15),
390
+ "standard": random.randint(3, 10),
391
+ "slow": random.randint(1, 5)
392
+ },
393
+ "polygon": {
394
+ "gas_price_gwei": random.randint(50, 200),
395
+ "fast": random.randint(100, 300),
396
+ "standard": random.randint(50, 150),
397
+ "slow": random.randint(20, 80)
398
  }
399
+ }
400
+
401
+ cache.set(cache_key, result)
402
+ return result
403
+
404
+ @app.get("/api/crypto/blockchain/stats")
405
+ async def get_blockchain_stats():
406
+ """
407
+ آمار بلاکچین - Blockchain Statistics
408
+ """
409
+ cache_key = "blockchain_stats"
410
+ cached = cache.get(cache_key, ttl=60)
411
+ if cached:
412
+ return cached
413
+
414
+ result = {
415
+ "ethereum": {
416
+ "latest_block": random.randint(18000000, 19000000),
417
+ "block_time": 12.0,
418
+ "pending_transactions": random.randint(100000, 500000),
419
+ "network_hashrate": random.randint(800, 1000)
420
+ },
421
+ "bitcoin": {
422
+ "latest_block": random.randint(800000, 850000),
423
+ "block_time": 600.0,
424
+ "pending_transactions": random.randint(5000, 50000),
425
+ "network_hashrate": random.randint(400, 600)
426
+ },
427
+ "bsc": {
428
+ "latest_block": random.randint(35000000, 36000000),
429
+ "block_time": 3.0,
430
+ "pending_transactions": random.randint(50000, 200000),
431
+ "network_hashrate": random.randint(100, 200)
432
+ }
433
+ }
434
+
435
+ cache.set(cache_key, result)
436
+ return result
437
+
438
+ # ============================================================================
439
+ # Helper Functions
440
+ # ============================================================================
441
+
442
+ def get_sentiment_classification(value: int) -> str:
443
+ """تبدیل عدد به دسته‌بندی احساسات"""
444
+ if value <= 20:
445
+ return "Extreme Fear"
446
+ elif value <= 40:
447
+ return "Fear"
448
+ elif value <= 60:
449
+ return "Neutral"
450
+ elif value <= 80:
451
+ return "Greed"
452
+ else:
453
+ return "Extreme Greed"
454
+
455
+ def get_sentiment_description(value: int) -> str:
456
+ """توضیح احساسات"""
457
+ if value <= 20:
458
+ return "بازار در ترس شدید است - زمان خوبی برای خرید"
459
+ elif value <= 40:
460
+ return "بازار ترسناک است - احتیاط کنید"
461
+ elif value <= 60:
462
+ return "بازار خنثی است - منتظر سیگنال بمانید"
463
+ elif value <= 80:
464
+ return "بازار طمعکار است - احتیاط در خرید"
465
+ else:
466
+ return "بازار در طمع شدید است - زمان فروش"
467
+
468
+ def generate_sample_coins(limit: int) -> List[Dict]:
469
+ """تولید داده‌های نمونه برای ارزها"""
470
+ coins = [
471
+ {"symbol": "BTC", "name": "Bitcoin", "base_price": 43000},
472
+ {"symbol": "ETH", "name": "Ethereum", "base_price": 2300},
473
+ {"symbol": "BNB", "name": "Binance Coin", "base_price": 320},
474
+ {"symbol": "ADA", "name": "Cardano", "base_price": 0.52},
475
+ {"symbol": "SOL", "name": "Solana", "base_price": 98},
476
+ {"symbol": "XRP", "name": "Ripple", "base_price": 0.63},
477
+ {"symbol": "DOT", "name": "Polkadot", "base_price": 7.5},
478
+ {"symbol": "DOGE", "name": "Dogecoin", "base_price": 0.085},
479
+ {"symbol": "MATIC", "name": "Polygon", "base_price": 0.89},
480
+ {"symbol": "AVAX", "name": "Avalanche", "base_price": 37},
481
+ ]
482
+
483
+ result = []
484
+ for i, coin in enumerate(coins[:limit]):
485
+ price_change = random.uniform(-10, 10)
486
+ price = coin["base_price"] * (1 + price_change / 100)
487
+
488
+ result.append({
489
+ "symbol": coin["symbol"],
490
+ "name": coin["name"],
491
+ "price": round(price, 2),
492
+ "price_change_24h": round(price_change, 2),
493
+ "market_cap": round(price * random.randint(10000000, 1000000000), 2),
494
+ "volume_24h": round(price * random.randint(1000000, 100000000), 2),
495
+ "rank": i + 1
496
+ })
497
+
498
+ return result
499
+
500
+ # ============================================================================
501
+ # Health Check
502
+ # ============================================================================
503
+
504
+ @app.get("/health")
505
+ async def health_check():
506
+ """بررسی سلامت API"""
507
+ return {
508
+ "status": "healthy",
509
+ "timestamp": datetime.now().isoformat(),
510
+ "version": "1.0.0"
511
+ }
512
 
513
+ # ============================================================================
514
+ # Run Application
515
+ # ============================================================================
516
 
517
+ if __name__ == "__main__":
518
+ import uvicorn
519
+ print("🚀 Starting Crypto Dashboard API on http://0.0.0.0:7860")
520
+ uvicorn.run(app, host="0.0.0.0", port=7860, log_level="info")