Spaces:
Runtime error
Runtime error
| #!/usr/bin/env python3 | |
| """ | |
| Premium Trading Dashboard - Full Featured | |
| Beautiful Vercel-style dashboard with VM data integration | |
| """ | |
| 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 | |
| from alpaca.trading.client import TradingClient | |
| from alpaca.trading.requests import GetOrdersRequest, GetPortfolioHistoryRequest | |
| from alpaca.trading.enums import OrderStatus | |
| from alpaca.data.timeframe import TimeFrame | |
| from alpaca.data.historical import StockHistoricalDataClient | |
| from textblob import TextBlob | |
| from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer | |
| import nltk | |
| # Import yfinance with fallback | |
| try: | |
| import yfinance as yf | |
| YF_AVAILABLE = True | |
| except ImportError as e: | |
| print(f"Warning: yfinance not available: {e}") | |
| YF_AVAILABLE = False | |
| # Get API keys and VM URL from environment variables | |
| 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') # Set this in Hugging Face | |
| # Configure detailed logging for debugging | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', | |
| handlers=[ | |
| logging.StreamHandler(), | |
| ] | |
| ) | |
| logger = logging.getLogger(__name__) | |
| # Log startup information | |
| logger.info("🚀 Starting Premium Trading Dashboard... (Build: 2025-07-29 05:15 - Fixed directory structure)") | |
| logger.info(f"Python version: {sys.version}") | |
| logger.info(f"Working directory: {os.getcwd()}") | |
| # Download required NLTK data | |
| logger.info("📚 Downloading NLTK data...") | |
| try: | |
| nltk.download('punkt', quiet=True) | |
| nltk.download('vader_lexicon', quiet=True) | |
| nltk.download('brown', quiet=True) | |
| logger.info("✅ NLTK data downloaded successfully") | |
| except Exception as e: | |
| logger.warning(f"⚠️ NLTK download warning: {e}") | |
| # Initialize Alpaca clients | |
| logger.info("🔌 Initializing Alpaca trading client...") | |
| try: | |
| trading_client = TradingClient(api_key=API_KEY, secret_key=SECRET_KEY) | |
| logger.info("✅ Alpaca trading client initialized successfully") | |
| except Exception as e: | |
| logger.error(f"❌ Failed to initialize Alpaca trading client: {e}") | |
| raise | |
| logger.info("📊 Initializing Alpaca data client...") | |
| try: | |
| data_client = StockHistoricalDataClient(API_KEY, SECRET_KEY) | |
| logger.info("✅ Alpaca data client initialized successfully") | |
| except Exception as e: | |
| logger.error(f"❌ Failed to initialize Alpaca data client: {e}") | |
| raise | |
| # Initialize sentiment analyzers | |
| logger.info("🧠 Initializing sentiment analysis engines...") | |
| try: | |
| vader = SentimentIntensityAnalyzer() | |
| logger.info("✅ VADER sentiment analyzer initialized") | |
| except Exception as e: | |
| logger.error(f"❌ Failed to initialize VADER: {e}") | |
| raise | |
| try: | |
| from textblob import TextBlob | |
| # Test TextBlob | |
| test_blob = TextBlob("test") | |
| logger.info("✅ TextBlob sentiment analyzer initialized") | |
| except Exception as e: | |
| logger.error(f"❌ Failed to initialize TextBlob: {e}") | |
| raise | |
| headers = {'User-Agent': 'TradingHistoryBacktester/1.0'} | |
| logger.info("✅ HTTP headers configured") | |
| # Modern color scheme | |
| COLORS = { | |
| 'primary': '#0070f3', | |
| 'success': '#00d647', | |
| 'error': '#ff0080', | |
| 'warning': '#f5a623', | |
| 'neutral': '#8b949e', | |
| 'background': '#fafafa', | |
| 'surface': '#ffffff', | |
| 'text': '#000000', | |
| 'text_secondary': '#666666', | |
| 'border': '#eaeaea' | |
| } | |
| def fetch_from_vm(endpoint, default_value=None): | |
| """Fetch data from VM API server""" | |
| 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 {endpoint} returned {response.status_code}") | |
| return default_value | |
| except Exception as e: | |
| logger.error(f"Error fetching from VM {endpoint}: {e}") | |
| return default_value | |
| def get_account_info(): | |
| """Get current account information from Alpaca""" | |
| try: | |
| account = trading_client.get_account() | |
| return { | |
| 'portfolio_value': float(account.portfolio_value), | |
| 'buying_power': float(account.buying_power), | |
| 'cash': float(account.cash), | |
| 'equity': float(account.equity), | |
| 'day_change': float(getattr(account, 'unrealized_pl', 0)) if hasattr(account, 'unrealized_pl') else 0, | |
| 'day_change_percent': float(getattr(account, 'unrealized_plpc', 0)) * 100 if hasattr(account, 'unrealized_plpc') else 0, | |
| 'last_equity': float(account.last_equity) if account.last_equity else 0 | |
| } | |
| except Exception as e: | |
| logger.error(f"Error fetching account info: {e}") | |
| return { | |
| 'portfolio_value': 0, 'buying_power': 0, 'cash': 0, 'equity': 0, | |
| 'day_change': 0, 'day_change_percent': 0, 'last_equity': 0 | |
| } | |
| def get_portfolio_history(): | |
| """Get portfolio value history from Alpaca""" | |
| try: | |
| portfolio_history_request = GetPortfolioHistoryRequest( | |
| period="1M", | |
| timeframe="1D", | |
| extended_hours=False | |
| ) | |
| portfolio_history = trading_client.get_portfolio_history(portfolio_history_request) | |
| timestamps = [datetime.fromtimestamp(ts, tz=timezone.utc) for ts in portfolio_history.timestamp] | |
| equity_values = portfolio_history.equity | |
| df = pd.DataFrame({ | |
| 'timestamp': timestamps, | |
| 'equity': equity_values | |
| }) | |
| return df.dropna() | |
| except Exception as e: | |
| logger.error(f"Error fetching portfolio history: {e}") | |
| return pd.DataFrame() | |
| def get_current_positions(): | |
| """Get current positions""" | |
| try: | |
| positions = trading_client.get_all_positions() | |
| position_data = [] | |
| for position in positions: | |
| position_data.append({ | |
| 'symbol': position.symbol, | |
| 'qty': float(position.qty), | |
| 'market_value': float(position.market_value), | |
| 'cost_basis': float(position.cost_basis), | |
| 'unrealized_pl': float(position.unrealized_pl), | |
| 'unrealized_plpc': float(position.unrealized_plpc) * 100, | |
| 'current_price': float(position.current_price) if position.current_price else 0 | |
| }) | |
| return position_data | |
| except Exception as e: | |
| logger.error(f"Error fetching positions: {e}") | |
| return [] | |
| def create_portfolio_chart(): | |
| """Create beautiful portfolio value chart""" | |
| portfolio_df = get_portfolio_history() | |
| if portfolio_df.empty: | |
| fig = go.Figure() | |
| fig.add_annotation( | |
| text="No portfolio history available", | |
| x=0.5, y=0.5, | |
| xref="paper", yref="paper", | |
| showarrow=False, | |
| font=dict(size=16, color=COLORS['text_secondary']) | |
| ) | |
| else: | |
| fig = go.Figure() | |
| fig.add_trace(go.Scatter( | |
| x=portfolio_df['timestamp'], | |
| y=portfolio_df['equity'], | |
| mode='lines', | |
| name='Portfolio Value', | |
| line=dict(color=COLORS['primary'], width=3), | |
| fill='tonexty', | |
| fillcolor=f"rgba(0, 112, 243, 0.1)", | |
| hovertemplate='<b>%{y:$,.2f}</b><br>%{x}<extra></extra>' | |
| )) | |
| if len(portfolio_df) > 0: | |
| current_value = portfolio_df['equity'].iloc[-1] | |
| fig.add_annotation( | |
| x=portfolio_df['timestamp'].iloc[-1], | |
| y=current_value, | |
| text=f"${current_value:,.2f}", | |
| showarrow=True, | |
| arrowhead=2, | |
| arrowcolor=COLORS['primary'], | |
| bgcolor="white", | |
| bordercolor=COLORS['primary'], | |
| borderwidth=2, | |
| font=dict(size=12, color=COLORS['text']) | |
| ) | |
| fig.update_layout( | |
| title=dict( | |
| text="Portfolio Value (Last 30 Days)", | |
| font=dict(size=24, color=COLORS['text'], family="Inter"), | |
| x=0.02 | |
| ), | |
| xaxis=dict( | |
| title="Date", | |
| showgrid=True, | |
| gridcolor=COLORS['border'], | |
| color=COLORS['text_secondary'] | |
| ), | |
| yaxis=dict( | |
| title="Portfolio Value ($)", | |
| showgrid=True, | |
| gridcolor=COLORS['border'], | |
| color=COLORS['text_secondary'], | |
| tickformat='$,.0f' | |
| ), | |
| plot_bgcolor='white', | |
| paper_bgcolor='white', | |
| height=400, | |
| margin=dict(l=60, r=40, t=60, b=60), | |
| hovermode='x unified', | |
| showlegend=False | |
| ) | |
| return fig | |
| def create_ipo_discovery_chart(): | |
| """Create IPO discovery chart with investment decisions""" | |
| ipos = fetch_from_vm('ipos?limit=100', []) | |
| if not ipos: | |
| fig = go.Figure() | |
| fig.add_annotation( | |
| text="No IPO data available from VM", | |
| x=0.5, y=0.5, | |
| xref="paper", yref="paper", | |
| showarrow=False, | |
| font=dict(size=16, color=COLORS['text_secondary']) | |
| ) | |
| else: | |
| # Count by status | |
| status_counts = {} | |
| for ipo in ipos: | |
| status = ipo.get('investment_status', 'UNKNOWN') | |
| status_counts[status] = status_counts.get(status, 0) + 1 | |
| # Create pie chart | |
| labels = list(status_counts.keys()) | |
| values = list(status_counts.values()) | |
| # Map status to colors | |
| color_map = { | |
| 'INVESTED': COLORS['success'], | |
| 'ELIGIBLE_NOT_INVESTED': COLORS['warning'], | |
| 'WRONG_TYPE': COLORS['neutral'], | |
| 'UNKNOWN': COLORS['error'] | |
| } | |
| colors = [color_map.get(label, COLORS['neutral']) for label in labels] | |
| fig = go.Figure(data=[go.Pie( | |
| labels=labels, | |
| values=values, | |
| hole=0.4, | |
| marker=dict(colors=colors), | |
| textinfo='label+percent', | |
| textposition='outside' | |
| )]) | |
| fig.update_layout( | |
| title=dict( | |
| text="IPO Investment Decisions", | |
| font=dict(size=24, color=COLORS['text'], family="Inter"), | |
| x=0.5 | |
| ), | |
| plot_bgcolor='white', | |
| paper_bgcolor='white', | |
| height=400, | |
| margin=dict(l=60, r=60, t=60, b=60), | |
| showlegend=True | |
| ) | |
| return fig | |
| def refresh_account_overview(): | |
| """Refresh account overview display""" | |
| account = get_account_info() | |
| portfolio_value = f"${account['portfolio_value']:,.2f}" | |
| buying_power = f"${account['buying_power']:,.2f}" | |
| cash = f"${account['cash']:,.2f}" | |
| day_change_value = account['day_change'] | |
| day_change_percent = account['day_change_percent'] | |
| if day_change_value > 0: | |
| day_change = f"↗️ +${day_change_value:,.2f} (+{day_change_percent:.2f}%)" | |
| elif day_change_value < 0: | |
| day_change = f"↘️ ${day_change_value:,.2f} ({day_change_percent:.2f}%)" | |
| else: | |
| day_change = f"➡️ ${day_change_value:,.2f} ({day_change_percent:.2f}%)" | |
| equity = f"${account['equity']:,.2f}" | |
| return portfolio_value, buying_power, cash, day_change, equity | |
| def refresh_positions_table(): | |
| """Refresh current positions table""" | |
| positions = get_current_positions() | |
| if not positions: | |
| return pd.DataFrame(columns=['Symbol', 'Quantity', 'Market Value', 'Unrealized P&L', 'Unrealized %']) | |
| df_data = [] | |
| for pos in positions: | |
| pnl_indicator = "🟢" if pos['unrealized_pl'] > 0 else "🔴" if pos['unrealized_pl'] < 0 else "⚪" | |
| df_data.append({ | |
| 'Symbol': f"{pnl_indicator} {pos['symbol']}", | |
| 'Quantity': f"{pos['qty']:.0f}", | |
| 'Market Value': f"${pos['market_value']:,.2f}", | |
| 'Unrealized P&L': f"${pos['unrealized_pl']:,.2f}", | |
| 'Unrealized %': f"{pos['unrealized_plpc']:.2f}%" | |
| }) | |
| return pd.DataFrame(df_data) | |
| def refresh_ipo_discoveries_table(): | |
| """Refresh IPO discoveries table with investment decisions""" | |
| ipos = fetch_from_vm('ipos?limit=100', []) | |
| if not ipos: | |
| return pd.DataFrame(columns=['Status', 'Symbol', 'Security Type', 'Price', 'Detected At']) | |
| df_data = [] | |
| for ipo in ipos: | |
| status_emoji = ipo.get('status_emoji', '⚪') | |
| status = ipo.get('investment_status', 'UNKNOWN') | |
| # Clean up status for display | |
| display_status = { | |
| 'INVESTED': '🟢 INVESTED', | |
| 'ELIGIBLE_NOT_INVESTED': '🟡 ELIGIBLE', | |
| 'WRONG_TYPE': '⚪ WRONG TYPE', | |
| 'UNKNOWN': '🔴 UNKNOWN' | |
| }.get(status, '⚪ UNKNOWN') | |
| df_data.append({ | |
| 'Status': display_status, | |
| 'Symbol': ipo.get('symbol', 'N/A'), | |
| 'Security Type': ipo.get('security_type', 'N/A'), | |
| 'Price': f"${ipo.get('trading_price', 0)}" if ipo.get('trading_price') != 'N/A' else 'N/A', | |
| 'Detected At': ipo.get('detected_at', 'N/A') | |
| }) | |
| return pd.DataFrame(df_data) | |
| def get_order_history(): | |
| """Get order history from Alpaca""" | |
| try: | |
| # Use Method 2 which works: 1 year with CLOSED status | |
| end_date = datetime.now(timezone.utc) | |
| start_date = end_date - timedelta(days=365) | |
| order_request = GetOrdersRequest( | |
| status="closed", # This is the key - use "closed" status | |
| limit=500, | |
| after=start_date, | |
| until=end_date | |
| ) | |
| orders = trading_client.get_orders(order_request) | |
| logger.info(f"Successfully fetched {len(orders)} orders using closed status filter") | |
| return orders | |
| except Exception as e: | |
| logger.error(f"Error fetching order history: {e}") | |
| return [] | |
| def refresh_investment_performance_table(): | |
| """Refresh investment performance table with P&L and sentiment analysis for all trading symbols""" | |
| logger.info("📊 Starting investment performance table refresh...") | |
| # Get IPO data and orders | |
| logger.info("🔌 Fetching IPO data from VM...") | |
| ipos = fetch_from_vm('ipos?limit=100', []) | |
| logger.info(f"📈 Retrieved {len(ipos)} IPO records from VM") | |
| logger.info("📋 Fetching order history from Alpaca...") | |
| orders = get_order_history() | |
| logger.info(f"📝 Retrieved {len(orders)} orders from Alpaca") | |
| logger.info("💼 Fetching current positions from Alpaca...") | |
| positions = get_current_positions() | |
| logger.info(f"🏦 Retrieved {len(positions)} current positions") | |
| # Create proper empty DataFrame with correct column names | |
| columns = ['Symbol', 'Status', 'IPO Price', 'Buy Price', 'Sell Price', 'Investment', 'P&L ($)', 'P&L (%)', 'Sentiment', 'Predicted', 'Date'] | |
| logger.info(f"Found {len(orders)} total orders for performance analysis") | |
| if not orders: | |
| return pd.DataFrame(columns=columns) | |
| # Get all unique symbols from order history | |
| symbols_traded = set() | |
| for order in orders: | |
| if hasattr(order, 'symbol') and order.symbol: | |
| symbols_traded.add(order.symbol) | |
| logger.info(f"Found {len(symbols_traded)} unique symbols traded: {list(symbols_traded)}") | |
| # Create IPO price lookup from VM data | |
| ipo_price_lookup = {} | |
| for ipo in ipos: | |
| symbol = ipo.get('symbol', '') | |
| if symbol: | |
| try: | |
| price = float(ipo.get('trading_price', 0)) | |
| if price > 0: | |
| ipo_price_lookup[symbol] = price | |
| except (ValueError, TypeError): | |
| pass | |
| invested_data = [] | |
| # Process each symbol that was traded | |
| for symbol in sorted(symbols_traded): | |
| # Get all orders for this symbol | |
| symbol_orders = [o for o in orders if o.symbol == symbol] | |
| if symbol_orders: | |
| # Calculate from actual orders | |
| buy_orders = [o for o in symbol_orders if o.side.value == 'buy'] | |
| sell_orders = [o for o in symbol_orders if o.side.value == 'sell'] | |
| if buy_orders: | |
| total_bought = sum(float(o.filled_qty or 0) for o in buy_orders) | |
| total_cost = sum(float(o.filled_qty or 0) * float(o.filled_avg_price or 0) for o in buy_orders) | |
| avg_buy_price = total_cost / total_bought if total_bought > 0 else 0 | |
| total_sold = sum(float(o.filled_qty or 0) for o in sell_orders) | |
| current_qty = total_bought - total_sold | |
| # Get IPO price if available | |
| ipo_price = ipo_price_lookup.get(symbol, 0) | |
| # Get first buy date and time for sentiment analysis | |
| first_buy_order = min(buy_orders, key=lambda x: x.filled_at) | |
| first_buy_date = first_buy_order.filled_at.strftime('%Y-%m-%d') | |
| investment_time = first_buy_order.filled_at | |
| logger.info(f"Date for {symbol}: {first_buy_date} (from {first_buy_order.filled_at})") | |
| # Calculate sell price (average of all sells) | |
| if sell_orders: | |
| avg_sell_price = sum(float(o.filled_qty or 0) * float(o.filled_avg_price or 0) for o in sell_orders) / sum(float(o.filled_qty or 0) for o in sell_orders) | |
| else: | |
| avg_sell_price = 0 | |
| current_qty = total_bought - total_sold | |
| if current_qty > 0: | |
| # Still holding - use current position for P&L | |
| status = "🟦 HOLDING" | |
| pos = next((p for p in positions if p['symbol'] == symbol), None) | |
| if pos: | |
| current_price = pos['current_price'] | |
| current_value = current_qty * current_price | |
| investment = current_qty * avg_buy_price | |
| pl_dollars = current_value - investment | |
| pl_percent = (pl_dollars / investment * 100) if investment > 0 else 0 | |
| else: | |
| # No current position data | |
| investment = current_qty * avg_buy_price | |
| pl_dollars = 0 | |
| pl_percent = 0 | |
| else: | |
| # Sold all - calculate realized P&L | |
| status = "🟨 SOLD" | |
| investment = total_cost | |
| sold_value = sum(float(o.filled_qty or 0) * float(o.filled_avg_price or 0) for o in sell_orders) | |
| pl_dollars = sold_value - investment | |
| pl_percent = (pl_dollars / investment * 100) if investment > 0 else 0 | |
| # Format P&L with arrows and colors | |
| if pl_dollars > 0: | |
| pl_arrow = "<span style='color: #00d647; font-size: 1.4em;'>▲</span>" | |
| pl_color = "#00d647" | |
| row_bg = "rgba(0, 214, 71, 0.1)" | |
| elif pl_dollars < 0: | |
| pl_arrow = "<span style='color: #ff0080; font-size: 1.4em;'>▼</span>" | |
| pl_color = "#ff0080" | |
| row_bg = "rgba(255, 0, 128, 0.1)" | |
| else: | |
| pl_arrow = "" | |
| pl_color = "#8b949e" | |
| row_bg = "rgba(139, 148, 158, 0.05)" | |
| # Format P&L values with styled arrows | |
| pl_dollar_str = f"{pl_arrow} <span style='color: {pl_color}; font-weight: 600;'>${abs(pl_dollars):.2f}</span>" | |
| pl_percent_str = f"{pl_arrow} <span style='color: {pl_color}; font-weight: 600;'>{abs(pl_percent):.2f}%</span>" | |
| # ADD SENTIMENT ANALYSIS FOR EACH STOCK | |
| logger.info(f"🧠 Starting sentiment analysis for {symbol}...") | |
| start_time = time.time() | |
| try: | |
| # Get pre-investment news (quick version) | |
| logger.info(f"📰 Gathering pre-investment news for {symbol}...") | |
| news_items = get_pre_investment_news(symbol, investment_time, hours_before=12) | |
| logger.info(f"📑 Found {len(news_items)} total news items for {symbol}") | |
| # Analyze sentiment | |
| logger.info(f"🔍 Analyzing sentiment for {symbol}...") | |
| avg_sentiment, predicted_change, prediction_label, source_breakdown = analyze_pre_investment_sentiment(news_items) | |
| analysis_time = time.time() - start_time | |
| logger.info(f"⚡ Sentiment analysis for {symbol} completed in {analysis_time:.1f}s") | |
| # Format sentiment display | |
| if prediction_label == "bullish": | |
| sentiment_display = f"<span style='color: #00d647; font-weight: 600;'>🚀 {prediction_label.title()}</span>" | |
| elif prediction_label == "bearish": | |
| sentiment_display = f"<span style='color: #ff0080; font-weight: 600;'>📉 {prediction_label.title()}</span>" | |
| else: | |
| sentiment_display = f"<span style='color: #8b949e; font-weight: 600;'>😐 {prediction_label.title()}</span>" | |
| # Format prediction | |
| if predicted_change > 0: | |
| predicted_display = f"<span style='color: #00d647; font-weight: 600;'>+{predicted_change:.1f}%</span>" | |
| elif predicted_change < 0: | |
| predicted_display = f"<span style='color: #ff0080; font-weight: 600;'>{predicted_change:.1f}%</span>" | |
| else: | |
| predicted_display = f"<span style='color: #8b949e; font-weight: 600;'>{predicted_change:.1f}%</span>" | |
| reddit_count = len(source_breakdown.get('Reddit', [])) | |
| news_count = len(source_breakdown.get('Google News', [])) | |
| logger.info(f"🎯 {symbol} RESULTS: {prediction_label.upper()} ({predicted_change:+.1f}%) | Reddit: {reddit_count} posts | News: {news_count} articles") | |
| # Log sample titles for debugging | |
| if reddit_count > 0: | |
| sample_reddit = source_breakdown['Reddit'][0]['title'][:50] | |
| logger.info(f"📱 Sample Reddit: {sample_reddit}...") | |
| if news_count > 0: | |
| sample_news = source_breakdown['Google News'][0]['title'][:50] | |
| logger.info(f"📰 Sample News: {sample_news}...") | |
| except Exception as e: | |
| analysis_time = time.time() - start_time | |
| logger.error(f"❌ Sentiment analysis failed for {symbol} after {analysis_time:.1f}s: {str(e)}") | |
| logger.error(f"🔍 Error type: {type(e).__name__}") | |
| import traceback | |
| logger.error(f"📋 Traceback: {traceback.format_exc()[:200]}...") | |
| sentiment_display = "<span style='color: #8b949e;'>❓ Error</span>" | |
| predicted_display = "<span style='color: #8b949e;'>N/A</span>" | |
| # Continue with next stock instead of failing completely | |
| pass | |
| invested_data.append({ | |
| 'Symbol': symbol, | |
| 'Status': status, | |
| 'IPO Price': f"${ipo_price:.2f}" if ipo_price > 0 else 'N/A', | |
| 'Buy Price': f"${avg_buy_price:.2f}", | |
| 'Sell Price': f"${avg_sell_price:.2f}" if avg_sell_price > 0 else 'N/A', | |
| 'Investment': f"${investment:.2f}", | |
| 'P&L ($)': pl_dollar_str, | |
| 'P&L (%)': pl_percent_str, | |
| 'Sentiment': sentiment_display, | |
| 'Predicted': predicted_display, | |
| 'Date': first_buy_date, | |
| '_row_bg': row_bg, # Store background color for styling | |
| '_sort_date': first_buy_order.filled_at # Store datetime for sorting | |
| }) | |
| # Sort by date (most recent first) | |
| invested_data.sort(key=lambda x: x['_sort_date'], reverse=True) | |
| logger.info(f"📋 Processed {len(invested_data)} investments with sentiment analysis") | |
| df = pd.DataFrame(invested_data) | |
| logger.info(f"✅ Investment performance table refresh completed - {len(df)} rows") | |
| return df | |
| def refresh_investment_performance_html(): | |
| """Return styled HTML table for investment performance""" | |
| df = refresh_investment_performance_table() | |
| if df.empty: | |
| return "<div style='text-align: center; padding: 2rem; color: #666;'>No trading data available</div>" | |
| # Build HTML table | |
| html = '<table class="investment-table">' | |
| # Header | |
| html += '<thead><tr>' | |
| for col in df.columns: | |
| if not col.startswith('_'): # Skip internal columns | |
| html += f'<th>{col}</th>' | |
| html += '</tr></thead>' | |
| # Body | |
| html += '<tbody>' | |
| for _, row in df.iterrows(): | |
| # Determine row class based on P&L | |
| row_class = "" | |
| pl_str = str(row.get('P&L ($)', '')) | |
| if '▲' in pl_str: | |
| row_class = "profit-row" | |
| elif '▼' in pl_str: | |
| row_class = "loss-row" | |
| else: | |
| row_class = "neutral-row" | |
| html += f'<tr class="{row_class}">' | |
| for col in df.columns: | |
| if not col.startswith('_'): # Skip internal columns | |
| html += f'<td>{row[col]}</td>' | |
| html += '</tr>' | |
| html += '</tbody></table>' | |
| return html | |
| def refresh_vm_stats(): | |
| """Refresh VM statistics""" | |
| stats = fetch_from_vm('stats', {}) | |
| if not stats: | |
| return "0", "0", "0", "0%", "No data" | |
| return ( | |
| str(stats.get('total_ipos_detected', 0)), | |
| str(stats.get('ipos_invested', 0)), | |
| str(stats.get('cs_stocks_detected', 0)), | |
| f"{stats.get('investment_rate', 0):.1f}%", | |
| stats.get('last_updated', 'N/A') | |
| ) | |
| def refresh_system_logs(): | |
| """Refresh system logs from VM""" | |
| logs = fetch_from_vm('logs', []) | |
| if not logs: | |
| return "No logs available from VM" | |
| # Format logs for display | |
| formatted_logs = [] | |
| for log in logs: | |
| emoji = log.get('emoji', '⚪') | |
| timestamp = log.get('timestamp', 'N/A') | |
| message = log.get('message', '') | |
| formatted_logs.append(f"{emoji} {timestamp} | {message}") | |
| return '\n'.join(formatted_logs) | |
| def refresh_raw_logs(): | |
| """Refresh raw logs from VM""" | |
| raw_data = fetch_from_vm('logs/raw?lines=1000', {}) | |
| if not raw_data: | |
| return "No raw logs available from VM" | |
| content = raw_data.get('content', 'No content') | |
| total_lines = raw_data.get('total_lines', 0) | |
| showing_lines = raw_data.get('showing_lines', 0) | |
| header = f"=== RAW CRON LOGS ===\nShowing last {showing_lines} of {total_lines} total lines\n\n" | |
| return header + content | |
| def run_vm_command(command, current_output="", command_history=""): | |
| """Execute command on VM and return output""" | |
| try: | |
| if not command.strip(): | |
| return current_output, "", command_history | |
| # Add command to history | |
| history_list = command_history.split("|||") if command_history else [] | |
| if command not in history_list: | |
| history_list.append(command) | |
| # Keep last 50 commands | |
| history_list = history_list[-50:] | |
| new_history = "|||".join(history_list) | |
| response = requests.post(f"{VM_API_URL}/api/execute", | |
| json={"command": command}, | |
| timeout=10) | |
| if response.status_code == 200: | |
| data = response.json() | |
| output = data.get('output', '') | |
| exit_code = data.get('exit_code', 0) | |
| # Add color coding for common patterns | |
| colored_output = colorize_output(output) | |
| # Format terminal-style output with clean spacing | |
| # Clean up output to avoid weird quote formatting | |
| clean_output = colored_output.strip().replace('\r', '') | |
| new_line = f"$ {command}\n{clean_output}" | |
| if exit_code != 0: | |
| new_line += f"\n[Exit code: {exit_code}]" | |
| new_line += "\n$ " | |
| # RADICAL FIX: Put newest content at TOP instead of bottom! | |
| if current_output.strip(): | |
| full_output = new_line + "\n" + current_output.rstrip() | |
| else: | |
| full_output = new_line | |
| return full_output, "", new_history | |
| else: | |
| error_line = f"\n$ {command}\nError: VM API returned {response.status_code}\n$ " | |
| return current_output + error_line, "", new_history | |
| except Exception as e: | |
| error_line = f"\n$ {command}\nError: {str(e)}\n$ " | |
| return current_output + error_line, "", new_history | |
| def colorize_output(output): | |
| """Add basic color coding to terminal output""" | |
| import re | |
| # Color patterns (using ANSI-like styling for web) | |
| colored = output | |
| # File permissions and directories (ls output) | |
| colored = re.sub(r'^(d)([rwx-]{9})', r'<span style="color: #4A90E2;">\1\2</span>', colored, flags=re.MULTILINE) | |
| colored = re.sub(r'^(-)([rwx-]{9})', r'<span style="color: #50E3C2;">\1\2</span>', colored, flags=re.MULTILINE) | |
| # Error messages | |
| colored = re.sub(r'(ERROR|Error|error)', r'<span style="color: #FF6B6B;">\1</span>', colored) | |
| colored = re.sub(r'(WARNING|Warning|warning)', r'<span style="color: #FFD93D;">\1</span>', colored) | |
| # Success indicators | |
| colored = re.sub(r'(SUCCESS|Success|success)', r'<span style="color: #6BCF7F;">\1</span>', colored) | |
| # File extensions | |
| colored = re.sub(r'(\w+\.(py|log|csv|json|txt))', r'<span style="color: #BD93F9;">\1</span>', colored) | |
| # Numbers and timestamps | |
| colored = re.sub(r'(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})', r'<span style="color: #50FA7B;">\1</span>', colored) | |
| return colored | |
| def debug_order_history(): | |
| """Debug function to show raw order history data""" | |
| try: | |
| # Try multiple approaches to get orders | |
| debug_info = f"=== ORDER HISTORY DEBUG ===\n" | |
| # Approach 1: All orders, last 6 months (DEPRECATED - doesn't work) | |
| try: | |
| end_date = datetime.now(timezone.utc) | |
| start_date = end_date - timedelta(days=180) | |
| old_request = GetOrdersRequest(limit=500, after=start_date, until=end_date) | |
| old_orders = trading_client.get_orders(old_request) | |
| debug_info += f"Method 1 (6 months, all statuses): {len(old_orders)} orders [DEPRECATED]\n" | |
| except Exception as e: | |
| debug_info += f"Method 1 failed: {str(e)}\n" | |
| # Approach 1B: Current working method used by Investment Performance | |
| orders = get_order_history() | |
| debug_info += f"Method 1B (PRIMARY - 1 year, CLOSED): {len(orders)} orders [CURRENTLY USED]\n" | |
| # Approach 2: Just filled orders, last year | |
| try: | |
| end_date = datetime.now(timezone.utc) | |
| start_date = end_date - timedelta(days=365) | |
| filled_request = GetOrdersRequest( | |
| status="closed", # Use string instead of enum | |
| limit=500, | |
| after=start_date, | |
| until=end_date | |
| ) | |
| filled_orders = trading_client.get_orders(filled_request) | |
| debug_info += f"Method 2 (1 year, CLOSED orders): {len(filled_orders)} orders\n" | |
| except Exception as e: | |
| debug_info += f"Method 2 failed: {str(e)}\n" | |
| # Approach 3: No date filter, just get recent orders | |
| try: | |
| recent_request = GetOrdersRequest(limit=100) | |
| recent_orders = trading_client.get_orders(recent_request) | |
| debug_info += f"Method 3 (recent 100, no date filter): {len(recent_orders)} orders\n" | |
| except Exception as e: | |
| debug_info += f"Method 3 failed: {str(e)}\n" | |
| debug_info += "\n" | |
| # Show any orders we found | |
| all_orders = orders if orders else (filled_orders if 'filled_orders' in locals() else (recent_orders if 'recent_orders' in locals() else [])) | |
| if all_orders: | |
| debug_info += f"Sample orders (showing first 10):\n" | |
| for i, order in enumerate(all_orders[:10]): | |
| debug_info += f"{i+1}. Symbol: {order.symbol}, Side: {order.side}, " | |
| debug_info += f"Qty: {order.filled_qty}, Price: {order.filled_avg_price}, " | |
| debug_info += f"Status: {order.status}, Time: {order.filled_at}, " | |
| debug_info += f"Created: {order.created_at}\n" | |
| else: | |
| debug_info += "❌ NO ORDERS FOUND WITH ANY METHOD!\n" | |
| debug_info += "\nLet's check account details:\n" | |
| # Check account info | |
| try: | |
| account = trading_client.get_account() | |
| debug_info += f"Account ID: {account.account_number}\n" | |
| debug_info += f"Account Status: {account.status}\n" | |
| debug_info += f"Trading Blocked: {account.trading_blocked}\n" | |
| debug_info += f"Pattern Day Trader: {account.pattern_day_trader}\n" | |
| debug_info += f"Cash: ${float(account.cash):,.2f}\n" | |
| debug_info += f"Portfolio Value: ${float(account.portfolio_value):,.2f}\n" | |
| # Check if this is paper trading | |
| debug_info += f"\nAPI Keys being used:\n" | |
| debug_info += f"API Key: {API_KEY[:8]}...{API_KEY[-4:]}\n" | |
| if "PK" in API_KEY: | |
| debug_info += "🟢 This appears to be PAPER TRADING (PK prefix)\n" | |
| elif "AK" in API_KEY: | |
| debug_info += "🔴 This appears to be LIVE TRADING (AK prefix)\n" | |
| else: | |
| debug_info += "❓ Unknown API key type\n" | |
| except Exception as e: | |
| debug_info += f"❌ Error getting account info: {str(e)}\n" | |
| debug_info += "\nPossible issues:\n" | |
| debug_info += "- No actual trading activity on this account\n" | |
| debug_info += "- Using paper trading account (no real orders)\n" | |
| debug_info += "- Orders are older than 1 year\n" | |
| debug_info += "- API key permissions issue\n" | |
| debug_info += "- Different Alpaca account than expected\n" | |
| return debug_info | |
| except Exception as e: | |
| return f"ERROR getting order history: {str(e)}" | |
| def debug_current_positions(): | |
| """Debug function to show current positions""" | |
| try: | |
| positions = get_current_positions() | |
| debug_info = f"=== CURRENT POSITIONS DEBUG ===\n" | |
| debug_info += f"Total positions: {len(positions)}\n\n" | |
| for pos in positions: | |
| debug_info += f"Symbol: {pos['symbol']}, Qty: {pos['qty']}, " | |
| debug_info += f"Market Value: ${pos['market_value']:.2f}, " | |
| debug_info += f"P&L: ${pos['unrealized_pl']:.2f}\n" | |
| return debug_info | |
| except Exception as e: | |
| return f"ERROR getting positions: {str(e)}" | |
| def debug_ipo_data(): | |
| """Debug function to show IPO data from VM""" | |
| try: | |
| ipos = fetch_from_vm('ipos?limit=20', []) | |
| debug_info = f"=== IPO DATA DEBUG ===\n" | |
| debug_info += f"Total IPOs: {len(ipos)}\n\n" | |
| invested_count = 0 | |
| for ipo in ipos: | |
| status = ipo.get('investment_status', 'UNKNOWN') | |
| if status == 'INVESTED': | |
| invested_count += 1 | |
| debug_info += f"INVESTED: {ipo.get('symbol')} - Price: ${ipo.get('trading_price')}\n" | |
| debug_info += f"\nTotal INVESTED IPOs: {invested_count}\n" | |
| return debug_info | |
| except Exception as e: | |
| return f"ERROR getting IPO data: {str(e)}" | |
| def debug_account_info(): | |
| """Debug function to show account info""" | |
| try: | |
| account = get_account_info() | |
| debug_info = f"=== ACCOUNT INFO DEBUG ===\n" | |
| for key, value in account.items(): | |
| debug_info += f"{key}: {value}\n" | |
| return debug_info | |
| except Exception as e: | |
| return f"ERROR getting account info: {str(e)}" | |
| def calculate_sequential_reinvestment(): | |
| """Calculate P&L% if reinvesting same amount sequentially in each stock""" | |
| try: | |
| orders = get_order_history() | |
| if not orders: | |
| return "No order data available for calculation" | |
| # Get unique symbols with their first buy date | |
| symbols_by_date = {} | |
| for order in orders: | |
| if order.side.value == 'buy' and order.status.value == 'filled': | |
| symbol = order.symbol | |
| fill_date = order.filled_at | |
| if symbol not in symbols_by_date or fill_date < symbols_by_date[symbol]: | |
| symbols_by_date[symbol] = fill_date | |
| # Sort by first buy date | |
| sorted_symbols = sorted(symbols_by_date.items(), key=lambda x: x[1]) | |
| # Calculate sequential reinvestment returns | |
| initial_investment = 1000 # Start with $1000 | |
| current_value = initial_investment | |
| results = [] | |
| total_return = 0 | |
| for symbol, first_date in sorted_symbols: | |
| # Get all orders for this symbol | |
| symbol_orders = [o for o in orders if o.symbol == symbol] | |
| buy_orders = [o for o in symbol_orders if o.side.value == 'buy'] | |
| sell_orders = [o for o in symbol_orders if o.side.value == 'sell'] | |
| if buy_orders: | |
| # Calculate actual P&L for this symbol | |
| total_bought = sum(float(o.filled_qty or 0) for o in buy_orders) | |
| total_cost = sum(float(o.filled_qty or 0) * float(o.filled_avg_price or 0) for o in buy_orders) | |
| if sell_orders: | |
| # Sold - use actual sell proceeds | |
| sold_value = sum(float(o.filled_qty or 0) * float(o.filled_avg_price or 0) for o in sell_orders) | |
| pl_percent = ((sold_value - total_cost) / total_cost) if total_cost > 0 else 0 | |
| else: | |
| # Still holding - estimate current value | |
| positions = get_current_positions() | |
| pos = next((p for p in positions if p['symbol'] == symbol), None) | |
| if pos: | |
| current_price = pos['current_price'] | |
| current_symbol_value = total_bought * current_price | |
| pl_percent = ((current_symbol_value - total_cost) / total_cost) if total_cost > 0 else 0 | |
| else: | |
| pl_percent = 0 | |
| # Apply return to current value | |
| new_value = current_value * (1 + pl_percent) | |
| gain_loss = new_value - current_value | |
| results.append(f"{symbol}: {pl_percent*100:+.2f}% | ${current_value:.2f} → ${new_value:.2f} ({gain_loss:+.2f})") | |
| current_value = new_value | |
| total_return += pl_percent | |
| final_return_pct = ((current_value - initial_investment) / initial_investment) * 100 | |
| output = f"🧮 SEQUENTIAL REINVESTMENT ANALYSIS\n" | |
| output += f"Starting Investment: ${initial_investment:.2f}\n" | |
| output += f"Final Value: ${current_value:.2f}\n" | |
| output += f"Total Return: {final_return_pct:+.2f}%\n" | |
| output += f"Number of Trades: {len(sorted_symbols)}\n\n" | |
| output += "Trade Sequence:\n" | |
| output += "\n".join(results) | |
| return output | |
| except Exception as e: | |
| return f"ERROR calculating sequential reinvestment: {str(e)}" | |
| def calculate_equal_weight_portfolio(): | |
| """Calculate P&L% if investing equal amounts in all stocks simultaneously""" | |
| try: | |
| orders = get_order_history() | |
| if not orders: | |
| return "No order data available for calculation" | |
| # Get unique symbols | |
| symbols = set() | |
| for order in orders: | |
| if order.side.value == 'buy': | |
| symbols.add(order.symbol) | |
| total_pl = 0 | |
| valid_symbols = 0 | |
| results = [] | |
| for symbol in sorted(symbols): | |
| symbol_orders = [o for o in orders if o.symbol == symbol] | |
| buy_orders = [o for o in symbol_orders if o.side.value == 'buy'] | |
| sell_orders = [o for o in symbol_orders if o.side.value == 'sell'] | |
| if buy_orders: | |
| total_bought = sum(float(o.filled_qty or 0) for o in buy_orders) | |
| total_cost = sum(float(o.filled_qty or 0) * float(o.filled_avg_price or 0) for o in buy_orders) | |
| avg_buy_price = total_cost / total_bought if total_bought > 0 else 0 | |
| if sell_orders: | |
| # Sold | |
| sold_value = sum(float(o.filled_qty or 0) * float(o.filled_avg_price or 0) for o in sell_orders) | |
| pl_percent = ((sold_value - total_cost) / total_cost) if total_cost > 0 else 0 | |
| status = "SOLD" | |
| else: | |
| # Still holding | |
| positions = get_current_positions() | |
| pos = next((p for p in positions if p['symbol'] == symbol), None) | |
| if pos: | |
| current_price = pos['current_price'] | |
| current_value = total_bought * current_price | |
| pl_percent = ((current_value - total_cost) / total_cost) if total_cost > 0 else 0 | |
| status = "HOLDING" | |
| else: | |
| pl_percent = 0 | |
| status = "UNKNOWN" | |
| total_pl += pl_percent | |
| valid_symbols += 1 | |
| results.append(f"{symbol}: {pl_percent*100:+.2f}% ({status})") | |
| avg_return = (total_pl / valid_symbols) * 100 if valid_symbols > 0 else 0 | |
| output = f"⚖️ EQUAL WEIGHT PORTFOLIO ANALYSIS\n" | |
| output += f"Total Symbols: {valid_symbols}\n" | |
| output += f"Average Return per Symbol: {avg_return:+.2f}%\n" | |
| output += f"Portfolio Return (equal weights): {avg_return:+.2f}%\n\n" | |
| output += "Individual Returns:\n" | |
| output += "\n".join(results) | |
| return output | |
| except Exception as e: | |
| return f"ERROR calculating equal weight portfolio: {str(e)}" | |
| def calculate_best_worst_performers(): | |
| """Find best and worst performing stocks""" | |
| try: | |
| orders = get_order_history() | |
| if not orders: | |
| return "No order data available for calculation" | |
| symbols = set() | |
| for order in orders: | |
| if order.side.value == 'buy': | |
| symbols.add(order.symbol) | |
| performance = [] | |
| for symbol in symbols: | |
| symbol_orders = [o for o in orders if o.symbol == symbol] | |
| buy_orders = [o for o in symbol_orders if o.side.value == 'buy'] | |
| sell_orders = [o for o in symbol_orders if o.side.value == 'sell'] | |
| if buy_orders: | |
| total_bought = sum(float(o.filled_qty or 0) for o in buy_orders) | |
| total_cost = sum(float(o.filled_qty or 0) * float(o.filled_avg_price or 0) for o in buy_orders) | |
| if sell_orders: | |
| sold_value = sum(float(o.filled_qty or 0) * float(o.filled_avg_price or 0) for o in sell_orders) | |
| pl_percent = ((sold_value - total_cost) / total_cost) if total_cost > 0 else 0 | |
| pl_dollars = sold_value - total_cost | |
| status = "SOLD" | |
| else: | |
| positions = get_current_positions() | |
| pos = next((p for p in positions if p['symbol'] == symbol), None) | |
| if pos: | |
| current_price = pos['current_price'] | |
| current_value = total_bought * current_price | |
| pl_percent = ((current_value - total_cost) / total_cost) if total_cost > 0 else 0 | |
| pl_dollars = current_value - total_cost | |
| status = "HOLDING" | |
| else: | |
| pl_percent = 0 | |
| pl_dollars = 0 | |
| status = "UNKNOWN" | |
| performance.append({ | |
| 'symbol': symbol, | |
| 'pl_percent': pl_percent, | |
| 'pl_dollars': pl_dollars, | |
| 'investment': total_cost, | |
| 'status': status | |
| }) | |
| # Sort by percentage return | |
| performance.sort(key=lambda x: x['pl_percent'], reverse=True) | |
| output = f"🏆 BEST vs WORST PERFORMERS\n\n" | |
| if performance: | |
| output += "🥇 TOP 5 PERFORMERS:\n" | |
| for i, perf in enumerate(performance[:5]): | |
| output += f"{i+1}. {perf['symbol']}: {perf['pl_percent']*100:+.2f}% (${perf['pl_dollars']:+.2f}) - {perf['status']}\n" | |
| output += "\n🥉 BOTTOM 5 PERFORMERS:\n" | |
| for i, perf in enumerate(performance[-5:]): | |
| rank = len(performance) - 4 + i | |
| output += f"{rank}. {perf['symbol']}: {perf['pl_percent']*100:+.2f}% (${perf['pl_dollars']:+.2f}) - {perf['status']}\n" | |
| # Calculate some stats | |
| total_winners = len([p for p in performance if p['pl_percent'] > 0]) | |
| total_losers = len([p for p in performance if p['pl_percent'] < 0]) | |
| output += f"\n📊 SUMMARY:\n" | |
| output += f"Winners: {total_winners}/{len(performance)} ({total_winners/len(performance)*100:.1f}%)\n" | |
| output += f"Losers: {total_losers}/{len(performance)} ({total_losers/len(performance)*100:.1f}%)\n" | |
| return output | |
| except Exception as e: | |
| return f"ERROR calculating best/worst performers: {str(e)}" | |
| def calculate_win_rate_metrics(): | |
| """Calculate win rate and average returns""" | |
| try: | |
| orders = get_order_history() | |
| if not orders: | |
| return "No order data available for calculation" | |
| symbols = set() | |
| for order in orders: | |
| if order.side.value == 'buy': | |
| symbols.add(order.symbol) | |
| performance = [] | |
| total_investment = 0 | |
| for symbol in symbols: | |
| symbol_orders = [o for o in orders if o.symbol == symbol] | |
| buy_orders = [o for o in symbol_orders if o.side.value == 'buy'] | |
| sell_orders = [o for o in symbol_orders if o.side.value == 'sell'] | |
| if buy_orders: | |
| total_bought = sum(float(o.filled_qty or 0) for o in buy_orders) | |
| total_cost = sum(float(o.filled_qty or 0) * float(o.filled_avg_price or 0) for o in buy_orders) | |
| total_investment += total_cost | |
| if sell_orders: | |
| sold_value = sum(float(o.filled_qty or 0) * float(o.filled_avg_price or 0) for o in sell_orders) | |
| pl_percent = ((sold_value - total_cost) / total_cost) if total_cost > 0 else 0 | |
| pl_dollars = sold_value - total_cost | |
| else: | |
| positions = get_current_positions() | |
| pos = next((p for p in positions if p['symbol'] == symbol), None) | |
| if pos: | |
| current_price = pos['current_price'] | |
| current_value = total_bought * current_price | |
| pl_percent = ((current_value - total_cost) / total_cost) if total_cost > 0 else 0 | |
| pl_dollars = current_value - total_cost | |
| else: | |
| pl_percent = 0 | |
| pl_dollars = 0 | |
| performance.append({ | |
| 'symbol': symbol, | |
| 'pl_percent': pl_percent, | |
| 'pl_dollars': pl_dollars, | |
| 'investment': total_cost | |
| }) | |
| if not performance: | |
| return "No performance data available" | |
| # Calculate metrics | |
| winners = [p for p in performance if p['pl_percent'] > 0] | |
| losers = [p for p in performance if p['pl_percent'] < 0] | |
| breakeven = [p for p in performance if p['pl_percent'] == 0] | |
| win_rate = len(winners) / len(performance) * 100 | |
| avg_win = sum(p['pl_percent'] for p in winners) / len(winners) * 100 if winners else 0 | |
| avg_loss = sum(p['pl_percent'] for p in losers) / len(losers) * 100 if losers else 0 | |
| total_pl_dollars = sum(p['pl_dollars'] for p in performance) | |
| total_pl_percent = (total_pl_dollars / total_investment) * 100 if total_investment > 0 else 0 | |
| # Risk/Reward ratio | |
| risk_reward = abs(avg_win / avg_loss) if avg_loss != 0 else float('inf') | |
| output = f"🎯 WIN RATE & AVERAGE RETURNS\n\n" | |
| output += f"Total Trades: {len(performance)}\n" | |
| output += f"Win Rate: {win_rate:.1f}% ({len(winners)} winners)\n" | |
| output += f"Loss Rate: {len(losers)/len(performance)*100:.1f}% ({len(losers)} losers)\n" | |
| output += f"Breakeven: {len(breakeven)} trades\n\n" | |
| output += f"📈 AVERAGE PERFORMANCE:\n" | |
| output += f"Average Winner: +{avg_win:.2f}%\n" | |
| output += f"Average Loser: {avg_loss:.2f}%\n" | |
| output += f"Risk/Reward Ratio: {risk_reward:.2f}:1\n\n" | |
| output += f"💰 TOTAL PERFORMANCE:\n" | |
| output += f"Total Invested: ${total_investment:.2f}\n" | |
| output += f"Total P&L: ${total_pl_dollars:+.2f}\n" | |
| output += f"Total Return: {total_pl_percent:+.2f}%\n" | |
| return output | |
| except Exception as e: | |
| return f"ERROR calculating win rate metrics: {str(e)}" | |
| def calculate_risk_metrics(): | |
| """Calculate risk metrics and volatility""" | |
| try: | |
| orders = get_order_history() | |
| if not orders: | |
| return "No order data available for calculation" | |
| symbols = set() | |
| for order in orders: | |
| if order.side.value == 'buy': | |
| symbols.add(order.symbol) | |
| returns = [] | |
| investments = [] | |
| for symbol in symbols: | |
| symbol_orders = [o for o in orders if o.symbol == symbol] | |
| buy_orders = [o for o in symbol_orders if o.side.value == 'buy'] | |
| sell_orders = [o for o in symbol_orders if o.side.value == 'sell'] | |
| if buy_orders: | |
| total_bought = sum(float(o.filled_qty or 0) for o in buy_orders) | |
| total_cost = sum(float(o.filled_qty or 0) * float(o.filled_avg_price or 0) for o in buy_orders) | |
| investments.append(total_cost) | |
| if sell_orders: | |
| sold_value = sum(float(o.filled_qty or 0) * float(o.filled_avg_price or 0) for o in sell_orders) | |
| pl_percent = ((sold_value - total_cost) / total_cost) if total_cost > 0 else 0 | |
| else: | |
| positions = get_current_positions() | |
| pos = next((p for p in positions if p['symbol'] == symbol), None) | |
| if pos: | |
| current_price = pos['current_price'] | |
| current_value = total_bought * current_price | |
| pl_percent = ((current_value - total_cost) / total_cost) if total_cost > 0 else 0 | |
| else: | |
| pl_percent = 0 | |
| returns.append(pl_percent) | |
| if not returns: | |
| return "No return data available" | |
| # Calculate statistics | |
| import statistics | |
| avg_return = statistics.mean(returns) * 100 | |
| median_return = statistics.median(returns) * 100 | |
| volatility = statistics.stdev(returns) * 100 if len(returns) > 1 else 0 | |
| # Sharpe-like ratio (assuming risk-free rate = 0) | |
| sharpe = avg_return / volatility if volatility > 0 else 0 | |
| # Max drawdown | |
| max_return = max(returns) * 100 | |
| min_return = min(returns) * 100 | |
| max_drawdown = max_return - min_return | |
| # Portfolio concentration | |
| total_investment = sum(investments) | |
| avg_position_size = statistics.mean(investments) | |
| largest_position = max(investments) | |
| concentration = (largest_position / total_investment) * 100 if total_investment > 0 else 0 | |
| output = f"⚠️ RISK METRICS & VOLATILITY\n\n" | |
| output += f"📊 RETURN STATISTICS:\n" | |
| output += f"Average Return: {avg_return:+.2f}%\n" | |
| output += f"Median Return: {median_return:+.2f}%\n" | |
| output += f"Volatility (StdDev): {volatility:.2f}%\n" | |
| output += f"Sharpe-like Ratio: {sharpe:.2f}\n\n" | |
| output += f"📉 RISK MEASURES:\n" | |
| output += f"Best Trade: +{max_return:.2f}%\n" | |
| output += f"Worst Trade: {min_return:.2f}%\n" | |
| output += f"Max Range: {max_drawdown:.2f}%\n\n" | |
| output += f"🎯 POSITION SIZING:\n" | |
| output += f"Average Position: ${avg_position_size:.2f}\n" | |
| output += f"Largest Position: ${largest_position:.2f}\n" | |
| output += f"Concentration Risk: {concentration:.1f}% in largest\n" | |
| return output | |
| except Exception as e: | |
| return f"ERROR calculating risk metrics: {str(e)}" | |
| def calculate_time_analysis(): | |
| """Analyze performance by time periods""" | |
| try: | |
| orders = get_order_history() | |
| if not orders: | |
| return "No order data available for calculation" | |
| from datetime import datetime, timezone | |
| # Group orders by month | |
| monthly_performance = {} | |
| for order in orders: | |
| if order.side.value == 'buy' and order.status.value == 'filled': | |
| month_key = order.filled_at.strftime('%Y-%m') | |
| if month_key not in monthly_performance: | |
| monthly_performance[month_key] = {'symbols': set(), 'investment': 0, 'returns': []} | |
| symbol = order.symbol | |
| monthly_performance[month_key]['symbols'].add(symbol) | |
| # Calculate returns for each month | |
| symbols = set() | |
| for order in orders: | |
| if order.side.value == 'buy': | |
| symbols.add(order.symbol) | |
| symbol_performance = {} | |
| for symbol in symbols: | |
| symbol_orders = [o for o in orders if o.symbol == symbol] | |
| buy_orders = [o for o in symbol_orders if o.side.value == 'buy'] | |
| sell_orders = [o for o in symbol_orders if o.side.value == 'sell'] | |
| if buy_orders: | |
| first_buy = min(buy_orders, key=lambda x: x.filled_at) | |
| month_key = first_buy.filled_at.strftime('%Y-%m') | |
| total_bought = sum(float(o.filled_qty or 0) for o in buy_orders) | |
| total_cost = sum(float(o.filled_qty or 0) * float(o.filled_avg_price or 0) for o in buy_orders) | |
| if sell_orders: | |
| sold_value = sum(float(o.filled_qty or 0) * float(o.filled_avg_price or 0) for o in sell_orders) | |
| pl_percent = ((sold_value - total_cost) / total_cost) if total_cost > 0 else 0 | |
| else: | |
| positions = get_current_positions() | |
| pos = next((p for p in positions if p['symbol'] == symbol), None) | |
| if pos: | |
| current_price = pos['current_price'] | |
| current_value = total_bought * current_price | |
| pl_percent = ((current_value - total_cost) / total_cost) if total_cost > 0 else 0 | |
| else: | |
| pl_percent = 0 | |
| if month_key in monthly_performance: | |
| monthly_performance[month_key]['investment'] += total_cost | |
| monthly_performance[month_key]['returns'].append(pl_percent) | |
| output = f"⏰ TIME-BASED PERFORMANCE ANALYSIS\n\n" | |
| for month in sorted(monthly_performance.keys()): | |
| data = monthly_performance[month] | |
| if data['returns']: | |
| avg_return = sum(data['returns']) / len(data['returns']) * 100 | |
| total_investment = data['investment'] | |
| num_trades = len(data['returns']) | |
| output += f"📅 {month}: {avg_return:+.2f}% avg return\n" | |
| output += f" • {num_trades} trades, ${total_investment:.2f} invested\n" | |
| # Calculate recent vs early performance | |
| sorted_months = sorted(monthly_performance.keys()) | |
| if len(sorted_months) >= 2: | |
| early_months = sorted_months[:len(sorted_months)//2] | |
| recent_months = sorted_months[len(sorted_months)//2:] | |
| early_returns = [] | |
| recent_returns = [] | |
| for month in early_months: | |
| early_returns.extend(monthly_performance[month]['returns']) | |
| for month in recent_months: | |
| recent_returns.extend(monthly_performance[month]['returns']) | |
| if early_returns and recent_returns: | |
| early_avg = sum(early_returns) / len(early_returns) * 100 | |
| recent_avg = sum(recent_returns) / len(recent_returns) * 100 | |
| output += f"\n📈 TREND ANALYSIS:\n" | |
| output += f"Early Period Avg: {early_avg:+.2f}% ({len(early_returns)} trades)\n" | |
| output += f"Recent Period Avg: {recent_avg:+.2f}% ({len(recent_returns)} trades)\n" | |
| output += f"Improvement: {recent_avg - early_avg:+.2f}% difference\n" | |
| return output | |
| except Exception as e: | |
| return f"ERROR calculating time analysis: {str(e)}" | |
| # Trading History Backtesting Functions | |
| def get_pre_investment_news(symbol, investment_time, hours_before=12): | |
| """Get news from 12 hours before we invested""" | |
| cutoff_time = investment_time - timedelta(minutes=30) # 30 min buffer | |
| search_start = investment_time - timedelta(hours=hours_before) | |
| logger.info(f"🔍 NEWS SEARCH for {symbol}:") | |
| logger.info(f" 📅 Time window: {search_start.strftime('%Y-%m-%d %H:%M')} → {cutoff_time.strftime('%Y-%m-%d %H:%M')}") | |
| logger.info(f" ⏰ Search duration: {hours_before} hours before investment") | |
| all_news = [] | |
| # Get Reddit posts | |
| logger.info(f"🧵 Starting Reddit search for {symbol}...") | |
| reddit_start = time.time() | |
| reddit_posts = get_reddit_pre_investment(symbol, search_start, cutoff_time) | |
| reddit_time = time.time() - reddit_start | |
| logger.info(f"✅ Reddit search completed in {reddit_time:.1f}s - found {len(reddit_posts)} posts") | |
| all_news.extend(reddit_posts) | |
| # Get Google News | |
| logger.info(f"📰 Starting Google News search for {symbol}...") | |
| news_start = time.time() | |
| google_news = get_google_news_pre_investment(symbol, search_start, cutoff_time) | |
| news_time = time.time() - news_start | |
| logger.info(f"✅ Google News search completed in {news_time:.1f}s - found {len(google_news)} articles") | |
| all_news.extend(google_news) | |
| logger.info(f"📊 TOTAL NEWS GATHERED for {symbol}: {len(all_news)} items ({len(reddit_posts)} Reddit + {len(google_news)} News)") | |
| return all_news | |
| def get_reddit_pre_investment(symbol, start_time, cutoff_time): | |
| """Get Reddit posts from before our investment""" | |
| reddit_posts = [] | |
| # Search key subreddits including WSB with multiple search strategies | |
| subreddits = ['wallstreetbets', 'stocks', 'investing'] | |
| search_terms = [symbol, f'{symbol} stock', f'{symbol} IPO', f'${symbol}'] | |
| 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': 5, # Reduced to avoid duplicates | |
| 't': 'all', # Search all time instead of just week | |
| '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 search: 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 | |
| # Check if we already have this post (avoid duplicates) | |
| title = post_data.get('title', '') | |
| if any(existing['title'] == title for existing in reddit_posts): | |
| continue | |
| # Only include posts that actually mention the symbol | |
| title_text = f"{title} {post_data.get('selftext', '')}".upper() | |
| if symbol.upper() in title_text or f'${symbol.upper()}' in title_text: | |
| reddit_post = { | |
| 'title': title, | |
| 'selftext': post_data.get('selftext', '')[:300], | |
| 'score': post_data.get('score', 0), | |
| 'num_comments': post_data.get('num_comments', 0), | |
| 'subreddit': subreddit, | |
| 'source': 'Reddit', | |
| 'url': f"https://reddit.com{post_data.get('permalink', '')}", | |
| 'search_term': search_term | |
| } | |
| reddit_posts.append(reddit_post) | |
| logger.info(f"Added Reddit post: {title[:50]}... (score: {post_data.get('score', 0)})") | |
| time.sleep(0.5) # Reduced rate limiting | |
| except Exception as e: | |
| logger.warning(f"Reddit error for r/{subreddit} + '{search_term}': {e}") | |
| logger.info(f"Total Reddit posts found for {symbol}: {len(reddit_posts)}") | |
| return reddit_posts | |
| def get_google_news_pre_investment(symbol, start_time, cutoff_time): | |
| """Get Google News from before our investment""" | |
| google_news = [] | |
| try: | |
| # Search for IPO-related news | |
| search_queries = [ | |
| f'{symbol} IPO', | |
| f'{symbol} stock', | |
| f'{symbol} public offering' | |
| ] | |
| for query in search_queries: | |
| url = "https://news.google.com/rss/search" | |
| params = { | |
| 'q': query, | |
| 'hl': 'en-US', | |
| 'gl': 'US', | |
| 'ceid': 'US:en' | |
| } | |
| response = requests.get(url, params=params, headers=headers, timeout=10) | |
| if response.status_code == 200: | |
| # Parse RSS | |
| from xml.etree import ElementTree as ET | |
| root = ET.fromstring(response.content) | |
| for item in root.findall('.//item')[:5]: # Limit per query | |
| title_elem = item.find('title') | |
| link_elem = item.find('link') | |
| description_elem = item.find('description') | |
| if title_elem is not None: | |
| description = description_elem.text if description_elem is not None else "" | |
| # Clean HTML | |
| import re | |
| description = re.sub(r'<[^>]+>', '', description) | |
| news_item = { | |
| 'title': title_elem.text, | |
| 'description': description, | |
| 'source': 'Google News', | |
| 'url': link_elem.text if link_elem is not None else '' | |
| } | |
| google_news.append(news_item) | |
| time.sleep(0.5) | |
| except Exception as e: | |
| logger.warning(f"Google News error: {e}") | |
| return google_news | |
| def analyze_pre_investment_sentiment(news_items): | |
| """Analyze sentiment from news before our investment""" | |
| if not news_items: | |
| return 0.0, 0.0, "neutral", {} | |
| sentiments = [] | |
| source_breakdown = {'Reddit': [], 'Google News': []} | |
| for item in news_items: | |
| # Combine title and description/selftext | |
| if item['source'] == 'Reddit': | |
| text = f"{item['title']} {item.get('selftext', '')}" | |
| else: | |
| text = f"{item['title']} {item.get('description', '')}" | |
| # Sentiment analysis | |
| vader_scores = vader.polarity_scores(text) | |
| blob = TextBlob(text) | |
| combined_sentiment = (vader_scores['compound'] * 0.6) + (blob.sentiment.polarity * 0.4) | |
| # Weight by engagement for Reddit | |
| if item['source'] == 'Reddit': | |
| engagement = item.get('score', 0) + item.get('num_comments', 0) | |
| weight = min(engagement / 100.0, 2.0) if engagement > 0 else 0.5 | |
| else: | |
| weight = 1.0 | |
| weighted_sentiment = combined_sentiment * weight | |
| sentiments.append(weighted_sentiment) | |
| # Track by source | |
| source_breakdown[item['source']].append({ | |
| 'sentiment': weighted_sentiment, | |
| 'title': item['title'][:80], | |
| 'weight': weight | |
| }) | |
| # Calculate overall metrics | |
| avg_sentiment = sum(sentiments) / len(sentiments) | |
| # Convert to predicted change | |
| predicted_change = avg_sentiment * 25.0 | |
| # Add confidence based on source agreement | |
| reddit_sentiments = [s['sentiment'] for s in source_breakdown['Reddit']] | |
| news_sentiments = [s['sentiment'] for s in source_breakdown['Google News']] | |
| reddit_avg = sum(reddit_sentiments) / len(reddit_sentiments) if reddit_sentiments else 0 | |
| news_avg = sum(news_sentiments) / len(news_sentiments) if news_sentiments else 0 | |
| # Boost prediction if sources agree | |
| if (reddit_avg > 0 and news_avg > 0) or (reddit_avg < 0 and news_avg < 0): | |
| predicted_change *= 1.2 | |
| # Classify prediction | |
| if predicted_change >= 5.0: | |
| prediction_label = "bullish" | |
| elif predicted_change <= -5.0: | |
| prediction_label = "bearish" | |
| else: | |
| prediction_label = "neutral" | |
| return avg_sentiment, predicted_change, prediction_label, source_breakdown | |
| def get_actual_performance(symbol, investment_time, investment_price): | |
| """Get actual stock performance after our investment""" | |
| try: | |
| ticker = yf.Ticker(symbol) | |
| # Get data from investment day | |
| start_date = investment_time.date() | |
| end_date = start_date + timedelta(days=5) # Get a few days | |
| hist = ticker.history(start=start_date, end=end_date, interval='1h') | |
| if hist.empty: | |
| return None, None, None | |
| # Find first hour performance (approximate) | |
| day_data = hist[hist.index.date == start_date] | |
| if len(day_data) > 0: | |
| first_price = day_data.iloc[0]['Open'] | |
| # First hour high (if we have hourly data) | |
| if len(day_data) >= 2: | |
| first_hour_high = day_data.iloc[0:2]['High'].max() | |
| first_hour_change = ((first_hour_high - first_price) / first_price) * 100 | |
| else: | |
| # Fall back to first day | |
| first_day_close = day_data.iloc[-1]['Close'] | |
| first_hour_change = ((first_day_close - first_price) / first_price) * 100 | |
| # End of day performance | |
| end_of_day_close = day_data.iloc[-1]['Close'] | |
| day_change = ((end_of_day_close - first_price) / first_price) * 100 | |
| return first_hour_change, day_change, first_price | |
| except Exception as e: | |
| logger.warning(f"Error getting {symbol} performance: {e}") | |
| return None, None, None | |
| def run_trading_history_backtest(): | |
| """Run backtest on all our actual investments""" | |
| logger.info("Starting trading history backtesting...") | |
| try: | |
| # Get our trading history | |
| orders = get_order_history() | |
| if not orders: | |
| return "❌ No trading history found", pd.DataFrame() | |
| # Get all unique symbols from order history | |
| symbols_traded = set() | |
| for order in orders: | |
| if hasattr(order, 'symbol') and order.symbol and order.side.value == 'buy': | |
| symbols_traded.add(order.symbol) | |
| logger.info(f"Found {len(symbols_traded)} unique symbols traded") | |
| results = [] | |
| total_error = 0 | |
| correct_directions = 0 | |
| valid_results = 0 | |
| summary_text = f"🎯 TRADING HISTORY BACKTESTING\n" | |
| summary_text += f"Testing sentiment analysis on {len(symbols_traded)} IPOs we actually invested in...\n" | |
| summary_text += f"Using news from 12 hours before our investment time\n\n" | |
| # Process each symbol that was traded | |
| for symbol in sorted(symbols_traded): | |
| # Get all orders for this symbol | |
| symbol_orders = [o for o in orders if o.symbol == symbol] | |
| buy_orders = [o for o in symbol_orders if o.side.value == 'buy'] | |
| if buy_orders: | |
| # Get first buy order details | |
| first_buy_order = min(buy_orders, key=lambda x: x.filled_at) | |
| investment_time = first_buy_order.filled_at | |
| total_bought = sum(float(o.filled_qty or 0) for o in buy_orders) | |
| total_cost = sum(float(o.filled_qty or 0) * float(o.filled_avg_price or 0) for o in buy_orders) | |
| avg_buy_price = total_cost / total_bought if total_bought > 0 else 0 | |
| logger.info(f"Analyzing {symbol} (invested {investment_time.strftime('%Y-%m-%d %H:%M')})...") | |
| # Get pre-investment news | |
| news_items = get_pre_investment_news(symbol, investment_time) | |
| # Analyze sentiment | |
| avg_sentiment, predicted_change, prediction_label, source_breakdown = analyze_pre_investment_sentiment(news_items) | |
| # Get actual performance | |
| first_hour_change, day_change, actual_open = get_actual_performance(symbol, investment_time, avg_buy_price) | |
| if first_hour_change is not None: | |
| # Calculate metrics | |
| error = abs(predicted_change - first_hour_change) | |
| total_error += error | |
| valid_results += 1 | |
| # Check direction | |
| predicted_direction = "UP" if predicted_change > 0 else "DOWN" if predicted_change < 0 else "FLAT" | |
| actual_direction = "UP" if first_hour_change > 0 else "DOWN" if first_hour_change < 0 else "FLAT" | |
| direction_correct = predicted_direction == actual_direction | |
| if direction_correct: | |
| correct_directions += 1 | |
| # Show top sources | |
| reddit_items = source_breakdown['Reddit'] | |
| news_items_found = source_breakdown['Google News'] | |
| top_reddit_title = "" | |
| if reddit_items: | |
| top_reddit = max(reddit_items, key=lambda x: abs(x['sentiment'])) | |
| top_reddit_title = top_reddit['title'] | |
| top_news_title = "" | |
| if news_items_found: | |
| top_news = max(news_items_found, key=lambda x: abs(x['sentiment'])) | |
| top_news_title = top_news['title'] | |
| result = { | |
| 'Symbol': symbol, | |
| 'Investment Date': investment_time.strftime('%Y-%m-%d'), | |
| 'Investment Price': f"${avg_buy_price:.2f}", | |
| 'Predicted Change': f"{predicted_change:+.1f}%", | |
| 'Actual 1H Change': f"{first_hour_change:+.1f}%", | |
| 'Error': f"{error:.1f}%", | |
| 'Direction': '✅ Correct' if direction_correct else '❌ Wrong', | |
| 'Sentiment': prediction_label.title(), | |
| 'News Sources': len(news_items), | |
| 'Reddit Posts': len(reddit_items), | |
| 'Top Reddit': top_reddit_title, | |
| 'Top News': top_news_title | |
| } | |
| else: | |
| result = { | |
| 'Symbol': symbol, | |
| 'Investment Date': investment_time.strftime('%Y-%m-%d'), | |
| 'Investment Price': f"${avg_buy_price:.2f}", | |
| 'Predicted Change': f"{predicted_change:+.1f}%", | |
| 'Actual 1H Change': 'N/A', | |
| 'Error': 'N/A', | |
| 'Direction': '❓ No Data', | |
| 'Sentiment': prediction_label.title(), | |
| 'News Sources': len(news_items), | |
| 'Reddit Posts': len(source_breakdown['Reddit']), | |
| 'Top Reddit': '', | |
| 'Top News': '' | |
| } | |
| results.append(result) | |
| # Calculate summary statistics | |
| if valid_results > 0: | |
| avg_error = total_error / valid_results | |
| direction_accuracy = (correct_directions / valid_results) * 100 | |
| summary_text += f"📈 BACKTESTING RESULTS SUMMARY:\n" | |
| summary_text += f" Total Investments Tested: {len(results)}\n" | |
| summary_text += f" Valid Results: {valid_results}\n" | |
| summary_text += f" Average Error: {avg_error:.1f}%\n" | |
| summary_text += f" Direction Accuracy: {direction_accuracy:.1f}% ({correct_directions}/{valid_results})\n\n" | |
| if direction_accuracy >= 60: | |
| summary_text += f" ✅ Strong predictive value!\n" | |
| elif direction_accuracy >= 40: | |
| summary_text += f" ⚡ Some predictive value\n" | |
| else: | |
| summary_text += f" ❌ Needs improvement\n" | |
| else: | |
| summary_text += f"❌ No valid results available for analysis\n" | |
| # Create DataFrame | |
| df = pd.DataFrame(results) | |
| return summary_text, df | |
| except Exception as e: | |
| error_msg = f"❌ Error running backtesting: {str(e)}" | |
| logger.error(error_msg) | |
| return error_msg, pd.DataFrame() | |
| def clear_terminal(): | |
| """Clear terminal output""" | |
| return "🖥️ VM Terminal Ready\n$ " | |
| def run_quick_command(cmd): | |
| """Helper for quick command buttons""" | |
| def execute(current_output): | |
| return run_vm_command(cmd, current_output) | |
| return execute | |
| # Custom CSS for gorgeous design | |
| custom_css = """ | |
| .gradio-container { | |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important; | |
| background: #fafafa !important; | |
| } | |
| .main-header { | |
| background: linear-gradient(135deg, #0070f3 0%, #0051a5 100%); | |
| color: white; | |
| padding: 2rem; | |
| border-radius: 16px; | |
| margin-bottom: 2rem; | |
| box-shadow: 0 10px 40px rgba(0, 112, 243, 0.3); | |
| } | |
| .metric-card { | |
| background: white; | |
| border: 1px solid #eaeaea; | |
| border-radius: 12px; | |
| padding: 1.5rem; | |
| box-shadow: 0 4px 16px rgba(0, 0, 0, 0.04); | |
| transition: all 0.3s ease; | |
| } | |
| .metric-card:hover { | |
| box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12); | |
| transform: translateY(-4px); | |
| } | |
| .gr-button { | |
| background: linear-gradient(135deg, #0070f3 0%, #0051a5 100%) !important; | |
| color: white !important; | |
| border: none !important; | |
| border-radius: 12px !important; | |
| font-weight: 600 !important; | |
| padding: 1rem 2rem !important; | |
| transition: all 0.3s ease !important; | |
| box-shadow: 0 4px 16px rgba(0, 112, 243, 0.3) !important; | |
| } | |
| .gr-button:hover { | |
| transform: translateY(-2px) !important; | |
| box-shadow: 0 8px 32px rgba(0, 112, 243, 0.4) !important; | |
| } | |
| .gr-textbox, .gr-dataframe { | |
| border: 1px solid #eaeaea !important; | |
| border-radius: 12px !important; | |
| background: white !important; | |
| box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04) !important; | |
| } | |
| .plotly-graph-div { | |
| border-radius: 16px !important; | |
| box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08) !important; | |
| background: white !important; | |
| } | |
| .status-invested { color: #00d647 !important; font-weight: 600 !important; } | |
| .status-eligible { color: #f5a623 !important; font-weight: 600 !important; } | |
| .status-wrong { color: #8b949e !important; } | |
| .status-unknown { color: #ff0080 !important; } | |
| /* Investment Performance Table Styling */ | |
| .investment-table { | |
| width: 100%; | |
| border-collapse: separate; | |
| border-spacing: 0; | |
| background: white; | |
| border-radius: 16px; | |
| overflow: hidden; | |
| box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); | |
| } | |
| .investment-table th { | |
| background: linear-gradient(135deg, #0070f3 0%, #0051a5 100%); | |
| color: white; | |
| padding: 1rem; | |
| font-weight: 600; | |
| text-align: left; | |
| border: none; | |
| } | |
| .investment-table th:first-child { | |
| border-top-left-radius: 16px; | |
| } | |
| .investment-table th:last-child { | |
| border-top-right-radius: 16px; | |
| } | |
| .investment-table td { | |
| padding: 1rem; | |
| border-bottom: 1px solid #f5f5f5; | |
| font-weight: 500; | |
| } | |
| .profit-row { | |
| background: rgba(0, 214, 71, 0.1) !important; | |
| border-left: 4px solid #00d647; | |
| } | |
| .loss-row { | |
| background: rgba(255, 0, 128, 0.1) !important; | |
| border-left: 4px solid #ff0080; | |
| } | |
| .neutral-row { | |
| background: rgba(139, 148, 158, 0.05) !important; | |
| border-left: 4px solid #8b949e; | |
| } | |
| .investment-table tr:last-child td:first-child { | |
| border-bottom-left-radius: 16px; | |
| } | |
| .investment-table tr:last-child td:last-child { | |
| border-bottom-right-radius: 16px; | |
| } | |
| .profit-positive { color: #00d647 !important; font-weight: 600 !important; } | |
| .profit-negative { color: #ff0080 !important; font-weight: 600 !important; } | |
| .profit-neutral { color: #8b949e !important; } | |
| .terminal-container { | |
| background: #000000 !important; | |
| border: 1px solid #333 !important; | |
| border-radius: 8px !important; | |
| padding: 0 !important; | |
| margin: 1rem 0 !important; | |
| height: 500px !important; | |
| overflow-y: auto !important; | |
| display: flex !important; | |
| flex-direction: column !important; | |
| /* Hide scrollbars but keep functionality */ | |
| scrollbar-width: none !important; /* Firefox */ | |
| -ms-overflow-style: none !important; /* IE/Edge */ | |
| } | |
| .terminal-container::-webkit-scrollbar { | |
| display: none !important; /* Chrome/Safari/Webkit */ | |
| } | |
| .terminal-display { | |
| font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace !important; | |
| background: #000000 !important; | |
| color: #ffffff !important; | |
| padding: 1rem !important; | |
| font-size: 14px !important; | |
| line-height: 1.4 !important; | |
| white-space: pre-wrap !important; | |
| word-wrap: break-word !important; | |
| margin: 0 !important; | |
| flex-grow: 1 !important; | |
| overflow-anchor: none !important; | |
| /* Always stick to bottom */ | |
| display: flex !important; | |
| flex-direction: column !important; | |
| justify-content: flex-end !important; | |
| } | |
| .terminal-display::-webkit-scrollbar { | |
| display: none !important; /* Chrome/Safari/Webkit */ | |
| } | |
| .terminal-input input { | |
| font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace !important; | |
| background: #1a1a1a !important; | |
| color: #ffffff !important; | |
| border: 1px solid #333 !important; | |
| border-radius: 4px !important; | |
| font-size: 14px !important; | |
| } | |
| .terminal-input input:focus { | |
| border-color: #00ff00 !important; | |
| box-shadow: 0 0 5px rgba(0, 255, 0, 0.3) !important; | |
| } | |
| /* Force Gradio HTML to stick to bottom */ | |
| .gr-html { | |
| height: 500px !important; | |
| overflow-y: auto !important; | |
| scrollbar-width: none !important; | |
| -ms-overflow-style: none !important; | |
| display: flex !important; | |
| flex-direction: column !important; | |
| } | |
| .gr-html::-webkit-scrollbar { | |
| display: none !important; | |
| } | |
| /* Force content to bottom with CSS anchor */ | |
| .gr-html > div { | |
| display: flex !important; | |
| flex-direction: column !important; | |
| justify-content: flex-end !important; | |
| min-height: 100% !important; | |
| } | |
| """ | |
| def create_dashboard(): | |
| logger.info("🎨 Creating Gradio dashboard interface...") | |
| try: | |
| with gr.Blocks( | |
| title="🚀 Premium Trading Dashboard", | |
| theme=gr.themes.Soft(primary_hue="blue"), | |
| css=custom_css | |
| ) as demo: | |
| logger.info("🖼️ Dashboard blocks created successfully") | |
| # Header | |
| gr.HTML(""" | |
| <div class="main-header"> | |
| <h1 style="margin: 0; font-size: 3rem; font-weight: 800; text-shadow: 0 2px 4px rgba(0,0,0,0.1);"> | |
| 🚀 Premium Trading Dashboard | |
| </h1> | |
| <p style="margin: 1rem 0 0 0; font-size: 1.3rem; opacity: 0.95;"> | |
| Real-time portfolio monitoring with IPO discovery analytics | |
| </p> | |
| </div> | |
| """) | |
| with gr.Tabs(): | |
| # Portfolio Overview Tab | |
| with gr.Tab("📊 Portfolio Overview"): | |
| gr.Markdown("## 💼 Account Summary") | |
| with gr.Row(): | |
| portfolio_value = gr.Textbox(label="💰 Portfolio Value", interactive=False) | |
| buying_power = gr.Textbox(label="💳 Buying Power", interactive=False) | |
| cash = gr.Textbox(label="💵 Cash", interactive=False) | |
| day_change = gr.Textbox(label="📈 Day Change", interactive=False) | |
| equity = gr.Textbox(label="🏦 Total Equity", interactive=False) | |
| gr.Markdown("## 📈 Portfolio Performance") | |
| portfolio_chart = gr.Plot(label="Portfolio Value Over Time") | |
| refresh_overview_btn = gr.Button("🔄 Refresh Portfolio Data", variant="primary", size="lg") | |
| # IPO Discoveries Tab | |
| with gr.Tab("🔍 IPO Discoveries"): | |
| gr.Markdown("## 📊 IPO Discovery Analytics") | |
| with gr.Row(): | |
| total_ipos = gr.Textbox(label="🎯 Total IPOs Detected", interactive=False) | |
| ipos_invested = gr.Textbox(label="💰 IPOs Invested", interactive=False) | |
| cs_stocks = gr.Textbox(label="📈 CS Stocks Found", interactive=False) | |
| investment_rate = gr.Textbox(label="🎲 Investment Rate", interactive=False) | |
| last_updated = gr.Textbox(label="🕒 Last Updated", interactive=False) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| ipo_chart = gr.Plot(label="Investment Decision Breakdown") | |
| with gr.Column(scale=2): | |
| gr.Markdown("## 🆕 Recent IPO Discoveries") | |
| ipo_table = gr.Dataframe( | |
| label="IPO Discoveries with Investment Decisions", | |
| ) | |
| refresh_ipo_btn = gr.Button("🔄 Refresh IPO Data", variant="primary", size="lg") | |
| # Investment Performance Tab | |
| with gr.Tab("💰 Investment Performance"): | |
| gr.Markdown("## 🎯 IPO Investment Performance") | |
| gr.Markdown("### Track profit/loss on your IPO investments with **real-time sentiment analysis**") | |
| gr.Markdown("🧠 **NEW**: Each row automatically shows sentiment predictions from Reddit + Google News!") | |
| investment_performance_table = gr.HTML( | |
| label="IPO Investment P&L Analysis", | |
| value="<div style='text-align: center; padding: 2rem; color: #666;'>Click Refresh to load investment performance data</div>" | |
| ) | |
| refresh_investment_btn = gr.Button("🔄 Refresh Investment Performance", variant="primary", size="lg") | |
| gr.Markdown("### 🧮 Trading Statistics & Analysis") | |
| gr.Markdown("Calculate interesting metrics from your trading data") | |
| with gr.Row(): | |
| calc_sequential_btn = gr.Button("📈 Sequential Reinvestment P&L%", variant="secondary", size="sm") | |
| calc_equal_weight_btn = gr.Button("⚖️ Equal Weight Portfolio P&L%", variant="secondary", size="sm") | |
| calc_best_worst_btn = gr.Button("🏆 Best vs Worst Performers", variant="secondary", size="sm") | |
| with gr.Row(): | |
| calc_win_rate_btn = gr.Button("🎯 Win Rate & Avg Returns", variant="secondary", size="sm") | |
| calc_risk_metrics_btn = gr.Button("⚠️ Risk Metrics & Volatility", variant="secondary", size="sm") | |
| calc_time_analysis_btn = gr.Button("⏰ Time-based Performance", variant="secondary", size="sm") | |
| stats_output = gr.Textbox( | |
| label="Statistical Analysis Results", | |
| lines=8, | |
| interactive=False, | |
| ) | |
| gr.Markdown("### 🔧 Debug API Calls") | |
| debug_output = gr.Textbox( | |
| label="Debug Output", | |
| lines=10, | |
| interactive=False | |
| ) | |
| with gr.Row(): | |
| debug_orders_btn = gr.Button("🔍 Debug Order History", variant="secondary") | |
| debug_positions_btn = gr.Button("📊 Debug Current Positions", variant="secondary") | |
| debug_ipos_btn = gr.Button("🎯 Debug IPO Data", variant="secondary") | |
| debug_account_btn = gr.Button("💼 Debug Account Info", variant="secondary") | |
| # VM Terminal Tab | |
| with gr.Tab("💻 VM Terminal"): | |
| gr.Markdown("## 🖥️ Remote VM Terminal") | |
| gr.Markdown("### Execute commands directly on your trading VM") | |
| # Hidden state for command history | |
| command_history = gr.State("") | |
| with gr.Row(): | |
| with gr.Column(scale=4): | |
| command_input = gr.Textbox( | |
| label="Command (Press Enter to run)", | |
| placeholder="Enter command to run on VM...", | |
| interactive=True, | |
| ) | |
| with gr.Column(scale=1): | |
| run_command_btn = gr.Button("▶️ Run", variant="primary", size="lg") | |
| clear_terminal_btn = gr.Button("🗑️ Clear", variant="secondary", size="lg") | |
| terminal_output = gr.HTML( | |
| label="Terminal Output", | |
| value='<div class="terminal-display" id="terminal-content">🖥️ VM Terminal Ready<br>$ </div>', | |
| ) | |
| gr.Markdown("**📁 File & System Commands:**") | |
| with gr.Row(): | |
| quick_ls = gr.Button("📁 ls -la", size="sm") | |
| quick_pwd = gr.Button("📍 pwd", size="sm") | |
| quick_ps = gr.Button("🔄 ps aux | grep python", size="sm") | |
| quick_vm_status = gr.Button("🖥️ uptime && df -h", size="sm") | |
| quick_who = gr.Button("👤 whoami", size="sm") | |
| gr.Markdown("**📋 Log Files:**") | |
| with gr.Row(): | |
| quick_script_log = gr.Button("📜 tail -50 script.log", size="sm") | |
| quick_server_log = gr.Button("🖥️ tail -50 server.log", size="sm") | |
| quick_cron_log = gr.Button("⏰ tail -50 /var/log/cron", size="sm") | |
| quick_portfolio = gr.Button("💼 cat portfolio.txt", size="sm") | |
| quick_tickers = gr.Button("🎯 head -20 new_tickers_log.csv", size="sm") | |
| gr.Markdown("**🔍 Search & Analysis:**") | |
| with gr.Row(): | |
| quick_errors = gr.Button("🚨 grep -i error script.log | tail -10", size="sm") | |
| quick_trades = gr.Button("💰 grep -i 'buy\\|sell' script.log | tail -10", size="sm") | |
| quick_ipos = gr.Button("🆕 grep -i 'new ticker' script.log | tail -10", size="sm") | |
| # System Logs Tab | |
| with gr.Tab("📋 System Logs"): | |
| gr.Markdown("## 🖥️ Trading Bot Activity") | |
| with gr.Row(): | |
| with gr.Column(): | |
| gr.Markdown("### 🎯 Parsed Logs (Color Coded)") | |
| system_logs = gr.Textbox( | |
| label="Recent System Activity", | |
| lines=20, | |
| max_lines=20, | |
| interactive=False, | |
| ) | |
| with gr.Column(): | |
| gr.Markdown("### 📄 Raw Cron Logs") | |
| raw_logs = gr.Textbox( | |
| label="Raw Log Output", | |
| lines=20, | |
| max_lines=20, | |
| interactive=False, | |
| ) | |
| refresh_logs_btn = gr.Button("🔄 Refresh All 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>🤖 Automated Trading Dashboard</strong></p> | |
| <p style="font-size: 0.95rem;">Real-time data from Alpaca Markets + VM Analytics | Built with ❤️</p> | |
| </div> | |
| """) | |
| logger.info("🔗 Setting up event handlers...") | |
| # Event Handlers - INSIDE Blocks context | |
| # Portfolio tab | |
| refresh_overview_btn.click( | |
| fn=refresh_account_overview, | |
| outputs=[portfolio_value, buying_power, cash, day_change, equity] | |
| ) | |
| refresh_overview_btn.click( | |
| fn=create_portfolio_chart, | |
| outputs=[portfolio_chart] | |
| ) | |
| # IPO tab | |
| refresh_ipo_btn.click( | |
| fn=refresh_vm_stats, | |
| outputs=[total_ipos, ipos_invested, cs_stocks, investment_rate, last_updated] | |
| ) | |
| refresh_ipo_btn.click( | |
| fn=create_ipo_discovery_chart, | |
| outputs=[ipo_chart] | |
| ) | |
| refresh_ipo_btn.click( | |
| fn=refresh_ipo_discoveries_table, | |
| outputs=[ipo_table] | |
| ) | |
| # Investment Performance tab | |
| refresh_performance_btn.click( | |
| fn=refresh_investment_performance_html, | |
| outputs=[investment_performance_table] | |
| ) | |
| # VM Terminal tab | |
| execute_btn.click( | |
| fn=execute_command, | |
| inputs=[command_input], | |
| outputs=[terminal_output] | |
| ) | |
| # System Logs tab | |
| refresh_logs_btn.click( | |
| fn=refresh_vm_logs, | |
| outputs=[log_output] | |
| ) | |
| # 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_vm_stats, | |
| outputs=[total_ipos, ipos_invested, cs_stocks, investment_rate, last_updated] | |
| ) | |
| demo.load(fn=create_ipo_discovery_chart, outputs=[ipo_chart]) | |
| demo.load(fn=refresh_ipo_discoveries_table, outputs=[ipo_table]) | |
| demo.load(fn=refresh_investment_performance_html, outputs=[investment_performance_table]) | |
| demo.queue() | |
| logger.info("✅ All event handlers configured successfully") | |
| return demo | |
| except Exception as e: | |
| logger.error(f"❌ Failed to create dashboard: {e}") | |
| raise | |
| # Create and launch | |
| logger.info("🏗️ Building dashboard...") | |
| try: | |
| demo = create_dashboard() | |
| logger.info("✅ Dashboard created successfully!") | |
| except Exception as e: | |
| logger.error(f"❌ Dashboard creation failed: {e}") | |
| raise | |
| if __name__ == "__main__": | |
| logger.info("🚀 Launching dashboard server...") | |
| try: | |
| demo.launch() | |
| logger.info("✅ Dashboard launched successfully!") | |
| except Exception as e: | |
| logger.error(f"❌ Dashboard launch failed: {e}") | |
| raise |