Spaces:
Sleeping
Sleeping
| #!/usr/bin/env python3 | |
| """ | |
| Premium Trading Dashboard - Full Enhanced Version | |
| Beautiful dashboard with sentiment analysis, Reddit integration, and advanced features | |
| """ | |
| import os | |
| import sys | |
| import pandas as pd | |
| import gradio as gr | |
| import plotly.graph_objects as go | |
| import plotly.express as px | |
| from datetime import datetime, timedelta, timezone | |
| import logging | |
| import requests | |
| import time | |
| import json | |
| import re | |
| import nltk | |
| import feedparser | |
| from urllib.parse import quote | |
| # Import dependencies with fallback | |
| try: | |
| from alpaca.trading.client import TradingClient | |
| from alpaca.trading.requests import GetOrdersRequest, GetPortfolioHistoryRequest | |
| from alpaca.trading.enums import OrderStatus, OrderSide | |
| from alpaca.data.timeframe import TimeFrame | |
| from alpaca.data.historical import StockHistoricalDataClient | |
| ALPACA_AVAILABLE = True | |
| except ImportError: | |
| ALPACA_AVAILABLE = False | |
| try: | |
| from textblob import TextBlob | |
| from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer | |
| SENTIMENT_AVAILABLE = True | |
| except ImportError: | |
| SENTIMENT_AVAILABLE = False | |
| try: | |
| import yfinance as yf | |
| YF_AVAILABLE = True | |
| except ImportError: | |
| YF_AVAILABLE = False | |
| # API Keys and Configuration | |
| API_KEY = os.getenv('ALPACA_API_KEY', 'PK2FD9B2S86LHR7ZBHG1') | |
| SECRET_KEY = os.getenv('ALPACA_SECRET_KEY', 'QPmGPDgbPArvHv6cldBXc7uWddapYcIAnBhtkuBW') | |
| VM_API_URL = os.getenv('VM_API_URL', 'http://34.56.193.18:8090') | |
| # Configure logging | |
| logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') | |
| logger = logging.getLogger(__name__) | |
| logger.info("π Starting Premium Trading Dashboard - Full Enhanced Version with 1-Hour P&L") | |
| # Download NLTK data | |
| try: | |
| nltk.download('punkt', quiet=True) | |
| nltk.download('vader_lexicon', quiet=True) | |
| nltk.download('brown', quiet=True) | |
| logger.info("β NLTK data downloaded") | |
| except Exception as e: | |
| logger.warning(f"β οΈ NLTK download failed: {e}") | |
| # Initialize sentiment analyzers | |
| sentiment_analyzer = None | |
| if SENTIMENT_AVAILABLE: | |
| try: | |
| sentiment_analyzer = SentimentIntensityAnalyzer() | |
| logger.info("β VADER sentiment analyzer initialized") | |
| except Exception as e: | |
| logger.warning(f"β οΈ Sentiment analyzer failed: {e}") | |
| # Initialize Alpaca clients | |
| trading_client = None | |
| data_client = None | |
| if ALPACA_AVAILABLE: | |
| try: | |
| trading_client = TradingClient(api_key=API_KEY, secret_key=SECRET_KEY) | |
| data_client = StockHistoricalDataClient(API_KEY, SECRET_KEY) | |
| logger.info("β Alpaca clients initialized") | |
| except Exception as e: | |
| logger.warning(f"β οΈ Alpaca clients failed: {e}") | |
| # HTTP headers for Reddit API | |
| headers = { | |
| 'User-Agent': 'TradingBot/1.0 (by u/TradingBot)' | |
| } | |
| # Color scheme | |
| COLORS = { | |
| 'primary': '#0070f3', | |
| 'success': '#00d647', | |
| 'error': '#ff0080', | |
| 'warning': '#f5a623', | |
| 'neutral': '#8b949e' | |
| } | |
| def fetch_from_vm(endpoint, default_value=None): | |
| """Fetch data from VM API server with fallback""" | |
| try: | |
| response = requests.get(f"{VM_API_URL}/api/{endpoint}", timeout=10) | |
| if response.status_code == 200: | |
| return response.json() | |
| else: | |
| logger.warning(f"VM API returned status {response.status_code}") | |
| return default_value | |
| except Exception as e: | |
| logger.warning(f"VM API error: {e}") | |
| return default_value | |
| def get_account_info(): | |
| """Get comprehensive account information""" | |
| if not trading_client: | |
| # Return demo data | |
| return { | |
| 'portfolio_value': 125000.00, | |
| 'buying_power': 31250.00, | |
| 'cash': 31250.00, | |
| 'day_change': 2750.50, | |
| 'equity': 125000.00, | |
| 'day_change_percent': 2.25 | |
| } | |
| try: | |
| account = trading_client.get_account() | |
| last_equity = float(account.last_equity) if account.last_equity else float(account.equity) | |
| current_equity = float(account.equity) | |
| day_change = current_equity - last_equity | |
| day_change_percent = (day_change / last_equity * 100) if last_equity > 0 else 0 | |
| return { | |
| 'portfolio_value': float(account.portfolio_value), | |
| 'buying_power': float(account.buying_power), | |
| 'cash': float(account.cash), | |
| 'day_change': day_change, | |
| 'equity': current_equity, | |
| 'day_change_percent': day_change_percent | |
| } | |
| except Exception as e: | |
| logger.error(f"Account info error: {e}") | |
| return {'error': str(e)} | |
| def get_order_history(limit=50): | |
| """Get recent order history""" | |
| if not trading_client: | |
| return [] | |
| try: | |
| request = GetOrdersRequest( | |
| status='all', | |
| limit=limit | |
| ) | |
| orders = trading_client.get_orders(filter=request) | |
| order_data = [] | |
| for order in orders: | |
| order_data.append({ | |
| 'symbol': order.symbol, | |
| 'side': order.side.value if hasattr(order.side, 'value') else str(order.side), | |
| 'qty': float(order.qty) if order.qty else 0, | |
| 'filled_qty': float(order.filled_qty) if order.filled_qty else 0, | |
| 'status': order.status.value if hasattr(order.status, 'value') else str(order.status), | |
| 'submitted_at': order.submitted_at.isoformat() if order.submitted_at else None, | |
| 'filled_at': order.filled_at.isoformat() if order.filled_at else None, | |
| 'filled_avg_price': float(order.filled_avg_price) if order.filled_avg_price else None | |
| }) | |
| return order_data | |
| except Exception as e: | |
| logger.error(f"Order history error: {e}") | |
| return [] | |
| def get_reddit_posts(symbol, start_time, cutoff_time): | |
| """Enhanced Reddit search with multiple strategies""" | |
| logger.info(f"π Searching Reddit for {symbol}...") | |
| reddit_posts = [] | |
| subreddits = ['wallstreetbets', 'stocks', 'investing', 'SecurityAnalysis', 'ValueInvesting'] | |
| search_terms = [symbol, f'{symbol} stock', f'{symbol} IPO', f'${symbol}', f'{symbol} earnings'] | |
| for subreddit in subreddits: | |
| for search_term in search_terms: | |
| try: | |
| url = f"https://www.reddit.com/r/{subreddit}/search.json" | |
| params = { | |
| 'q': search_term, | |
| 'restrict_sr': 'true', | |
| 'limit': 10, | |
| 't': 'all', | |
| 'sort': 'relevance' | |
| } | |
| response = requests.get(url, params=params, headers=headers, timeout=10) | |
| if response.status_code == 200: | |
| data = response.json() | |
| posts_found = len(data.get('data', {}).get('children', [])) | |
| logger.info(f"Reddit: r/{subreddit} + '{search_term}' found {posts_found} posts") | |
| for post in data.get('data', {}).get('children', []): | |
| post_data = post.get('data', {}) | |
| if not post_data.get('title'): | |
| continue | |
| # Filter by time window | |
| post_time = datetime.fromtimestamp(post_data.get('created_utc', 0), tz=timezone.utc) | |
| if not (start_time <= post_time <= cutoff_time): | |
| continue | |
| # Check relevance | |
| title_lower = post_data.get('title', '').lower() | |
| body_lower = post_data.get('selftext', '').lower() | |
| symbol_lower = symbol.lower() | |
| if symbol_lower not in title_lower and symbol_lower not in body_lower: | |
| continue | |
| # Remove duplicates | |
| post_id = post_data.get('id') | |
| if any(p.get('id') == post_id for p in reddit_posts): | |
| continue | |
| reddit_posts.append({ | |
| 'id': post_id, | |
| 'title': post_data.get('title', ''), | |
| 'selftext': post_data.get('selftext', ''), | |
| 'score': post_data.get('score', 0), | |
| 'num_comments': post_data.get('num_comments', 0), | |
| 'created_utc': post_data.get('created_utc', 0), | |
| 'subreddit': subreddit, | |
| 'search_term': search_term, | |
| 'url': f"https://reddit.com{post_data.get('permalink', '')}" | |
| }) | |
| time.sleep(0.1) # Rate limiting | |
| except Exception as e: | |
| logger.warning(f"Reddit search error for r/{subreddit}: {e}") | |
| continue | |
| logger.info(f"π Total Reddit posts found for {symbol}: {len(reddit_posts)}") | |
| return reddit_posts | |
| def get_google_news(symbol, start_time, cutoff_time): | |
| """Get Google News articles for symbol""" | |
| logger.info(f"π° Searching Google News for {symbol}...") | |
| try: | |
| # Build search query | |
| search_queries = [ | |
| f'{symbol} stock', | |
| f'{symbol} IPO', | |
| f'{symbol} earnings', | |
| f'{symbol} company' | |
| ] | |
| all_articles = [] | |
| for query in search_queries: | |
| try: | |
| encoded_query = quote(query) | |
| url = f"https://news.google.com/rss/search?q={encoded_query}&hl=en&gl=US&ceid=US:en" | |
| feed = feedparser.parse(url) | |
| for entry in feed.entries: | |
| # Parse publication date | |
| try: | |
| pub_date = datetime(*entry.published_parsed[:6], tzinfo=timezone.utc) | |
| if not (start_time <= pub_date <= cutoff_time): | |
| continue | |
| except: | |
| continue | |
| # Check relevance | |
| title_lower = entry.title.lower() | |
| summary_lower = getattr(entry, 'summary', '').lower() | |
| symbol_lower = symbol.lower() | |
| if symbol_lower not in title_lower and symbol_lower not in summary_lower: | |
| continue | |
| article = { | |
| 'title': entry.title, | |
| 'summary': getattr(entry, 'summary', ''), | |
| 'published': entry.published, | |
| 'published_parsed': pub_date.isoformat(), | |
| 'link': entry.link, | |
| 'source': getattr(entry, 'source', {}).get('title', 'Google News'), | |
| 'search_query': query | |
| } | |
| # Remove duplicates | |
| if not any(a.get('link') == article['link'] for a in all_articles): | |
| all_articles.append(article) | |
| time.sleep(0.2) # Rate limiting | |
| except Exception as e: | |
| logger.warning(f"Google News error for query '{query}': {e}") | |
| continue | |
| logger.info(f"π Total Google News articles found for {symbol}: {len(all_articles)}") | |
| return all_articles | |
| except Exception as e: | |
| logger.error(f"Google News search failed: {e}") | |
| return [] | |
| def analyze_sentiment(news_items): | |
| """Analyze sentiment of news items using VADER and TextBlob""" | |
| if not news_items or not SENTIMENT_AVAILABLE: | |
| return 0.0, 0.0, "Neutral", {'Reddit': [], 'Google News': []} | |
| logger.info(f"π§ Analyzing sentiment for {len(news_items)} items...") | |
| sentiment_scores = [] | |
| source_breakdown = {'Reddit': [], 'Google News': []} | |
| for item in news_items: | |
| try: | |
| # Determine text to analyze | |
| if 'title' in item and 'selftext' in item: # Reddit post | |
| text = f"{item['title']} {item.get('selftext', '')}" | |
| source = 'Reddit' | |
| weight = max(1, item.get('score', 1) + item.get('num_comments', 0) * 0.5) | |
| else: # News article | |
| text = f"{item['title']} {item.get('summary', '')}" | |
| source = 'Google News' | |
| weight = 1.0 | |
| if not text.strip(): | |
| continue | |
| # VADER sentiment | |
| vader_score = 0.0 | |
| if sentiment_analyzer: | |
| vader_result = sentiment_analyzer.polarity_scores(text) | |
| vader_score = vader_result['compound'] | |
| # TextBlob sentiment | |
| textblob_score = 0.0 | |
| try: | |
| blob = TextBlob(text) | |
| textblob_score = blob.sentiment.polarity | |
| except: | |
| pass | |
| # Combined score | |
| combined_score = (vader_score + textblob_score) / 2 | |
| weighted_score = combined_score * weight | |
| sentiment_scores.append(weighted_score) | |
| source_breakdown[source].append({ | |
| 'text': text[:200] + '...' if len(text) > 200 else text, | |
| 'vader_score': vader_score, | |
| 'textblob_score': textblob_score, | |
| 'combined_score': combined_score, | |
| 'weight': weight, | |
| 'weighted_score': weighted_score | |
| }) | |
| except Exception as e: | |
| logger.warning(f"Sentiment analysis error: {e}") | |
| continue | |
| if not sentiment_scores: | |
| return 0.0, 0.0, "Neutral", source_breakdown | |
| # Calculate average sentiment | |
| avg_sentiment = sum(sentiment_scores) / len(sentiment_scores) | |
| # Predict percentage change based on sentiment | |
| # Strong positive sentiment -> higher predicted gain | |
| # Strong negative sentiment -> higher predicted loss | |
| if avg_sentiment > 0.5: | |
| predicted_change = min(15.0, avg_sentiment * 20) # Cap at 15% | |
| prediction_label = "Strong Buy" | |
| elif avg_sentiment > 0.2: | |
| predicted_change = avg_sentiment * 10 | |
| prediction_label = "Buy" | |
| elif avg_sentiment > -0.2: | |
| predicted_change = avg_sentiment * 5 | |
| prediction_label = "Hold" | |
| elif avg_sentiment > -0.5: | |
| predicted_change = avg_sentiment * 10 | |
| prediction_label = "Sell" | |
| else: | |
| predicted_change = max(-15.0, avg_sentiment * 20) # Cap at -15% | |
| prediction_label = "Strong Sell" | |
| logger.info(f"π Sentiment analysis complete: {avg_sentiment:.3f} -> {prediction_label} ({predicted_change:+.1f}%)") | |
| return avg_sentiment, predicted_change, prediction_label, source_breakdown | |
| def get_pre_investment_news(symbol, investment_time, hours_before=12): | |
| """Get news from before investment time""" | |
| start_time = investment_time - timedelta(hours=hours_before) | |
| cutoff_time = investment_time - timedelta(minutes=30) # 30 min buffer | |
| logger.info(f"π Getting pre-investment news for {symbol}") | |
| logger.info(f" Time window: {start_time} to {cutoff_time}") | |
| # Get Reddit posts | |
| reddit_posts = get_reddit_posts(symbol, start_time, cutoff_time) | |
| # Get Google News | |
| google_news = get_google_news(symbol, start_time, cutoff_time) | |
| # Combine all news items | |
| all_news = reddit_posts + google_news | |
| logger.info(f"π Total news items: {len(all_news)} ({len(reddit_posts)} Reddit + {len(google_news)} News)") | |
| return all_news | |
| def refresh_account_overview(): | |
| """Refresh account overview with enhanced data""" | |
| logger.info("π Refreshing account overview...") | |
| info = get_account_info() | |
| if 'error' in info: | |
| return "Error", "Error", "Error", "Error", "Error" | |
| # Format with colors based on performance | |
| day_change_color = COLORS['success'] if info['day_change'] >= 0 else COLORS['error'] | |
| day_change_formatted = f"<span style='color: {day_change_color}'>${info['day_change']:+,.2f} ({info.get('day_change_percent', 0):+.2f}%)</span>" | |
| return ( | |
| f"${info['portfolio_value']:,.2f}", | |
| f"${info['buying_power']:,.2f}", | |
| f"${info['cash']:,.2f}", | |
| day_change_formatted, | |
| f"${info['equity']:,.2f}" | |
| ) | |
| def create_portfolio_chart(): | |
| """Create enhanced portfolio performance chart""" | |
| logger.info("π Creating portfolio chart...") | |
| if not trading_client: | |
| # Demo data | |
| dates = pd.date_range(start='2024-01-01', end='2024-12-31', freq='D') | |
| values = [100000 + i * 50 + (i % 30 - 15) * 200 for i in range(len(dates))] | |
| fig = go.Figure() | |
| fig.add_trace(go.Scatter( | |
| x=dates, | |
| y=values, | |
| mode='lines', | |
| name='Portfolio Value', | |
| line=dict(color=COLORS['primary'], width=2), | |
| fill='tonexty', | |
| fillcolor=f'rgba(0, 112, 243, 0.1)' | |
| )) | |
| fig.update_layout( | |
| title="Portfolio Performance (Demo Data)", | |
| xaxis_title="Date", | |
| yaxis_title="Portfolio Value ($)", | |
| hovermode='x unified', | |
| template='plotly_white' | |
| ) | |
| return fig | |
| try: | |
| # Get portfolio history from Alpaca | |
| request = GetPortfolioHistoryRequest( | |
| period='1M', | |
| timeframe=TimeFrame.Day | |
| ) | |
| portfolio_history = trading_client.get_portfolio_history(filter=request) | |
| if portfolio_history.equity: | |
| timestamps = [datetime.fromtimestamp(ts) for ts in portfolio_history.timestamp] | |
| equity_values = portfolio_history.equity | |
| fig = go.Figure() | |
| fig.add_trace(go.Scatter( | |
| x=timestamps, | |
| y=equity_values, | |
| mode='lines', | |
| name='Portfolio Value', | |
| line=dict(color=COLORS['primary'], width=2), | |
| fill='tonexty', | |
| fillcolor=f'rgba(0, 112, 243, 0.1)' | |
| )) | |
| fig.update_layout( | |
| title="Portfolio Performance (Last 30 Days)", | |
| xaxis_title="Date", | |
| yaxis_title="Portfolio Value ($)", | |
| hovermode='x unified', | |
| template='plotly_white' | |
| ) | |
| return fig | |
| except Exception as e: | |
| logger.error(f"Portfolio chart error: {e}") | |
| # Fallback empty chart | |
| fig = go.Figure() | |
| fig.update_layout(title="Portfolio Chart (No Data Available)") | |
| return fig | |
| def refresh_ipo_discoveries(): | |
| """Get IPO discoveries from VM""" | |
| logger.info("π Refreshing IPO discoveries...") | |
| vm_data = fetch_from_vm('ipos', []) | |
| if not vm_data: | |
| return """ | |
| <div style="padding: 2rem; text-align: center; background: #f8f9fa; border-radius: 8px; margin: 1rem 0;"> | |
| <h3>π IPO Discovery System</h3> | |
| <p>No recent IPO discoveries available. The system continuously monitors for new tradeable securities.</p> | |
| <p><small>π‘ VM Connection Status: Offline</small></p> | |
| </div> | |
| """ | |
| # Format IPO discoveries | |
| html_content = """ | |
| <div style="background: white; border-radius: 8px; padding: 1rem; margin: 1rem 0;"> | |
| <h3>π― Recent IPO Discoveries</h3> | |
| <table style="width: 100%; border-collapse: collapse; font-size: 0.9rem;"> | |
| <thead> | |
| <tr style="background: #f8f9fa; border-bottom: 2px solid #dee2e6;"> | |
| <th style="padding: 12px 8px; text-align: left;">Symbol</th> | |
| <th style="padding: 12px 8px; text-align: left;">Discovery Time</th> | |
| <th style="padding: 12px 8px; text-align: left;">Type</th> | |
| <th style="padding: 12px 8px; text-align: left;">Decision</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| """ | |
| for idx, ipo in enumerate(vm_data[:20]): # Show last 20 | |
| row_bg = "#f8f9fa" if idx % 2 == 0 else "white" | |
| symbol = ipo.get('symbol', 'N/A') | |
| discovery_time = ipo.get('discovery_time', 'N/A') | |
| asset_type = ipo.get('type', 'Unknown') | |
| decision = ipo.get('investment_decision', 'Pending') | |
| decision_color = COLORS['success'] if 'invested' in decision.lower() else COLORS['warning'] | |
| html_content += f""" | |
| <tr style="background: {row_bg}; border-bottom: 1px solid #dee2e6;"> | |
| <td style="padding: 10px 8px; font-weight: bold;">{symbol}</td> | |
| <td style="padding: 10px 8px;">{discovery_time}</td> | |
| <td style="padding: 10px 8px;">{asset_type}</td> | |
| <td style="padding: 10px 8px; color: {decision_color};">{decision}</td> | |
| </tr> | |
| """ | |
| html_content += """ | |
| </tbody> | |
| </table> | |
| </div> | |
| """ | |
| return html_content | |
| def refresh_investment_performance(): | |
| """Get investment performance with sentiment analysis""" | |
| logger.info("π Refreshing investment performance with sentiment analysis...") | |
| orders = get_order_history() | |
| if not orders: | |
| return """ | |
| <div style="padding: 2rem; text-align: center; background: #f8f9fa; border-radius: 8px; margin: 1rem 0;"> | |
| <h3>π° Investment Performance</h3> | |
| <p>No trading history available yet.</p> | |
| <p><small>Start trading to see performance analytics with sentiment analysis!</small></p> | |
| </div> | |
| """ | |
| # Group orders by symbol | |
| symbol_data = {} | |
| for order in orders: | |
| symbol = order['symbol'] | |
| if symbol not in symbol_data: | |
| symbol_data[symbol] = [] | |
| symbol_data[symbol].append(order) | |
| html_content = """ | |
| <div style="background: white; border-radius: 8px; padding: 1rem; margin: 1rem 0;"> | |
| <h3>π Investment Performance with Sentiment Analysis</h3> | |
| <table style="width: 100%; border-collapse: collapse; font-size: 0.85rem;"> | |
| <thead> | |
| <tr style="background: #f8f9fa; border-bottom: 2px solid #dee2e6;"> | |
| <th style="padding: 10px 6px; text-align: left;">Symbol</th> | |
| <th style="padding: 10px 6px; text-align: center;">Investment</th> | |
| <th style="padding: 10px 6px; text-align: center;">1-Hour P&L</th> | |
| <th style="padding: 10px 6px; text-align: center;">Sentiment</th> | |
| <th style="padding: 10px 6px; text-align: center;">Prediction</th> | |
| <th style="padding: 10px 6px; text-align: center;">Sources</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| """ | |
| for idx, (symbol, symbol_orders) in enumerate(list(symbol_data.items())[:15]): # Limit to 15 for performance | |
| row_bg = "#f8f9fa" if idx % 2 == 0 else "white" | |
| # Calculate investment amount | |
| total_investment = sum( | |
| float(order.get('filled_avg_price', 0)) * float(order.get('filled_qty', 0)) | |
| for order in symbol_orders | |
| if order.get('side') == 'buy' and order.get('status') == 'filled' | |
| ) | |
| if total_investment == 0: | |
| continue | |
| # Get investment time (first buy order) | |
| buy_orders = [o for o in symbol_orders if o.get('side') == 'buy' and o.get('filled_at')] | |
| if not buy_orders: | |
| continue | |
| investment_time = datetime.fromisoformat(buy_orders[0]['filled_at'].replace('Z', '+00:00')) | |
| # Run sentiment analysis | |
| logger.info(f"π§ Starting sentiment analysis for {symbol}...") | |
| try: | |
| news_items = get_pre_investment_news(symbol, investment_time, hours_before=12) | |
| avg_sentiment, predicted_change, prediction_label, source_breakdown = analyze_sentiment(news_items) | |
| sentiment_color = COLORS['success'] if avg_sentiment > 0.1 else COLORS['error'] if avg_sentiment < -0.1 else COLORS['neutral'] | |
| prediction_color = COLORS['success'] if predicted_change > 0 else COLORS['error'] if predicted_change < 0 else COLORS['neutral'] | |
| # Count sources | |
| reddit_count = len(source_breakdown.get('Reddit', [])) | |
| news_count = len(source_breakdown.get('Google News', [])) | |
| except Exception as e: | |
| logger.error(f"Sentiment analysis failed for {symbol}: {e}") | |
| avg_sentiment = 0.0 | |
| predicted_change = 0.0 | |
| prediction_label = "Error" | |
| sentiment_color = COLORS['neutral'] | |
| prediction_color = COLORS['neutral'] | |
| reddit_count = 0 | |
| news_count = 0 | |
| # Calculate IPO first-hour P&L using Yahoo Finance | |
| one_hour_pnl = 0.0 | |
| pnl_percentage = 0.0 | |
| try: | |
| if YF_AVAILABLE: | |
| # Get stock data for the investment day | |
| investment_date = investment_time.date() | |
| ticker = yf.Ticker(symbol) | |
| # Get minute-by-minute data for the investment day | |
| hist = ticker.history(period="1d", interval="1m", start=investment_date, end=investment_date + timedelta(days=1)) | |
| if not hist.empty: | |
| # Find IPO opening price and price 1 hour after IPO opening | |
| # IPO opening = first available price of the day (market open) | |
| ipo_open_price = hist.iloc[0]['Open'] # First price of the day | |
| ipo_open_time = hist.index[0] | |
| # Find price exactly 1 hour after IPO opened | |
| one_hour_after_ipo = ipo_open_time + timedelta(hours=1) | |
| # Find closest price to 1 hour after IPO opening | |
| one_hour_price = None | |
| one_hour_time_diff = float('inf') | |
| for timestamp, row in hist.iterrows(): | |
| time_diff = abs((timestamp - one_hour_after_ipo).total_seconds()) | |
| if time_diff < one_hour_time_diff and time_diff <= 30 * 60: # Within 30 minutes | |
| one_hour_price = row['Close'] | |
| one_hour_time_diff = time_diff | |
| if ipo_open_price and one_hour_price: | |
| # Calculate shares that could be purchased with our investment | |
| total_shares = total_investment / ipo_open_price if ipo_open_price > 0 else 0 | |
| # Calculate P&L based on IPO first-hour price movement | |
| price_change = one_hour_price - ipo_open_price | |
| one_hour_pnl = price_change * total_shares | |
| pnl_percentage = (price_change / ipo_open_price) * 100 if ipo_open_price > 0 else 0 | |
| logger.info(f"π {symbol}: IPO Open @ ${ipo_open_price:.2f}, 1hr later @ ${one_hour_price:.2f}, P&L: ${one_hour_pnl:+.2f} ({pnl_percentage:+.1f}%)") | |
| else: | |
| logger.warning(f"β οΈ {symbol}: Could not find IPO first-hour price data") | |
| one_hour_pnl = 0.0 | |
| else: | |
| logger.warning(f"β οΈ {symbol}: No historical data available") | |
| one_hour_pnl = 0.0 | |
| else: | |
| logger.warning("β οΈ yfinance not available, using mock P&L") | |
| one_hour_pnl = total_investment * 0.02 # Mock 2% gain | |
| pnl_percentage = 2.0 | |
| except Exception as e: | |
| logger.error(f"β Error calculating IPO first-hour P&L for {symbol}: {e}") | |
| one_hour_pnl = 0.0 | |
| pnl_percentage = 0.0 | |
| pnl_color = COLORS['success'] if one_hour_pnl >= 0 else COLORS['error'] | |
| html_content += f""" | |
| <tr style="background: {row_bg}; border-bottom: 1px solid #dee2e6;"> | |
| <td style="padding: 8px 6px; font-weight: bold;">{symbol}</td> | |
| <td style="padding: 8px 6px; text-align: center;">${total_investment:,.0f}</td> | |
| <td style="padding: 8px 6px; text-align: center; color: {pnl_color};">${one_hour_pnl:+,.2f}<br><small>({pnl_percentage:+.1f}%)</small></td> | |
| <td style="padding: 8px 6px; text-align: center; color: {sentiment_color};">{avg_sentiment:+.3f}</td> | |
| <td style="padding: 8px 6px; text-align: center; color: {prediction_color};">{prediction_label}<br><small>{predicted_change:+.1f}%</small></td> | |
| <td style="padding: 8px 6px; text-align: center; font-size: 0.8rem;">π¨οΈ{reddit_count}<br>π°{news_count}</td> | |
| </tr> | |
| """ | |
| html_content += """ | |
| </tbody> | |
| </table> | |
| <div style="margin-top: 1rem; padding: 1rem; background: #f8f9fa; border-radius: 4px; font-size: 0.8rem;"> | |
| <strong>π Analysis Legend:</strong><br> | |
| π¨οΈ Reddit posts analyzed | π° News articles analyzed<br> | |
| <strong>1-Hour P&L:</strong> IPO performance from opening price to 1 hour after IPO launch (e.g., 10am to 11am)<br> | |
| <strong>Sentiment:</strong> -1.0 (Very Negative) to +1.0 (Very Positive)<br> | |
| <strong>Prediction:</strong> Expected first-hour price movement based on sentiment analysis | |
| </div> | |
| </div> | |
| """ | |
| return html_content | |
| def execute_vm_command(command): | |
| """Execute command on VM""" | |
| logger.info(f"π» Executing VM command: {command}") | |
| try: | |
| response = requests.post(f"{VM_API_URL}/api/execute", | |
| json={'command': command}, | |
| timeout=30) | |
| if response.status_code == 200: | |
| result = response.json() | |
| output = result.get('output', 'No output') | |
| # Add color coding for common patterns | |
| if 'error' in output.lower() or 'failed' in output.lower(): | |
| output = f"<span style='color: {COLORS['error']}'>{output}</span>" | |
| elif 'success' in output.lower() or 'complete' in output.lower(): | |
| output = f"<span style='color: {COLORS['success']}'>{output}</span>" | |
| return f"$ {command}\n{output}" | |
| else: | |
| return f"$ {command}\nError: HTTP {response.status_code}" | |
| except Exception as e: | |
| return f"$ {command}\nError: {str(e)}" | |
| def refresh_system_logs(): | |
| """Get system logs from VM""" | |
| logger.info("π Refreshing system logs...") | |
| vm_logs = fetch_from_vm('logs', {'logs': 'No logs available'}) | |
| if isinstance(vm_logs, dict) and 'logs' in vm_logs: | |
| logs_text = vm_logs['logs'] | |
| else: | |
| logs_text = "No logs available from VM" | |
| # Add basic color coding | |
| lines = logs_text.split('\n') | |
| colored_lines = [] | |
| for line in lines: | |
| if 'ERROR' in line or 'error' in line: | |
| colored_lines.append(f"<span style='color: {COLORS['error']}'>{line}</span>") | |
| elif 'WARN' in line or 'warning' in line: | |
| colored_lines.append(f"<span style='color: {COLORS['warning']}'>{line}</span>") | |
| elif 'INFO' in line or 'success' in line: | |
| colored_lines.append(f"<span style='color: {COLORS['success']}'>{line}</span>") | |
| else: | |
| colored_lines.append(line) | |
| return '\n'.join(colored_lines[-100:]) # Last 100 lines | |
| def create_enhanced_dashboard(): | |
| """Create the enhanced dashboard with all features""" | |
| logger.info("π¨ Creating enhanced dashboard interface...") | |
| # Custom CSS for better styling | |
| custom_css = """ | |
| .gradio-container { | |
| max-width: 1400px !important; | |
| margin: auto !important; | |
| } | |
| .metric-card { | |
| background: white !important; | |
| border: 1px solid #e1e5e9 !important; | |
| border-radius: 8px !important; | |
| padding: 1rem !important; | |
| } | |
| """ | |
| # ALL components must be defined inside this context | |
| with gr.Blocks( | |
| title="π Premium Trading Dashboard", | |
| theme=gr.themes.Soft(primary_hue="blue"), | |
| css=custom_css | |
| ) as demo: | |
| logger.info("πΌοΈ Inside Blocks context - creating enhanced interface") | |
| # Header with gradient | |
| gr.HTML(""" | |
| <div style="text-align: center; padding: 3rem; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; margin-bottom: 2rem; border-radius: 16px; box-shadow: 0 8px 32px rgba(0,0,0,0.1);"> | |
| <h1 style="margin: 0; font-size: 3rem; font-weight: 700;">π Premium Trading Dashboard</h1> | |
| <p style="margin: 1rem 0 0 0; font-size: 1.3rem; opacity: 0.9;">Advanced IPO Trading with AI-Powered Sentiment Analysis</p> | |
| <div style="margin-top: 1rem; font-size: 0.9rem; opacity: 0.8;"> | |
| π Real-time Data β’ π§ Sentiment Analysis β’ π Reddit Integration β’ π° News Monitoring | |
| </div> | |
| </div> | |
| """) | |
| with gr.Tabs(): | |
| # Portfolio Overview Tab | |
| with gr.Tab("π Portfolio Overview"): | |
| gr.Markdown("## πΌ Account Summary") | |
| with gr.Row(): | |
| portfolio_value = gr.HTML(label="π° Portfolio Value") | |
| buying_power = gr.HTML(label="π³ Buying Power") | |
| cash = gr.HTML(label="π΅ Cash") | |
| day_change = gr.HTML(label="π Day Change") | |
| equity = gr.HTML(label="π¦ Total Equity") | |
| refresh_overview_btn = gr.Button("π Refresh Overview", variant="primary", size="lg") | |
| gr.Markdown("## π Portfolio Performance") | |
| portfolio_chart = gr.Plot(label="Portfolio Value Over Time") | |
| refresh_chart_btn = gr.Button("π Refresh Chart", variant="secondary") | |
| # IPO Discoveries Tab | |
| with gr.Tab("π IPO Discoveries"): | |
| gr.Markdown("## π― IPO Discovery & Classification") | |
| ipo_discoveries = gr.HTML() | |
| refresh_ipo_btn = gr.Button("π Refresh IPO Data", variant="primary", size="lg") | |
| # Investment Performance Tab with Sentiment Analysis | |
| with gr.Tab("π° Investment Performance + Sentiment"): | |
| gr.Markdown("## π Advanced P&L Analysis with AI Sentiment") | |
| gr.HTML(""" | |
| <div style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); color: white; padding: 1rem; border-radius: 8px; margin-bottom: 1rem; text-align: center;"> | |
| <strong>π§ AI-Powered Sentiment Analysis</strong><br> | |
| <small>Analyzes Reddit (including WallStreetBets) and Google News from 12 hours before each investment</small> | |
| </div> | |
| """) | |
| investment_performance = gr.HTML() | |
| refresh_performance_btn = gr.Button("π Refresh Performance + Sentiment", variant="primary", size="lg") | |
| # VM Terminal Tab | |
| with gr.Tab("π» VM Terminal"): | |
| gr.Markdown("## π₯οΈ Remote Terminal Access") | |
| with gr.Row(): | |
| command_input = gr.Textbox( | |
| label="Command", | |
| placeholder="Enter command (e.g., 'ls -la', 'tail -n 20 script.log', 'ps aux')", | |
| scale=4 | |
| ) | |
| execute_btn = gr.Button("βΆοΈ Execute", variant="primary", scale=1) | |
| terminal_output = gr.Textbox( | |
| label="Terminal Output", | |
| lines=15, | |
| interactive=False, | |
| show_copy_button=True | |
| ) | |
| # Quick command buttons | |
| with gr.Row(): | |
| ls_btn = gr.Button("π ls -la", size="sm") | |
| logs_btn = gr.Button("π tail logs", size="sm") | |
| status_btn = gr.Button("β‘ system status", size="sm") | |
| portfolio_btn = gr.Button("πΌ check portfolio", size="sm") | |
| # System Logs Tab | |
| with gr.Tab("π System Logs"): | |
| gr.Markdown("## π Trading Bot Activity Logs") | |
| system_logs = gr.Textbox( | |
| label="System Logs", | |
| lines=20, | |
| interactive=False, | |
| show_copy_button=True | |
| ) | |
| refresh_logs_btn = gr.Button("π Refresh Logs", variant="primary", size="lg") | |
| # Footer | |
| gr.HTML(""" | |
| <div style="text-align: center; padding: 2rem; color: #666; border-top: 1px solid #eaeaea; margin-top: 3rem; background: white; border-radius: 16px;"> | |
| <p style="font-size: 1.1rem;"><strong>π€ Advanced Automated Trading Dashboard</strong></p> | |
| <p style="font-size: 0.95rem;">Real-time data from Alpaca Markets β’ VM Analytics β’ AI Sentiment Analysis β’ Built with β€οΈ</p> | |
| <p style="font-size: 0.85rem; margin-top: 1rem; opacity: 0.7;"> | |
| π Last Updated: <span id="timestamp">{}</span> β’ | |
| π‘ VM Status: Connected β’ | |
| π§ AI Analysis: Active β’ | |
| π Data Sources: Reddit, Google News, Alpaca Markets | |
| </p> | |
| </div> | |
| """.format(datetime.now().strftime("%Y-%m-%d %H:%M:%S UTC"))) | |
| # Event Handlers - ALL INSIDE the Blocks context | |
| logger.info("π Setting up enhanced event handlers...") | |
| # Portfolio tab events | |
| refresh_overview_btn.click( | |
| fn=refresh_account_overview, | |
| outputs=[portfolio_value, buying_power, cash, day_change, equity] | |
| ) | |
| refresh_chart_btn.click( | |
| fn=create_portfolio_chart, | |
| outputs=[portfolio_chart] | |
| ) | |
| # IPO tab events | |
| refresh_ipo_btn.click( | |
| fn=refresh_ipo_discoveries, | |
| outputs=[ipo_discoveries] | |
| ) | |
| # Performance tab events (with sentiment analysis) | |
| refresh_performance_btn.click( | |
| fn=refresh_investment_performance, | |
| outputs=[investment_performance] | |
| ) | |
| # Terminal events | |
| execute_btn.click( | |
| fn=execute_vm_command, | |
| inputs=[command_input], | |
| outputs=[terminal_output] | |
| ) | |
| # Quick command buttons | |
| ls_btn.click( | |
| fn=lambda: execute_vm_command("ls -la"), | |
| outputs=[terminal_output] | |
| ) | |
| logs_btn.click( | |
| fn=lambda: execute_vm_command("tail -n 20 script.log"), | |
| outputs=[terminal_output] | |
| ) | |
| status_btn.click( | |
| fn=lambda: execute_vm_command("ps aux | grep python"), | |
| outputs=[terminal_output] | |
| ) | |
| portfolio_btn.click( | |
| fn=lambda: execute_vm_command("cat portfolio.txt"), | |
| outputs=[terminal_output] | |
| ) | |
| # System logs events | |
| refresh_logs_btn.click( | |
| fn=refresh_system_logs, | |
| outputs=[system_logs] | |
| ) | |
| # Initial data load | |
| demo.load( | |
| fn=refresh_account_overview, | |
| outputs=[portfolio_value, buying_power, cash, day_change, equity] | |
| ) | |
| demo.load( | |
| fn=create_portfolio_chart, | |
| outputs=[portfolio_chart] | |
| ) | |
| demo.load( | |
| fn=refresh_ipo_discoveries, | |
| outputs=[ipo_discoveries] | |
| ) | |
| demo.load( | |
| fn=refresh_system_logs, | |
| outputs=[system_logs] | |
| ) | |
| demo.queue() | |
| logger.info("β Enhanced event handlers configured successfully") | |
| logger.info("β Enhanced dashboard created successfully") | |
| return demo | |
| if __name__ == "__main__": | |
| try: | |
| demo = create_enhanced_dashboard() | |
| logger.info("β Enhanced dashboard created successfully!") | |
| logger.info("π Launching enhanced dashboard server...") | |
| demo.launch() | |
| logger.info("β Enhanced dashboard launched successfully!") | |
| except Exception as e: | |
| logger.error(f"β Enhanced dashboard failed: {e}") | |
| raise |