mirror of
https://github.com/Sabermrddz/QuantCore-FX.git
synced 2026-08-15 11:48:05 +00:00
layer one v4
This commit is contained in:
+106
-112
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user