mirror of
https://github.com/Sabermrddz/QuantCore-FX.git
synced 2026-07-27 18:47:51 +00:00
308 lines
11 KiB
Python
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()
|