jetpackjules Claude commited on
Commit
d43088c
Β·
1 Parent(s): 4bae4e2

Add Investment Performance tab and VM Terminal with comprehensive features

Browse files

- Added Investment Performance tab with P&L analysis for invested IPOs
- Added VM Terminal tab with command execution and color-coded output
- Added comprehensive debug functions for troubleshooting order history
- Fixed OrderStatus enum validation error by using string "closed" instead of enum
- Added terminal with newest-commands-at-top approach and proper formatting
- Enhanced dashboard with 6 tabs: Portfolio, IPO Discoveries, Positions, Investment Performance, VM Terminal, System Logs

πŸ€– Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

Files changed (1) hide show
  1. app.py +656 -1
app.py CHANGED
@@ -197,7 +197,7 @@ def create_portfolio_chart():
197
 
198
  def create_ipo_discovery_chart():
199
  """Create IPO discovery chart with investment decisions"""
200
- ipos = fetch_from_vm('ipos?limit=30', [])
201
 
202
  if not ipos:
203
  fig = go.Figure()
@@ -322,6 +322,142 @@ def refresh_ipo_discoveries_table():
322
 
323
  return pd.DataFrame(df_data)
324
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
325
  def refresh_vm_stats():
326
  """Refresh VM statistics"""
327
  stats = fetch_from_vm('stats', {})
@@ -368,6 +504,220 @@ def refresh_raw_logs():
368
  header = f"=== RAW CRON LOGS ===\nShowing last {showing_lines} of {total_lines} total lines\n\n"
369
  return header + content
370
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
371
  # Custom CSS for gorgeous design
372
  custom_css = """
373
  .gradio-container {
@@ -431,6 +781,87 @@ custom_css = """
431
  .status-eligible { color: #f5a623 !important; font-weight: 600 !important; }
432
  .status-wrong { color: #8b949e !important; }
433
  .status-unknown { color: #ff0080 !important; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
434
  """
435
 
436
  def create_dashboard():
@@ -498,6 +929,78 @@ def create_dashboard():
498
  positions_table = gr.Dataframe(label="Current Holdings", elem_classes=["gr-dataframe"])
499
  refresh_positions_btn = gr.Button("πŸ”„ Refresh Positions", variant="primary", size="lg")
500
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
501
  # System Logs Tab
502
  with gr.Tab("πŸ“‹ System Logs"):
503
  gr.Markdown("## πŸ–₯️ Trading Bot Activity")
@@ -565,6 +1068,157 @@ def create_dashboard():
565
  outputs=[positions_table]
566
  )
567
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
568
  # Logs tab
569
  refresh_logs_btn.click(
570
  fn=refresh_system_logs,
@@ -588,6 +1242,7 @@ def create_dashboard():
588
  )
589
  demo.load(fn=create_ipo_discovery_chart, outputs=[ipo_chart])
590
  demo.load(fn=refresh_ipo_discoveries_table, outputs=[ipo_table])
 
591
 
592
  return demo
593
 
 
197
 
198
  def create_ipo_discovery_chart():
199
  """Create IPO discovery chart with investment decisions"""
200
+ ipos = fetch_from_vm('ipos?limit=100', [])
201
 
202
  if not ipos:
203
  fig = go.Figure()
 
322
 
323
  return pd.DataFrame(df_data)
324
 
325
+ def get_order_history():
326
+ """Get order history from Alpaca"""
327
+ try:
328
+ # Get orders from last 6 months with ALL statuses
329
+ end_date = datetime.now(timezone.utc)
330
+ start_date = end_date - timedelta(days=180)
331
+
332
+ order_request = GetOrdersRequest(
333
+ # Remove status filter to get ALL orders
334
+ limit=500,
335
+ after=start_date,
336
+ until=end_date
337
+ )
338
+
339
+ orders = trading_client.get_orders(order_request)
340
+ return orders
341
+ except Exception as e:
342
+ logger.error(f"Error fetching order history: {e}")
343
+ return []
344
+
345
+ def refresh_investment_performance_table():
346
+ """Refresh investment performance table with P&L for invested IPOs"""
347
+ # Get IPO data and orders
348
+ ipos = fetch_from_vm('ipos?limit=100', [])
349
+ orders = get_order_history()
350
+ positions = get_current_positions()
351
+
352
+ # Create proper empty DataFrame with correct column names
353
+ columns = ['Symbol', 'Status', 'IPO Price', 'Buy Price', 'Current Price', 'Quantity', 'Investment', 'Current Value', 'P&L ($)', 'P&L (%)']
354
+
355
+ if not ipos:
356
+ return pd.DataFrame(columns=columns)
357
+
358
+ # Debug: Let's see what we're getting
359
+ logger.info(f"Found {len(ipos)} total IPOs")
360
+ invested_symbols = [ipo for ipo in ipos if ipo.get('investment_status') == 'INVESTED']
361
+ logger.info(f"Found {len(invested_symbols)} invested IPOs")
362
+ logger.info(f"Found {len(orders)} total orders")
363
+
364
+ invested_data = []
365
+
366
+ if not invested_symbols:
367
+ # No invested IPOs found, add a debug row to see what's happening
368
+ invested_data.append({
369
+ 'Symbol': "πŸ” DEBUG",
370
+ 'Status': f"Found {len(ipos)} IPOs total",
371
+ 'IPO Price': "Check logs",
372
+ 'Buy Price': "N/A",
373
+ 'Current Price': "N/A",
374
+ 'Quantity': "0",
375
+ 'Investment': "$0.00",
376
+ 'Current Value': "N/A",
377
+ 'P&L ($)': "$0.00",
378
+ 'P&L (%)': "0.00%"
379
+ })
380
+ return pd.DataFrame(invested_data)
381
+
382
+ # Simple version first - just show the IPOs that were invested in
383
+ for ipo in invested_symbols:
384
+ symbol = ipo.get('symbol', 'N/A')
385
+ ipo_price = ipo.get('trading_price', 0)
386
+
387
+ # Check if we have order data for this symbol
388
+ symbol_orders = [o for o in orders if o.symbol == symbol]
389
+
390
+ if symbol_orders:
391
+ # Calculate from actual orders
392
+ buy_orders = [o for o in symbol_orders if o.side.value == 'buy']
393
+ sell_orders = [o for o in symbol_orders if o.side.value == 'sell']
394
+
395
+ if buy_orders:
396
+ total_bought = sum(float(o.filled_qty or 0) for o in buy_orders)
397
+ total_cost = sum(float(o.filled_qty or 0) * float(o.filled_avg_price or 0) for o in buy_orders)
398
+ avg_buy_price = total_cost / total_bought if total_bought > 0 else 0
399
+
400
+ total_sold = sum(float(o.filled_qty or 0) for o in sell_orders)
401
+ current_qty = total_bought - total_sold
402
+
403
+ if current_qty > 0:
404
+ # Still holding
405
+ status = "🟦 HOLDING"
406
+ pos = next((p for p in positions if p['symbol'] == symbol), None)
407
+ if pos:
408
+ current_price = pos['current_price']
409
+ current_value = current_qty * current_price
410
+ investment = current_qty * avg_buy_price
411
+ pl_dollars = current_value - investment
412
+ pl_percent = (pl_dollars / investment * 100) if investment > 0 else 0
413
+ else:
414
+ current_price = 0
415
+ current_value = 0
416
+ investment = current_qty * avg_buy_price
417
+ pl_dollars = 0
418
+ pl_percent = 0
419
+ else:
420
+ # Sold all
421
+ status = "🟨 SOLD"
422
+ current_price = 0
423
+ current_value = 0
424
+ investment = total_cost
425
+ sold_value = sum(float(o.filled_qty or 0) * float(o.filled_avg_price or 0) for o in sell_orders)
426
+ pl_dollars = sold_value - investment
427
+ pl_percent = (pl_dollars / investment * 100) if investment > 0 else 0
428
+
429
+ # Color indicator
430
+ pl_indicator = "🟒" if pl_dollars > 0 else "πŸ”΄" if pl_dollars < 0 else "βšͺ"
431
+
432
+ invested_data.append({
433
+ 'Symbol': f"{pl_indicator} {symbol}",
434
+ 'Status': status,
435
+ 'IPO Price': f"${ipo_price}" if ipo_price != 'N/A' else 'N/A',
436
+ 'Buy Price': f"${avg_buy_price:.2f}",
437
+ 'Current Price': f"${current_price:.2f}" if current_price > 0 else "N/A",
438
+ 'Quantity': f"{current_qty:.0f}",
439
+ 'Investment': f"${investment:.2f}",
440
+ 'Current Value': f"${current_value:.2f}" if current_value > 0 else "N/A",
441
+ 'P&L ($)': f"${pl_dollars:+.2f}",
442
+ 'P&L (%)': f"{pl_percent:+.2f}%"
443
+ })
444
+ else:
445
+ # No order data found, show placeholder
446
+ invested_data.append({
447
+ 'Symbol': f"❓ {symbol}",
448
+ 'Status': "πŸ” NO ORDERS",
449
+ 'IPO Price': f"${ipo_price}" if ipo_price != 'N/A' else 'N/A',
450
+ 'Buy Price': "N/A",
451
+ 'Current Price': "N/A",
452
+ 'Quantity': "0",
453
+ 'Investment': "$0.00",
454
+ 'Current Value': "N/A",
455
+ 'P&L ($)': "$0.00",
456
+ 'P&L (%)': "0.00%"
457
+ })
458
+
459
+ return pd.DataFrame(invested_data)
460
+
461
  def refresh_vm_stats():
462
  """Refresh VM statistics"""
463
  stats = fetch_from_vm('stats', {})
 
504
  header = f"=== RAW CRON LOGS ===\nShowing last {showing_lines} of {total_lines} total lines\n\n"
505
  return header + content
506
 
507
+ def run_vm_command(command, current_output="", command_history=""):
508
+ """Execute command on VM and return output"""
509
+ try:
510
+ if not command.strip():
511
+ return current_output, "", command_history
512
+
513
+ # Add command to history
514
+ history_list = command_history.split("|||") if command_history else []
515
+ if command not in history_list:
516
+ history_list.append(command)
517
+ # Keep last 50 commands
518
+ history_list = history_list[-50:]
519
+ new_history = "|||".join(history_list)
520
+
521
+ response = requests.post(f"{VM_API_URL}/api/execute",
522
+ json={"command": command},
523
+ timeout=10)
524
+
525
+ if response.status_code == 200:
526
+ data = response.json()
527
+ output = data.get('output', '')
528
+ exit_code = data.get('exit_code', 0)
529
+
530
+ # Add color coding for common patterns
531
+ colored_output = colorize_output(output)
532
+
533
+ # Format terminal-style output with clean spacing
534
+ # Clean up output to avoid weird quote formatting
535
+ clean_output = colored_output.strip().replace('\r', '')
536
+ new_line = f"$ {command}\n{clean_output}"
537
+ if exit_code != 0:
538
+ new_line += f"\n[Exit code: {exit_code}]"
539
+ new_line += "\n$ "
540
+
541
+ # RADICAL FIX: Put newest content at TOP instead of bottom!
542
+ if current_output.strip():
543
+ full_output = new_line + "\n" + current_output.rstrip()
544
+ else:
545
+ full_output = new_line
546
+
547
+ return full_output, "", new_history
548
+ else:
549
+ error_line = f"\n$ {command}\nError: VM API returned {response.status_code}\n$ "
550
+ return current_output + error_line, "", new_history
551
+
552
+ except Exception as e:
553
+ error_line = f"\n$ {command}\nError: {str(e)}\n$ "
554
+ return current_output + error_line, "", new_history
555
+
556
+ def colorize_output(output):
557
+ """Add basic color coding to terminal output"""
558
+ import re
559
+
560
+ # Color patterns (using ANSI-like styling for web)
561
+ colored = output
562
+
563
+ # File permissions and directories (ls output)
564
+ colored = re.sub(r'^(d)([rwx-]{9})', r'<span style="color: #4A90E2;">\1\2</span>', colored, flags=re.MULTILINE)
565
+ colored = re.sub(r'^(-)([rwx-]{9})', r'<span style="color: #50E3C2;">\1\2</span>', colored, flags=re.MULTILINE)
566
+
567
+ # Error messages
568
+ colored = re.sub(r'(ERROR|Error|error)', r'<span style="color: #FF6B6B;">\1</span>', colored)
569
+ colored = re.sub(r'(WARNING|Warning|warning)', r'<span style="color: #FFD93D;">\1</span>', colored)
570
+
571
+ # Success indicators
572
+ colored = re.sub(r'(SUCCESS|Success|success)', r'<span style="color: #6BCF7F;">\1</span>', colored)
573
+
574
+ # File extensions
575
+ colored = re.sub(r'(\w+\.(py|log|csv|json|txt))', r'<span style="color: #BD93F9;">\1</span>', colored)
576
+
577
+ # Numbers and timestamps
578
+ colored = re.sub(r'(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})', r'<span style="color: #50FA7B;">\1</span>', colored)
579
+
580
+ return colored
581
+
582
+ def debug_order_history():
583
+ """Debug function to show raw order history data"""
584
+ try:
585
+ # Try multiple approaches to get orders
586
+ debug_info = f"=== ORDER HISTORY DEBUG ===\n"
587
+
588
+ # Approach 1: All orders, last 6 months
589
+ orders = get_order_history()
590
+ debug_info += f"Method 1 (6 months, all statuses): {len(orders)} orders\n"
591
+
592
+ # Approach 2: Just filled orders, last year
593
+ try:
594
+ end_date = datetime.now(timezone.utc)
595
+ start_date = end_date - timedelta(days=365)
596
+ filled_request = GetOrdersRequest(
597
+ status="closed", # Use string instead of enum
598
+ limit=500,
599
+ after=start_date,
600
+ until=end_date
601
+ )
602
+ filled_orders = trading_client.get_orders(filled_request)
603
+ debug_info += f"Method 2 (1 year, CLOSED orders): {len(filled_orders)} orders\n"
604
+ except Exception as e:
605
+ debug_info += f"Method 2 failed: {str(e)}\n"
606
+
607
+ # Approach 3: No date filter, just get recent orders
608
+ try:
609
+ recent_request = GetOrdersRequest(limit=100)
610
+ recent_orders = trading_client.get_orders(recent_request)
611
+ debug_info += f"Method 3 (recent 100, no date filter): {len(recent_orders)} orders\n"
612
+ except Exception as e:
613
+ debug_info += f"Method 3 failed: {str(e)}\n"
614
+
615
+ debug_info += "\n"
616
+
617
+ # Show any orders we found
618
+ all_orders = orders if orders else (filled_orders if 'filled_orders' in locals() else (recent_orders if 'recent_orders' in locals() else []))
619
+
620
+ if all_orders:
621
+ debug_info += f"Sample orders (showing first 10):\n"
622
+ for i, order in enumerate(all_orders[:10]):
623
+ debug_info += f"{i+1}. Symbol: {order.symbol}, Side: {order.side}, "
624
+ debug_info += f"Qty: {order.filled_qty}, Price: {order.filled_avg_price}, "
625
+ debug_info += f"Status: {order.status}, Time: {order.filled_at}, "
626
+ debug_info += f"Created: {order.created_at}\n"
627
+ else:
628
+ debug_info += "❌ NO ORDERS FOUND WITH ANY METHOD!\n"
629
+ debug_info += "\nLet's check account details:\n"
630
+
631
+ # Check account info
632
+ try:
633
+ account = trading_client.get_account()
634
+ debug_info += f"Account ID: {account.account_number}\n"
635
+ debug_info += f"Account Status: {account.status}\n"
636
+ debug_info += f"Trading Blocked: {account.trading_blocked}\n"
637
+ debug_info += f"Pattern Day Trader: {account.pattern_day_trader}\n"
638
+ debug_info += f"Cash: ${float(account.cash):,.2f}\n"
639
+ debug_info += f"Portfolio Value: ${float(account.portfolio_value):,.2f}\n"
640
+
641
+ # Check if this is paper trading
642
+ debug_info += f"\nAPI Keys being used:\n"
643
+ debug_info += f"API Key: {API_KEY[:8]}...{API_KEY[-4:]}\n"
644
+ if "PK" in API_KEY:
645
+ debug_info += "🟒 This appears to be PAPER TRADING (PK prefix)\n"
646
+ elif "AK" in API_KEY:
647
+ debug_info += "πŸ”΄ This appears to be LIVE TRADING (AK prefix)\n"
648
+ else:
649
+ debug_info += "❓ Unknown API key type\n"
650
+
651
+ except Exception as e:
652
+ debug_info += f"❌ Error getting account info: {str(e)}\n"
653
+
654
+ debug_info += "\nPossible issues:\n"
655
+ debug_info += "- No actual trading activity on this account\n"
656
+ debug_info += "- Using paper trading account (no real orders)\n"
657
+ debug_info += "- Orders are older than 1 year\n"
658
+ debug_info += "- API key permissions issue\n"
659
+ debug_info += "- Different Alpaca account than expected\n"
660
+
661
+ return debug_info
662
+ except Exception as e:
663
+ return f"ERROR getting order history: {str(e)}"
664
+
665
+ def debug_current_positions():
666
+ """Debug function to show current positions"""
667
+ try:
668
+ positions = get_current_positions()
669
+ debug_info = f"=== CURRENT POSITIONS DEBUG ===\n"
670
+ debug_info += f"Total positions: {len(positions)}\n\n"
671
+
672
+ for pos in positions:
673
+ debug_info += f"Symbol: {pos['symbol']}, Qty: {pos['qty']}, "
674
+ debug_info += f"Market Value: ${pos['market_value']:.2f}, "
675
+ debug_info += f"P&L: ${pos['unrealized_pl']:.2f}\n"
676
+
677
+ return debug_info
678
+ except Exception as e:
679
+ return f"ERROR getting positions: {str(e)}"
680
+
681
+ def debug_ipo_data():
682
+ """Debug function to show IPO data from VM"""
683
+ try:
684
+ ipos = fetch_from_vm('ipos?limit=20', [])
685
+ debug_info = f"=== IPO DATA DEBUG ===\n"
686
+ debug_info += f"Total IPOs: {len(ipos)}\n\n"
687
+
688
+ invested_count = 0
689
+ for ipo in ipos:
690
+ status = ipo.get('investment_status', 'UNKNOWN')
691
+ if status == 'INVESTED':
692
+ invested_count += 1
693
+ debug_info += f"INVESTED: {ipo.get('symbol')} - Price: ${ipo.get('trading_price')}\n"
694
+
695
+ debug_info += f"\nTotal INVESTED IPOs: {invested_count}\n"
696
+ return debug_info
697
+ except Exception as e:
698
+ return f"ERROR getting IPO data: {str(e)}"
699
+
700
+ def debug_account_info():
701
+ """Debug function to show account info"""
702
+ try:
703
+ account = get_account_info()
704
+ debug_info = f"=== ACCOUNT INFO DEBUG ===\n"
705
+ for key, value in account.items():
706
+ debug_info += f"{key}: {value}\n"
707
+ return debug_info
708
+ except Exception as e:
709
+ return f"ERROR getting account info: {str(e)}"
710
+
711
+ def clear_terminal():
712
+ """Clear terminal output"""
713
+ return "πŸ–₯️ VM Terminal Ready\n$ "
714
+
715
+ def run_quick_command(cmd):
716
+ """Helper for quick command buttons"""
717
+ def execute(current_output):
718
+ return run_vm_command(cmd, current_output)
719
+ return execute
720
+
721
  # Custom CSS for gorgeous design
722
  custom_css = """
723
  .gradio-container {
 
781
  .status-eligible { color: #f5a623 !important; font-weight: 600 !important; }
782
  .status-wrong { color: #8b949e !important; }
783
  .status-unknown { color: #ff0080 !important; }
784
+
785
+ .profit-positive { color: #00d647 !important; font-weight: 600 !important; }
786
+ .profit-negative { color: #ff0080 !important; font-weight: 600 !important; }
787
+ .profit-neutral { color: #8b949e !important; }
788
+
789
+ .terminal-container {
790
+ background: #000000 !important;
791
+ border: 1px solid #333 !important;
792
+ border-radius: 8px !important;
793
+ padding: 0 !important;
794
+ margin: 1rem 0 !important;
795
+ height: 500px !important;
796
+ overflow-y: auto !important;
797
+ display: flex !important;
798
+ flex-direction: column !important;
799
+ /* Hide scrollbars but keep functionality */
800
+ scrollbar-width: none !important; /* Firefox */
801
+ -ms-overflow-style: none !important; /* IE/Edge */
802
+ }
803
+
804
+ .terminal-container::-webkit-scrollbar {
805
+ display: none !important; /* Chrome/Safari/Webkit */
806
+ }
807
+
808
+ .terminal-display {
809
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace !important;
810
+ background: #000000 !important;
811
+ color: #ffffff !important;
812
+ padding: 1rem !important;
813
+ font-size: 14px !important;
814
+ line-height: 1.4 !important;
815
+ white-space: pre-wrap !important;
816
+ word-wrap: break-word !important;
817
+ margin: 0 !important;
818
+ flex-grow: 1 !important;
819
+ overflow-anchor: none !important;
820
+ /* Always stick to bottom */
821
+ display: flex !important;
822
+ flex-direction: column !important;
823
+ justify-content: flex-end !important;
824
+ }
825
+
826
+ .terminal-display::-webkit-scrollbar {
827
+ display: none !important; /* Chrome/Safari/Webkit */
828
+ }
829
+
830
+ .terminal-input input {
831
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace !important;
832
+ background: #1a1a1a !important;
833
+ color: #ffffff !important;
834
+ border: 1px solid #333 !important;
835
+ border-radius: 4px !important;
836
+ font-size: 14px !important;
837
+ }
838
+
839
+ .terminal-input input:focus {
840
+ border-color: #00ff00 !important;
841
+ box-shadow: 0 0 5px rgba(0, 255, 0, 0.3) !important;
842
+ }
843
+
844
+ /* Force Gradio HTML to stick to bottom */
845
+ .gr-html {
846
+ height: 500px !important;
847
+ overflow-y: auto !important;
848
+ scrollbar-width: none !important;
849
+ -ms-overflow-style: none !important;
850
+ display: flex !important;
851
+ flex-direction: column !important;
852
+ }
853
+
854
+ .gr-html::-webkit-scrollbar {
855
+ display: none !important;
856
+ }
857
+
858
+ /* Force content to bottom with CSS anchor */
859
+ .gr-html > div {
860
+ display: flex !important;
861
+ flex-direction: column !important;
862
+ justify-content: flex-end !important;
863
+ min-height: 100% !important;
864
+ }
865
  """
866
 
867
  def create_dashboard():
 
929
  positions_table = gr.Dataframe(label="Current Holdings", elem_classes=["gr-dataframe"])
930
  refresh_positions_btn = gr.Button("πŸ”„ Refresh Positions", variant="primary", size="lg")
931
 
932
+ # Investment Performance Tab
933
+ with gr.Tab("πŸ’° Investment Performance"):
934
+ gr.Markdown("## 🎯 IPO Investment Performance")
935
+ gr.Markdown("### Track profit/loss on your IPO investments with real-time market data")
936
+
937
+ investment_performance_table = gr.Dataframe(
938
+ label="IPO Investment P&L Analysis",
939
+ elem_classes=["gr-dataframe"]
940
+ )
941
+ refresh_investment_btn = gr.Button("πŸ”„ Refresh Investment Performance", variant="primary", size="lg")
942
+
943
+ gr.Markdown("### πŸ”§ Debug API Calls")
944
+ debug_output = gr.Textbox(
945
+ label="Debug Output",
946
+ lines=10,
947
+ interactive=False
948
+ )
949
+
950
+ with gr.Row():
951
+ debug_orders_btn = gr.Button("πŸ” Debug Order History", variant="secondary")
952
+ debug_positions_btn = gr.Button("πŸ“Š Debug Current Positions", variant="secondary")
953
+ debug_ipos_btn = gr.Button("🎯 Debug IPO Data", variant="secondary")
954
+ debug_account_btn = gr.Button("πŸ’Ό Debug Account Info", variant="secondary")
955
+
956
+ # VM Terminal Tab
957
+ with gr.Tab("πŸ’» VM Terminal"):
958
+ gr.Markdown("## πŸ–₯️ Remote VM Terminal")
959
+ gr.Markdown("### Execute commands directly on your trading VM")
960
+
961
+ # Hidden state for command history
962
+ command_history = gr.State("")
963
+
964
+ with gr.Row():
965
+ with gr.Column(scale=4):
966
+ command_input = gr.Textbox(
967
+ label="Command (Press Enter to run)",
968
+ placeholder="Enter command to run on VM...",
969
+ interactive=True,
970
+ elem_classes=["terminal-input"]
971
+ )
972
+ with gr.Column(scale=1):
973
+ run_command_btn = gr.Button("▢️ Run", variant="primary", size="lg")
974
+ clear_terminal_btn = gr.Button("πŸ—‘οΈ Clear", variant="secondary", size="lg")
975
+
976
+ terminal_output = gr.HTML(
977
+ label="Terminal Output",
978
+ value='<div class="terminal-display" id="terminal-content">πŸ–₯️ VM Terminal Ready<br>$ </div>',
979
+ elem_classes=["terminal-container"]
980
+ )
981
+
982
+ gr.Markdown("**πŸ“ File & System Commands:**")
983
+ with gr.Row():
984
+ quick_ls = gr.Button("πŸ“ ls -la", size="sm")
985
+ quick_pwd = gr.Button("πŸ“ pwd", size="sm")
986
+ quick_ps = gr.Button("πŸ”„ ps aux | grep python", size="sm")
987
+ quick_vm_status = gr.Button("πŸ–₯️ uptime && df -h", size="sm")
988
+ quick_who = gr.Button("πŸ‘€ whoami", size="sm")
989
+
990
+ gr.Markdown("**πŸ“‹ Log Files:**")
991
+ with gr.Row():
992
+ quick_script_log = gr.Button("πŸ“œ tail -50 script.log", size="sm")
993
+ quick_server_log = gr.Button("πŸ–₯️ tail -50 server.log", size="sm")
994
+ quick_cron_log = gr.Button("⏰ tail -50 /var/log/cron", size="sm")
995
+ quick_portfolio = gr.Button("πŸ’Ό cat portfolio.txt", size="sm")
996
+ quick_tickers = gr.Button("🎯 head -20 new_tickers_log.csv", size="sm")
997
+
998
+ gr.Markdown("**πŸ” Search & Analysis:**")
999
+ with gr.Row():
1000
+ quick_errors = gr.Button("🚨 grep -i error script.log | tail -10", size="sm")
1001
+ quick_trades = gr.Button("πŸ’° grep -i 'buy\\|sell' script.log | tail -10", size="sm")
1002
+ quick_ipos = gr.Button("πŸ†• grep -i 'new ticker' script.log | tail -10", size="sm")
1003
+
1004
  # System Logs Tab
1005
  with gr.Tab("πŸ“‹ System Logs"):
1006
  gr.Markdown("## πŸ–₯️ Trading Bot Activity")
 
1068
  outputs=[positions_table]
1069
  )
1070
 
1071
+ # Investment Performance tab
1072
+ refresh_investment_btn.click(
1073
+ fn=refresh_investment_performance_table,
1074
+ outputs=[investment_performance_table]
1075
+ )
1076
+
1077
+ # Debug buttons
1078
+ debug_orders_btn.click(
1079
+ fn=debug_order_history,
1080
+ outputs=[debug_output]
1081
+ )
1082
+ debug_positions_btn.click(
1083
+ fn=debug_current_positions,
1084
+ outputs=[debug_output]
1085
+ )
1086
+ debug_ipos_btn.click(
1087
+ fn=debug_ipo_data,
1088
+ outputs=[debug_output]
1089
+ )
1090
+ debug_account_btn.click(
1091
+ fn=debug_account_info,
1092
+ outputs=[debug_output]
1093
+ )
1094
+
1095
+ # VM Terminal tab - RADICAL FIX: Use <pre> instead of <br> and force scroll with element replacement
1096
+ def run_and_clear(cmd, output, history):
1097
+ new_output, _, new_history = run_vm_command(cmd, output, history)
1098
+
1099
+ # RADICAL CHANGE: Use <pre> tag to preserve exact formatting
1100
+ import html
1101
+ escaped_output = html.escape(new_output)
1102
+
1103
+ # Create a unique ID for this update to force refresh
1104
+ unique_id = f"terminal-{hash(new_output) % 100000}"
1105
+
1106
+ html_output = f'''<div class="terminal-display">
1107
+ <pre id="{unique_id}" style="margin: 0; font-family: inherit; color: inherit; background: inherit; white-space: pre-wrap; word-wrap: break-word;">{escaped_output}</pre>
1108
+ </div>
1109
+ <script>
1110
+ // Force immediate scroll to top by targeting multiple elements
1111
+ setTimeout(() => {{
1112
+ // Method 1: All possible scroll containers
1113
+ document.querySelectorAll('.gr-html, .terminal-container, .terminal-display').forEach(el => {{
1114
+ if (el && el.scrollTop !== undefined) el.scrollTop = 0;
1115
+ }});
1116
+
1117
+ // Method 2: Force scroll on the new element's containers
1118
+ const newEl = document.getElementById('{unique_id}');
1119
+ if (newEl) {{
1120
+ let parent = newEl.parentElement;
1121
+ while (parent) {{
1122
+ if (parent.scrollTop !== undefined) parent.scrollTop = 0;
1123
+ parent = parent.parentElement;
1124
+ }}
1125
+ }}
1126
+
1127
+ // Method 3: Nuclear option - scroll everything
1128
+ window.scrollTo(0, 0);
1129
+ }}, 5);
1130
+ </script>'''
1131
+ return html_output, "", new_history
1132
+
1133
+ def clear_and_reset():
1134
+ return '<div class="terminal-display" id="terminal-content">πŸ–₯️ VM Terminal Ready<br>$ </div>', ""
1135
+
1136
+ run_command_btn.click(
1137
+ fn=run_and_clear,
1138
+ inputs=[command_input, terminal_output, command_history],
1139
+ outputs=[terminal_output, command_input, command_history]
1140
+ )
1141
+ command_input.submit(
1142
+ fn=run_and_clear,
1143
+ inputs=[command_input, terminal_output, command_history],
1144
+ outputs=[terminal_output, command_input, command_history]
1145
+ )
1146
+ clear_terminal_btn.click(
1147
+ fn=clear_and_reset,
1148
+ outputs=[terminal_output, command_input]
1149
+ )
1150
+
1151
+ # File & System Commands
1152
+ quick_ls.click(
1153
+ fn=lambda output, hist: run_and_clear("ls -la", output, hist),
1154
+ inputs=[terminal_output, command_history],
1155
+ outputs=[terminal_output, command_input, command_history]
1156
+ )
1157
+ quick_pwd.click(
1158
+ fn=lambda output, hist: run_and_clear("pwd", output, hist),
1159
+ inputs=[terminal_output, command_history],
1160
+ outputs=[terminal_output, command_input, command_history]
1161
+ )
1162
+ quick_ps.click(
1163
+ fn=lambda output, hist: run_and_clear("ps aux | grep python", output, hist),
1164
+ inputs=[terminal_output, command_history],
1165
+ outputs=[terminal_output, command_input, command_history]
1166
+ )
1167
+ quick_vm_status.click(
1168
+ fn=lambda output, hist: run_and_clear("uptime && df -h", output, hist),
1169
+ inputs=[terminal_output, command_history],
1170
+ outputs=[terminal_output, command_input, command_history]
1171
+ )
1172
+ quick_who.click(
1173
+ fn=lambda output, hist: run_and_clear("whoami", output, hist),
1174
+ inputs=[terminal_output, command_history],
1175
+ outputs=[terminal_output, command_input, command_history]
1176
+ )
1177
+
1178
+ # Log Files
1179
+ quick_script_log.click(
1180
+ fn=lambda output, hist: run_and_clear("tail -50 script.log", output, hist),
1181
+ inputs=[terminal_output, command_history],
1182
+ outputs=[terminal_output, command_input, command_history]
1183
+ )
1184
+ quick_server_log.click(
1185
+ fn=lambda output, hist: run_and_clear("tail -50 server.log", output, hist),
1186
+ inputs=[terminal_output, command_history],
1187
+ outputs=[terminal_output, command_input, command_history]
1188
+ )
1189
+ quick_cron_log.click(
1190
+ fn=lambda output, hist: run_and_clear("tail -50 /var/log/cron", output, hist),
1191
+ inputs=[terminal_output, command_history],
1192
+ outputs=[terminal_output, command_input, command_history]
1193
+ )
1194
+ quick_portfolio.click(
1195
+ fn=lambda output, hist: run_and_clear("cat portfolio.txt", output, hist),
1196
+ inputs=[terminal_output, command_history],
1197
+ outputs=[terminal_output, command_input, command_history]
1198
+ )
1199
+ quick_tickers.click(
1200
+ fn=lambda output, hist: run_and_clear("head -20 new_tickers_log.csv", output, hist),
1201
+ inputs=[terminal_output, command_history],
1202
+ outputs=[terminal_output, command_input, command_history]
1203
+ )
1204
+
1205
+ # Search & Analysis
1206
+ quick_errors.click(
1207
+ fn=lambda output, hist: run_and_clear("grep -i error script.log | tail -10", output, hist),
1208
+ inputs=[terminal_output, command_history],
1209
+ outputs=[terminal_output, command_input, command_history]
1210
+ )
1211
+ quick_trades.click(
1212
+ fn=lambda output, hist: run_and_clear("grep -i 'buy\\|sell' script.log | tail -10", output, hist),
1213
+ inputs=[terminal_output, command_history],
1214
+ outputs=[terminal_output, command_input, command_history]
1215
+ )
1216
+ quick_ipos.click(
1217
+ fn=lambda output, hist: run_and_clear("grep -i 'new ticker' script.log | tail -10", output, hist),
1218
+ inputs=[terminal_output, command_history],
1219
+ outputs=[terminal_output, command_input, command_history]
1220
+ )
1221
+
1222
  # Logs tab
1223
  refresh_logs_btn.click(
1224
  fn=refresh_system_logs,
 
1242
  )
1243
  demo.load(fn=create_ipo_discovery_chart, outputs=[ipo_chart])
1244
  demo.load(fn=refresh_ipo_discoveries_table, outputs=[ipo_table])
1245
+ demo.load(fn=refresh_investment_performance_table, outputs=[investment_performance_table])
1246
 
1247
  return demo
1248