diff --git a/.gitignore b/.gitignore index 14339837f..f70e185a4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,26 +1,48 @@ -__pycache__/* -yfinance/__pycache__/* -dist -yfinance.egg-info -*.pyc -.coverage -.idea/ -.vscode/ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python build/ -*.html -*.css -test.ipynb +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg -# Environments -.env -.venv -env/ +# Virtual Environment venv/ ENV/ +env/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Flask +instance/ +.webassets-cache + +# Environment variables +.env +.env.local + +# OS +.DS_Store +Thumbs.db -# Documentation -/doc/build/ -/doc/_build/ -/doc/source/reference/api -!yfinance.css -!/doc/source/development/assets/branches.png +# Logs +*.log diff --git a/MARKETPLACE_LISTING.md b/MARKETPLACE_LISTING.md new file mode 100644 index 000000000..1b38b17ec --- /dev/null +++ b/MARKETPLACE_LISTING.md @@ -0,0 +1,97 @@ +# Stock Portfolio Tracker - Marketplace Listing + +## Product Overview + +**Price:** $10 USD +**Category:** Web Application / SaaS Prototype +**Platform:** Web (All Devices) + +## Description + +A professional-grade Stock Portfolio Tracker web application built with Flask and modern web technologies. This responsive application allows users to track their stock investments in real-time with beautiful charts and an intuitive interface. + +## Key Features + +✅ **Real-time Stock Data** - Live quotes from Yahoo Finance +✅ **Interactive Charts** - Price history with multiple time periods +✅ **Portfolio Management** - Add/remove stocks easily +✅ **Fully Responsive** - Works perfectly on desktop, tablet, and mobile +✅ **Auto-refresh** - Portfolio updates every 30 seconds +✅ **Local Storage** - Portfolio saved in browser +✅ **Modern UI** - Clean, professional design +✅ **No API Keys Required** - Uses free Yahoo Finance data + +## Technical Stack + +- **Backend:** Flask (Python) +- **Frontend:** HTML5, CSS3, JavaScript (Vanilla) +- **Charts:** Chart.js +- **Data:** Yahoo Finance via yfinance library +- **Storage:** Browser LocalStorage + +## What's Included + +- Complete source code +- Responsive HTML/CSS/JavaScript +- Flask backend API +- Deployment instructions +- Quick start script +- Documentation + +## Quick Start + +```bash +# Install dependencies +pip install -r requirements-app.txt + +# Run the application +python app.py + +# Or use the quick start script +./start.sh +``` + +Then open http://localhost:5000 in your browser. + +## Deployment Options + +- ✅ Heroku +- ✅ Railway +- ✅ Render +- ✅ DigitalOcean +- ✅ AWS +- ✅ Any Python hosting platform + +## Screenshots Description + +1. **Main Dashboard** - Clean interface showing portfolio grid +2. **Stock Cards** - Individual cards with price, change, volume, market cap +3. **Interactive Charts** - Price history with multiple time periods +4. **Mobile View** - Responsive design on mobile devices +5. **Add Stock** - Simple search and add functionality + +## Use Cases + +- Personal portfolio tracking +- Stock market education +- Investment research +- Financial dashboard +- Learning web development + +## Requirements + +- Python 3.7+ +- Modern web browser +- Internet connection (for stock data) + +## Support + +Full documentation included. Code is well-commented and easy to customize. + +## License + +Full source code included. Commercial use allowed. + +--- + +**Perfect for:** Developers, investors, students, entrepreneurs looking for a ready-to-deploy stock tracking solution. diff --git a/PROTOTYPE_README.md b/PROTOTYPE_README.md new file mode 100644 index 000000000..4ce46bd0b --- /dev/null +++ b/PROTOTYPE_README.md @@ -0,0 +1,141 @@ +# Stock Portfolio Tracker - Marketplace Prototype ($10) + +A responsive web application for tracking stock portfolios in real-time. Works seamlessly across all devices (desktop, tablet, mobile). + +## Features + +- 📈 **Real-time Stock Quotes** - Get live stock prices and market data +- 📊 **Interactive Charts** - View price history with multiple time periods (5D, 1M, 3M, 6M, 1Y) +- 💼 **Portfolio Management** - Add/remove stocks and track your portfolio +- 📱 **Fully Responsive** - Beautiful UI that works on all screen sizes +- 💾 **Local Storage** - Your portfolio is saved locally in your browser +- 🔄 **Auto-refresh** - Portfolio updates every 30 seconds automatically + +## Quick Start + +### Prerequisites + +- Python 3.7 or higher +- pip (Python package manager) + +### Installation + +1. Install dependencies: +```bash +pip install -r requirements-app.txt +``` + +2. Run the application: +```bash +python app.py +``` + +3. Open your browser and navigate to: +``` +http://localhost:5000 +``` + +## Usage + +1. **Add Stocks**: Enter a stock symbol (e.g., AAPL, MSFT, GOOGL) and click "Add Stock" +2. **View Details**: Click on any stock card to see its price history chart +3. **Change Time Period**: Use the period buttons (5D, 1M, 3M, 6M, 1Y) to view different timeframes +4. **Remove Stocks**: Click the × button on any stock card to remove it from your portfolio + +## Deployment + +### Local Development +```bash +python app.py +``` + +### Production Deployment + +#### Option 1: Using Gunicorn (Recommended) +```bash +pip install gunicorn +gunicorn -w 4 -b 0.0.0.0:5000 app:app +``` + +#### Option 2: Using Docker +Create a `Dockerfile`: +```dockerfile +FROM python:3.9-slim +WORKDIR /app +COPY requirements-app.txt . +RUN pip install --no-cache-dir -r requirements-app.txt +COPY . . +EXPOSE 5000 +CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:5000", "app:app"] +``` + +Build and run: +```bash +docker build -t stock-tracker . +docker run -p 5000:5000 stock-tracker +``` + +#### Option 3: Deploy to Cloud Platforms + +**Heroku:** +```bash +heroku create stock-portfolio-tracker +git push heroku main +``` + +**Railway:** +- Connect your repository +- Set start command: `gunicorn -w 4 -b 0.0.0.0:$PORT app:app` +- Deploy automatically + +**Render:** +- Create a new Web Service +- Set build command: `pip install -r requirements-app.txt` +- Set start command: `gunicorn -w 4 -b 0.0.0.0:5000 app:app` + +## Marketplace Listing + +### Product Description +"Stock Portfolio Tracker - A professional-grade web application for tracking your stock investments in real-time. Features interactive charts, responsive design, and automatic portfolio updates. Perfect for investors who want a simple, elegant solution to monitor their stocks." + +### Pricing +**$10 USD** - One-time purchase includes: +- Full source code +- Deployment instructions +- Lifetime updates +- Commercial use license + +### Key Selling Points +- ✅ Works on all devices (desktop, tablet, mobile) +- ✅ Real-time stock data from Yahoo Finance +- ✅ Beautiful, modern UI +- ✅ Easy to deploy and customize +- ✅ No API keys required +- ✅ Lightweight and fast + +## Technical Details + +- **Backend**: Flask (Python) +- **Frontend**: HTML5, CSS3, JavaScript (Vanilla) +- **Charts**: Chart.js +- **Data Source**: Yahoo Finance (via yfinance library) +- **Storage**: Browser LocalStorage + +## Browser Support + +- Chrome/Edge (latest) +- Firefox (latest) +- Safari (latest) +- Mobile browsers (iOS Safari, Chrome Mobile) + +## License + +This prototype is provided as-is for marketplace sale. The buyer receives full source code and deployment rights. + +## Support + +For questions or issues, please refer to the deployment documentation or contact the seller. + +--- + +**Note**: This application uses Yahoo Finance data which is intended for personal use only. Please refer to Yahoo's terms of use for commercial usage rights. diff --git a/app.py b/app.py new file mode 100644 index 000000000..3d86193b1 --- /dev/null +++ b/app.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Stock Portfolio Tracker - A $10 Marketplace Prototype +A responsive web application for tracking stock portfolios across all devices +""" + +from flask import Flask, render_template, jsonify, request +import yfinance as yf +from datetime import datetime, timedelta +import json + +app = Flask(__name__) + +@app.route('/') +def index(): + """Main page""" + return render_template('index.html') + +@app.route('/api/quote/') +def get_quote(ticker): + """Get current quote for a ticker""" + try: + stock = yf.Ticker(ticker.upper()) + info = stock.info + + # Get current price + hist = stock.history(period="1d", interval="1m") + if not hist.empty: + current_price = float(hist['Close'].iloc[-1]) + else: + current_price = info.get('currentPrice', info.get('regularMarketPrice', 0)) + + # Get previous close + prev_close = info.get('previousClose', current_price) + change = current_price - prev_close + change_percent = (change / prev_close * 100) if prev_close else 0 + + return jsonify({ + 'success': True, + 'ticker': ticker.upper(), + 'name': info.get('longName', info.get('shortName', ticker.upper())), + 'price': round(current_price, 2), + 'change': round(change, 2), + 'changePercent': round(change_percent, 2), + 'volume': info.get('volume', 0), + 'marketCap': info.get('marketCap', 0), + 'currency': info.get('currency', 'USD') + }) + except Exception as e: + return jsonify({ + 'success': False, + 'error': str(e) + }), 400 + +@app.route('/api/history/') +def get_history(ticker): + """Get historical data for charting""" + try: + period = request.args.get('period', '1mo') + stock = yf.Ticker(ticker.upper()) + hist = stock.history(period=period) + + if hist.empty: + return jsonify({ + 'success': False, + 'error': 'No data available' + }), 400 + + # Format data for chart + data = [] + for date, row in hist.iterrows(): + data.append({ + 'date': date.strftime('%Y-%m-%d'), + 'open': round(float(row['Open']), 2), + 'high': round(float(row['High']), 2), + 'low': round(float(row['Low']), 2), + 'close': round(float(row['Close']), 2), + 'volume': int(row['Volume']) + }) + + return jsonify({ + 'success': True, + 'ticker': ticker.upper(), + 'data': data + }) + except Exception as e: + return jsonify({ + 'success': False, + 'error': str(e) + }), 400 + +@app.route('/api/batch', methods=['POST']) +def get_batch(): + """Get quotes for multiple tickers""" + try: + data = request.get_json() + tickers = data.get('tickers', []) + + if not tickers: + return jsonify({ + 'success': False, + 'error': 'No tickers provided' + }), 400 + + results = [] + for ticker in tickers: + try: + stock = yf.Ticker(ticker.upper()) + info = stock.info + + hist = stock.history(period="1d", interval="1m") + if not hist.empty: + current_price = float(hist['Close'].iloc[-1]) + else: + current_price = info.get('currentPrice', info.get('regularMarketPrice', 0)) + + prev_close = info.get('previousClose', current_price) + change = current_price - prev_close + change_percent = (change / prev_close * 100) if prev_close else 0 + + results.append({ + 'ticker': ticker.upper(), + 'name': info.get('longName', info.get('shortName', ticker.upper())), + 'price': round(current_price, 2), + 'change': round(change, 2), + 'changePercent': round(change_percent, 2), + 'volume': info.get('volume', 0), + 'marketCap': info.get('marketCap', 0), + 'currency': info.get('currency', 'USD') + }) + except Exception as e: + results.append({ + 'ticker': ticker.upper(), + 'error': str(e) + }) + + return jsonify({ + 'success': True, + 'results': results + }) + except Exception as e: + return jsonify({ + 'success': False, + 'error': str(e) + }), 400 + +if __name__ == '__main__': + app.run(debug=True, host='0.0.0.0', port=5000) diff --git a/requirements-app.txt b/requirements-app.txt new file mode 100644 index 000000000..8d97b14c8 --- /dev/null +++ b/requirements-app.txt @@ -0,0 +1,4 @@ +Flask>=2.3.0 +yfinance>=0.2.0 +pandas>=1.3.0 +numpy>=1.16.5 diff --git a/start.sh b/start.sh new file mode 100755 index 000000000..90cc48861 --- /dev/null +++ b/start.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# Quick start script for Stock Portfolio Tracker + +echo "🚀 Starting Stock Portfolio Tracker..." +echo "" + +# Check if virtual environment exists +if [ ! -d "venv" ]; then + echo "Creating virtual environment..." + python3 -m venv venv +fi + +# Activate virtual environment +source venv/bin/activate + +# Install dependencies +echo "Installing dependencies..." +pip install -q -r requirements-app.txt + +# Start the application +echo "" +echo "✅ Starting Flask server..." +echo "📱 Open http://localhost:5000 in your browser" +echo "" +python app.py diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 000000000..4b81ee2f3 --- /dev/null +++ b/static/css/style.css @@ -0,0 +1,445 @@ +/* Stock Portfolio Tracker - Responsive Design for All Devices */ + +:root { + --primary-color: #2563eb; + --primary-dark: #1e40af; + --success-color: #10b981; + --danger-color: #ef4444; + --bg-color: #f8fafc; + --card-bg: #ffffff; + --text-primary: #1e293b; + --text-secondary: #64748b; + --border-color: #e2e8f0; + --shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: var(--bg-color); + color: var(--text-primary); + line-height: 1.6; + min-height: 100vh; +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 1rem; +} + +/* Header */ +header { + background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-dark) 100%); + color: white; + padding: 2rem 1rem; + border-radius: 12px; + margin-bottom: 2rem; + box-shadow: var(--shadow-lg); +} + +.header-content { + text-align: center; +} + +header h1 { + font-size: 2rem; + font-weight: 700; + margin-bottom: 0.5rem; +} + +.subtitle { + font-size: 1rem; + opacity: 0.9; + font-weight: 300; +} + +/* Search Section */ +.search-section { + margin-bottom: 2rem; +} + +.search-box { + display: flex; + gap: 0.75rem; + margin-bottom: 1rem; +} + +#tickerInput { + flex: 1; + padding: 0.875rem 1rem; + border: 2px solid var(--border-color); + border-radius: 8px; + font-size: 1rem; + transition: all 0.2s; + background: var(--card-bg); +} + +#tickerInput:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); +} + +.btn-primary { + padding: 0.875rem 1.5rem; + background: var(--primary-color); + color: white; + border: none; + border-radius: 8px; + font-size: 1rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; + white-space: nowrap; +} + +.btn-primary:hover { + background: var(--primary-dark); + transform: translateY(-1px); + box-shadow: var(--shadow-lg); +} + +.btn-primary:active { + transform: translateY(0); +} + +.error-message { + color: var(--danger-color); + font-size: 0.875rem; + padding: 0.5rem; + display: none; +} + +.error-message.show { + display: block; +} + +/* Portfolio Section */ +.portfolio-section { + background: var(--card-bg); + border-radius: 12px; + padding: 1.5rem; + box-shadow: var(--shadow); + margin-bottom: 2rem; +} + +.portfolio-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; + flex-wrap: wrap; + gap: 1rem; +} + +.portfolio-header h2 { + font-size: 1.5rem; + font-weight: 600; +} + +.portfolio-stats { + display: flex; + gap: 2rem; +} + +.stat { + display: flex; + flex-direction: column; +} + +.stat-label { + font-size: 0.875rem; + color: var(--text-secondary); + margin-bottom: 0.25rem; +} + +.stat-value { + font-size: 1.25rem; + font-weight: 600; +} + +.portfolio-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 1rem; +} + +.empty-state { + grid-column: 1 / -1; + text-align: center; + padding: 3rem 1rem; + color: var(--text-secondary); +} + +.empty-state p { + margin-bottom: 0.5rem; +} + +.hint { + font-size: 0.875rem; + font-style: italic; +} + +/* Stock Card */ +.stock-card { + background: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 1.25rem; + transition: all 0.2s; + cursor: pointer; + position: relative; +} + +.stock-card:hover { + box-shadow: var(--shadow-lg); + transform: translateY(-2px); +} + +.stock-card-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 1rem; +} + +.stock-info h3 { + font-size: 1.125rem; + font-weight: 600; + margin-bottom: 0.25rem; +} + +.stock-info .ticker { + font-size: 0.875rem; + color: var(--text-secondary); + font-weight: 500; +} + +.remove-btn { + background: none; + border: none; + color: var(--text-secondary); + font-size: 1.25rem; + cursor: pointer; + padding: 0.25rem; + line-height: 1; + transition: color 0.2s; +} + +.remove-btn:hover { + color: var(--danger-color); +} + +.stock-price { + font-size: 1.75rem; + font-weight: 700; + margin-bottom: 0.5rem; +} + +.stock-change { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.875rem; + font-weight: 500; +} + +.stock-change.positive { + color: var(--success-color); +} + +.stock-change.negative { + color: var(--danger-color); +} + +.stock-meta { + margin-top: 1rem; + padding-top: 1rem; + border-top: 1px solid var(--border-color); + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.75rem; + font-size: 0.875rem; +} + +.meta-item { + display: flex; + flex-direction: column; +} + +.meta-label { + color: var(--text-secondary); + margin-bottom: 0.25rem; +} + +.meta-value { + font-weight: 500; +} + +/* Chart Section */ +.chart-section { + background: var(--card-bg); + border-radius: 12px; + padding: 1.5rem; + box-shadow: var(--shadow); + margin-bottom: 2rem; +} + +.chart-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; + flex-wrap: wrap; + gap: 1rem; +} + +.chart-header h3 { + font-size: 1.25rem; + font-weight: 600; +} + +.chart-controls { + display: flex; + gap: 0.5rem; +} + +.period-btn { + padding: 0.5rem 1rem; + border: 1px solid var(--border-color); + background: var(--card-bg); + border-radius: 6px; + font-size: 0.875rem; + cursor: pointer; + transition: all 0.2s; +} + +.period-btn:hover { + border-color: var(--primary-color); + color: var(--primary-color); +} + +.period-btn.active { + background: var(--primary-color); + color: white; + border-color: var(--primary-color); +} + +.chart-container { + position: relative; + height: 300px; + width: 100%; +} + +/* Footer */ +footer { + text-align: center; + padding: 2rem 1rem; + color: var(--text-secondary); + font-size: 0.875rem; +} + +.disclaimer { + margin-top: 0.5rem; + font-size: 0.75rem; + opacity: 0.7; +} + +/* Loading State */ +.loading { + opacity: 0.6; + pointer-events: none; +} + +/* Responsive Design */ +@media (max-width: 768px) { + header h1 { + font-size: 1.5rem; + } + + .search-box { + flex-direction: column; + } + + .btn-primary { + width: 100%; + } + + .portfolio-header { + flex-direction: column; + align-items: flex-start; + } + + .portfolio-stats { + width: 100%; + justify-content: space-between; + } + + .portfolio-grid { + grid-template-columns: 1fr; + } + + .chart-header { + flex-direction: column; + align-items: flex-start; + } + + .chart-controls { + width: 100%; + flex-wrap: wrap; + } + + .chart-container { + height: 250px; + } +} + +@media (max-width: 480px) { + .container { + padding: 0.5rem; + } + + header { + padding: 1.5rem 1rem; + } + + header h1 { + font-size: 1.25rem; + } + + .subtitle { + font-size: 0.875rem; + } + + .portfolio-section, + .chart-section { + padding: 1rem; + } + + .stock-price { + font-size: 1.5rem; + } +} + +/* Animation */ +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.stock-card { + animation: fadeIn 0.3s ease-out; +} diff --git a/static/js/app.js b/static/js/app.js new file mode 100644 index 000000000..e0a2d94a8 --- /dev/null +++ b/static/js/app.js @@ -0,0 +1,416 @@ +// Stock Portfolio Tracker - Frontend JavaScript + +class PortfolioTracker { + constructor() { + this.portfolio = this.loadPortfolio(); + this.currentChart = null; + this.currentTicker = null; + this.currentPeriod = '1mo'; + this.init(); + } + + init() { + this.setupEventListeners(); + this.renderPortfolio(); + this.updatePortfolioStats(); + + // Auto-refresh every 30 seconds + setInterval(() => this.refreshPortfolio(), 30000); + } + + setupEventListeners() { + // Add stock button + document.getElementById('addStockBtn').addEventListener('click', () => this.addStock()); + + // Enter key in input + document.getElementById('tickerInput').addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + this.addStock(); + } + }); + + // Period buttons + document.querySelectorAll('.period-btn').forEach(btn => { + btn.addEventListener('click', (e) => { + document.querySelectorAll('.period-btn').forEach(b => b.classList.remove('active')); + e.target.classList.add('active'); + this.currentPeriod = e.target.dataset.period; + if (this.currentChart) { + this.loadChart(this.currentTicker, this.currentPeriod); + } + }); + }); + } + + async addStock() { + const input = document.getElementById('tickerInput'); + const ticker = input.value.trim().toUpperCase(); + const errorDiv = document.getElementById('errorMessage'); + + if (!ticker) { + this.showError('Please enter a stock symbol'); + return; + } + + if (this.portfolio.includes(ticker)) { + this.showError(`${ticker} is already in your portfolio`); + return; + } + + errorDiv.classList.remove('show'); + input.disabled = true; + document.getElementById('addStockBtn').disabled = true; + + try { + const response = await fetch(`/api/quote/${ticker}`); + const data = await response.json(); + + if (data.success) { + this.portfolio.push(ticker); + this.savePortfolio(); + this.renderPortfolio(); + this.updatePortfolioStats(); + input.value = ''; + } else { + this.showError(data.error || 'Failed to fetch stock data'); + } + } catch (error) { + this.showError('Network error. Please try again.'); + } finally { + input.disabled = false; + document.getElementById('addStockBtn').disabled = false; + input.focus(); + } + } + + async removeStock(ticker) { + this.portfolio = this.portfolio.filter(t => t !== ticker); + this.savePortfolio(); + this.renderPortfolio(); + this.updatePortfolioStats(); + + if (this.currentTicker === ticker) { + document.getElementById('chartSection').style.display = 'none'; + this.currentChart = null; + this.currentTicker = null; + } + } + + async refreshPortfolio() { + if (this.portfolio.length === 0) return; + + try { + const response = await fetch('/api/batch', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ tickers: this.portfolio }) + }); + + const data = await response.json(); + if (data.success) { + this.renderPortfolio(); + this.updatePortfolioStats(); + } + } catch (error) { + console.error('Failed to refresh portfolio:', error); + } + } + + async renderPortfolio() { + const grid = document.getElementById('portfolioGrid'); + + if (this.portfolio.length === 0) { + grid.innerHTML = ` +
+

👆 Add stocks to start tracking your portfolio

+

Try: AAPL, MSFT, GOOGL, TSLA, AMZN

+
+ `; + return; + } + + grid.innerHTML = ''; + + // Fetch all stock data + try { + const response = await fetch('/api/batch', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ tickers: this.portfolio }) + }); + + const data = await response.json(); + if (data.success) { + data.results.forEach(stock => { + if (stock.error) { + grid.innerHTML += this.createErrorCard(stock.ticker); + } else { + grid.innerHTML += this.createStockCard(stock); + } + }); + + // Attach event listeners to new cards + this.attachCardListeners(); + } + } catch (error) { + grid.innerHTML = '

Error loading portfolio. Please refresh.

'; + } + } + + createStockCard(stock) { + const changeClass = stock.change >= 0 ? 'positive' : 'negative'; + const changeSymbol = stock.change >= 0 ? '+' : ''; + const currency = stock.currency || 'USD'; + + return ` +
+
+
+

${stock.name}

+ ${stock.ticker} +
+ +
+
${this.formatCurrency(stock.price, currency)}
+
+ ${changeSymbol}${this.formatCurrency(stock.change, currency)} + (${changeSymbol}${stock.changePercent.toFixed(2)}%) +
+
+
+ Volume + ${this.formatNumber(stock.volume)} +
+
+ Market Cap + ${this.formatMarketCap(stock.marketCap)} +
+
+
+ `; + } + + createErrorCard(ticker) { + return ` +
+
+
+

${ticker}

+ Error +
+ +
+

Failed to load data

+
+ `; + } + + attachCardListeners() { + document.querySelectorAll('.stock-card').forEach(card => { + const ticker = card.dataset.ticker; + if (ticker) { + card.addEventListener('click', (e) => { + // Don't trigger if clicking remove button + if (!e.target.classList.contains('remove-btn')) { + this.loadChart(ticker, this.currentPeriod); + } + }); + } + }); + } + + async loadChart(ticker, period) { + this.currentTicker = ticker; + this.currentPeriod = period; + + const chartSection = document.getElementById('chartSection'); + const chartTitle = document.getElementById('chartTitle'); + chartSection.style.display = 'block'; + chartTitle.textContent = `${ticker} Price History`; + + try { + const response = await fetch(`/api/history/${ticker}?period=${period}`); + const data = await response.json(); + + if (data.success && data.data.length > 0) { + this.renderChart(data.data, ticker); + } else { + chartTitle.textContent = `No data available for ${ticker}`; + } + } catch (error) { + console.error('Failed to load chart:', error); + chartTitle.textContent = `Error loading chart for ${ticker}`; + } + } + + renderChart(data, ticker) { + const ctx = document.getElementById('priceChart').getContext('2d'); + + if (this.currentChart) { + this.currentChart.destroy(); + } + + const labels = data.map(d => { + const date = new Date(d.date); + return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); + }); + + const prices = data.map(d => d.close); + + this.currentChart = new Chart(ctx, { + type: 'line', + data: { + labels: labels, + datasets: [{ + label: 'Price', + data: prices, + borderColor: '#2563eb', + backgroundColor: 'rgba(37, 99, 235, 0.1)', + borderWidth: 2, + fill: true, + tension: 0.4, + pointRadius: 0, + pointHoverRadius: 4 + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: false + }, + tooltip: { + mode: 'index', + intersect: false, + callbacks: { + label: (context) => { + const point = data[context.dataIndex]; + return [ + `Open: $${point.open.toFixed(2)}`, + `High: $${point.high.toFixed(2)}`, + `Low: $${point.low.toFixed(2)}`, + `Close: $${point.close.toFixed(2)}` + ]; + } + } + } + }, + scales: { + y: { + beginAtZero: false, + ticks: { + callback: (value) => '$' + value.toFixed(2) + } + }, + x: { + ticks: { + maxTicksLimit: 10 + } + } + }, + interaction: { + mode: 'nearest', + axis: 'x', + intersect: false + } + } + }); + } + + updatePortfolioStats() { + if (this.portfolio.length === 0) { + document.getElementById('totalValue').textContent = '$0.00'; + document.getElementById('totalChange').textContent = '$0.00'; + return; + } + + // This is a simplified calculation - in a real app, you'd track shares and purchase prices + fetch('/api/batch', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ tickers: this.portfolio }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + let totalValue = 0; + let totalChange = 0; + + data.results.forEach(stock => { + if (!stock.error) { + totalValue += stock.price; + totalChange += stock.change; + } + }); + + document.getElementById('totalValue').textContent = this.formatCurrency(totalValue); + const changeClass = totalChange >= 0 ? 'positive' : 'negative'; + const changeElement = document.getElementById('totalChange'); + changeElement.textContent = `${totalChange >= 0 ? '+' : ''}${this.formatCurrency(totalChange)}`; + changeElement.className = `stat-value ${changeClass}`; + } + }) + .catch(error => console.error('Failed to update stats:', error)); + } + + formatCurrency(value, currency = 'USD') { + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: currency + }).format(value); + } + + formatNumber(value) { + if (value >= 1e9) { + return (value / 1e9).toFixed(2) + 'B'; + } else if (value >= 1e6) { + return (value / 1e6).toFixed(2) + 'M'; + } else if (value >= 1e3) { + return (value / 1e3).toFixed(2) + 'K'; + } + return value.toLocaleString(); + } + + formatMarketCap(value) { + if (!value) return 'N/A'; + if (value >= 1e12) { + return '$' + (value / 1e12).toFixed(2) + 'T'; + } else if (value >= 1e9) { + return '$' + (value / 1e9).toFixed(2) + 'B'; + } else if (value >= 1e6) { + return '$' + (value / 1e6).toFixed(2) + 'M'; + } + return this.formatCurrency(value); + } + + showError(message) { + const errorDiv = document.getElementById('errorMessage'); + errorDiv.textContent = message; + errorDiv.classList.add('show'); + setTimeout(() => { + errorDiv.classList.remove('show'); + }, 5000); + } + + loadPortfolio() { + const saved = localStorage.getItem('portfolio'); + return saved ? JSON.parse(saved) : []; + } + + savePortfolio() { + localStorage.setItem('portfolio', JSON.stringify(this.portfolio)); + } +} + +// Initialize the tracker when page loads +let tracker; +document.addEventListener('DOMContentLoaded', () => { + tracker = new PortfolioTracker(); +}); diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 000000000..4864d8780 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,80 @@ + + + + + + + Stock Portfolio Tracker | $10 Marketplace Prototype + + + + + + +
+
+
+

📈 Stock Portfolio Tracker

+

Track your investments in real-time

+
+
+ +
+
+ +
+
+ +
+
+

Your Portfolio

+
+
+ Total Value + $0.00 +
+
+ Total Change + $0.00 +
+
+
+ +
+
+

👆 Add stocks to start tracking your portfolio

+

Try: AAPL, MSFT, GOOGL, TSLA, AMZN

+
+
+
+ + +
+ +
+

Stock Portfolio Tracker - Marketplace Prototype ($10)

+

Data provided by Yahoo Finance. For educational purposes only.

+
+
+ + + + +