""" Analytics Tab - Portfolio Analytics Dashboard. Displays performance metrics, allocation, risk metrics, and top holdings. """ import logging from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QLabel, QTableWidget, QTableWidgetItem, QHeaderView, QGridLayout ) from PyQt6.QtCore import Qt from PyQt6.QtGui import QColor from src.utils.formatters import format_currency, format_percentage, color_for_pnl logger = logging.getLogger(__name__) class AnalyticsTab(QWidget): """Analytics dashboard tab.""" def __init__(self, parent): """ Initialize analytics tab. Args: parent: MainWindow instance """ super().__init__() self.parent = parent self.init_ui() self.load_data() def init_ui(self): """Initialize user interface.""" layout = QVBoxLayout(self) # Performance section perf_group = QGroupBox("📊 PERFORMANCE") perf_layout = QGridLayout(perf_group) self.lbl_invested = QLabel("€ 0.00") self.lbl_current = QLabel("€ 0.00") self.lbl_pnl = QLabel("+€ 0.00 (+0.0%)") self.lbl_dividends = QLabel("€ 0.00") perf_layout.addWidget(QLabel("Total Invested:"), 0, 0) perf_layout.addWidget(self.lbl_invested, 0, 1) perf_layout.addWidget(QLabel("Current Value:"), 1, 0) perf_layout.addWidget(self.lbl_current, 1, 1) perf_layout.addWidget(QLabel("Total P&L:"), 2, 0) perf_layout.addWidget(self.lbl_pnl, 2, 1) perf_layout.addWidget(QLabel("Dividends:"), 3, 0) perf_layout.addWidget(self.lbl_dividends, 3, 1) layout.addWidget(perf_group) # Allocation section alloc_group = QGroupBox("📈 ALLOCATION") alloc_layout = QHBoxLayout(alloc_group) # By Type type_layout = QVBoxLayout() type_layout.addWidget(QLabel("By Type:")) self.lbl_alloc_type = QLabel() type_layout.addWidget(self.lbl_alloc_type) type_layout.addStretch() alloc_layout.addLayout(type_layout) # By Geography geo_layout = QVBoxLayout() geo_layout.addWidget(QLabel("By Geography:")) self.lbl_alloc_geo = QLabel() geo_layout.addWidget(self.lbl_alloc_geo) geo_layout.addStretch() alloc_layout.addLayout(geo_layout) layout.addWidget(alloc_group) # Risk Metrics section risk_group = QGroupBox("⚠️ RISK METRICS") risk_layout = QVBoxLayout(risk_group) self.lbl_max_holding = QLabel() self.lbl_stock_conc = QLabel() self.lbl_etf_conc = QLabel() self.lbl_diversification = QLabel() risk_layout.addWidget(self.lbl_max_holding) risk_layout.addWidget(self.lbl_stock_conc) risk_layout.addWidget(self.lbl_etf_conc) risk_layout.addWidget(self.lbl_diversification) layout.addWidget(risk_group) # Top 5 Holdings section top_group = QGroupBox("📌 TOP 5 HOLDINGS") top_layout = QVBoxLayout(top_group) self.table_top = QTableWidget() self.table_top.setColumnCount(4) self.table_top.setHorizontalHeaderLabels([ "Ticker", "Value €", "Weight %", "P&L %" ]) self.table_top.setMaximumHeight(200) # Configure table self.table_top.setAlternatingRowColors(True) header = self.table_top.horizontalHeader() header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) header.setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents) header.setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents) header.setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents) top_layout.addWidget(self.table_top) layout.addWidget(top_group) layout.addStretch() def load_data(self): """Load and display analytics data.""" logger.debug("Loading analytics data...") try: # Reload portfolio self.parent.portfolio.load_holdings() # Update all sections self.update_performance() self.update_allocation() self.update_risk_metrics() self.update_top_holdings() logger.info("Analytics data loaded successfully") except Exception as e: logger.error(f"Error loading analytics: {e}", exc_info=True) def update_performance(self): """Update performance section.""" perf = self.parent.analytics.get_performance_summary() # Total Invested self.lbl_invested.setText(format_currency(perf['total_invested'])) # Current Value self.lbl_current.setText(format_currency(perf['current_value'])) # Total P&L pnl_amount = perf['total_pnl_amount'] pnl_percent = perf['total_pnl_percent'] pnl_text = f"{format_currency(pnl_amount)} ({format_percentage(pnl_percent)})" pnl_color = color_for_pnl(pnl_amount) self.lbl_pnl.setText(pnl_text) self.lbl_pnl.setStyleSheet(f"color: {pnl_color}; font-weight: bold;") # Dividends self.lbl_dividends.setText(format_currency(perf['total_dividends'])) def update_allocation(self): """Update allocation section.""" # By Type alloc_type = self.parent.analytics.get_allocation_by_type() type_text = "" for asset_type, weight in sorted(alloc_type.items(), key=lambda x: x[1], reverse=True): type_text += f"• {asset_type}: {weight:.1f}%\n" self.lbl_alloc_type.setText(type_text) # By Geography alloc_geo = self.parent.analytics.get_allocation_by_geography() geo_text = "" for geography, weight in sorted(alloc_geo.items(), key=lambda x: x[1], reverse=True): geo_text += f"• {geography}: {weight:.1f}%\n" self.lbl_alloc_geo.setText(geo_text) def update_risk_metrics(self): """Update risk metrics section.""" risk = self.parent.analytics.get_risk_metrics() diversification = self.parent.analytics.get_diversification_score() # Max Single Holding max_ticker, max_weight = risk['max_single_holding'] warning_level = risk['concentration_warning'] max_text = f"Max Single Holding: {max_weight:.1f}% ({max_ticker})" if warning_level == 'danger': max_text += " ⚠️ >60%" max_color = "#dc3545" # Red elif warning_level == 'warning': max_text += " ⚠️ >40%" max_color = "#ffc107" # Yellow else: max_color = "#28a745" # Green self.lbl_max_holding.setText(max_text) self.lbl_max_holding.setStyleSheet(f"color: {max_color}; font-weight: bold;") # Stock Concentration self.lbl_stock_conc.setText(f"Stock Concentration: {risk['stock_concentration']:.1f}%") # ETF Concentration self.lbl_etf_conc.setText(f"ETF Concentration: {risk['etf_concentration']:.1f}%") # Diversification Score div_text = (f"Diversification: {diversification['diversification_score']:.0f}/100 " f"({diversification['assessment']}) - " f"{diversification['effective_holdings']:.1f} effective holdings") self.lbl_diversification.setText(div_text) def update_top_holdings(self): """Update top 5 holdings table.""" top_holdings = self.parent.analytics.get_top_holdings(n=5) self.table_top.setRowCount(len(top_holdings)) for row, holding in enumerate(top_holdings): # Ticker self.table_top.setItem(row, 0, QTableWidgetItem(holding['ticker'])) # Value value_item = QTableWidgetItem(format_currency(holding['current_value'])) value_item.setTextAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) self.table_top.setItem(row, 1, value_item) # Weight % weight_item = QTableWidgetItem(f"{holding['weight_pct']:.1f}%") weight_item.setTextAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) self.table_top.setItem(row, 2, weight_item) # P&L % pnl_pct = holding['pnl_percent'] pnl_item = QTableWidgetItem(format_percentage(pnl_pct)) pnl_item.setTextAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) pnl_item.setForeground(QColor(color_for_pnl(pnl_pct))) self.table_top.setItem(row, 3, pnl_item)