layer one v4

This commit is contained in:
saber
2026-06-18 09:35:03 +01:00
parent e28f2db97c
commit 5d4ffb1da5
29 changed files with 1056 additions and 2067 deletions
+106 -112
View File
@@ -13,7 +13,7 @@ from PyQt5.QtWidgets import (
QPushButton, QFrame, QMessageBox, QProgressBar
)
from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtGui import QColor, QFont, QBrush
from PyQt5.QtGui import QColor, QFont
from typing import Dict, Optional
from datetime import datetime
import config
@@ -31,7 +31,7 @@ class ConfluenceSignalsTab(QWidget):
self.db = db
self.tech_analyzer = tech_analyzer
self.confluence = ConfluenceFilter(tech_analyzer)
self.confluence = ConfluenceFilter(tech_analyzer, db=db)
self.risk_mgmt = RiskManagementSystem(account_balance=config.ACCOUNT_BALANCE)
self.signal_history = SignalHistory()
@@ -41,24 +41,25 @@ class ConfluenceSignalsTab(QWidget):
def _init_ui(self):
"""Build UI layout."""
layout = QVBoxLayout()
layout.setSpacing(12)
# ====== Confluence Status Card ======
card = self._build_status_card()
layout.addWidget(card)
layout.addSpacing(15)
# ====== Active Signals Table ======
layout.addWidget(QLabel("Confluence Signals (Layer 1 + Layer 2)"))
heading = QLabel("Confluence Signals (Layer 1 + Layer 2)")
heading.setProperty("heading", True)
layout.addWidget(heading)
self.signals_table = QTableWidget()
self.signals_table.setColumnCount(8)
self.signals_table.setHorizontalHeaderLabels([
"Pair", "L1 Gap", "L2 Z-Score", "Status", "Confidence", "Entry Price", "Position Size", "Action"
"Pair", "Signal", "Entry", "SL", "TP", "L1 Gap", "Z-Score", "Confidence"
])
self.signals_table.setRowCount(10)
layout.addWidget(self.signals_table)
layout.addSpacing(15)
# ====== Risk Management Panel ======
risk_layout = QHBoxLayout()
@@ -66,22 +67,26 @@ class ConfluenceSignalsTab(QWidget):
self.exposure_bar = QProgressBar()
self.exposure_bar.setMaximum(100)
self.exposure_bar.setFormat("%v% exposed")
risk_layout.addWidget(self.exposure_bar)
self.leverage_label = QLabel("Leverage: —")
self.leverage_label.setStyleSheet("font-weight: 600; color: #5d6d7e;")
risk_layout.addWidget(self.leverage_label)
risk_layout.addStretch()
layout.addLayout(risk_layout)
layout.addSpacing(10)
# ====== Control Buttons ======
button_layout = QHBoxLayout()
refresh_btn = QPushButton("Refresh Signals")
refresh_btn.setObjectName("secondary")
refresh_btn.clicked.connect(self._refresh_signals)
button_layout.addWidget(refresh_btn)
execute_btn = QPushButton("Execute Top Signal")
execute_btn.setObjectName("success")
execute_btn.clicked.connect(self._execute_signal)
button_layout.addWidget(execute_btn)
@@ -94,70 +99,62 @@ class ConfluenceSignalsTab(QWidget):
def _build_status_card(self) -> QFrame:
"""Build confluence status card."""
card = QFrame()
card.setStyleSheet("""
QFrame {
background-color: #f8f9fa;
border: 2px solid #dee2e6;
border-radius: 8px;
padding: 15px;
}
""")
card.setObjectName("statusCard")
layout = QVBoxLayout()
layout.setSpacing(8)
title = QLabel("CONFLUENCE STATUS")
title.setFont(QFont("Arial", 10, QFont.Bold))
title.setProperty("subheading", True)
layout.addWidget(title)
layout.addSpacing(5)
# Layer 1 status
layer1_layout = QHBoxLayout()
layer1_layout.addWidget(QLabel("Layer 1 (Fundamental):"))
l1 = QHBoxLayout()
l1.addWidget(QLabel("Layer 1 (Fundamental):"))
self.layer1_status_label = QLabel("No bias")
self.layer1_status_label.setFont(QFont("Arial", 11, QFont.Bold))
layer1_layout.addWidget(self.layer1_status_label)
layer1_layout.addStretch()
layout.addLayout(layer1_layout)
self.layer1_status_label.setStyleSheet("font-weight: 700;")
l1.addWidget(self.layer1_status_label)
l1.addStretch()
layout.addLayout(l1)
# Directional Bias Matrix display
bias_layout = QHBoxLayout()
bias_layout.addWidget(QLabel("Macro Boundaries:"))
bl = QHBoxLayout()
bl.addWidget(QLabel("Macro Boundaries:"))
self.bias_matrix_label = QLabel("No bias matrix")
self.bias_matrix_label.setStyleSheet("color: #8e44ad; font-size: 10px;")
bias_layout.addWidget(self.bias_matrix_label)
bias_layout.addStretch()
layout.addLayout(bias_layout)
self.bias_matrix_label.setStyleSheet("color: #8e44ad; font-size: 12px;")
bl.addWidget(self.bias_matrix_label)
bl.addStretch()
layout.addLayout(bl)
# Layer 2 status
layer2_layout = QHBoxLayout()
layer2_layout.addWidget(QLabel("Layer 2 (Technical):"))
l2 = QHBoxLayout()
l2.addWidget(QLabel("Layer 2 (Technical):"))
self.layer2_status_label = QLabel("No extreme")
self.layer2_status_label.setStyleSheet("color: #95a5a6;")
layer2_layout.addWidget(self.layer2_status_label)
layer2_layout.addStretch()
layout.addLayout(layer2_layout)
l2.addWidget(self.layer2_status_label)
l2.addStretch()
layout.addLayout(l2)
# Confluence result
conf_layout = QHBoxLayout()
conf_layout.addWidget(QLabel("Confluence Result:"))
self.confluence_status_label = QLabel(" NO CONFLUENCE")
self.confluence_status_label.setStyleSheet("color: #e74c3c; font-weight: bold;")
self.confluence_status_label.setFont(QFont("Arial", 12, QFont.Bold))
conf_layout.addWidget(self.confluence_status_label)
conf_layout.addStretch()
layout.addLayout(conf_layout)
cf = QHBoxLayout()
cf.addWidget(QLabel("Confluence Result:"))
self.confluence_status_label = QLabel(" NO CONFLUENCE")
self.confluence_status_label.setStyleSheet("color: #e74c3c; font-weight: 700; font-size: 14px;")
cf.addWidget(self.confluence_status_label)
cf.addStretch()
layout.addLayout(cf)
# Matrix cross (S.A.T.O.R.I.)
matrix_layout = QHBoxLayout()
matrix_layout.addWidget(QLabel("Matrix Cross:"))
mx = QHBoxLayout()
mx.addWidget(QLabel("Matrix Cross:"))
self.matrix_cross_label = QLabel("")
self.matrix_cross_label.setStyleSheet("font-weight: bold; color: #8e44ad;")
matrix_layout.addWidget(self.matrix_cross_label)
matrix_layout.addStretch()
layout.addLayout(matrix_layout)
self.matrix_cross_label.setStyleSheet("font-weight: 700; color: #8e44ad;")
mx.addWidget(self.matrix_cross_label)
mx.addStretch()
layout.addLayout(mx)
self.matrix_detail_label = QLabel("")
self.matrix_detail_label.setStyleSheet("color: #7f8c8d; font-size: 10px;")
self.matrix_detail_label.setStyleSheet("color: #7f8c8d; font-size: 11px;")
layout.addWidget(self.matrix_detail_label)
card.setLayout(layout)
@@ -170,16 +167,20 @@ class ConfluenceSignalsTab(QWidget):
self._refresh_signals()
def _refresh_signals(self):
"""Refresh confluence signal display."""
"""Refresh confluence signal display — single matrix build."""
try:
report = self.confluence.get_confluence_report()
cf = self.confluence
should_enter, reason, strength, sl_tp = cf.check_entry_confluence()
m = cf.matrix
mr = m.get_report() if m else {}
bias = getattr(cf, 'bias_matrix', {})
# Update bias matrix display
bias = report.get("bias_matrix", {})
if bias:
parts = []
for ccy in config.CURRENCIES:
d = bias.get(ccy, "")
d = bias.get(ccy, {}).get("direction", "")
if d == "STRONG":
parts.append(f"{ccy}")
elif d == "WEAK":
@@ -187,23 +188,18 @@ class ConfluenceSignalsTab(QWidget):
else:
parts.append(f"{ccy}")
self.bias_matrix_label.setText(" ".join(parts))
self.bias_matrix_label.setStyleSheet("color: #8e44ad; font-size: 10px;")
else:
self.bias_matrix_label.setText("No bias matrix")
self.bias_matrix_label.setStyleSheet("color: #95a5a6; font-size: 10px;")
# Update matrix cross display
mc = report.get("matrix_cross", "")
gap = report.get("divergence_gap", 0)
has_div = report.get("has_matrix_divergence", False)
ranked = report.get("matrix_ranked", [])
mc = mr.get("matrix_cross", "")
gap = mr.get("divergence_gap", 0)
has_div = mr.get("has_divergence", False)
ranked = mr.get("ranked", [])
if mc and mc != "N/A":
self.matrix_cross_label.setText(f"{mc} (spread: {gap:.2f}σ)")
if has_div:
self.matrix_cross_label.setStyleSheet("font-weight: bold; color: #e74c3c;")
else:
self.matrix_cross_label.setStyleSheet("font-weight: bold; color: #8e44ad;")
color = "#e74c3c" if has_div else "#8e44ad"
self.matrix_cross_label.setStyleSheet(f"font-weight: bold; color: {color};")
else:
self.matrix_cross_label.setText("")
self.matrix_cross_label.setStyleSheet("font-weight: bold; color: #95a5a6;")
@@ -217,36 +213,27 @@ class ConfluenceSignalsTab(QWidget):
else:
self.matrix_detail_label.setText("")
# Check for confluence
should_enter, reason, strength = self.confluence.check_entry_confluence()
# Update status
if should_enter:
l1s = self.confluence.layer1_strongest or ""
l1w = self.confluence.layer1_weakest or ""
self.layer1_status_label.setText(f"🟢 {l1s}/{l1w} (Gap: {self.confluence.layer1_gap:.1f})")
l1s = cf.layer1_strongest or ""
l1w = cf.layer1_weakest or ""
self.layer1_status_label.setText(f"PAR {l1s}/{l1w} (Gap: {cf.layer1_gap:.1f})")
pair = f"{self.confluence.layer1_strongest}_{self.confluence.layer1_weakest}" if self.confluence.layer1_strongest else ""
pair = f"{cf.layer1_strongest}_{cf.layer1_weakest}" if cf.layer1_strongest else ""
z_score = self.tech_analyzer.get_z_score(pair) if pair != "" else 0
self.layer2_status_label.setText(f"🔴 {pair} Z-score: {z_score:.2f}")
self.layer2_status_label.setText(f"TCH {pair} Z-score: {z_score:.2f}")
self.layer2_status_label.setStyleSheet("color: #27ae60;")
if has_div:
self.confluence_status_label.setText(f"✅ MATRIX DIVERGENCE: {strength:.0f}%")
else:
self.confluence_status_label.setText(f"✅ CONFLUENCE: {strength:.0f}% confidence")
label_text = f"✅ MATRIX DIVERGENCE: {strength:.0f}%" if has_div else f"✅ CONFLUENCE: {strength:.0f}%"
self.confluence_status_label.setText(label_text)
self.confluence_status_label.setStyleSheet("color: #27ae60; font-weight: bold;")
self._populate_signal_table(strength)
else:
self.layer1_status_label.setText("No signal")
self.layer2_status_label.setText("No extreme")
self.layer2_status_label.setStyleSheet("color: #95a5a6;")
self.confluence_status_label.setText("❌ NO CONFLUENCE")
self.confluence_status_label.setText("✕ NO CONFLUENCE")
self.confluence_status_label.setStyleSheet("color: #e74c3c; font-weight: bold;")
# Update risk metrics
portfolio = self.risk_mgmt.get_portfolio_summary()
exposure_pct = min((portfolio['total_exposure'] / config.ACCOUNT_BALANCE) * 100, 100)
self.exposure_bar.setValue(int(exposure_pct))
@@ -256,7 +243,6 @@ class ConfluenceSignalsTab(QWidget):
print(f"[Confluence] Error refreshing: {e}")
def _populate_signal_table(self, confluence_strength: float):
"""Populate the signals table with matrix and confluence data."""
report = self.confluence.get_confluence_report()
signals = self.confluence.get_all_signals()
@@ -267,42 +253,50 @@ class ConfluenceSignalsTab(QWidget):
if row >= self.signals_table.rowCount():
break
pair_item = QTableWidgetItem(pair_key)
pair_item.setFlags(pair_item.flags() & ~Qt.ItemIsEditable)
if signal.get('type') == 'MATRIX_DIVERGENCE':
pair_item.setForeground(QColor("#8e44ad"))
self.signals_table.setItem(row, 0, pair_item)
def _item(text, align=True):
item = QTableWidgetItem(str(text))
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
if align:
item.setTextAlignment(Qt.AlignCenter)
return item
# L1 Gap (from report)
gap_item = QTableWidgetItem(f"{report.get('layer1_gap', 0):.1f}")
gap_item.setFlags(gap_item.flags() & ~Qt.ItemIsEditable)
self.signals_table.setItem(row, 1, gap_item)
# 0: Pair
self.signals_table.setItem(row, 0, _item(pair_key, False))
# L2 Z-Score
# 1: Signal CALL/SHORT
direction = signal.get('direction', '')
signal_text = "CALL" if direction == 'LONG' else "SHORT"
sig_item = _item(signal_text)
sig_item.setForeground(QColor("#27ae60") if signal_text == "CALL" else QColor("#e74c3c"))
sig_item.setFont(QFont("Segoe UI", 11, QFont.Bold))
self.signals_table.setItem(row, 1, sig_item)
# 2: Entry
entry = signal.get('entry')
self.signals_table.setItem(row, 2, _item(f"{entry:.5f}" if entry else ""))
# 3: SL
sl = signal.get('sl')
self.signals_table.setItem(row, 3, _item(f"{sl:.5f}" if sl else ""))
# 4: TP
tp = signal.get('tp')
self.signals_table.setItem(row, 4, _item(f"{tp:.5f}" if tp else ""))
# 5: L1 Gap
self.signals_table.setItem(row, 5, _item(f"{report.get('layer1_gap', 0):.1f}"))
# 6: Z-Score
z = report.get('layer2_z_score', 0)
if signal.get('type') == 'MATRIX_DIVERGENCE':
z = report.get('matrix_cross_z', 0)
z_item = QTableWidgetItem(f"{z:.2f}")
z_item.setFlags(z_item.flags() & ~Qt.ItemIsEditable)
self.signals_table.setItem(row, 2, z_item)
self.signals_table.setItem(row, 6, _item(f"{z:.2f}"))
# Status
sig_type = signal.get('type', 'SIGNAL').replace('_', ' ')
status_item = QTableWidgetItem(sig_type)
if 'DIVERGENCE' in sig_type:
status_item.setBackground(QColor("#f3e5f5"))
status_item.setForeground(QColor("#6a1b9a"))
else:
status_item.setBackground(QColor("#e8f5e9"))
status_item.setFlags(status_item.flags() & ~Qt.ItemIsEditable)
self.signals_table.setItem(row, 3, status_item)
# Confidence
# 7: Confidence
strength = signal.get('strength', confluence_strength)
conf_item = QTableWidgetItem(f"{strength:.0f}%")
conf_item.setFont(QFont("Arial", 10, QFont.Bold))
conf_item.setFlags(conf_item.flags() & ~Qt.ItemIsEditable)
self.signals_table.setItem(row, 4, conf_item)
conf_item = _item(f"{strength:.0f}%")
conf_item.setFont(QFont("Segoe UI", 10, QFont.Bold))
self.signals_table.setItem(row, 7, conf_item)
row += 1
+99 -77
View File
@@ -19,10 +19,10 @@ Display:
from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QTableWidget, QTableWidgetItem,
QFrame, QPushButton, QSpinBox
QFrame, QPushButton, QProgressBar, QHeaderView
)
from PyQt5.QtCore import Qt, pyqtSignal, QSize
from PyQt5.QtGui import QColor, QFont, QBrush, QPixmap
from PyQt5.QtCore import Qt, pyqtSignal
from PyQt5.QtGui import QColor, QFont
from typing import Dict, Optional
from datetime import datetime
import config
@@ -57,14 +57,16 @@ class DashboardTab(QWidget):
def _init_ui(self):
"""Build the UI layout."""
layout = QVBoxLayout()
layout.setSpacing(12)
# ====== Signal Card ======
signal_card = self._build_signal_card()
layout.addWidget(signal_card)
layout.addSpacing(15)
# ====== Ranked Score Table ======
layout.addWidget(QLabel("Currency Rankings"))
heading = QLabel("Currency Rankings")
heading.setProperty("heading", True)
layout.addWidget(heading)
self.score_table = QTableWidget()
self.score_table.setColumnCount(8)
@@ -76,22 +78,35 @@ class DashboardTab(QWidget):
self.score_table.setSelectionBehavior(QTableWidget.SelectRows)
self.score_table.setSelectionMode(QTableWidget.SingleSelection)
# Store progress bars for strength column (column 7)
self.strength_bars = {}
# 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)
# Add progress bar for strength column
bar = QProgressBar()
bar.setRange(0, 100)
bar.setValue(0)
bar.setTextVisible(True)
bar.setFormat("")
self.score_table.setCellWidget(row, 7, bar)
self.strength_bars[row] = bar
self.score_table.resizeColumnsToContents()
for col in range(7):
self.score_table.horizontalHeader().setSectionResizeMode(col, QHeaderView.Stretch)
self.score_table.setColumnWidth(7, 140)
layout.addWidget(self.score_table)
layout.addSpacing(15)
# ====== Refresh Button ======
button_layout = QHBoxLayout()
button_layout.addStretch()
self.refresh_btn = QPushButton("Refresh")
self.refresh_btn.setObjectName("secondary")
self.refresh_btn.clicked.connect(self._refresh_display)
button_layout.addWidget(self.refresh_btn)
@@ -107,42 +122,41 @@ class DashboardTab(QWidget):
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;
}
""")
card.setObjectName("statusCard")
layout = QVBoxLayout()
layout.setSpacing(6)
# Title
title = QLabel("PRIMARY SIGNAL")
title.setFont(QFont("Arial", 10, QFont.Bold))
title.setStyleSheet("color: #495057;")
title.setProperty("subheading", True)
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.setProperty("value", True)
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))
self.gap_label.setStyleSheet("font-size: 15px; color: #5d6d7e;")
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;")
self.updated_label.setStyleSheet("color: #95a5a6; font-size: 12px;")
layout.addWidget(self.updated_label)
# Staleness warning (hidden by default)
self.stale_warning = QLabel("")
self.stale_warning.setStyleSheet(
"color: #e74c3c; font-weight: 700; font-size: 13px; padding: 6px 0;"
)
self.stale_warning.hide()
layout.addWidget(self.stale_warning)
layout.addStretch()
card.setLayout(layout)
return card
@@ -168,14 +182,11 @@ class DashboardTab(QWidget):
bias_matrix = {}
self.signal_generated.emit(strongest, weakest, gap, bias_matrix)
# Update signal label
self.signal_label.setText(signal_text)
# Color code based on status
if status == "ACTIVE":
self.signal_label.setStyleSheet("color: #27ae60;") # Green
self.signal_label.setStyleSheet("color: #27ae60;")
else:
self.signal_label.setStyleSheet("color: #e74c3c;") # Red
self.signal_label.setStyleSheet("color: #e74c3c;")
# Update gap label
gap_tier = scorer.get_gap_tier(gap)
@@ -195,6 +206,9 @@ class DashboardTab(QWidget):
# Update timestamp
self.updated_label.setText(f"Updated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
# Check for stale CPI/PMI data
self._check_data_staleness()
# Refresh score table
self._refresh_score_table()
@@ -203,28 +217,54 @@ class DashboardTab(QWidget):
self.signal_label.setText("ERROR")
self.signal_label.setStyleSheet("color: #e74c3c;")
def _check_data_staleness(self):
"""Show a warning if CPI/PMI data is older than 35 days."""
try:
monthly = self.db.get_monthly_data(self.current_month)
if not monthly:
return
timestamps = []
for ccy, data in monthly.items():
ts = data.get("entered_at")
if ts:
timestamps.append(ts)
if not timestamps:
return
latest = max(timestamps)
try:
latest_dt = datetime.strptime(latest.split(".")[0], "%Y-%m-%dT%H:%M:%S")
except ValueError:
latest_dt = datetime.strptime(latest[:10], "%Y-%m-%d")
days_old = (datetime.now() - latest_dt).days
if days_old > 35:
self.stale_warning.setText(
f"⚠ CPI/PMI data is {days_old} days old — refresh fundamental data"
)
self.stale_warning.show()
else:
self.stale_warning.hide()
except Exception:
self.stale_warning.hide()
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("")
self.strength_bars[row].setValue(0)
self.strength_bars[row].setFormat("")
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):
@@ -235,29 +275,15 @@ class DashboardTab(QWidget):
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, 2).setText(f"{rate:.2f}" if rate is not None else "")
self.score_table.item(row, 3).setText(f"{cpi:.2f}" if cpi is not None else "")
self.score_table.item(row, 4).setText(f"{pmi:.1f}" if pmi is not None else "")
self.score_table.item(row, 5).setText(f"{total_score:.1f}")
# Signal tag (BUY for strongest, SELL for weakest)
# Signal tag
if rank == 1:
self.score_table.item(row, 6).setText("BUY")
elif rank == len(config.CURRENCIES):
@@ -265,34 +291,30 @@ class DashboardTab(QWidget):
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)}%")
# Strength progress bar
bar = self.strength_bars[row]
score_int = min(int(total_score), 100)
bar.setValue(score_int)
bar.setFormat(f"{score_int}%")
bar.setStyleSheet(
"QProgressBar::chunk { background: qlineargradient(x1:0, y1:0, x2:1, y2:0, "
"stop:0 #2ecc71, stop:1 #27ae60); border-radius: 4px; }"
if rank == 1 else
"QProgressBar::chunk { background: qlineargradient(x1:0, y1:0, x2:1, y2:0, "
"stop:0 #e74c3c, stop:1 #c0392b); border-radius: 4px; }"
if rank == len(config.CURRENCIES) else
""
)
# 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))
# Row color coding
bg = QColor("#d5f4e6") if rank == 1 else QColor("#fadbd8") if rank == len(config.CURRENCIES) else QColor("#ffffff")
fg = QColor("#1e8449") if rank == 1 else QColor("#c0392b") if rank == len(config.CURRENCIES) else QColor("#2c3e50")
font = QFont("Segoe UI", 11, QFont.Bold) if rank in (1, len(config.CURRENCIES)) else QFont("Segoe UI", 11)
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()
for col in range(7):
self.score_table.item(row, col).setBackground(bg)
self.score_table.item(row, col).setForeground(fg)
self.score_table.item(row, col).setFont(font)
except Exception as e:
print(f"[ERROR] Failed to refresh score table: {e}")
+6 -39
View File
@@ -23,11 +23,11 @@ User flow:
from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QTableWidget, QTableWidgetItem,
QPushButton, QProgressBar, QComboBox, QSpinBox, QDoubleSpinBox, QHeaderView,
QPushButton, QProgressBar, QComboBox, QDoubleSpinBox, QHeaderView,
QFileDialog, QMessageBox
)
from PyQt5.QtCore import Qt, pyqtSignal, QDate
from PyQt5.QtGui import QColor, QFont, QBrush
from PyQt5.QtCore import Qt, pyqtSignal
from PyQt5.QtGui import QColor
from typing import Dict, Optional
from datetime import datetime, timedelta
import config
@@ -190,49 +190,16 @@ class MonthlyEntryTab(QWidget):
# ====== Buttons ======
button_layout = QHBoxLayout()
# Import Excel button
self.import_btn = QPushButton("📊 Import Excel")
self.import_btn.setMinimumHeight(40)
self.import_btn.setFont(QFont("Arial", 11, QFont.Bold))
self.import_btn.setStyleSheet("""
QPushButton {
background-color: #3498db;
color: white;
border: none;
border-radius: 5px;
padding: 10px 20px;
}
QPushButton:hover {
background-color: #2980b9;
}
""")
self.import_btn.setMinimumHeight(42)
button_layout.addWidget(self.import_btn)
button_layout.addStretch()
self.save_btn = QPushButton("Save & Calculate Scores")
self.save_btn.setObjectName("success")
self.save_btn.setEnabled(False)
self.save_btn.setMinimumHeight(40)
self.save_btn.setFont(QFont("Arial", 11, QFont.Bold))
self.save_btn.setStyleSheet("""
QPushButton:enabled {
background-color: #2ecc71;
color: white;
border: none;
border-radius: 5px;
padding: 10px 20px;
}
QPushButton:hover:enabled {
background-color: #27ae60;
}
QPushButton:disabled {
background-color: #95a5a6;
color: #7f8c8d;
border: none;
border-radius: 5px;
padding: 10px 20px;
}
""")
self.save_btn.setMinimumHeight(42)
button_layout.addWidget(self.save_btn)
layout.addLayout(button_layout)
+5 -4
View File
@@ -11,12 +11,11 @@ Features:
"""
from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QTableWidget, QTableWidgetItem, QHeaderView,
QWidget, QVBoxLayout, QLabel, QTableWidget, QTableWidgetItem, QHeaderView,
QMessageBox
)
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QColor, QFont
from typing import Dict, List
import config
from database import Database
@@ -41,6 +40,10 @@ class HistoryTab(QWidget):
"""Build the UI layout."""
layout = QVBoxLayout()
heading = QLabel("Signal History (Past 24 Months)")
heading.setProperty("heading", True)
layout.addWidget(heading)
# History table
self.history_table = QTableWidget()
self.history_table.setColumnCount(6)
@@ -48,8 +51,6 @@ class HistoryTab(QWidget):
"Month", "Signal", "Gap", "Strongest", "Weakest", "Status"
])
# Enable sorting
self.history_table.setSortingEnabled(False)
self.history_table.setSelectionBehavior(QTableWidget.SelectRows)
self.history_table.setSelectionMode(QTableWidget.SingleSelection)
self.history_table.itemClicked.connect(self._on_row_clicked)
+147 -61
View File
@@ -1,15 +1,15 @@
from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QTableWidget, QTableWidgetItem,
QPushButton, QComboBox, QCheckBox, QMessageBox, QStatusBar, QProgressBar,
QPushButton, QComboBox, QCheckBox, QMessageBox,
QHeaderView
)
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer
from PyQt5.QtGui import QColor, QFont, QBrush
from PyQt5.QtGui import QColor, QFont
from typing import Dict, Optional
from datetime import datetime, timezone
import config
from layer2_technical import TechnicalAnalyzer
from data_feeder import Mt5DataFeeder, MockDataFeeder
from data_feeder import Mt5DataFeeder
from currency_strength_matrix import CurrencyStrengthMatrix
@@ -75,43 +75,57 @@ class Layer2MonitorTab(QWidget):
def _init_ui(self):
"""Build UI layout."""
layout = QVBoxLayout()
layout.setSpacing(12)
# ====== Connection Panel ======
connection_layout = QHBoxLayout()
connection_layout.addWidget(QLabel("Data Source:"))
self.source_combo = QComboBox()
self.source_combo.addItems(["Mock (Test)", "MT5 (Live)"])
self.source_combo.addItems(["MT5 (Live)"])
self.source_combo.setCurrentIndex(0)
connection_layout.addWidget(self.source_combo)
connection_layout.addWidget(QLabel("TF:"))
self.tf_combo = QComboBox()
for tf_key in config.TIMEFRAMES:
self.tf_combo.addItem(config.TIMEFRAMES[tf_key]["label"], tf_key)
self.tf_combo.setCurrentText(config.TIMEFRAMES[config.DEFAULT_TIMEFRAME]["label"])
self.tf_combo.currentIndexChanged.connect(self._on_timeframe_changed)
connection_layout.addWidget(self.tf_combo)
self.connect_btn = QPushButton("Connect")
self.connect_btn.clicked.connect(self._on_connect_clicked)
connection_layout.addWidget(self.connect_btn)
# Status dot indicator
self.status_dot = QLabel("")
self.status_dot.setStyleSheet("color: #e74c3c; font-size: 18px;")
connection_layout.addWidget(self.status_dot)
self.status_label = QLabel("Disconnected")
self.status_label.setStyleSheet("color: red; font-weight: bold;")
self.status_label.setStyleSheet("color: #e74c3c; font-weight: 600;")
connection_layout.addWidget(self.status_label)
connection_layout.addStretch()
layout.addLayout(connection_layout)
layout.addSpacing(10)
# ====== Active Session Indicator (Task 4.1) ======
session_layout = QHBoxLayout()
session_layout.addWidget(QLabel("Active Session:"))
self.session_label = QLabel("")
self.session_label.setStyleSheet(
"font-weight: bold; font-size: 14px; padding: 2px 8px; "
"background-color: #ecf0f1; border-radius: 4px;"
"font-weight: 700; font-size: 14px; padding: 4px 14px; "
"background-color: #ecf0f1; border-radius: 12px;"
)
session_layout.addWidget(self.session_label)
session_layout.addStretch()
layout.addLayout(session_layout)
layout.addSpacing(5)
# ====== Z-Score Table ======
layout.addWidget(QLabel("Technical Analysis — All Pairs"))
heading = QLabel("Technical Analysis — All Pairs")
heading.setProperty("heading", True)
layout.addWidget(heading)
self.tech_table = QTableWidget()
self.tech_table.setColumnCount(7)
@@ -120,48 +134,52 @@ class Layer2MonitorTab(QWidget):
])
self.tech_table.setRowCount(28)
self.tech_table.setAlternatingRowColors(True)
self.tech_table.horizontalHeader().setStretchLastSection(True)
header = self.tech_table.horizontalHeader()
for c in range(7):
header.setSectionResizeMode(c, QHeaderView.Stretch)
layout.addWidget(self.tech_table)
layout.addSpacing(10)
# ====== Alerts Panel ======
alerts_layout = QHBoxLayout()
alerts_layout.addWidget(QLabel("Overbought Pairs:"))
alerts_layout.addWidget(QLabel("Overbought:"))
self.overbought_label = QLabel("")
self.overbought_label.setStyleSheet("color: #e74c3c; font-weight: bold;")
self.overbought_label.setStyleSheet("color: #e74c3c; font-weight: 700;")
alerts_layout.addWidget(self.overbought_label)
alerts_layout.addSpacing(20)
alerts_layout.addWidget(QLabel("Oversold Pairs:"))
alerts_layout.addWidget(QLabel("Oversold:"))
self.oversold_label = QLabel("")
self.oversold_label.setStyleSheet("color: #27ae60; font-weight: bold;")
self.oversold_label.setStyleSheet("color: #27ae60; font-weight: 700;")
alerts_layout.addWidget(self.oversold_label)
alerts_layout.addStretch()
layout.addLayout(alerts_layout)
layout.addSpacing(10)
# ====== Currency Strength Matrix ======
matrix_group = QWidget()
matrix_layout = QVBoxLayout(matrix_group)
matrix_layout.setContentsMargins(0, 0, 0, 0)
matrix_heading = QLabel("Currency Strength Matrix (S.A.T.O.R.I.)")
matrix_heading.setProperty("heading", True)
layout.addWidget(matrix_heading)
matrix_layout.addWidget(QLabel("Currency Strength Matrix (S.A.T.O.R.I.)"))
self.matrix_cross_label = QLabel("Matrix Cross: —")
self.matrix_cross_label.setStyleSheet("font-weight: bold; font-size: 13px; color: #2c3e50;")
matrix_layout.addWidget(self.matrix_cross_label)
self.matrix_cross_label.setStyleSheet("font-weight: 700; font-size: 14px;")
layout.addWidget(self.matrix_cross_label)
self.divergence_label = QLabel("Divergence Gap: 0.0")
self.divergence_label.setStyleSheet("color: #7f8c8d;")
matrix_layout.addWidget(self.divergence_label)
self.divergence_label.setStyleSheet("color: #7f8c8d; font-size: 12px;")
layout.addWidget(self.divergence_label)
alert_row = QHBoxLayout()
self.strong_alert = QLabel("")
matrix_layout.addWidget(self.strong_alert)
self.strong_alert.setStyleSheet("color: #27ae60; font-weight: 600;")
alert_row.addWidget(self.strong_alert)
self.weak_alert = QLabel("")
matrix_layout.addWidget(self.weak_alert)
self.weak_alert.setStyleSheet("color: #e74c3c; font-weight: 600;")
alert_row.addWidget(self.weak_alert)
alert_row.addStretch()
layout.addLayout(alert_row)
self.matrix_table = QTableWidget()
self.matrix_table.setColumnCount(5)
@@ -169,14 +187,13 @@ class Layer2MonitorTab(QWidget):
"Rank", "Currency", "Strength Z", "Direction", "Session SRV"
])
self.matrix_table.setRowCount(8)
self.matrix_table.setMaximumHeight(240)
self.matrix_table.horizontalHeader().setStretchLastSection(True)
matrix_layout.addWidget(self.matrix_table)
self.matrix_table.setMaximumHeight(220)
m_header = self.matrix_table.horizontalHeader()
for c in range(5):
m_header.setSectionResizeMode(c, QHeaderView.Stretch)
layout.addWidget(self.matrix_table)
layout.addWidget(matrix_group)
layout.addSpacing(10)
# ====== Refresh Button ======
# ====== Controls ======
button_layout = QHBoxLayout()
self.auto_refresh_check = QCheckBox("Auto-refresh (every 1s)")
@@ -184,6 +201,7 @@ class Layer2MonitorTab(QWidget):
button_layout.addWidget(self.auto_refresh_check)
refresh_btn = QPushButton("Refresh Now")
refresh_btn.setObjectName("secondary")
refresh_btn.clicked.connect(self._refresh_display)
button_layout.addWidget(refresh_btn)
@@ -196,13 +214,13 @@ class Layer2MonitorTab(QWidget):
self.refresh_timer = QTimer()
self.refresh_timer.timeout.connect(self._refresh_display)
self._debounce_timer = QTimer()
self._debounce_timer.setSingleShot(True)
self._debounce_timer.timeout.connect(self._refresh_display)
def _setup_data_source(self):
"""Initialize data source."""
source = self.source_combo.currentText()
if "MT5" in source:
self.data_feeder = Mt5DataFeeder()
else:
self.data_feeder = MockDataFeeder()
self.data_feeder = Mt5DataFeeder()
def _on_connect_clicked(self):
"""Handle connect button click."""
@@ -211,17 +229,82 @@ class Layer2MonitorTab(QWidget):
else:
self._connect()
def _on_timeframe_changed(self, idx: int):
tf_key = self.tf_combo.itemData(idx)
if tf_key:
self.tech_analyzer.set_timeframe(tf_key)
if self.connected and hasattr(self.data_feeder, 'USD_PAIRS'):
self._seed_historical_bars()
def _seed_historical_bars(self):
"""Seed the analyzer with 24h of historical M5 bar data for stable Z-scores."""
if hasattr(self.data_feeder, 'generate_mock_bars'):
bars = self.data_feeder.generate_mock_bars(n_bars=config.BAR_LOOKBACK_BARS)
elif hasattr(self.data_feeder, 'fetch_historical_closes_all_pairs'):
bars = self.data_feeder.fetch_historical_closes_all_pairs(
days=1, interval="5min"
)
else:
"""Seed the analyzer with bar data at the selected timeframe.
Fetches only the 7 USD pairs and derives all cross rates.
"""
if not hasattr(self.data_feeder, 'USD_PAIRS'):
return
self.tech_analyzer.seed_bars(bars)
interval_map = {
"M5": "5min", "M15": "15min",
"H1": "1h", "H4": "4h",
}
tf_key = self.tech_analyzer.current_timeframe
interval = interval_map.get(tf_key, "15min")
outputsize = "full" if tf_key in ("M5", "M15") else "full"
usd_pairs = self.data_feeder.USD_PAIRS
raw_ohlc: dict[str, list[dict]] = {}
for pair in usd_pairs:
base, quote = pair.split("_")
candles = self.data_feeder.get_historical_candles(
from_currency=base, to_currency=quote,
interval=interval, outputsize=outputsize
)
if candles:
raw_ohlc[pair] = candles
if not raw_ohlc:
return
n_bars = min(len(c) for c in raw_ohlc.values())
if n_bars < 2:
return
currencies = config.CURRENCIES
derived_ohlc: dict[str, list[dict]] = {}
for base in currencies:
for quote in currencies:
if base == quote:
continue
derived_ohlc[f"{base}_{quote}"] = []
for i in range(n_bars):
usd_rates: dict[str, float] = {"USD": 1.0}
for pair in usd_pairs:
base, quote = pair.split("_")
c = raw_ohlc[pair][i]
mid = c["close"]
if base == "USD":
usd_rates[quote] = 1.0 / mid if mid else None
else:
usd_rates[base] = mid
for base in currencies:
bv = usd_rates.get(base)
if bv is None:
continue
for quote in currencies:
if base == quote:
continue
qv = usd_rates.get(quote)
if qv is not None:
rate = bv / qv
# Estimate OHLC for the cross pair
derived_ohlc[f"{base}_{quote}"].append({
"close": rate,
"high": rate * 1.0003,
"low": rate * 0.9997,
})
self.tech_analyzer.seed_ohlc(derived_ohlc)
def _connect(self):
"""Connect to data source."""
@@ -231,10 +314,6 @@ class Layer2MonitorTab(QWidget):
QMessageBox.warning(self, "Connection Error", f"Failed to connect to data source:\n{reason}")
return
# Seed bar_history with 24h of M5 close prices so Z-scores are
# anchored to a meaningful multi-hour frame, not tick noise.
self._seed_historical_bars()
instruments = self.data_feeder.get_all_major_pairs()
self.streamer_thread = DataStreamerThread(self.data_feeder, instruments)
self.streamer_thread.price_updated.connect(self._on_price_received)
@@ -243,14 +322,20 @@ class Layer2MonitorTab(QWidget):
self.streamer_thread.start()
if self.auto_refresh_check.isChecked():
self.refresh_timer.start(1000)
self.refresh_timer.start(3000)
self.connected = True
self.connect_btn.setText("Disconnect")
self.connect_btn.setObjectName("danger")
self.connect_btn.style().unpolish(self.connect_btn)
self.connect_btn.style().polish(self.connect_btn)
self.status_dot.setStyleSheet("color: #27ae60; font-size: 18px;")
self.status_label.setText("Connected")
self.status_label.setStyleSheet("color: #27ae60; font-weight: bold;")
self.status_label.setStyleSheet("color: #27ae60; font-weight: 600;")
self._refresh_display()
QTimer.singleShot(0, self._seed_historical_bars)
except Exception as e:
QMessageBox.critical(self, "Error", f"Connection failed: {e}")
@@ -265,17 +350,22 @@ class Layer2MonitorTab(QWidget):
self.connected = False
self.connect_btn.setText("Connect")
self.connect_btn.setObjectName("")
self.connect_btn.style().unpolish(self.connect_btn)
self.connect_btn.style().polish(self.connect_btn)
self.status_dot.setStyleSheet("color: #e74c3c; font-size: 18px;")
self.status_label.setText("Disconnected")
self.status_label.setStyleSheet("color: #e74c3c; font-weight: bold;")
self.status_label.setStyleSheet("color: #e74c3c; font-weight: 600;")
def _on_price_received(self, price_data):
"""Handle price update from data feeder."""
"""Handle price update — just add data, debounce display refresh."""
pair = price_data.get('pair')
mid_price = price_data.get('mid')
if pair and mid_price:
self.tech_analyzer.add_price_data(pair, mid_price)
self._refresh_display()
if not self._debounce_timer.isActive():
self._debounce_timer.start(2000)
def _on_connected(self, is_connected):
"""Handle connection status change."""
@@ -358,8 +448,6 @@ class Layer2MonitorTab(QWidget):
self.overbought_label.setText(overbought_text)
self.oversold_label.setText(oversold_text)
self.tech_table.resizeColumnsToContents()
# ====== Currency Strength Matrix (persistent instance) ======
current_prices = {}
for pair in z_scores:
@@ -466,8 +554,6 @@ class Layer2MonitorTab(QWidget):
srv_item.setBackground(QColor("#fff3e0"))
self.matrix_table.setItem(row, 4, srv_item)
self.matrix_table.resizeColumnsToContents()
except Exception as e:
print(f"[Layer2] Display error: {e}")
+4 -17
View File
@@ -16,13 +16,12 @@ from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QDoubleSpinBox,
QPushButton, QCheckBox, QGroupBox, QSpinBox, QMessageBox, QScrollArea
)
from PyQt5.QtCore import Qt, pyqtSignal, QThread
from PyQt5.QtCore import pyqtSignal, QThread
from PyQt5.QtGui import QFont
from typing import Dict, Optional
from typing import Optional
import config
from data_feeder import Mt5DataFeeder
from fred_client import FredClient
import os
from pathlib import Path
@@ -272,24 +271,12 @@ class SettingsTab(QWidget):
save_layout.addStretch()
save_btn = QPushButton("Save Settings")
save_btn.setMinimumHeight(40)
save_btn.setFont(QFont("Arial", 11, QFont.Bold))
save_btn.setStyleSheet("""
QPushButton {
background-color: #3498db;
color: white;
border: none;
border-radius: 5px;
padding: 10px 20px;
}
QPushButton:hover {
background-color: #2980b9;
}
""")
save_btn.setMinimumHeight(42)
save_btn.clicked.connect(self._save_settings)
save_layout.addWidget(save_btn)
reset_btn = QPushButton("Reset to Defaults")
reset_btn.setObjectName("secondary")
reset_btn.clicked.connect(self._reset_to_defaults)
save_layout.addWidget(reset_btn)