jetpackjules commited on
Commit
893fc6c
·
1 Parent(s): b641db3

TEST: Use minimal Gradio app to verify basic functionality works

Browse files
Files changed (3) hide show
  1. app.py +50 -2254
  2. app_full.py +2286 -0
  3. app_minimal.py +82 -0
app.py CHANGED
@@ -1,2286 +1,82 @@
1
  #!/usr/bin/env python3
2
  """
3
- Premium Trading Dashboard - Full Featured
4
- Beautiful Vercel-style dashboard with VM data integration
5
  """
6
 
7
- import os
8
- import sys
9
- import pandas as pd
10
  import gradio as gr
11
- import plotly.graph_objects as go
12
- import plotly.express as px
13
- from datetime import datetime, timedelta, timezone
14
  import logging
15
- import requests
16
- import time
17
- from alpaca.trading.client import TradingClient
18
- from alpaca.trading.requests import GetOrdersRequest, GetPortfolioHistoryRequest
19
- from alpaca.trading.enums import OrderStatus
20
- from alpaca.data.timeframe import TimeFrame
21
- from alpaca.data.historical import StockHistoricalDataClient
22
- from textblob import TextBlob
23
- from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer
24
- import nltk
25
 
26
- # Import yfinance with fallback
27
- try:
28
- import yfinance as yf
29
- YF_AVAILABLE = True
30
- except ImportError as e:
31
- print(f"Warning: yfinance not available: {e}")
32
- YF_AVAILABLE = False
33
-
34
- # Get API keys and VM URL from environment variables
35
- API_KEY = os.getenv('ALPACA_API_KEY', 'PK2FD9B2S86LHR7ZBHG1')
36
- SECRET_KEY = os.getenv('ALPACA_SECRET_KEY', 'QPmGPDgbPArvHv6cldBXc7uWddapYcIAnBhtkuBW')
37
- VM_API_URL = os.getenv('VM_API_URL', 'http://34.56.193.18:8090') # Set this in Hugging Face
38
-
39
- # Configure detailed logging for debugging
40
- logging.basicConfig(
41
- level=logging.INFO,
42
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
43
- handlers=[
44
- logging.StreamHandler(),
45
- ]
46
- )
47
  logger = logging.getLogger(__name__)
48
 
49
- # Log startup information
50
- logger.info("🚀 Starting Premium Trading Dashboard... (Build: 2025-07-29 05:15 - Fixed directory structure)")
51
- logger.info(f"Python version: {sys.version}")
52
- logger.info(f"Working directory: {os.getcwd()}")
53
-
54
- # Download required NLTK data
55
- logger.info("📚 Downloading NLTK data...")
56
- try:
57
- nltk.download('punkt', quiet=True)
58
- nltk.download('vader_lexicon', quiet=True)
59
- nltk.download('brown', quiet=True)
60
- logger.info("✅ NLTK data downloaded successfully")
61
- except Exception as e:
62
- logger.warning(f"⚠️ NLTK download warning: {e}")
63
-
64
- # Initialize Alpaca clients
65
- logger.info("🔌 Initializing Alpaca trading client...")
66
- try:
67
- trading_client = TradingClient(api_key=API_KEY, secret_key=SECRET_KEY)
68
- logger.info("✅ Alpaca trading client initialized successfully")
69
- except Exception as e:
70
- logger.error(f"❌ Failed to initialize Alpaca trading client: {e}")
71
- raise
72
-
73
- logger.info("📊 Initializing Alpaca data client...")
74
- try:
75
- data_client = StockHistoricalDataClient(API_KEY, SECRET_KEY)
76
- logger.info("✅ Alpaca data client initialized successfully")
77
- except Exception as e:
78
- logger.error(f"❌ Failed to initialize Alpaca data client: {e}")
79
- raise
80
-
81
- # Initialize sentiment analyzers
82
- logger.info("🧠 Initializing sentiment analysis engines...")
83
- try:
84
- vader = SentimentIntensityAnalyzer()
85
- logger.info("✅ VADER sentiment analyzer initialized")
86
- except Exception as e:
87
- logger.error(f"❌ Failed to initialize VADER: {e}")
88
- raise
89
-
90
- try:
91
- from textblob import TextBlob
92
- # Test TextBlob
93
- test_blob = TextBlob("test")
94
- logger.info("✅ TextBlob sentiment analyzer initialized")
95
- except Exception as e:
96
- logger.error(f"❌ Failed to initialize TextBlob: {e}")
97
- raise
98
-
99
- headers = {'User-Agent': 'TradingHistoryBacktester/1.0'}
100
- logger.info("✅ HTTP headers configured")
101
-
102
- # Modern color scheme
103
- COLORS = {
104
- 'primary': '#0070f3',
105
- 'success': '#00d647',
106
- 'error': '#ff0080',
107
- 'warning': '#f5a623',
108
- 'neutral': '#8b949e',
109
- 'background': '#fafafa',
110
- 'surface': '#ffffff',
111
- 'text': '#000000',
112
- 'text_secondary': '#666666',
113
- 'border': '#eaeaea'
114
- }
115
-
116
- def fetch_from_vm(endpoint, default_value=None):
117
- """Fetch data from VM API server"""
118
- try:
119
- response = requests.get(f"{VM_API_URL}/api/{endpoint}", timeout=10)
120
- if response.status_code == 200:
121
- return response.json()
122
- else:
123
- logger.warning(f"VM API {endpoint} returned {response.status_code}")
124
- return default_value
125
- except Exception as e:
126
- logger.error(f"Error fetching from VM {endpoint}: {e}")
127
- return default_value
128
-
129
- def get_account_info():
130
- """Get current account information from Alpaca"""
131
- try:
132
- account = trading_client.get_account()
133
- return {
134
- 'portfolio_value': float(account.portfolio_value),
135
- 'buying_power': float(account.buying_power),
136
- 'cash': float(account.cash),
137
- 'equity': float(account.equity),
138
- 'day_change': float(getattr(account, 'unrealized_pl', 0)) if hasattr(account, 'unrealized_pl') else 0,
139
- 'day_change_percent': float(getattr(account, 'unrealized_plpc', 0)) * 100 if hasattr(account, 'unrealized_plpc') else 0,
140
- 'last_equity': float(account.last_equity) if account.last_equity else 0
141
- }
142
- except Exception as e:
143
- logger.error(f"Error fetching account info: {e}")
144
- return {
145
- 'portfolio_value': 0, 'buying_power': 0, 'cash': 0, 'equity': 0,
146
- 'day_change': 0, 'day_change_percent': 0, 'last_equity': 0
147
- }
148
-
149
- def get_portfolio_history():
150
- """Get portfolio value history from Alpaca"""
151
- try:
152
- portfolio_history_request = GetPortfolioHistoryRequest(
153
- period="1M",
154
- timeframe="1D",
155
- extended_hours=False
156
- )
157
-
158
- portfolio_history = trading_client.get_portfolio_history(portfolio_history_request)
159
-
160
- timestamps = [datetime.fromtimestamp(ts, tz=timezone.utc) for ts in portfolio_history.timestamp]
161
- equity_values = portfolio_history.equity
162
-
163
- df = pd.DataFrame({
164
- 'timestamp': timestamps,
165
- 'equity': equity_values
166
- })
167
-
168
- return df.dropna()
169
- except Exception as e:
170
- logger.error(f"Error fetching portfolio history: {e}")
171
- return pd.DataFrame()
172
 
173
- def get_current_positions():
174
- """Get current positions"""
175
- try:
176
- positions = trading_client.get_all_positions()
177
- position_data = []
178
-
179
- for position in positions:
180
- position_data.append({
181
- 'symbol': position.symbol,
182
- 'qty': float(position.qty),
183
- 'market_value': float(position.market_value),
184
- 'cost_basis': float(position.cost_basis),
185
- 'unrealized_pl': float(position.unrealized_pl),
186
- 'unrealized_plpc': float(position.unrealized_plpc) * 100,
187
- 'current_price': float(position.current_price) if position.current_price else 0
188
- })
189
-
190
- return position_data
191
- except Exception as e:
192
- logger.error(f"Error fetching positions: {e}")
193
- return []
194
 
195
- def create_portfolio_chart():
196
- """Create beautiful portfolio value chart"""
197
- portfolio_df = get_portfolio_history()
198
 
199
- if portfolio_df.empty:
200
- fig = go.Figure()
201
- fig.add_annotation(
202
- text="No portfolio history available",
203
- x=0.5, y=0.5,
204
- xref="paper", yref="paper",
205
- showarrow=False,
206
- font=dict(size=16, color=COLORS['text_secondary'])
207
- )
208
- else:
209
- fig = go.Figure()
210
-
211
- fig.add_trace(go.Scatter(
212
- x=portfolio_df['timestamp'],
213
- y=portfolio_df['equity'],
214
- mode='lines',
215
- name='Portfolio Value',
216
- line=dict(color=COLORS['primary'], width=3),
217
- fill='tonexty',
218
- fillcolor=f"rgba(0, 112, 243, 0.1)",
219
- hovertemplate='<b>%{y:$,.2f}</b><br>%{x}<extra></extra>'
220
- ))
221
-
222
- if len(portfolio_df) > 0:
223
- current_value = portfolio_df['equity'].iloc[-1]
224
- fig.add_annotation(
225
- x=portfolio_df['timestamp'].iloc[-1],
226
- y=current_value,
227
- text=f"${current_value:,.2f}",
228
- showarrow=True,
229
- arrowhead=2,
230
- arrowcolor=COLORS['primary'],
231
- bgcolor="white",
232
- bordercolor=COLORS['primary'],
233
- borderwidth=2,
234
- font=dict(size=12, color=COLORS['text'])
235
- )
236
-
237
- fig.update_layout(
238
- title=dict(
239
- text="Portfolio Value (Last 30 Days)",
240
- font=dict(size=24, color=COLORS['text'], family="Inter"),
241
- x=0.02
242
- ),
243
- xaxis=dict(
244
- title="Date",
245
- showgrid=True,
246
- gridcolor=COLORS['border'],
247
- color=COLORS['text_secondary']
248
- ),
249
- yaxis=dict(
250
- title="Portfolio Value ($)",
251
- showgrid=True,
252
- gridcolor=COLORS['border'],
253
- color=COLORS['text_secondary'],
254
- tickformat='$,.0f'
255
- ),
256
- plot_bgcolor='white',
257
- paper_bgcolor='white',
258
- height=400,
259
- margin=dict(l=60, r=40, t=60, b=60),
260
- hovermode='x unified',
261
- showlegend=False
262
- )
263
 
264
- return fig
265
-
266
- def create_ipo_discovery_chart():
267
- """Create IPO discovery chart with investment decisions"""
268
- ipos = fetch_from_vm('ipos?limit=100', [])
269
-
270
- if not ipos:
271
- fig = go.Figure()
272
- fig.add_annotation(
273
- text="No IPO data available from VM",
274
- x=0.5, y=0.5,
275
- xref="paper", yref="paper",
276
- showarrow=False,
277
- font=dict(size=16, color=COLORS['text_secondary'])
278
- )
279
- else:
280
- # Count by status
281
- status_counts = {}
282
- for ipo in ipos:
283
- status = ipo.get('investment_status', 'UNKNOWN')
284
- status_counts[status] = status_counts.get(status, 0) + 1
285
 
286
- # Create pie chart
287
- labels = list(status_counts.keys())
288
- values = list(status_counts.values())
289
 
290
- # Map status to colors
291
- color_map = {
292
- 'INVESTED': COLORS['success'],
293
- 'ELIGIBLE_NOT_INVESTED': COLORS['warning'],
294
- 'WRONG_TYPE': COLORS['neutral'],
295
- 'UNKNOWN': COLORS['error']
296
- }
297
- colors = [color_map.get(label, COLORS['neutral']) for label in labels]
298
 
299
- fig = go.Figure(data=[go.Pie(
300
- labels=labels,
301
- values=values,
302
- hole=0.4,
303
- marker=dict(colors=colors),
304
- textinfo='label+percent',
305
- textposition='outside'
306
- )])
307
-
308
- fig.update_layout(
309
- title=dict(
310
- text="IPO Investment Decisions",
311
- font=dict(size=24, color=COLORS['text'], family="Inter"),
312
- x=0.5
313
- ),
314
- plot_bgcolor='white',
315
- paper_bgcolor='white',
316
- height=400,
317
- margin=dict(l=60, r=60, t=60, b=60),
318
- showlegend=True
319
- )
320
-
321
- return fig
322
-
323
- def refresh_account_overview():
324
- """Refresh account overview display"""
325
- account = get_account_info()
326
-
327
- portfolio_value = f"${account['portfolio_value']:,.2f}"
328
- buying_power = f"${account['buying_power']:,.2f}"
329
- cash = f"${account['cash']:,.2f}"
330
-
331
- day_change_value = account['day_change']
332
- day_change_percent = account['day_change_percent']
333
- if day_change_value > 0:
334
- day_change = f"↗️ +${day_change_value:,.2f} (+{day_change_percent:.2f}%)"
335
- elif day_change_value < 0:
336
- day_change = f"↘️ ${day_change_value:,.2f} ({day_change_percent:.2f}%)"
337
- else:
338
- day_change = f"➡️ ${day_change_value:,.2f} ({day_change_percent:.2f}%)"
339
-
340
- equity = f"${account['equity']:,.2f}"
341
-
342
- return portfolio_value, buying_power, cash, day_change, equity
343
-
344
- def refresh_positions_table():
345
- """Refresh current positions table"""
346
- positions = get_current_positions()
347
- if not positions:
348
- return pd.DataFrame(columns=['Symbol', 'Quantity', 'Market Value', 'Unrealized P&L', 'Unrealized %'])
349
-
350
- df_data = []
351
- for pos in positions:
352
- pnl_indicator = "🟢" if pos['unrealized_pl'] > 0 else "🔴" if pos['unrealized_pl'] < 0 else "⚪"
353
- df_data.append({
354
- 'Symbol': f"{pnl_indicator} {pos['symbol']}",
355
- 'Quantity': f"{pos['qty']:.0f}",
356
- 'Market Value': f"${pos['market_value']:,.2f}",
357
- 'Unrealized P&L': f"${pos['unrealized_pl']:,.2f}",
358
- 'Unrealized %': f"{pos['unrealized_plpc']:.2f}%"
359
- })
360
-
361
- return pd.DataFrame(df_data)
362
-
363
- def refresh_ipo_discoveries_table():
364
- """Refresh IPO discoveries table with investment decisions"""
365
- ipos = fetch_from_vm('ipos?limit=100', [])
366
-
367
- if not ipos:
368
- return pd.DataFrame(columns=['Status', 'Symbol', 'Security Type', 'Price', 'Detected At'])
369
-
370
- df_data = []
371
- for ipo in ipos:
372
- status_emoji = ipo.get('status_emoji', '⚪')
373
- status = ipo.get('investment_status', 'UNKNOWN')
374
 
375
- # Clean up status for display
376
- display_status = {
377
- 'INVESTED': '🟢 INVESTED',
378
- 'ELIGIBLE_NOT_INVESTED': '🟡 ELIGIBLE',
379
- 'WRONG_TYPE': '⚪ WRONG TYPE',
380
- 'UNKNOWN': '🔴 UNKNOWN'
381
- }.get(status, '⚪ UNKNOWN')
382
 
383
- df_data.append({
384
- 'Status': display_status,
385
- 'Symbol': ipo.get('symbol', 'N/A'),
386
- 'Security Type': ipo.get('security_type', 'N/A'),
387
- 'Price': f"${ipo.get('trading_price', 0)}" if ipo.get('trading_price') != 'N/A' else 'N/A',
388
- 'Detected At': ipo.get('detected_at', 'N/A')
389
- })
390
-
391
- return pd.DataFrame(df_data)
392
-
393
- def get_order_history():
394
- """Get order history from Alpaca"""
395
- try:
396
- # Use Method 2 which works: 1 year with CLOSED status
397
- end_date = datetime.now(timezone.utc)
398
- start_date = end_date - timedelta(days=365)
399
 
400
- order_request = GetOrdersRequest(
401
- status="closed", # This is the key - use "closed" status
402
- limit=500,
403
- after=start_date,
404
- until=end_date
405
  )
406
 
407
- orders = trading_client.get_orders(order_request)
408
- logger.info(f"Successfully fetched {len(orders)} orders using closed status filter")
409
- return orders
410
- except Exception as e:
411
- logger.error(f"Error fetching order history: {e}")
412
- return []
413
-
414
- def refresh_investment_performance_table():
415
- """Refresh investment performance table with P&L and sentiment analysis for all trading symbols"""
416
- logger.info("📊 Starting investment performance table refresh...")
417
-
418
- # Get IPO data and orders
419
- logger.info("🔌 Fetching IPO data from VM...")
420
- ipos = fetch_from_vm('ipos?limit=100', [])
421
- logger.info(f"📈 Retrieved {len(ipos)} IPO records from VM")
422
-
423
- logger.info("📋 Fetching order history from Alpaca...")
424
- orders = get_order_history()
425
- logger.info(f"📝 Retrieved {len(orders)} orders from Alpaca")
426
-
427
- logger.info("💼 Fetching current positions from Alpaca...")
428
- positions = get_current_positions()
429
- logger.info(f"🏦 Retrieved {len(positions)} current positions")
430
-
431
- # Create proper empty DataFrame with correct column names
432
- columns = ['Symbol', 'Status', 'IPO Price', 'Buy Price', 'Sell Price', 'Investment', 'P&L ($)', 'P&L (%)', 'Sentiment', 'Predicted', 'Date']
433
-
434
- logger.info(f"Found {len(orders)} total orders for performance analysis")
435
-
436
- if not orders:
437
- return pd.DataFrame(columns=columns)
438
-
439
- # Get all unique symbols from order history
440
- symbols_traded = set()
441
- for order in orders:
442
- if hasattr(order, 'symbol') and order.symbol:
443
- symbols_traded.add(order.symbol)
444
-
445
- logger.info(f"Found {len(symbols_traded)} unique symbols traded: {list(symbols_traded)}")
446
-
447
- # Create IPO price lookup from VM data
448
- ipo_price_lookup = {}
449
- for ipo in ipos:
450
- symbol = ipo.get('symbol', '')
451
- if symbol:
452
- try:
453
- price = float(ipo.get('trading_price', 0))
454
- if price > 0:
455
- ipo_price_lookup[symbol] = price
456
- except (ValueError, TypeError):
457
- pass
458
-
459
- invested_data = []
460
-
461
- # Process each symbol that was traded
462
- for symbol in sorted(symbols_traded):
463
- # Get all orders for this symbol
464
- symbol_orders = [o for o in orders if o.symbol == symbol]
465
-
466
- if symbol_orders:
467
- # Calculate from actual orders
468
- buy_orders = [o for o in symbol_orders if o.side.value == 'buy']
469
- sell_orders = [o for o in symbol_orders if o.side.value == 'sell']
470
-
471
- if buy_orders:
472
- total_bought = sum(float(o.filled_qty or 0) for o in buy_orders)
473
- total_cost = sum(float(o.filled_qty or 0) * float(o.filled_avg_price or 0) for o in buy_orders)
474
- avg_buy_price = total_cost / total_bought if total_bought > 0 else 0
475
-
476
- total_sold = sum(float(o.filled_qty or 0) for o in sell_orders)
477
- current_qty = total_bought - total_sold
478
-
479
- # Get IPO price if available
480
- ipo_price = ipo_price_lookup.get(symbol, 0)
481
-
482
- # Get first buy date and time for sentiment analysis
483
- first_buy_order = min(buy_orders, key=lambda x: x.filled_at)
484
- first_buy_date = first_buy_order.filled_at.strftime('%Y-%m-%d')
485
- investment_time = first_buy_order.filled_at
486
- logger.info(f"Date for {symbol}: {first_buy_date} (from {first_buy_order.filled_at})")
487
-
488
- # Calculate sell price (average of all sells)
489
- if sell_orders:
490
- 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)
491
- else:
492
- avg_sell_price = 0
493
-
494
- current_qty = total_bought - total_sold
495
-
496
- if current_qty > 0:
497
- # Still holding - use current position for P&L
498
- status = "🟦 HOLDING"
499
- pos = next((p for p in positions if p['symbol'] == symbol), None)
500
- if pos:
501
- current_price = pos['current_price']
502
- current_value = current_qty * current_price
503
- investment = current_qty * avg_buy_price
504
- pl_dollars = current_value - investment
505
- pl_percent = (pl_dollars / investment * 100) if investment > 0 else 0
506
- else:
507
- # No current position data
508
- investment = current_qty * avg_buy_price
509
- pl_dollars = 0
510
- pl_percent = 0
511
- else:
512
- # Sold all - calculate realized P&L
513
- status = "🟨 SOLD"
514
- investment = total_cost
515
- sold_value = sum(float(o.filled_qty or 0) * float(o.filled_avg_price or 0) for o in sell_orders)
516
- pl_dollars = sold_value - investment
517
- pl_percent = (pl_dollars / investment * 100) if investment > 0 else 0
518
-
519
- # Format P&L with arrows and colors
520
- if pl_dollars > 0:
521
- pl_arrow = "<span style='color: #00d647; font-size: 1.4em;'>▲</span>"
522
- pl_color = "#00d647"
523
- row_bg = "rgba(0, 214, 71, 0.1)"
524
- elif pl_dollars < 0:
525
- pl_arrow = "<span style='color: #ff0080; font-size: 1.4em;'>▼</span>"
526
- pl_color = "#ff0080"
527
- row_bg = "rgba(255, 0, 128, 0.1)"
528
- else:
529
- pl_arrow = ""
530
- pl_color = "#8b949e"
531
- row_bg = "rgba(139, 148, 158, 0.05)"
532
-
533
- # Format P&L values with styled arrows
534
- pl_dollar_str = f"{pl_arrow} <span style='color: {pl_color}; font-weight: 600;'>${abs(pl_dollars):.2f}</span>"
535
- pl_percent_str = f"{pl_arrow} <span style='color: {pl_color}; font-weight: 600;'>{abs(pl_percent):.2f}%</span>"
536
-
537
- # ADD SENTIMENT ANALYSIS FOR EACH STOCK
538
- logger.info(f"🧠 Starting sentiment analysis for {symbol}...")
539
- start_time = time.time()
540
- try:
541
- # Get pre-investment news (quick version)
542
- logger.info(f"📰 Gathering pre-investment news for {symbol}...")
543
- news_items = get_pre_investment_news(symbol, investment_time, hours_before=12)
544
- logger.info(f"📑 Found {len(news_items)} total news items for {symbol}")
545
-
546
- # Analyze sentiment
547
- logger.info(f"🔍 Analyzing sentiment for {symbol}...")
548
- avg_sentiment, predicted_change, prediction_label, source_breakdown = analyze_pre_investment_sentiment(news_items)
549
-
550
- analysis_time = time.time() - start_time
551
- logger.info(f"⚡ Sentiment analysis for {symbol} completed in {analysis_time:.1f}s")
552
-
553
- # Format sentiment display
554
- if prediction_label == "bullish":
555
- sentiment_display = f"<span style='color: #00d647; font-weight: 600;'>🚀 {prediction_label.title()}</span>"
556
- elif prediction_label == "bearish":
557
- sentiment_display = f"<span style='color: #ff0080; font-weight: 600;'>📉 {prediction_label.title()}</span>"
558
- else:
559
- sentiment_display = f"<span style='color: #8b949e; font-weight: 600;'>😐 {prediction_label.title()}</span>"
560
-
561
- # Format prediction
562
- if predicted_change > 0:
563
- predicted_display = f"<span style='color: #00d647; font-weight: 600;'>+{predicted_change:.1f}%</span>"
564
- elif predicted_change < 0:
565
- predicted_display = f"<span style='color: #ff0080; font-weight: 600;'>{predicted_change:.1f}%</span>"
566
- else:
567
- predicted_display = f"<span style='color: #8b949e; font-weight: 600;'>{predicted_change:.1f}%</span>"
568
-
569
- reddit_count = len(source_breakdown.get('Reddit', []))
570
- news_count = len(source_breakdown.get('Google News', []))
571
- logger.info(f"🎯 {symbol} RESULTS: {prediction_label.upper()} ({predicted_change:+.1f}%) | Reddit: {reddit_count} posts | News: {news_count} articles")
572
-
573
- # Log sample titles for debugging
574
- if reddit_count > 0:
575
- sample_reddit = source_breakdown['Reddit'][0]['title'][:50]
576
- logger.info(f"📱 Sample Reddit: {sample_reddit}...")
577
- if news_count > 0:
578
- sample_news = source_breakdown['Google News'][0]['title'][:50]
579
- logger.info(f"📰 Sample News: {sample_news}...")
580
-
581
- except Exception as e:
582
- analysis_time = time.time() - start_time
583
- logger.error(f"❌ Sentiment analysis failed for {symbol} after {analysis_time:.1f}s: {str(e)}")
584
- logger.error(f"🔍 Error type: {type(e).__name__}")
585
- import traceback
586
- logger.error(f"📋 Traceback: {traceback.format_exc()[:200]}...")
587
- sentiment_display = "<span style='color: #8b949e;'>❓ Error</span>"
588
- predicted_display = "<span style='color: #8b949e;'>N/A</span>"
589
-
590
- # Continue with next stock instead of failing completely
591
- pass
592
-
593
- invested_data.append({
594
- 'Symbol': symbol,
595
- 'Status': status,
596
- 'IPO Price': f"${ipo_price:.2f}" if ipo_price > 0 else 'N/A',
597
- 'Buy Price': f"${avg_buy_price:.2f}",
598
- 'Sell Price': f"${avg_sell_price:.2f}" if avg_sell_price > 0 else 'N/A',
599
- 'Investment': f"${investment:.2f}",
600
- 'P&L ($)': pl_dollar_str,
601
- 'P&L (%)': pl_percent_str,
602
- 'Sentiment': sentiment_display,
603
- 'Predicted': predicted_display,
604
- 'Date': first_buy_date,
605
- '_row_bg': row_bg, # Store background color for styling
606
- '_sort_date': first_buy_order.filled_at # Store datetime for sorting
607
- })
608
-
609
- # Sort by date (most recent first)
610
- invested_data.sort(key=lambda x: x['_sort_date'], reverse=True)
611
- logger.info(f"📋 Processed {len(invested_data)} investments with sentiment analysis")
612
-
613
- df = pd.DataFrame(invested_data)
614
- logger.info(f"✅ Investment performance table refresh completed - {len(df)} rows")
615
- return df
616
-
617
- def refresh_investment_performance_html():
618
- """Return styled HTML table for investment performance"""
619
- df = refresh_investment_performance_table()
620
-
621
- if df.empty:
622
- return "<div style='text-align: center; padding: 2rem; color: #666;'>No trading data available</div>"
623
-
624
- # Build HTML table
625
- html = '<table class="investment-table">'
626
-
627
- # Header
628
- html += '<thead><tr>'
629
- for col in df.columns:
630
- if not col.startswith('_'): # Skip internal columns
631
- html += f'<th>{col}</th>'
632
- html += '</tr></thead>'
633
-
634
- # Body
635
- html += '<tbody>'
636
- for _, row in df.iterrows():
637
- # Determine row class based on P&L
638
- row_class = ""
639
- pl_str = str(row.get('P&L ($)', ''))
640
- if '▲' in pl_str:
641
- row_class = "profit-row"
642
- elif '▼' in pl_str:
643
- row_class = "loss-row"
644
- else:
645
- row_class = "neutral-row"
646
-
647
- html += f'<tr class="{row_class}">'
648
- for col in df.columns:
649
- if not col.startswith('_'): # Skip internal columns
650
- html += f'<td>{row[col]}</td>'
651
- html += '</tr>'
652
-
653
- html += '</tbody></table>'
654
- return html
655
-
656
- def refresh_vm_stats():
657
- """Refresh VM statistics"""
658
- stats = fetch_from_vm('stats', {})
659
-
660
- if not stats:
661
- return "0", "0", "0", "0%", "No data"
662
-
663
- return (
664
- str(stats.get('total_ipos_detected', 0)),
665
- str(stats.get('ipos_invested', 0)),
666
- str(stats.get('cs_stocks_detected', 0)),
667
- f"{stats.get('investment_rate', 0):.1f}%",
668
- stats.get('last_updated', 'N/A')
669
- )
670
-
671
- def refresh_system_logs():
672
- """Refresh system logs from VM"""
673
- logs = fetch_from_vm('logs', [])
674
-
675
- if not logs:
676
- return "No logs available from VM"
677
-
678
- # Format logs for display
679
- formatted_logs = []
680
- for log in logs:
681
- emoji = log.get('emoji', '⚪')
682
- timestamp = log.get('timestamp', 'N/A')
683
- message = log.get('message', '')
684
- formatted_logs.append(f"{emoji} {timestamp} | {message}")
685
-
686
- return '\n'.join(formatted_logs)
687
-
688
- def refresh_raw_logs():
689
- """Refresh raw logs from VM"""
690
- raw_data = fetch_from_vm('logs/raw?lines=1000', {})
691
-
692
- if not raw_data:
693
- return "No raw logs available from VM"
694
-
695
- content = raw_data.get('content', 'No content')
696
- total_lines = raw_data.get('total_lines', 0)
697
- showing_lines = raw_data.get('showing_lines', 0)
698
-
699
- header = f"=== RAW CRON LOGS ===\nShowing last {showing_lines} of {total_lines} total lines\n\n"
700
- return header + content
701
-
702
- def run_vm_command(command, current_output="", command_history=""):
703
- """Execute command on VM and return output"""
704
- try:
705
- if not command.strip():
706
- return current_output, "", command_history
707
-
708
- # Add command to history
709
- history_list = command_history.split("|||") if command_history else []
710
- if command not in history_list:
711
- history_list.append(command)
712
- # Keep last 50 commands
713
- history_list = history_list[-50:]
714
- new_history = "|||".join(history_list)
715
-
716
- response = requests.post(f"{VM_API_URL}/api/execute",
717
- json={"command": command},
718
- timeout=10)
719
-
720
- if response.status_code == 200:
721
- data = response.json()
722
- output = data.get('output', '')
723
- exit_code = data.get('exit_code', 0)
724
-
725
- # Add color coding for common patterns
726
- colored_output = colorize_output(output)
727
-
728
- # Format terminal-style output with clean spacing
729
- # Clean up output to avoid weird quote formatting
730
- clean_output = colored_output.strip().replace('\r', '')
731
- new_line = f"$ {command}\n{clean_output}"
732
- if exit_code != 0:
733
- new_line += f"\n[Exit code: {exit_code}]"
734
- new_line += "\n$ "
735
-
736
- # RADICAL FIX: Put newest content at TOP instead of bottom!
737
- if current_output.strip():
738
- full_output = new_line + "\n" + current_output.rstrip()
739
- else:
740
- full_output = new_line
741
-
742
- return full_output, "", new_history
743
- else:
744
- error_line = f"\n$ {command}\nError: VM API returned {response.status_code}\n$ "
745
- return current_output + error_line, "", new_history
746
-
747
- except Exception as e:
748
- error_line = f"\n$ {command}\nError: {str(e)}\n$ "
749
- return current_output + error_line, "", new_history
750
-
751
- def colorize_output(output):
752
- """Add basic color coding to terminal output"""
753
- import re
754
-
755
- # Color patterns (using ANSI-like styling for web)
756
- colored = output
757
-
758
- # File permissions and directories (ls output)
759
- colored = re.sub(r'^(d)([rwx-]{9})', r'<span style="color: #4A90E2;">\1\2</span>', colored, flags=re.MULTILINE)
760
- colored = re.sub(r'^(-)([rwx-]{9})', r'<span style="color: #50E3C2;">\1\2</span>', colored, flags=re.MULTILINE)
761
-
762
- # Error messages
763
- colored = re.sub(r'(ERROR|Error|error)', r'<span style="color: #FF6B6B;">\1</span>', colored)
764
- colored = re.sub(r'(WARNING|Warning|warning)', r'<span style="color: #FFD93D;">\1</span>', colored)
765
-
766
- # Success indicators
767
- colored = re.sub(r'(SUCCESS|Success|success)', r'<span style="color: #6BCF7F;">\1</span>', colored)
768
-
769
- # File extensions
770
- colored = re.sub(r'(\w+\.(py|log|csv|json|txt))', r'<span style="color: #BD93F9;">\1</span>', colored)
771
-
772
- # Numbers and timestamps
773
- colored = re.sub(r'(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})', r'<span style="color: #50FA7B;">\1</span>', colored)
774
-
775
- return colored
776
-
777
- def debug_order_history():
778
- """Debug function to show raw order history data"""
779
- try:
780
- # Try multiple approaches to get orders
781
- debug_info = f"=== ORDER HISTORY DEBUG ===\n"
782
-
783
- # Approach 1: All orders, last 6 months (DEPRECATED - doesn't work)
784
- try:
785
- end_date = datetime.now(timezone.utc)
786
- start_date = end_date - timedelta(days=180)
787
- old_request = GetOrdersRequest(limit=500, after=start_date, until=end_date)
788
- old_orders = trading_client.get_orders(old_request)
789
- debug_info += f"Method 1 (6 months, all statuses): {len(old_orders)} orders [DEPRECATED]\n"
790
- except Exception as e:
791
- debug_info += f"Method 1 failed: {str(e)}\n"
792
-
793
- # Approach 1B: Current working method used by Investment Performance
794
- orders = get_order_history()
795
- debug_info += f"Method 1B (PRIMARY - 1 year, CLOSED): {len(orders)} orders [CURRENTLY USED]\n"
796
-
797
- # Approach 2: Just filled orders, last year
798
- try:
799
- end_date = datetime.now(timezone.utc)
800
- start_date = end_date - timedelta(days=365)
801
- filled_request = GetOrdersRequest(
802
- status="closed", # Use string instead of enum
803
- limit=500,
804
- after=start_date,
805
- until=end_date
806
- )
807
- filled_orders = trading_client.get_orders(filled_request)
808
- debug_info += f"Method 2 (1 year, CLOSED orders): {len(filled_orders)} orders\n"
809
- except Exception as e:
810
- debug_info += f"Method 2 failed: {str(e)}\n"
811
-
812
- # Approach 3: No date filter, just get recent orders
813
- try:
814
- recent_request = GetOrdersRequest(limit=100)
815
- recent_orders = trading_client.get_orders(recent_request)
816
- debug_info += f"Method 3 (recent 100, no date filter): {len(recent_orders)} orders\n"
817
- except Exception as e:
818
- debug_info += f"Method 3 failed: {str(e)}\n"
819
-
820
- debug_info += "\n"
821
-
822
- # Show any orders we found
823
- all_orders = orders if orders else (filled_orders if 'filled_orders' in locals() else (recent_orders if 'recent_orders' in locals() else []))
824
-
825
- if all_orders:
826
- debug_info += f"Sample orders (showing first 10):\n"
827
- for i, order in enumerate(all_orders[:10]):
828
- debug_info += f"{i+1}. Symbol: {order.symbol}, Side: {order.side}, "
829
- debug_info += f"Qty: {order.filled_qty}, Price: {order.filled_avg_price}, "
830
- debug_info += f"Status: {order.status}, Time: {order.filled_at}, "
831
- debug_info += f"Created: {order.created_at}\n"
832
- else:
833
- debug_info += "❌ NO ORDERS FOUND WITH ANY METHOD!\n"
834
- debug_info += "\nLet's check account details:\n"
835
-
836
- # Check account info
837
- try:
838
- account = trading_client.get_account()
839
- debug_info += f"Account ID: {account.account_number}\n"
840
- debug_info += f"Account Status: {account.status}\n"
841
- debug_info += f"Trading Blocked: {account.trading_blocked}\n"
842
- debug_info += f"Pattern Day Trader: {account.pattern_day_trader}\n"
843
- debug_info += f"Cash: ${float(account.cash):,.2f}\n"
844
- debug_info += f"Portfolio Value: ${float(account.portfolio_value):,.2f}\n"
845
-
846
- # Check if this is paper trading
847
- debug_info += f"\nAPI Keys being used:\n"
848
- debug_info += f"API Key: {API_KEY[:8]}...{API_KEY[-4:]}\n"
849
- if "PK" in API_KEY:
850
- debug_info += "🟢 This appears to be PAPER TRADING (PK prefix)\n"
851
- elif "AK" in API_KEY:
852
- debug_info += "🔴 This appears to be LIVE TRADING (AK prefix)\n"
853
- else:
854
- debug_info += "❓ Unknown API key type\n"
855
-
856
- except Exception as e:
857
- debug_info += f"❌ Error getting account info: {str(e)}\n"
858
-
859
- debug_info += "\nPossible issues:\n"
860
- debug_info += "- No actual trading activity on this account\n"
861
- debug_info += "- Using paper trading account (no real orders)\n"
862
- debug_info += "- Orders are older than 1 year\n"
863
- debug_info += "- API key permissions issue\n"
864
- debug_info += "- Different Alpaca account than expected\n"
865
-
866
- return debug_info
867
- except Exception as e:
868
- return f"ERROR getting order history: {str(e)}"
869
-
870
- def debug_current_positions():
871
- """Debug function to show current positions"""
872
- try:
873
- positions = get_current_positions()
874
- debug_info = f"=== CURRENT POSITIONS DEBUG ===\n"
875
- debug_info += f"Total positions: {len(positions)}\n\n"
876
-
877
- for pos in positions:
878
- debug_info += f"Symbol: {pos['symbol']}, Qty: {pos['qty']}, "
879
- debug_info += f"Market Value: ${pos['market_value']:.2f}, "
880
- debug_info += f"P&L: ${pos['unrealized_pl']:.2f}\n"
881
-
882
- return debug_info
883
- except Exception as e:
884
- return f"ERROR getting positions: {str(e)}"
885
-
886
- def debug_ipo_data():
887
- """Debug function to show IPO data from VM"""
888
- try:
889
- ipos = fetch_from_vm('ipos?limit=20', [])
890
- debug_info = f"=== IPO DATA DEBUG ===\n"
891
- debug_info += f"Total IPOs: {len(ipos)}\n\n"
892
-
893
- invested_count = 0
894
- for ipo in ipos:
895
- status = ipo.get('investment_status', 'UNKNOWN')
896
- if status == 'INVESTED':
897
- invested_count += 1
898
- debug_info += f"INVESTED: {ipo.get('symbol')} - Price: ${ipo.get('trading_price')}\n"
899
-
900
- debug_info += f"\nTotal INVESTED IPOs: {invested_count}\n"
901
- return debug_info
902
- except Exception as e:
903
- return f"ERROR getting IPO data: {str(e)}"
904
-
905
- def debug_account_info():
906
- """Debug function to show account info"""
907
- try:
908
- account = get_account_info()
909
- debug_info = f"=== ACCOUNT INFO DEBUG ===\n"
910
- for key, value in account.items():
911
- debug_info += f"{key}: {value}\n"
912
- return debug_info
913
- except Exception as e:
914
- return f"ERROR getting account info: {str(e)}"
915
-
916
- def calculate_sequential_reinvestment():
917
- """Calculate P&L% if reinvesting same amount sequentially in each stock"""
918
- try:
919
- orders = get_order_history()
920
- if not orders:
921
- return "No order data available for calculation"
922
-
923
- # Get unique symbols with their first buy date
924
- symbols_by_date = {}
925
- for order in orders:
926
- if order.side.value == 'buy' and order.status.value == 'filled':
927
- symbol = order.symbol
928
- fill_date = order.filled_at
929
- if symbol not in symbols_by_date or fill_date < symbols_by_date[symbol]:
930
- symbols_by_date[symbol] = fill_date
931
-
932
- # Sort by first buy date
933
- sorted_symbols = sorted(symbols_by_date.items(), key=lambda x: x[1])
934
-
935
- # Calculate sequential reinvestment returns
936
- initial_investment = 1000 # Start with $1000
937
- current_value = initial_investment
938
-
939
- results = []
940
- total_return = 0
941
-
942
- for symbol, first_date in sorted_symbols:
943
- # Get all orders for this symbol
944
- symbol_orders = [o for o in orders if o.symbol == symbol]
945
- buy_orders = [o for o in symbol_orders if o.side.value == 'buy']
946
- sell_orders = [o for o in symbol_orders if o.side.value == 'sell']
947
-
948
- if buy_orders:
949
- # Calculate actual P&L for this symbol
950
- total_bought = sum(float(o.filled_qty or 0) for o in buy_orders)
951
- total_cost = sum(float(o.filled_qty or 0) * float(o.filled_avg_price or 0) for o in buy_orders)
952
-
953
- if sell_orders:
954
- # Sold - use actual sell proceeds
955
- sold_value = sum(float(o.filled_qty or 0) * float(o.filled_avg_price or 0) for o in sell_orders)
956
- pl_percent = ((sold_value - total_cost) / total_cost) if total_cost > 0 else 0
957
- else:
958
- # Still holding - estimate current value
959
- positions = get_current_positions()
960
- pos = next((p for p in positions if p['symbol'] == symbol), None)
961
- if pos:
962
- current_price = pos['current_price']
963
- current_symbol_value = total_bought * current_price
964
- pl_percent = ((current_symbol_value - total_cost) / total_cost) if total_cost > 0 else 0
965
- else:
966
- pl_percent = 0
967
-
968
- # Apply return to current value
969
- new_value = current_value * (1 + pl_percent)
970
- gain_loss = new_value - current_value
971
-
972
- results.append(f"{symbol}: {pl_percent*100:+.2f}% | ${current_value:.2f} → ${new_value:.2f} ({gain_loss:+.2f})")
973
- current_value = new_value
974
- total_return += pl_percent
975
-
976
- final_return_pct = ((current_value - initial_investment) / initial_investment) * 100
977
-
978
- output = f"🧮 SEQUENTIAL REINVESTMENT ANALYSIS\n"
979
- output += f"Starting Investment: ${initial_investment:.2f}\n"
980
- output += f"Final Value: ${current_value:.2f}\n"
981
- output += f"Total Return: {final_return_pct:+.2f}%\n"
982
- output += f"Number of Trades: {len(sorted_symbols)}\n\n"
983
- output += "Trade Sequence:\n"
984
- output += "\n".join(results)
985
-
986
- return output
987
-
988
- except Exception as e:
989
- return f"ERROR calculating sequential reinvestment: {str(e)}"
990
-
991
- def calculate_equal_weight_portfolio():
992
- """Calculate P&L% if investing equal amounts in all stocks simultaneously"""
993
- try:
994
- orders = get_order_history()
995
- if not orders:
996
- return "No order data available for calculation"
997
-
998
- # Get unique symbols
999
- symbols = set()
1000
- for order in orders:
1001
- if order.side.value == 'buy':
1002
- symbols.add(order.symbol)
1003
-
1004
- total_pl = 0
1005
- valid_symbols = 0
1006
- results = []
1007
-
1008
- for symbol in sorted(symbols):
1009
- symbol_orders = [o for o in orders if o.symbol == symbol]
1010
- buy_orders = [o for o in symbol_orders if o.side.value == 'buy']
1011
- sell_orders = [o for o in symbol_orders if o.side.value == 'sell']
1012
-
1013
- if buy_orders:
1014
- total_bought = sum(float(o.filled_qty or 0) for o in buy_orders)
1015
- total_cost = sum(float(o.filled_qty or 0) * float(o.filled_avg_price or 0) for o in buy_orders)
1016
- avg_buy_price = total_cost / total_bought if total_bought > 0 else 0
1017
-
1018
- if sell_orders:
1019
- # Sold
1020
- sold_value = sum(float(o.filled_qty or 0) * float(o.filled_avg_price or 0) for o in sell_orders)
1021
- pl_percent = ((sold_value - total_cost) / total_cost) if total_cost > 0 else 0
1022
- status = "SOLD"
1023
- else:
1024
- # Still holding
1025
- positions = get_current_positions()
1026
- pos = next((p for p in positions if p['symbol'] == symbol), None)
1027
- if pos:
1028
- current_price = pos['current_price']
1029
- current_value = total_bought * current_price
1030
- pl_percent = ((current_value - total_cost) / total_cost) if total_cost > 0 else 0
1031
- status = "HOLDING"
1032
- else:
1033
- pl_percent = 0
1034
- status = "UNKNOWN"
1035
-
1036
- total_pl += pl_percent
1037
- valid_symbols += 1
1038
-
1039
- results.append(f"{symbol}: {pl_percent*100:+.2f}% ({status})")
1040
-
1041
- avg_return = (total_pl / valid_symbols) * 100 if valid_symbols > 0 else 0
1042
-
1043
- output = f"⚖️ EQUAL WEIGHT PORTFOLIO ANALYSIS\n"
1044
- output += f"Total Symbols: {valid_symbols}\n"
1045
- output += f"Average Return per Symbol: {avg_return:+.2f}%\n"
1046
- output += f"Portfolio Return (equal weights): {avg_return:+.2f}%\n\n"
1047
- output += "Individual Returns:\n"
1048
- output += "\n".join(results)
1049
-
1050
- return output
1051
-
1052
- except Exception as e:
1053
- return f"ERROR calculating equal weight portfolio: {str(e)}"
1054
-
1055
- def calculate_best_worst_performers():
1056
- """Find best and worst performing stocks"""
1057
- try:
1058
- orders = get_order_history()
1059
- if not orders:
1060
- return "No order data available for calculation"
1061
-
1062
- symbols = set()
1063
- for order in orders:
1064
- if order.side.value == 'buy':
1065
- symbols.add(order.symbol)
1066
-
1067
- performance = []
1068
-
1069
- for symbol in symbols:
1070
- symbol_orders = [o for o in orders if o.symbol == symbol]
1071
- buy_orders = [o for o in symbol_orders if o.side.value == 'buy']
1072
- sell_orders = [o for o in symbol_orders if o.side.value == 'sell']
1073
-
1074
- if buy_orders:
1075
- total_bought = sum(float(o.filled_qty or 0) for o in buy_orders)
1076
- total_cost = sum(float(o.filled_qty or 0) * float(o.filled_avg_price or 0) for o in buy_orders)
1077
-
1078
- if sell_orders:
1079
- sold_value = sum(float(o.filled_qty or 0) * float(o.filled_avg_price or 0) for o in sell_orders)
1080
- pl_percent = ((sold_value - total_cost) / total_cost) if total_cost > 0 else 0
1081
- pl_dollars = sold_value - total_cost
1082
- status = "SOLD"
1083
- else:
1084
- positions = get_current_positions()
1085
- pos = next((p for p in positions if p['symbol'] == symbol), None)
1086
- if pos:
1087
- current_price = pos['current_price']
1088
- current_value = total_bought * current_price
1089
- pl_percent = ((current_value - total_cost) / total_cost) if total_cost > 0 else 0
1090
- pl_dollars = current_value - total_cost
1091
- status = "HOLDING"
1092
- else:
1093
- pl_percent = 0
1094
- pl_dollars = 0
1095
- status = "UNKNOWN"
1096
-
1097
- performance.append({
1098
- 'symbol': symbol,
1099
- 'pl_percent': pl_percent,
1100
- 'pl_dollars': pl_dollars,
1101
- 'investment': total_cost,
1102
- 'status': status
1103
- })
1104
-
1105
- # Sort by percentage return
1106
- performance.sort(key=lambda x: x['pl_percent'], reverse=True)
1107
-
1108
- output = f"🏆 BEST vs WORST PERFORMERS\n\n"
1109
-
1110
- if performance:
1111
- output += "🥇 TOP 5 PERFORMERS:\n"
1112
- for i, perf in enumerate(performance[:5]):
1113
- output += f"{i+1}. {perf['symbol']}: {perf['pl_percent']*100:+.2f}% (${perf['pl_dollars']:+.2f}) - {perf['status']}\n"
1114
-
1115
- output += "\n🥉 BOTTOM 5 PERFORMERS:\n"
1116
- for i, perf in enumerate(performance[-5:]):
1117
- rank = len(performance) - 4 + i
1118
- output += f"{rank}. {perf['symbol']}: {perf['pl_percent']*100:+.2f}% (${perf['pl_dollars']:+.2f}) - {perf['status']}\n"
1119
-
1120
- # Calculate some stats
1121
- total_winners = len([p for p in performance if p['pl_percent'] > 0])
1122
- total_losers = len([p for p in performance if p['pl_percent'] < 0])
1123
-
1124
- output += f"\n📊 SUMMARY:\n"
1125
- output += f"Winners: {total_winners}/{len(performance)} ({total_winners/len(performance)*100:.1f}%)\n"
1126
- output += f"Losers: {total_losers}/{len(performance)} ({total_losers/len(performance)*100:.1f}%)\n"
1127
-
1128
- return output
1129
-
1130
- except Exception as e:
1131
- return f"ERROR calculating best/worst performers: {str(e)}"
1132
-
1133
- def calculate_win_rate_metrics():
1134
- """Calculate win rate and average returns"""
1135
- try:
1136
- orders = get_order_history()
1137
- if not orders:
1138
- return "No order data available for calculation"
1139
-
1140
- symbols = set()
1141
- for order in orders:
1142
- if order.side.value == 'buy':
1143
- symbols.add(order.symbol)
1144
-
1145
- performance = []
1146
- total_investment = 0
1147
-
1148
- for symbol in symbols:
1149
- symbol_orders = [o for o in orders if o.symbol == symbol]
1150
- buy_orders = [o for o in symbol_orders if o.side.value == 'buy']
1151
- sell_orders = [o for o in symbol_orders if o.side.value == 'sell']
1152
-
1153
- if buy_orders:
1154
- total_bought = sum(float(o.filled_qty or 0) for o in buy_orders)
1155
- total_cost = sum(float(o.filled_qty or 0) * float(o.filled_avg_price or 0) for o in buy_orders)
1156
- total_investment += total_cost
1157
-
1158
- if sell_orders:
1159
- sold_value = sum(float(o.filled_qty or 0) * float(o.filled_avg_price or 0) for o in sell_orders)
1160
- pl_percent = ((sold_value - total_cost) / total_cost) if total_cost > 0 else 0
1161
- pl_dollars = sold_value - total_cost
1162
- else:
1163
- positions = get_current_positions()
1164
- pos = next((p for p in positions if p['symbol'] == symbol), None)
1165
- if pos:
1166
- current_price = pos['current_price']
1167
- current_value = total_bought * current_price
1168
- pl_percent = ((current_value - total_cost) / total_cost) if total_cost > 0 else 0
1169
- pl_dollars = current_value - total_cost
1170
- else:
1171
- pl_percent = 0
1172
- pl_dollars = 0
1173
-
1174
- performance.append({
1175
- 'symbol': symbol,
1176
- 'pl_percent': pl_percent,
1177
- 'pl_dollars': pl_dollars,
1178
- 'investment': total_cost
1179
- })
1180
-
1181
- if not performance:
1182
- return "No performance data available"
1183
-
1184
- # Calculate metrics
1185
- winners = [p for p in performance if p['pl_percent'] > 0]
1186
- losers = [p for p in performance if p['pl_percent'] < 0]
1187
- breakeven = [p for p in performance if p['pl_percent'] == 0]
1188
-
1189
- win_rate = len(winners) / len(performance) * 100
1190
- avg_win = sum(p['pl_percent'] for p in winners) / len(winners) * 100 if winners else 0
1191
- avg_loss = sum(p['pl_percent'] for p in losers) / len(losers) * 100 if losers else 0
1192
-
1193
- total_pl_dollars = sum(p['pl_dollars'] for p in performance)
1194
- total_pl_percent = (total_pl_dollars / total_investment) * 100 if total_investment > 0 else 0
1195
-
1196
- # Risk/Reward ratio
1197
- risk_reward = abs(avg_win / avg_loss) if avg_loss != 0 else float('inf')
1198
-
1199
- output = f"🎯 WIN RATE & AVERAGE RETURNS\n\n"
1200
- output += f"Total Trades: {len(performance)}\n"
1201
- output += f"Win Rate: {win_rate:.1f}% ({len(winners)} winners)\n"
1202
- output += f"Loss Rate: {len(losers)/len(performance)*100:.1f}% ({len(losers)} losers)\n"
1203
- output += f"Breakeven: {len(breakeven)} trades\n\n"
1204
-
1205
- output += f"📈 AVERAGE PERFORMANCE:\n"
1206
- output += f"Average Winner: +{avg_win:.2f}%\n"
1207
- output += f"Average Loser: {avg_loss:.2f}%\n"
1208
- output += f"Risk/Reward Ratio: {risk_reward:.2f}:1\n\n"
1209
-
1210
- output += f"💰 TOTAL PERFORMANCE:\n"
1211
- output += f"Total Invested: ${total_investment:.2f}\n"
1212
- output += f"Total P&L: ${total_pl_dollars:+.2f}\n"
1213
- output += f"Total Return: {total_pl_percent:+.2f}%\n"
1214
-
1215
- return output
1216
-
1217
- except Exception as e:
1218
- return f"ERROR calculating win rate metrics: {str(e)}"
1219
-
1220
- def calculate_risk_metrics():
1221
- """Calculate risk metrics and volatility"""
1222
- try:
1223
- orders = get_order_history()
1224
- if not orders:
1225
- return "No order data available for calculation"
1226
-
1227
- symbols = set()
1228
- for order in orders:
1229
- if order.side.value == 'buy':
1230
- symbols.add(order.symbol)
1231
-
1232
- returns = []
1233
- investments = []
1234
-
1235
- for symbol in symbols:
1236
- symbol_orders = [o for o in orders if o.symbol == symbol]
1237
- buy_orders = [o for o in symbol_orders if o.side.value == 'buy']
1238
- sell_orders = [o for o in symbol_orders if o.side.value == 'sell']
1239
-
1240
- if buy_orders:
1241
- total_bought = sum(float(o.filled_qty or 0) for o in buy_orders)
1242
- total_cost = sum(float(o.filled_qty or 0) * float(o.filled_avg_price or 0) for o in buy_orders)
1243
- investments.append(total_cost)
1244
-
1245
- if sell_orders:
1246
- sold_value = sum(float(o.filled_qty or 0) * float(o.filled_avg_price or 0) for o in sell_orders)
1247
- pl_percent = ((sold_value - total_cost) / total_cost) if total_cost > 0 else 0
1248
- else:
1249
- positions = get_current_positions()
1250
- pos = next((p for p in positions if p['symbol'] == symbol), None)
1251
- if pos:
1252
- current_price = pos['current_price']
1253
- current_value = total_bought * current_price
1254
- pl_percent = ((current_value - total_cost) / total_cost) if total_cost > 0 else 0
1255
- else:
1256
- pl_percent = 0
1257
-
1258
- returns.append(pl_percent)
1259
-
1260
- if not returns:
1261
- return "No return data available"
1262
-
1263
- # Calculate statistics
1264
- import statistics
1265
- avg_return = statistics.mean(returns) * 100
1266
- median_return = statistics.median(returns) * 100
1267
- volatility = statistics.stdev(returns) * 100 if len(returns) > 1 else 0
1268
-
1269
- # Sharpe-like ratio (assuming risk-free rate = 0)
1270
- sharpe = avg_return / volatility if volatility > 0 else 0
1271
-
1272
- # Max drawdown
1273
- max_return = max(returns) * 100
1274
- min_return = min(returns) * 100
1275
- max_drawdown = max_return - min_return
1276
-
1277
- # Portfolio concentration
1278
- total_investment = sum(investments)
1279
- avg_position_size = statistics.mean(investments)
1280
- largest_position = max(investments)
1281
- concentration = (largest_position / total_investment) * 100 if total_investment > 0 else 0
1282
-
1283
- output = f"⚠️ RISK METRICS & VOLATILITY\n\n"
1284
- output += f"📊 RETURN STATISTICS:\n"
1285
- output += f"Average Return: {avg_return:+.2f}%\n"
1286
- output += f"Median Return: {median_return:+.2f}%\n"
1287
- output += f"Volatility (StdDev): {volatility:.2f}%\n"
1288
- output += f"Sharpe-like Ratio: {sharpe:.2f}\n\n"
1289
-
1290
- output += f"📉 RISK MEASURES:\n"
1291
- output += f"Best Trade: +{max_return:.2f}%\n"
1292
- output += f"Worst Trade: {min_return:.2f}%\n"
1293
- output += f"Max Range: {max_drawdown:.2f}%\n\n"
1294
-
1295
- output += f"🎯 POSITION SIZING:\n"
1296
- output += f"Average Position: ${avg_position_size:.2f}\n"
1297
- output += f"Largest Position: ${largest_position:.2f}\n"
1298
- output += f"Concentration Risk: {concentration:.1f}% in largest\n"
1299
-
1300
- return output
1301
-
1302
- except Exception as e:
1303
- return f"ERROR calculating risk metrics: {str(e)}"
1304
-
1305
- def calculate_time_analysis():
1306
- """Analyze performance by time periods"""
1307
- try:
1308
- orders = get_order_history()
1309
- if not orders:
1310
- return "No order data available for calculation"
1311
-
1312
- from datetime import datetime, timezone
1313
-
1314
- # Group orders by month
1315
- monthly_performance = {}
1316
-
1317
- for order in orders:
1318
- if order.side.value == 'buy' and order.status.value == 'filled':
1319
- month_key = order.filled_at.strftime('%Y-%m')
1320
- if month_key not in monthly_performance:
1321
- monthly_performance[month_key] = {'symbols': set(), 'investment': 0, 'returns': []}
1322
-
1323
- symbol = order.symbol
1324
- monthly_performance[month_key]['symbols'].add(symbol)
1325
-
1326
- # Calculate returns for each month
1327
- symbols = set()
1328
- for order in orders:
1329
- if order.side.value == 'buy':
1330
- symbols.add(order.symbol)
1331
-
1332
- symbol_performance = {}
1333
- for symbol in symbols:
1334
- symbol_orders = [o for o in orders if o.symbol == symbol]
1335
- buy_orders = [o for o in symbol_orders if o.side.value == 'buy']
1336
- sell_orders = [o for o in symbol_orders if o.side.value == 'sell']
1337
-
1338
- if buy_orders:
1339
- first_buy = min(buy_orders, key=lambda x: x.filled_at)
1340
- month_key = first_buy.filled_at.strftime('%Y-%m')
1341
-
1342
- total_bought = sum(float(o.filled_qty or 0) for o in buy_orders)
1343
- total_cost = sum(float(o.filled_qty or 0) * float(o.filled_avg_price or 0) for o in buy_orders)
1344
-
1345
- if sell_orders:
1346
- sold_value = sum(float(o.filled_qty or 0) * float(o.filled_avg_price or 0) for o in sell_orders)
1347
- pl_percent = ((sold_value - total_cost) / total_cost) if total_cost > 0 else 0
1348
- else:
1349
- positions = get_current_positions()
1350
- pos = next((p for p in positions if p['symbol'] == symbol), None)
1351
- if pos:
1352
- current_price = pos['current_price']
1353
- current_value = total_bought * current_price
1354
- pl_percent = ((current_value - total_cost) / total_cost) if total_cost > 0 else 0
1355
- else:
1356
- pl_percent = 0
1357
-
1358
- if month_key in monthly_performance:
1359
- monthly_performance[month_key]['investment'] += total_cost
1360
- monthly_performance[month_key]['returns'].append(pl_percent)
1361
-
1362
- output = f"⏰ TIME-BASED PERFORMANCE ANALYSIS\n\n"
1363
-
1364
- for month in sorted(monthly_performance.keys()):
1365
- data = monthly_performance[month]
1366
- if data['returns']:
1367
- avg_return = sum(data['returns']) / len(data['returns']) * 100
1368
- total_investment = data['investment']
1369
- num_trades = len(data['returns'])
1370
-
1371
- output += f"📅 {month}: {avg_return:+.2f}% avg return\n"
1372
- output += f" • {num_trades} trades, ${total_investment:.2f} invested\n"
1373
-
1374
- # Calculate recent vs early performance
1375
- sorted_months = sorted(monthly_performance.keys())
1376
- if len(sorted_months) >= 2:
1377
- early_months = sorted_months[:len(sorted_months)//2]
1378
- recent_months = sorted_months[len(sorted_months)//2:]
1379
-
1380
- early_returns = []
1381
- recent_returns = []
1382
-
1383
- for month in early_months:
1384
- early_returns.extend(monthly_performance[month]['returns'])
1385
-
1386
- for month in recent_months:
1387
- recent_returns.extend(monthly_performance[month]['returns'])
1388
-
1389
- if early_returns and recent_returns:
1390
- early_avg = sum(early_returns) / len(early_returns) * 100
1391
- recent_avg = sum(recent_returns) / len(recent_returns) * 100
1392
-
1393
- output += f"\n📈 TREND ANALYSIS:\n"
1394
- output += f"Early Period Avg: {early_avg:+.2f}% ({len(early_returns)} trades)\n"
1395
- output += f"Recent Period Avg: {recent_avg:+.2f}% ({len(recent_returns)} trades)\n"
1396
- output += f"Improvement: {recent_avg - early_avg:+.2f}% difference\n"
1397
-
1398
- return output
1399
-
1400
- except Exception as e:
1401
- return f"ERROR calculating time analysis: {str(e)}"
1402
-
1403
- # Trading History Backtesting Functions
1404
- def get_pre_investment_news(symbol, investment_time, hours_before=12):
1405
- """Get news from 12 hours before we invested"""
1406
-
1407
- cutoff_time = investment_time - timedelta(minutes=30) # 30 min buffer
1408
- search_start = investment_time - timedelta(hours=hours_before)
1409
-
1410
- logger.info(f"🔍 NEWS SEARCH for {symbol}:")
1411
- logger.info(f" 📅 Time window: {search_start.strftime('%Y-%m-%d %H:%M')} → {cutoff_time.strftime('%Y-%m-%d %H:%M')}")
1412
- logger.info(f" ⏰ Search duration: {hours_before} hours before investment")
1413
-
1414
- all_news = []
1415
-
1416
- # Get Reddit posts
1417
- logger.info(f"🧵 Starting Reddit search for {symbol}...")
1418
- reddit_start = time.time()
1419
- reddit_posts = get_reddit_pre_investment(symbol, search_start, cutoff_time)
1420
- reddit_time = time.time() - reddit_start
1421
- logger.info(f"✅ Reddit search completed in {reddit_time:.1f}s - found {len(reddit_posts)} posts")
1422
- all_news.extend(reddit_posts)
1423
-
1424
- # Get Google News
1425
- logger.info(f"📰 Starting Google News search for {symbol}...")
1426
- news_start = time.time()
1427
- google_news = get_google_news_pre_investment(symbol, search_start, cutoff_time)
1428
- news_time = time.time() - news_start
1429
- logger.info(f"✅ Google News search completed in {news_time:.1f}s - found {len(google_news)} articles")
1430
- all_news.extend(google_news)
1431
-
1432
- logger.info(f"📊 TOTAL NEWS GATHERED for {symbol}: {len(all_news)} items ({len(reddit_posts)} Reddit + {len(google_news)} News)")
1433
- return all_news
1434
-
1435
- def get_reddit_pre_investment(symbol, start_time, cutoff_time):
1436
- """Get Reddit posts from before our investment"""
1437
-
1438
- reddit_posts = []
1439
-
1440
- # Search key subreddits including WSB with multiple search strategies
1441
- subreddits = ['wallstreetbets', 'stocks', 'investing']
1442
- search_terms = [symbol, f'{symbol} stock', f'{symbol} IPO', f'${symbol}']
1443
-
1444
- for subreddit in subreddits:
1445
- for search_term in search_terms:
1446
- try:
1447
- url = f"https://www.reddit.com/r/{subreddit}/search.json"
1448
- params = {
1449
- 'q': search_term,
1450
- 'restrict_sr': 'true',
1451
- 'limit': 5, # Reduced to avoid duplicates
1452
- 't': 'all', # Search all time instead of just week
1453
- 'sort': 'relevance'
1454
- }
1455
-
1456
- response = requests.get(url, params=params, headers=headers, timeout=10)
1457
- if response.status_code == 200:
1458
- data = response.json()
1459
- posts_found = len(data.get('data', {}).get('children', []))
1460
- logger.info(f"Reddit search: r/{subreddit} + '{search_term}' found {posts_found} posts")
1461
-
1462
- for post in data.get('data', {}).get('children', []):
1463
- post_data = post.get('data', {})
1464
-
1465
- if not post_data.get('title'):
1466
- continue
1467
-
1468
- # Check if we already have this post (avoid duplicates)
1469
- title = post_data.get('title', '')
1470
- if any(existing['title'] == title for existing in reddit_posts):
1471
- continue
1472
-
1473
- # Only include posts that actually mention the symbol
1474
- title_text = f"{title} {post_data.get('selftext', '')}".upper()
1475
- if symbol.upper() in title_text or f'${symbol.upper()}' in title_text:
1476
- reddit_post = {
1477
- 'title': title,
1478
- 'selftext': post_data.get('selftext', '')[:300],
1479
- 'score': post_data.get('score', 0),
1480
- 'num_comments': post_data.get('num_comments', 0),
1481
- 'subreddit': subreddit,
1482
- 'source': 'Reddit',
1483
- 'url': f"https://reddit.com{post_data.get('permalink', '')}",
1484
- 'search_term': search_term
1485
- }
1486
- reddit_posts.append(reddit_post)
1487
- logger.info(f"Added Reddit post: {title[:50]}... (score: {post_data.get('score', 0)})")
1488
-
1489
- time.sleep(0.5) # Reduced rate limiting
1490
-
1491
- except Exception as e:
1492
- logger.warning(f"Reddit error for r/{subreddit} + '{search_term}': {e}")
1493
-
1494
- logger.info(f"Total Reddit posts found for {symbol}: {len(reddit_posts)}")
1495
- return reddit_posts
1496
-
1497
- def get_google_news_pre_investment(symbol, start_time, cutoff_time):
1498
- """Get Google News from before our investment"""
1499
-
1500
- google_news = []
1501
-
1502
- try:
1503
- # Search for IPO-related news
1504
- search_queries = [
1505
- f'{symbol} IPO',
1506
- f'{symbol} stock',
1507
- f'{symbol} public offering'
1508
- ]
1509
-
1510
- for query in search_queries:
1511
- url = "https://news.google.com/rss/search"
1512
- params = {
1513
- 'q': query,
1514
- 'hl': 'en-US',
1515
- 'gl': 'US',
1516
- 'ceid': 'US:en'
1517
- }
1518
-
1519
- response = requests.get(url, params=params, headers=headers, timeout=10)
1520
- if response.status_code == 200:
1521
- # Parse RSS
1522
- from xml.etree import ElementTree as ET
1523
- root = ET.fromstring(response.content)
1524
-
1525
- for item in root.findall('.//item')[:5]: # Limit per query
1526
- title_elem = item.find('title')
1527
- link_elem = item.find('link')
1528
- description_elem = item.find('description')
1529
-
1530
- if title_elem is not None:
1531
- description = description_elem.text if description_elem is not None else ""
1532
- # Clean HTML
1533
- import re
1534
- description = re.sub(r'<[^>]+>', '', description)
1535
-
1536
- news_item = {
1537
- 'title': title_elem.text,
1538
- 'description': description,
1539
- 'source': 'Google News',
1540
- 'url': link_elem.text if link_elem is not None else ''
1541
- }
1542
- google_news.append(news_item)
1543
-
1544
- time.sleep(0.5)
1545
-
1546
- except Exception as e:
1547
- logger.warning(f"Google News error: {e}")
1548
-
1549
- return google_news
1550
-
1551
- def analyze_pre_investment_sentiment(news_items):
1552
- """Analyze sentiment from news before our investment"""
1553
-
1554
- if not news_items:
1555
- return 0.0, 0.0, "neutral", {}
1556
-
1557
- sentiments = []
1558
- source_breakdown = {'Reddit': [], 'Google News': []}
1559
-
1560
- for item in news_items:
1561
- # Combine title and description/selftext
1562
- if item['source'] == 'Reddit':
1563
- text = f"{item['title']} {item.get('selftext', '')}"
1564
- else:
1565
- text = f"{item['title']} {item.get('description', '')}"
1566
-
1567
- # Sentiment analysis
1568
- vader_scores = vader.polarity_scores(text)
1569
- blob = TextBlob(text)
1570
- combined_sentiment = (vader_scores['compound'] * 0.6) + (blob.sentiment.polarity * 0.4)
1571
-
1572
- # Weight by engagement for Reddit
1573
- if item['source'] == 'Reddit':
1574
- engagement = item.get('score', 0) + item.get('num_comments', 0)
1575
- weight = min(engagement / 100.0, 2.0) if engagement > 0 else 0.5
1576
- else:
1577
- weight = 1.0
1578
-
1579
- weighted_sentiment = combined_sentiment * weight
1580
- sentiments.append(weighted_sentiment)
1581
-
1582
- # Track by source
1583
- source_breakdown[item['source']].append({
1584
- 'sentiment': weighted_sentiment,
1585
- 'title': item['title'][:80],
1586
- 'weight': weight
1587
- })
1588
-
1589
- # Calculate overall metrics
1590
- avg_sentiment = sum(sentiments) / len(sentiments)
1591
-
1592
- # Convert to predicted change
1593
- predicted_change = avg_sentiment * 25.0
1594
-
1595
- # Add confidence based on source agreement
1596
- reddit_sentiments = [s['sentiment'] for s in source_breakdown['Reddit']]
1597
- news_sentiments = [s['sentiment'] for s in source_breakdown['Google News']]
1598
-
1599
- reddit_avg = sum(reddit_sentiments) / len(reddit_sentiments) if reddit_sentiments else 0
1600
- news_avg = sum(news_sentiments) / len(news_sentiments) if news_sentiments else 0
1601
-
1602
- # Boost prediction if sources agree
1603
- if (reddit_avg > 0 and news_avg > 0) or (reddit_avg < 0 and news_avg < 0):
1604
- predicted_change *= 1.2
1605
-
1606
- # Classify prediction
1607
- if predicted_change >= 5.0:
1608
- prediction_label = "bullish"
1609
- elif predicted_change <= -5.0:
1610
- prediction_label = "bearish"
1611
- else:
1612
- prediction_label = "neutral"
1613
-
1614
- return avg_sentiment, predicted_change, prediction_label, source_breakdown
1615
-
1616
- def get_actual_performance(symbol, investment_time, investment_price):
1617
- """Get actual stock performance after our investment"""
1618
-
1619
- try:
1620
- ticker = yf.Ticker(symbol)
1621
-
1622
- # Get data from investment day
1623
- start_date = investment_time.date()
1624
- end_date = start_date + timedelta(days=5) # Get a few days
1625
-
1626
- hist = ticker.history(start=start_date, end=end_date, interval='1h')
1627
-
1628
- if hist.empty:
1629
- return None, None, None
1630
-
1631
- # Find first hour performance (approximate)
1632
- day_data = hist[hist.index.date == start_date]
1633
 
1634
- if len(day_data) > 0:
1635
- first_price = day_data.iloc[0]['Open']
1636
-
1637
- # First hour high (if we have hourly data)
1638
- if len(day_data) >= 2:
1639
- first_hour_high = day_data.iloc[0:2]['High'].max()
1640
- first_hour_change = ((first_hour_high - first_price) / first_price) * 100
1641
- else:
1642
- # Fall back to first day
1643
- first_day_close = day_data.iloc[-1]['Close']
1644
- first_hour_change = ((first_day_close - first_price) / first_price) * 100
1645
-
1646
- # End of day performance
1647
- end_of_day_close = day_data.iloc[-1]['Close']
1648
- day_change = ((end_of_day_close - first_price) / first_price) * 100
1649
-
1650
- return first_hour_change, day_change, first_price
1651
 
1652
- except Exception as e:
1653
- logger.warning(f"Error getting {symbol} performance: {e}")
1654
 
1655
- return None, None, None
1656
-
1657
- def run_trading_history_backtest():
1658
- """Run backtest on all our actual investments"""
1659
-
1660
- logger.info("Starting trading history backtesting...")
1661
-
1662
- try:
1663
- # Get our trading history
1664
- orders = get_order_history()
1665
-
1666
- if not orders:
1667
- return "❌ No trading history found", pd.DataFrame()
1668
-
1669
- # Get all unique symbols from order history
1670
- symbols_traded = set()
1671
- for order in orders:
1672
- if hasattr(order, 'symbol') and order.symbol and order.side.value == 'buy':
1673
- symbols_traded.add(order.symbol)
1674
-
1675
- logger.info(f"Found {len(symbols_traded)} unique symbols traded")
1676
-
1677
- results = []
1678
- total_error = 0
1679
- correct_directions = 0
1680
- valid_results = 0
1681
-
1682
- summary_text = f"🎯 TRADING HISTORY BACKTESTING\n"
1683
- summary_text += f"Testing sentiment analysis on {len(symbols_traded)} IPOs we actually invested in...\n"
1684
- summary_text += f"Using news from 12 hours before our investment time\n\n"
1685
-
1686
- # Process each symbol that was traded
1687
- for symbol in sorted(symbols_traded):
1688
- # Get all orders for this symbol
1689
- symbol_orders = [o for o in orders if o.symbol == symbol]
1690
- buy_orders = [o for o in symbol_orders if o.side.value == 'buy']
1691
-
1692
- if buy_orders:
1693
- # Get first buy order details
1694
- first_buy_order = min(buy_orders, key=lambda x: x.filled_at)
1695
- investment_time = first_buy_order.filled_at
1696
-
1697
- total_bought = sum(float(o.filled_qty or 0) for o in buy_orders)
1698
- total_cost = sum(float(o.filled_qty or 0) * float(o.filled_avg_price or 0) for o in buy_orders)
1699
- avg_buy_price = total_cost / total_bought if total_bought > 0 else 0
1700
-
1701
- logger.info(f"Analyzing {symbol} (invested {investment_time.strftime('%Y-%m-%d %H:%M')})...")
1702
-
1703
- # Get pre-investment news
1704
- news_items = get_pre_investment_news(symbol, investment_time)
1705
-
1706
- # Analyze sentiment
1707
- avg_sentiment, predicted_change, prediction_label, source_breakdown = analyze_pre_investment_sentiment(news_items)
1708
-
1709
- # Get actual performance
1710
- first_hour_change, day_change, actual_open = get_actual_performance(symbol, investment_time, avg_buy_price)
1711
-
1712
- if first_hour_change is not None:
1713
- # Calculate metrics
1714
- error = abs(predicted_change - first_hour_change)
1715
- total_error += error
1716
- valid_results += 1
1717
-
1718
- # Check direction
1719
- predicted_direction = "UP" if predicted_change > 0 else "DOWN" if predicted_change < 0 else "FLAT"
1720
- actual_direction = "UP" if first_hour_change > 0 else "DOWN" if first_hour_change < 0 else "FLAT"
1721
- direction_correct = predicted_direction == actual_direction
1722
-
1723
- if direction_correct:
1724
- correct_directions += 1
1725
-
1726
- # Show top sources
1727
- reddit_items = source_breakdown['Reddit']
1728
- news_items_found = source_breakdown['Google News']
1729
-
1730
- top_reddit_title = ""
1731
- if reddit_items:
1732
- top_reddit = max(reddit_items, key=lambda x: abs(x['sentiment']))
1733
- top_reddit_title = top_reddit['title']
1734
-
1735
- top_news_title = ""
1736
- if news_items_found:
1737
- top_news = max(news_items_found, key=lambda x: abs(x['sentiment']))
1738
- top_news_title = top_news['title']
1739
-
1740
- result = {
1741
- 'Symbol': symbol,
1742
- 'Investment Date': investment_time.strftime('%Y-%m-%d'),
1743
- 'Investment Price': f"${avg_buy_price:.2f}",
1744
- 'Predicted Change': f"{predicted_change:+.1f}%",
1745
- 'Actual 1H Change': f"{first_hour_change:+.1f}%",
1746
- 'Error': f"{error:.1f}%",
1747
- 'Direction': '✅ Correct' if direction_correct else '❌ Wrong',
1748
- 'Sentiment': prediction_label.title(),
1749
- 'News Sources': len(news_items),
1750
- 'Reddit Posts': len(reddit_items),
1751
- 'Top Reddit': top_reddit_title,
1752
- 'Top News': top_news_title
1753
- }
1754
-
1755
- else:
1756
- result = {
1757
- 'Symbol': symbol,
1758
- 'Investment Date': investment_time.strftime('%Y-%m-%d'),
1759
- 'Investment Price': f"${avg_buy_price:.2f}",
1760
- 'Predicted Change': f"{predicted_change:+.1f}%",
1761
- 'Actual 1H Change': 'N/A',
1762
- 'Error': 'N/A',
1763
- 'Direction': '❓ No Data',
1764
- 'Sentiment': prediction_label.title(),
1765
- 'News Sources': len(news_items),
1766
- 'Reddit Posts': len(source_breakdown['Reddit']),
1767
- 'Top Reddit': '',
1768
- 'Top News': ''
1769
- }
1770
-
1771
- results.append(result)
1772
-
1773
- # Calculate summary statistics
1774
- if valid_results > 0:
1775
- avg_error = total_error / valid_results
1776
- direction_accuracy = (correct_directions / valid_results) * 100
1777
-
1778
- summary_text += f"📈 BACKTESTING RESULTS SUMMARY:\n"
1779
- summary_text += f" Total Investments Tested: {len(results)}\n"
1780
- summary_text += f" Valid Results: {valid_results}\n"
1781
- summary_text += f" Average Error: {avg_error:.1f}%\n"
1782
- summary_text += f" Direction Accuracy: {direction_accuracy:.1f}% ({correct_directions}/{valid_results})\n\n"
1783
-
1784
- if direction_accuracy >= 60:
1785
- summary_text += f" ✅ Strong predictive value!\n"
1786
- elif direction_accuracy >= 40:
1787
- summary_text += f" ⚡ Some predictive value\n"
1788
- else:
1789
- summary_text += f" ❌ Needs improvement\n"
1790
- else:
1791
- summary_text += f"❌ No valid results available for analysis\n"
1792
-
1793
- # Create DataFrame
1794
- df = pd.DataFrame(results)
1795
-
1796
- return summary_text, df
1797
-
1798
- except Exception as e:
1799
- error_msg = f"❌ Error running backtesting: {str(e)}"
1800
- logger.error(error_msg)
1801
- return error_msg, pd.DataFrame()
1802
-
1803
- def clear_terminal():
1804
- """Clear terminal output"""
1805
- return "🖥️ VM Terminal Ready\n$ "
1806
-
1807
- def run_quick_command(cmd):
1808
- """Helper for quick command buttons"""
1809
- def execute(current_output):
1810
- return run_vm_command(cmd, current_output)
1811
- return execute
1812
-
1813
- # Custom CSS for gorgeous design
1814
- custom_css = """
1815
- .gradio-container {
1816
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
1817
- background: #fafafa !important;
1818
- }
1819
-
1820
- .main-header {
1821
- background: linear-gradient(135deg, #0070f3 0%, #0051a5 100%);
1822
- color: white;
1823
- padding: 2rem;
1824
- border-radius: 16px;
1825
- margin-bottom: 2rem;
1826
- box-shadow: 0 10px 40px rgba(0, 112, 243, 0.3);
1827
- }
1828
-
1829
- .metric-card {
1830
- background: white;
1831
- border: 1px solid #eaeaea;
1832
- border-radius: 12px;
1833
- padding: 1.5rem;
1834
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.04);
1835
- transition: all 0.3s ease;
1836
- }
1837
-
1838
- .metric-card:hover {
1839
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
1840
- transform: translateY(-4px);
1841
- }
1842
-
1843
- .gr-button {
1844
- background: linear-gradient(135deg, #0070f3 0%, #0051a5 100%) !important;
1845
- color: white !important;
1846
- border: none !important;
1847
- border-radius: 12px !important;
1848
- font-weight: 600 !important;
1849
- padding: 1rem 2rem !important;
1850
- transition: all 0.3s ease !important;
1851
- box-shadow: 0 4px 16px rgba(0, 112, 243, 0.3) !important;
1852
- }
1853
 
1854
- .gr-button:hover {
1855
- transform: translateY(-2px) !important;
1856
- box-shadow: 0 8px 32px rgba(0, 112, 243, 0.4) !important;
1857
- }
1858
-
1859
- .gr-textbox, .gr-dataframe {
1860
- border: 1px solid #eaeaea !important;
1861
- border-radius: 12px !important;
1862
- background: white !important;
1863
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04) !important;
1864
- }
1865
-
1866
- .plotly-graph-div {
1867
- border-radius: 16px !important;
1868
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08) !important;
1869
- background: white !important;
1870
- }
1871
-
1872
- .status-invested { color: #00d647 !important; font-weight: 600 !important; }
1873
- .status-eligible { color: #f5a623 !important; font-weight: 600 !important; }
1874
- .status-wrong { color: #8b949e !important; }
1875
- .status-unknown { color: #ff0080 !important; }
1876
-
1877
- /* Investment Performance Table Styling */
1878
- .investment-table {
1879
- width: 100%;
1880
- border-collapse: separate;
1881
- border-spacing: 0;
1882
- background: white;
1883
- border-radius: 16px;
1884
- overflow: hidden;
1885
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
1886
- }
1887
-
1888
- .investment-table th {
1889
- background: linear-gradient(135deg, #0070f3 0%, #0051a5 100%);
1890
- color: white;
1891
- padding: 1rem;
1892
- font-weight: 600;
1893
- text-align: left;
1894
- border: none;
1895
- }
1896
-
1897
- .investment-table th:first-child {
1898
- border-top-left-radius: 16px;
1899
- }
1900
-
1901
- .investment-table th:last-child {
1902
- border-top-right-radius: 16px;
1903
- }
1904
-
1905
- .investment-table td {
1906
- padding: 1rem;
1907
- border-bottom: 1px solid #f5f5f5;
1908
- font-weight: 500;
1909
- }
1910
-
1911
- .profit-row {
1912
- background: rgba(0, 214, 71, 0.1) !important;
1913
- border-left: 4px solid #00d647;
1914
- }
1915
-
1916
- .loss-row {
1917
- background: rgba(255, 0, 128, 0.1) !important;
1918
- border-left: 4px solid #ff0080;
1919
- }
1920
-
1921
- .neutral-row {
1922
- background: rgba(139, 148, 158, 0.05) !important;
1923
- border-left: 4px solid #8b949e;
1924
- }
1925
-
1926
- .investment-table tr:last-child td:first-child {
1927
- border-bottom-left-radius: 16px;
1928
- }
1929
-
1930
- .investment-table tr:last-child td:last-child {
1931
- border-bottom-right-radius: 16px;
1932
- }
1933
-
1934
- .profit-positive { color: #00d647 !important; font-weight: 600 !important; }
1935
- .profit-negative { color: #ff0080 !important; font-weight: 600 !important; }
1936
- .profit-neutral { color: #8b949e !important; }
1937
-
1938
- .terminal-container {
1939
- background: #000000 !important;
1940
- border: 1px solid #333 !important;
1941
- border-radius: 8px !important;
1942
- padding: 0 !important;
1943
- margin: 1rem 0 !important;
1944
- height: 500px !important;
1945
- overflow-y: auto !important;
1946
- display: flex !important;
1947
- flex-direction: column !important;
1948
- /* Hide scrollbars but keep functionality */
1949
- scrollbar-width: none !important; /* Firefox */
1950
- -ms-overflow-style: none !important; /* IE/Edge */
1951
- }
1952
-
1953
- .terminal-container::-webkit-scrollbar {
1954
- display: none !important; /* Chrome/Safari/Webkit */
1955
- }
1956
-
1957
- .terminal-display {
1958
- font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace !important;
1959
- background: #000000 !important;
1960
- color: #ffffff !important;
1961
- padding: 1rem !important;
1962
- font-size: 14px !important;
1963
- line-height: 1.4 !important;
1964
- white-space: pre-wrap !important;
1965
- word-wrap: break-word !important;
1966
- margin: 0 !important;
1967
- flex-grow: 1 !important;
1968
- overflow-anchor: none !important;
1969
- /* Always stick to bottom */
1970
- display: flex !important;
1971
- flex-direction: column !important;
1972
- justify-content: flex-end !important;
1973
- }
1974
-
1975
- .terminal-display::-webkit-scrollbar {
1976
- display: none !important; /* Chrome/Safari/Webkit */
1977
- }
1978
-
1979
- .terminal-input input {
1980
- font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace !important;
1981
- background: #1a1a1a !important;
1982
- color: #ffffff !important;
1983
- border: 1px solid #333 !important;
1984
- border-radius: 4px !important;
1985
- font-size: 14px !important;
1986
- }
1987
-
1988
- .terminal-input input:focus {
1989
- border-color: #00ff00 !important;
1990
- box-shadow: 0 0 5px rgba(0, 255, 0, 0.3) !important;
1991
- }
1992
-
1993
- /* Force Gradio HTML to stick to bottom */
1994
- .gr-html {
1995
- height: 500px !important;
1996
- overflow-y: auto !important;
1997
- scrollbar-width: none !important;
1998
- -ms-overflow-style: none !important;
1999
- display: flex !important;
2000
- flex-direction: column !important;
2001
- }
2002
-
2003
- .gr-html::-webkit-scrollbar {
2004
- display: none !important;
2005
- }
2006
-
2007
- /* Force content to bottom with CSS anchor */
2008
- .gr-html > div {
2009
- display: flex !important;
2010
- flex-direction: column !important;
2011
- justify-content: flex-end !important;
2012
- min-height: 100% !important;
2013
- }
2014
- """
2015
-
2016
- def create_dashboard():
2017
- logger.info("🎨 Creating Gradio dashboard interface...")
2018
 
2019
  try:
2020
- with gr.Blocks(
2021
- title="🚀 Premium Trading Dashboard",
2022
- theme=gr.themes.Soft(primary_hue="blue"),
2023
- css=custom_css
2024
- ) as demo:
2025
- logger.info("🖼️ Dashboard blocks created successfully")
2026
-
2027
- # Header
2028
- gr.HTML("""
2029
- <div class="main-header">
2030
- <h1 style="margin: 0; font-size: 3rem; font-weight: 800; text-shadow: 0 2px 4px rgba(0,0,0,0.1);">
2031
- 🚀 Premium Trading Dashboard
2032
- </h1>
2033
- <p style="margin: 1rem 0 0 0; font-size: 1.3rem; opacity: 0.95;">
2034
- Real-time portfolio monitoring with IPO discovery analytics
2035
- </p>
2036
- </div>
2037
- """)
2038
-
2039
- with gr.Tabs():
2040
- # Portfolio Overview Tab
2041
- with gr.Tab("📊 Portfolio Overview"):
2042
- gr.Markdown("## 💼 Account Summary")
2043
- with gr.Row():
2044
- portfolio_value = gr.Textbox(label="💰 Portfolio Value", interactive=False)
2045
- buying_power = gr.Textbox(label="💳 Buying Power", interactive=False)
2046
- cash = gr.Textbox(label="💵 Cash", interactive=False)
2047
- day_change = gr.Textbox(label="📈 Day Change", interactive=False)
2048
- equity = gr.Textbox(label="🏦 Total Equity", interactive=False)
2049
-
2050
- gr.Markdown("## 📈 Portfolio Performance")
2051
- portfolio_chart = gr.Plot(label="Portfolio Value Over Time")
2052
-
2053
- refresh_overview_btn = gr.Button("🔄 Refresh Portfolio Data", variant="primary", size="lg")
2054
-
2055
- # IPO Discoveries Tab
2056
- with gr.Tab("🔍 IPO Discoveries"):
2057
- gr.Markdown("## 📊 IPO Discovery Analytics")
2058
-
2059
- with gr.Row():
2060
- total_ipos = gr.Textbox(label="🎯 Total IPOs Detected", interactive=False)
2061
- ipos_invested = gr.Textbox(label="💰 IPOs Invested", interactive=False)
2062
- cs_stocks = gr.Textbox(label="📈 CS Stocks Found", interactive=False)
2063
- investment_rate = gr.Textbox(label="🎲 Investment Rate", interactive=False)
2064
- last_updated = gr.Textbox(label="🕒 Last Updated", interactive=False)
2065
-
2066
- with gr.Row():
2067
- with gr.Column(scale=1):
2068
- ipo_chart = gr.Plot(label="Investment Decision Breakdown")
2069
-
2070
- with gr.Column(scale=2):
2071
- gr.Markdown("## 🆕 Recent IPO Discoveries")
2072
- ipo_table = gr.Dataframe(
2073
- label="IPO Discoveries with Investment Decisions",
2074
- )
2075
-
2076
- refresh_ipo_btn = gr.Button("🔄 Refresh IPO Data", variant="primary", size="lg")
2077
-
2078
- # Investment Performance Tab
2079
- with gr.Tab("💰 Investment Performance"):
2080
- gr.Markdown("## 🎯 IPO Investment Performance")
2081
- gr.Markdown("### Track profit/loss on your IPO investments with **real-time sentiment analysis**")
2082
- gr.Markdown("🧠 **NEW**: Each row automatically shows sentiment predictions from Reddit + Google News!")
2083
-
2084
- investment_performance_table = gr.HTML(
2085
- label="IPO Investment P&L Analysis",
2086
- value="<div style='text-align: center; padding: 2rem; color: #666;'>Click Refresh to load investment performance data</div>"
2087
- )
2088
- refresh_investment_btn = gr.Button("🔄 Refresh Investment Performance", variant="primary", size="lg")
2089
-
2090
- gr.Markdown("### 🧮 Trading Statistics & Analysis")
2091
- gr.Markdown("Calculate interesting metrics from your trading data")
2092
-
2093
- with gr.Row():
2094
- calc_sequential_btn = gr.Button("📈 Sequential Reinvestment P&L%", variant="secondary", size="sm")
2095
- calc_equal_weight_btn = gr.Button("⚖️ Equal Weight Portfolio P&L%", variant="secondary", size="sm")
2096
- calc_best_worst_btn = gr.Button("🏆 Best vs Worst Performers", variant="secondary", size="sm")
2097
-
2098
- with gr.Row():
2099
- calc_win_rate_btn = gr.Button("🎯 Win Rate & Avg Returns", variant="secondary", size="sm")
2100
- calc_risk_metrics_btn = gr.Button("⚠️ Risk Metrics & Volatility", variant="secondary", size="sm")
2101
- calc_time_analysis_btn = gr.Button("⏰ Time-based Performance", variant="secondary", size="sm")
2102
-
2103
- stats_output = gr.Textbox(
2104
- label="Statistical Analysis Results",
2105
- lines=8,
2106
- interactive=False,
2107
- )
2108
-
2109
- gr.Markdown("### 🔧 Debug API Calls")
2110
- debug_output = gr.Textbox(
2111
- label="Debug Output",
2112
- lines=10,
2113
- interactive=False
2114
- )
2115
-
2116
- with gr.Row():
2117
- debug_orders_btn = gr.Button("🔍 Debug Order History", variant="secondary")
2118
- debug_positions_btn = gr.Button("📊 Debug Current Positions", variant="secondary")
2119
- debug_ipos_btn = gr.Button("🎯 Debug IPO Data", variant="secondary")
2120
- debug_account_btn = gr.Button("💼 Debug Account Info", variant="secondary")
2121
-
2122
- # VM Terminal Tab
2123
- with gr.Tab("💻 VM Terminal"):
2124
- gr.Markdown("## 🖥️ Remote VM Terminal")
2125
- gr.Markdown("### Execute commands directly on your trading VM")
2126
-
2127
- # Hidden state for command history
2128
- command_history = gr.State("")
2129
-
2130
- with gr.Row():
2131
- with gr.Column(scale=4):
2132
- command_input = gr.Textbox(
2133
- label="Command (Press Enter to run)",
2134
- placeholder="Enter command to run on VM...",
2135
- interactive=True,
2136
- )
2137
- with gr.Column(scale=1):
2138
- run_command_btn = gr.Button("▶️ Run", variant="primary", size="lg")
2139
- clear_terminal_btn = gr.Button("🗑️ Clear", variant="secondary", size="lg")
2140
-
2141
- terminal_output = gr.HTML(
2142
- label="Terminal Output",
2143
- value='<div class="terminal-display" id="terminal-content">🖥️ VM Terminal Ready<br>$ </div>',
2144
- )
2145
-
2146
- gr.Markdown("**📁 File & System Commands:**")
2147
- with gr.Row():
2148
- quick_ls = gr.Button("📁 ls -la", size="sm")
2149
- quick_pwd = gr.Button("📍 pwd", size="sm")
2150
- quick_ps = gr.Button("🔄 ps aux | grep python", size="sm")
2151
- quick_vm_status = gr.Button("🖥️ uptime && df -h", size="sm")
2152
- quick_who = gr.Button("👤 whoami", size="sm")
2153
-
2154
- gr.Markdown("**📋 Log Files:**")
2155
- with gr.Row():
2156
- quick_script_log = gr.Button("📜 tail -50 script.log", size="sm")
2157
- quick_server_log = gr.Button("🖥️ tail -50 server.log", size="sm")
2158
- quick_cron_log = gr.Button("⏰ tail -50 /var/log/cron", size="sm")
2159
- quick_portfolio = gr.Button("💼 cat portfolio.txt", size="sm")
2160
- quick_tickers = gr.Button("🎯 head -20 new_tickers_log.csv", size="sm")
2161
-
2162
- gr.Markdown("**🔍 Search & Analysis:**")
2163
- with gr.Row():
2164
- quick_errors = gr.Button("🚨 grep -i error script.log | tail -10", size="sm")
2165
- quick_trades = gr.Button("💰 grep -i 'buy\\|sell' script.log | tail -10", size="sm")
2166
- quick_ipos = gr.Button("🆕 grep -i 'new ticker' script.log | tail -10", size="sm")
2167
-
2168
- # System Logs Tab
2169
- with gr.Tab("📋 System Logs"):
2170
- gr.Markdown("## 🖥️ Trading Bot Activity")
2171
-
2172
- with gr.Row():
2173
- with gr.Column():
2174
- gr.Markdown("### 🎯 Parsed Logs (Color Coded)")
2175
- system_logs = gr.Textbox(
2176
- label="Recent System Activity",
2177
- lines=20,
2178
- max_lines=20,
2179
- interactive=False,
2180
- )
2181
-
2182
- with gr.Column():
2183
- gr.Markdown("### 📄 Raw Cron Logs")
2184
- raw_logs = gr.Textbox(
2185
- label="Raw Log Output",
2186
- lines=20,
2187
- max_lines=20,
2188
- interactive=False,
2189
- )
2190
-
2191
- refresh_logs_btn = gr.Button("🔄 Refresh All Logs", variant="primary", size="lg")
2192
 
2193
- # Footer
2194
- gr.HTML("""
2195
- <div style="text-align: center; padding: 2rem; color: #666; border-top: 1px solid #eaeaea; margin-top: 3rem; background: white; border-radius: 16px;">
2196
- <p style="font-size: 1.1rem;"><strong>🤖 Automated Trading Dashboard</strong></p>
2197
- <p style="font-size: 0.95rem;">Real-time data from Alpaca Markets + VM Analytics | Built with ❤️</p>
2198
- </div>
2199
- """)
2200
-
2201
- logger.info("🔗 Setting up event handlers...")
2202
-
2203
- # Event Handlers - INSIDE Blocks context
2204
-
2205
- # Portfolio tab
2206
- refresh_overview_btn.click(
2207
- fn=refresh_account_overview,
2208
- outputs=[portfolio_value, buying_power, cash, day_change, equity]
2209
- )
2210
- refresh_overview_btn.click(
2211
- fn=create_portfolio_chart,
2212
- outputs=[portfolio_chart]
2213
- )
2214
-
2215
- # IPO tab
2216
- refresh_ipo_btn.click(
2217
- fn=refresh_vm_stats,
2218
- outputs=[total_ipos, ipos_invested, cs_stocks, investment_rate, last_updated]
2219
- )
2220
- refresh_ipo_btn.click(
2221
- fn=create_ipo_discovery_chart,
2222
- outputs=[ipo_chart]
2223
- )
2224
- refresh_ipo_btn.click(
2225
- fn=refresh_ipo_discoveries_table,
2226
- outputs=[ipo_table]
2227
- )
2228
-
2229
- # Investment Performance tab
2230
- refresh_performance_btn.click(
2231
- fn=refresh_investment_performance_html,
2232
- outputs=[investment_performance_table]
2233
- )
2234
-
2235
- # VM Terminal tab
2236
- execute_btn.click(
2237
- fn=execute_command,
2238
- inputs=[command_input],
2239
- outputs=[terminal_output]
2240
- )
2241
-
2242
- # System Logs tab
2243
- refresh_logs_btn.click(
2244
- fn=refresh_vm_logs,
2245
- outputs=[log_output]
2246
- )
2247
-
2248
- # Initial data load
2249
- demo.load(
2250
- fn=refresh_account_overview,
2251
- outputs=[portfolio_value, buying_power, cash, day_change, equity]
2252
- )
2253
- demo.load(fn=create_portfolio_chart, outputs=[portfolio_chart])
2254
- demo.load(
2255
- fn=refresh_vm_stats,
2256
- outputs=[total_ipos, ipos_invested, cs_stocks, investment_rate, last_updated]
2257
- )
2258
- demo.load(fn=create_ipo_discovery_chart, outputs=[ipo_chart])
2259
- demo.load(fn=refresh_ipo_discoveries_table, outputs=[ipo_table])
2260
- demo.load(fn=refresh_investment_performance_html, outputs=[investment_performance_table])
2261
-
2262
- demo.queue()
2263
- logger.info("✅ All event handlers configured successfully")
2264
- return demo
2265
-
2266
- except Exception as e:
2267
- logger.error(f"❌ Failed to create dashboard: {e}")
2268
- raise
2269
-
2270
- # Create and launch
2271
- logger.info("🏗️ Building dashboard...")
2272
- try:
2273
- demo = create_dashboard()
2274
- logger.info("✅ Dashboard created successfully!")
2275
- except Exception as e:
2276
- logger.error(f"❌ Dashboard creation failed: {e}")
2277
- raise
2278
-
2279
- if __name__ == "__main__":
2280
- logger.info("🚀 Launching dashboard server...")
2281
- try:
2282
  demo.launch()
2283
  logger.info("✅ Dashboard launched successfully!")
 
2284
  except Exception as e:
2285
- logger.error(f"❌ Dashboard launch failed: {e}")
2286
  raise
 
1
  #!/usr/bin/env python3
2
  """
3
+ Minimal working Gradio app to test deployment
 
4
  """
5
 
 
 
 
6
  import gradio as gr
 
 
 
7
  import logging
 
 
 
 
 
 
 
 
 
 
8
 
9
+ # Configure logging
10
+ logging.basicConfig(level=logging.INFO)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  logger = logging.getLogger(__name__)
12
 
13
+ def greet(name):
14
+ """Simple greeting function"""
15
+ logger.info(f"Greeting user: {name}")
16
+ return f"Hello {name}! 🚀 Trading Dashboard works!"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
+ def refresh_data():
19
+ """Simple refresh function"""
20
+ logger.info("Refreshing data...")
21
+ return "Data refreshed! ✅"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
+ def create_minimal_dashboard():
24
+ """Create minimal dashboard to test Gradio"""
 
25
 
26
+ logger.info("🚀 Creating minimal dashboard...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
+ with gr.Blocks(title="Test Dashboard") as demo:
29
+ logger.info("🎨 Inside Blocks context...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
 
31
+ gr.Markdown("# 🧪 Test Trading Dashboard")
 
 
32
 
33
+ with gr.Row():
34
+ name_input = gr.Textbox(label="Your Name", placeholder="Enter your name")
35
+ greeting_output = gr.Textbox(label="Greeting", interactive=False)
 
 
 
 
 
36
 
37
+ with gr.Row():
38
+ greet_btn = gr.Button("👋 Greet", variant="primary")
39
+ refresh_btn = gr.Button("🔄 Refresh", variant="secondary")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
+ status_output = gr.Textbox(label="Status", interactive=False)
 
 
 
 
 
 
42
 
43
+ # Event handlers - INSIDE the Blocks context
44
+ logger.info("🔗 Setting up event handlers...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
 
46
+ greet_btn.click(
47
+ fn=greet,
48
+ inputs=[name_input],
49
+ outputs=[greeting_output]
 
50
  )
51
 
52
+ refresh_btn.click(
53
+ fn=refresh_data,
54
+ outputs=[status_output]
55
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
 
57
+ # Initial load
58
+ demo.load(
59
+ fn=lambda: "Ready to test! 🎯",
60
+ outputs=[status_output]
61
+ )
 
 
 
 
 
 
 
 
 
 
 
 
62
 
63
+ demo.queue()
64
+ logger.info(" Event handlers configured successfully")
65
 
66
+ logger.info("✅ Dashboard created successfully")
67
+ return demo
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
+ if __name__ == "__main__":
70
+ logger.info("🚀 Starting minimal test dashboard...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
 
72
  try:
73
+ demo = create_minimal_dashboard()
74
+ logger.info(" Dashboard created successfully!")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
 
76
+ logger.info("🚀 Launching dashboard server...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  demo.launch()
78
  logger.info("✅ Dashboard launched successfully!")
79
+
80
  except Exception as e:
81
+ logger.error(f"❌ Dashboard failed: {e}")
82
  raise
app_full.py ADDED
@@ -0,0 +1,2286 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Premium Trading Dashboard - Full Featured
4
+ Beautiful Vercel-style dashboard with VM data integration
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ import pandas as pd
10
+ import gradio as gr
11
+ import plotly.graph_objects as go
12
+ import plotly.express as px
13
+ from datetime import datetime, timedelta, timezone
14
+ import logging
15
+ import requests
16
+ import time
17
+ from alpaca.trading.client import TradingClient
18
+ from alpaca.trading.requests import GetOrdersRequest, GetPortfolioHistoryRequest
19
+ from alpaca.trading.enums import OrderStatus
20
+ from alpaca.data.timeframe import TimeFrame
21
+ from alpaca.data.historical import StockHistoricalDataClient
22
+ from textblob import TextBlob
23
+ from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer
24
+ import nltk
25
+
26
+ # Import yfinance with fallback
27
+ try:
28
+ import yfinance as yf
29
+ YF_AVAILABLE = True
30
+ except ImportError as e:
31
+ print(f"Warning: yfinance not available: {e}")
32
+ YF_AVAILABLE = False
33
+
34
+ # Get API keys and VM URL from environment variables
35
+ API_KEY = os.getenv('ALPACA_API_KEY', 'PK2FD9B2S86LHR7ZBHG1')
36
+ SECRET_KEY = os.getenv('ALPACA_SECRET_KEY', 'QPmGPDgbPArvHv6cldBXc7uWddapYcIAnBhtkuBW')
37
+ VM_API_URL = os.getenv('VM_API_URL', 'http://34.56.193.18:8090') # Set this in Hugging Face
38
+
39
+ # Configure detailed logging for debugging
40
+ logging.basicConfig(
41
+ level=logging.INFO,
42
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
43
+ handlers=[
44
+ logging.StreamHandler(),
45
+ ]
46
+ )
47
+ logger = logging.getLogger(__name__)
48
+
49
+ # Log startup information
50
+ logger.info("🚀 Starting Premium Trading Dashboard... (Build: 2025-07-29 05:15 - Fixed directory structure)")
51
+ logger.info(f"Python version: {sys.version}")
52
+ logger.info(f"Working directory: {os.getcwd()}")
53
+
54
+ # Download required NLTK data
55
+ logger.info("📚 Downloading NLTK data...")
56
+ try:
57
+ nltk.download('punkt', quiet=True)
58
+ nltk.download('vader_lexicon', quiet=True)
59
+ nltk.download('brown', quiet=True)
60
+ logger.info("✅ NLTK data downloaded successfully")
61
+ except Exception as e:
62
+ logger.warning(f"⚠️ NLTK download warning: {e}")
63
+
64
+ # Initialize Alpaca clients
65
+ logger.info("🔌 Initializing Alpaca trading client...")
66
+ try:
67
+ trading_client = TradingClient(api_key=API_KEY, secret_key=SECRET_KEY)
68
+ logger.info("✅ Alpaca trading client initialized successfully")
69
+ except Exception as e:
70
+ logger.error(f"❌ Failed to initialize Alpaca trading client: {e}")
71
+ raise
72
+
73
+ logger.info("📊 Initializing Alpaca data client...")
74
+ try:
75
+ data_client = StockHistoricalDataClient(API_KEY, SECRET_KEY)
76
+ logger.info("✅ Alpaca data client initialized successfully")
77
+ except Exception as e:
78
+ logger.error(f"❌ Failed to initialize Alpaca data client: {e}")
79
+ raise
80
+
81
+ # Initialize sentiment analyzers
82
+ logger.info("🧠 Initializing sentiment analysis engines...")
83
+ try:
84
+ vader = SentimentIntensityAnalyzer()
85
+ logger.info("✅ VADER sentiment analyzer initialized")
86
+ except Exception as e:
87
+ logger.error(f"❌ Failed to initialize VADER: {e}")
88
+ raise
89
+
90
+ try:
91
+ from textblob import TextBlob
92
+ # Test TextBlob
93
+ test_blob = TextBlob("test")
94
+ logger.info("✅ TextBlob sentiment analyzer initialized")
95
+ except Exception as e:
96
+ logger.error(f"❌ Failed to initialize TextBlob: {e}")
97
+ raise
98
+
99
+ headers = {'User-Agent': 'TradingHistoryBacktester/1.0'}
100
+ logger.info("✅ HTTP headers configured")
101
+
102
+ # Modern color scheme
103
+ COLORS = {
104
+ 'primary': '#0070f3',
105
+ 'success': '#00d647',
106
+ 'error': '#ff0080',
107
+ 'warning': '#f5a623',
108
+ 'neutral': '#8b949e',
109
+ 'background': '#fafafa',
110
+ 'surface': '#ffffff',
111
+ 'text': '#000000',
112
+ 'text_secondary': '#666666',
113
+ 'border': '#eaeaea'
114
+ }
115
+
116
+ def fetch_from_vm(endpoint, default_value=None):
117
+ """Fetch data from VM API server"""
118
+ try:
119
+ response = requests.get(f"{VM_API_URL}/api/{endpoint}", timeout=10)
120
+ if response.status_code == 200:
121
+ return response.json()
122
+ else:
123
+ logger.warning(f"VM API {endpoint} returned {response.status_code}")
124
+ return default_value
125
+ except Exception as e:
126
+ logger.error(f"Error fetching from VM {endpoint}: {e}")
127
+ return default_value
128
+
129
+ def get_account_info():
130
+ """Get current account information from Alpaca"""
131
+ try:
132
+ account = trading_client.get_account()
133
+ return {
134
+ 'portfolio_value': float(account.portfolio_value),
135
+ 'buying_power': float(account.buying_power),
136
+ 'cash': float(account.cash),
137
+ 'equity': float(account.equity),
138
+ 'day_change': float(getattr(account, 'unrealized_pl', 0)) if hasattr(account, 'unrealized_pl') else 0,
139
+ 'day_change_percent': float(getattr(account, 'unrealized_plpc', 0)) * 100 if hasattr(account, 'unrealized_plpc') else 0,
140
+ 'last_equity': float(account.last_equity) if account.last_equity else 0
141
+ }
142
+ except Exception as e:
143
+ logger.error(f"Error fetching account info: {e}")
144
+ return {
145
+ 'portfolio_value': 0, 'buying_power': 0, 'cash': 0, 'equity': 0,
146
+ 'day_change': 0, 'day_change_percent': 0, 'last_equity': 0
147
+ }
148
+
149
+ def get_portfolio_history():
150
+ """Get portfolio value history from Alpaca"""
151
+ try:
152
+ portfolio_history_request = GetPortfolioHistoryRequest(
153
+ period="1M",
154
+ timeframe="1D",
155
+ extended_hours=False
156
+ )
157
+
158
+ portfolio_history = trading_client.get_portfolio_history(portfolio_history_request)
159
+
160
+ timestamps = [datetime.fromtimestamp(ts, tz=timezone.utc) for ts in portfolio_history.timestamp]
161
+ equity_values = portfolio_history.equity
162
+
163
+ df = pd.DataFrame({
164
+ 'timestamp': timestamps,
165
+ 'equity': equity_values
166
+ })
167
+
168
+ return df.dropna()
169
+ except Exception as e:
170
+ logger.error(f"Error fetching portfolio history: {e}")
171
+ return pd.DataFrame()
172
+
173
+ def get_current_positions():
174
+ """Get current positions"""
175
+ try:
176
+ positions = trading_client.get_all_positions()
177
+ position_data = []
178
+
179
+ for position in positions:
180
+ position_data.append({
181
+ 'symbol': position.symbol,
182
+ 'qty': float(position.qty),
183
+ 'market_value': float(position.market_value),
184
+ 'cost_basis': float(position.cost_basis),
185
+ 'unrealized_pl': float(position.unrealized_pl),
186
+ 'unrealized_plpc': float(position.unrealized_plpc) * 100,
187
+ 'current_price': float(position.current_price) if position.current_price else 0
188
+ })
189
+
190
+ return position_data
191
+ except Exception as e:
192
+ logger.error(f"Error fetching positions: {e}")
193
+ return []
194
+
195
+ def create_portfolio_chart():
196
+ """Create beautiful portfolio value chart"""
197
+ portfolio_df = get_portfolio_history()
198
+
199
+ if portfolio_df.empty:
200
+ fig = go.Figure()
201
+ fig.add_annotation(
202
+ text="No portfolio history available",
203
+ x=0.5, y=0.5,
204
+ xref="paper", yref="paper",
205
+ showarrow=False,
206
+ font=dict(size=16, color=COLORS['text_secondary'])
207
+ )
208
+ else:
209
+ fig = go.Figure()
210
+
211
+ fig.add_trace(go.Scatter(
212
+ x=portfolio_df['timestamp'],
213
+ y=portfolio_df['equity'],
214
+ mode='lines',
215
+ name='Portfolio Value',
216
+ line=dict(color=COLORS['primary'], width=3),
217
+ fill='tonexty',
218
+ fillcolor=f"rgba(0, 112, 243, 0.1)",
219
+ hovertemplate='<b>%{y:$,.2f}</b><br>%{x}<extra></extra>'
220
+ ))
221
+
222
+ if len(portfolio_df) > 0:
223
+ current_value = portfolio_df['equity'].iloc[-1]
224
+ fig.add_annotation(
225
+ x=portfolio_df['timestamp'].iloc[-1],
226
+ y=current_value,
227
+ text=f"${current_value:,.2f}",
228
+ showarrow=True,
229
+ arrowhead=2,
230
+ arrowcolor=COLORS['primary'],
231
+ bgcolor="white",
232
+ bordercolor=COLORS['primary'],
233
+ borderwidth=2,
234
+ font=dict(size=12, color=COLORS['text'])
235
+ )
236
+
237
+ fig.update_layout(
238
+ title=dict(
239
+ text="Portfolio Value (Last 30 Days)",
240
+ font=dict(size=24, color=COLORS['text'], family="Inter"),
241
+ x=0.02
242
+ ),
243
+ xaxis=dict(
244
+ title="Date",
245
+ showgrid=True,
246
+ gridcolor=COLORS['border'],
247
+ color=COLORS['text_secondary']
248
+ ),
249
+ yaxis=dict(
250
+ title="Portfolio Value ($)",
251
+ showgrid=True,
252
+ gridcolor=COLORS['border'],
253
+ color=COLORS['text_secondary'],
254
+ tickformat='$,.0f'
255
+ ),
256
+ plot_bgcolor='white',
257
+ paper_bgcolor='white',
258
+ height=400,
259
+ margin=dict(l=60, r=40, t=60, b=60),
260
+ hovermode='x unified',
261
+ showlegend=False
262
+ )
263
+
264
+ return fig
265
+
266
+ def create_ipo_discovery_chart():
267
+ """Create IPO discovery chart with investment decisions"""
268
+ ipos = fetch_from_vm('ipos?limit=100', [])
269
+
270
+ if not ipos:
271
+ fig = go.Figure()
272
+ fig.add_annotation(
273
+ text="No IPO data available from VM",
274
+ x=0.5, y=0.5,
275
+ xref="paper", yref="paper",
276
+ showarrow=False,
277
+ font=dict(size=16, color=COLORS['text_secondary'])
278
+ )
279
+ else:
280
+ # Count by status
281
+ status_counts = {}
282
+ for ipo in ipos:
283
+ status = ipo.get('investment_status', 'UNKNOWN')
284
+ status_counts[status] = status_counts.get(status, 0) + 1
285
+
286
+ # Create pie chart
287
+ labels = list(status_counts.keys())
288
+ values = list(status_counts.values())
289
+
290
+ # Map status to colors
291
+ color_map = {
292
+ 'INVESTED': COLORS['success'],
293
+ 'ELIGIBLE_NOT_INVESTED': COLORS['warning'],
294
+ 'WRONG_TYPE': COLORS['neutral'],
295
+ 'UNKNOWN': COLORS['error']
296
+ }
297
+ colors = [color_map.get(label, COLORS['neutral']) for label in labels]
298
+
299
+ fig = go.Figure(data=[go.Pie(
300
+ labels=labels,
301
+ values=values,
302
+ hole=0.4,
303
+ marker=dict(colors=colors),
304
+ textinfo='label+percent',
305
+ textposition='outside'
306
+ )])
307
+
308
+ fig.update_layout(
309
+ title=dict(
310
+ text="IPO Investment Decisions",
311
+ font=dict(size=24, color=COLORS['text'], family="Inter"),
312
+ x=0.5
313
+ ),
314
+ plot_bgcolor='white',
315
+ paper_bgcolor='white',
316
+ height=400,
317
+ margin=dict(l=60, r=60, t=60, b=60),
318
+ showlegend=True
319
+ )
320
+
321
+ return fig
322
+
323
+ def refresh_account_overview():
324
+ """Refresh account overview display"""
325
+ account = get_account_info()
326
+
327
+ portfolio_value = f"${account['portfolio_value']:,.2f}"
328
+ buying_power = f"${account['buying_power']:,.2f}"
329
+ cash = f"${account['cash']:,.2f}"
330
+
331
+ day_change_value = account['day_change']
332
+ day_change_percent = account['day_change_percent']
333
+ if day_change_value > 0:
334
+ day_change = f"↗️ +${day_change_value:,.2f} (+{day_change_percent:.2f}%)"
335
+ elif day_change_value < 0:
336
+ day_change = f"↘️ ${day_change_value:,.2f} ({day_change_percent:.2f}%)"
337
+ else:
338
+ day_change = f"➡️ ${day_change_value:,.2f} ({day_change_percent:.2f}%)"
339
+
340
+ equity = f"${account['equity']:,.2f}"
341
+
342
+ return portfolio_value, buying_power, cash, day_change, equity
343
+
344
+ def refresh_positions_table():
345
+ """Refresh current positions table"""
346
+ positions = get_current_positions()
347
+ if not positions:
348
+ return pd.DataFrame(columns=['Symbol', 'Quantity', 'Market Value', 'Unrealized P&L', 'Unrealized %'])
349
+
350
+ df_data = []
351
+ for pos in positions:
352
+ pnl_indicator = "🟢" if pos['unrealized_pl'] > 0 else "🔴" if pos['unrealized_pl'] < 0 else "⚪"
353
+ df_data.append({
354
+ 'Symbol': f"{pnl_indicator} {pos['symbol']}",
355
+ 'Quantity': f"{pos['qty']:.0f}",
356
+ 'Market Value': f"${pos['market_value']:,.2f}",
357
+ 'Unrealized P&L': f"${pos['unrealized_pl']:,.2f}",
358
+ 'Unrealized %': f"{pos['unrealized_plpc']:.2f}%"
359
+ })
360
+
361
+ return pd.DataFrame(df_data)
362
+
363
+ def refresh_ipo_discoveries_table():
364
+ """Refresh IPO discoveries table with investment decisions"""
365
+ ipos = fetch_from_vm('ipos?limit=100', [])
366
+
367
+ if not ipos:
368
+ return pd.DataFrame(columns=['Status', 'Symbol', 'Security Type', 'Price', 'Detected At'])
369
+
370
+ df_data = []
371
+ for ipo in ipos:
372
+ status_emoji = ipo.get('status_emoji', '⚪')
373
+ status = ipo.get('investment_status', 'UNKNOWN')
374
+
375
+ # Clean up status for display
376
+ display_status = {
377
+ 'INVESTED': '🟢 INVESTED',
378
+ 'ELIGIBLE_NOT_INVESTED': '🟡 ELIGIBLE',
379
+ 'WRONG_TYPE': '⚪ WRONG TYPE',
380
+ 'UNKNOWN': '🔴 UNKNOWN'
381
+ }.get(status, '⚪ UNKNOWN')
382
+
383
+ df_data.append({
384
+ 'Status': display_status,
385
+ 'Symbol': ipo.get('symbol', 'N/A'),
386
+ 'Security Type': ipo.get('security_type', 'N/A'),
387
+ 'Price': f"${ipo.get('trading_price', 0)}" if ipo.get('trading_price') != 'N/A' else 'N/A',
388
+ 'Detected At': ipo.get('detected_at', 'N/A')
389
+ })
390
+
391
+ return pd.DataFrame(df_data)
392
+
393
+ def get_order_history():
394
+ """Get order history from Alpaca"""
395
+ try:
396
+ # Use Method 2 which works: 1 year with CLOSED status
397
+ end_date = datetime.now(timezone.utc)
398
+ start_date = end_date - timedelta(days=365)
399
+
400
+ order_request = GetOrdersRequest(
401
+ status="closed", # This is the key - use "closed" status
402
+ limit=500,
403
+ after=start_date,
404
+ until=end_date
405
+ )
406
+
407
+ orders = trading_client.get_orders(order_request)
408
+ logger.info(f"Successfully fetched {len(orders)} orders using closed status filter")
409
+ return orders
410
+ except Exception as e:
411
+ logger.error(f"Error fetching order history: {e}")
412
+ return []
413
+
414
+ def refresh_investment_performance_table():
415
+ """Refresh investment performance table with P&L and sentiment analysis for all trading symbols"""
416
+ logger.info("📊 Starting investment performance table refresh...")
417
+
418
+ # Get IPO data and orders
419
+ logger.info("🔌 Fetching IPO data from VM...")
420
+ ipos = fetch_from_vm('ipos?limit=100', [])
421
+ logger.info(f"📈 Retrieved {len(ipos)} IPO records from VM")
422
+
423
+ logger.info("📋 Fetching order history from Alpaca...")
424
+ orders = get_order_history()
425
+ logger.info(f"📝 Retrieved {len(orders)} orders from Alpaca")
426
+
427
+ logger.info("💼 Fetching current positions from Alpaca...")
428
+ positions = get_current_positions()
429
+ logger.info(f"🏦 Retrieved {len(positions)} current positions")
430
+
431
+ # Create proper empty DataFrame with correct column names
432
+ columns = ['Symbol', 'Status', 'IPO Price', 'Buy Price', 'Sell Price', 'Investment', 'P&L ($)', 'P&L (%)', 'Sentiment', 'Predicted', 'Date']
433
+
434
+ logger.info(f"Found {len(orders)} total orders for performance analysis")
435
+
436
+ if not orders:
437
+ return pd.DataFrame(columns=columns)
438
+
439
+ # Get all unique symbols from order history
440
+ symbols_traded = set()
441
+ for order in orders:
442
+ if hasattr(order, 'symbol') and order.symbol:
443
+ symbols_traded.add(order.symbol)
444
+
445
+ logger.info(f"Found {len(symbols_traded)} unique symbols traded: {list(symbols_traded)}")
446
+
447
+ # Create IPO price lookup from VM data
448
+ ipo_price_lookup = {}
449
+ for ipo in ipos:
450
+ symbol = ipo.get('symbol', '')
451
+ if symbol:
452
+ try:
453
+ price = float(ipo.get('trading_price', 0))
454
+ if price > 0:
455
+ ipo_price_lookup[symbol] = price
456
+ except (ValueError, TypeError):
457
+ pass
458
+
459
+ invested_data = []
460
+
461
+ # Process each symbol that was traded
462
+ for symbol in sorted(symbols_traded):
463
+ # Get all orders for this symbol
464
+ symbol_orders = [o for o in orders if o.symbol == symbol]
465
+
466
+ if symbol_orders:
467
+ # Calculate from actual orders
468
+ buy_orders = [o for o in symbol_orders if o.side.value == 'buy']
469
+ sell_orders = [o for o in symbol_orders if o.side.value == 'sell']
470
+
471
+ if buy_orders:
472
+ total_bought = sum(float(o.filled_qty or 0) for o in buy_orders)
473
+ total_cost = sum(float(o.filled_qty or 0) * float(o.filled_avg_price or 0) for o in buy_orders)
474
+ avg_buy_price = total_cost / total_bought if total_bought > 0 else 0
475
+
476
+ total_sold = sum(float(o.filled_qty or 0) for o in sell_orders)
477
+ current_qty = total_bought - total_sold
478
+
479
+ # Get IPO price if available
480
+ ipo_price = ipo_price_lookup.get(symbol, 0)
481
+
482
+ # Get first buy date and time for sentiment analysis
483
+ first_buy_order = min(buy_orders, key=lambda x: x.filled_at)
484
+ first_buy_date = first_buy_order.filled_at.strftime('%Y-%m-%d')
485
+ investment_time = first_buy_order.filled_at
486
+ logger.info(f"Date for {symbol}: {first_buy_date} (from {first_buy_order.filled_at})")
487
+
488
+ # Calculate sell price (average of all sells)
489
+ if sell_orders:
490
+ 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)
491
+ else:
492
+ avg_sell_price = 0
493
+
494
+ current_qty = total_bought - total_sold
495
+
496
+ if current_qty > 0:
497
+ # Still holding - use current position for P&L
498
+ status = "🟦 HOLDING"
499
+ pos = next((p for p in positions if p['symbol'] == symbol), None)
500
+ if pos:
501
+ current_price = pos['current_price']
502
+ current_value = current_qty * current_price
503
+ investment = current_qty * avg_buy_price
504
+ pl_dollars = current_value - investment
505
+ pl_percent = (pl_dollars / investment * 100) if investment > 0 else 0
506
+ else:
507
+ # No current position data
508
+ investment = current_qty * avg_buy_price
509
+ pl_dollars = 0
510
+ pl_percent = 0
511
+ else:
512
+ # Sold all - calculate realized P&L
513
+ status = "🟨 SOLD"
514
+ investment = total_cost
515
+ sold_value = sum(float(o.filled_qty or 0) * float(o.filled_avg_price or 0) for o in sell_orders)
516
+ pl_dollars = sold_value - investment
517
+ pl_percent = (pl_dollars / investment * 100) if investment > 0 else 0
518
+
519
+ # Format P&L with arrows and colors
520
+ if pl_dollars > 0:
521
+ pl_arrow = "<span style='color: #00d647; font-size: 1.4em;'>▲</span>"
522
+ pl_color = "#00d647"
523
+ row_bg = "rgba(0, 214, 71, 0.1)"
524
+ elif pl_dollars < 0:
525
+ pl_arrow = "<span style='color: #ff0080; font-size: 1.4em;'>▼</span>"
526
+ pl_color = "#ff0080"
527
+ row_bg = "rgba(255, 0, 128, 0.1)"
528
+ else:
529
+ pl_arrow = ""
530
+ pl_color = "#8b949e"
531
+ row_bg = "rgba(139, 148, 158, 0.05)"
532
+
533
+ # Format P&L values with styled arrows
534
+ pl_dollar_str = f"{pl_arrow} <span style='color: {pl_color}; font-weight: 600;'>${abs(pl_dollars):.2f}</span>"
535
+ pl_percent_str = f"{pl_arrow} <span style='color: {pl_color}; font-weight: 600;'>{abs(pl_percent):.2f}%</span>"
536
+
537
+ # ADD SENTIMENT ANALYSIS FOR EACH STOCK
538
+ logger.info(f"🧠 Starting sentiment analysis for {symbol}...")
539
+ start_time = time.time()
540
+ try:
541
+ # Get pre-investment news (quick version)
542
+ logger.info(f"📰 Gathering pre-investment news for {symbol}...")
543
+ news_items = get_pre_investment_news(symbol, investment_time, hours_before=12)
544
+ logger.info(f"📑 Found {len(news_items)} total news items for {symbol}")
545
+
546
+ # Analyze sentiment
547
+ logger.info(f"🔍 Analyzing sentiment for {symbol}...")
548
+ avg_sentiment, predicted_change, prediction_label, source_breakdown = analyze_pre_investment_sentiment(news_items)
549
+
550
+ analysis_time = time.time() - start_time
551
+ logger.info(f"⚡ Sentiment analysis for {symbol} completed in {analysis_time:.1f}s")
552
+
553
+ # Format sentiment display
554
+ if prediction_label == "bullish":
555
+ sentiment_display = f"<span style='color: #00d647; font-weight: 600;'>🚀 {prediction_label.title()}</span>"
556
+ elif prediction_label == "bearish":
557
+ sentiment_display = f"<span style='color: #ff0080; font-weight: 600;'>📉 {prediction_label.title()}</span>"
558
+ else:
559
+ sentiment_display = f"<span style='color: #8b949e; font-weight: 600;'>😐 {prediction_label.title()}</span>"
560
+
561
+ # Format prediction
562
+ if predicted_change > 0:
563
+ predicted_display = f"<span style='color: #00d647; font-weight: 600;'>+{predicted_change:.1f}%</span>"
564
+ elif predicted_change < 0:
565
+ predicted_display = f"<span style='color: #ff0080; font-weight: 600;'>{predicted_change:.1f}%</span>"
566
+ else:
567
+ predicted_display = f"<span style='color: #8b949e; font-weight: 600;'>{predicted_change:.1f}%</span>"
568
+
569
+ reddit_count = len(source_breakdown.get('Reddit', []))
570
+ news_count = len(source_breakdown.get('Google News', []))
571
+ logger.info(f"🎯 {symbol} RESULTS: {prediction_label.upper()} ({predicted_change:+.1f}%) | Reddit: {reddit_count} posts | News: {news_count} articles")
572
+
573
+ # Log sample titles for debugging
574
+ if reddit_count > 0:
575
+ sample_reddit = source_breakdown['Reddit'][0]['title'][:50]
576
+ logger.info(f"📱 Sample Reddit: {sample_reddit}...")
577
+ if news_count > 0:
578
+ sample_news = source_breakdown['Google News'][0]['title'][:50]
579
+ logger.info(f"📰 Sample News: {sample_news}...")
580
+
581
+ except Exception as e:
582
+ analysis_time = time.time() - start_time
583
+ logger.error(f"❌ Sentiment analysis failed for {symbol} after {analysis_time:.1f}s: {str(e)}")
584
+ logger.error(f"🔍 Error type: {type(e).__name__}")
585
+ import traceback
586
+ logger.error(f"📋 Traceback: {traceback.format_exc()[:200]}...")
587
+ sentiment_display = "<span style='color: #8b949e;'>❓ Error</span>"
588
+ predicted_display = "<span style='color: #8b949e;'>N/A</span>"
589
+
590
+ # Continue with next stock instead of failing completely
591
+ pass
592
+
593
+ invested_data.append({
594
+ 'Symbol': symbol,
595
+ 'Status': status,
596
+ 'IPO Price': f"${ipo_price:.2f}" if ipo_price > 0 else 'N/A',
597
+ 'Buy Price': f"${avg_buy_price:.2f}",
598
+ 'Sell Price': f"${avg_sell_price:.2f}" if avg_sell_price > 0 else 'N/A',
599
+ 'Investment': f"${investment:.2f}",
600
+ 'P&L ($)': pl_dollar_str,
601
+ 'P&L (%)': pl_percent_str,
602
+ 'Sentiment': sentiment_display,
603
+ 'Predicted': predicted_display,
604
+ 'Date': first_buy_date,
605
+ '_row_bg': row_bg, # Store background color for styling
606
+ '_sort_date': first_buy_order.filled_at # Store datetime for sorting
607
+ })
608
+
609
+ # Sort by date (most recent first)
610
+ invested_data.sort(key=lambda x: x['_sort_date'], reverse=True)
611
+ logger.info(f"📋 Processed {len(invested_data)} investments with sentiment analysis")
612
+
613
+ df = pd.DataFrame(invested_data)
614
+ logger.info(f"✅ Investment performance table refresh completed - {len(df)} rows")
615
+ return df
616
+
617
+ def refresh_investment_performance_html():
618
+ """Return styled HTML table for investment performance"""
619
+ df = refresh_investment_performance_table()
620
+
621
+ if df.empty:
622
+ return "<div style='text-align: center; padding: 2rem; color: #666;'>No trading data available</div>"
623
+
624
+ # Build HTML table
625
+ html = '<table class="investment-table">'
626
+
627
+ # Header
628
+ html += '<thead><tr>'
629
+ for col in df.columns:
630
+ if not col.startswith('_'): # Skip internal columns
631
+ html += f'<th>{col}</th>'
632
+ html += '</tr></thead>'
633
+
634
+ # Body
635
+ html += '<tbody>'
636
+ for _, row in df.iterrows():
637
+ # Determine row class based on P&L
638
+ row_class = ""
639
+ pl_str = str(row.get('P&L ($)', ''))
640
+ if '▲' in pl_str:
641
+ row_class = "profit-row"
642
+ elif '▼' in pl_str:
643
+ row_class = "loss-row"
644
+ else:
645
+ row_class = "neutral-row"
646
+
647
+ html += f'<tr class="{row_class}">'
648
+ for col in df.columns:
649
+ if not col.startswith('_'): # Skip internal columns
650
+ html += f'<td>{row[col]}</td>'
651
+ html += '</tr>'
652
+
653
+ html += '</tbody></table>'
654
+ return html
655
+
656
+ def refresh_vm_stats():
657
+ """Refresh VM statistics"""
658
+ stats = fetch_from_vm('stats', {})
659
+
660
+ if not stats:
661
+ return "0", "0", "0", "0%", "No data"
662
+
663
+ return (
664
+ str(stats.get('total_ipos_detected', 0)),
665
+ str(stats.get('ipos_invested', 0)),
666
+ str(stats.get('cs_stocks_detected', 0)),
667
+ f"{stats.get('investment_rate', 0):.1f}%",
668
+ stats.get('last_updated', 'N/A')
669
+ )
670
+
671
+ def refresh_system_logs():
672
+ """Refresh system logs from VM"""
673
+ logs = fetch_from_vm('logs', [])
674
+
675
+ if not logs:
676
+ return "No logs available from VM"
677
+
678
+ # Format logs for display
679
+ formatted_logs = []
680
+ for log in logs:
681
+ emoji = log.get('emoji', '⚪')
682
+ timestamp = log.get('timestamp', 'N/A')
683
+ message = log.get('message', '')
684
+ formatted_logs.append(f"{emoji} {timestamp} | {message}")
685
+
686
+ return '\n'.join(formatted_logs)
687
+
688
+ def refresh_raw_logs():
689
+ """Refresh raw logs from VM"""
690
+ raw_data = fetch_from_vm('logs/raw?lines=1000', {})
691
+
692
+ if not raw_data:
693
+ return "No raw logs available from VM"
694
+
695
+ content = raw_data.get('content', 'No content')
696
+ total_lines = raw_data.get('total_lines', 0)
697
+ showing_lines = raw_data.get('showing_lines', 0)
698
+
699
+ header = f"=== RAW CRON LOGS ===\nShowing last {showing_lines} of {total_lines} total lines\n\n"
700
+ return header + content
701
+
702
+ def run_vm_command(command, current_output="", command_history=""):
703
+ """Execute command on VM and return output"""
704
+ try:
705
+ if not command.strip():
706
+ return current_output, "", command_history
707
+
708
+ # Add command to history
709
+ history_list = command_history.split("|||") if command_history else []
710
+ if command not in history_list:
711
+ history_list.append(command)
712
+ # Keep last 50 commands
713
+ history_list = history_list[-50:]
714
+ new_history = "|||".join(history_list)
715
+
716
+ response = requests.post(f"{VM_API_URL}/api/execute",
717
+ json={"command": command},
718
+ timeout=10)
719
+
720
+ if response.status_code == 200:
721
+ data = response.json()
722
+ output = data.get('output', '')
723
+ exit_code = data.get('exit_code', 0)
724
+
725
+ # Add color coding for common patterns
726
+ colored_output = colorize_output(output)
727
+
728
+ # Format terminal-style output with clean spacing
729
+ # Clean up output to avoid weird quote formatting
730
+ clean_output = colored_output.strip().replace('\r', '')
731
+ new_line = f"$ {command}\n{clean_output}"
732
+ if exit_code != 0:
733
+ new_line += f"\n[Exit code: {exit_code}]"
734
+ new_line += "\n$ "
735
+
736
+ # RADICAL FIX: Put newest content at TOP instead of bottom!
737
+ if current_output.strip():
738
+ full_output = new_line + "\n" + current_output.rstrip()
739
+ else:
740
+ full_output = new_line
741
+
742
+ return full_output, "", new_history
743
+ else:
744
+ error_line = f"\n$ {command}\nError: VM API returned {response.status_code}\n$ "
745
+ return current_output + error_line, "", new_history
746
+
747
+ except Exception as e:
748
+ error_line = f"\n$ {command}\nError: {str(e)}\n$ "
749
+ return current_output + error_line, "", new_history
750
+
751
+ def colorize_output(output):
752
+ """Add basic color coding to terminal output"""
753
+ import re
754
+
755
+ # Color patterns (using ANSI-like styling for web)
756
+ colored = output
757
+
758
+ # File permissions and directories (ls output)
759
+ colored = re.sub(r'^(d)([rwx-]{9})', r'<span style="color: #4A90E2;">\1\2</span>', colored, flags=re.MULTILINE)
760
+ colored = re.sub(r'^(-)([rwx-]{9})', r'<span style="color: #50E3C2;">\1\2</span>', colored, flags=re.MULTILINE)
761
+
762
+ # Error messages
763
+ colored = re.sub(r'(ERROR|Error|error)', r'<span style="color: #FF6B6B;">\1</span>', colored)
764
+ colored = re.sub(r'(WARNING|Warning|warning)', r'<span style="color: #FFD93D;">\1</span>', colored)
765
+
766
+ # Success indicators
767
+ colored = re.sub(r'(SUCCESS|Success|success)', r'<span style="color: #6BCF7F;">\1</span>', colored)
768
+
769
+ # File extensions
770
+ colored = re.sub(r'(\w+\.(py|log|csv|json|txt))', r'<span style="color: #BD93F9;">\1</span>', colored)
771
+
772
+ # Numbers and timestamps
773
+ colored = re.sub(r'(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})', r'<span style="color: #50FA7B;">\1</span>', colored)
774
+
775
+ return colored
776
+
777
+ def debug_order_history():
778
+ """Debug function to show raw order history data"""
779
+ try:
780
+ # Try multiple approaches to get orders
781
+ debug_info = f"=== ORDER HISTORY DEBUG ===\n"
782
+
783
+ # Approach 1: All orders, last 6 months (DEPRECATED - doesn't work)
784
+ try:
785
+ end_date = datetime.now(timezone.utc)
786
+ start_date = end_date - timedelta(days=180)
787
+ old_request = GetOrdersRequest(limit=500, after=start_date, until=end_date)
788
+ old_orders = trading_client.get_orders(old_request)
789
+ debug_info += f"Method 1 (6 months, all statuses): {len(old_orders)} orders [DEPRECATED]\n"
790
+ except Exception as e:
791
+ debug_info += f"Method 1 failed: {str(e)}\n"
792
+
793
+ # Approach 1B: Current working method used by Investment Performance
794
+ orders = get_order_history()
795
+ debug_info += f"Method 1B (PRIMARY - 1 year, CLOSED): {len(orders)} orders [CURRENTLY USED]\n"
796
+
797
+ # Approach 2: Just filled orders, last year
798
+ try:
799
+ end_date = datetime.now(timezone.utc)
800
+ start_date = end_date - timedelta(days=365)
801
+ filled_request = GetOrdersRequest(
802
+ status="closed", # Use string instead of enum
803
+ limit=500,
804
+ after=start_date,
805
+ until=end_date
806
+ )
807
+ filled_orders = trading_client.get_orders(filled_request)
808
+ debug_info += f"Method 2 (1 year, CLOSED orders): {len(filled_orders)} orders\n"
809
+ except Exception as e:
810
+ debug_info += f"Method 2 failed: {str(e)}\n"
811
+
812
+ # Approach 3: No date filter, just get recent orders
813
+ try:
814
+ recent_request = GetOrdersRequest(limit=100)
815
+ recent_orders = trading_client.get_orders(recent_request)
816
+ debug_info += f"Method 3 (recent 100, no date filter): {len(recent_orders)} orders\n"
817
+ except Exception as e:
818
+ debug_info += f"Method 3 failed: {str(e)}\n"
819
+
820
+ debug_info += "\n"
821
+
822
+ # Show any orders we found
823
+ all_orders = orders if orders else (filled_orders if 'filled_orders' in locals() else (recent_orders if 'recent_orders' in locals() else []))
824
+
825
+ if all_orders:
826
+ debug_info += f"Sample orders (showing first 10):\n"
827
+ for i, order in enumerate(all_orders[:10]):
828
+ debug_info += f"{i+1}. Symbol: {order.symbol}, Side: {order.side}, "
829
+ debug_info += f"Qty: {order.filled_qty}, Price: {order.filled_avg_price}, "
830
+ debug_info += f"Status: {order.status}, Time: {order.filled_at}, "
831
+ debug_info += f"Created: {order.created_at}\n"
832
+ else:
833
+ debug_info += "❌ NO ORDERS FOUND WITH ANY METHOD!\n"
834
+ debug_info += "\nLet's check account details:\n"
835
+
836
+ # Check account info
837
+ try:
838
+ account = trading_client.get_account()
839
+ debug_info += f"Account ID: {account.account_number}\n"
840
+ debug_info += f"Account Status: {account.status}\n"
841
+ debug_info += f"Trading Blocked: {account.trading_blocked}\n"
842
+ debug_info += f"Pattern Day Trader: {account.pattern_day_trader}\n"
843
+ debug_info += f"Cash: ${float(account.cash):,.2f}\n"
844
+ debug_info += f"Portfolio Value: ${float(account.portfolio_value):,.2f}\n"
845
+
846
+ # Check if this is paper trading
847
+ debug_info += f"\nAPI Keys being used:\n"
848
+ debug_info += f"API Key: {API_KEY[:8]}...{API_KEY[-4:]}\n"
849
+ if "PK" in API_KEY:
850
+ debug_info += "🟢 This appears to be PAPER TRADING (PK prefix)\n"
851
+ elif "AK" in API_KEY:
852
+ debug_info += "🔴 This appears to be LIVE TRADING (AK prefix)\n"
853
+ else:
854
+ debug_info += "❓ Unknown API key type\n"
855
+
856
+ except Exception as e:
857
+ debug_info += f"❌ Error getting account info: {str(e)}\n"
858
+
859
+ debug_info += "\nPossible issues:\n"
860
+ debug_info += "- No actual trading activity on this account\n"
861
+ debug_info += "- Using paper trading account (no real orders)\n"
862
+ debug_info += "- Orders are older than 1 year\n"
863
+ debug_info += "- API key permissions issue\n"
864
+ debug_info += "- Different Alpaca account than expected\n"
865
+
866
+ return debug_info
867
+ except Exception as e:
868
+ return f"ERROR getting order history: {str(e)}"
869
+
870
+ def debug_current_positions():
871
+ """Debug function to show current positions"""
872
+ try:
873
+ positions = get_current_positions()
874
+ debug_info = f"=== CURRENT POSITIONS DEBUG ===\n"
875
+ debug_info += f"Total positions: {len(positions)}\n\n"
876
+
877
+ for pos in positions:
878
+ debug_info += f"Symbol: {pos['symbol']}, Qty: {pos['qty']}, "
879
+ debug_info += f"Market Value: ${pos['market_value']:.2f}, "
880
+ debug_info += f"P&L: ${pos['unrealized_pl']:.2f}\n"
881
+
882
+ return debug_info
883
+ except Exception as e:
884
+ return f"ERROR getting positions: {str(e)}"
885
+
886
+ def debug_ipo_data():
887
+ """Debug function to show IPO data from VM"""
888
+ try:
889
+ ipos = fetch_from_vm('ipos?limit=20', [])
890
+ debug_info = f"=== IPO DATA DEBUG ===\n"
891
+ debug_info += f"Total IPOs: {len(ipos)}\n\n"
892
+
893
+ invested_count = 0
894
+ for ipo in ipos:
895
+ status = ipo.get('investment_status', 'UNKNOWN')
896
+ if status == 'INVESTED':
897
+ invested_count += 1
898
+ debug_info += f"INVESTED: {ipo.get('symbol')} - Price: ${ipo.get('trading_price')}\n"
899
+
900
+ debug_info += f"\nTotal INVESTED IPOs: {invested_count}\n"
901
+ return debug_info
902
+ except Exception as e:
903
+ return f"ERROR getting IPO data: {str(e)}"
904
+
905
+ def debug_account_info():
906
+ """Debug function to show account info"""
907
+ try:
908
+ account = get_account_info()
909
+ debug_info = f"=== ACCOUNT INFO DEBUG ===\n"
910
+ for key, value in account.items():
911
+ debug_info += f"{key}: {value}\n"
912
+ return debug_info
913
+ except Exception as e:
914
+ return f"ERROR getting account info: {str(e)}"
915
+
916
+ def calculate_sequential_reinvestment():
917
+ """Calculate P&L% if reinvesting same amount sequentially in each stock"""
918
+ try:
919
+ orders = get_order_history()
920
+ if not orders:
921
+ return "No order data available for calculation"
922
+
923
+ # Get unique symbols with their first buy date
924
+ symbols_by_date = {}
925
+ for order in orders:
926
+ if order.side.value == 'buy' and order.status.value == 'filled':
927
+ symbol = order.symbol
928
+ fill_date = order.filled_at
929
+ if symbol not in symbols_by_date or fill_date < symbols_by_date[symbol]:
930
+ symbols_by_date[symbol] = fill_date
931
+
932
+ # Sort by first buy date
933
+ sorted_symbols = sorted(symbols_by_date.items(), key=lambda x: x[1])
934
+
935
+ # Calculate sequential reinvestment returns
936
+ initial_investment = 1000 # Start with $1000
937
+ current_value = initial_investment
938
+
939
+ results = []
940
+ total_return = 0
941
+
942
+ for symbol, first_date in sorted_symbols:
943
+ # Get all orders for this symbol
944
+ symbol_orders = [o for o in orders if o.symbol == symbol]
945
+ buy_orders = [o for o in symbol_orders if o.side.value == 'buy']
946
+ sell_orders = [o for o in symbol_orders if o.side.value == 'sell']
947
+
948
+ if buy_orders:
949
+ # Calculate actual P&L for this symbol
950
+ total_bought = sum(float(o.filled_qty or 0) for o in buy_orders)
951
+ total_cost = sum(float(o.filled_qty or 0) * float(o.filled_avg_price or 0) for o in buy_orders)
952
+
953
+ if sell_orders:
954
+ # Sold - use actual sell proceeds
955
+ sold_value = sum(float(o.filled_qty or 0) * float(o.filled_avg_price or 0) for o in sell_orders)
956
+ pl_percent = ((sold_value - total_cost) / total_cost) if total_cost > 0 else 0
957
+ else:
958
+ # Still holding - estimate current value
959
+ positions = get_current_positions()
960
+ pos = next((p for p in positions if p['symbol'] == symbol), None)
961
+ if pos:
962
+ current_price = pos['current_price']
963
+ current_symbol_value = total_bought * current_price
964
+ pl_percent = ((current_symbol_value - total_cost) / total_cost) if total_cost > 0 else 0
965
+ else:
966
+ pl_percent = 0
967
+
968
+ # Apply return to current value
969
+ new_value = current_value * (1 + pl_percent)
970
+ gain_loss = new_value - current_value
971
+
972
+ results.append(f"{symbol}: {pl_percent*100:+.2f}% | ${current_value:.2f} → ${new_value:.2f} ({gain_loss:+.2f})")
973
+ current_value = new_value
974
+ total_return += pl_percent
975
+
976
+ final_return_pct = ((current_value - initial_investment) / initial_investment) * 100
977
+
978
+ output = f"🧮 SEQUENTIAL REINVESTMENT ANALYSIS\n"
979
+ output += f"Starting Investment: ${initial_investment:.2f}\n"
980
+ output += f"Final Value: ${current_value:.2f}\n"
981
+ output += f"Total Return: {final_return_pct:+.2f}%\n"
982
+ output += f"Number of Trades: {len(sorted_symbols)}\n\n"
983
+ output += "Trade Sequence:\n"
984
+ output += "\n".join(results)
985
+
986
+ return output
987
+
988
+ except Exception as e:
989
+ return f"ERROR calculating sequential reinvestment: {str(e)}"
990
+
991
+ def calculate_equal_weight_portfolio():
992
+ """Calculate P&L% if investing equal amounts in all stocks simultaneously"""
993
+ try:
994
+ orders = get_order_history()
995
+ if not orders:
996
+ return "No order data available for calculation"
997
+
998
+ # Get unique symbols
999
+ symbols = set()
1000
+ for order in orders:
1001
+ if order.side.value == 'buy':
1002
+ symbols.add(order.symbol)
1003
+
1004
+ total_pl = 0
1005
+ valid_symbols = 0
1006
+ results = []
1007
+
1008
+ for symbol in sorted(symbols):
1009
+ symbol_orders = [o for o in orders if o.symbol == symbol]
1010
+ buy_orders = [o for o in symbol_orders if o.side.value == 'buy']
1011
+ sell_orders = [o for o in symbol_orders if o.side.value == 'sell']
1012
+
1013
+ if buy_orders:
1014
+ total_bought = sum(float(o.filled_qty or 0) for o in buy_orders)
1015
+ total_cost = sum(float(o.filled_qty or 0) * float(o.filled_avg_price or 0) for o in buy_orders)
1016
+ avg_buy_price = total_cost / total_bought if total_bought > 0 else 0
1017
+
1018
+ if sell_orders:
1019
+ # Sold
1020
+ sold_value = sum(float(o.filled_qty or 0) * float(o.filled_avg_price or 0) for o in sell_orders)
1021
+ pl_percent = ((sold_value - total_cost) / total_cost) if total_cost > 0 else 0
1022
+ status = "SOLD"
1023
+ else:
1024
+ # Still holding
1025
+ positions = get_current_positions()
1026
+ pos = next((p for p in positions if p['symbol'] == symbol), None)
1027
+ if pos:
1028
+ current_price = pos['current_price']
1029
+ current_value = total_bought * current_price
1030
+ pl_percent = ((current_value - total_cost) / total_cost) if total_cost > 0 else 0
1031
+ status = "HOLDING"
1032
+ else:
1033
+ pl_percent = 0
1034
+ status = "UNKNOWN"
1035
+
1036
+ total_pl += pl_percent
1037
+ valid_symbols += 1
1038
+
1039
+ results.append(f"{symbol}: {pl_percent*100:+.2f}% ({status})")
1040
+
1041
+ avg_return = (total_pl / valid_symbols) * 100 if valid_symbols > 0 else 0
1042
+
1043
+ output = f"⚖️ EQUAL WEIGHT PORTFOLIO ANALYSIS\n"
1044
+ output += f"Total Symbols: {valid_symbols}\n"
1045
+ output += f"Average Return per Symbol: {avg_return:+.2f}%\n"
1046
+ output += f"Portfolio Return (equal weights): {avg_return:+.2f}%\n\n"
1047
+ output += "Individual Returns:\n"
1048
+ output += "\n".join(results)
1049
+
1050
+ return output
1051
+
1052
+ except Exception as e:
1053
+ return f"ERROR calculating equal weight portfolio: {str(e)}"
1054
+
1055
+ def calculate_best_worst_performers():
1056
+ """Find best and worst performing stocks"""
1057
+ try:
1058
+ orders = get_order_history()
1059
+ if not orders:
1060
+ return "No order data available for calculation"
1061
+
1062
+ symbols = set()
1063
+ for order in orders:
1064
+ if order.side.value == 'buy':
1065
+ symbols.add(order.symbol)
1066
+
1067
+ performance = []
1068
+
1069
+ for symbol in symbols:
1070
+ symbol_orders = [o for o in orders if o.symbol == symbol]
1071
+ buy_orders = [o for o in symbol_orders if o.side.value == 'buy']
1072
+ sell_orders = [o for o in symbol_orders if o.side.value == 'sell']
1073
+
1074
+ if buy_orders:
1075
+ total_bought = sum(float(o.filled_qty or 0) for o in buy_orders)
1076
+ total_cost = sum(float(o.filled_qty or 0) * float(o.filled_avg_price or 0) for o in buy_orders)
1077
+
1078
+ if sell_orders:
1079
+ sold_value = sum(float(o.filled_qty or 0) * float(o.filled_avg_price or 0) for o in sell_orders)
1080
+ pl_percent = ((sold_value - total_cost) / total_cost) if total_cost > 0 else 0
1081
+ pl_dollars = sold_value - total_cost
1082
+ status = "SOLD"
1083
+ else:
1084
+ positions = get_current_positions()
1085
+ pos = next((p for p in positions if p['symbol'] == symbol), None)
1086
+ if pos:
1087
+ current_price = pos['current_price']
1088
+ current_value = total_bought * current_price
1089
+ pl_percent = ((current_value - total_cost) / total_cost) if total_cost > 0 else 0
1090
+ pl_dollars = current_value - total_cost
1091
+ status = "HOLDING"
1092
+ else:
1093
+ pl_percent = 0
1094
+ pl_dollars = 0
1095
+ status = "UNKNOWN"
1096
+
1097
+ performance.append({
1098
+ 'symbol': symbol,
1099
+ 'pl_percent': pl_percent,
1100
+ 'pl_dollars': pl_dollars,
1101
+ 'investment': total_cost,
1102
+ 'status': status
1103
+ })
1104
+
1105
+ # Sort by percentage return
1106
+ performance.sort(key=lambda x: x['pl_percent'], reverse=True)
1107
+
1108
+ output = f"🏆 BEST vs WORST PERFORMERS\n\n"
1109
+
1110
+ if performance:
1111
+ output += "🥇 TOP 5 PERFORMERS:\n"
1112
+ for i, perf in enumerate(performance[:5]):
1113
+ output += f"{i+1}. {perf['symbol']}: {perf['pl_percent']*100:+.2f}% (${perf['pl_dollars']:+.2f}) - {perf['status']}\n"
1114
+
1115
+ output += "\n🥉 BOTTOM 5 PERFORMERS:\n"
1116
+ for i, perf in enumerate(performance[-5:]):
1117
+ rank = len(performance) - 4 + i
1118
+ output += f"{rank}. {perf['symbol']}: {perf['pl_percent']*100:+.2f}% (${perf['pl_dollars']:+.2f}) - {perf['status']}\n"
1119
+
1120
+ # Calculate some stats
1121
+ total_winners = len([p for p in performance if p['pl_percent'] > 0])
1122
+ total_losers = len([p for p in performance if p['pl_percent'] < 0])
1123
+
1124
+ output += f"\n📊 SUMMARY:\n"
1125
+ output += f"Winners: {total_winners}/{len(performance)} ({total_winners/len(performance)*100:.1f}%)\n"
1126
+ output += f"Losers: {total_losers}/{len(performance)} ({total_losers/len(performance)*100:.1f}%)\n"
1127
+
1128
+ return output
1129
+
1130
+ except Exception as e:
1131
+ return f"ERROR calculating best/worst performers: {str(e)}"
1132
+
1133
+ def calculate_win_rate_metrics():
1134
+ """Calculate win rate and average returns"""
1135
+ try:
1136
+ orders = get_order_history()
1137
+ if not orders:
1138
+ return "No order data available for calculation"
1139
+
1140
+ symbols = set()
1141
+ for order in orders:
1142
+ if order.side.value == 'buy':
1143
+ symbols.add(order.symbol)
1144
+
1145
+ performance = []
1146
+ total_investment = 0
1147
+
1148
+ for symbol in symbols:
1149
+ symbol_orders = [o for o in orders if o.symbol == symbol]
1150
+ buy_orders = [o for o in symbol_orders if o.side.value == 'buy']
1151
+ sell_orders = [o for o in symbol_orders if o.side.value == 'sell']
1152
+
1153
+ if buy_orders:
1154
+ total_bought = sum(float(o.filled_qty or 0) for o in buy_orders)
1155
+ total_cost = sum(float(o.filled_qty or 0) * float(o.filled_avg_price or 0) for o in buy_orders)
1156
+ total_investment += total_cost
1157
+
1158
+ if sell_orders:
1159
+ sold_value = sum(float(o.filled_qty or 0) * float(o.filled_avg_price or 0) for o in sell_orders)
1160
+ pl_percent = ((sold_value - total_cost) / total_cost) if total_cost > 0 else 0
1161
+ pl_dollars = sold_value - total_cost
1162
+ else:
1163
+ positions = get_current_positions()
1164
+ pos = next((p for p in positions if p['symbol'] == symbol), None)
1165
+ if pos:
1166
+ current_price = pos['current_price']
1167
+ current_value = total_bought * current_price
1168
+ pl_percent = ((current_value - total_cost) / total_cost) if total_cost > 0 else 0
1169
+ pl_dollars = current_value - total_cost
1170
+ else:
1171
+ pl_percent = 0
1172
+ pl_dollars = 0
1173
+
1174
+ performance.append({
1175
+ 'symbol': symbol,
1176
+ 'pl_percent': pl_percent,
1177
+ 'pl_dollars': pl_dollars,
1178
+ 'investment': total_cost
1179
+ })
1180
+
1181
+ if not performance:
1182
+ return "No performance data available"
1183
+
1184
+ # Calculate metrics
1185
+ winners = [p for p in performance if p['pl_percent'] > 0]
1186
+ losers = [p for p in performance if p['pl_percent'] < 0]
1187
+ breakeven = [p for p in performance if p['pl_percent'] == 0]
1188
+
1189
+ win_rate = len(winners) / len(performance) * 100
1190
+ avg_win = sum(p['pl_percent'] for p in winners) / len(winners) * 100 if winners else 0
1191
+ avg_loss = sum(p['pl_percent'] for p in losers) / len(losers) * 100 if losers else 0
1192
+
1193
+ total_pl_dollars = sum(p['pl_dollars'] for p in performance)
1194
+ total_pl_percent = (total_pl_dollars / total_investment) * 100 if total_investment > 0 else 0
1195
+
1196
+ # Risk/Reward ratio
1197
+ risk_reward = abs(avg_win / avg_loss) if avg_loss != 0 else float('inf')
1198
+
1199
+ output = f"🎯 WIN RATE & AVERAGE RETURNS\n\n"
1200
+ output += f"Total Trades: {len(performance)}\n"
1201
+ output += f"Win Rate: {win_rate:.1f}% ({len(winners)} winners)\n"
1202
+ output += f"Loss Rate: {len(losers)/len(performance)*100:.1f}% ({len(losers)} losers)\n"
1203
+ output += f"Breakeven: {len(breakeven)} trades\n\n"
1204
+
1205
+ output += f"📈 AVERAGE PERFORMANCE:\n"
1206
+ output += f"Average Winner: +{avg_win:.2f}%\n"
1207
+ output += f"Average Loser: {avg_loss:.2f}%\n"
1208
+ output += f"Risk/Reward Ratio: {risk_reward:.2f}:1\n\n"
1209
+
1210
+ output += f"💰 TOTAL PERFORMANCE:\n"
1211
+ output += f"Total Invested: ${total_investment:.2f}\n"
1212
+ output += f"Total P&L: ${total_pl_dollars:+.2f}\n"
1213
+ output += f"Total Return: {total_pl_percent:+.2f}%\n"
1214
+
1215
+ return output
1216
+
1217
+ except Exception as e:
1218
+ return f"ERROR calculating win rate metrics: {str(e)}"
1219
+
1220
+ def calculate_risk_metrics():
1221
+ """Calculate risk metrics and volatility"""
1222
+ try:
1223
+ orders = get_order_history()
1224
+ if not orders:
1225
+ return "No order data available for calculation"
1226
+
1227
+ symbols = set()
1228
+ for order in orders:
1229
+ if order.side.value == 'buy':
1230
+ symbols.add(order.symbol)
1231
+
1232
+ returns = []
1233
+ investments = []
1234
+
1235
+ for symbol in symbols:
1236
+ symbol_orders = [o for o in orders if o.symbol == symbol]
1237
+ buy_orders = [o for o in symbol_orders if o.side.value == 'buy']
1238
+ sell_orders = [o for o in symbol_orders if o.side.value == 'sell']
1239
+
1240
+ if buy_orders:
1241
+ total_bought = sum(float(o.filled_qty or 0) for o in buy_orders)
1242
+ total_cost = sum(float(o.filled_qty or 0) * float(o.filled_avg_price or 0) for o in buy_orders)
1243
+ investments.append(total_cost)
1244
+
1245
+ if sell_orders:
1246
+ sold_value = sum(float(o.filled_qty or 0) * float(o.filled_avg_price or 0) for o in sell_orders)
1247
+ pl_percent = ((sold_value - total_cost) / total_cost) if total_cost > 0 else 0
1248
+ else:
1249
+ positions = get_current_positions()
1250
+ pos = next((p for p in positions if p['symbol'] == symbol), None)
1251
+ if pos:
1252
+ current_price = pos['current_price']
1253
+ current_value = total_bought * current_price
1254
+ pl_percent = ((current_value - total_cost) / total_cost) if total_cost > 0 else 0
1255
+ else:
1256
+ pl_percent = 0
1257
+
1258
+ returns.append(pl_percent)
1259
+
1260
+ if not returns:
1261
+ return "No return data available"
1262
+
1263
+ # Calculate statistics
1264
+ import statistics
1265
+ avg_return = statistics.mean(returns) * 100
1266
+ median_return = statistics.median(returns) * 100
1267
+ volatility = statistics.stdev(returns) * 100 if len(returns) > 1 else 0
1268
+
1269
+ # Sharpe-like ratio (assuming risk-free rate = 0)
1270
+ sharpe = avg_return / volatility if volatility > 0 else 0
1271
+
1272
+ # Max drawdown
1273
+ max_return = max(returns) * 100
1274
+ min_return = min(returns) * 100
1275
+ max_drawdown = max_return - min_return
1276
+
1277
+ # Portfolio concentration
1278
+ total_investment = sum(investments)
1279
+ avg_position_size = statistics.mean(investments)
1280
+ largest_position = max(investments)
1281
+ concentration = (largest_position / total_investment) * 100 if total_investment > 0 else 0
1282
+
1283
+ output = f"⚠️ RISK METRICS & VOLATILITY\n\n"
1284
+ output += f"📊 RETURN STATISTICS:\n"
1285
+ output += f"Average Return: {avg_return:+.2f}%\n"
1286
+ output += f"Median Return: {median_return:+.2f}%\n"
1287
+ output += f"Volatility (StdDev): {volatility:.2f}%\n"
1288
+ output += f"Sharpe-like Ratio: {sharpe:.2f}\n\n"
1289
+
1290
+ output += f"📉 RISK MEASURES:\n"
1291
+ output += f"Best Trade: +{max_return:.2f}%\n"
1292
+ output += f"Worst Trade: {min_return:.2f}%\n"
1293
+ output += f"Max Range: {max_drawdown:.2f}%\n\n"
1294
+
1295
+ output += f"🎯 POSITION SIZING:\n"
1296
+ output += f"Average Position: ${avg_position_size:.2f}\n"
1297
+ output += f"Largest Position: ${largest_position:.2f}\n"
1298
+ output += f"Concentration Risk: {concentration:.1f}% in largest\n"
1299
+
1300
+ return output
1301
+
1302
+ except Exception as e:
1303
+ return f"ERROR calculating risk metrics: {str(e)}"
1304
+
1305
+ def calculate_time_analysis():
1306
+ """Analyze performance by time periods"""
1307
+ try:
1308
+ orders = get_order_history()
1309
+ if not orders:
1310
+ return "No order data available for calculation"
1311
+
1312
+ from datetime import datetime, timezone
1313
+
1314
+ # Group orders by month
1315
+ monthly_performance = {}
1316
+
1317
+ for order in orders:
1318
+ if order.side.value == 'buy' and order.status.value == 'filled':
1319
+ month_key = order.filled_at.strftime('%Y-%m')
1320
+ if month_key not in monthly_performance:
1321
+ monthly_performance[month_key] = {'symbols': set(), 'investment': 0, 'returns': []}
1322
+
1323
+ symbol = order.symbol
1324
+ monthly_performance[month_key]['symbols'].add(symbol)
1325
+
1326
+ # Calculate returns for each month
1327
+ symbols = set()
1328
+ for order in orders:
1329
+ if order.side.value == 'buy':
1330
+ symbols.add(order.symbol)
1331
+
1332
+ symbol_performance = {}
1333
+ for symbol in symbols:
1334
+ symbol_orders = [o for o in orders if o.symbol == symbol]
1335
+ buy_orders = [o for o in symbol_orders if o.side.value == 'buy']
1336
+ sell_orders = [o for o in symbol_orders if o.side.value == 'sell']
1337
+
1338
+ if buy_orders:
1339
+ first_buy = min(buy_orders, key=lambda x: x.filled_at)
1340
+ month_key = first_buy.filled_at.strftime('%Y-%m')
1341
+
1342
+ total_bought = sum(float(o.filled_qty or 0) for o in buy_orders)
1343
+ total_cost = sum(float(o.filled_qty or 0) * float(o.filled_avg_price or 0) for o in buy_orders)
1344
+
1345
+ if sell_orders:
1346
+ sold_value = sum(float(o.filled_qty or 0) * float(o.filled_avg_price or 0) for o in sell_orders)
1347
+ pl_percent = ((sold_value - total_cost) / total_cost) if total_cost > 0 else 0
1348
+ else:
1349
+ positions = get_current_positions()
1350
+ pos = next((p for p in positions if p['symbol'] == symbol), None)
1351
+ if pos:
1352
+ current_price = pos['current_price']
1353
+ current_value = total_bought * current_price
1354
+ pl_percent = ((current_value - total_cost) / total_cost) if total_cost > 0 else 0
1355
+ else:
1356
+ pl_percent = 0
1357
+
1358
+ if month_key in monthly_performance:
1359
+ monthly_performance[month_key]['investment'] += total_cost
1360
+ monthly_performance[month_key]['returns'].append(pl_percent)
1361
+
1362
+ output = f"⏰ TIME-BASED PERFORMANCE ANALYSIS\n\n"
1363
+
1364
+ for month in sorted(monthly_performance.keys()):
1365
+ data = monthly_performance[month]
1366
+ if data['returns']:
1367
+ avg_return = sum(data['returns']) / len(data['returns']) * 100
1368
+ total_investment = data['investment']
1369
+ num_trades = len(data['returns'])
1370
+
1371
+ output += f"📅 {month}: {avg_return:+.2f}% avg return\n"
1372
+ output += f" • {num_trades} trades, ${total_investment:.2f} invested\n"
1373
+
1374
+ # Calculate recent vs early performance
1375
+ sorted_months = sorted(monthly_performance.keys())
1376
+ if len(sorted_months) >= 2:
1377
+ early_months = sorted_months[:len(sorted_months)//2]
1378
+ recent_months = sorted_months[len(sorted_months)//2:]
1379
+
1380
+ early_returns = []
1381
+ recent_returns = []
1382
+
1383
+ for month in early_months:
1384
+ early_returns.extend(monthly_performance[month]['returns'])
1385
+
1386
+ for month in recent_months:
1387
+ recent_returns.extend(monthly_performance[month]['returns'])
1388
+
1389
+ if early_returns and recent_returns:
1390
+ early_avg = sum(early_returns) / len(early_returns) * 100
1391
+ recent_avg = sum(recent_returns) / len(recent_returns) * 100
1392
+
1393
+ output += f"\n📈 TREND ANALYSIS:\n"
1394
+ output += f"Early Period Avg: {early_avg:+.2f}% ({len(early_returns)} trades)\n"
1395
+ output += f"Recent Period Avg: {recent_avg:+.2f}% ({len(recent_returns)} trades)\n"
1396
+ output += f"Improvement: {recent_avg - early_avg:+.2f}% difference\n"
1397
+
1398
+ return output
1399
+
1400
+ except Exception as e:
1401
+ return f"ERROR calculating time analysis: {str(e)}"
1402
+
1403
+ # Trading History Backtesting Functions
1404
+ def get_pre_investment_news(symbol, investment_time, hours_before=12):
1405
+ """Get news from 12 hours before we invested"""
1406
+
1407
+ cutoff_time = investment_time - timedelta(minutes=30) # 30 min buffer
1408
+ search_start = investment_time - timedelta(hours=hours_before)
1409
+
1410
+ logger.info(f"🔍 NEWS SEARCH for {symbol}:")
1411
+ logger.info(f" 📅 Time window: {search_start.strftime('%Y-%m-%d %H:%M')} → {cutoff_time.strftime('%Y-%m-%d %H:%M')}")
1412
+ logger.info(f" ⏰ Search duration: {hours_before} hours before investment")
1413
+
1414
+ all_news = []
1415
+
1416
+ # Get Reddit posts
1417
+ logger.info(f"🧵 Starting Reddit search for {symbol}...")
1418
+ reddit_start = time.time()
1419
+ reddit_posts = get_reddit_pre_investment(symbol, search_start, cutoff_time)
1420
+ reddit_time = time.time() - reddit_start
1421
+ logger.info(f"✅ Reddit search completed in {reddit_time:.1f}s - found {len(reddit_posts)} posts")
1422
+ all_news.extend(reddit_posts)
1423
+
1424
+ # Get Google News
1425
+ logger.info(f"📰 Starting Google News search for {symbol}...")
1426
+ news_start = time.time()
1427
+ google_news = get_google_news_pre_investment(symbol, search_start, cutoff_time)
1428
+ news_time = time.time() - news_start
1429
+ logger.info(f"✅ Google News search completed in {news_time:.1f}s - found {len(google_news)} articles")
1430
+ all_news.extend(google_news)
1431
+
1432
+ logger.info(f"📊 TOTAL NEWS GATHERED for {symbol}: {len(all_news)} items ({len(reddit_posts)} Reddit + {len(google_news)} News)")
1433
+ return all_news
1434
+
1435
+ def get_reddit_pre_investment(symbol, start_time, cutoff_time):
1436
+ """Get Reddit posts from before our investment"""
1437
+
1438
+ reddit_posts = []
1439
+
1440
+ # Search key subreddits including WSB with multiple search strategies
1441
+ subreddits = ['wallstreetbets', 'stocks', 'investing']
1442
+ search_terms = [symbol, f'{symbol} stock', f'{symbol} IPO', f'${symbol}']
1443
+
1444
+ for subreddit in subreddits:
1445
+ for search_term in search_terms:
1446
+ try:
1447
+ url = f"https://www.reddit.com/r/{subreddit}/search.json"
1448
+ params = {
1449
+ 'q': search_term,
1450
+ 'restrict_sr': 'true',
1451
+ 'limit': 5, # Reduced to avoid duplicates
1452
+ 't': 'all', # Search all time instead of just week
1453
+ 'sort': 'relevance'
1454
+ }
1455
+
1456
+ response = requests.get(url, params=params, headers=headers, timeout=10)
1457
+ if response.status_code == 200:
1458
+ data = response.json()
1459
+ posts_found = len(data.get('data', {}).get('children', []))
1460
+ logger.info(f"Reddit search: r/{subreddit} + '{search_term}' found {posts_found} posts")
1461
+
1462
+ for post in data.get('data', {}).get('children', []):
1463
+ post_data = post.get('data', {})
1464
+
1465
+ if not post_data.get('title'):
1466
+ continue
1467
+
1468
+ # Check if we already have this post (avoid duplicates)
1469
+ title = post_data.get('title', '')
1470
+ if any(existing['title'] == title for existing in reddit_posts):
1471
+ continue
1472
+
1473
+ # Only include posts that actually mention the symbol
1474
+ title_text = f"{title} {post_data.get('selftext', '')}".upper()
1475
+ if symbol.upper() in title_text or f'${symbol.upper()}' in title_text:
1476
+ reddit_post = {
1477
+ 'title': title,
1478
+ 'selftext': post_data.get('selftext', '')[:300],
1479
+ 'score': post_data.get('score', 0),
1480
+ 'num_comments': post_data.get('num_comments', 0),
1481
+ 'subreddit': subreddit,
1482
+ 'source': 'Reddit',
1483
+ 'url': f"https://reddit.com{post_data.get('permalink', '')}",
1484
+ 'search_term': search_term
1485
+ }
1486
+ reddit_posts.append(reddit_post)
1487
+ logger.info(f"Added Reddit post: {title[:50]}... (score: {post_data.get('score', 0)})")
1488
+
1489
+ time.sleep(0.5) # Reduced rate limiting
1490
+
1491
+ except Exception as e:
1492
+ logger.warning(f"Reddit error for r/{subreddit} + '{search_term}': {e}")
1493
+
1494
+ logger.info(f"Total Reddit posts found for {symbol}: {len(reddit_posts)}")
1495
+ return reddit_posts
1496
+
1497
+ def get_google_news_pre_investment(symbol, start_time, cutoff_time):
1498
+ """Get Google News from before our investment"""
1499
+
1500
+ google_news = []
1501
+
1502
+ try:
1503
+ # Search for IPO-related news
1504
+ search_queries = [
1505
+ f'{symbol} IPO',
1506
+ f'{symbol} stock',
1507
+ f'{symbol} public offering'
1508
+ ]
1509
+
1510
+ for query in search_queries:
1511
+ url = "https://news.google.com/rss/search"
1512
+ params = {
1513
+ 'q': query,
1514
+ 'hl': 'en-US',
1515
+ 'gl': 'US',
1516
+ 'ceid': 'US:en'
1517
+ }
1518
+
1519
+ response = requests.get(url, params=params, headers=headers, timeout=10)
1520
+ if response.status_code == 200:
1521
+ # Parse RSS
1522
+ from xml.etree import ElementTree as ET
1523
+ root = ET.fromstring(response.content)
1524
+
1525
+ for item in root.findall('.//item')[:5]: # Limit per query
1526
+ title_elem = item.find('title')
1527
+ link_elem = item.find('link')
1528
+ description_elem = item.find('description')
1529
+
1530
+ if title_elem is not None:
1531
+ description = description_elem.text if description_elem is not None else ""
1532
+ # Clean HTML
1533
+ import re
1534
+ description = re.sub(r'<[^>]+>', '', description)
1535
+
1536
+ news_item = {
1537
+ 'title': title_elem.text,
1538
+ 'description': description,
1539
+ 'source': 'Google News',
1540
+ 'url': link_elem.text if link_elem is not None else ''
1541
+ }
1542
+ google_news.append(news_item)
1543
+
1544
+ time.sleep(0.5)
1545
+
1546
+ except Exception as e:
1547
+ logger.warning(f"Google News error: {e}")
1548
+
1549
+ return google_news
1550
+
1551
+ def analyze_pre_investment_sentiment(news_items):
1552
+ """Analyze sentiment from news before our investment"""
1553
+
1554
+ if not news_items:
1555
+ return 0.0, 0.0, "neutral", {}
1556
+
1557
+ sentiments = []
1558
+ source_breakdown = {'Reddit': [], 'Google News': []}
1559
+
1560
+ for item in news_items:
1561
+ # Combine title and description/selftext
1562
+ if item['source'] == 'Reddit':
1563
+ text = f"{item['title']} {item.get('selftext', '')}"
1564
+ else:
1565
+ text = f"{item['title']} {item.get('description', '')}"
1566
+
1567
+ # Sentiment analysis
1568
+ vader_scores = vader.polarity_scores(text)
1569
+ blob = TextBlob(text)
1570
+ combined_sentiment = (vader_scores['compound'] * 0.6) + (blob.sentiment.polarity * 0.4)
1571
+
1572
+ # Weight by engagement for Reddit
1573
+ if item['source'] == 'Reddit':
1574
+ engagement = item.get('score', 0) + item.get('num_comments', 0)
1575
+ weight = min(engagement / 100.0, 2.0) if engagement > 0 else 0.5
1576
+ else:
1577
+ weight = 1.0
1578
+
1579
+ weighted_sentiment = combined_sentiment * weight
1580
+ sentiments.append(weighted_sentiment)
1581
+
1582
+ # Track by source
1583
+ source_breakdown[item['source']].append({
1584
+ 'sentiment': weighted_sentiment,
1585
+ 'title': item['title'][:80],
1586
+ 'weight': weight
1587
+ })
1588
+
1589
+ # Calculate overall metrics
1590
+ avg_sentiment = sum(sentiments) / len(sentiments)
1591
+
1592
+ # Convert to predicted change
1593
+ predicted_change = avg_sentiment * 25.0
1594
+
1595
+ # Add confidence based on source agreement
1596
+ reddit_sentiments = [s['sentiment'] for s in source_breakdown['Reddit']]
1597
+ news_sentiments = [s['sentiment'] for s in source_breakdown['Google News']]
1598
+
1599
+ reddit_avg = sum(reddit_sentiments) / len(reddit_sentiments) if reddit_sentiments else 0
1600
+ news_avg = sum(news_sentiments) / len(news_sentiments) if news_sentiments else 0
1601
+
1602
+ # Boost prediction if sources agree
1603
+ if (reddit_avg > 0 and news_avg > 0) or (reddit_avg < 0 and news_avg < 0):
1604
+ predicted_change *= 1.2
1605
+
1606
+ # Classify prediction
1607
+ if predicted_change >= 5.0:
1608
+ prediction_label = "bullish"
1609
+ elif predicted_change <= -5.0:
1610
+ prediction_label = "bearish"
1611
+ else:
1612
+ prediction_label = "neutral"
1613
+
1614
+ return avg_sentiment, predicted_change, prediction_label, source_breakdown
1615
+
1616
+ def get_actual_performance(symbol, investment_time, investment_price):
1617
+ """Get actual stock performance after our investment"""
1618
+
1619
+ try:
1620
+ ticker = yf.Ticker(symbol)
1621
+
1622
+ # Get data from investment day
1623
+ start_date = investment_time.date()
1624
+ end_date = start_date + timedelta(days=5) # Get a few days
1625
+
1626
+ hist = ticker.history(start=start_date, end=end_date, interval='1h')
1627
+
1628
+ if hist.empty:
1629
+ return None, None, None
1630
+
1631
+ # Find first hour performance (approximate)
1632
+ day_data = hist[hist.index.date == start_date]
1633
+
1634
+ if len(day_data) > 0:
1635
+ first_price = day_data.iloc[0]['Open']
1636
+
1637
+ # First hour high (if we have hourly data)
1638
+ if len(day_data) >= 2:
1639
+ first_hour_high = day_data.iloc[0:2]['High'].max()
1640
+ first_hour_change = ((first_hour_high - first_price) / first_price) * 100
1641
+ else:
1642
+ # Fall back to first day
1643
+ first_day_close = day_data.iloc[-1]['Close']
1644
+ first_hour_change = ((first_day_close - first_price) / first_price) * 100
1645
+
1646
+ # End of day performance
1647
+ end_of_day_close = day_data.iloc[-1]['Close']
1648
+ day_change = ((end_of_day_close - first_price) / first_price) * 100
1649
+
1650
+ return first_hour_change, day_change, first_price
1651
+
1652
+ except Exception as e:
1653
+ logger.warning(f"Error getting {symbol} performance: {e}")
1654
+
1655
+ return None, None, None
1656
+
1657
+ def run_trading_history_backtest():
1658
+ """Run backtest on all our actual investments"""
1659
+
1660
+ logger.info("Starting trading history backtesting...")
1661
+
1662
+ try:
1663
+ # Get our trading history
1664
+ orders = get_order_history()
1665
+
1666
+ if not orders:
1667
+ return "❌ No trading history found", pd.DataFrame()
1668
+
1669
+ # Get all unique symbols from order history
1670
+ symbols_traded = set()
1671
+ for order in orders:
1672
+ if hasattr(order, 'symbol') and order.symbol and order.side.value == 'buy':
1673
+ symbols_traded.add(order.symbol)
1674
+
1675
+ logger.info(f"Found {len(symbols_traded)} unique symbols traded")
1676
+
1677
+ results = []
1678
+ total_error = 0
1679
+ correct_directions = 0
1680
+ valid_results = 0
1681
+
1682
+ summary_text = f"🎯 TRADING HISTORY BACKTESTING\n"
1683
+ summary_text += f"Testing sentiment analysis on {len(symbols_traded)} IPOs we actually invested in...\n"
1684
+ summary_text += f"Using news from 12 hours before our investment time\n\n"
1685
+
1686
+ # Process each symbol that was traded
1687
+ for symbol in sorted(symbols_traded):
1688
+ # Get all orders for this symbol
1689
+ symbol_orders = [o for o in orders if o.symbol == symbol]
1690
+ buy_orders = [o for o in symbol_orders if o.side.value == 'buy']
1691
+
1692
+ if buy_orders:
1693
+ # Get first buy order details
1694
+ first_buy_order = min(buy_orders, key=lambda x: x.filled_at)
1695
+ investment_time = first_buy_order.filled_at
1696
+
1697
+ total_bought = sum(float(o.filled_qty or 0) for o in buy_orders)
1698
+ total_cost = sum(float(o.filled_qty or 0) * float(o.filled_avg_price or 0) for o in buy_orders)
1699
+ avg_buy_price = total_cost / total_bought if total_bought > 0 else 0
1700
+
1701
+ logger.info(f"Analyzing {symbol} (invested {investment_time.strftime('%Y-%m-%d %H:%M')})...")
1702
+
1703
+ # Get pre-investment news
1704
+ news_items = get_pre_investment_news(symbol, investment_time)
1705
+
1706
+ # Analyze sentiment
1707
+ avg_sentiment, predicted_change, prediction_label, source_breakdown = analyze_pre_investment_sentiment(news_items)
1708
+
1709
+ # Get actual performance
1710
+ first_hour_change, day_change, actual_open = get_actual_performance(symbol, investment_time, avg_buy_price)
1711
+
1712
+ if first_hour_change is not None:
1713
+ # Calculate metrics
1714
+ error = abs(predicted_change - first_hour_change)
1715
+ total_error += error
1716
+ valid_results += 1
1717
+
1718
+ # Check direction
1719
+ predicted_direction = "UP" if predicted_change > 0 else "DOWN" if predicted_change < 0 else "FLAT"
1720
+ actual_direction = "UP" if first_hour_change > 0 else "DOWN" if first_hour_change < 0 else "FLAT"
1721
+ direction_correct = predicted_direction == actual_direction
1722
+
1723
+ if direction_correct:
1724
+ correct_directions += 1
1725
+
1726
+ # Show top sources
1727
+ reddit_items = source_breakdown['Reddit']
1728
+ news_items_found = source_breakdown['Google News']
1729
+
1730
+ top_reddit_title = ""
1731
+ if reddit_items:
1732
+ top_reddit = max(reddit_items, key=lambda x: abs(x['sentiment']))
1733
+ top_reddit_title = top_reddit['title']
1734
+
1735
+ top_news_title = ""
1736
+ if news_items_found:
1737
+ top_news = max(news_items_found, key=lambda x: abs(x['sentiment']))
1738
+ top_news_title = top_news['title']
1739
+
1740
+ result = {
1741
+ 'Symbol': symbol,
1742
+ 'Investment Date': investment_time.strftime('%Y-%m-%d'),
1743
+ 'Investment Price': f"${avg_buy_price:.2f}",
1744
+ 'Predicted Change': f"{predicted_change:+.1f}%",
1745
+ 'Actual 1H Change': f"{first_hour_change:+.1f}%",
1746
+ 'Error': f"{error:.1f}%",
1747
+ 'Direction': '✅ Correct' if direction_correct else '❌ Wrong',
1748
+ 'Sentiment': prediction_label.title(),
1749
+ 'News Sources': len(news_items),
1750
+ 'Reddit Posts': len(reddit_items),
1751
+ 'Top Reddit': top_reddit_title,
1752
+ 'Top News': top_news_title
1753
+ }
1754
+
1755
+ else:
1756
+ result = {
1757
+ 'Symbol': symbol,
1758
+ 'Investment Date': investment_time.strftime('%Y-%m-%d'),
1759
+ 'Investment Price': f"${avg_buy_price:.2f}",
1760
+ 'Predicted Change': f"{predicted_change:+.1f}%",
1761
+ 'Actual 1H Change': 'N/A',
1762
+ 'Error': 'N/A',
1763
+ 'Direction': '❓ No Data',
1764
+ 'Sentiment': prediction_label.title(),
1765
+ 'News Sources': len(news_items),
1766
+ 'Reddit Posts': len(source_breakdown['Reddit']),
1767
+ 'Top Reddit': '',
1768
+ 'Top News': ''
1769
+ }
1770
+
1771
+ results.append(result)
1772
+
1773
+ # Calculate summary statistics
1774
+ if valid_results > 0:
1775
+ avg_error = total_error / valid_results
1776
+ direction_accuracy = (correct_directions / valid_results) * 100
1777
+
1778
+ summary_text += f"📈 BACKTESTING RESULTS SUMMARY:\n"
1779
+ summary_text += f" Total Investments Tested: {len(results)}\n"
1780
+ summary_text += f" Valid Results: {valid_results}\n"
1781
+ summary_text += f" Average Error: {avg_error:.1f}%\n"
1782
+ summary_text += f" Direction Accuracy: {direction_accuracy:.1f}% ({correct_directions}/{valid_results})\n\n"
1783
+
1784
+ if direction_accuracy >= 60:
1785
+ summary_text += f" ✅ Strong predictive value!\n"
1786
+ elif direction_accuracy >= 40:
1787
+ summary_text += f" ⚡ Some predictive value\n"
1788
+ else:
1789
+ summary_text += f" ❌ Needs improvement\n"
1790
+ else:
1791
+ summary_text += f"❌ No valid results available for analysis\n"
1792
+
1793
+ # Create DataFrame
1794
+ df = pd.DataFrame(results)
1795
+
1796
+ return summary_text, df
1797
+
1798
+ except Exception as e:
1799
+ error_msg = f"❌ Error running backtesting: {str(e)}"
1800
+ logger.error(error_msg)
1801
+ return error_msg, pd.DataFrame()
1802
+
1803
+ def clear_terminal():
1804
+ """Clear terminal output"""
1805
+ return "🖥️ VM Terminal Ready\n$ "
1806
+
1807
+ def run_quick_command(cmd):
1808
+ """Helper for quick command buttons"""
1809
+ def execute(current_output):
1810
+ return run_vm_command(cmd, current_output)
1811
+ return execute
1812
+
1813
+ # Custom CSS for gorgeous design
1814
+ custom_css = """
1815
+ .gradio-container {
1816
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
1817
+ background: #fafafa !important;
1818
+ }
1819
+
1820
+ .main-header {
1821
+ background: linear-gradient(135deg, #0070f3 0%, #0051a5 100%);
1822
+ color: white;
1823
+ padding: 2rem;
1824
+ border-radius: 16px;
1825
+ margin-bottom: 2rem;
1826
+ box-shadow: 0 10px 40px rgba(0, 112, 243, 0.3);
1827
+ }
1828
+
1829
+ .metric-card {
1830
+ background: white;
1831
+ border: 1px solid #eaeaea;
1832
+ border-radius: 12px;
1833
+ padding: 1.5rem;
1834
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.04);
1835
+ transition: all 0.3s ease;
1836
+ }
1837
+
1838
+ .metric-card:hover {
1839
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
1840
+ transform: translateY(-4px);
1841
+ }
1842
+
1843
+ .gr-button {
1844
+ background: linear-gradient(135deg, #0070f3 0%, #0051a5 100%) !important;
1845
+ color: white !important;
1846
+ border: none !important;
1847
+ border-radius: 12px !important;
1848
+ font-weight: 600 !important;
1849
+ padding: 1rem 2rem !important;
1850
+ transition: all 0.3s ease !important;
1851
+ box-shadow: 0 4px 16px rgba(0, 112, 243, 0.3) !important;
1852
+ }
1853
+
1854
+ .gr-button:hover {
1855
+ transform: translateY(-2px) !important;
1856
+ box-shadow: 0 8px 32px rgba(0, 112, 243, 0.4) !important;
1857
+ }
1858
+
1859
+ .gr-textbox, .gr-dataframe {
1860
+ border: 1px solid #eaeaea !important;
1861
+ border-radius: 12px !important;
1862
+ background: white !important;
1863
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04) !important;
1864
+ }
1865
+
1866
+ .plotly-graph-div {
1867
+ border-radius: 16px !important;
1868
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08) !important;
1869
+ background: white !important;
1870
+ }
1871
+
1872
+ .status-invested { color: #00d647 !important; font-weight: 600 !important; }
1873
+ .status-eligible { color: #f5a623 !important; font-weight: 600 !important; }
1874
+ .status-wrong { color: #8b949e !important; }
1875
+ .status-unknown { color: #ff0080 !important; }
1876
+
1877
+ /* Investment Performance Table Styling */
1878
+ .investment-table {
1879
+ width: 100%;
1880
+ border-collapse: separate;
1881
+ border-spacing: 0;
1882
+ background: white;
1883
+ border-radius: 16px;
1884
+ overflow: hidden;
1885
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
1886
+ }
1887
+
1888
+ .investment-table th {
1889
+ background: linear-gradient(135deg, #0070f3 0%, #0051a5 100%);
1890
+ color: white;
1891
+ padding: 1rem;
1892
+ font-weight: 600;
1893
+ text-align: left;
1894
+ border: none;
1895
+ }
1896
+
1897
+ .investment-table th:first-child {
1898
+ border-top-left-radius: 16px;
1899
+ }
1900
+
1901
+ .investment-table th:last-child {
1902
+ border-top-right-radius: 16px;
1903
+ }
1904
+
1905
+ .investment-table td {
1906
+ padding: 1rem;
1907
+ border-bottom: 1px solid #f5f5f5;
1908
+ font-weight: 500;
1909
+ }
1910
+
1911
+ .profit-row {
1912
+ background: rgba(0, 214, 71, 0.1) !important;
1913
+ border-left: 4px solid #00d647;
1914
+ }
1915
+
1916
+ .loss-row {
1917
+ background: rgba(255, 0, 128, 0.1) !important;
1918
+ border-left: 4px solid #ff0080;
1919
+ }
1920
+
1921
+ .neutral-row {
1922
+ background: rgba(139, 148, 158, 0.05) !important;
1923
+ border-left: 4px solid #8b949e;
1924
+ }
1925
+
1926
+ .investment-table tr:last-child td:first-child {
1927
+ border-bottom-left-radius: 16px;
1928
+ }
1929
+
1930
+ .investment-table tr:last-child td:last-child {
1931
+ border-bottom-right-radius: 16px;
1932
+ }
1933
+
1934
+ .profit-positive { color: #00d647 !important; font-weight: 600 !important; }
1935
+ .profit-negative { color: #ff0080 !important; font-weight: 600 !important; }
1936
+ .profit-neutral { color: #8b949e !important; }
1937
+
1938
+ .terminal-container {
1939
+ background: #000000 !important;
1940
+ border: 1px solid #333 !important;
1941
+ border-radius: 8px !important;
1942
+ padding: 0 !important;
1943
+ margin: 1rem 0 !important;
1944
+ height: 500px !important;
1945
+ overflow-y: auto !important;
1946
+ display: flex !important;
1947
+ flex-direction: column !important;
1948
+ /* Hide scrollbars but keep functionality */
1949
+ scrollbar-width: none !important; /* Firefox */
1950
+ -ms-overflow-style: none !important; /* IE/Edge */
1951
+ }
1952
+
1953
+ .terminal-container::-webkit-scrollbar {
1954
+ display: none !important; /* Chrome/Safari/Webkit */
1955
+ }
1956
+
1957
+ .terminal-display {
1958
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace !important;
1959
+ background: #000000 !important;
1960
+ color: #ffffff !important;
1961
+ padding: 1rem !important;
1962
+ font-size: 14px !important;
1963
+ line-height: 1.4 !important;
1964
+ white-space: pre-wrap !important;
1965
+ word-wrap: break-word !important;
1966
+ margin: 0 !important;
1967
+ flex-grow: 1 !important;
1968
+ overflow-anchor: none !important;
1969
+ /* Always stick to bottom */
1970
+ display: flex !important;
1971
+ flex-direction: column !important;
1972
+ justify-content: flex-end !important;
1973
+ }
1974
+
1975
+ .terminal-display::-webkit-scrollbar {
1976
+ display: none !important; /* Chrome/Safari/Webkit */
1977
+ }
1978
+
1979
+ .terminal-input input {
1980
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace !important;
1981
+ background: #1a1a1a !important;
1982
+ color: #ffffff !important;
1983
+ border: 1px solid #333 !important;
1984
+ border-radius: 4px !important;
1985
+ font-size: 14px !important;
1986
+ }
1987
+
1988
+ .terminal-input input:focus {
1989
+ border-color: #00ff00 !important;
1990
+ box-shadow: 0 0 5px rgba(0, 255, 0, 0.3) !important;
1991
+ }
1992
+
1993
+ /* Force Gradio HTML to stick to bottom */
1994
+ .gr-html {
1995
+ height: 500px !important;
1996
+ overflow-y: auto !important;
1997
+ scrollbar-width: none !important;
1998
+ -ms-overflow-style: none !important;
1999
+ display: flex !important;
2000
+ flex-direction: column !important;
2001
+ }
2002
+
2003
+ .gr-html::-webkit-scrollbar {
2004
+ display: none !important;
2005
+ }
2006
+
2007
+ /* Force content to bottom with CSS anchor */
2008
+ .gr-html > div {
2009
+ display: flex !important;
2010
+ flex-direction: column !important;
2011
+ justify-content: flex-end !important;
2012
+ min-height: 100% !important;
2013
+ }
2014
+ """
2015
+
2016
+ def create_dashboard():
2017
+ logger.info("🎨 Creating Gradio dashboard interface...")
2018
+
2019
+ try:
2020
+ with gr.Blocks(
2021
+ title="🚀 Premium Trading Dashboard",
2022
+ theme=gr.themes.Soft(primary_hue="blue"),
2023
+ css=custom_css
2024
+ ) as demo:
2025
+ logger.info("🖼️ Dashboard blocks created successfully")
2026
+
2027
+ # Header
2028
+ gr.HTML("""
2029
+ <div class="main-header">
2030
+ <h1 style="margin: 0; font-size: 3rem; font-weight: 800; text-shadow: 0 2px 4px rgba(0,0,0,0.1);">
2031
+ 🚀 Premium Trading Dashboard
2032
+ </h1>
2033
+ <p style="margin: 1rem 0 0 0; font-size: 1.3rem; opacity: 0.95;">
2034
+ Real-time portfolio monitoring with IPO discovery analytics
2035
+ </p>
2036
+ </div>
2037
+ """)
2038
+
2039
+ with gr.Tabs():
2040
+ # Portfolio Overview Tab
2041
+ with gr.Tab("📊 Portfolio Overview"):
2042
+ gr.Markdown("## 💼 Account Summary")
2043
+ with gr.Row():
2044
+ portfolio_value = gr.Textbox(label="💰 Portfolio Value", interactive=False)
2045
+ buying_power = gr.Textbox(label="💳 Buying Power", interactive=False)
2046
+ cash = gr.Textbox(label="💵 Cash", interactive=False)
2047
+ day_change = gr.Textbox(label="📈 Day Change", interactive=False)
2048
+ equity = gr.Textbox(label="🏦 Total Equity", interactive=False)
2049
+
2050
+ gr.Markdown("## 📈 Portfolio Performance")
2051
+ portfolio_chart = gr.Plot(label="Portfolio Value Over Time")
2052
+
2053
+ refresh_overview_btn = gr.Button("🔄 Refresh Portfolio Data", variant="primary", size="lg")
2054
+
2055
+ # IPO Discoveries Tab
2056
+ with gr.Tab("🔍 IPO Discoveries"):
2057
+ gr.Markdown("## 📊 IPO Discovery Analytics")
2058
+
2059
+ with gr.Row():
2060
+ total_ipos = gr.Textbox(label="🎯 Total IPOs Detected", interactive=False)
2061
+ ipos_invested = gr.Textbox(label="💰 IPOs Invested", interactive=False)
2062
+ cs_stocks = gr.Textbox(label="📈 CS Stocks Found", interactive=False)
2063
+ investment_rate = gr.Textbox(label="🎲 Investment Rate", interactive=False)
2064
+ last_updated = gr.Textbox(label="🕒 Last Updated", interactive=False)
2065
+
2066
+ with gr.Row():
2067
+ with gr.Column(scale=1):
2068
+ ipo_chart = gr.Plot(label="Investment Decision Breakdown")
2069
+
2070
+ with gr.Column(scale=2):
2071
+ gr.Markdown("## 🆕 Recent IPO Discoveries")
2072
+ ipo_table = gr.Dataframe(
2073
+ label="IPO Discoveries with Investment Decisions",
2074
+ )
2075
+
2076
+ refresh_ipo_btn = gr.Button("🔄 Refresh IPO Data", variant="primary", size="lg")
2077
+
2078
+ # Investment Performance Tab
2079
+ with gr.Tab("💰 Investment Performance"):
2080
+ gr.Markdown("## 🎯 IPO Investment Performance")
2081
+ gr.Markdown("### Track profit/loss on your IPO investments with **real-time sentiment analysis**")
2082
+ gr.Markdown("🧠 **NEW**: Each row automatically shows sentiment predictions from Reddit + Google News!")
2083
+
2084
+ investment_performance_table = gr.HTML(
2085
+ label="IPO Investment P&L Analysis",
2086
+ value="<div style='text-align: center; padding: 2rem; color: #666;'>Click Refresh to load investment performance data</div>"
2087
+ )
2088
+ refresh_investment_btn = gr.Button("🔄 Refresh Investment Performance", variant="primary", size="lg")
2089
+
2090
+ gr.Markdown("### 🧮 Trading Statistics & Analysis")
2091
+ gr.Markdown("Calculate interesting metrics from your trading data")
2092
+
2093
+ with gr.Row():
2094
+ calc_sequential_btn = gr.Button("📈 Sequential Reinvestment P&L%", variant="secondary", size="sm")
2095
+ calc_equal_weight_btn = gr.Button("⚖️ Equal Weight Portfolio P&L%", variant="secondary", size="sm")
2096
+ calc_best_worst_btn = gr.Button("🏆 Best vs Worst Performers", variant="secondary", size="sm")
2097
+
2098
+ with gr.Row():
2099
+ calc_win_rate_btn = gr.Button("🎯 Win Rate & Avg Returns", variant="secondary", size="sm")
2100
+ calc_risk_metrics_btn = gr.Button("⚠️ Risk Metrics & Volatility", variant="secondary", size="sm")
2101
+ calc_time_analysis_btn = gr.Button("⏰ Time-based Performance", variant="secondary", size="sm")
2102
+
2103
+ stats_output = gr.Textbox(
2104
+ label="Statistical Analysis Results",
2105
+ lines=8,
2106
+ interactive=False,
2107
+ )
2108
+
2109
+ gr.Markdown("### 🔧 Debug API Calls")
2110
+ debug_output = gr.Textbox(
2111
+ label="Debug Output",
2112
+ lines=10,
2113
+ interactive=False
2114
+ )
2115
+
2116
+ with gr.Row():
2117
+ debug_orders_btn = gr.Button("🔍 Debug Order History", variant="secondary")
2118
+ debug_positions_btn = gr.Button("📊 Debug Current Positions", variant="secondary")
2119
+ debug_ipos_btn = gr.Button("🎯 Debug IPO Data", variant="secondary")
2120
+ debug_account_btn = gr.Button("💼 Debug Account Info", variant="secondary")
2121
+
2122
+ # VM Terminal Tab
2123
+ with gr.Tab("💻 VM Terminal"):
2124
+ gr.Markdown("## 🖥️ Remote VM Terminal")
2125
+ gr.Markdown("### Execute commands directly on your trading VM")
2126
+
2127
+ # Hidden state for command history
2128
+ command_history = gr.State("")
2129
+
2130
+ with gr.Row():
2131
+ with gr.Column(scale=4):
2132
+ command_input = gr.Textbox(
2133
+ label="Command (Press Enter to run)",
2134
+ placeholder="Enter command to run on VM...",
2135
+ interactive=True,
2136
+ )
2137
+ with gr.Column(scale=1):
2138
+ run_command_btn = gr.Button("▶️ Run", variant="primary", size="lg")
2139
+ clear_terminal_btn = gr.Button("🗑️ Clear", variant="secondary", size="lg")
2140
+
2141
+ terminal_output = gr.HTML(
2142
+ label="Terminal Output",
2143
+ value='<div class="terminal-display" id="terminal-content">🖥️ VM Terminal Ready<br>$ </div>',
2144
+ )
2145
+
2146
+ gr.Markdown("**📁 File & System Commands:**")
2147
+ with gr.Row():
2148
+ quick_ls = gr.Button("📁 ls -la", size="sm")
2149
+ quick_pwd = gr.Button("📍 pwd", size="sm")
2150
+ quick_ps = gr.Button("🔄 ps aux | grep python", size="sm")
2151
+ quick_vm_status = gr.Button("🖥️ uptime && df -h", size="sm")
2152
+ quick_who = gr.Button("👤 whoami", size="sm")
2153
+
2154
+ gr.Markdown("**📋 Log Files:**")
2155
+ with gr.Row():
2156
+ quick_script_log = gr.Button("📜 tail -50 script.log", size="sm")
2157
+ quick_server_log = gr.Button("🖥️ tail -50 server.log", size="sm")
2158
+ quick_cron_log = gr.Button("⏰ tail -50 /var/log/cron", size="sm")
2159
+ quick_portfolio = gr.Button("💼 cat portfolio.txt", size="sm")
2160
+ quick_tickers = gr.Button("🎯 head -20 new_tickers_log.csv", size="sm")
2161
+
2162
+ gr.Markdown("**🔍 Search & Analysis:**")
2163
+ with gr.Row():
2164
+ quick_errors = gr.Button("🚨 grep -i error script.log | tail -10", size="sm")
2165
+ quick_trades = gr.Button("💰 grep -i 'buy\\|sell' script.log | tail -10", size="sm")
2166
+ quick_ipos = gr.Button("🆕 grep -i 'new ticker' script.log | tail -10", size="sm")
2167
+
2168
+ # System Logs Tab
2169
+ with gr.Tab("📋 System Logs"):
2170
+ gr.Markdown("## 🖥️ Trading Bot Activity")
2171
+
2172
+ with gr.Row():
2173
+ with gr.Column():
2174
+ gr.Markdown("### 🎯 Parsed Logs (Color Coded)")
2175
+ system_logs = gr.Textbox(
2176
+ label="Recent System Activity",
2177
+ lines=20,
2178
+ max_lines=20,
2179
+ interactive=False,
2180
+ )
2181
+
2182
+ with gr.Column():
2183
+ gr.Markdown("### 📄 Raw Cron Logs")
2184
+ raw_logs = gr.Textbox(
2185
+ label="Raw Log Output",
2186
+ lines=20,
2187
+ max_lines=20,
2188
+ interactive=False,
2189
+ )
2190
+
2191
+ refresh_logs_btn = gr.Button("🔄 Refresh All Logs", variant="primary", size="lg")
2192
+
2193
+ # Footer
2194
+ gr.HTML("""
2195
+ <div style="text-align: center; padding: 2rem; color: #666; border-top: 1px solid #eaeaea; margin-top: 3rem; background: white; border-radius: 16px;">
2196
+ <p style="font-size: 1.1rem;"><strong>🤖 Automated Trading Dashboard</strong></p>
2197
+ <p style="font-size: 0.95rem;">Real-time data from Alpaca Markets + VM Analytics | Built with ❤️</p>
2198
+ </div>
2199
+ """)
2200
+
2201
+ logger.info("🔗 Setting up event handlers...")
2202
+
2203
+ # Event Handlers - INSIDE Blocks context
2204
+
2205
+ # Portfolio tab
2206
+ refresh_overview_btn.click(
2207
+ fn=refresh_account_overview,
2208
+ outputs=[portfolio_value, buying_power, cash, day_change, equity]
2209
+ )
2210
+ refresh_overview_btn.click(
2211
+ fn=create_portfolio_chart,
2212
+ outputs=[portfolio_chart]
2213
+ )
2214
+
2215
+ # IPO tab
2216
+ refresh_ipo_btn.click(
2217
+ fn=refresh_vm_stats,
2218
+ outputs=[total_ipos, ipos_invested, cs_stocks, investment_rate, last_updated]
2219
+ )
2220
+ refresh_ipo_btn.click(
2221
+ fn=create_ipo_discovery_chart,
2222
+ outputs=[ipo_chart]
2223
+ )
2224
+ refresh_ipo_btn.click(
2225
+ fn=refresh_ipo_discoveries_table,
2226
+ outputs=[ipo_table]
2227
+ )
2228
+
2229
+ # Investment Performance tab
2230
+ refresh_performance_btn.click(
2231
+ fn=refresh_investment_performance_html,
2232
+ outputs=[investment_performance_table]
2233
+ )
2234
+
2235
+ # VM Terminal tab
2236
+ execute_btn.click(
2237
+ fn=execute_command,
2238
+ inputs=[command_input],
2239
+ outputs=[terminal_output]
2240
+ )
2241
+
2242
+ # System Logs tab
2243
+ refresh_logs_btn.click(
2244
+ fn=refresh_vm_logs,
2245
+ outputs=[log_output]
2246
+ )
2247
+
2248
+ # Initial data load
2249
+ demo.load(
2250
+ fn=refresh_account_overview,
2251
+ outputs=[portfolio_value, buying_power, cash, day_change, equity]
2252
+ )
2253
+ demo.load(fn=create_portfolio_chart, outputs=[portfolio_chart])
2254
+ demo.load(
2255
+ fn=refresh_vm_stats,
2256
+ outputs=[total_ipos, ipos_invested, cs_stocks, investment_rate, last_updated]
2257
+ )
2258
+ demo.load(fn=create_ipo_discovery_chart, outputs=[ipo_chart])
2259
+ demo.load(fn=refresh_ipo_discoveries_table, outputs=[ipo_table])
2260
+ demo.load(fn=refresh_investment_performance_html, outputs=[investment_performance_table])
2261
+
2262
+ demo.queue()
2263
+ logger.info("✅ All event handlers configured successfully")
2264
+ return demo
2265
+
2266
+ except Exception as e:
2267
+ logger.error(f"❌ Failed to create dashboard: {e}")
2268
+ raise
2269
+
2270
+ # Create and launch
2271
+ logger.info("🏗️ Building dashboard...")
2272
+ try:
2273
+ demo = create_dashboard()
2274
+ logger.info("✅ Dashboard created successfully!")
2275
+ except Exception as e:
2276
+ logger.error(f"❌ Dashboard creation failed: {e}")
2277
+ raise
2278
+
2279
+ if __name__ == "__main__":
2280
+ logger.info("🚀 Launching dashboard server...")
2281
+ try:
2282
+ demo.launch()
2283
+ logger.info("✅ Dashboard launched successfully!")
2284
+ except Exception as e:
2285
+ logger.error(f"❌ Dashboard launch failed: {e}")
2286
+ raise
app_minimal.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Minimal working Gradio app to test deployment
4
+ """
5
+
6
+ import gradio as gr
7
+ import logging
8
+
9
+ # Configure logging
10
+ logging.basicConfig(level=logging.INFO)
11
+ logger = logging.getLogger(__name__)
12
+
13
+ def greet(name):
14
+ """Simple greeting function"""
15
+ logger.info(f"Greeting user: {name}")
16
+ return f"Hello {name}! 🚀 Trading Dashboard works!"
17
+
18
+ def refresh_data():
19
+ """Simple refresh function"""
20
+ logger.info("Refreshing data...")
21
+ return "Data refreshed! ✅"
22
+
23
+ def create_minimal_dashboard():
24
+ """Create minimal dashboard to test Gradio"""
25
+
26
+ logger.info("🚀 Creating minimal dashboard...")
27
+
28
+ with gr.Blocks(title="Test Dashboard") as demo:
29
+ logger.info("🎨 Inside Blocks context...")
30
+
31
+ gr.Markdown("# 🧪 Test Trading Dashboard")
32
+
33
+ with gr.Row():
34
+ name_input = gr.Textbox(label="Your Name", placeholder="Enter your name")
35
+ greeting_output = gr.Textbox(label="Greeting", interactive=False)
36
+
37
+ with gr.Row():
38
+ greet_btn = gr.Button("👋 Greet", variant="primary")
39
+ refresh_btn = gr.Button("🔄 Refresh", variant="secondary")
40
+
41
+ status_output = gr.Textbox(label="Status", interactive=False)
42
+
43
+ # Event handlers - INSIDE the Blocks context
44
+ logger.info("🔗 Setting up event handlers...")
45
+
46
+ greet_btn.click(
47
+ fn=greet,
48
+ inputs=[name_input],
49
+ outputs=[greeting_output]
50
+ )
51
+
52
+ refresh_btn.click(
53
+ fn=refresh_data,
54
+ outputs=[status_output]
55
+ )
56
+
57
+ # Initial load
58
+ demo.load(
59
+ fn=lambda: "Ready to test! 🎯",
60
+ outputs=[status_output]
61
+ )
62
+
63
+ demo.queue()
64
+ logger.info("✅ Event handlers configured successfully")
65
+
66
+ logger.info("✅ Dashboard created successfully")
67
+ return demo
68
+
69
+ if __name__ == "__main__":
70
+ logger.info("🚀 Starting minimal test dashboard...")
71
+
72
+ try:
73
+ demo = create_minimal_dashboard()
74
+ logger.info("✅ Dashboard created successfully!")
75
+
76
+ logger.info("🚀 Launching dashboard server...")
77
+ demo.launch()
78
+ logger.info("✅ Dashboard launched successfully!")
79
+
80
+ except Exception as e:
81
+ logger.error(f"❌ Dashboard failed: {e}")
82
+ raise