"""
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)