Dmitry Beresnev commited on
Commit
447c952
Β·
1 Parent(s): 1b2c0fb

Add ticker scanner module with Telegram integration

Browse files

- Create modular ticker scanner system for monitoring growing stocks
- Implement TickerAnalyzer orchestrator with parallel data downloading
- Add comprehensive growth metrics analysis (CAGR, Sharpe, acceleration)
- Fix scheduler bugs and enable hourly monitoring
- Integrate TradingView link generation for each ticker
- Add /scan command to Telegram bot for on-demand scanning
- Add logging throughout ticker scanner modules
- Refactor legacy code into maintainable components

The scanner analyzes top 20 fast and stable growing tickers from Yahoo Finance,
ranks them by velocity score, and sends formatted reports to Telegram.

src/core/ticker_scanner/__init__.py CHANGED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Ticker Scanner Module
3
+ Monitors and analyzes stock tickers for growth potential
4
+ """
5
+
6
+ from src.core.ticker_scanner.ticker_analyzer import TickerAnalyzer
7
+ from src.core.ticker_scanner.scheduler import Scheduler
8
+ from src.core.ticker_scanner.growth_speed_analyzer import GrowthSpeedAnalyzer
9
+ from src.core.ticker_scanner.core_enums import StockExchange, GrowthCategory
10
+ from src.core.ticker_scanner.growth_metrics import GrowthSpeedMetrics
11
+
12
+ __all__ = [
13
+ 'TickerAnalyzer',
14
+ 'Scheduler',
15
+ 'GrowthSpeedAnalyzer',
16
+ 'StockExchange',
17
+ 'GrowthCategory',
18
+ 'GrowthSpeedMetrics',
19
+ ]
src/core/ticker_scanner/growth_metrics.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+
3
+ from src.core.ticker_scanner.core_enums import GrowthCategory
4
+
5
+
6
+ @dataclass
7
+ class GrowthSpeedMetrics:
8
+ """Detailed growth velocity analysis"""
9
+ # Linear metrics
10
+ linear_slope: float # Daily price change slope
11
+ linear_r_squared: float # Goodness of fit
12
+ # Compound metrics
13
+ cagr: float # Compound Annual Growth Rate
14
+ total_return: float # Total percentage return
15
+ # Acceleration metrics
16
+ acceleration: float # Second derivative of growth
17
+ recent_momentum: float # Last 6 months vs overall trend
18
+ # Volatility-adjusted
19
+ sharpe_ratio: float # Risk-adjusted return
20
+ sortino_ratio: float # Downside risk-adjusted return
21
+ # Speed classification
22
+ growth_velocity_score: float # 0-100 composite score
23
+ growth_category: GrowthCategory
src/core/ticker_scanner/growth_speed_analyzer.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import pandas as pd
3
+ from sklearn.linear_model import LinearRegression
4
+
5
+ from src.core.ticker_scanner.core_enums import GrowthCategory
6
+ from src.core.ticker_scanner.growth_metrics import GrowthSpeedMetrics
7
+ from src.telegram_bot.logger import main_logger as logger
8
+
9
+
10
+ class GrowthSpeedAnalyzer:
11
+ """Advanced growth velocity and acceleration analysis"""
12
+
13
+ @staticmethod
14
+ def analyze(prices: np.ndarray, dates: pd.DatetimeIndex) -> GrowthSpeedMetrics:
15
+ """Comprehensive growth speed analysis"""
16
+
17
+ # Time series setup
18
+ n = len(prices)
19
+ x = np.arange(n).reshape(-1, 1)
20
+ y = prices.reshape(-1, 1)
21
+
22
+ # 1. Linear regression metrics
23
+ model = LinearRegression().fit(x, y)
24
+ linear_slope = model.coef_[0][0]
25
+ predictions = model.predict(x)
26
+
27
+ ss_res = np.sum((y - predictions) ** 2)
28
+ ss_tot = np.sum((y - np.mean(y)) ** 2)
29
+ r_squared = 1 - (ss_res / ss_tot) if ss_tot > 0 else 0
30
+
31
+ # 2. Compound growth metrics
32
+ years = (dates[-1] - dates[0]).days / 365.25
33
+ total_return = ((prices[-1] / prices[0]) - 1) * 100
34
+ cagr = (pow(prices[-1] / prices[0], 1 / years) - 1) * 100 if years > 0 else 0
35
+
36
+ # 3. Acceleration (second derivative)
37
+ returns = np.diff(np.log(prices))
38
+ if len(returns) > 1:
39
+ acceleration = np.polyfit(range(len(returns)), returns, 1)[0] * 252 # Annualized
40
+ else:
41
+ acceleration = 0
42
+
43
+ # 4. Recent momentum (last 6 months vs overall)
44
+ six_months_ago = max(0, n - 126) # ~6 months of trading days
45
+ recent_return = (prices[-1] / prices[six_months_ago] - 1) * 100 if six_months_ago < n else 0
46
+ recent_momentum = recent_return - (total_return / years * 0.5) if years > 0 else 0
47
+
48
+ # 5. Risk-adjusted metrics
49
+ daily_returns = np.diff(prices) / prices[:-1]
50
+
51
+ if len(daily_returns) > 0:
52
+ mean_return = np.mean(daily_returns) * 252 # Annualized
53
+ std_return = np.std(daily_returns) * np.sqrt(252) # Annualized
54
+
55
+ sharpe_ratio = mean_return / std_return if std_return > 0 else 0
56
+
57
+ # Sortino ratio (downside deviation)
58
+ downside_returns = daily_returns[daily_returns < 0]
59
+ downside_std = np.std(downside_returns) * np.sqrt(252) if len(downside_returns) > 0 else std_return
60
+ sortino_ratio = mean_return / downside_std if downside_std > 0 else 0
61
+ else:
62
+ sharpe_ratio = sortino_ratio = 0
63
+
64
+ # 6. Composite growth velocity score (0-100)
65
+ velocity_score = GrowthSpeedAnalyzer._calculate_velocity_score(
66
+ cagr, r_squared, acceleration, sharpe_ratio, recent_momentum
67
+ )
68
+
69
+ # 7. Categorize growth speed
70
+ growth_category = GrowthSpeedAnalyzer._categorize_growth(velocity_score)
71
+
72
+ return GrowthSpeedMetrics(
73
+ linear_slope=linear_slope,
74
+ linear_r_squared=r_squared,
75
+ cagr=cagr,
76
+ total_return=total_return,
77
+ acceleration=acceleration,
78
+ recent_momentum=recent_momentum,
79
+ sharpe_ratio=sharpe_ratio,
80
+ sortino_ratio=sortino_ratio,
81
+ growth_velocity_score=velocity_score,
82
+ growth_category=growth_category
83
+ )
84
+
85
+ @staticmethod
86
+ def _calculate_velocity_score(cagr: float, r_squared: float,
87
+ acceleration: float, sharpe: float,
88
+ momentum: float) -> float:
89
+ """Calculate composite 0-100 growth velocity score"""
90
+ # Normalize components to 0-1 scale
91
+ cagr_norm = min(max(cagr / 50, 0), 1) # Cap at 50% CAGR
92
+ r_squared_norm = max(r_squared, 0)
93
+ accel_norm = min(max((acceleration + 1) / 2, 0), 1) # Center at 0
94
+ sharpe_norm = min(max(sharpe / 3, 0), 1) # Cap at 3.0
95
+ momentum_norm = min(max((momentum + 50) / 100, 0), 1) # -50 to +50 range
96
+ # Weighted combination
97
+ score = (
98
+ 0.35 * cagr_norm +
99
+ 0.20 * r_squared_norm +
100
+ 0.20 * accel_norm +
101
+ 0.15 * sharpe_norm +
102
+ 0.10 * momentum_norm
103
+ ) * 100
104
+ return round(score, 2)
105
+
106
+ @staticmethod
107
+ def _categorize_growth(velocity_score: float) -> GrowthCategory:
108
+ """Classify growth speed"""
109
+ if velocity_score >= 75:
110
+ return GrowthCategory.EXPLOSIVE
111
+ elif velocity_score >= 60:
112
+ return GrowthCategory.STRONG
113
+ elif velocity_score >= 40:
114
+ return GrowthCategory.MODERATE
115
+ else:
116
+ return GrowthCategory.SLOW
src/core/ticker_scanner/parallel_data_downloader.py CHANGED
@@ -14,6 +14,7 @@ import yfinance as yf
14
 
15
  from src.core.ticker_scanner.core_enums import StockExchange
16
  from src.core.ticker_scanner.tickers_provider import TickersProvider
 
17
 
18
 
19
  MAX_WORKERS = 8 # Number of parallel processes
@@ -26,20 +27,25 @@ MIN_DATA_POINTS = 50 # Minimum number of price points required
26
  def fetch_prices(ticker: str, max_retries: int = MAX_RETRIES) -> dict[str, Any]:
27
  """
28
  Download all-time closing prices for a single ticker safely.
29
- Returns dict {'ticker': ticker, 'prices': ndarray} or None if failed.
30
  """
31
  for attempt in range(max_retries):
32
  try:
33
  df = yf.download(ticker, period="max", progress=False, auto_adjust=True)
34
- closes = df["Close"].dropna().values
35
  if len(closes) < MIN_DATA_POINTS:
36
  return None
37
- return {"ticker": ticker, "prices": closes}
 
 
 
 
38
  except yf.shared.YFRateLimitError:
39
  wait = SLEEP_BETWEEN_RETRIES + random.random()
40
- print(f"⚠️ Rate limited for {ticker}. Waiting {wait:.1f}s and retrying...")
41
  time.sleep(wait)
42
- except Exception:
 
43
  return None
44
  return None
45
 
@@ -57,23 +63,22 @@ def batch(iterable: list[str], n: int = BATCH_SIZE):
57
  def download_tickers_parallel(tickers: list[str], max_workers: int = MAX_WORKERS) -> list[dict[str, Any]]:
58
  """
59
  Download a large list of tickers in parallel batches.
60
- Returns a list of {'ticker': ..., 'prices': ...} dicts.
61
  """
62
  all_results = []
63
  all_failed = []
64
 
65
  for batch_num, ticker_batch in enumerate(batch(tickers, BATCH_SIZE), start=1):
66
- print(f"πŸ”Ή Processing batch {batch_num}: {len(ticker_batch)} tickers")
67
  results, failed = process_batch(ticker_batch, max_workers)
68
  all_results.extend(results)
69
  all_failed.extend(failed)
70
  # small sleep between batches to reduce rate-limit chance
71
  time.sleep(1 + random.random())
72
 
73
- print(f"\nβœ… Total downloaded: {len(all_results)}")
74
  if all_failed:
75
- print(f"❌ Total failed: {len(all_failed)}")
76
- print("Failed tickers:", all_failed)
77
  return all_results
78
 
79
  def process_batch(ticker_batch: list[str], max_workers: int) -> tuple[list[dict[str, Any]], list[Any]]:
@@ -97,13 +102,24 @@ def process_batch(ticker_batch: list[str], max_workers: int) -> tuple[list[dict[
97
  failed.append(ticker)
98
  return results, failed
99
 
100
- def run_parallel_data_downloader():
101
- all_tickers = TickersProvider().get_tickers(StockExchange.NASDAQ)
102
- tickers = all_tickers[:200] # Limit to first 200 for testing
103
- print("πŸš€ Starting parallel download...")
 
 
 
 
 
 
 
 
 
 
 
104
  data = download_tickers_parallel(tickers)
105
- for d in data:
106
- print(f"{d['ticker']}: {len(d['prices'])} price points")
107
 
108
 
109
  if __name__ == "__main__":
 
14
 
15
  from src.core.ticker_scanner.core_enums import StockExchange
16
  from src.core.ticker_scanner.tickers_provider import TickersProvider
17
+ from src.telegram_bot.logger import main_logger as logger
18
 
19
 
20
  MAX_WORKERS = 8 # Number of parallel processes
 
27
  def fetch_prices(ticker: str, max_retries: int = MAX_RETRIES) -> dict[str, Any]:
28
  """
29
  Download all-time closing prices for a single ticker safely.
30
+ Returns dict {'ticker': ticker, 'prices': ndarray, 'dates': DatetimeIndex} or None if failed.
31
  """
32
  for attempt in range(max_retries):
33
  try:
34
  df = yf.download(ticker, period="max", progress=False, auto_adjust=True)
35
+ closes = df["Close"].dropna()
36
  if len(closes) < MIN_DATA_POINTS:
37
  return None
38
+ return {
39
+ "ticker": ticker,
40
+ "prices": closes.values,
41
+ "dates": closes.index
42
+ }
43
  except yf.shared.YFRateLimitError:
44
  wait = SLEEP_BETWEEN_RETRIES + random.random()
45
+ logger.warning(f"Rate limited for {ticker}. Waiting {wait:.1f}s and retrying...")
46
  time.sleep(wait)
47
+ except Exception as e:
48
+ logger.debug(f"Failed to fetch {ticker}: {e}")
49
  return None
50
  return None
51
 
 
63
  def download_tickers_parallel(tickers: list[str], max_workers: int = MAX_WORKERS) -> list[dict[str, Any]]:
64
  """
65
  Download a large list of tickers in parallel batches.
66
+ Returns a list of {'ticker': ..., 'prices': ..., 'dates': ...} dicts.
67
  """
68
  all_results = []
69
  all_failed = []
70
 
71
  for batch_num, ticker_batch in enumerate(batch(tickers, BATCH_SIZE), start=1):
72
+ logger.info(f"Processing batch {batch_num}: {len(ticker_batch)} tickers")
73
  results, failed = process_batch(ticker_batch, max_workers)
74
  all_results.extend(results)
75
  all_failed.extend(failed)
76
  # small sleep between batches to reduce rate-limit chance
77
  time.sleep(1 + random.random())
78
 
79
+ logger.info(f"Total downloaded: {len(all_results)}")
80
  if all_failed:
81
+ logger.warning(f"Total failed: {len(all_failed)} - {all_failed[:10]}") # Show first 10
 
82
  return all_results
83
 
84
  def process_batch(ticker_batch: list[str], max_workers: int) -> tuple[list[dict[str, Any]], list[Any]]:
 
102
  failed.append(ticker)
103
  return results, failed
104
 
105
+ def run_parallel_data_downloader(exchange: StockExchange = StockExchange.NASDAQ,
106
+ limit: int = 200) -> list[dict[str, Any]]:
107
+ """
108
+ Main function to download ticker data in parallel.
109
+
110
+ Args:
111
+ exchange: Stock exchange to download from
112
+ limit: Maximum number of tickers to download
113
+
114
+ Returns:
115
+ List of dicts with ticker, prices, and dates
116
+ """
117
+ all_tickers = TickersProvider().get_tickers(exchange)
118
+ tickers = all_tickers[:limit]
119
+ logger.info(f"Starting parallel download for {len(tickers)} tickers from {exchange.value}...")
120
  data = download_tickers_parallel(tickers)
121
+ logger.info(f"Downloaded {len(data)} tickers successfully")
122
+ return data
123
 
124
 
125
  if __name__ == "__main__":
src/core/ticker_scanner/scheduler.py CHANGED
@@ -1,17 +1,21 @@
1
  import time
2
  import signal
 
3
 
4
  import schedule
5
 
6
  from src.telegram_bot.logger import main_logger as logger
 
 
 
7
 
8
 
9
  class Scheduler:
10
  """Schedule and manage periodic analysis"""
11
- def __init__(self, exchange: Exchange = Exchange.SP500,
12
- schedule_time: str = "18:00"):
13
  self.exchange = exchange
14
- self.schedule_time = schedule_time
 
15
  self.running = True
16
  # Setup signal handlers for graceful shutdown
17
  signal.signal(signal.SIGINT, self._signal_handler)
@@ -22,31 +26,33 @@ class Scheduler:
22
  logger.info("Received shutdown signal. Cleaning up...")
23
  self.running = False
24
 
25
- def run_scheduled_job(self):
26
  """Execute the analysis job"""
27
  try:
28
- analyzer = AsyncTrendAnalyzer(self.exchange)
29
- asyncio.run(analyzer.run_analysis())
30
- analyzer.cleanup()
 
 
 
 
31
  except Exception as e:
32
  logger.error(f"Scheduled job failed: {e}", exc_info=True)
33
 
34
- def start(self):
35
  """Start the scheduler"""
36
- logger.info(f"Starting scheduled analyzer for {self.exchange.value}")
37
- logger.info(f"Schedule: Daily at {self.schedule_time}")
38
-
39
  # Schedule the job
40
- schedule.every().day.at(self.schedule_time).do(self.run_scheduled_job)
41
-
 
42
  # Run immediately on startup
43
  logger.info("Running initial analysis...")
44
- self.run_scheduled_job()
45
-
46
  # Main scheduler loop
47
  logger.info("Scheduler active. Press Ctrl+C to stop.")
48
  while self.running:
49
  schedule.run_pending()
50
- time.sleep(60) # Check every minute
51
-
52
  logger.info("Scheduler stopped gracefully")
 
1
  import time
2
  import signal
3
+ import asyncio
4
 
5
  import schedule
6
 
7
  from src.telegram_bot.logger import main_logger as logger
8
+ from src.core.ticker_scanner.growth_speed_analyzer import GrowthSpeedAnalyzer
9
+ from src.core.ticker_scanner.parallel_data_downloader import run_parallel_data_downloader
10
+ from src.core.ticker_scanner.ticker_analyzer import TickerAnalyzer
11
 
12
 
13
  class Scheduler:
14
  """Schedule and manage periodic analysis"""
15
+ def __init__(self, exchange: str = "NASDAQ", interval_hours: int = 1, telegram_bot_service=None):
 
16
  self.exchange = exchange
17
+ self.interval_hours = interval_hours
18
+ self.telegram_bot_service = telegram_bot_service
19
  self.running = True
20
  # Setup signal handlers for graceful shutdown
21
  signal.signal(signal.SIGINT, self._signal_handler)
 
26
  logger.info("Received shutdown signal. Cleaning up...")
27
  self.running = False
28
 
29
+ async def run_scheduled_job(self):
30
  """Execute the analysis job"""
31
  try:
32
+ logger.info(f"Starting scheduled analysis for {self.exchange}")
33
+ analyzer = TickerAnalyzer(
34
+ exchange=self.exchange,
35
+ telegram_bot_service=self.telegram_bot_service
36
+ )
37
+ await analyzer.run_analysis()
38
+ logger.info(f"Completed scheduled analysis for {self.exchange}")
39
  except Exception as e:
40
  logger.error(f"Scheduled job failed: {e}", exc_info=True)
41
 
42
+ async def start(self):
43
  """Start the scheduler"""
44
+ logger.info(f"Starting scheduled analyzer for {self.exchange}")
45
+ logger.info(f"Schedule: Every {self.interval_hours} hour(s)")
 
46
  # Schedule the job
47
+ schedule.every(self.interval_hours).hours.do(
48
+ lambda: asyncio.create_task(self.run_scheduled_job())
49
+ )
50
  # Run immediately on startup
51
  logger.info("Running initial analysis...")
52
+ await self.run_scheduled_job()
 
53
  # Main scheduler loop
54
  logger.info("Scheduler active. Press Ctrl+C to stop.")
55
  while self.running:
56
  schedule.run_pending()
57
+ await asyncio.sleep(60) # Check every minute
 
58
  logger.info("Scheduler stopped gracefully")
src/core/ticker_scanner/tickers_provider.py CHANGED
@@ -1,6 +1,7 @@
1
  import pandas as pd
2
 
3
  from src.core.ticker_scanner.core_enums import StockExchange
 
4
 
5
 
6
  class TickersProvider:
@@ -21,8 +22,12 @@ class TickersProvider:
21
  return tickers
22
 
23
  def get_tickers(self, exchange: StockExchange) -> list[str]:
 
24
  if exchange == exchange.NASDAQ:
25
- return self.load_active_nasdaq_tickers()
26
  elif exchange == exchange.NYSE:
27
- return self.load_active_nyse_tickers()
28
- return []
 
 
 
 
1
  import pandas as pd
2
 
3
  from src.core.ticker_scanner.core_enums import StockExchange
4
+ from src.telegram_bot.logger import main_logger as logger
5
 
6
 
7
  class TickersProvider:
 
22
  return tickers
23
 
24
  def get_tickers(self, exchange: StockExchange) -> list[str]:
25
+ logger.info(f"Fetching tickers for {exchange.value}")
26
  if exchange == exchange.NASDAQ:
27
+ tickers = self.load_active_nasdaq_tickers()
28
  elif exchange == exchange.NYSE:
29
+ tickers = self.load_active_nyse_tickers()
30
+ else:
31
+ tickers = []
32
+ logger.info(f"Found {len(tickers)} tickers for {exchange.value}")
33
+ return tickers
src/core/ticker_scanner/trend_analyzer.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ DEPRECATED: This file has been refactored into modular components.
3
+
4
+ Please use the new modular system instead:
5
+ - TickerAnalyzer: Main orchestrator (ticker_analyzer.py)
6
+ - GrowthSpeedAnalyzer: Growth metrics analysis (growth_speed_analyzer.py)
7
+ - Scheduler: Scheduling system (scheduler.py)
8
+ - parallel_data_downloader: Data downloading (parallel_data_downloader.py)
9
+
10
+ Example usage:
11
+ from src.core.ticker_scanner import TickerAnalyzer, Scheduler
12
+
13
+ # One-time analysis
14
+ analyzer = TickerAnalyzer(exchange="NASDAQ", limit=200)
15
+ await analyzer.run_analysis()
16
+
17
+ # Scheduled analysis
18
+ scheduler = Scheduler(exchange="NASDAQ", interval_hours=1)
19
+ await scheduler.start()
20
+ """
21
+
22
+ import logging
23
+
24
+ logger = logging.getLogger(__name__)
25
+ logger.warning(
26
+ "trend_analyzer.py is deprecated. "
27
+ "Use TickerAnalyzer from ticker_analyzer.py instead."
28
+ )
29
+
30
+ # Legacy code archived below for reference
31
+ """
32
+ Original code has been refactored into:
33
+
34
+ 1. ticker_analyzer.py - Main orchestrator (TickerAnalyzer class)
35
+ 2. growth_speed_analyzer.py - Growth analysis logic
36
+ 3. scheduler.py - Scheduling system
37
+ 4. parallel_data_downloader.py - Data downloading
38
+ 5. core_enums.py - Enums and constants
39
+ 6. growth_metrics.py - Data classes for metrics
40
+
41
+ The new system provides:
42
+ - Better separation of concerns
43
+ - Modular and testable components
44
+ - Proper logging integration
45
+ - Type hints and documentation
46
+ - Telegram integration support
47
+ - TradingView link generation
48
+ """
src/core/ticker_scanner/trend_metrics.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+ from datetime import datetime
3
+
4
+ from src.core.ticker_scanner.growth_metrics import GrowthSpeedMetrics
5
+
6
+
7
+ @dataclass
8
+ class TrendMetrics:
9
+ """Comprehensive trend analysis"""
10
+ ticker: str
11
+ # Growth speed
12
+ growth_speed: GrowthSpeedMetrics
13
+ # Trend characteristics
14
+ positive_years_pct: float
15
+ consecutive_positive_years: int
16
+ max_drawdown: float
17
+ recovery_speed: float # Days to recover from max drawdown
18
+ # Consistency metrics
19
+ volatility: float
20
+ beta: float | None # vs market if available
21
+ trend_strength: float # Custom composite 0-1
22
+ # Data quality
23
+ data_points: int
24
+ years_of_data: float
25
+ first_date: datetime
26
+ last_date: datetime
27
+ # Rankings
28
+ rank_by_velocity: int | None = None
29
+ rank_by_consistency: int | None = None
src/telegram_bot/telegram_bot_service.py CHANGED
@@ -24,6 +24,7 @@ from src.services.async_trading_grid_calculator import generate_grid_message
24
  from src.core.fundamental_analysis.async_fundamental_analyzer import AsyncFundamentalAnalyzer
25
  from src.api.insiders.insider_trading_aggregator import InsiderTradingAggregator
26
  from src.telegram_bot.logger import main_logger as logger
 
27
 
28
 
29
  class TelegramBotService:
@@ -186,6 +187,7 @@ class TelegramBotService:
186
  response += "/fa TICKER - Fundamental analysis report (e.g., /fa AAPL)\n"
187
  response += "/insiders - Provides key insider's trades\n"
188
  response += "/insiders NVDA 30 - Insider's trades for the last 30 days\n"
 
189
 
190
  elif base_command == "/status":
191
  response = "βœ… <b>Bot Status: Online</b>\n\n"
@@ -234,6 +236,11 @@ class TelegramBotService:
234
  text=None, user_name=user_name)
235
  return
236
 
 
 
 
 
 
237
  else:
238
  response = f"❓ Unknown command: {command}\n\n"
239
  response += "Type /help to see available commands."
@@ -652,3 +659,47 @@ class TelegramBotService:
652
  else:
653
  logger.info(f"No recent trades found for {ticker}")
654
  await self.send_message_via_proxy(chat_id,f"No recent trades found for {ticker}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  from src.core.fundamental_analysis.async_fundamental_analyzer import AsyncFundamentalAnalyzer
25
  from src.api.insiders.insider_trading_aggregator import InsiderTradingAggregator
26
  from src.telegram_bot.logger import main_logger as logger
27
+ from src.core.ticker_scanner import TickerAnalyzer
28
 
29
 
30
  class TelegramBotService:
 
187
  response += "/fa TICKER - Fundamental analysis report (e.g., /fa AAPL)\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
 
192
  elif base_command == "/status":
193
  response = "βœ… <b>Bot Status: Online</b>\n\n"
 
236
  text=None, user_name=user_name)
237
  return
238
 
239
+ elif base_command == "/scan":
240
+ await self.handle_scan_command(chat_id=chat_id, command_parts=command_parts,
241
+ text=None, user_name=user_name)
242
+ return
243
+
244
  else:
245
  response = f"❓ Unknown command: {command}\n\n"
246
  response += "Type /help to see available commands."
 
659
  else:
660
  logger.info(f"No recent trades found for {ticker}")
661
  await self.send_message_via_proxy(chat_id,f"No recent trades found for {ticker}")
662
+
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"
686
+ loading_msg += "πŸ† Ranking tickers..."
687
+ await self.send_message_via_proxy(chat_id, loading_msg)
688
+ try:
689
+ # Create analyzer and run analysis
690
+ analyzer = TickerAnalyzer(
691
+ exchange=exchange,
692
+ telegram_bot_service=self,
693
+ limit=200 # Limit to 200 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"
704
+ error_msg += "Please try again later."
705
+ await self.send_message_via_proxy(chat_id, error_msg)