Files
QuantCore-FX/ui/dashboard_tab.py
T
2026-06-16 12:27:37 +01:00

308 lines
11 KiB
Python

"""
APEX Layer 1 — Tab 1: Dashboard
This is the main screen the user sees every day.
Features:
- Signal card at top (shows PRIMARY SIGNAL, gap, status, updated date)
- Ranked score table below with all 8 currencies
- Strongest row highlighted GREEN (BUY)
- Weakest row highlighted RED (SELL)
- Score bar charts per row (visual progress)
- Auto-refresh when data updated from Entry tab or FRED API
Display:
- Rank, Currency, Rate, CPI, PMI, Score columns
- Color-coded rows, "BUY" and "SELL" tags
- Last updated timestamp
"""
from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QTableWidget, QTableWidgetItem,
QFrame, QPushButton, QSpinBox
)
from PyQt5.QtCore import Qt, pyqtSignal, QSize
from PyQt5.QtGui import QColor, QFont, QBrush, QPixmap
from typing import Dict, Optional
from datetime import datetime
import config
from database import Database
import scorer
class DashboardTab(QWidget):
"""Main dashboard showing current signal and currency rankings."""
# Signal to request FRED fetch
fetch_rates_requested = pyqtSignal()
def __init__(self, db: Database):
"""
Initialize Dashboard tab.
Args:
db: Database instance
"""
super().__init__()
self.db = db
self.current_month = datetime.now().strftime("%Y-%m")
self._init_ui()
self._refresh_display()
def _init_ui(self):
"""Build the UI layout."""
layout = QVBoxLayout()
# ====== Signal Card ======
signal_card = self._build_signal_card()
layout.addWidget(signal_card)
layout.addSpacing(15)
# ====== Ranked Score Table ======
layout.addWidget(QLabel("Currency Rankings"))
self.score_table = QTableWidget()
self.score_table.setColumnCount(8)
self.score_table.setHorizontalHeaderLabels([
"Rank", "Currency", "Rate (%)", "CPI (%)", "PMI", "Score", "Signal", "Strength"
])
self.score_table.setRowCount(len(config.CURRENCIES))
self.score_table.setAlternatingRowColors(True)
self.score_table.setSelectionBehavior(QTableWidget.SelectRows)
self.score_table.setSelectionMode(QTableWidget.SingleSelection)
# Pre-fill with placeholder rows
for row in range(len(config.CURRENCIES)):
for col in range(8):
item = QTableWidgetItem("—")
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
self.score_table.setItem(row, col, item)
self.score_table.resizeColumnsToContents()
layout.addWidget(self.score_table)
layout.addSpacing(15)
# ====== Refresh Button ======
button_layout = QHBoxLayout()
button_layout.addStretch()
self.refresh_btn = QPushButton("Refresh")
self.refresh_btn.clicked.connect(self._refresh_display)
button_layout.addWidget(self.refresh_btn)
fetch_btn = QPushButton("Fetch Rates (FRED)")
fetch_btn.clicked.connect(self._on_fetch_rates)
button_layout.addWidget(fetch_btn)
layout.addLayout(button_layout)
layout.addStretch()
self.setLayout(layout)
def _build_signal_card(self) -> QFrame:
"""Build the signal card frame."""
card = QFrame()
card.setStyleSheet("""
QFrame {
background-color: #f8f9fa;
border: 2px solid #dee2e6;
border-radius: 8px;
padding: 15px;
}
""")
layout = QVBoxLayout()
# Title
title = QLabel("PRIMARY SIGNAL")
title.setFont(QFont("Arial", 10, QFont.Bold))
title.setStyleSheet("color: #495057;")
layout.addWidget(title)
layout.addSpacing(5)
# Signal text (large, bold)
self.signal_label = QLabel("NO TRADE — Initializing...")
self.signal_label.setFont(QFont("Arial", 24, QFont.Bold))
self.signal_label.setStyleSheet("color: #2c3e50;")
layout.addWidget(self.signal_label)
layout.addSpacing(10)
# Gap and status
self.gap_label = QLabel("Gap: — points")
self.gap_label.setFont(QFont("Arial", 12))
layout.addWidget(self.gap_label)
# Updated timestamp
self.updated_label = QLabel("Updated: —")
self.updated_label.setFont(QFont("Arial", 10))
self.updated_label.setStyleSheet("color: #7f8c8d;")
layout.addWidget(self.updated_label)
card.setLayout(layout)
return card
def _refresh_display(self):
"""Refresh dashboard with latest data."""
try:
# Get signal for current month
signal_data = self.db.get_signal(self.current_month)
if signal_data:
signal_text = signal_data["signal"]
gap = signal_data["gap"]
status = signal_data["status"]
# Update signal label
self.signal_label.setText(signal_text)
# Color code based on status
if status == "ACTIVE":
self.signal_label.setStyleSheet("color: #27ae60;") # Green
else:
self.signal_label.setStyleSheet("color: #e74c3c;") # Red
# Update gap label
gap_tier = scorer.get_gap_tier(gap)
tier_name = {
"no_trade": "Too narrow",
"weak": "Weak signal",
"standard": "Standard signal",
"strong": "Strong signal"
}.get(gap_tier, "Unknown")
self.gap_label.setText(f"Gap: {gap:.1f} points · {tier_name}")
else:
self.signal_label.setText("NO TRADE — No data yet")
self.signal_label.setStyleSheet("color: #e74c3c;")
self.gap_label.setText("Gap: — points")
# Update timestamp
self.updated_label.setText(f"Updated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
# Refresh score table
self._refresh_score_table()
except Exception as e:
print(f"[ERROR] Failed to refresh dashboard: {e}")
self.signal_label.setText("ERROR")
self.signal_label.setStyleSheet("color: #e74c3c;")
def _refresh_score_table(self):
"""Refresh the ranked currency table."""
try:
scores = self.db.get_month_scores(self.current_month)
if not scores:
# No scores yet
for row in range(len(config.CURRENCIES)):
for col in range(8):
self.score_table.item(row, col).setText("—")
return
# Get sorted list
ranked = [(c, s) for c, s in sorted(
scores.items(),
key=lambda x: x[1]['rank']
)]
# Get rates for display
rates = self.db.get_all_rates()
# Get monthly data for CPI display
monthly_data = self.db.get_monthly_data(self.current_month)
for row, (currency, score_data) in enumerate(ranked):
rank = score_data['rank']
total_score = score_data['total_score']
rate = rates.get(currency)
cpi_data = monthly_data.get(currency, {})
cpi = cpi_data.get('cpi_actual')
pmi = cpi_data.get('pmi_actual')
# Rank
self.score_table.item(row, 0).setText(str(rank))
# Currency
currency_text = f"{config.CURRENCY_EMOJIS.get(currency, '')} {currency}"
self.score_table.item(row, 1).setText(currency_text)
# Rate
rate_text = f"{rate:.2f}" if rate is not None else "—"
self.score_table.item(row, 2).setText(rate_text)
# CPI
cpi_text = f"{cpi:.2f}" if cpi is not None else "—"
self.score_table.item(row, 3).setText(cpi_text)
# PMI
pmi_text = f"{pmi:.1f}" if pmi is not None else "—"
self.score_table.item(row, 4).setText(pmi_text)
# Score (two decimals)
self.score_table.item(row, 5).setText(f"{total_score:.1f}")
# Signal tag (BUY for strongest, SELL for weakest)
if rank == 1:
self.score_table.item(row, 6).setText("BUY")
elif rank == len(config.CURRENCIES):
self.score_table.item(row, 6).setText("SELL")
else:
self.score_table.item(row, 6).setText("")
# Strength bar (visual progress 0-100)
strength_item = self.score_table.item(row, 7)
strength_item.setText(f"{int(total_score)}%")
# Color code rows
if rank == 1:
# Strongest = GREEN
for col in range(8):
self.score_table.item(row, col).setBackground(QColor("#d5f4e6"))
self.score_table.item(row, col).setForeground(QColor("#27ae60"))
self.score_table.item(row, col).setFont(QFont("Arial", 10, QFont.Bold))
elif rank == len(config.CURRENCIES):
# Weakest = RED
for col in range(8):
self.score_table.item(row, col).setBackground(QColor("#fadbd8"))
self.score_table.item(row, col).setForeground(QColor("#e74c3c"))
self.score_table.item(row, col).setFont(QFont("Arial", 10, QFont.Bold))
else:
# Middle = neutral
for col in range(8):
self.score_table.item(row, col).setBackground(QColor("#ffffff"))
self.score_table.item(row, col).setForeground(QColor("#2c3e50"))
self.score_table.item(row, col).setFont(QFont("Arial", 10))
# Auto-resize columns to content
self.score_table.resizeColumnsToContents()
except Exception as e:
print(f"[ERROR] Failed to refresh score table: {e}")
def _on_fetch_rates(self):
"""Handle fetch rates button click."""
self.fetch_rates_requested.emit()
def on_data_saved(self, month: str):
"""
Called when entry tab saves new data.
Args:
month: Month string (YYYY-MM)
"""
self.current_month = month
self._refresh_display()
def on_rates_updated(self, rates: Dict[str, float]):
"""
Called when FRED rates fetched successfully.
Args:
rates: Dict mapping currency to rate
"""
# Rates are saved to DB by the thread, just refresh display
self._refresh_display()