diff --git a/Stock-Price-Tracker/README.md b/Stock-Price-Tracker/README.md new file mode 100644 index 0000000..b946f2c --- /dev/null +++ b/Stock-Price-Tracker/README.md @@ -0,0 +1,298 @@ +# Stock Price Tracker šŸ“ˆ + +A comprehensive Python application that tracks stock prices using Yahoo Finance API, visualizes daily/weekly trends with interactive charts, and sends automated Telegram notifications for price movements and alerts. + +![Python](https://img.shields.io/badge/python-3.8+-blue.svg) +![License](https://img.shields.io/badge/license-MIT-green.svg) +![Status](https://img.shields.io/badge/status-active-success.svg) + +## ✨ Features + +- šŸ“Š **Real-time Stock Data**: Fetch live stock prices and historical data using Yahoo Finance API +- šŸ“ˆ **Daily & Weekly Trends**: Visualize price movements over different timeframes +- šŸŽØ **Multiple Chart Types**: Line charts, candlestick charts, volume analysis, and summary reports +- šŸ“± **Telegram Alerts**: Receive instant notifications for price changes and threshold alerts +- šŸŽÆ **Price Thresholds**: Set custom price alerts and get notified when triggered +- šŸ“‰ **Technical Analysis**: Moving averages, daily returns, and price distribution analysis +- šŸ’¾ **Export Functionality**: Save charts as PNG/HTML files for reporting +- šŸ–„ļø **Command-line Interface**: Easy-to-use CLI with multiple options + +## šŸ“‹ Requirements + +- Python 3.8 or higher +- Internet connection (for fetching stock data) +- Telegram Bot (optional, for alerts) + +## šŸš€ Installation + +1. **Clone or download this project** + ```bash + cd Stock-Price-Tracker + ``` + +2. **Install required dependencies** + ```bash + pip install -r requirements.txt + ``` + +3. **Set up Telegram Bot (Optional but recommended)** + + To receive price alerts via Telegram: + + - **Create a Telegram Bot:** + 1. Open Telegram and search for `@BotFather` + 2. Send `/newbot` command + 3. Follow instructions to create your bot + 4. Copy the **Bot Token** provided + + - **Get your Chat ID:** + 1. Start a chat with your new bot + 2. Search for `@userinfobot` on Telegram + 3. Start a chat and it will show your **Chat ID** + + - **Configure environment variables:** + ```bash + # Copy the example file + cp config/.env.example config/.env + + # Edit config/.env and add your credentials + TELEGRAM_BOT_TOKEN=your_bot_token_here + TELEGRAM_CHAT_ID=your_chat_id_here + ``` + +## šŸ“– Usage + +### Basic Commands + +**Track daily prices:** +```bash +python stock_tracker.py AAPL --daily +``` + +**Track weekly prices:** +```bash +python stock_tracker.py GOOGL --weekly +``` + +**Get stock information:** +```bash +python stock_tracker.py TSLA --info +``` + +### Advanced Usage + +**Track with custom timeframe:** +```bash +# 60 days of daily data +python stock_tracker.py MSFT --daily --days 60 + +# 24 weeks of weekly data +python stock_tracker.py AMZN --weekly --weeks 24 +``` + +**Save charts without displaying:** +```bash +python stock_tracker.py AAPL --daily --save-plots --no-plot +``` + +**Enable Telegram alerts:** +```bash +# Track with alert notification +python stock_tracker.py TSLA --daily --telegram --alert + +# Check price threshold and alert +python stock_tracker.py AAPL --threshold 150 --threshold-type above --telegram +``` + +**Price threshold monitoring:** +```bash +# Alert when price goes above $200 +python stock_tracker.py GOOGL --threshold 200 --threshold-type above --telegram + +# Alert when price goes below $100 +python stock_tracker.py MSFT --threshold 100 --threshold-type below --telegram +``` + +### Command-line Arguments + +| Argument | Description | Default | +|----------|-------------|---------| +| `ticker` | Stock ticker symbol (required) | - | +| `--daily` | Track daily prices | False | +| `--weekly` | Track weekly prices | False | +| `--days` | Number of days to track | 30 | +| `--weeks` | Number of weeks to track | 12 | +| `--plot` | Display plots | True | +| `--no-plot` | Don't display plots | False | +| `--save-plots` | Save plots to files | False | +| `--telegram` | Enable Telegram alerts | False | +| `--alert` | Send Telegram alert | False | +| `--threshold` | Price threshold for alerts | None | +| `--threshold-type` | Threshold type (above/below) | above | +| `--info` | Display stock info only | False | + +## šŸ“Š Example Output + +### Console Output +``` +============================================================ +šŸ“ˆ STOCK PRICE TRACKER +============================================================ +šŸ” Validating ticker symbol: AAPL +āœ… Ticker AAPL is valid + +============================================================ +šŸ“Š STOCK INFORMATION: AAPL +============================================================ +Company: Apple Inc. +Sector: Technology +Industry: Consumer Electronics +Current Price: $178.45 +Previous Close: $175.20 +52W High: $198.23 +52W Low: $124.17 +Market Cap: $2850.00B +============================================================ + +šŸ“ˆ Fetching 30 days of data for AAPL... +āœ… Retrieved 30 days of data + +──────────────────────────────────────────────────────────── +šŸ“Š DAILY STATISTICS +──────────────────────────────────────────────────────────── +Start Price: $165.30 +End Price: $178.45 +Change: $13.15 (7.95%) +Highest: $180.50 +Lowest: $164.90 +──────────────────────────────────────────────────────────── +``` + +### Telegram Alert Examples + +**Daily Summary Alert:** +``` +šŸ“Š DAILY SUMMARY: AAPL + +šŸ“ˆ Overall: UP 7.95% + +šŸ’° Opening: $165.30 +šŸ’µ Closing: $178.45 +šŸ”¼ High: $180.50 +šŸ”½ Low: $164.90 +šŸ“Š Change: $13.15 (+7.95%) + +šŸ“… Date: 2025-12-08 10:30:45 +``` + +**Threshold Alert:** +``` +šŸ”” THRESHOLD ALERT: AAPL + +ā†—ļø Price is now above $175.00! + +šŸ’° Current Price: $178.45 +šŸŽÆ Threshold: $175.00 + +šŸ“… Time: 2025-12-08 10:30:45 +``` + +## šŸ“ Project Structure + +``` +Stock-Price-Tracker/ +ā”œā”€ā”€ stock_tracker.py # Main application +ā”œā”€ā”€ utils/ +│ ā”œā”€ā”€ __init__.py +│ ā”œā”€ā”€ yahoo_finance.py # Yahoo Finance API integration +│ ā”œā”€ā”€ plotter.py # Chart generation & visualization +│ └── telegram_alert.py # Telegram bot integration +ā”œā”€ā”€ config/ +│ └── .env.example # Environment variables template +ā”œā”€ā”€ assets/ +│ └── img/ # Generated charts and screenshots +ā”œā”€ā”€ requirements.txt # Python dependencies +└── README.md # Documentation +``` + +## šŸ› ļø Tech Stack + +| Technology | Purpose | +|------------|---------| +| **yfinance** | Yahoo Finance API for stock data | +| **pandas** | Data manipulation and analysis | +| **matplotlib** | Static chart generation | +| **plotly** | Interactive candlestick charts | +| **python-telegram-bot** | Telegram bot integration | +| **python-dotenv** | Environment variable management | + +## šŸ“ˆ Visualization Features + +1. **Price Trend Chart** + - Line chart with closing prices + - 7-day and 30-day moving averages + - Clean, professional styling + +2. **Candlestick Chart** + - Interactive OHLC (Open, High, Low, Close) visualization + - Volume bar chart + - Zoom and pan capabilities + +3. **Volume Analysis** + - Price vs. volume correlation + - Average volume indicators + - Color-coded volume bars + +4. **Summary Report** + - Comprehensive multi-panel view + - Price statistics summary + - Daily range visualization + +## šŸ”” Telegram Alerts + +The app sends various types of alerts: + +- **šŸ“Š Price Updates**: Regular stock price summaries +- **🚨 Threshold Alerts**: Notifications when price crosses set levels +- **šŸ“ˆ Daily Summaries**: End-of-day price reports with statistics +- **āŒ Error Alerts**: Notifications for any issues or errors + +## šŸŽÆ Use Cases + +- **Day Traders**: Monitor intraday price movements with alerts +- **Long-term Investors**: Track weekly trends and major price changes +- **Portfolio Monitoring**: Set threshold alerts for multiple stocks +- **Market Research**: Analyze historical trends and patterns +- **Automated Reporting**: Generate and save charts for presentations + +## šŸ” Supported Stock Symbols + +Any valid ticker symbol from Yahoo Finance, including: +- **US Stocks**: AAPL, GOOGL, MSFT, TSLA, AMZN, etc. +- **International**: Use country suffix (e.g., RELIANCE.NS for India) +- **ETFs**: SPY, QQQ, VTI, etc. +- **Cryptocurrencies**: BTC-USD, ETH-USD, etc. + +## šŸ¤ Contributing + +Contributions are welcome! Feel free to: +- Report bugs or issues +- Suggest new features +- Improve documentation +- Submit pull requests + +## šŸ“ License + +This project is open source and available for educational purposes. + +## āš ļø Disclaimer + +This tool is for informational and educational purposes only. It should not be considered as financial advice. Always do your own research before making investment decisions. + +## šŸ‘Øā€šŸ’» Author + +Created as part of the [Python-Projects](https://github.com/Grow-with-Open-Source/Python-Projects) repository. + +--- + +**Happy Trading! šŸ“ˆšŸ’°** diff --git a/Stock-Price-Tracker/config/.env.example b/Stock-Price-Tracker/config/.env.example new file mode 100644 index 0000000..efcae89 --- /dev/null +++ b/Stock-Price-Tracker/config/.env.example @@ -0,0 +1,9 @@ +# Telegram Bot Configuration +# Get your bot token from @BotFather on Telegram +TELEGRAM_BOT_TOKEN=your_bot_token_here + +# Get your chat ID from @userinfobot on Telegram +TELEGRAM_CHAT_ID=your_chat_id_here + +# Optional: Set price alert thresholds (percentage) +PRICE_ALERT_THRESHOLD=5.0 diff --git a/Stock-Price-Tracker/requirements.txt b/Stock-Price-Tracker/requirements.txt new file mode 100644 index 0000000..d32a0fd --- /dev/null +++ b/Stock-Price-Tracker/requirements.txt @@ -0,0 +1,7 @@ +yfinance>=0.2.32 +pandas>=2.0.0 +matplotlib>=3.7.0 +plotly>=5.17.0 +python-telegram-bot>=20.0 +python-dotenv>=1.0.0 +kaleido>=0.2.1 diff --git a/Stock-Price-Tracker/stock_tracker.py b/Stock-Price-Tracker/stock_tracker.py new file mode 100644 index 0000000..357a8b1 --- /dev/null +++ b/Stock-Price-Tracker/stock_tracker.py @@ -0,0 +1,347 @@ +#!/usr/bin/env python3 +""" +Stock Price Tracker +Track stock prices, visualize trends, and send Telegram alerts + +Author: Python-Projects Contributors +""" + +import os +import sys +import argparse +from datetime import datetime +from pathlib import Path +from dotenv import load_dotenv + +# Add utils to path +sys.path.insert(0, str(Path(__file__).parent)) + +from utils.yahoo_finance import StockDataFetcher +from utils.plotter import StockPlotter +from utils.telegram_alert import TelegramAlert + + +class StockTracker: + """Main Stock Price Tracker Application""" + + def __init__(self, ticker: str, use_telegram: bool = False): + """ + Initialize Stock Tracker + + Args: + ticker (str): Stock ticker symbol + use_telegram (bool): Enable Telegram alerts + """ + self.ticker = ticker.upper() + self.fetcher = StockDataFetcher(self.ticker) + self.plotter = StockPlotter(self.ticker) + self.telegram = TelegramAlert() if use_telegram else None + + def validate_ticker(self) -> bool: + """Validate if ticker symbol is valid""" + print(f"šŸ” Validating ticker symbol: {self.ticker}") + if self.fetcher.is_valid_ticker(): + print(f"āœ… Ticker {self.ticker} is valid") + return True + else: + print(f"āŒ Invalid ticker symbol: {self.ticker}") + return False + + def display_stock_info(self): + """Display basic stock information""" + print(f"\n{'='*60}") + print(f"šŸ“Š STOCK INFORMATION: {self.ticker}") + print(f"{'='*60}") + + info = self.fetcher.get_stock_info() + if info: + print(f"Company: {info.get('name', 'N/A')}") + print(f"Sector: {info.get('sector', 'N/A')}") + print(f"Industry: {info.get('industry', 'N/A')}") + print(f"Current Price: ${info.get('current_price', 'N/A')}") + print(f"Previous Close: ${info.get('previous_close', 'N/A')}") + print(f"52W High: ${info.get('52_week_high', 'N/A')}") + print(f"52W Low: ${info.get('52_week_low', 'N/A')}") + + if isinstance(info.get('market_cap'), (int, float)): + market_cap_b = info['market_cap'] / 1e9 + print(f"Market Cap: ${market_cap_b:.2f}B") + print(f"{'='*60}\n") + else: + print("āŒ Unable to fetch stock information\n") + + def track_daily(self, days: int = 30, plot: bool = True, + save_plots: bool = False, send_alert: bool = False): + """ + Track daily stock prices + + Args: + days (int): Number of days to track + plot (bool): Display plots + save_plots (bool): Save plots to files + send_alert (bool): Send Telegram alert + """ + print(f"\nšŸ“ˆ Fetching {days} days of data for {self.ticker}...") + data = self.fetcher.get_daily_data(days) + + if data.empty: + print("āŒ No data available") + return + + print(f"āœ… Retrieved {len(data)} days of data") + + # Calculate statistics + stats = self.fetcher.calculate_price_change(data) + + # Display statistics + self._display_statistics(stats, "Daily") + + # Create plots + if plot or save_plots: + self._create_plots(data, stats, "daily", plot, save_plots) + + # Send Telegram alert + if send_alert and self.telegram and self.telegram.is_configured(): + print("\nšŸ“± Sending Telegram alert...") + success = self.telegram.send_stock_alert(self.ticker, stats, "daily_summary") + if success: + print("āœ… Alert sent successfully") + else: + print("āŒ Failed to send alert") + + def track_weekly(self, weeks: int = 12, plot: bool = True, + save_plots: bool = False, send_alert: bool = False): + """ + Track weekly stock prices + + Args: + weeks (int): Number of weeks to track + plot (bool): Display plots + save_plots (bool): Save plots to files + send_alert (bool): Send Telegram alert + """ + print(f"\nšŸ“Š Fetching {weeks} weeks of data for {self.ticker}...") + data = self.fetcher.get_weekly_data(weeks) + + if data.empty: + print("āŒ No data available") + return + + print(f"āœ… Retrieved {len(data)} weeks of data") + + # Calculate statistics + stats = self.fetcher.calculate_price_change(data) + + # Display statistics + self._display_statistics(stats, "Weekly") + + # Create plots + if plot or save_plots: + self._create_plots(data, stats, "weekly", plot, save_plots) + + # Send Telegram alert + if send_alert and self.telegram and self.telegram.is_configured(): + print("\nšŸ“± Sending Telegram alert...") + success = self.telegram.send_stock_alert(self.ticker, stats, "update") + if success: + print("āœ… Alert sent successfully") + else: + print("āŒ Failed to send alert") + + def check_price_threshold(self, threshold: float, threshold_type: str = "above"): + """ + Check if current price crosses a threshold + + Args: + threshold (float): Price threshold + threshold_type (str): 'above' or 'below' + """ + print(f"\nšŸŽÆ Checking price threshold...") + current_price = self.fetcher.get_current_price() + + if current_price is None: + print("āŒ Unable to fetch current price") + return + + print(f"Current Price: ${current_price:.2f}") + print(f"Threshold: ${threshold:.2f} ({threshold_type})") + + triggered = False + if threshold_type == "above" and current_price > threshold: + triggered = True + print(f"āœ… Price is above threshold!") + elif threshold_type == "below" and current_price < threshold: + triggered = True + print(f"āœ… Price is below threshold!") + else: + print(f"ā„¹ļø Threshold not triggered") + + if triggered and self.telegram and self.telegram.is_configured(): + print("\nšŸ“± Sending threshold alert...") + success = self.telegram.send_price_threshold_alert( + self.ticker, current_price, threshold, threshold_type + ) + if success: + print("āœ… Alert sent successfully") + + def _display_statistics(self, stats: dict, timeframe: str): + """Display price change statistics""" + print(f"\n{'─'*60}") + print(f"šŸ“Š {timeframe.upper()} STATISTICS") + print(f"{'─'*60}") + print(f"Start Price: ${stats.get('start_price', 'N/A')}") + print(f"End Price: ${stats.get('end_price', 'N/A')}") + print(f"Change: ${stats.get('change', 'N/A')} ({stats.get('change_percent', 'N/A')}%)") + print(f"Highest: ${stats.get('highest', 'N/A')}") + print(f"Lowest: ${stats.get('lowest', 'N/A')}") + print(f"{'─'*60}\n") + + def _create_plots(self, data, stats, timeframe: str, + show: bool, save: bool): + """Create and save plots""" + assets_dir = Path(__file__).parent / "assets" / "img" + assets_dir.mkdir(parents=True, exist_ok=True) + + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + + # Price trend plot + print(f"šŸ“Š Creating price trend plot...") + save_path = None + if save: + save_path = str(assets_dir / f"{self.ticker}_{timeframe}_trend_{timestamp}.png") + + self.plotter.plot_price_trend( + data, + title=f"{self.ticker} {timeframe.capitalize()} Price Trend", + save_path=save_path, + show_plot=show + ) + + # Candlestick chart (only for daily data with OHLC) + if timeframe == "daily" and len(data) > 0: + print(f"šŸ“Š Creating candlestick chart...") + save_path = None + if save: + save_path = str(assets_dir / f"{self.ticker}_candlestick_{timestamp}.html") + + self.plotter.plot_candlestick( + data, + save_path=save_path, + show_plot=show + ) + + # Summary report + print(f"šŸ“Š Creating summary report...") + save_path = None + if save: + save_path = str(assets_dir / f"{self.ticker}_summary_{timestamp}.png") + + self.plotter.create_summary_report( + data, stats, + save_path=save_path, + show_plot=show + ) + + +def main(): + """Main function""" + parser = argparse.ArgumentParser( + description="Stock Price Tracker - Track stocks, visualize trends, send alerts", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Track daily prices for Apple + python stock_tracker.py AAPL --daily + + # Track weekly prices for Google with plots + python stock_tracker.py GOOGL --weekly --plot + + # Track with Telegram alerts + python stock_tracker.py TSLA --daily --telegram --alert + + # Save plots without displaying + python stock_tracker.py MSFT --daily --save-plots --no-plot + + # Check price threshold + python stock_tracker.py AAPL --threshold 150 --threshold-type above --telegram + """ + ) + + parser.add_argument('ticker', type=str, help='Stock ticker symbol (e.g., AAPL, GOOGL, TSLA)') + parser.add_argument('--daily', action='store_true', help='Track daily prices') + parser.add_argument('--weekly', action='store_true', help='Track weekly prices') + parser.add_argument('--days', type=int, default=30, help='Number of days to track (default: 30)') + parser.add_argument('--weeks', type=int, default=12, help='Number of weeks to track (default: 12)') + parser.add_argument('--plot', action='store_true', default=True, help='Display plots (default: True)') + parser.add_argument('--no-plot', action='store_true', help='Do not display plots') + parser.add_argument('--save-plots', action='store_true', help='Save plots to files') + parser.add_argument('--telegram', action='store_true', help='Enable Telegram alerts') + parser.add_argument('--alert', action='store_true', help='Send Telegram alert') + parser.add_argument('--threshold', type=float, help='Price threshold for alerts') + parser.add_argument('--threshold-type', choices=['above', 'below'], default='above', + help='Threshold type (default: above)') + parser.add_argument('--info', action='store_true', help='Display stock information only') + + args = parser.parse_args() + + # Load environment variables + env_path = Path(__file__).parent / 'config' / '.env' + if env_path.exists(): + load_dotenv(env_path) + + # Display header + print("\n" + "="*60) + print("šŸ“ˆ STOCK PRICE TRACKER") + print("="*60) + + # Initialize tracker + tracker = StockTracker(args.ticker, use_telegram=args.telegram) + + # Validate ticker + if not tracker.validate_ticker(): + sys.exit(1) + + # Test Telegram connection if enabled + if args.telegram: + print("\nšŸ“± Testing Telegram connection...") + if tracker.telegram and tracker.telegram.test_connection(): + tracker.telegram.send_welcome_message(args.ticker) + else: + print("āš ļø Telegram not configured properly. Continuing without alerts.") + + # Display stock info + if args.info: + tracker.display_stock_info() + return + + # Check threshold + if args.threshold is not None: + tracker.check_price_threshold(args.threshold, args.threshold_type) + return + + # Determine plot settings + show_plots = args.plot and not args.no_plot + + # Track prices + if args.weekly: + tracker.track_weekly( + weeks=args.weeks, + plot=show_plots, + save_plots=args.save_plots, + send_alert=args.alert + ) + elif args.daily or (not args.daily and not args.weekly): + # Default to daily if neither specified + tracker.track_daily( + days=args.days, + plot=show_plots, + save_plots=args.save_plots, + send_alert=args.alert + ) + + print("\nāœ… Stock tracking complete!") + print("="*60 + "\n") + + +if __name__ == "__main__": + main() diff --git a/Stock-Price-Tracker/utils/__init__.py b/Stock-Price-Tracker/utils/__init__.py new file mode 100644 index 0000000..965527b --- /dev/null +++ b/Stock-Price-Tracker/utils/__init__.py @@ -0,0 +1,9 @@ +""" +Stock Price Tracker Utilities +""" + +from .yahoo_finance import StockDataFetcher +from .plotter import StockPlotter +from .telegram_alert import TelegramAlert + +__all__ = ['StockDataFetcher', 'StockPlotter', 'TelegramAlert'] diff --git a/Stock-Price-Tracker/utils/plotter.py b/Stock-Price-Tracker/utils/plotter.py new file mode 100644 index 0000000..c7c229a --- /dev/null +++ b/Stock-Price-Tracker/utils/plotter.py @@ -0,0 +1,339 @@ +""" +Stock Price Plotter +Creates various charts and visualizations for stock data +""" + +import pandas as pd +import matplotlib.pyplot as plt +import matplotlib.dates as mdates +import plotly.graph_objects as go +from plotly.subplots import make_subplots +from datetime import datetime +from typing import Optional + + +class StockPlotter: + """Create visualizations for stock data""" + + def __init__(self, ticker: str): + """ + Initialize the stock plotter + + Args: + ticker (str): Stock ticker symbol + """ + self.ticker = ticker.upper() + + def plot_price_trend(self, data: pd.DataFrame, title: str = None, + save_path: Optional[str] = None, show_plot: bool = True): + """ + Plot stock price trend using matplotlib + + Args: + data (pd.DataFrame): Stock data with 'Close' column + title (str): Plot title + save_path (str): Path to save the plot + show_plot (bool): Whether to display the plot + """ + if data.empty: + print("No data to plot") + return + + plt.figure(figsize=(14, 7)) + + # 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() + + def plot_candlestick(self, data: pd.DataFrame, title: str = None, + save_path: Optional[str] = None, show_plot: bool = True): + """ + Create interactive candlestick chart using plotly + + Args: + data (pd.DataFrame): Stock data with OHLC columns + title (str): Chart title + save_path (str): Path to save the chart + show_plot (bool): Whether to display the chart + """ + if data.empty: + print("No data to plot") + return + + # Create figure with secondary y-axis + fig = make_subplots( + rows=2, cols=1, + shared_xaxes=True, + vertical_spacing=0.03, + row_heights=[0.7, 0.3], + subplot_titles=(f'{self.ticker} Price', 'Volume') + ) + + # Candlestick chart + fig.add_trace( + go.Candlestick( + x=data.index, + open=data['Open'], + high=data['High'], + low=data['Low'], + close=data['Close'], + name='OHLC', + increasing_line_color='#26A69A', + decreasing_line_color='#EF5350' + ), + row=1, col=1 + ) + + # Volume bar chart + colors = ['#26A69A' if close >= open else '#EF5350' + for close, open in zip(data['Close'], data['Open'])] + + fig.add_trace( + go.Bar( + x=data.index, + y=data['Volume'], + name='Volume', + marker_color=colors, + showlegend=False + ), + row=2, col=1 + ) + + # Update layout + if title is None: + title = f'{self.ticker} Candlestick Chart' + + fig.update_layout( + title=title, + title_font_size=18, + xaxis_rangeslider_visible=False, + height=700, + template='plotly_white', + hovermode='x unified' + ) + + fig.update_xaxes(title_text="Date", row=2, col=1) + fig.update_yaxes(title_text="Price ($)", row=1, col=1) + fig.update_yaxes(title_text="Volume", row=2, col=1) + + if save_path: + fig.write_html(save_path) + print(f"Interactive chart saved to {save_path}") + + if show_plot: + fig.show() + + def plot_volume_analysis(self, data: pd.DataFrame, title: str = None, + save_path: Optional[str] = None, show_plot: bool = True): + """ + Plot volume analysis + + Args: + data (pd.DataFrame): Stock data with Volume column + title (str): Plot title + save_path (str): Path to save the plot + show_plot (bool): Whether to display the plot + """ + if data.empty: + print("No data to plot") + return + + fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 8), + height_ratios=[2, 1], sharex=True) + + # Price subplot + ax1.plot(data.index, data['Close'], label='Close Price', + color='#2E86C1', linewidth=2) + ax1.set_ylabel('Price ($)', fontsize=12, fontweight='bold') + ax1.legend(loc='best') + ax1.grid(True, alpha=0.3) + + # Volume subplot + colors = ['#26A69A' if close >= open else '#EF5350' + for close, open in zip(data['Close'], data['Open'])] + ax2.bar(data.index, data['Volume'], color=colors, alpha=0.7) + ax2.set_xlabel('Date', fontsize=12, fontweight='bold') + ax2.set_ylabel('Volume', fontsize=12, fontweight='bold') + ax2.grid(True, alpha=0.3, axis='y') + + # Add average volume line + avg_volume = data['Volume'].mean() + ax2.axhline(y=avg_volume, color='orange', linestyle='--', + linewidth=2, label=f'Avg Volume: {avg_volume:,.0f}') + ax2.legend(loc='best') + + if title is None: + title = f'{self.ticker} Price & Volume Analysis' + fig.suptitle(title, fontsize=16, fontweight='bold') + + plt.gcf().autofmt_xdate() + plt.tight_layout() + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + print(f"Volume analysis plot saved to {save_path}") + + if show_plot: + plt.show() + else: + plt.close() + + def plot_comparison(self, data: pd.DataFrame, comparison_type: str = 'daily', + save_path: Optional[str] = None, show_plot: bool = True): + """ + Create comparison plots (daily vs weekly) + + Args: + data (pd.DataFrame): Stock data + comparison_type (str): Type of comparison ('daily' or 'weekly') + save_path (str): Path to save the plot + show_plot (bool): Whether to display the plot + """ + if data.empty: + print("No data to plot") + return + + fig, axes = plt.subplots(2, 2, figsize=(16, 10)) + fig.suptitle(f'{self.ticker} Stock Analysis - {comparison_type.capitalize()}', + fontsize=16, fontweight='bold') + + # Price trend + axes[0, 0].plot(data.index, data['Close'], color='#2E86C1', linewidth=2) + axes[0, 0].set_title('Closing Price', fontweight='bold') + axes[0, 0].set_ylabel('Price ($)') + axes[0, 0].grid(True, alpha=0.3) + + # Volume + axes[0, 1].bar(data.index, data['Volume'], color='#3498DB', alpha=0.7) + axes[0, 1].set_title('Trading Volume', fontweight='bold') + axes[0, 1].set_ylabel('Volume') + axes[0, 1].grid(True, alpha=0.3, axis='y') + + # Daily returns + returns = data['Close'].pct_change() * 100 + axes[1, 0].plot(data.index, returns, color='#27AE60', linewidth=1.5) + axes[1, 0].axhline(y=0, color='red', linestyle='--', linewidth=1) + axes[1, 0].set_title('Daily Returns (%)', fontweight='bold') + axes[1, 0].set_ylabel('Return (%)') + axes[1, 0].set_xlabel('Date') + axes[1, 0].grid(True, alpha=0.3) + + # Price distribution + axes[1, 1].hist(data['Close'], bins=30, color='#E67E22', alpha=0.7, edgecolor='black') + axes[1, 1].set_title('Price Distribution', fontweight='bold') + axes[1, 1].set_xlabel('Price ($)') + axes[1, 1].set_ylabel('Frequency') + axes[1, 1].grid(True, alpha=0.3, axis='y') + + plt.tight_layout() + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + print(f"Comparison plot saved to {save_path}") + + if show_plot: + plt.show() + else: + plt.close() + + def create_summary_report(self, data: pd.DataFrame, stats: dict, + save_path: Optional[str] = None, show_plot: bool = True): + """ + Create a comprehensive visual summary report + + Args: + data (pd.DataFrame): Stock data + stats (dict): Price change statistics + save_path (str): Path to save the report + show_plot (bool): Whether to display the report + """ + if data.empty or not stats: + print("Insufficient data for summary report") + return + + fig = plt.figure(figsize=(16, 10)) + 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() diff --git a/Stock-Price-Tracker/utils/telegram_alert.py b/Stock-Price-Tracker/utils/telegram_alert.py new file mode 100644 index 0000000..d36797b --- /dev/null +++ b/Stock-Price-Tracker/utils/telegram_alert.py @@ -0,0 +1,286 @@ +""" +Telegram Alert System +Send stock price alerts via Telegram bot +""" + +import os +from typing import Optional, Dict, Any +from datetime import datetime +from telegram import Bot +from telegram.error import TelegramError +import asyncio + + +class TelegramAlert: + """Send alerts via Telegram bot""" + + def __init__(self, bot_token: Optional[str] = None, chat_id: Optional[str] = None): + """ + Initialize Telegram bot + + Args: + bot_token (str): Telegram bot token from @BotFather + chat_id (str): Telegram chat ID to send messages to + """ + self.bot_token = bot_token or os.getenv('TELEGRAM_BOT_TOKEN') + self.chat_id = chat_id or os.getenv('TELEGRAM_CHAT_ID') + + if not self.bot_token or not self.chat_id: + print("āš ļø Warning: Telegram credentials not configured") + print("Set TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID in .env file") + self.bot = None + else: + self.bot = Bot(token=self.bot_token) + + def is_configured(self) -> bool: + """Check if Telegram bot is properly configured""" + return self.bot is not None + + async def send_message_async(self, message: str) -> bool: + """ + Send a text message via Telegram (async) + + Args: + message (str): Message to send + + Returns: + bool: True if successful, False otherwise + """ + if not self.is_configured(): + print("Telegram bot not configured. Skipping alert.") + return False + + try: + await self.bot.send_message( + chat_id=self.chat_id, + text=message, + parse_mode='HTML' + ) + return True + except TelegramError as e: + print(f"Error sending Telegram message: {e}") + return False + + def send_message(self, message: str) -> bool: + """ + Send a text message via Telegram (sync wrapper) + + Args: + message (str): Message to send + + Returns: + bool: True if successful, False otherwise + """ + if not self.is_configured(): + print("Telegram bot not configured. Skipping alert.") + return False + + try: + # Run async function in sync context + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + result = loop.run_until_complete(self.send_message_async(message)) + loop.close() + return result + except Exception as e: + print(f"Error in sync message send: {e}") + return False + + def format_stock_alert(self, ticker: str, stats: Dict[str, Any], + alert_type: str = "update") -> str: + """ + Format stock data into a nice alert message + + Args: + ticker (str): Stock ticker symbol + stats (dict): Stock statistics + alert_type (str): Type of alert ('update', 'threshold', 'daily_summary') + + Returns: + str: Formatted message + """ + timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + + # Emoji based on price change + change_percent = stats.get('change_percent', 0) + if change_percent > 0: + emoji = "šŸ“ˆ" + direction = "UP" + elif change_percent < 0: + emoji = "šŸ“‰" + direction = "DOWN" + else: + emoji = "āž”ļø" + direction = "FLAT" + + if alert_type == "threshold": + message = f""" +🚨 PRICE ALERT: {ticker} 🚨 + +{emoji} Price moved {direction} by {abs(change_percent):.2f}%! + +šŸ’° Current Price: ${stats.get('end_price', 'N/A')} +šŸ“Š Previous Price: ${stats.get('start_price', 'N/A')} +šŸ“‰ Change: ${stats.get('change', 'N/A')} ({change_percent:+.2f}%) + +šŸ“… Time: {timestamp} +""" + elif alert_type == "daily_summary": + message = f""" +šŸ“Š DAILY SUMMARY: {ticker} + +{emoji} Overall: {direction} {abs(change_percent):.2f}% + +šŸ’° Opening: ${stats.get('start_price', 'N/A')} +šŸ’µ Closing: ${stats.get('end_price', 'N/A')} +šŸ”¼ High: ${stats.get('highest', 'N/A')} +šŸ”½ Low: ${stats.get('lowest', 'N/A')} +šŸ“Š Change: ${stats.get('change', 'N/A')} ({change_percent:+.2f}%) + +šŸ“… Date: {timestamp} +""" + else: # update + message = f""" +šŸ“ˆ Stock Update: {ticker} + +šŸ’° Current Price: ${stats.get('end_price', 'N/A')} +šŸ“Š Change: ${stats.get('change', 'N/A')} ({change_percent:+.2f}%) {emoji} +šŸ”¼ High: ${stats.get('highest', 'N/A')} +šŸ”½ Low: ${stats.get('lowest', 'N/A')} + +šŸ“… Updated: {timestamp} +""" + + return message.strip() + + def send_stock_alert(self, ticker: str, stats: Dict[str, Any], + alert_type: str = "update") -> bool: + """ + Send a formatted stock alert + + Args: + ticker (str): Stock ticker symbol + stats (dict): Stock statistics + alert_type (str): Type of alert + + Returns: + bool: True if successful + """ + message = self.format_stock_alert(ticker, stats, alert_type) + return self.send_message(message) + + def send_price_threshold_alert(self, ticker: str, current_price: float, + threshold: float, threshold_type: str = "above") -> bool: + """ + Send alert when price crosses a threshold + + Args: + ticker (str): Stock ticker symbol + current_price (float): Current stock price + threshold (float): Price threshold + threshold_type (str): 'above' or 'below' + + Returns: + bool: True if successful + """ + emoji = "šŸ””" if threshold_type == "above" else "āš ļø" + arrow = "ā†—ļø" if threshold_type == "above" else "ā†˜ļø" + + message = f""" +{emoji} THRESHOLD ALERT: {ticker} + +{arrow} Price is now {threshold_type} ${threshold:.2f}! + +šŸ’° Current Price: ${current_price:.2f} +šŸŽÆ Threshold: ${threshold:.2f} + +šŸ“… Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} +""" + + return self.send_message(message.strip()) + + def send_error_alert(self, ticker: str, error_message: str) -> bool: + """ + Send error notification + + Args: + ticker (str): Stock ticker symbol + error_message (str): Error description + + Returns: + bool: True if successful + """ + message = f""" +āŒ ERROR: {ticker} + +āš ļø Issue: {error_message} + +šŸ“… Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} + +Please check your stock tracker configuration. +""" + + return self.send_message(message.strip()) + + def send_welcome_message(self, ticker: str) -> bool: + """ + Send welcome/initialization message + + Args: + ticker (str): Stock ticker symbol + + Returns: + bool: True if successful + """ + message = f""" +šŸš€ Stock Price Tracker Started + +šŸ“Š Now tracking: {ticker} + +You will receive alerts when: +• Significant price changes occur +• Daily summaries are available +• Threshold levels are crossed + +šŸ’” Stay informed about your investments! +""" + + return self.send_message(message.strip()) + + async def test_connection_async(self) -> bool: + """ + Test Telegram bot connection (async) + + Returns: + bool: True if connection is successful + """ + if not self.is_configured(): + return False + + try: + bot_info = await self.bot.get_me() + print(f"āœ… Connected to Telegram bot: @{bot_info.username}") + return True + except TelegramError as e: + print(f"āŒ Failed to connect to Telegram: {e}") + return False + + def test_connection(self) -> bool: + """ + Test Telegram bot connection (sync wrapper) + + Returns: + bool: True if connection is successful + """ + if not self.is_configured(): + return False + + try: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + result = loop.run_until_complete(self.test_connection_async()) + loop.close() + return result + except Exception as e: + print(f"Error testing connection: {e}") + return False diff --git a/Stock-Price-Tracker/utils/yahoo_finance.py b/Stock-Price-Tracker/utils/yahoo_finance.py new file mode 100644 index 0000000..9ba07d8 --- /dev/null +++ b/Stock-Price-Tracker/utils/yahoo_finance.py @@ -0,0 +1,205 @@ +""" +Yahoo Finance Data Fetcher +Fetches stock data using yfinance API +""" + +import yfinance as yf +import pandas as pd +from datetime import datetime, timedelta +from typing import Optional, Dict, Any + + +class StockDataFetcher: + """Fetch stock data from Yahoo Finance API""" + + def __init__(self, ticker: str): + """ + Initialize the stock data fetcher + + Args: + ticker (str): Stock ticker symbol (e.g., 'AAPL', 'GOOGL', 'TSLA') + """ + self.ticker = ticker.upper() + self.stock = yf.Ticker(self.ticker) + + def get_stock_info(self) -> Dict[str, Any]: + """ + Get basic stock information + + Returns: + dict: Stock information including name, sector, market cap, etc. + """ + try: + info = self.stock.info + return { + 'symbol': self.ticker, + 'name': info.get('longName', 'N/A'), + 'sector': info.get('sector', 'N/A'), + 'industry': info.get('industry', 'N/A'), + 'market_cap': info.get('marketCap', 'N/A'), + 'current_price': info.get('currentPrice', info.get('regularMarketPrice', 'N/A')), + 'previous_close': info.get('previousClose', 'N/A'), + 'volume': info.get('volume', 'N/A'), + 'average_volume': info.get('averageVolume', 'N/A'), + '52_week_high': info.get('fiftyTwoWeekHigh', 'N/A'), + '52_week_low': info.get('fiftyTwoWeekLow', 'N/A'), + } + except Exception as e: + print(f"Error fetching stock info: {e}") + return {} + + def get_historical_data(self, period: str = '1mo', interval: str = '1d') -> pd.DataFrame: + """ + Get historical stock data + + Args: + period (str): Time period ('1d', '5d', '1mo', '3mo', '6mo', '1y', '2y', '5y', 'max') + interval (str): Data interval ('1m', '5m', '15m', '1h', '1d', '1wk', '1mo') + + Returns: + pd.DataFrame: Historical stock data with OHLCV (Open, High, Low, Close, Volume) + """ + try: + data = self.stock.history(period=period, interval=interval) + if data.empty: + print(f"No data found for {self.ticker}") + return pd.DataFrame() + return data + except Exception as e: + print(f"Error fetching historical data: {e}") + return pd.DataFrame() + + def get_data_by_date_range(self, start_date: str, end_date: str, interval: str = '1d') -> pd.DataFrame: + """ + Get historical data for a specific date range + + Args: + start_date (str): Start date in 'YYYY-MM-DD' format + end_date (str): End date in 'YYYY-MM-DD' format + interval (str): Data interval ('1d', '1wk', '1mo') + + Returns: + pd.DataFrame: Historical stock data + """ + try: + data = self.stock.history(start=start_date, end=end_date, interval=interval) + if data.empty: + print(f"No data found for {self.ticker} between {start_date} and {end_date}") + return pd.DataFrame() + return data + except Exception as e: + print(f"Error fetching data by date range: {e}") + return pd.DataFrame() + + def get_daily_data(self, days: int = 30) -> pd.DataFrame: + """ + Get daily stock data for the specified number of days + + Args: + days (int): Number of days to fetch (default: 30) + + Returns: + pd.DataFrame: Daily stock data + """ + period_map = { + 5: '5d', + 7: '1mo', + 30: '1mo', + 90: '3mo', + 180: '6mo', + 365: '1y' + } + + # Find closest period + period = '1mo' + for day_limit, p in sorted(period_map.items()): + if days <= day_limit: + period = p + break + if days > 365: + period = 'max' + + return self.get_historical_data(period=period, interval='1d') + + def get_weekly_data(self, weeks: int = 12) -> pd.DataFrame: + """ + Get weekly stock data + + Args: + weeks (int): Number of weeks to fetch (default: 12) + + Returns: + pd.DataFrame: Weekly stock data + """ + if weeks <= 4: + period = '1mo' + elif weeks <= 12: + period = '3mo' + elif weeks <= 26: + period = '6mo' + else: + period = '1y' + + return self.get_historical_data(period=period, interval='1wk') + + def calculate_price_change(self, data: pd.DataFrame) -> Dict[str, float]: + """ + Calculate price change statistics + + Args: + data (pd.DataFrame): Stock data + + Returns: + dict: Price change statistics (absolute, percentage, high, low) + """ + if data.empty: + return {} + + try: + start_price = data['Close'].iloc[0] + end_price = data['Close'].iloc[-1] + highest_price = data['High'].max() + lowest_price = data['Low'].min() + + change = end_price - start_price + change_percent = (change / start_price) * 100 + + return { + 'start_price': round(start_price, 2), + 'end_price': round(end_price, 2), + 'change': round(change, 2), + 'change_percent': round(change_percent, 2), + 'highest': round(highest_price, 2), + 'lowest': round(lowest_price, 2) + } + except Exception as e: + print(f"Error calculating price change: {e}") + return {} + + def get_current_price(self) -> Optional[float]: + """ + Get the current/latest stock price + + Returns: + float: Current price or None if unavailable + """ + try: + info = self.stock.info + price = info.get('currentPrice', info.get('regularMarketPrice')) + return price + except Exception as e: + print(f"Error fetching current price: {e}") + return None + + def is_valid_ticker(self) -> bool: + """ + Check if the ticker symbol is valid + + Returns: + bool: True if valid, False otherwise + """ + try: + info = self.stock.info + return 'symbol' in info or 'longName' in info + except: + return False