-
Notifications
You must be signed in to change notification settings - Fork 46
Added my stock-price-tracker #50
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
|
👋 @HARI21HP |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds a comprehensive Stock Price Tracker application that fetches stock data from Yahoo Finance, creates visualizations, and sends Telegram alerts for price movements. The implementation is well-structured with separate utility modules for data fetching, plotting, and notifications.
Key Changes:
- New Stock Price Tracker application with CLI interface
- Yahoo Finance integration for real-time and historical stock data
- Matplotlib and Plotly-based visualization system with multiple chart types
- Telegram bot integration for automated price alerts and notifications
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 28 comments.
Show a summary per file
| File | Description |
|---|---|
Stock-Price-Tracker/stock_tracker.py |
Main CLI application orchestrating stock tracking, visualization, and alerts |
Stock-Price-Tracker/utils/yahoo_finance.py |
Yahoo Finance API wrapper providing stock data fetching and price calculations |
Stock-Price-Tracker/utils/telegram_alert.py |
Telegram bot integration for sending formatted stock price alerts |
Stock-Price-Tracker/utils/plotter.py |
Visualization module creating price trends, candlestick charts, and summary reports |
Stock-Price-Tracker/utils/__init__.py |
Package initialization exposing main utility classes |
Stock-Price-Tracker/requirements.txt |
Python dependencies including yfinance, pandas, matplotlib, plotly, and telegram bot |
Stock-Price-Tracker/config/.env.example |
Environment variable template for Telegram bot configuration |
Stock-Price-Tracker/README.md |
Comprehensive documentation with features, installation, usage examples, and project structure |
Comments suppressed due to low confidence (1)
Stock-Price-Tracker/utils/yahoo_finance.py:204
- Except block directly handles BaseException.
except:
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| '52_week_low': info.get('fiftyTwoWeekLow', 'N/A'), | ||
| } | ||
| except Exception as e: | ||
| print(f"Error fetching stock info: {e}") |
Copilot
AI
Dec 7, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error message only prints the exception object but doesn't provide context about which ticker failed or guidance for users. Consider enhancing it to: print(f"Error fetching stock info for {self.ticker}: {e}") to make debugging easier.
| print(f"Error fetching stock info: {e}") | |
| print(f"Error fetching stock info for {self.ticker}: {e}") |
| 'lowest': round(lowest_price, 2) | ||
| } | ||
| except Exception as e: | ||
| print(f"Error calculating price change: {e}") |
Copilot
AI
Dec 7, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Error message should include the ticker symbol for context: print(f"Error calculating price change for {self.ticker}: {e}")
| print(f"Error calculating price change: {e}") | |
| print(f"Error calculating price change for {self.ticker}: {e}") |
| Change: ${stats.get('change', 'N/A')} ({stats.get('change_percent', 'N/A')}%) | ||
| Highest: ${stats.get('highest', 'N/A')} | ||
| Lowest: ${stats.get('lowest', 'N/A')} | ||
| Range: ${stats.get('highest', 0) - stats.get('lowest', 0):.2f} |
Copilot
AI
Dec 7, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This calculation ${stats.get('highest', 0) - stats.get('lowest', 0):.2f} can produce incorrect results if either 'highest' or 'lowest' is missing (defaulting to 0). The range calculation should handle missing values more gracefully, perhaps by checking if both values exist first or using a more appropriate default like 'N/A'.
| info = self.stock.info | ||
| return 'symbol' in info or 'longName' in info |
Copilot
AI
Dec 7, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to get_current_price(), this method fetches all stock info via an API call just to validate the ticker. This is inefficient if called multiple times. Consider caching the validation result or implementing a lighter validation method.
|
|
||
| # Plot closing price | ||
| plt.plot(data.index, data['Close'], label='Close Price', | ||
| color='#2E86C1', linewidth=2) | ||
|
|
||
| # Add moving averages | ||
| if len(data) >= 7: | ||
| ma7 = data['Close'].rolling(window=7).mean() | ||
| plt.plot(data.index, ma7, label='7-Day MA', | ||
| color='#E67E22', linestyle='--', linewidth=1.5) | ||
|
|
||
| if len(data) >= 30: | ||
| ma30 = data['Close'].rolling(window=30).mean() | ||
| plt.plot(data.index, ma30, label='30-Day MA', | ||
| color='#27AE60', linestyle='--', linewidth=1.5) | ||
|
|
||
| # Formatting | ||
| if title is None: | ||
| title = f'{self.ticker} Stock Price Trend' | ||
| plt.title(title, fontsize=16, fontweight='bold', pad=20) | ||
| plt.xlabel('Date', fontsize=12, fontweight='bold') | ||
| plt.ylabel('Price ($)', fontsize=12, fontweight='bold') | ||
| plt.legend(loc='best', fontsize=10) | ||
| plt.grid(True, alpha=0.3, linestyle='--') | ||
|
|
||
| # Format x-axis | ||
| plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d')) | ||
| plt.gcf().autofmt_xdate() | ||
|
|
||
| plt.tight_layout() | ||
|
|
||
| if save_path: | ||
| plt.savefig(save_path, dpi=300, bbox_inches='tight') | ||
| print(f"Plot saved to {save_path}") | ||
|
|
||
| if show_plot: | ||
| plt.show() | ||
| else: | ||
| plt.close() |
Copilot
AI
Dec 7, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Creating a new figure without explicitly closing it when show_plot=False can lead to memory leaks in long-running applications. While plt.close() is called on line 81, if any exception occurs between lines 42-80, the figure remains in memory. Consider using a context manager or adding a try-finally block to ensure cleanup.
| # Plot closing price | |
| plt.plot(data.index, data['Close'], label='Close Price', | |
| color='#2E86C1', linewidth=2) | |
| # Add moving averages | |
| if len(data) >= 7: | |
| ma7 = data['Close'].rolling(window=7).mean() | |
| plt.plot(data.index, ma7, label='7-Day MA', | |
| color='#E67E22', linestyle='--', linewidth=1.5) | |
| if len(data) >= 30: | |
| ma30 = data['Close'].rolling(window=30).mean() | |
| plt.plot(data.index, ma30, label='30-Day MA', | |
| color='#27AE60', linestyle='--', linewidth=1.5) | |
| # Formatting | |
| if title is None: | |
| title = f'{self.ticker} Stock Price Trend' | |
| plt.title(title, fontsize=16, fontweight='bold', pad=20) | |
| plt.xlabel('Date', fontsize=12, fontweight='bold') | |
| plt.ylabel('Price ($)', fontsize=12, fontweight='bold') | |
| plt.legend(loc='best', fontsize=10) | |
| plt.grid(True, alpha=0.3, linestyle='--') | |
| # Format x-axis | |
| plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d')) | |
| plt.gcf().autofmt_xdate() | |
| plt.tight_layout() | |
| if save_path: | |
| plt.savefig(save_path, dpi=300, bbox_inches='tight') | |
| print(f"Plot saved to {save_path}") | |
| if show_plot: | |
| plt.show() | |
| else: | |
| plt.close() | |
| try: | |
| # Plot closing price | |
| plt.plot(data.index, data['Close'], label='Close Price', | |
| color='#2E86C1', linewidth=2) | |
| # Add moving averages | |
| if len(data) >= 7: | |
| ma7 = data['Close'].rolling(window=7).mean() | |
| plt.plot(data.index, ma7, label='7-Day MA', | |
| color='#E67E22', linestyle='--', linewidth=1.5) | |
| if len(data) >= 30: | |
| ma30 = data['Close'].rolling(window=30).mean() | |
| plt.plot(data.index, ma30, label='30-Day MA', | |
| color='#27AE60', linestyle='--', linewidth=1.5) | |
| # Formatting | |
| if title is None: | |
| title = f'{self.ticker} Stock Price Trend' | |
| plt.title(title, fontsize=16, fontweight='bold', pad=20) | |
| plt.xlabel('Date', fontsize=12, fontweight='bold') | |
| plt.ylabel('Price ($)', fontsize=12, fontweight='bold') | |
| plt.legend(loc='best', fontsize=10) | |
| plt.grid(True, alpha=0.3, linestyle='--') | |
| # Format x-axis | |
| plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d')) | |
| plt.gcf().autofmt_xdate() | |
| plt.tight_layout() | |
| if save_path: | |
| plt.savefig(save_path, dpi=300, bbox_inches='tight') | |
| print(f"Plot saved to {save_path}") | |
| if show_plot: | |
| plt.show() | |
| finally: | |
| if not show_plot: | |
| plt.close() |
| Returns: | ||
| float: Current price or None if unavailable | ||
| """ | ||
| try: |
Copilot
AI
Dec 7, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method calls self.stock.info which makes an API request to fetch all stock information just to get the current price. Consider caching the info data or using a more specific API call if available. If this method is called frequently, it could lead to rate limiting or unnecessary network overhead.
| try: | |
| try: | |
| # Try fast_info for quick price access | |
| price = None | |
| if hasattr(self.stock, "fast_info"): | |
| price = getattr(self.stock.fast_info, "last_price", None) | |
| if price is not None: | |
| return price | |
| # Fallback: use history for the latest close price | |
| hist = self.stock.history(period="1d", interval="1m") | |
| if not hist.empty: | |
| # Use the last available price (close or regularMarketPrice) | |
| price = hist["Close"].iloc[-1] | |
| if pd.notnull(price): | |
| return float(price) | |
| # Last resort: use info (slow) |
| gs = fig.add_gridspec(3, 2, hspace=0.3, wspace=0.3) | ||
|
|
||
| # Title | ||
| fig.suptitle(f'{self.ticker} Stock Analysis Report', | ||
| fontsize=18, fontweight='bold', y=0.98) | ||
|
|
||
| # Main price chart | ||
| ax1 = fig.add_subplot(gs[0:2, :]) | ||
| ax1.plot(data.index, data['Close'], label='Close Price', | ||
| color='#2E86C1', linewidth=2.5) | ||
| ax1.fill_between(data.index, data['Low'], data['High'], | ||
| alpha=0.2, color='#85C1E9', label='Daily Range') | ||
| ax1.set_title('Price Trend with Daily Range', fontweight='bold', fontsize=14) | ||
| ax1.set_ylabel('Price ($)', fontsize=12) | ||
| ax1.legend(loc='best') | ||
| ax1.grid(True, alpha=0.3) | ||
|
|
||
| # Statistics text box | ||
| ax2 = fig.add_subplot(gs[2, 0]) | ||
| ax2.axis('off') | ||
| stats_text = f""" | ||
| STATISTICS SUMMARY | ||
| ━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
| Start Price: ${stats.get('start_price', 'N/A')} | ||
| End Price: ${stats.get('end_price', 'N/A')} | ||
| Change: ${stats.get('change', 'N/A')} ({stats.get('change_percent', 'N/A')}%) | ||
| Highest: ${stats.get('highest', 'N/A')} | ||
| Lowest: ${stats.get('lowest', 'N/A')} | ||
| Range: ${stats.get('highest', 0) - stats.get('lowest', 0):.2f} | ||
| """ | ||
| ax2.text(0.1, 0.5, stats_text, fontsize=11, family='monospace', | ||
| verticalalignment='center', bbox=dict(boxstyle='round', | ||
| facecolor='wheat', alpha=0.5)) | ||
|
|
||
| # Volume chart | ||
| ax3 = fig.add_subplot(gs[2, 1]) | ||
| ax3.bar(data.index, data['Volume'], color='#3498DB', alpha=0.6) | ||
| ax3.set_title('Trading Volume', fontweight='bold', fontsize=12) | ||
| ax3.set_ylabel('Volume', fontsize=10) | ||
| ax3.grid(True, alpha=0.3, axis='y') | ||
| ax3.tick_params(axis='x', rotation=45) | ||
|
|
||
| if save_path: | ||
| plt.savefig(save_path, dpi=300, bbox_inches='tight') | ||
| print(f"Summary report saved to {save_path}") | ||
|
|
||
| if show_plot: | ||
| plt.show() | ||
| else: | ||
| plt.close() |
Copilot
AI
Dec 7, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same figure cleanup issue: figure created without guaranteed cleanup if an exception occurs before line 339. Consider using try-finally or a context manager.
| gs = fig.add_gridspec(3, 2, hspace=0.3, wspace=0.3) | |
| # Title | |
| fig.suptitle(f'{self.ticker} Stock Analysis Report', | |
| fontsize=18, fontweight='bold', y=0.98) | |
| # Main price chart | |
| ax1 = fig.add_subplot(gs[0:2, :]) | |
| ax1.plot(data.index, data['Close'], label='Close Price', | |
| color='#2E86C1', linewidth=2.5) | |
| ax1.fill_between(data.index, data['Low'], data['High'], | |
| alpha=0.2, color='#85C1E9', label='Daily Range') | |
| ax1.set_title('Price Trend with Daily Range', fontweight='bold', fontsize=14) | |
| ax1.set_ylabel('Price ($)', fontsize=12) | |
| ax1.legend(loc='best') | |
| ax1.grid(True, alpha=0.3) | |
| # Statistics text box | |
| ax2 = fig.add_subplot(gs[2, 0]) | |
| ax2.axis('off') | |
| stats_text = f""" | |
| STATISTICS SUMMARY | |
| ━━━━━━━━━━━━━━━━━━━━━━━━━ | |
| Start Price: ${stats.get('start_price', 'N/A')} | |
| End Price: ${stats.get('end_price', 'N/A')} | |
| Change: ${stats.get('change', 'N/A')} ({stats.get('change_percent', 'N/A')}%) | |
| Highest: ${stats.get('highest', 'N/A')} | |
| Lowest: ${stats.get('lowest', 'N/A')} | |
| Range: ${stats.get('highest', 0) - stats.get('lowest', 0):.2f} | |
| """ | |
| ax2.text(0.1, 0.5, stats_text, fontsize=11, family='monospace', | |
| verticalalignment='center', bbox=dict(boxstyle='round', | |
| facecolor='wheat', alpha=0.5)) | |
| # Volume chart | |
| ax3 = fig.add_subplot(gs[2, 1]) | |
| ax3.bar(data.index, data['Volume'], color='#3498DB', alpha=0.6) | |
| ax3.set_title('Trading Volume', fontweight='bold', fontsize=12) | |
| ax3.set_ylabel('Volume', fontsize=10) | |
| ax3.grid(True, alpha=0.3, axis='y') | |
| ax3.tick_params(axis='x', rotation=45) | |
| if save_path: | |
| plt.savefig(save_path, dpi=300, bbox_inches='tight') | |
| print(f"Summary report saved to {save_path}") | |
| if show_plot: | |
| plt.show() | |
| else: | |
| plt.close() | |
| try: | |
| gs = fig.add_gridspec(3, 2, hspace=0.3, wspace=0.3) | |
| # Title | |
| fig.suptitle(f'{self.ticker} Stock Analysis Report', | |
| fontsize=18, fontweight='bold', y=0.98) | |
| # Main price chart | |
| ax1 = fig.add_subplot(gs[0:2, :]) | |
| ax1.plot(data.index, data['Close'], label='Close Price', | |
| color='#2E86C1', linewidth=2.5) | |
| ax1.fill_between(data.index, data['Low'], data['High'], | |
| alpha=0.2, color='#85C1E9', label='Daily Range') | |
| ax1.set_title('Price Trend with Daily Range', fontweight='bold', fontsize=14) | |
| ax1.set_ylabel('Price ($)', fontsize=12) | |
| ax1.legend(loc='best') | |
| ax1.grid(True, alpha=0.3) | |
| # Statistics text box | |
| ax2 = fig.add_subplot(gs[2, 0]) | |
| ax2.axis('off') | |
| stats_text = f""" | |
| STATISTICS SUMMARY | |
| ━━━━━━━━━━━━━━━━━━━━━━━━━ | |
| Start Price: ${stats.get('start_price', 'N/A')} | |
| End Price: ${stats.get('end_price', 'N/A')} | |
| Change: ${stats.get('change', 'N/A')} ({stats.get('change_percent', 'N/A')}%) | |
| Highest: ${stats.get('highest', 'N/A')} | |
| Lowest: ${stats.get('lowest', 'N/A')} | |
| Range: ${stats.get('highest', 0) - stats.get('lowest', 0):.2f} | |
| """ | |
| ax2.text(0.1, 0.5, stats_text, fontsize=11, family='monospace', | |
| verticalalignment='center', bbox=dict(boxstyle='round', | |
| facecolor='wheat', alpha=0.5)) | |
| # Volume chart | |
| ax3 = fig.add_subplot(gs[2, 1]) | |
| ax3.bar(data.index, data['Volume'], color='#3498DB', alpha=0.6) | |
| ax3.set_title('Trading Volume', fontweight='bold', fontsize=12) | |
| ax3.set_ylabel('Volume', fontsize=10) | |
| ax3.grid(True, alpha=0.3, axis='y') | |
| ax3.tick_params(axis='x', rotation=45) | |
| if save_path: | |
| plt.savefig(save_path, dpi=300, bbox_inches='tight') | |
| print(f"Summary report saved to {save_path}") | |
| if show_plot: | |
| plt.show() | |
| finally: | |
| plt.close(fig) |
| import matplotlib.dates as mdates | ||
| import plotly.graph_objects as go | ||
| from plotly.subplots import make_subplots | ||
| from datetime import datetime |
Copilot
AI
Dec 7, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Import of 'datetime' is not used.
| from datetime import datetime |
| Author: Python-Projects Contributors | ||
| """ | ||
|
|
||
| import os |
Copilot
AI
Dec 7, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Import of 'os' is not used.
| import os |
|
|
||
| import yfinance as yf | ||
| import pandas as pd | ||
| from datetime import datetime, timedelta |
Copilot
AI
Dec 7, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Import of 'datetime' is not used.
Import of 'timedelta' is not used.
| from datetime import datetime, timedelta |
No description provided.