sophhub 0.2.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/package.json +1 -1
  2. package/skills/consensus/skill.json +20 -0
  3. package/skills/consensus/src/SKILL.md +93 -0
  4. package/skills/deepwiki/skill.json +20 -0
  5. package/skills/deepwiki/src/SKILL.md +45 -0
  6. package/skills/deepwiki/src/_meta.json +6 -0
  7. package/skills/deepwiki/src/scripts/deepwiki.js +135 -0
  8. package/skills/feishu-bitable/skill.json +20 -0
  9. package/skills/feishu-bitable/src/CHECKLIST.md +150 -0
  10. package/skills/feishu-bitable/src/README.md +178 -0
  11. package/skills/feishu-bitable/src/SKILL.md +113 -0
  12. package/skills/feishu-bitable/src/_meta.json +6 -0
  13. package/skills/feishu-bitable/src/api.js +381 -0
  14. package/skills/feishu-bitable/src/bin/cli.js +284 -0
  15. package/skills/feishu-bitable/src/description.md +143 -0
  16. package/skills/feishu-bitable/src/examples/create-records.json +52 -0
  17. package/skills/feishu-bitable/src/examples/create-table.json +64 -0
  18. package/skills/feishu-bitable/src/package-lock.json +324 -0
  19. package/skills/feishu-bitable/src/package.json +33 -0
  20. package/skills/feishu-bitable/src/publish-config.json +14 -0
  21. package/skills/feishu-bitable/src/test-simple.js +61 -0
  22. package/skills/feishu-bitable/src/utils.js +261 -0
  23. package/skills/google-maps/skill.json +20 -0
  24. package/skills/google-maps/src/SKILL.md +237 -0
  25. package/skills/google-maps/src/_meta.json +6 -0
  26. package/skills/google-maps/src/lib/map_helper.py +912 -0
  27. package/skills/large-task-router/skill.json +20 -0
  28. package/skills/large-task-router/src/SKILL.md +79 -0
  29. package/skills/large-task-router/src/templates/plan.md +74 -0
  30. package/skills/notes-hub-assistant/skill.json +20 -0
  31. package/skills/notes-hub-assistant/src/SKILL.md +233 -0
  32. package/skills/notes-hub-assistant/src/scripts/_resolve_lark_cli.py +48 -0
  33. package/skills/notes-hub-assistant/src/scripts/openclaw_meeting_minutes.py +473 -0
  34. package/skills/notes-hub-assistant/src/scripts/openclaw_notes_crud.py +596 -0
  35. package/skills/notes-hub-assistant/src/scripts/openclaw_wolai_notes_crud.py +364 -0
  36. package/skills/notes-hub-assistant/src/scripts/run_meeting_minutes.py +79 -0
  37. package/skills/notes-hub-assistant/src/scripts/run_note_crud.py +37 -0
  38. package/skills/notes-hub-assistant/src/scripts/run_notionbot.py +36 -0
  39. package/skills/notes-hub-assistant/src/scripts/run_wolai_note_crud.py +27 -0
  40. package/skills/skillhub/skill.json +11 -4
  41. package/skills/skillhub/src/SKILL.md +11 -1
  42. package/skills/sophnet-dailynews/skill.json +20 -0
  43. package/skills/sophnet-dailynews/src/SKILL.md +179 -0
  44. package/skills/sophnet-dailynews/src/cache.json +151 -0
  45. package/skills/sophnet-dailynews/src/sources.json +230 -0
  46. package/skills/sophnet-schedule/skill.json +20 -0
  47. package/skills/sophnet-schedule/src/ARCHITECTURE.md +321 -0
  48. package/skills/sophnet-schedule/src/IMPROVEMENTS.md +145 -0
  49. package/skills/sophnet-schedule/src/SKILL.md +1050 -0
  50. package/skills/sophnet-schedule/src/_meta.json +6 -0
  51. package/skills/sophnet-schedule/src/api/__init__.py +0 -0
  52. package/skills/sophnet-schedule/src/api/models.py +245 -0
  53. package/skills/sophnet-schedule/src/apps/add_event.py +237 -0
  54. package/skills/sophnet-schedule/src/apps/check_reminders.py +112 -0
  55. package/skills/sophnet-schedule/src/apps/check_roc.py +246 -0
  56. package/skills/sophnet-schedule/src/apps/generate_daily_plan.py +342 -0
  57. package/skills/sophnet-schedule/src/apps/import_events.py +216 -0
  58. package/skills/sophnet-schedule/src/apps/monitor_calendar_changes.py +140 -0
  59. package/skills/sophnet-schedule/src/apps/register_tasks.py +169 -0
  60. package/skills/sophnet-schedule/src/apps/sync_roc_to_gcal.py +174 -0
  61. package/skills/sophnet-schedule/src/compat.py +66 -0
  62. package/skills/sophnet-schedule/src/config/__init__.py +0 -0
  63. package/skills/sophnet-schedule/src/config/reminder_rules.yaml +96 -0
  64. package/skills/sophnet-schedule/src/config/roc_events.yaml +44 -0
  65. package/skills/sophnet-schedule/src/config/settings.py +133 -0
  66. package/skills/sophnet-schedule/src/config/task_registry.yaml +92 -0
  67. package/skills/sophnet-schedule/src/docs/FRONTEND_INTEGRATION_GUIDE.md +437 -0
  68. package/skills/sophnet-schedule/src/gcal/__init__.py +0 -0
  69. package/skills/sophnet-schedule/src/gcal/client.py +374 -0
  70. package/skills/sophnet-schedule/src/gcal/models.py +91 -0
  71. package/skills/sophnet-schedule/src/requirements.txt +6 -0
  72. package/skills/sophnet-schedule/src/scripts/setup_gcal_token.py +85 -0
  73. package/skills/sophnet-schedule/src/server.py +669 -0
  74. package/skills/sophnet-schedule/src/services/__init__.py +0 -0
  75. package/skills/sophnet-schedule/src/services/calendar_backend.py +139 -0
  76. package/skills/sophnet-schedule/src/services/conflict_detector.py +96 -0
  77. package/skills/sophnet-schedule/src/services/datetime_utils.py +117 -0
  78. package/skills/sophnet-schedule/src/services/event_classifier.py +100 -0
  79. package/skills/sophnet-schedule/src/services/event_diff.py +160 -0
  80. package/skills/sophnet-schedule/src/services/google_integration.py +500 -0
  81. package/skills/sophnet-schedule/src/services/job_store.py +100 -0
  82. package/skills/sophnet-schedule/src/services/local_event_store.py +266 -0
  83. package/skills/sophnet-schedule/src/services/reminder_planner.py +116 -0
  84. package/skills/sophnet-schedule/src/services/runtime_utils.py +31 -0
  85. package/skills/sophnet-schedule/src/services/table_parser.py +286 -0
  86. package/skills/sophnet-schedule/src/services/task_builder.py +167 -0
  87. package/skills/sophnet-schedule/src/services/time_window.py +72 -0
  88. package/skills/sophnet-stock/skill.json +20 -0
  89. package/skills/sophnet-stock/src/App-Plan.md +442 -0
  90. package/skills/sophnet-stock/src/README.md +214 -0
  91. package/skills/sophnet-stock/src/SKILL.md +236 -0
  92. package/skills/sophnet-stock/src/TODO.md +394 -0
  93. package/skills/sophnet-stock/src/_meta.json +6 -0
  94. package/skills/sophnet-stock/src/docs/ARCHITECTURE.md +408 -0
  95. package/skills/sophnet-stock/src/docs/CONCEPT.md +233 -0
  96. package/skills/sophnet-stock/src/docs/HOT_SCANNER.md +288 -0
  97. package/skills/sophnet-stock/src/docs/README.md +95 -0
  98. package/skills/sophnet-stock/src/docs/USAGE.md +465 -0
  99. package/skills/sophnet-stock/src/scripts/analyze_stock.py +2565 -0
  100. package/skills/sophnet-stock/src/scripts/dividends.py +365 -0
  101. package/skills/sophnet-stock/src/scripts/hot_scanner.py +582 -0
  102. package/skills/sophnet-stock/src/scripts/portfolio.py +548 -0
  103. package/skills/sophnet-stock/src/scripts/rumor_scanner.py +342 -0
  104. package/skills/sophnet-stock/src/scripts/test_stock_analysis.py +409 -0
  105. package/skills/sophnet-stock/src/scripts/watchlist.py +336 -0
  106. package/skills/xiaohongshu/skill.json +20 -0
  107. package/skills/xiaohongshu/src/SKILL.md +91 -0
  108. package/skills/xiaohongshu/src/_meta.json +6 -0
  109. package/skills/xiaohongshu/src/assets/card.html +216 -0
  110. package/skills/xiaohongshu/src/assets/cover.html +82 -0
  111. package/skills/xiaohongshu/src/assets/example.md +84 -0
  112. package/skills/xiaohongshu/src/assets/styles.css +318 -0
  113. package/skills/xiaohongshu/src/scripts/render_xhs_v2.py +737 -0
  114. package/skills/xiaohongshu/src/scripts/sign_server.py +158 -0
  115. package/skills/xiaohongshu/src/scripts/stealth.min.js +7 -0
  116. package/skills/xiaohongshu/src/scripts/xhs_tool.py +186 -0
  117. package/skills/xiaohongshu/src/workflow.py +185 -0
@@ -0,0 +1,582 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ šŸ”„ HOT SCANNER v2 - Find viral stocks & crypto trends
4
+ Now with Twitter/X, Reddit, and improved Yahoo Finance
5
+ """
6
+
7
+ import json
8
+ import urllib.request
9
+ import urllib.error
10
+ import xml.etree.ElementTree as ET
11
+ import gzip
12
+ import io
13
+ import subprocess
14
+ import os
15
+ from datetime import datetime, timezone
16
+ from pathlib import Path
17
+ import re
18
+ import ssl
19
+ from collections import defaultdict
20
+ from concurrent.futures import ThreadPoolExecutor, as_completed
21
+
22
+ # Load .env file if exists
23
+ ENV_FILE = Path(__file__).parent.parent / ".env"
24
+ if ENV_FILE.exists():
25
+ with open(ENV_FILE) as f:
26
+ for line in f:
27
+ line = line.strip()
28
+ if line and not line.startswith("#") and "=" in line:
29
+ key, value = line.split("=", 1)
30
+ os.environ[key] = value
31
+
32
+ # Cache directory
33
+ CACHE_DIR = Path(__file__).parent.parent / "cache"
34
+ CACHE_DIR.mkdir(exist_ok=True)
35
+
36
+ # SSL context
37
+ SSL_CONTEXT = ssl.create_default_context()
38
+
39
+ class HotScanner:
40
+ def __init__(self, include_social=True):
41
+ self.include_social = include_social
42
+ self.results = {
43
+ "timestamp": datetime.now(timezone.utc).isoformat(),
44
+ "crypto": [],
45
+ "stocks": [],
46
+ "news": [],
47
+ "movers": [],
48
+ "social": []
49
+ }
50
+ self.mentions = defaultdict(lambda: {"count": 0, "sources": [], "sentiment_hints": []})
51
+ self.headers = {
52
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
53
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
54
+ "Accept-Language": "en-US,en;q=0.5",
55
+ "Accept-Encoding": "gzip, deflate",
56
+ }
57
+
58
+ def _fetch(self, url, timeout=15):
59
+ """Fetch URL with gzip support."""
60
+ req = urllib.request.Request(url, headers=self.headers)
61
+ with urllib.request.urlopen(req, timeout=timeout, context=SSL_CONTEXT) as resp:
62
+ data = resp.read()
63
+ # Handle gzip
64
+ if resp.info().get('Content-Encoding') == 'gzip' or data[:2] == b'\x1f\x8b':
65
+ data = gzip.decompress(data)
66
+ return data.decode('utf-8', errors='replace')
67
+
68
+ def _fetch_json(self, url, timeout=15):
69
+ """Fetch and parse JSON."""
70
+ return json.loads(self._fetch(url, timeout))
71
+
72
+ def scan_all(self):
73
+ """Run all scans in parallel."""
74
+ print("šŸ” Scanning for hot trends...\n")
75
+
76
+ tasks = [
77
+ ("CoinGecko Trending", self.scan_coingecko_trending),
78
+ ("CoinGecko Movers", self.scan_coingecko_gainers_losers),
79
+ ("Google News Finance", self.scan_google_news_finance),
80
+ ("Google News Crypto", self.scan_google_news_crypto),
81
+ ("Yahoo Movers", self.scan_yahoo_movers),
82
+ ]
83
+
84
+ if self.include_social:
85
+ tasks.extend([
86
+ ("Reddit WSB", self.scan_reddit_wsb),
87
+ ("Reddit Crypto", self.scan_reddit_crypto),
88
+ ("Twitter/X", self.scan_twitter),
89
+ ])
90
+
91
+ with ThreadPoolExecutor(max_workers=8) as executor:
92
+ futures = {executor.submit(task[1]): task[0] for task in tasks}
93
+ for future in as_completed(futures):
94
+ name = futures[future]
95
+ try:
96
+ future.result()
97
+ except Exception as e:
98
+ print(f" āŒ {name}: {str(e)[:50]}")
99
+
100
+ return self.results
101
+
102
+ def scan_coingecko_trending(self):
103
+ """Get trending crypto from CoinGecko."""
104
+ print(" šŸ“Š CoinGecko Trending...")
105
+ try:
106
+ url = "https://api.coingecko.com/api/v3/search/trending"
107
+ data = self._fetch_json(url)
108
+
109
+ for item in data.get("coins", [])[:10]:
110
+ coin = item.get("item", {})
111
+ price_data = coin.get("data", {})
112
+ price_change = price_data.get("price_change_percentage_24h", {}).get("usd", 0)
113
+
114
+ entry = {
115
+ "symbol": coin.get("symbol", "").upper(),
116
+ "name": coin.get("name", ""),
117
+ "rank": coin.get("market_cap_rank"),
118
+ "price_change_24h": round(price_change, 2) if price_change else None,
119
+ "source": "coingecko_trending"
120
+ }
121
+ self.results["crypto"].append(entry)
122
+
123
+ sym = entry["symbol"]
124
+ self.mentions[sym]["count"] += 2 # Trending gets extra weight
125
+ self.mentions[sym]["sources"].append("CoinGecko Trending")
126
+ if price_change:
127
+ direction = "šŸš€ bullish" if price_change > 0 else "šŸ“‰ bearish"
128
+ self.mentions[sym]["sentiment_hints"].append(f"{direction} ({price_change:+.1f}%)")
129
+
130
+ print(f" āœ… {len(data.get('coins', []))} trending coins")
131
+ except Exception as e:
132
+ print(f" āŒ CoinGecko trending: {e}")
133
+
134
+ def scan_coingecko_gainers_losers(self):
135
+ """Get top gainers/losers."""
136
+ print(" šŸ“ˆ CoinGecko Movers...")
137
+ try:
138
+ url = "https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=100&page=1&price_change_percentage=24h"
139
+ data = self._fetch_json(url)
140
+
141
+ sorted_data = sorted(data, key=lambda x: abs(x.get("price_change_percentage_24h") or 0), reverse=True)
142
+
143
+ count = 0
144
+ for coin in sorted_data[:20]:
145
+ change = coin.get("price_change_percentage_24h", 0)
146
+ if abs(change or 0) > 3:
147
+ entry = {
148
+ "symbol": coin.get("symbol", "").upper(),
149
+ "name": coin.get("name", ""),
150
+ "price": coin.get("current_price"),
151
+ "change_24h": round(change, 2) if change else None,
152
+ "volume": coin.get("total_volume"),
153
+ "source": "coingecko_movers"
154
+ }
155
+ self.results["movers"].append(entry)
156
+ count += 1
157
+
158
+ sym = entry["symbol"]
159
+ self.mentions[sym]["count"] += 1
160
+ self.mentions[sym]["sources"].append("CoinGecko Movers")
161
+ direction = "šŸš€ pumping" if change > 0 else "šŸ“‰ dumping"
162
+ self.mentions[sym]["sentiment_hints"].append(f"{direction} ({change:+.1f}%)")
163
+
164
+ print(f" āœ… {count} significant movers")
165
+ except Exception as e:
166
+ print(f" āŒ CoinGecko movers: {e}")
167
+
168
+ def scan_google_news_finance(self):
169
+ """Get finance news from Google News RSS."""
170
+ print(" šŸ“° Google News Finance...")
171
+ try:
172
+ # Business news topic
173
+ url = "https://news.google.com/rss/topics/CAAqJggKIiBDQkFTRWdvSUwyMHZNRGx6TVdZU0FtVnVHZ0pWVXlnQVAB?hl=en-US&gl=US&ceid=US:en"
174
+ text = self._fetch(url)
175
+ root = ET.fromstring(text)
176
+ items = root.findall(".//item")
177
+
178
+ for item in items[:15]:
179
+ title_elem = item.find("title")
180
+ title = title_elem.text if title_elem is not None else ""
181
+ tickers = self._extract_tickers(title)
182
+
183
+ news_entry = {
184
+ "title": title,
185
+ "tickers_mentioned": tickers,
186
+ "source": "google_news_finance"
187
+ }
188
+ self.results["news"].append(news_entry)
189
+
190
+ for ticker in tickers:
191
+ self.mentions[ticker]["count"] += 1
192
+ self.mentions[ticker]["sources"].append("Google News")
193
+ self.mentions[ticker]["sentiment_hints"].append(f"šŸ“° {title[:40]}...")
194
+
195
+ print(f" āœ… {len(items)} news items")
196
+ except Exception as e:
197
+ print(f" āŒ Google News Finance: {e}")
198
+
199
+ def scan_google_news_crypto(self):
200
+ """Search for crypto news."""
201
+ print(" šŸ“° Google News Crypto...")
202
+ try:
203
+ url = "https://news.google.com/rss/search?q=bitcoin+OR+ethereum+OR+crypto+crash+OR+crypto+pump&hl=en-US&gl=US&ceid=US:en"
204
+ text = self._fetch(url)
205
+ root = ET.fromstring(text)
206
+ items = root.findall(".//item")
207
+
208
+ crypto_keywords = {
209
+ "bitcoin": "BTC", "btc": "BTC", "ethereum": "ETH", "eth": "ETH",
210
+ "solana": "SOL", "xrp": "XRP", "ripple": "XRP", "dogecoin": "DOGE",
211
+ "cardano": "ADA", "polkadot": "DOT", "avalanche": "AVAX",
212
+ }
213
+
214
+ for item in items[:12]:
215
+ title_elem = item.find("title")
216
+ title = title_elem.text if title_elem is not None else ""
217
+ tickers = self._extract_tickers(title)
218
+
219
+ for word, ticker in crypto_keywords.items():
220
+ if word in title.lower():
221
+ tickers.append(ticker)
222
+ tickers = list(set(tickers))
223
+
224
+ if tickers:
225
+ news_entry = {
226
+ "title": title,
227
+ "tickers_mentioned": tickers,
228
+ "source": "google_news_crypto"
229
+ }
230
+ self.results["news"].append(news_entry)
231
+
232
+ for ticker in tickers:
233
+ self.mentions[ticker]["count"] += 1
234
+ self.mentions[ticker]["sources"].append("Google News Crypto")
235
+
236
+ print(f" āœ… Processed crypto news")
237
+ except Exception as e:
238
+ print(f" āŒ Google News Crypto: {e}")
239
+
240
+ def scan_yahoo_movers(self):
241
+ """Scrape Yahoo Finance movers with gzip support."""
242
+ print(" šŸ“ˆ Yahoo Finance Movers...")
243
+ categories = [
244
+ ("gainers", "https://finance.yahoo.com/gainers/"),
245
+ ("losers", "https://finance.yahoo.com/losers/"),
246
+ ("most_active", "https://finance.yahoo.com/most-active/")
247
+ ]
248
+
249
+ for category, url in categories:
250
+ try:
251
+ text = self._fetch(url, timeout=12)
252
+
253
+ # Multiple patterns for ticker extraction
254
+ tickers = []
255
+ # Pattern 1: data-symbol attribute
256
+ tickers.extend(re.findall(r'data-symbol="([A-Z]{1,5})"', text))
257
+ # Pattern 2: ticker in URL
258
+ tickers.extend(re.findall(r'/quote/([A-Z]{1,5})[/"\?]', text))
259
+ # Pattern 3: fin-streamer
260
+ tickers.extend(re.findall(r'fin-streamer[^>]*symbol="([A-Z]{1,5})"', text))
261
+
262
+ unique_tickers = list(dict.fromkeys(tickers))[:15]
263
+
264
+ for ticker in unique_tickers:
265
+ # Skip common false positives
266
+ if ticker in ['USA', 'CEO', 'IPO', 'ETF', 'SEC', 'FDA', 'NYSE', 'API']:
267
+ continue
268
+ self.results["stocks"].append({
269
+ "symbol": ticker,
270
+ "category": category,
271
+ "source": f"yahoo_{category}"
272
+ })
273
+ self.mentions[ticker]["count"] += 1
274
+ self.mentions[ticker]["sources"].append(f"Yahoo {category.replace('_', ' ').title()}")
275
+
276
+ if unique_tickers:
277
+ print(f" āœ… Yahoo {category}: {len(unique_tickers)} tickers")
278
+ except Exception as e:
279
+ print(f" āš ļø Yahoo {category}: {str(e)[:30]}")
280
+
281
+ def scan_reddit_wsb(self):
282
+ """Scrape r/wallstreetbets for hot stocks."""
283
+ print(" šŸ¦ Reddit r/wallstreetbets...")
284
+ try:
285
+ # Use old.reddit.com (more scrape-friendly)
286
+ url = "https://old.reddit.com/r/wallstreetbets/hot/.json"
287
+ headers = {**self.headers, "Accept": "application/json"}
288
+ req = urllib.request.Request(url, headers=headers)
289
+
290
+ with urllib.request.urlopen(req, timeout=15, context=SSL_CONTEXT) as resp:
291
+ data = resp.read()
292
+ if data[:2] == b'\x1f\x8b':
293
+ data = gzip.decompress(data)
294
+ posts = json.loads(data.decode('utf-8'))
295
+
296
+ tickers_found = []
297
+ for post in posts.get("data", {}).get("children", [])[:25]:
298
+ title = post.get("data", {}).get("title", "")
299
+ score = post.get("data", {}).get("score", 0)
300
+
301
+ # Extract tickers
302
+ tickers = self._extract_tickers(title)
303
+ for ticker in tickers:
304
+ if ticker not in ['USA', 'CEO', 'IPO', 'DD', 'WSB', 'YOLO', 'FD']:
305
+ weight = 2 if score > 1000 else 1
306
+ self.mentions[ticker]["count"] += weight
307
+ self.mentions[ticker]["sources"].append("Reddit WSB")
308
+ self.mentions[ticker]["sentiment_hints"].append(f"šŸ¦ WSB: {title[:35]}...")
309
+ tickers_found.append(ticker)
310
+
311
+ self.results["social"].append({
312
+ "platform": "reddit_wsb",
313
+ "title": title[:100],
314
+ "score": score,
315
+ "tickers": tickers
316
+ })
317
+
318
+ print(f" āœ… WSB: {len(set(tickers_found))} tickers mentioned")
319
+ except Exception as e:
320
+ print(f" āŒ Reddit WSB: {str(e)[:40]}")
321
+
322
+ def scan_reddit_crypto(self):
323
+ """Scrape r/cryptocurrency for hot coins."""
324
+ print(" šŸ’Ž Reddit r/cryptocurrency...")
325
+ try:
326
+ url = "https://old.reddit.com/r/cryptocurrency/hot/.json"
327
+ headers = {**self.headers, "Accept": "application/json"}
328
+ req = urllib.request.Request(url, headers=headers)
329
+
330
+ with urllib.request.urlopen(req, timeout=15, context=SSL_CONTEXT) as resp:
331
+ data = resp.read()
332
+ if data[:2] == b'\x1f\x8b':
333
+ data = gzip.decompress(data)
334
+ posts = json.loads(data.decode('utf-8'))
335
+
336
+ crypto_keywords = {
337
+ "bitcoin": "BTC", "btc": "BTC", "ethereum": "ETH", "eth": "ETH",
338
+ "solana": "SOL", "sol": "SOL", "xrp": "XRP", "cardano": "ADA",
339
+ "dogecoin": "DOGE", "doge": "DOGE", "shiba": "SHIB", "pepe": "PEPE",
340
+ "avalanche": "AVAX", "polkadot": "DOT", "chainlink": "LINK",
341
+ }
342
+
343
+ tickers_found = []
344
+ for post in posts.get("data", {}).get("children", [])[:20]:
345
+ title = post.get("data", {}).get("title", "").lower()
346
+ score = post.get("data", {}).get("score", 0)
347
+
348
+ for word, ticker in crypto_keywords.items():
349
+ if word in title:
350
+ weight = 2 if score > 500 else 1
351
+ self.mentions[ticker]["count"] += weight
352
+ self.mentions[ticker]["sources"].append("Reddit Crypto")
353
+ tickers_found.append(ticker)
354
+
355
+ print(f" āœ… r/crypto: {len(set(tickers_found))} coins mentioned")
356
+ except Exception as e:
357
+ print(f" āŒ Reddit Crypto: {str(e)[:40]}")
358
+
359
+ def scan_twitter(self):
360
+ """Use bird CLI to get trending finance/crypto tweets."""
361
+ print(" 🐦 Twitter/X...")
362
+ try:
363
+ # Find bird binary
364
+ bird_paths = [
365
+ "/home/clawdbot/.nvm/versions/node/v24.12.0/bin/bird",
366
+ "/usr/local/bin/bird",
367
+ "bird"
368
+ ]
369
+ bird_bin = None
370
+ for p in bird_paths:
371
+ if Path(p).exists() or p == "bird":
372
+ bird_bin = p
373
+ break
374
+
375
+ if not bird_bin:
376
+ print(" āš ļø Twitter: bird not found")
377
+ return
378
+
379
+ # Search for finance tweets
380
+ searches = [
381
+ ("stocks", "stock OR $SPY OR $QQQ OR earnings"),
382
+ ("crypto", "bitcoin OR ethereum OR crypto OR $BTC"),
383
+ ]
384
+
385
+ for category, query in searches:
386
+ try:
387
+ env = os.environ.copy()
388
+ result = subprocess.run(
389
+ [bird_bin, "search", query, "-n", "15", "--json"],
390
+ capture_output=True, text=True, timeout=30, env=env
391
+ )
392
+
393
+ if result.returncode == 0 and result.stdout.strip():
394
+ tweets = json.loads(result.stdout)
395
+ for tweet in tweets[:10]:
396
+ text = tweet.get("text", "")
397
+ tickers = self._extract_tickers(text)
398
+
399
+ # Add crypto keywords
400
+ crypto_map = {"bitcoin": "BTC", "ethereum": "ETH", "solana": "SOL"}
401
+ for word, ticker in crypto_map.items():
402
+ if word in text.lower():
403
+ tickers.append(ticker)
404
+
405
+ for ticker in set(tickers):
406
+ self.mentions[ticker]["count"] += 1
407
+ self.mentions[ticker]["sources"].append("Twitter/X")
408
+ self.mentions[ticker]["sentiment_hints"].append(f"🐦 {text[:35]}...")
409
+
410
+ self.results["social"].append({
411
+ "platform": "twitter",
412
+ "text": text[:100],
413
+ "tickers": list(set(tickers))
414
+ })
415
+
416
+ print(f" āœ… Twitter {category}: processed")
417
+ except subprocess.TimeoutExpired:
418
+ print(f" āš ļø Twitter {category}: timeout")
419
+ except json.JSONDecodeError:
420
+ print(f" āš ļø Twitter {category}: no auth?")
421
+ except FileNotFoundError:
422
+ print(" āš ļø Twitter: bird CLI not found")
423
+ except Exception as e:
424
+ print(f" āŒ Twitter: {str(e)[:40]}")
425
+
426
+ def _extract_tickers(self, text):
427
+ """Extract stock/crypto tickers from text."""
428
+ patterns = [
429
+ r'\$([A-Z]{1,5})\b', # $AAPL
430
+ r'\(([A-Z]{2,5})\)', # (AAPL)
431
+ r'(?:^|\s)([A-Z]{2,4})(?:\s|$|[,.])', # Standalone caps
432
+ ]
433
+
434
+ tickers = []
435
+ for pattern in patterns:
436
+ matches = re.findall(pattern, text)
437
+ tickers.extend(matches)
438
+
439
+ # Company mappings
440
+ companies = {
441
+ "Apple": "AAPL", "Microsoft": "MSFT", "Google": "GOOGL", "Alphabet": "GOOGL",
442
+ "Amazon": "AMZN", "Tesla": "TSLA", "Nvidia": "NVDA", "Meta": "META",
443
+ "Netflix": "NFLX", "GameStop": "GME", "AMD": "AMD", "Intel": "INTC",
444
+ "Palantir": "PLTR", "Coinbase": "COIN", "MicroStrategy": "MSTR",
445
+ }
446
+
447
+ for company, ticker in companies.items():
448
+ if company.lower() in text.lower():
449
+ tickers.append(ticker)
450
+
451
+ # Filter out common words
452
+ skip = {'USA', 'CEO', 'IPO', 'ETF', 'SEC', 'FDA', 'NYSE', 'API', 'USD', 'EU',
453
+ 'UK', 'US', 'AI', 'IT', 'AT', 'TO', 'IN', 'ON', 'IS', 'IF', 'OR', 'AN',
454
+ 'DD', 'WSB', 'YOLO', 'FD', 'OP', 'PM', 'AM'}
455
+
456
+ return list(set(t for t in tickers if t not in skip and len(t) >= 2))
457
+
458
+ def get_hot_summary(self):
459
+ """Generate summary."""
460
+ sorted_mentions = sorted(
461
+ self.mentions.items(),
462
+ key=lambda x: x[1]["count"],
463
+ reverse=True
464
+ )
465
+
466
+ summary = {
467
+ "scan_time": self.results["timestamp"],
468
+ "top_trending": [],
469
+ "crypto_highlights": [],
470
+ "stock_highlights": [],
471
+ "social_buzz": [],
472
+ "breaking_news": []
473
+ }
474
+
475
+ for symbol, data in sorted_mentions[:20]:
476
+ summary["top_trending"].append({
477
+ "symbol": symbol,
478
+ "mentions": data["count"],
479
+ "sources": list(set(data["sources"])),
480
+ "signals": data["sentiment_hints"][:3]
481
+ })
482
+
483
+ # Crypto
484
+ seen = set()
485
+ for coin in self.results["crypto"] + self.results["movers"]:
486
+ if coin["symbol"] not in seen:
487
+ summary["crypto_highlights"].append(coin)
488
+ seen.add(coin["symbol"])
489
+
490
+ # Stocks
491
+ seen = set()
492
+ for stock in self.results["stocks"]:
493
+ if stock["symbol"] not in seen:
494
+ summary["stock_highlights"].append(stock)
495
+ seen.add(stock["symbol"])
496
+
497
+ # Social
498
+ for item in self.results["social"][:15]:
499
+ summary["social_buzz"].append(item)
500
+
501
+ # News
502
+ for news in self.results["news"][:10]:
503
+ if news.get("tickers_mentioned"):
504
+ summary["breaking_news"].append({
505
+ "title": news["title"],
506
+ "tickers": news["tickers_mentioned"]
507
+ })
508
+
509
+ return summary
510
+
511
+
512
+ def main():
513
+ import argparse
514
+ parser = argparse.ArgumentParser(description="šŸ”„ Hot Scanner - Find trending stocks & crypto")
515
+ parser.add_argument("--no-social", action="store_true", help="Skip social media scans")
516
+ parser.add_argument("--json", action="store_true", help="Output only JSON")
517
+ args = parser.parse_args()
518
+
519
+ scanner = HotScanner(include_social=not args.no_social)
520
+
521
+ if not args.json:
522
+ print("=" * 60)
523
+ print("šŸ”„ HOT SCANNER v2 - What's Trending Right Now?")
524
+ print(f"šŸ“… {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} UTC")
525
+ print("=" * 60)
526
+ print()
527
+
528
+ scanner.scan_all()
529
+ summary = scanner.get_hot_summary()
530
+
531
+ # Save
532
+ output_file = CACHE_DIR / "hot_scan_latest.json"
533
+ with open(output_file, "w") as f:
534
+ json.dump(summary, f, indent=2, default=str)
535
+
536
+ if args.json:
537
+ print(json.dumps(summary, indent=2, default=str))
538
+ return
539
+
540
+ print()
541
+ print("=" * 60)
542
+ print("šŸ”„ RESULTS")
543
+ print("=" * 60)
544
+
545
+ print("\nšŸ“Š TOP TRENDING (by buzz):\n")
546
+ for i, item in enumerate(summary["top_trending"][:12], 1):
547
+ sources = ", ".join(item["sources"][:2])
548
+ signal = item["signals"][0][:30] if item["signals"] else ""
549
+ print(f" {i:2}. {item['symbol']:8} ({item['mentions']:2} pts) [{sources}] {signal}")
550
+
551
+ print("\nšŸŖ™ CRYPTO:\n")
552
+ for coin in summary["crypto_highlights"][:8]:
553
+ change = coin.get("change_24h") or coin.get("price_change_24h")
554
+ change_str = f"{change:+.1f}%" if change else "šŸ”„"
555
+ emoji = "šŸš€" if (change or 0) > 0 else "šŸ“‰" if (change or 0) < 0 else "šŸ”„"
556
+ print(f" {emoji} {coin.get('symbol', '?'):8} {coin.get('name', '')[:16]:16} {change_str:>8}")
557
+
558
+ print("\nšŸ“ˆ STOCKS:\n")
559
+ cat_emoji = {"gainers": "🟢", "losers": "šŸ”“", "most_active": "šŸ“Š"}
560
+ for stock in summary["stock_highlights"][:10]:
561
+ emoji = cat_emoji.get(stock.get("category"), "•")
562
+ print(f" {emoji} {stock['symbol']:6} ({stock.get('category', 'N/A').replace('_', ' ')})")
563
+
564
+ if summary["social_buzz"]:
565
+ print("\n🐦 SOCIAL BUZZ:\n")
566
+ for item in summary["social_buzz"][:5]:
567
+ platform = item.get("platform", "?")
568
+ text = item.get("title") or item.get("text", "")
569
+ text = text[:55] + "..." if len(text) > 55 else text
570
+ print(f" [{platform}] {text}")
571
+
572
+ print("\nšŸ“° NEWS:\n")
573
+ for news in summary["breaking_news"][:5]:
574
+ tickers = ", ".join(news["tickers"][:3])
575
+ title = news["title"][:55] + "..." if len(news["title"]) > 55 else news["title"]
576
+ print(f" [{tickers}] {title}")
577
+
578
+ print(f"\nšŸ’¾ Saved: {output_file}\n")
579
+
580
+
581
+ if __name__ == "__main__":
582
+ main()