Dmitry Beresnev commited on
Commit
ef06dad
Β·
1 Parent(s): 4d33e57

add fix cache module, add timeframes, etc

Browse files
src/core/ticker_scanner/core_enums.py CHANGED
@@ -2,18 +2,30 @@ from enum import Enum
2
 
3
 
4
  class StockExchange(Enum):
 
5
  NYSE = "NYSE" # New York Stock Exchange
6
  NASDAQ = "NASDAQ" # NASDAQ
 
 
 
7
  LSE = "LSE" # London Stock Exchange
 
 
 
 
 
8
  TSE = "TSE" # Tokyo Stock Exchange
9
- SSE = "SSE" # Shanghai Stock Exchange
10
  HKEX = "HKEX" # Hong Kong Stock Exchange
 
11
  BSE = "BSE" # Bombay Stock Exchange
12
  NSE = "NSE" # National Stock Exchange of India
 
 
13
  ASX = "ASX" # Australian Securities Exchange
14
  TSX = "TSX" # Toronto Stock Exchange
15
- SIX = "SIX" # Swiss Exchange
16
- FWB = "FWB" # Frankfurt Stock Exchange
 
17
 
18
 
19
  class GrowthCategory(Enum):
 
2
 
3
 
4
  class StockExchange(Enum):
5
+ # American Exchanges
6
  NYSE = "NYSE" # New York Stock Exchange
7
  NASDAQ = "NASDAQ" # NASDAQ
8
+ AMEX = "AMEX" # American Stock Exchange (NYSE American)
9
+
10
+ # European Exchanges
11
  LSE = "LSE" # London Stock Exchange
12
+ EURONEXT = "EURONEXT" # Euronext (Paris, Amsterdam, Brussels)
13
+ SIX = "SIX" # Swiss Exchange
14
+ FWB = "FWB" # Frankfurt Stock Exchange (Deutsche BΓΆrse)
15
+
16
+ # Asian Exchanges
17
  TSE = "TSE" # Tokyo Stock Exchange
 
18
  HKEX = "HKEX" # Hong Kong Stock Exchange
19
+ SSE = "SSE" # Shanghai Stock Exchange
20
  BSE = "BSE" # Bombay Stock Exchange
21
  NSE = "NSE" # National Stock Exchange of India
22
+
23
+ # Others
24
  ASX = "ASX" # Australian Securities Exchange
25
  TSX = "TSX" # Toronto Stock Exchange
26
+
27
+ # Special Categories
28
+ ETF = "ETF" # Exchange-Traded Funds
29
 
30
 
31
  class GrowthCategory(Enum):
src/core/ticker_scanner/parallel_data_downloader.py CHANGED
@@ -40,14 +40,15 @@ def get_cache_stats() -> dict[str, Any]:
40
  return _cache.get_stats()
41
 
42
 
43
- def fetch_prices(ticker: str, max_retries: int = MAX_RETRIES, use_cache: bool = False) -> Optional[dict[str, Any]]:
44
  """
45
- Download all-time closing prices for a single ticker safely.
46
 
47
  Args:
48
  ticker: Stock ticker symbol
49
  max_retries: Maximum number of retry attempts
50
  use_cache: Whether to use cached data (NOTE: typically False when called from subprocess)
 
51
 
52
  Returns:
53
  dict {'ticker': ticker, 'prices': ndarray, 'dates': DatetimeIndex} or None if failed
@@ -55,7 +56,7 @@ def fetch_prices(ticker: str, max_retries: int = MAX_RETRIES, use_cache: bool =
55
  # Download fresh data (cache is handled in main process)
56
  for attempt in range(max_retries):
57
  try:
58
- df = yf.download(ticker, period="max", progress=False, auto_adjust=True)
59
 
60
  # Handle empty or invalid dataframes
61
  if df is None or df.empty:
@@ -111,7 +112,8 @@ def batch(iterable: list[str], n: int = BATCH_SIZE):
111
 
112
  def download_tickers_parallel(tickers: list[str], exchange: str,
113
  max_workers: int = MAX_WORKERS,
114
- use_cache: bool = True) -> list[dict[str, Any]]:
 
115
  """
116
  Download a large list of tickers in parallel batches.
117
  Uses in-memory cache to avoid re-downloading recently fetched data.
@@ -121,6 +123,7 @@ def download_tickers_parallel(tickers: list[str], exchange: str,
121
  exchange: Exchange name (e.g., "NASDAQ", "NYSE")
122
  max_workers: Number of parallel workers
123
  use_cache: Whether to use cached data
 
124
 
125
  Returns:
126
  List of {'ticker': ..., 'prices': ..., 'dates': ...} dicts
@@ -131,14 +134,14 @@ def download_tickers_parallel(tickers: list[str], exchange: str,
131
 
132
  if use_cache:
133
  for ticker in tickers:
134
- cached_data = _cache.get(exchange, ticker)
135
  if cached_data:
136
  cached_results.append(cached_data)
137
  else:
138
  tickers_to_download.append(ticker)
139
 
140
  if cached_results:
141
- logger.info(f"Using cached data for {len(cached_results)} tickers")
142
  else:
143
  tickers_to_download = tickers
144
 
@@ -150,7 +153,7 @@ def download_tickers_parallel(tickers: list[str], exchange: str,
150
  logger.info(f"Downloading {len(tickers_to_download)} tickers...")
151
  for batch_num, ticker_batch in enumerate(batch(tickers_to_download, BATCH_SIZE), start=1):
152
  logger.info(f"Processing batch {batch_num}: {len(ticker_batch)} tickers")
153
- results, failed = process_batch(ticker_batch, exchange, max_workers)
154
  all_results.extend(results)
155
  all_failed.extend(failed)
156
  # small sleep between batches to reduce rate-limit chance
@@ -162,7 +165,7 @@ def download_tickers_parallel(tickers: list[str], exchange: str,
162
 
163
  return all_results
164
 
165
- def process_batch(ticker_batch: list[str], exchange: str, max_workers: int) -> tuple[list[dict[str, Any]], list[Any]]:
166
  """
167
  Process a batch of tickers in parallel using multiprocessing.
168
  Returns tuple (successful_results, failed_tickers)
@@ -171,6 +174,7 @@ def process_batch(ticker_batch: list[str], exchange: str, max_workers: int) -> t
171
  ticker_batch: List of ticker symbols to process
172
  exchange: Exchange name for cache key
173
  max_workers: Number of parallel workers
 
174
 
175
  Note: Downloads always fetch fresh data (cache checked before this step)
176
  """
@@ -178,14 +182,14 @@ def process_batch(ticker_batch: list[str], exchange: str, max_workers: int) -> t
178
  failed = []
179
  with ProcessPoolExecutor(max_workers=max_workers) as executor:
180
  # Don't use cache in subprocess - already handled in main process
181
- futures = {executor.submit(fetch_prices, t, use_cache=False): t for t in ticker_batch}
182
  for future in as_completed(futures):
183
  ticker = futures[future]
184
  try:
185
  res = future.result()
186
  if res:
187
  # Cache the result in the main process after download
188
- _cache.set(exchange, res['ticker'], res)
189
  results.append(res)
190
  else:
191
  failed.append(ticker)
@@ -195,7 +199,8 @@ def process_batch(ticker_batch: list[str], exchange: str, max_workers: int) -> t
195
 
196
  def run_parallel_data_downloader(exchange: StockExchange = StockExchange.NASDAQ,
197
  limit: int = 200,
198
- use_cache: bool = True) -> list[dict[str, Any]]:
 
199
  """
200
  Main function to download ticker data in parallel with caching.
201
 
@@ -203,6 +208,7 @@ def run_parallel_data_downloader(exchange: StockExchange = StockExchange.NASDAQ,
203
  exchange: Stock exchange to download from
204
  limit: Maximum number of tickers to download
205
  use_cache: Whether to use cached data (expires after 2 hours)
 
206
 
207
  Returns:
208
  List of dicts with ticker, prices, and dates
@@ -212,10 +218,13 @@ def run_parallel_data_downloader(exchange: StockExchange = StockExchange.NASDAQ,
212
 
213
  # Log cache stats
214
  cache_stats = get_cache_stats()
215
- logger.info(f"Cache stats: {cache_stats['valid_cached']} valid, {cache_stats['expired_cached']} expired")
 
 
 
216
 
217
- logger.info(f"Starting download for {len(tickers)} tickers from {exchange.value}...")
218
- data = download_tickers_parallel(tickers, exchange.value, use_cache=use_cache)
219
  logger.info(f"Retrieved {len(data)} tickers successfully")
220
  return data
221
 
 
40
  return _cache.get_stats()
41
 
42
 
43
+ def fetch_prices(ticker: str, max_retries: int = MAX_RETRIES, use_cache: bool = False, period: str = "max") -> Optional[dict[str, Any]]:
44
  """
45
+ Download historical closing prices for a single ticker safely.
46
 
47
  Args:
48
  ticker: Stock ticker symbol
49
  max_retries: Maximum number of retry attempts
50
  use_cache: Whether to use cached data (NOTE: typically False when called from subprocess)
51
+ period: Timeframe for historical data (1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, 10y, ytd, max)
52
 
53
  Returns:
54
  dict {'ticker': ticker, 'prices': ndarray, 'dates': DatetimeIndex} or None if failed
 
56
  # Download fresh data (cache is handled in main process)
57
  for attempt in range(max_retries):
58
  try:
59
+ df = yf.download(ticker, period=period, progress=False, auto_adjust=True)
60
 
61
  # Handle empty or invalid dataframes
62
  if df is None or df.empty:
 
112
 
113
  def download_tickers_parallel(tickers: list[str], exchange: str,
114
  max_workers: int = MAX_WORKERS,
115
+ use_cache: bool = True,
116
+ period: str = "max") -> list[dict[str, Any]]:
117
  """
118
  Download a large list of tickers in parallel batches.
119
  Uses in-memory cache to avoid re-downloading recently fetched data.
 
123
  exchange: Exchange name (e.g., "NASDAQ", "NYSE")
124
  max_workers: Number of parallel workers
125
  use_cache: Whether to use cached data
126
+ period: Timeframe for historical data (1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, 10y, ytd, max)
127
 
128
  Returns:
129
  List of {'ticker': ..., 'prices': ..., 'dates': ...} dicts
 
134
 
135
  if use_cache:
136
  for ticker in tickers:
137
+ cached_data = _cache.get(exchange, ticker, period)
138
  if cached_data:
139
  cached_results.append(cached_data)
140
  else:
141
  tickers_to_download.append(ticker)
142
 
143
  if cached_results:
144
+ logger.info(f"Using cached data for {len(cached_results)} tickers (timeframe: {period})")
145
  else:
146
  tickers_to_download = tickers
147
 
 
153
  logger.info(f"Downloading {len(tickers_to_download)} tickers...")
154
  for batch_num, ticker_batch in enumerate(batch(tickers_to_download, BATCH_SIZE), start=1):
155
  logger.info(f"Processing batch {batch_num}: {len(ticker_batch)} tickers")
156
+ results, failed = process_batch(ticker_batch, exchange, max_workers, period)
157
  all_results.extend(results)
158
  all_failed.extend(failed)
159
  # small sleep between batches to reduce rate-limit chance
 
165
 
166
  return all_results
167
 
168
+ def process_batch(ticker_batch: list[str], exchange: str, max_workers: int, period: str = "max") -> tuple[list[dict[str, Any]], list[Any]]:
169
  """
170
  Process a batch of tickers in parallel using multiprocessing.
171
  Returns tuple (successful_results, failed_tickers)
 
174
  ticker_batch: List of ticker symbols to process
175
  exchange: Exchange name for cache key
176
  max_workers: Number of parallel workers
177
+ period: Timeframe for historical data (1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, 10y, ytd, max)
178
 
179
  Note: Downloads always fetch fresh data (cache checked before this step)
180
  """
 
182
  failed = []
183
  with ProcessPoolExecutor(max_workers=max_workers) as executor:
184
  # Don't use cache in subprocess - already handled in main process
185
+ futures = {executor.submit(fetch_prices, t, use_cache=False, period=period): t for t in ticker_batch}
186
  for future in as_completed(futures):
187
  ticker = futures[future]
188
  try:
189
  res = future.result()
190
  if res:
191
  # Cache the result in the main process after download
192
+ _cache.set(exchange, res['ticker'], res, period)
193
  results.append(res)
194
  else:
195
  failed.append(ticker)
 
199
 
200
  def run_parallel_data_downloader(exchange: StockExchange = StockExchange.NASDAQ,
201
  limit: int = 200,
202
+ use_cache: bool = True,
203
+ timeframe: str = "max") -> list[dict[str, Any]]:
204
  """
205
  Main function to download ticker data in parallel with caching.
206
 
 
208
  exchange: Stock exchange to download from
209
  limit: Maximum number of tickers to download
210
  use_cache: Whether to use cached data (expires after 2 hours)
211
+ timeframe: Historical data timeframe (1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, 10y, ytd, max)
212
 
213
  Returns:
214
  List of dicts with ticker, prices, and dates
 
218
 
219
  # Log cache stats
220
  cache_stats = get_cache_stats()
221
+ logger.info(
222
+ f"Cache stats: {cache_stats['valid_cached']} valid, {cache_stats['expired_cached']} expired, "
223
+ f"{cache_stats['size_gb']} GB / {cache_stats['max_size_gb']} GB ({cache_stats['usage_percent']}%)"
224
+ )
225
 
226
+ logger.info(f"Starting download for {len(tickers)} tickers from {exchange.value} (timeframe: {timeframe})...")
227
+ data = download_tickers_parallel(tickers, exchange.value, use_cache=use_cache, period=timeframe)
228
  logger.info(f"Retrieved {len(data)} tickers successfully")
229
  return data
230
 
src/core/ticker_scanner/ticker_analyzer.py CHANGED
@@ -6,6 +6,8 @@ Coordinates data downloading, growth analysis, ranking, and Telegram notificatio
6
  from typing import Any
7
  from datetime import datetime
8
 
 
 
9
  from src.core.ticker_scanner.core_enums import StockExchange
10
  from src.core.ticker_scanner.parallel_data_downloader import run_parallel_data_downloader
11
  from src.core.ticker_scanner.growth_speed_analyzer import GrowthSpeedAnalyzer
@@ -19,7 +21,7 @@ class TickerAnalyzer:
19
  Manages the complete workflow: download -> analyze -> rank -> notify
20
  """
21
 
22
- def __init__(self, exchange: str = "NASDAQ", telegram_bot_service=None, limit: int = 200):
23
  """
24
  Initialize the analyzer.
25
 
@@ -27,10 +29,12 @@ class TickerAnalyzer:
27
  exchange: Stock exchange name (NASDAQ, NYSE, etc.)
28
  telegram_bot_service: Optional Telegram service for notifications
29
  limit: Maximum number of tickers to analyze
 
30
  """
31
  self.exchange = StockExchange[exchange]
32
  self.telegram_bot_service = telegram_bot_service
33
  self.limit = limit
 
34
  self.growth_analyzer = GrowthSpeedAnalyzer()
35
 
36
  async def run_analysis(self, top_tickers_max_count: int = 10) -> list[dict[str, Any]]:
@@ -43,8 +47,8 @@ class TickerAnalyzer:
43
  logger.info(f"Starting ticker analysis for {self.exchange.value}")
44
 
45
  # Step 1: Download ticker data in parallel
46
- logger.info("Step 1: Downloading ticker data...")
47
- ticker_data = run_parallel_data_downloader(self.exchange, self.limit)
48
  logger.info(f"Downloaded {len(ticker_data)} tickers")
49
 
50
  # Step 2: Analyze growth metrics for each ticker
@@ -156,6 +160,41 @@ class TickerAnalyzer:
156
  # TODO: Implement actual Telegram sending when chat_id is configured
157
  # await self.telegram_bot_service.send_message_via_proxy(chat_id, message)
158
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  def _format_telegram_message(self, top_tickers: list[dict[str, Any]]) -> str:
160
  """
161
  Format top tickers as a Telegram message with TradingView links.
@@ -169,8 +208,34 @@ class TickerAnalyzer:
169
  timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
170
 
171
  message = f"πŸ“Š <b>Top {len(top_tickers)} Growing Tickers - {self.exchange.value}</b>\n"
 
172
  message += f"πŸ• {timestamp}\n\n"
173
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
  for i, ticker_data in enumerate(top_tickers, 1):
175
  ticker = ticker_data['ticker']
176
  metrics = ticker_data['metrics']
@@ -216,8 +281,28 @@ class TickerAnalyzer:
216
  """
217
  # Map exchange enum to TradingView exchange code
218
  exchange_map = {
 
219
  StockExchange.NASDAQ: "NASDAQ",
220
  StockExchange.NYSE: "NYSE",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
  }
222
 
223
  tv_exchange = exchange_map.get(self.exchange, self.exchange.value)
@@ -242,8 +327,28 @@ class TickerAnalyzer:
242
  """
243
  # Map exchange enum to TradingView exchange code
244
  exchange_map = {
 
245
  StockExchange.NASDAQ: "NASDAQ",
246
  StockExchange.NYSE: "NYSE",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
  }
248
 
249
  tv_exchange = exchange_map.get(self.exchange, self.exchange.value)
 
6
  from typing import Any
7
  from datetime import datetime
8
 
9
+ import yfinance as yf
10
+
11
  from src.core.ticker_scanner.core_enums import StockExchange
12
  from src.core.ticker_scanner.parallel_data_downloader import run_parallel_data_downloader
13
  from src.core.ticker_scanner.growth_speed_analyzer import GrowthSpeedAnalyzer
 
21
  Manages the complete workflow: download -> analyze -> rank -> notify
22
  """
23
 
24
+ def __init__(self, exchange: str = "NASDAQ", telegram_bot_service=None, limit: int = 200, timeframe: str = "max"):
25
  """
26
  Initialize the analyzer.
27
 
 
29
  exchange: Stock exchange name (NASDAQ, NYSE, etc.)
30
  telegram_bot_service: Optional Telegram service for notifications
31
  limit: Maximum number of tickers to analyze
32
+ timeframe: Historical data timeframe (1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, 10y, ytd, max)
33
  """
34
  self.exchange = StockExchange[exchange]
35
  self.telegram_bot_service = telegram_bot_service
36
  self.limit = limit
37
+ self.timeframe = timeframe
38
  self.growth_analyzer = GrowthSpeedAnalyzer()
39
 
40
  async def run_analysis(self, top_tickers_max_count: int = 10) -> list[dict[str, Any]]:
 
47
  logger.info(f"Starting ticker analysis for {self.exchange.value}")
48
 
49
  # Step 1: Download ticker data in parallel
50
+ logger.info(f"Step 1: Downloading ticker data (timeframe: {self.timeframe})...")
51
+ ticker_data = run_parallel_data_downloader(self.exchange, self.limit, timeframe=self.timeframe)
52
  logger.info(f"Downloaded {len(ticker_data)} tickers")
53
 
54
  # Step 2: Analyze growth metrics for each ticker
 
160
  # TODO: Implement actual Telegram sending when chat_id is configured
161
  # await self.telegram_bot_service.send_message_via_proxy(chat_id, message)
162
 
163
+ def _get_market_indicators(self) -> dict[str, Any]:
164
+ """
165
+ Fetch real-time market indicators.
166
+
167
+ Returns:
168
+ Dictionary with indicator values and status
169
+ """
170
+ indicators = {
171
+ 'vix': None,
172
+ 'vix_status': 'Unknown'
173
+ }
174
+
175
+ try:
176
+ # Fetch VIX from Yahoo Finance
177
+ vix_ticker = yf.Ticker("^VIX")
178
+ vix_data = vix_ticker.history(period="1d")
179
+
180
+ if not vix_data.empty:
181
+ vix_value = vix_data['Close'].iloc[-1]
182
+ indicators['vix'] = vix_value
183
+
184
+ # Classify VIX level
185
+ if vix_value < 12:
186
+ indicators['vix_status'] = '🟒 Low (Complacent)'
187
+ elif vix_value < 20:
188
+ indicators['vix_status'] = '🟑 Normal'
189
+ elif vix_value < 30:
190
+ indicators['vix_status'] = '🟠 Elevated'
191
+ else:
192
+ indicators['vix_status'] = 'πŸ”΄ High (Fear)'
193
+ except Exception as e:
194
+ logger.warning(f"Failed to fetch VIX: {e}")
195
+
196
+ return indicators
197
+
198
  def _format_telegram_message(self, top_tickers: list[dict[str, Any]]) -> str:
199
  """
200
  Format top tickers as a Telegram message with TradingView links.
 
208
  timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
209
 
210
  message = f"πŸ“Š <b>Top {len(top_tickers)} Growing Tickers - {self.exchange.value}</b>\n"
211
+ message += f"⏱️ Timeframe: <b>{self.timeframe}</b>\n"
212
  message += f"πŸ• {timestamp}\n\n"
213
 
214
+ # Market Indicators Section
215
+ message += "πŸ“ˆ <b>Market Indicators</b>\n"
216
+
217
+ # Get real-time indicators
218
+ indicators = self._get_market_indicators()
219
+
220
+ # VIX
221
+ if indicators['vix'] is not None:
222
+ message += f"β€’ <b>VIX:</b> {indicators['vix']:.2f} - {indicators['vix_status']}\n"
223
+ message += f" <a href='https://www.tradingview.com/symbols/CBOE-VIX/'>πŸ“Š Chart</a>\n"
224
+ else:
225
+ message += f"β€’ <b>VIX:</b> <a href='https://www.tradingview.com/symbols/CBOE-VIX/'>πŸ“Š Chart</a>\n"
226
+
227
+ # Fear & Greed Index
228
+ message += f"β€’ <b>Fear & Greed:</b> <a href='https://www.feargreedmeter.com/'>πŸ“Š Meter</a>\n"
229
+
230
+ # FRED Financial Stress Index
231
+ message += f"β€’ <b>Financial Stress (FRED):</b> <a href='https://www.tradingview.com/symbols/ECONOMICS-STLFSI4/'>πŸ“Š STLFSI4</a>\n"
232
+
233
+ # CME FedWatch Tool
234
+ message += f"β€’ <b>Fed Rates (CME):</b> <a href='https://www.cmegroup.com/markets/interest-rates/cme-fedwatch-tool.html'>πŸ“Š FedWatch</a>\n\n"
235
+
236
+ # Top Tickers
237
+ message += f"πŸ† <b>Top Growing Tickers</b>\n\n"
238
+
239
  for i, ticker_data in enumerate(top_tickers, 1):
240
  ticker = ticker_data['ticker']
241
  metrics = ticker_data['metrics']
 
281
  """
282
  # Map exchange enum to TradingView exchange code
283
  exchange_map = {
284
+ # American Exchanges
285
  StockExchange.NASDAQ: "NASDAQ",
286
  StockExchange.NYSE: "NYSE",
287
+ StockExchange.AMEX: "AMEX",
288
+
289
+ # European Exchanges
290
+ StockExchange.LSE: "LSE", # London
291
+ StockExchange.EURONEXT: "EURONEXT", # Paris/Amsterdam/Brussels
292
+ StockExchange.FWB: "FWB", # Frankfurt
293
+ StockExchange.SIX: "SIX", # Swiss
294
+
295
+ # Asian Exchanges
296
+ StockExchange.TSE: "TSE", # Tokyo
297
+ StockExchange.HKEX: "HKEX", # Hong Kong
298
+ StockExchange.SSE: "SSE", # Shanghai
299
+
300
+ # Others
301
+ StockExchange.TSX: "TSX", # Toronto
302
+ StockExchange.ASX: "ASX", # Australia
303
+
304
+ # Special Categories
305
+ StockExchange.ETF: "NASDAQ", # ETFs traded on US exchanges
306
  }
307
 
308
  tv_exchange = exchange_map.get(self.exchange, self.exchange.value)
 
327
  """
328
  # Map exchange enum to TradingView exchange code
329
  exchange_map = {
330
+ # American Exchanges
331
  StockExchange.NASDAQ: "NASDAQ",
332
  StockExchange.NYSE: "NYSE",
333
+ StockExchange.AMEX: "AMEX",
334
+
335
+ # European Exchanges
336
+ StockExchange.LSE: "LSE", # London
337
+ StockExchange.EURONEXT: "EURONEXT", # Paris/Amsterdam/Brussels
338
+ StockExchange.FWB: "FWB", # Frankfurt
339
+ StockExchange.SIX: "SIX", # Swiss
340
+
341
+ # Asian Exchanges
342
+ StockExchange.TSE: "TSE", # Tokyo
343
+ StockExchange.HKEX: "HKEX", # Hong Kong
344
+ StockExchange.SSE: "SSE", # Shanghai
345
+
346
+ # Others
347
+ StockExchange.TSX: "TSX", # Toronto
348
+ StockExchange.ASX: "ASX", # Australia
349
+
350
+ # Special Categories
351
+ StockExchange.ETF: "NASDAQ", # ETFs traded on US exchanges
352
  }
353
 
354
  tv_exchange = exchange_map.get(self.exchange, self.exchange.value)
src/core/ticker_scanner/ticker_cache.py CHANGED
@@ -1,70 +1,202 @@
1
  from typing import Any, Optional
2
  from datetime import datetime, timedelta
 
3
 
4
  from src.telegram_bot.logger import main_logger as logger
5
 
6
 
7
  CACHE_EXPIRY_HOURS = 2 # Cache expiry time in hours
 
 
8
 
9
 
10
  class TickerCache:
11
  """
12
- In-memory cache for ticker data with automatic expiry.
13
- Uses exchange:ticker as key to support multiple exchanges.
 
 
 
 
 
14
  """
15
 
16
- def __init__(self, expiry_hours: int = CACHE_EXPIRY_HOURS):
17
  self._cache: dict[str, dict[str, Any]] = {}
18
  self._timestamps: dict[str, datetime] = {}
 
 
 
19
  self._expiry_hours = expiry_hours
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
- def _make_key(self, exchange: str, ticker: str) -> str:
22
- """Create cache key from exchange and ticker"""
23
- return f"{exchange}:{ticker}"
24
 
25
- def is_valid(self, exchange: str, ticker: str) -> bool:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  """Check if cached data is still valid (not expired)"""
27
- key = self._make_key(exchange, ticker)
28
  if key not in self._timestamps:
29
  return False
30
 
31
  cache_age = datetime.now() - self._timestamps[key]
32
  return cache_age < timedelta(hours=self._expiry_hours)
33
 
34
- def get(self, exchange: str, ticker: str) -> Optional[dict[str, Any]]:
35
- """Get cached data if valid, None otherwise"""
36
- if self.is_valid(exchange, ticker):
37
- key = self._make_key(exchange, ticker)
 
 
 
 
 
38
  logger.debug(f"Using cached data for {key}")
39
  return self._cache.get(key)
40
  return None
41
 
42
- def set(self, exchange: str, ticker: str, data: dict[str, Any]) -> None:
43
- """Cache ticker data with timestamp"""
44
- key = self._make_key(exchange, ticker)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  self._cache[key] = data
46
  self._timestamps[key] = datetime.now()
47
- logger.debug(f"Cached data for {key}")
 
 
 
 
 
 
 
48
 
49
  def clear(self) -> None:
50
- """Clear all cached data"""
51
  self._cache.clear()
52
  self._timestamps.clear()
 
 
 
53
  logger.info("Cache cleared")
54
 
55
  def get_stats(self) -> dict[str, Any]:
56
- """Get cache statistics"""
 
 
 
 
 
57
  valid_count = 0
58
  for key in self._cache.keys():
59
- # Parse key to get exchange and ticker
60
- parts = key.split(':', 1)
61
- if len(parts) == 2:
62
- exchange, ticker = parts
63
- if self.is_valid(exchange, ticker):
64
  valid_count += 1
65
 
66
  return {
67
  'total_cached': len(self._cache),
68
  'valid_cached': valid_count,
69
- 'expired_cached': len(self._cache) - valid_count
 
 
 
 
 
70
  }
 
1
  from typing import Any, Optional
2
  from datetime import datetime, timedelta
3
+ import sys
4
 
5
  from src.telegram_bot.logger import main_logger as logger
6
 
7
 
8
  CACHE_EXPIRY_HOURS = 2 # Cache expiry time in hours
9
+ MAX_CACHE_SIZE_GB = 3 # Maximum cache size in gigabytes
10
+ MAX_CACHE_SIZE_BYTES = MAX_CACHE_SIZE_GB * 1024 * 1024 * 1024 # 3GB in bytes
11
 
12
 
13
  class TickerCache:
14
  """
15
+ In-memory cache for ticker data with automatic expiry and size limit.
16
+ Uses exchange:ticker:timeframe as key to support multiple exchanges and timeframes.
17
+
18
+ Features:
19
+ - Time-based expiry (default 2 hours)
20
+ - Size-based eviction (max 3GB)
21
+ - LRU (Least Recently Used) eviction policy
22
  """
23
 
24
+ def __init__(self, expiry_hours: int = CACHE_EXPIRY_HOURS, max_size_bytes: int = MAX_CACHE_SIZE_BYTES):
25
  self._cache: dict[str, dict[str, Any]] = {}
26
  self._timestamps: dict[str, datetime] = {}
27
+ self._access_times: dict[str, datetime] = {} # Track last access for LRU
28
+ self._entry_sizes: dict[str, int] = {} # Track size of each entry
29
+ self._total_size_bytes: int = 0 # Running total of cache size
30
  self._expiry_hours = expiry_hours
31
+ self._max_size_bytes = max_size_bytes
32
+
33
+ def _make_key(self, exchange: str, ticker: str, timeframe: str = "max") -> str:
34
+ """Create cache key from exchange, ticker, and timeframe"""
35
+ return f"{exchange}:{ticker}:{timeframe}"
36
+
37
+ def _calculate_size(self, data: dict[str, Any]) -> int:
38
+ """
39
+ Calculate approximate size of cached data in bytes.
40
+
41
+ This includes the size of:
42
+ - The ticker string
43
+ - The prices numpy array
44
+ - The dates DatetimeIndex
45
+ """
46
+ size = 0
47
+
48
+ # Size of ticker string
49
+ size += sys.getsizeof(data.get('ticker', ''))
50
+
51
+ # Size of prices array (numpy array has nbytes attribute)
52
+ prices = data.get('prices')
53
+ if prices is not None:
54
+ if hasattr(prices, 'nbytes'):
55
+ size += prices.nbytes
56
+ else:
57
+ size += sys.getsizeof(prices)
58
+
59
+ # Size of dates index
60
+ dates = data.get('dates')
61
+ if dates is not None:
62
+ if hasattr(dates, 'nbytes'):
63
+ size += dates.nbytes
64
+ else:
65
+ size += sys.getsizeof(dates)
66
+
67
+ # Add overhead for the dict itself
68
+ size += sys.getsizeof(data)
69
+
70
+ return size
71
+
72
+ def _evict_lru_entries(self, bytes_needed: int) -> None:
73
+ """
74
+ Evict least recently used entries until we have enough space.
75
+
76
+ Args:
77
+ bytes_needed: Number of bytes we need to free up
78
+ """
79
+ if not self._access_times:
80
+ return
81
+
82
+ # Sort keys by access time (oldest first)
83
+ sorted_keys = sorted(self._access_times.keys(), key=lambda k: self._access_times[k])
84
 
85
+ bytes_freed = 0
86
+ evicted_count = 0
 
87
 
88
+ for key in sorted_keys:
89
+ if bytes_freed >= bytes_needed:
90
+ break
91
+
92
+ # Remove this entry
93
+ if key in self._cache:
94
+ entry_size = self._entry_sizes.get(key, 0)
95
+
96
+ del self._cache[key]
97
+ del self._timestamps[key]
98
+ del self._access_times[key]
99
+ del self._entry_sizes[key]
100
+
101
+ self._total_size_bytes -= entry_size
102
+ bytes_freed += entry_size
103
+ evicted_count += 1
104
+
105
+ if evicted_count > 0:
106
+ logger.info(f"Evicted {evicted_count} LRU entries, freed {bytes_freed / (1024**2):.2f} MB")
107
+
108
+ def is_valid(self, exchange: str, ticker: str, timeframe: str = "max") -> bool:
109
  """Check if cached data is still valid (not expired)"""
110
+ key = self._make_key(exchange, ticker, timeframe)
111
  if key not in self._timestamps:
112
  return False
113
 
114
  cache_age = datetime.now() - self._timestamps[key]
115
  return cache_age < timedelta(hours=self._expiry_hours)
116
 
117
+ def get(self, exchange: str, ticker: str, timeframe: str = "max") -> Optional[dict[str, Any]]:
118
+ """
119
+ Get cached data if valid, None otherwise.
120
+ Updates access time for LRU tracking.
121
+ """
122
+ if self.is_valid(exchange, ticker, timeframe):
123
+ key = self._make_key(exchange, ticker, timeframe)
124
+ # Update access time for LRU
125
+ self._access_times[key] = datetime.now()
126
  logger.debug(f"Using cached data for {key}")
127
  return self._cache.get(key)
128
  return None
129
 
130
+ def set(self, exchange: str, ticker: str, data: dict[str, Any], timeframe: str = "max") -> None:
131
+ """
132
+ Cache ticker data with timestamp and size tracking.
133
+ Evicts LRU entries if cache size limit would be exceeded.
134
+ """
135
+ key = self._make_key(exchange, ticker, timeframe)
136
+
137
+ # Calculate size of new data
138
+ new_entry_size = self._calculate_size(data)
139
+
140
+ # If updating existing entry, account for old size
141
+ old_entry_size = 0
142
+ if key in self._cache:
143
+ old_entry_size = self._entry_sizes.get(key, 0)
144
+ self._total_size_bytes -= old_entry_size
145
+
146
+ # Check if we need to evict entries
147
+ size_after_add = self._total_size_bytes + new_entry_size
148
+ if size_after_add > self._max_size_bytes:
149
+ bytes_to_free = size_after_add - self._max_size_bytes
150
+ logger.warning(
151
+ f"Cache size would exceed limit ({size_after_add / (1024**3):.2f} GB > "
152
+ f"{self._max_size_bytes / (1024**3):.2f} GB). Evicting LRU entries..."
153
+ )
154
+ self._evict_lru_entries(bytes_to_free)
155
+
156
+ # Add/update entry
157
  self._cache[key] = data
158
  self._timestamps[key] = datetime.now()
159
+ self._access_times[key] = datetime.now()
160
+ self._entry_sizes[key] = new_entry_size
161
+ self._total_size_bytes += new_entry_size
162
+
163
+ logger.debug(
164
+ f"Cached data for {key} (size: {new_entry_size / (1024**2):.2f} MB, "
165
+ f"total cache: {self._total_size_bytes / (1024**3):.2f} GB)"
166
+ )
167
 
168
  def clear(self) -> None:
169
+ """Clear all cached data and reset size tracking"""
170
  self._cache.clear()
171
  self._timestamps.clear()
172
+ self._access_times.clear()
173
+ self._entry_sizes.clear()
174
+ self._total_size_bytes = 0
175
  logger.info("Cache cleared")
176
 
177
  def get_stats(self) -> dict[str, Any]:
178
+ """
179
+ Get comprehensive cache statistics including size information.
180
+
181
+ Returns:
182
+ Dictionary with cache stats including entry counts and memory usage
183
+ """
184
  valid_count = 0
185
  for key in self._cache.keys():
186
+ # Parse key to get exchange, ticker, and timeframe
187
+ parts = key.split(':')
188
+ if len(parts) == 3:
189
+ exchange, ticker, timeframe = parts
190
+ if self.is_valid(exchange, ticker, timeframe):
191
  valid_count += 1
192
 
193
  return {
194
  'total_cached': len(self._cache),
195
  'valid_cached': valid_count,
196
+ 'expired_cached': len(self._cache) - valid_count,
197
+ 'size_bytes': self._total_size_bytes,
198
+ 'size_mb': round(self._total_size_bytes / (1024**2), 2),
199
+ 'size_gb': round(self._total_size_bytes / (1024**3), 3),
200
+ 'max_size_gb': self._max_size_bytes / (1024**3),
201
+ 'usage_percent': round((self._total_size_bytes / self._max_size_bytes) * 100, 1) if self._max_size_bytes > 0 else 0
202
  }
src/core/ticker_scanner/ticker_lists/__init__.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Ticker Lists Module
3
+ Organized ticker lists for various global stock exchanges and commodities
4
+ """
5
+
6
+ from src.core.ticker_scanner.ticker_lists.nasdaq import NASDAQ_TICKERS
7
+ from src.core.ticker_scanner.ticker_lists.nyse import NYSE_TICKERS
8
+ from src.core.ticker_scanner.ticker_lists.lse import LSE_TICKERS
9
+ from src.core.ticker_scanner.ticker_lists.amex import AMEX_TICKERS
10
+ from src.core.ticker_scanner.ticker_lists.tse import TSE_TICKERS
11
+ from src.core.ticker_scanner.ticker_lists.hkex import HKEX_TICKERS
12
+ from src.core.ticker_scanner.ticker_lists.tsx import TSX_TICKERS
13
+ from src.core.ticker_scanner.ticker_lists.euronext import EURONEXT_TICKERS
14
+ from src.core.ticker_scanner.ticker_lists.commodities import COMMODITIES_TICKERS
15
+ from src.core.ticker_scanner.ticker_lists.etf import ETF_TICKERS
16
+
17
+ __all__ = [
18
+ 'NASDAQ_TICKERS',
19
+ 'NYSE_TICKERS',
20
+ 'LSE_TICKERS',
21
+ 'AMEX_TICKERS',
22
+ 'TSE_TICKERS',
23
+ 'HKEX_TICKERS',
24
+ 'TSX_TICKERS',
25
+ 'EURONEXT_TICKERS',
26
+ 'COMMODITIES_TICKERS',
27
+ 'ETF_TICKERS',
28
+ ]
src/core/ticker_scanner/ticker_lists/amex.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ AMEX (American Stock Exchange / NYSE American) - Popular Tickers
3
+ Curated list of top AMEX-listed securities (mostly ETFs)
4
+ """
5
+
6
+ AMEX_TICKERS = [
7
+ "SPY", "QQQ", "IWM", "DIA", "VXX", "GLD", "SLV", "XLF",
8
+ "EEM", "XLE", "XLK", "XLP", "XLI", "XLV", "XLU", "XLY",
9
+ "VTI", "EFA", "HYG", "LQD", "TLT", "AGG", "GDX", "SH",
10
+ "EWJ", "FXI", "EWZ", "RSX", "TBT", "UNG", "USO", "VEA",
11
+ "IYR", "XOP", "XME", "ITB", "XHB", "KRE", "XRT", "IBB"
12
+ ]
src/core/ticker_scanner/ticker_lists/commodities.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Commodities & ETFs - Popular Tickers
3
+ Curated list of commodity ETFs, forex commodity symbols, sector funds, and crypto-related tickers
4
+ Traded on NYSE/NASDAQ/AMEX and Forex markets
5
+ """
6
+
7
+ COMMODITIES_TICKERS = [
8
+ # Forex - Precious Metals (spot prices)
9
+ "XAUUSD", # Gold/USD
10
+ "XAGUSD", # Silver/USD
11
+ "XPTUSD", # Platinum/USD
12
+ "XPDUSD", # Palladium/USD
13
+
14
+ # Forex - Energy
15
+ "XBRUSD", # Brent Crude Oil/USD
16
+ "XTIUSD", # WTI Crude Oil/USD
17
+ "XNGUSD", # Natural Gas/USD
18
+
19
+ # Forex - Industrial Metals
20
+ "XCUUSD", # Copper/USD
21
+
22
+ # ETFs - Precious Metals
23
+ "GLD", "SLV", "PPLT", "PALL", "IAU", "PHYS", "PSLV",
24
+
25
+ # ETFs - Energy
26
+ "USO", "UNG", "BNO", "UGA", "OIL", "XLE", "XES",
27
+
28
+ # ETFs - Agriculture
29
+ "CORN", "WEAT", "SOYB", "DBA", "JJG", "JO", "NIB",
30
+
31
+ # ETFs - Industrial Metals
32
+ "CPER", "JJC", "DBB", "JJN", "LD", "JJT",
33
+
34
+ # ETFs - Broad Commodities
35
+ "DBC", "GSG", "COMT", "PDBC", "GCC", "DJP",
36
+
37
+ # Crypto-related
38
+ "GBTC", "ETHE", "BITO", "BTF", "IBIT"
39
+ ]
src/core/ticker_scanner/ticker_lists/etf.py ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ETFs (Exchange-Traded Funds) - Popular Tickers
3
+ Curated list of top ETFs across various categories
4
+ Traded on NYSE/NASDAQ/AMEX
5
+ """
6
+
7
+ ETF_TICKERS = [
8
+ # Major Index ETFs
9
+ "SPY", # SPDR S&P 500
10
+ "QQQ", # Invesco QQQ (NASDAQ-100)
11
+ "DIA", # SPDR Dow Jones
12
+ "IWM", # iShares Russell 2000
13
+ "VTI", # Vanguard Total Stock Market
14
+ "VOO", # Vanguard S&P 500
15
+ "VEA", # Vanguard FTSE Developed Markets
16
+ "VWO", # Vanguard FTSE Emerging Markets
17
+ "EFA", # iShares MSCI EAFE
18
+ "EEM", # iShares MSCI Emerging Markets
19
+
20
+ # Technology Sector
21
+ "XLK", # Technology Select Sector SPDR
22
+ "VGT", # Vanguard Information Technology
23
+ "ARKK", # ARK Innovation
24
+ "ARKW", # ARK Next Generation Internet
25
+ "ARKG", # ARK Genomic Revolution
26
+ "ARKF", # ARK Fintech Innovation
27
+ "IGV", # iShares Expanded Tech-Software
28
+ "SOXX", # iShares Semiconductor
29
+ "SMH", # VanEck Semiconductor
30
+
31
+ # Financial Sector
32
+ "XLF", # Financial Select Sector SPDR
33
+ "VFH", # Vanguard Financials
34
+ "KRE", # SPDR S&P Regional Banking
35
+ "KBE", # SPDR S&P Bank
36
+
37
+ # Healthcare Sector
38
+ "XLV", # Health Care Select Sector SPDR
39
+ "VHT", # Vanguard Health Care
40
+ "IBB", # iShares Biotechnology
41
+ "XBI", # SPDR S&P Biotech
42
+
43
+ # Energy Sector
44
+ "XLE", # Energy Select Sector SPDR
45
+ "VDE", # Vanguard Energy
46
+ "XES", # SPDR S&P Oil & Gas Exploration
47
+
48
+ # Consumer Sectors
49
+ "XLY", # Consumer Discretionary SPDR
50
+ "XLP", # Consumer Staples SPDR
51
+ "VCR", # Vanguard Consumer Discretionary
52
+ "VDC", # Vanguard Consumer Staples
53
+
54
+ # Industrial Sector
55
+ "XLI", # Industrial Select Sector SPDR
56
+ "VIS", # Vanguard Industrials
57
+
58
+ # Real Estate
59
+ "VNQ", # Vanguard Real Estate
60
+ "IYR", # iShares U.S. Real Estate
61
+ "XLRE", # Real Estate Select Sector SPDR
62
+
63
+ # Utilities
64
+ "XLU", # Utilities Select Sector SPDR
65
+ "VPU", # Vanguard Utilities
66
+
67
+ # Materials
68
+ "XLB", # Materials Select Sector SPDR
69
+ "VAW", # Vanguard Materials
70
+
71
+ # Communications
72
+ "XLC", # Communication Services SPDR
73
+ "VOX", # Vanguard Communication Services
74
+
75
+ # Bond ETFs
76
+ "AGG", # iShares Core U.S. Aggregate Bond
77
+ "BND", # Vanguard Total Bond Market
78
+ "TLT", # iShares 20+ Year Treasury Bond
79
+ "IEF", # iShares 7-10 Year Treasury Bond
80
+ "SHY", # iShares 1-3 Year Treasury Bond
81
+ "LQD", # iShares iBoxx Investment Grade Corporate
82
+ "HYG", # iShares iBoxx High Yield Corporate
83
+ "JNK", # SPDR Bloomberg High Yield Bond
84
+ "TIP", # iShares TIPS Bond
85
+ "MUB", # iShares National Muni Bond
86
+
87
+ # Commodity ETFs
88
+ "GLD", # SPDR Gold Shares
89
+ "SLV", # iShares Silver Trust
90
+ "USO", # United States Oil Fund
91
+ "UNG", # United States Natural Gas Fund
92
+ "DBC", # Invesco DB Commodity Index
93
+ "PDBC", # Invesco Optimum Yield Diversified Commodity
94
+
95
+ # Volatility
96
+ "VXX", # iPath Series B S&P 500 VIX Short-Term
97
+ "UVXY", # ProShares Ultra VIX Short-Term
98
+
99
+ # International
100
+ "IXUS", # iShares Core MSCI Total International
101
+ "VXUS", # Vanguard Total International Stock
102
+ "IEFA", # iShares Core MSCI EAFE
103
+ "IEMG", # iShares Core MSCI Emerging Markets
104
+
105
+ # Thematic/Growth
106
+ "ICLN", # iShares Global Clean Energy
107
+ "TAN", # Invesco Solar
108
+ "LIT", # Global X Lithium & Battery Tech
109
+ "BOTZ", # Global X Robotics & AI
110
+ "FINX", # Global X FinTech
111
+ "CLOU", # Global X Cloud Computing
112
+ "HACK", # ETFMG Prime Cyber Security
113
+ "BETZ", # Roundhill Sports Betting & iGaming
114
+
115
+ # Leveraged/Inverse (use with caution)
116
+ "TQQQ", # ProShares UltraPro QQQ
117
+ "SQQQ", # ProShares UltraPro Short QQQ
118
+ "SPXU", # ProShares UltraPro Short S&P500
119
+ "UPRO", # ProShares UltraPro S&P500
120
+ "TNA", # Direxion Daily Small Cap Bull 3X
121
+ "TZA", # Direxion Daily Small Cap Bear 3X
122
+
123
+ # Dividend ETFs
124
+ "SCHD", # Schwab U.S. Dividend Equity
125
+ "VYM", # Vanguard High Dividend Yield
126
+ "DVY", # iShares Select Dividend
127
+ "NOBL", # ProShares S&P 500 Dividend Aristocrats
128
+ "VIG", # Vanguard Dividend Appreciation
129
+ ]
src/core/ticker_scanner/ticker_lists/euronext.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Euronext (Paris, Amsterdam, Brussels) - Popular Tickers
3
+ Curated list of top Euronext-listed stocks
4
+ Note: Suffixes - .PA (Paris), .AS (Amsterdam), .BR (Brussels)
5
+ """
6
+
7
+ EURONEXT_TICKERS = [
8
+ # Paris (PA)
9
+ "MC.PA", "OR.PA", "SAN.PA", "AIR.PA", "BNP.PA", "TTE.PA", "SU.PA", "SAF.PA",
10
+ "CS.PA", "GLE.PA", "RMS.PA", "CAP.PA", "ACA.PA", "VIV.PA", "DG.PA", "EN.PA",
11
+ # Amsterdam (AS)
12
+ "ASML.AS", "ADYEN.AS", "HEIA.AS", "INGA.AS", "ABN.AS", "PHIA.AS", "KPN.AS", "MT.AS",
13
+ # Brussels (BR)
14
+ "ABI.BR", "KBC.BR", "ACKB.BR", "UCB.BR", "COFB.BR", "SOF.BR", "SOLB.BR", "GLPG.BR"
15
+ ]
src/core/ticker_scanner/ticker_lists/hkex.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ HKEX (Hong Kong Stock Exchange) - Popular Tickers
3
+ Curated list of top HKEX-listed stocks
4
+ Note: HKEX tickers end with .HK suffix for Yahoo Finance
5
+ """
6
+
7
+ HKEX_TICKERS = [
8
+ "0700.HK", "9988.HK", "0939.HK", "0941.HK", "1299.HK", "0005.HK", "3690.HK", "2318.HK",
9
+ "1398.HK", "3988.HK", "0388.HK", "1211.HK", "0883.HK", "0001.HK", "0002.HK", "0003.HK",
10
+ "2382.HK", "9618.HK", "1810.HK", "2020.HK", "9999.HK", "1093.HK", "0016.HK", "0011.HK",
11
+ "1113.HK", "0688.HK", "0012.HK", "2269.HK", "1024.HK", "2628.HK", "1109.HK", "0968.HK"
12
+ ]
src/core/ticker_scanner/ticker_lists/lse.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ LSE (London Stock Exchange) - Popular Tickers
3
+ Curated list of top LSE-listed stocks
4
+ Note: LSE tickers end with .L suffix for Yahoo Finance
5
+ """
6
+
7
+ LSE_TICKERS = [
8
+ "SHEL.L", "AZN.L", "HSBA.L", "BP.L", "ULVR.L", "DGE.L", "GSK.L", "RIO.L",
9
+ "REL.L", "NG.L", "BARC.L", "VOD.L", "LSEG.L", "PRU.L", "BT-A.L", "LLOY.L",
10
+ "AAL.L", "GLEN.L", "BA.L", "CRH.L", "IMB.L", "EXPN.L", "RKT.L", "ANTO.L",
11
+ "AUTO.L", "FRES.L", "III.L", "SBRY.L", "WPP.L", "MNG.L", "OCDO.L", "BHP.L",
12
+ "STAN.L", "CPG.L", "LGEN.L", "RMV.L", "BATS.L", "RTO.L", "INF.L", "NWG.L",
13
+ "SSE.L", "SGE.L", "SMDS.L", "SMT.L", "SMIN.L", "BNZL.L", "LAND.L", "PSN.L"
14
+ ]
src/core/ticker_scanner/ticker_lists/nasdaq.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ NASDAQ Stock Exchange - Popular Tickers
3
+ Curated list of top NASDAQ-listed stocks
4
+ """
5
+
6
+ NASDAQ_TICKERS = [
7
+ "AAPL", "MSFT", "GOOGL", "GOOG", "AMZN", "NVDA", "META", "TSLA",
8
+ "AVGO", "ASML", "COST", "NFLX", "AMD", "PEP", "ADBE", "CSCO",
9
+ "CMCSA", "INTC", "TMUS", "INTU", "TXN", "QCOM", "AMGN", "HON",
10
+ "AMAT", "SBUX", "BKNG", "MDLZ", "ADI", "GILD", "ISRG", "VRTX",
11
+ "REGN", "LRCX", "ADP", "PANW", "MU", "PYPL", "MELI", "SNPS",
12
+ "KLAC", "CDNS", "MAR", "ABNB", "CTAS", "ORLY", "MRVL", "NXPI",
13
+ "CRWD", "FTNT", "CSX", "ADSK", "MNST", "DXCM", "WDAY", "AZN",
14
+ "PCAR", "ROP", "PAYX", "ROST", "CPRT", "FAST", "ODFL", "CTSH",
15
+ "EA", "VRSK", "CHTR", "CSGP", "GEHC", "BKR", "XEL", "TEAM",
16
+ "IDXX", "DASH", "ON", "LULU", "KDP", "ANSS", "ZS", "FANG",
17
+ "MCHP", "TTWO", "BIIB", "DDOG", "CDW", "ALGN", "ILMN", "WBD",
18
+ "MRNA", "GFS", "SMCI", "WBA", "ZM", "RIVN", "LCID", "PLUG"
19
+ ]
src/core/ticker_scanner/ticker_lists/nyse.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ NYSE (New York Stock Exchange) - Popular Tickers
3
+ Curated list of top NYSE-listed stocks
4
+ """
5
+
6
+ NYSE_TICKERS = [
7
+ "BRK.B", "UNH", "JNJ", "XOM", "JPM", "V", "PG", "MA", "HD", "CVX",
8
+ "MRK", "ABBV", "KO", "LLY", "BAC", "PFE", "WMT", "TMO", "DIS", "ABT",
9
+ "CRM", "ACN", "VZ", "ORCL", "NKE", "MCD", "ADBE", "DHR", "PM", "NEE",
10
+ "CSCO", "WFC", "TXN", "BMY", "UPS", "RTX", "LOW", "MS", "SPGI", "HON",
11
+ "UNP", "QCOM", "T", "GS", "ELV", "INTU", "CAT", "IBM", "DE", "AMGN",
12
+ "BA", "BLK", "AXP", "PLD", "GILD", "SBUX", "ADI", "MDLZ", "GE", "ISRG",
13
+ "C", "BKNG", "TJX", "VRTX", "CB", "MMC", "SYK", "AMT", "REGN", "CI",
14
+ "LRCX", "ADP", "SO", "DUK", "ZTS", "MO", "PGR", "FI", "SCHW", "BSX",
15
+ "ITW", "EOG", "SLB", "MMM", "APD", "CL", "BDX", "TGT", "USB", "NOC",
16
+ "HUM", "AON", "EMR", "ICE", "PNC", "CME", "ETN", "SHW", "MCO", "FCX"
17
+ ]
src/core/ticker_scanner/ticker_lists/tse.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ TSE (Tokyo Stock Exchange) - Popular Tickers
3
+ Curated list of top TSE-listed stocks (Japan)
4
+ Note: TSE tickers end with .T suffix for Yahoo Finance
5
+ """
6
+
7
+ TSE_TICKERS = [
8
+ "7203.T", "6758.T", "9984.T", "8306.T", "9432.T", "6861.T", "7267.T", "8035.T",
9
+ "6902.T", "8316.T", "4502.T", "4503.T", "6501.T", "8058.T", "9433.T", "6752.T",
10
+ "8001.T", "5401.T", "7974.T", "7751.T", "6954.T", "4519.T", "8031.T", "8766.T",
11
+ "6367.T", "4661.T", "2914.T", "9022.T", "4568.T", "6273.T", "6971.T", "6594.T"
12
+ ]
src/core/ticker_scanner/ticker_lists/tsx.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ TSX (Toronto Stock Exchange) - Popular Tickers
3
+ Curated list of top TSX-listed stocks (Canada)
4
+ Note: TSX tickers end with .TO suffix for Yahoo Finance
5
+ """
6
+
7
+ TSX_TICKERS = [
8
+ "SHOP.TO", "RY.TO", "TD.TO", "ENB.TO", "BNS.TO", "BMO.TO", "CNR.TO", "CNQ.TO",
9
+ "CP.TO", "TRI.TO", "WCN.TO", "SU.TO", "ABX.TO", "MFC.TO", "BCE.TO", "CVE.TO",
10
+ "CM.TO", "ATD.TO", "L.TO", "FNV.TO", "NTR.TO", "TRP.TO", "IMO.TO", "BAM.TO",
11
+ "QSR.TO", "WN.TO", "SLF.TO", "CSU.TO", "MG.TO", "GIB-A.TO", "FSV.TO", "PPL.TO"
12
+ ]
src/core/ticker_scanner/tickers_provider.py CHANGED
@@ -4,113 +4,180 @@ from io import StringIO
4
 
5
  from src.core.ticker_scanner.core_enums import StockExchange
6
  from src.telegram_bot.logger import main_logger as logger
 
 
 
 
 
 
 
 
 
 
 
 
7
 
8
 
9
- '''
10
  class TickersProvider:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  def load_active_nasdaq_tickers(self) -> list[str]:
12
- url = "ftp://ftp.nasdaqtrader.com/SymbolDirectory/nasdaqtraded.txt"
13
- df = pd.read_csv(url, sep="|")
14
- # Keep only active tickers (Test Issue == 'N')
15
- df_active = df[df["Test Issue"] == "N"]
16
- tickers = df_active["NASDAQ Symbol"].tolist()
17
- return tickers
 
 
 
 
 
 
 
18
 
19
  def load_active_nyse_tickers(self) -> list[str]:
20
- url = "https://eodhistoricaldata.com/api/exchange-symbol-list/NYSE.csv"
21
- df = pd.read_csv(url)
22
- # Keep only active, common stocks
23
- df_active = df[(df['Type'] == 'Common Stock') & (df['Delisted'] != 1)]
24
- tickers = df_active['Code'].tolist()
25
- return tickers
 
 
 
 
 
 
 
26
 
27
- def get_tickers(self, exchange: StockExchange) -> list[str]:
28
- logger.info(f"Fetching tickers for {exchange.value}")
29
- if exchange == exchange.NASDAQ:
30
- tickers = self.load_active_nasdaq_tickers()
31
- elif exchange == exchange.NYSE:
32
- tickers = self.load_active_nyse_tickers()
33
- else:
34
- tickers = []
35
- logger.info(f"Found {len(tickers)} tickers for {exchange.value}")
36
- return tickers
37
- '''
 
 
38
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
- class TickersProvider:
41
- # Fallback list of popular NASDAQ tickers
42
- NASDAQ_POPULAR = [
43
- "AAPL", "MSFT", "GOOGL", "GOOG", "AMZN", "NVDA", "META", "TSLA",
44
- "AVGO", "ASML", "COST", "NFLX", "AMD", "PEP", "ADBE", "CSCO",
45
- "CMCSA", "INTC", "TMUS", "INTU", "TXN", "QCOM", "AMGN", "HON",
46
- "AMAT", "SBUX", "BKNG", "MDLZ", "ADI", "GILD", "ISRG", "VRTX",
47
- "REGN", "LRCX", "ADP", "PANW", "MU", "PYPL", "MELI", "SNPS",
48
- "KLAC", "CDNS", "MAR", "ABNB", "CTAS", "ORLY", "MRVL", "NXPI",
49
- "CRWD", "FTNT", "CSX", "ADSK", "MNST", "DXCM", "WDAY", "AZN",
50
- "PCAR", "ROP", "PAYX", "ROST", "CPRT", "FAST", "ODFL", "CTSH",
51
- "EA", "VRSK", "CHTR", "CSGP", "GEHC", "BKR", "XEL", "TEAM",
52
- "IDXX", "DASH", "ON", "LULU", "KDP", "ANSS", "ZS", "FANG",
53
- "MCHP", "TTWO", "BIIB", "DDOG", "CDW", "ALGN", "ILMN", "WBD",
54
- "MRNA", "GFS", "SMCI", "WBA", "ZM", "RIVN", "LCID", "PLUG"
55
- ]
56
-
57
- # Fallback list of popular NYSE tickers
58
- NYSE_POPULAR = [
59
- "BRK.B", "UNH", "JNJ", "XOM", "JPM", "V", "PG", "MA", "HD", "CVX",
60
- "MRK", "ABBV", "KO", "LLY", "BAC", "PFE", "WMT", "TMO", "DIS", "ABT",
61
- "CRM", "ACN", "VZ", "ORCL", "NKE", "MCD", "ADBE", "DHR", "PM", "NEE",
62
- "CSCO", "WFC", "TXN", "BMY", "UPS", "RTX", "LOW", "MS", "SPGI", "HON",
63
- "UNP", "QCOM", "T", "GS", "ELV", "INTU", "CAT", "IBM", "DE", "AMGN",
64
- "BA", "BLK", "AXP", "PLD", "GILD", "SBUX", "ADI", "MDLZ", "GE", "ISRG",
65
- "C", "BKNG", "TJX", "VRTX", "CB", "MMC", "SYK", "AMT", "REGN", "CI",
66
- "LRCX", "ADP", "SO", "DUK", "ZTS", "MO", "PGR", "FI", "SCHW", "BSX",
67
- "ITW", "EOG", "SLB", "MMM", "APD", "CL", "BDX", "TGT", "USB", "NOC",
68
- "HUM", "AON", "EMR", "ICE", "PNC", "CME", "ETN", "SHW", "MCO", "FCX"
69
- ]
70
 
71
- def load_active_nasdaq_tickers(self) -> list[str]:
72
- """Load NASDAQ tickers with fallback to popular list"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  try:
74
- # Try alternative HTTP source (uses GitHub mirror)
75
- logger.info("Attempting to fetch NASDAQ tickers from alternative source...")
76
- url = "https://raw.githubusercontent.com/rreichel3/US-Stock-Symbols/main/nasdaq/nasdaq_tickers.txt"
77
- response = requests.get(url, timeout=10)
78
- if response.status_code == 200:
79
- tickers = [line.strip() for line in response.text.strip().split('\n') if line.strip()]
80
- logger.info(f"Successfully fetched {len(tickers)} NASDAQ tickers from alternative source")
81
- return tickers
82
  except Exception as e:
83
- logger.warning(f"Failed to fetch from alternative source: {e}")
 
84
 
85
- # Fallback to popular tickers
86
- logger.warning("Using fallback list of popular NASDAQ tickers")
87
- return self.NASDAQ_POPULAR.copy()
 
 
 
 
 
 
 
 
 
 
88
 
89
- def load_active_nyse_tickers(self) -> list[str]:
90
- """Load NYSE tickers with fallback to popular list"""
91
  try:
92
- # Try alternative HTTP source
93
- logger.info("Attempting to fetch NYSE tickers from alternative source...")
94
- url = "https://raw.githubusercontent.com/rreichel3/US-Stock-Symbols/main/nyse/nyse_tickers.txt"
95
- response = requests.get(url, timeout=10)
96
- if response.status_code == 200:
97
- tickers = [line.strip() for line in response.text.strip().split('\n') if line.strip()]
98
- logger.info(f"Successfully fetched {len(tickers)} NYSE tickers from alternative source")
99
- return tickers
100
  except Exception as e:
101
- logger.warning(f"Failed to fetch from alternative source: {e}")
 
102
 
103
- # Fallback to popular tickers
104
- logger.warning("Using fallback list of popular NYSE tickers")
105
- return self.NYSE_POPULAR.copy()
 
106
 
107
  def get_tickers(self, exchange: StockExchange) -> list[str]:
108
  logger.info(f"Fetching tickers for {exchange.value}")
109
  try:
110
- if exchange == StockExchange.NASDAQ:
111
- tickers = self.load_active_nasdaq_tickers()
112
- elif exchange == StockExchange.NYSE:
113
- tickers = self.load_active_nyse_tickers()
 
 
 
 
 
 
 
 
 
 
 
114
  else:
115
  logger.warning(f"Unknown exchange: {exchange.value}, using NASDAQ fallback")
116
  tickers = self.NASDAQ_POPULAR.copy()
@@ -119,6 +186,5 @@ class TickersProvider:
119
  return tickers
120
  except Exception as e:
121
  logger.error(f"Error fetching tickers: {e}", exc_info=True)
122
- # Final fallback
123
  logger.warning("Using NASDAQ popular tickers as final fallback")
124
  return self.NASDAQ_POPULAR.copy()
 
4
 
5
  from src.core.ticker_scanner.core_enums import StockExchange
6
  from src.telegram_bot.logger import main_logger as logger
7
+ from src.core.ticker_scanner.ticker_lists import (
8
+ NASDAQ_TICKERS,
9
+ NYSE_TICKERS,
10
+ LSE_TICKERS,
11
+ AMEX_TICKERS,
12
+ TSE_TICKERS,
13
+ HKEX_TICKERS,
14
+ TSX_TICKERS,
15
+ EURONEXT_TICKERS,
16
+ COMMODITIES_TICKERS,
17
+ ETF_TICKERS,
18
+ )
19
 
20
 
 
21
  class TickersProvider:
22
+ """
23
+ Provides ticker lists for various global stock exchanges.
24
+ Only returns curated/popular lists (no full lists from external sources).
25
+ """
26
+
27
+ NASDAQ_POPULAR = NASDAQ_TICKERS
28
+ NYSE_POPULAR = NYSE_TICKERS
29
+ LSE_POPULAR = LSE_TICKERS
30
+ AMEX_POPULAR = AMEX_TICKERS
31
+ TSE_POPULAR = TSE_TICKERS
32
+ HKEX_POPULAR = HKEX_TICKERS
33
+ TSX_POPULAR = TSX_TICKERS
34
+ EURONEXT_POPULAR = EURONEXT_TICKERS
35
+ COMMODITIES_POPULAR = COMMODITIES_TICKERS
36
+ ETF_POPULAR = ETF_TICKERS
37
+
38
  def load_active_nasdaq_tickers(self) -> list[str]:
39
+ """Load NASDAQ tickers from API, fallback to curated list"""
40
+ try:
41
+ url = "https://api.nasdaq.com/api/screener/stocks?exchange=nasdaq"
42
+ headers = {"User-Agent": "Mozilla/5.0"}
43
+ resp = requests.get(url, headers=headers, timeout=10)
44
+ resp.raise_for_status()
45
+ data = resp.json()
46
+ tickers = [row["symbol"] for row in data["data"]["rows"]]
47
+ logger.info(f"Loaded {len(tickers)} NASDAQ tickers from API")
48
+ return tickers
49
+ except Exception as e:
50
+ logger.warning(f"Failed to load NASDAQ tickers from API: {e}")
51
+ return self.NASDAQ_POPULAR.copy()
52
 
53
  def load_active_nyse_tickers(self) -> list[str]:
54
+ """Load NYSE tickers from API, fallback to curated list"""
55
+ try:
56
+ url = "https://api.nasdaq.com/api/screener/stocks?exchange=nyse"
57
+ headers = {"User-Agent": "Mozilla/5.0"}
58
+ resp = requests.get(url, headers=headers, timeout=10)
59
+ resp.raise_for_status()
60
+ data = resp.json()
61
+ tickers = [row["symbol"] for row in data["data"]["rows"]]
62
+ logger.info(f"Loaded {len(tickers)} NYSE tickers from API")
63
+ return tickers
64
+ except Exception as e:
65
+ logger.warning(f"Failed to load NYSE tickers from API: {e}")
66
+ return self.NYSE_POPULAR.copy()
67
 
68
+ def load_active_lse_tickers(self) -> list[str]:
69
+ """Load LSE tickers from API, fallback to curated list"""
70
+ try:
71
+ url = "https://www.londonstockexchange.com/api/v1/symbols"
72
+ resp = requests.get(url, timeout=10)
73
+ resp.raise_for_status()
74
+ data = resp.json()
75
+ tickers = [item["symbol"] for item in data["symbols"]]
76
+ logger.info(f"Loaded {len(tickers)} LSE tickers from API")
77
+ return tickers
78
+ except Exception as e:
79
+ logger.warning(f"Failed to load LSE tickers from API: {e}")
80
+ return self.LSE_POPULAR.copy()
81
 
82
+ def load_active_amex_tickers(self) -> list[str]:
83
+ """Load AMEX tickers from API, fallback to curated list"""
84
+ try:
85
+ url = "https://api.nasdaq.com/api/screener/stocks?exchange=amex"
86
+ headers = {"User-Agent": "Mozilla/5.0"}
87
+ resp = requests.get(url, headers=headers, timeout=10)
88
+ resp.raise_for_status()
89
+ data = resp.json()
90
+ tickers = [row["symbol"] for row in data["data"]["rows"]]
91
+ logger.info(f"Loaded {len(tickers)} AMEX tickers from API")
92
+ return tickers
93
+ except Exception as e:
94
+ logger.warning(f"Failed to load AMEX tickers from API: {e}")
95
+ return self.AMEX_POPULAR.copy()
96
 
97
+ def load_commodities_tickers(self) -> list[str]:
98
+ """Return curated commodity tickers only (no public API available)"""
99
+ logger.info("Using curated list of commodity tickers")
100
+ return self.COMMODITIES_POPULAR.copy()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
 
102
+ def load_active_tse_tickers(self) -> list[str]:
103
+ """Load TSE tickers from API, fallback to curated list"""
104
+ try:
105
+ url = "https://www.jpx.co.jp/english/markets/statistics-equities/misc/tvdivq00000030km-att/data.csv"
106
+ resp = requests.get(url, timeout=10)
107
+ resp.raise_for_status()
108
+ df = pd.read_csv(StringIO(resp.text))
109
+ tickers = df["Code"].astype(str).tolist()
110
+ logger.info(f"Loaded {len(tickers)} TSE tickers from API")
111
+ return tickers
112
+ except Exception as e:
113
+ logger.warning(f"Failed to load TSE tickers from API: {e}")
114
+ return self.TSE_POPULAR.copy()
115
+
116
+ def load_active_hkex_tickers(self) -> list[str]:
117
+ """Load HKEX tickers from API, fallback to curated list"""
118
  try:
119
+ url = "https://www.hkex.com.hk/eng/services/trading/securities/securitieslists/ListOfSecurities.xlsx"
120
+ resp = requests.get(url, timeout=10)
121
+ resp.raise_for_status()
122
+ # For simplicity, fallback to curated list (parsing XLSX requires more code)
123
+ # TODO: Implement XLSX parsing if needed
124
+ logger.info("HKEX API loaded, but using curated list for now")
125
+ return self.HKEX_POPULAR.copy()
 
126
  except Exception as e:
127
+ logger.warning(f"Failed to load HKEX tickers from API: {e}")
128
+ return self.HKEX_POPULAR.copy()
129
 
130
+ def load_active_tsx_tickers(self) -> list[str]:
131
+ """Load TSX tickers from API, fallback to curated list"""
132
+ try:
133
+ url = "https://www.tsx.com/json/company-directory/search"
134
+ resp = requests.get(url, timeout=10)
135
+ resp.raise_for_status()
136
+ data = resp.json()
137
+ tickers = [item["symbol"] for item in data["results"]]
138
+ logger.info(f"Loaded {len(tickers)} TSX tickers from API")
139
+ return tickers
140
+ except Exception as e:
141
+ logger.warning(f"Failed to load TSX tickers from API: {e}")
142
+ return self.TSX_POPULAR.copy()
143
 
144
+ def load_active_euronext_tickers(self) -> list[str]:
145
+ """Load Euronext tickers from API, fallback to curated list"""
146
  try:
147
+ url = "https://live.euronext.com/en/markets/equities/directory"
148
+ resp = requests.get(url, timeout=10)
149
+ resp.raise_for_status()
150
+ # For simplicity, fallback to curated list (parsing HTML requires more code)
151
+ # TODO: Implement HTML parsing if needed
152
+ logger.info("Euronext API loaded, but using curated list for now")
153
+ return self.EURONEXT_POPULAR.copy()
 
154
  except Exception as e:
155
+ logger.warning(f"Failed to load Euronext tickers from API: {e}")
156
+ return self.EURONEXT_POPULAR.copy()
157
 
158
+ def load_active_etf_tickers(self) -> list[str]:
159
+ """Return curated ETF tickers only (no public API available)"""
160
+ logger.info("Using curated list of ETF tickers")
161
+ return self.ETF_POPULAR.copy()
162
 
163
  def get_tickers(self, exchange: StockExchange) -> list[str]:
164
  logger.info(f"Fetching tickers for {exchange.value}")
165
  try:
166
+ loaders = {
167
+ StockExchange.NASDAQ: self.load_active_nasdaq_tickers,
168
+ StockExchange.NYSE: self.load_active_nyse_tickers,
169
+ StockExchange.AMEX: self.load_active_amex_tickers,
170
+ StockExchange.LSE: self.load_active_lse_tickers,
171
+ StockExchange.TSE: self.load_active_tse_tickers,
172
+ StockExchange.HKEX: self.load_active_hkex_tickers,
173
+ StockExchange.TSX: self.load_active_tsx_tickers,
174
+ StockExchange.EURONEXT: self.load_active_euronext_tickers,
175
+ StockExchange.ETF: self.load_active_etf_tickers,
176
+ }
177
+
178
+ loader = loaders.get(exchange)
179
+ if loader:
180
+ tickers = loader()
181
  else:
182
  logger.warning(f"Unknown exchange: {exchange.value}, using NASDAQ fallback")
183
  tickers = self.NASDAQ_POPULAR.copy()
 
186
  return tickers
187
  except Exception as e:
188
  logger.error(f"Error fetching tickers: {e}", exc_info=True)
 
189
  logger.warning("Using NASDAQ popular tickers as final fallback")
190
  return self.NASDAQ_POPULAR.copy()
src/telegram_bot/telegram_bot_service.py CHANGED
@@ -188,6 +188,18 @@ class TelegramBotService:
188
  response += "/insiders - Provides key insider's trades\n"
189
  response += "/insiders NVDA 30 - Insider's trades for the last 30 days\n"
190
  response += "/scan EXCHANGE - Scan for top 20 growing tickers (e.g., /scan NASDAQ)\n"
 
 
 
 
 
 
 
 
 
 
 
 
191
 
192
  elif base_command == "/status":
193
  response = "βœ… <b>Bot Status: Online</b>\n\n"
@@ -663,23 +675,75 @@ class TelegramBotService:
663
  async def handle_scan_command(
664
  self, chat_id: int, command_parts: list[str], text: str | None, user_name: str
665
  ) -> None:
666
- """Ticker scanner command handler"""
667
- # Default to NASDAQ
 
 
 
 
 
 
 
 
 
668
  exchange = "NASDAQ"
 
 
 
669
  if len(command_parts) >= 2:
670
  exchange = command_parts[1].upper()
 
 
 
 
 
671
  # Validate exchange
672
- valid_exchanges = ["NASDAQ", "NYSE"]
 
 
 
 
 
 
 
 
 
 
 
673
  if exchange not in valid_exchanges:
674
  await self.send_message_via_proxy(
675
  chat_id,
676
  f"❌ Invalid exchange: {exchange}\n\n"
677
- f"Supported exchanges: {', '.join(valid_exchanges)}\n\n"
678
- f"Examples:\nβ€’ /scan NASDAQ\nβ€’ /scan NYSE"
 
 
 
 
 
 
 
679
  )
680
  return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
681
  # Send loading message
682
- loading_msg = f"πŸ” <b>Scanning {exchange} for top growing tickers...</b>\n\n"
683
  loading_msg += "⏳ This may take a few minutes:\n"
684
  loading_msg += "πŸ“₯ Downloading historical data...\n"
685
  loading_msg += "πŸ“Š Analyzing growth metrics...\n"
@@ -690,14 +754,15 @@ class TelegramBotService:
690
  analyzer = TickerAnalyzer(
691
  exchange=exchange,
692
  telegram_bot_service=self,
693
- limit=1000 # Limit to 1000 tickers for reasonable execution time
 
694
  )
695
- logger.info(f"Starting ticker scan for {exchange}")
696
  top_tickers = await analyzer.run_analysis()
697
  # Format and send results
698
  message = analyzer._format_telegram_message(top_tickers)
699
  await self.send_message_via_proxy(chat_id, message)
700
- logger.info(f"Ticker scan completed for {exchange}")
701
  except Exception as e:
702
  logger.error(f"Error in ticker scanner: {e}", exc_info=True)
703
  error_msg = f"❌ An error occurred during ticker scanning:\n\n{str(e)}\n\n"
 
188
  response += "/insiders - Provides key insider's trades\n"
189
  response += "/insiders NVDA 30 - Insider's trades for the last 30 days\n"
190
  response += "/scan EXCHANGE - Scan for top 20 growing tickers (e.g., /scan NASDAQ)\n"
191
+ response += (
192
+ "\n<b>/scan Command Details:</b>\n"
193
+ "Usage: <code>/scan [EXCHANGE] [TIMEFRAME]</code>\n"
194
+ "Supported timeframes: <b>1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, 10y, ytd, max</b>\n"
195
+ "Examples:\n"
196
+ "β€’ /scan NASDAQ 1y\n"
197
+ "β€’ /scan NYSE 6mo\n"
198
+ "β€’ /scan LSE max\n"
199
+ "β€’ /scan ETF 3mo\n"
200
+ )
201
+ response += "πŸ€– AI-powered trading insights\n"
202
+ response += "πŸ”— Powered by OpenRouter and Gemini API\n\n"
203
 
204
  elif base_command == "/status":
205
  response = "βœ… <b>Bot Status: Online</b>\n\n"
 
675
  async def handle_scan_command(
676
  self, chat_id: int, command_parts: list[str], text: str | None, user_name: str
677
  ) -> None:
678
+ """
679
+ Ticker scanner command handler
680
+
681
+ Usage: /scan [EXCHANGE] [TIMEFRAME]
682
+ Examples:
683
+ /scan -> NASDAQ, max
684
+ /scan NYSE -> NYSE, max
685
+ /scan NASDAQ 6mo -> NASDAQ, 6mo
686
+ /scan NYSE 2y -> NYSE, 2y
687
+ """
688
+ # Default values
689
  exchange = "NASDAQ"
690
+ timeframe = "max"
691
+
692
+ # Parse exchange
693
  if len(command_parts) >= 2:
694
  exchange = command_parts[1].upper()
695
+
696
+ # Parse timeframe
697
+ if len(command_parts) >= 3:
698
+ timeframe = command_parts[2].lower()
699
+
700
  # Validate exchange
701
+ valid_exchanges = [
702
+ # American
703
+ "NASDAQ", "NYSE", "AMEX",
704
+ # European
705
+ "LSE", "EURONEXT", "FWB", "SIX",
706
+ # Asian
707
+ "TSE", "HKEX", "SSE",
708
+ # Others
709
+ "TSX", "ASX",
710
+ # Special
711
+ "ETF"
712
+ ]
713
  if exchange not in valid_exchanges:
714
  await self.send_message_via_proxy(
715
  chat_id,
716
  f"❌ Invalid exchange: {exchange}\n\n"
717
+ f"πŸ‡ΊπŸ‡Έ <b>American:</b> NASDAQ, NYSE, AMEX\n"
718
+ f"πŸ‡ͺπŸ‡Ί <b>European:</b> LSE, EURONEXT, FWB, SIX\n"
719
+ f"πŸ‡¦πŸ‡Έ <b>Asian:</b> TSE, HKEX, SSE\n"
720
+ f"🌎 <b>Others:</b> TSX, ASX\n"
721
+ f"πŸ“Š <b>Special:</b> ETF\n\n"
722
+ f"<b>Examples:</b>\n"
723
+ f"β€’ /scan NASDAQ\n"
724
+ f"β€’ /scan TSE 1y\n"
725
+ f"β€’ /scan ETF 6mo"
726
  )
727
  return
728
+
729
+ # Validate timeframe
730
+ valid_timeframes = ["1d", "5d", "1mo", "3mo", "6mo", "1y", "2y", "5y", "10y", "ytd", "max"]
731
+ if timeframe not in valid_timeframes:
732
+ await self.send_message_via_proxy(
733
+ chat_id,
734
+ f"❌ Invalid timeframe: {timeframe}\n\n"
735
+ f"Supported timeframes:\n"
736
+ f"β€’ Short: 1d, 5d, 1mo, 3mo\n"
737
+ f"β€’ Medium: 6mo, 1y, 2y\n"
738
+ f"β€’ Long: 5y, 10y, ytd, max\n\n"
739
+ f"Examples:\n"
740
+ f"β€’ /scan NASDAQ 1y\n"
741
+ f"β€’ /scan NYSE 6mo"
742
+ )
743
+ return
744
+
745
  # Send loading message
746
+ loading_msg = f"πŸ” <b>Scanning {exchange} ({timeframe}) for top growing tickers...</b>\n\n"
747
  loading_msg += "⏳ This may take a few minutes:\n"
748
  loading_msg += "πŸ“₯ Downloading historical data...\n"
749
  loading_msg += "πŸ“Š Analyzing growth metrics...\n"
 
754
  analyzer = TickerAnalyzer(
755
  exchange=exchange,
756
  telegram_bot_service=self,
757
+ limit=1000, # Limit to 1000 tickers for reasonable execution time
758
+ timeframe=timeframe
759
  )
760
+ logger.info(f"Starting ticker scan for {exchange} with timeframe {timeframe}")
761
  top_tickers = await analyzer.run_analysis()
762
  # Format and send results
763
  message = analyzer._format_telegram_message(top_tickers)
764
  await self.send_message_via_proxy(chat_id, message)
765
+ logger.info(f"Ticker scan completed for {exchange} ({timeframe})")
766
  except Exception as e:
767
  logger.error(f"Error in ticker scanner: {e}", exc_info=True)
768
  error_msg = f"❌ An error occurred during ticker scanning:\n\n{str(e)}\n\n"