heisbuba commited on
Commit
1fd14f2
Β·
verified Β·
1 Parent(s): 5a29da8

Upload spot_engine.py

Browse files
Files changed (1) hide show
  1. src/services/spot_engine.py +321 -0
src/services/spot_engine.py ADDED
@@ -0,0 +1,321 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+ import datetime
3
+ import threading
4
+ import requests
5
+ from typing import List, Dict, Any, Tuple
6
+ from concurrent.futures import ThreadPoolExecutor, as_completed
7
+
8
+ # Import Shared Modules
9
+ from src.state import get_user_temp_dir
10
+ from src.config import STABLECOINS
11
+ from src.services.utils import short_num, now_str
12
+
13
+ # Spot Volume Tracker
14
+ def spot_volume_tracker(user_keys, user_id) -> None:
15
+ """
16
+ Aggregates spot market data with accuracy and performance.
17
+ Prioritizes CoinGecko data for volume accuracy.
18
+ """
19
+ def safe_float(val, default):
20
+ try:
21
+ if val is None or str(val).strip() == "":
22
+ return default
23
+ return float(val)
24
+ except (ValueError, TypeError):
25
+ return default
26
+
27
+ print(" πŸ“Š Starting fresh spot analysis...")
28
+
29
+ # Set thread name once at the start
30
+ threading.current_thread().name = f"user_{user_id}"
31
+
32
+ # Extract API keys
33
+ CMC_API_KEY = user_keys.get("CMC_API_KEY", "CONFIG_REQUIRED_CMC")
34
+ COINGECKO_API_KEY = user_keys.get("COINGECKO_API_KEY", "CONFIG_REQUIRED_CG")
35
+ LIVECOINWATCH_API_KEY = user_keys.get("LIVECOINWATCH_API_KEY", "CONFIG_REQUIRED_LCW")
36
+
37
+ # User Filters & Safety Helper
38
+ settings = user_keys.get("engine_settings", {})
39
+ MIN_VTMR = safe_float(settings.get('min_vtmr'), 0.5)
40
+ MAX_VTMR = safe_float(settings.get('max_vtmr'), 199.0)
41
+ MIN_LC_VTMR = safe_float(settings.get('min_largecap_vtmr'), 0.5)
42
+ LC_THRESHOLD = 1_000_000_000
43
+ FETCH_THRESHOLD = min(MIN_VTMR, MIN_LC_VTMR)
44
+
45
+ # --- Stealth Headers Injection ---
46
+ STEALTH_HEADERS = {
47
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
48
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
49
+ "Accept-Language": "en-US,en;q=0.9",
50
+ "Accept-Encoding": "gzip, deflate, br",
51
+ "Connection": "keep-alive",
52
+ "Upgrade-Insecure-Requests": "1",
53
+ "Sec-Fetch-Dest": "document",
54
+ "Sec-Fetch-Mode": "navigate",
55
+ "Sec-Fetch-Site": "none",
56
+ "Sec-Fetch-User": "?1",
57
+ "Cache-Control": "max-age=0",
58
+ }
59
+
60
+ def create_html_report(hot_tokens: List[Dict[str, Any]]) -> str:
61
+ """Generates a mobile-responsive HTML report."""
62
+ # 1. Setup File Paths & Time
63
+ date_prefix = datetime.datetime.now().strftime("%b-%d-%y_%H-%M")
64
+ user_dir = get_user_temp_dir(user_id)
65
+ html_file = user_dir / f"Spot_Analysis_Report_{date_prefix}.html"
66
+ current_time = now_str("%d-%m-%Y %H:%M:%S")
67
+
68
+ # 2. Calculate Header Stats
69
+ max_flip = max((t.get('flipping_multiple', 0) for t in hot_tokens), default=0)
70
+
71
+ # 3. Build HTML Header with Viewport Tag (Crucial for Mobile Interactivity)
72
+ html_content = f"""
73
+ <!DOCTYPE html>
74
+ <html>
75
+ <head>
76
+ <title>Spot Analysis {date_prefix}</title>
77
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
78
+ <style>
79
+ body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; margin: 0; background-color: #f5f5f5; color: #333; }}
80
+ .header {{ background-color: #2c3e50; color: white; padding: 15px; text-align: center; }}
81
+ .summary {{ background-color: #fff; padding: 15px; margin: 10px; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.05); font-size: 0.9rem; }}
82
+
83
+ /* Table Optimization for Mobile */
84
+ .table-container {{ overflow-x: auto; -webkit-overflow-scrolling: touch; margin: 10px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }}
85
+ table {{ width: 100%; border-collapse: collapse; background-color: white; min-width: 100%; }}
86
+
87
+ th {{ background-color: #3498db; color: white; padding: 12px 8px; text-align: left; font-size: 0.85rem; white-space: nowrap; }}
88
+ td {{ padding: 0; border-bottom: 1px solid #eee; height: 50px; vertical-align: middle; font-size: 0.9rem; }}
89
+
90
+ tr:nth-child(even) {{ background-color: #fafafa; }}
91
+
92
+ /* Visual Cue for Large Cap (Green Left Border) */
93
+ tr.large-cap {{ background-color: #e8f6f3 !important; border-left: 4px solid #10b981; }}
94
+
95
+ /* Mobile Tappable Link Button */
96
+ .ticker-btn {{
97
+ display: block;
98
+ width: 100%;
99
+ height: 100%;
100
+ padding: 14px 5px; /* Large touch target */
101
+ color: #2980b9;
102
+ text-decoration: none;
103
+ font-weight: 800;
104
+ box-sizing: border-box;
105
+ }}
106
+ .ticker-btn:active {{ background-color: rgba(52, 152, 219, 0.1); }}
107
+
108
+ .vol-high {{ color: #e74c3c; font-weight: bold; }}
109
+ </style>
110
+ </head>
111
+ <body>
112
+ <div class="header">
113
+ <h3 style="margin:0;">SPOT VOLUMETRICS</h3>
114
+ <small>{current_time}</small>
115
+ </div>
116
+
117
+ <div class="summary">
118
+ <strong>Found:</strong> {len(hot_tokens)} Tokens |
119
+ <strong>Peak VTMR:</strong> {max_flip:.1f}x <br>
120
+ <small style="color:#10b981;">🟩 Green Rows = Large Cap (>$1B)</small>
121
+ </div>
122
+
123
+ <div class="table-container">
124
+ <table>
125
+ <thead>
126
+ <tr>
127
+ <th style="width: 40px; text-align:center;">#</th>
128
+ <th>Ticker</th>
129
+ <th>MCap</th>
130
+ <th>Vol 24h</th>
131
+ <th>VTMR</th>
132
+ </tr>
133
+ </thead>
134
+ <tbody>
135
+ """
136
+
137
+ if hot_tokens:
138
+ for i, token in enumerate(hot_tokens):
139
+ # 4. Prepare Logic
140
+ is_lc = token.get('large_cap', False)
141
+ row_class = "large-cap" if is_lc else ""
142
+ vol_class = "vol-high" if token.get('flipping_multiple', 0) >= 2 else ""
143
+ sym = token.get('symbol', '???')
144
+
145
+ # --- SENIOR ENGINEER SAFETY NET ---
146
+ # We embed the removed data as hidden attributes so your parser works later.
147
+ row_attrs = f'data-large-cap="{is_lc}" data-sources="{token.get("source_count")}"'
148
+
149
+ # 5. Create Tappable Link (Absolute Path)
150
+ link = f'<a href="/deep-diver?ticker={sym}" class="ticker-btn">{sym}</a>'
151
+
152
+ html_content += f"""
153
+ <tr class="{row_class}" {row_attrs}>
154
+ <td style="text-align:center; color:#999; font-size:0.8rem;">{i+1}</td>
155
+
156
+ <td style="min-width: 60px;">{link}</td>
157
+
158
+ <td style="padding-left:8px;">${short_num(token.get('marketcap', 0))}</td>
159
+ <td style="padding-left:8px;">${short_num(token.get('volume', 0))}</td>
160
+ <td class="{vol_class}" style="padding-left:8px;">{token.get('flipping_multiple', 0):.1f}x</td>
161
+ </tr>
162
+ """
163
+ html_content += "</tbody></table></div></body></html>"
164
+ else:
165
+ html_content += "<div style='padding:20px; text-align:center;'>No significant volume detected.</div>"
166
+
167
+ with open(html_file, "w", encoding="utf-8") as f:
168
+ f.write(html_content)
169
+ return html_file
170
+
171
+ # --- Data Fetching Functions ---
172
+
173
+ def fetch_coingecko(session: requests.Session) -> List[Dict[str, Any]]:
174
+ threading.current_thread().name = f"user_{user_id}"
175
+ tokens: List[Dict[str, Any]] = []
176
+ use_key = bool(COINGECKO_API_KEY and COINGECKO_API_KEY != "CONFIG_REQUIRED_CG")
177
+
178
+ for page in range(1, 5):
179
+ try:
180
+ url = "https://api.coingecko.com/api/v3/coins/markets"
181
+ params = {"vs_currency": "usd", "order": "market_cap_desc", "per_page": 250, "page": page}
182
+ headers = STEALTH_HEADERS.copy()
183
+
184
+ if use_key:
185
+ if page == 1: print(" ⚑ Scanning CoinGecko...")
186
+ headers["x-cg-demo-api-key"] = COINGECKO_API_KEY
187
+ delay = 0.05
188
+ else:
189
+ if page == 1: print(" 🐌 Scanning CoinGecko (Slow Mode)...")
190
+ delay = 0.2
191
+
192
+ r = session.get(url, params=params, headers=headers, timeout=15)
193
+ if use_key and r.status_code in [401, 403, 429]:
194
+ use_key = False
195
+ delay = 0.2
196
+ r = session.get(url, params=params, headers=STEALTH_HEADERS, timeout=15)
197
+
198
+ r.raise_for_status()
199
+ for t in r.json():
200
+ symbol = (t.get("symbol") or "").upper()
201
+ if symbol in STABLECOINS: continue
202
+ vol, mc = float(t.get("total_volume") or 0), float(t.get("market_cap") or 0)
203
+
204
+ # Fetching pre-filter
205
+ if mc > 0 and (vol / mc) >= FETCH_THRESHOLD:
206
+ tokens.append({"symbol": symbol, "marketcap": mc, "volume": vol, "source": "CG"})
207
+ time.sleep(delay)
208
+ except Exception: continue
209
+ print(f" βœ… CoinGecko: {len(tokens)} tokens")
210
+ return tokens
211
+
212
+ def fetch_coinmarketcap(session: requests.Session) -> List[Dict[str, Any]]:
213
+ threading.current_thread().name = f"user_{user_id}"
214
+ tokens: List[Dict[str, Any]] = []
215
+ if not CMC_API_KEY or CMC_API_KEY == "CONFIG_REQUIRED_CMC": return tokens
216
+
217
+ print(" ⚑ Scanning CoinMarketCap...")
218
+ headers = STEALTH_HEADERS.copy()
219
+ headers["X-CMC_PRO_API_KEY"] = CMC_API_KEY
220
+ for start in range(1, 1001, 100):
221
+ try:
222
+ r = session.get("https://pro-api.coinmarketcap.com/v1/cryptocurrency/listings/latest",
223
+ headers=headers, params={"start": start, "limit": 100, "convert": "USD"}, timeout=15)
224
+ r.raise_for_status()
225
+ for t in r.json().get("data", []):
226
+ symbol = (t.get("symbol") or "").upper()
227
+ if symbol in STABLECOINS: continue
228
+ q = t.get("quote", {}).get("USD", {})
229
+ vol, mc = float(q.get("volume_24h") or 0), float(q.get("market_cap") or 0)
230
+ if mc > 0 and (vol / mc) >= FETCH_THRESHOLD:
231
+ tokens.append({"symbol": symbol, "marketcap": mc, "volume": vol, "source": "CMC"})
232
+ time.sleep(0.2)
233
+ except Exception: continue
234
+ print(f" βœ… CoinMarketCap: {len(tokens)} tokens")
235
+ return tokens
236
+
237
+ def fetch_livecoinwatch(session: requests.Session) -> List[Dict[str, Any]]:
238
+ threading.current_thread().name = f"user_{user_id}"
239
+ tokens: List[Dict[str, Any]] = []
240
+ if not LIVECOINWATCH_API_KEY or LIVECOINWATCH_API_KEY == "CONFIG_REQUIRED_LCW": return tokens
241
+
242
+ print(" ⚑ Scanning LiveCoinWatch...")
243
+ headers = STEALTH_HEADERS.copy()
244
+ headers.update({"content-type": "application/json", "x-api-key": LIVECOINWATCH_API_KEY})
245
+ payload = {"currency": "USD", "sort": "rank", "order": "ascending", "offset": 0, "limit": 1000, "meta": True}
246
+ try:
247
+ r = session.post("https://api.livecoinwatch.com/coins/list", json=payload, headers=headers, timeout=20)
248
+ r.raise_for_status()
249
+ for t in r.json():
250
+ symbol = (t.get("code") or "").upper()
251
+ if symbol in STABLECOINS: continue
252
+ vol, mc = float(t.get("volume") or 0), float(t.get("cap") or 0)
253
+ if mc > 0 and (vol / mc) >= FETCH_THRESHOLD:
254
+ tokens.append({"symbol": symbol, "marketcap": mc, "volume": vol, "source": "LCW"})
255
+ except Exception: pass
256
+ print(f" βœ… LiveCoinWatch: {len(tokens)} tokens")
257
+ return tokens
258
+
259
+ def fetch_all_sources() -> Tuple[List[Dict[str, Any]], int]:
260
+ print(" πŸ” Scanning for high-volumed tokens...")
261
+ print(" ⬆️ CoinGecko data for accuracy... ")
262
+ sources = [fetch_coingecko, fetch_coinmarketcap, fetch_livecoinwatch]
263
+ results = []
264
+ with ThreadPoolExecutor(max_workers=3) as exe:
265
+ futures = [exe.submit(fn, requests.Session()) for fn in sources]
266
+ for f in as_completed(futures):
267
+ try:
268
+ res = f.result(timeout=60)
269
+ if res: results.extend(res)
270
+ except Exception: continue
271
+ print(f" πŸ“Š Total raw results: {len(results)}")
272
+ return results, len(results)
273
+
274
+ # --- Processing Logic ---
275
+ raw_tokens, _ = fetch_all_sources()
276
+ all_data = {}
277
+ for t in raw_tokens:
278
+ all_data.setdefault(t['symbol'], []).append(t)
279
+
280
+ verified_tokens = []
281
+ for sym, tokens in all_data.items():
282
+ # Identify CoinGecko entry specifically
283
+ cg_data = next((t for t in tokens if t['source'] == 'CG'), None)
284
+
285
+ if cg_data:
286
+ # --- GATEKEEPER RULE: COINGECKO IS SOVEREIGN ---
287
+ # If CG has it, we ignore all other sources and use CG metrics alone
288
+ volume, marketcap = cg_data['volume'], cg_data['marketcap']
289
+ ratio = volume / marketcap
290
+ is_large = (marketcap > LC_THRESHOLD)
291
+
292
+ # Flexible Thresholds
293
+ if (is_large and ratio >= MIN_LC_VTMR and ratio <= MAX_VTMR) or \
294
+ (not is_large and ratio >= MIN_VTMR and ratio <= MAX_VTMR):
295
+ verified_tokens.append({
296
+ "symbol": sym, "marketcap": marketcap, "volume": volume,
297
+ "flipping_multiple": ratio, "source_count": len(tokens), "large_cap": is_large
298
+ })
299
+ else:
300
+ # --- FALLBACK RULE: MULTI-SOURCE VERIFICATION ---
301
+ # If CG is missing, the token MUST have at least 2 other sources to even be considered
302
+ if len(tokens) >= 2:
303
+ volume = sum(t['volume'] for t in tokens) / len(tokens)
304
+ marketcap = sum(t['marketcap'] for t in tokens) / len(tokens)
305
+ ratio = volume / marketcap
306
+ is_large = any(t['marketcap'] > LC_THRESHOLD for t in tokens)
307
+
308
+ if (is_large and ratio >= MIN_LC_VTMR and ratio <= MAX_VTMR) or \
309
+ (not is_large and ratio >= MIN_VTMR and ratio <= MAX_VTMR):
310
+ verified_tokens.append({
311
+ "symbol": sym, "marketcap": marketcap, "volume": volume,
312
+ "flipping_multiple": ratio, "source_count": len(tokens), "large_cap": is_large
313
+ })
314
+ hot_tokens = sorted(verified_tokens, key=lambda x: x["flipping_multiple"], reverse=True)
315
+ html_file = create_html_report(hot_tokens)
316
+ #-- Print
317
+ report_filename = html_file.name
318
+ now_h = datetime.datetime.now().strftime("%H:%M:%S")
319
+ print(f" πŸ’Ž Found {len(hot_tokens)} high-volume tokens at {now_h}")
320
+ print(f" πŸ“‚ HTML report saved: /reports-list/{report_filename}")
321
+ print(" 🏁 Spot volume analysis completed!")