Added my stock-price-tracker#50
Conversation
|
👋 @HARI21HP |
There was a problem hiding this comment.
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}") |
There was a problem hiding this comment.
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}") |
There was a problem hiding this comment.
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} |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Import of 'datetime' is not used.
| from datetime import datetime |
| Author: Python-Projects Contributors | ||
| """ | ||
|
|
||
| import os |
There was a problem hiding this comment.
Import of 'os' is not used.
| import os |
|
|
||
| import yfinance as yf | ||
| import pandas as pd | ||
| from datetime import datetime, timedelta |
There was a problem hiding this comment.
Import of 'datetime' is not used.
Import of 'timedelta' is not used.
| from datetime import datetime, timedelta |
|
@HARI21HP Please implement the changes suggested by Copliot. |
|
This PR is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 10 days. |
No description provided.