""" APEX Confluence Signals Tab — Layer 1 + Layer 2 Merging DISPLAY ONLY — no position sizing, no auto-execution, no hedging. Shows pair, direction, strength/gap, confluence agreement, and text-described zones. """ from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLabel, QTableWidget, QTableWidgetItem, QPushButton, QFrame, QHeaderView ) from PyQt5.QtCore import Qt, QTimer from PyQt5.QtGui import QColor, QFont from typing import Dict, Optional from datetime import datetime import config from layer2_technical import TechnicalAnalyzer from confluence_filter import ConfluenceFilter from database import Database class ConfluenceSignalsTab(QWidget): """Confluence signals monitoring — display only, no execution.""" def __init__(self, db: Database, tech_analyzer: TechnicalAnalyzer): super().__init__() self.db = db self.tech_analyzer = tech_analyzer self.confluence = ConfluenceFilter(tech_analyzer, db=db) self._init_ui() self._setup_auto_refresh() def _init_ui(self): """Build UI layout.""" layout = QVBoxLayout() layout.setSpacing(12) # ====== Confluence Status Card ====== card = self._build_status_card() layout.addWidget(card) # ====== Active Signals Table ====== 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", "Signal", "Entry", "SL", "TP", "L1 Gap", "Z-Score", "Confidence" ]) self.signals_table.setRowCount(10) layout.addWidget(self.signals_table) # ====== Controls ====== button_layout = QHBoxLayout() refresh_btn = QPushButton("Refresh Signals") refresh_btn.setObjectName("secondary") refresh_btn.clicked.connect(self._refresh_signals) button_layout.addWidget(refresh_btn) button_layout.addStretch() layout.addLayout(button_layout) layout.addStretch() self.setLayout(layout) def _build_status_card(self) -> QFrame: """Build confluence status card.""" card = QFrame() card.setObjectName("statusCard") layout = QVBoxLayout() layout.setSpacing(8) title = QLabel("CONFLUENCE STATUS") title.setProperty("subheading", True) layout.addWidget(title) # Layer 1 status l1 = QHBoxLayout() l1.addWidget(QLabel("Layer 1 (Fundamental):")) self.layer1_status_label = QLabel("No bias") self.layer1_status_label.setStyleSheet("font-weight: 700;") l1.addWidget(self.layer1_status_label) l1.addStretch() layout.addLayout(l1) # Directional Bias Matrix display bl = QHBoxLayout() bl.addWidget(QLabel("Macro Boundaries:")) self.bias_matrix_label = QLabel("No bias matrix") self.bias_matrix_label.setStyleSheet("color: #8e44ad; font-size: 12px;") bl.addWidget(self.bias_matrix_label) bl.addStretch() layout.addLayout(bl) # Layer 2 status l2 = QHBoxLayout() l2.addWidget(QLabel("Layer 2 (Technical):")) self.layer2_status_label = QLabel("No extreme") self.layer2_status_label.setStyleSheet("color: #95a5a6;") l2.addWidget(self.layer2_status_label) l2.addStretch() layout.addLayout(l2) # Confluence result 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.) mx = QHBoxLayout() mx.addWidget(QLabel("Matrix Cross:")) self.matrix_cross_label = QLabel("—") 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: 11px;") layout.addWidget(self.matrix_detail_label) card.setLayout(layout) return card def set_layer1_signal(self, strongest: str, weakest: str, gap: float, bias_matrix: dict = None): """Update Layer 1 directional bias matrix from Dashboard.""" self.confluence.set_layer1_bias(strongest, weakest, gap, bias_matrix) self._refresh_signals() def _refresh_signals(self): """Refresh confluence signal display — single matrix build.""" try: 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', {}) if bias: parts = [] for ccy in config.CURRENCIES: d = bias.get(ccy, {}).get("direction", "—") if d == "STRONG": parts.append(f"{ccy}↑") elif d == "WEAK": parts.append(f"{ccy}↓") else: parts.append(f"{ccy}—") self.bias_matrix_label.setText(" ".join(parts)) else: self.bias_matrix_label.setText("No bias matrix") 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}σ)") 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;") if ranked: top3 = [f"{c[0]}({c[1]:+.1f})" for c in ranked[:3]] bot3 = [f"{c[0]}({c[1]:+.1f})" for c in ranked[-3:]] self.matrix_detail_label.setText( f"Strongest → {' | '.join(top3)} — Weakest → {' | '.join(bot3)}" ) else: self.matrix_detail_label.setText("") if should_enter: 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"{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"TCH {pair} Z-score: {z_score:.2f}") self.layer2_status_label.setStyleSheet("color: #27ae60;") 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.setStyleSheet("color: #e74c3c; font-weight: bold;") except Exception as e: print(f"[Confluence] Error refreshing: {e}") def _populate_signal_table(self, confluence_strength: float): report = self.confluence.get_confluence_report() signals = self.confluence.get_all_signals() self.signals_table.clearContents() row = 0 for pair_key, signal in signals.items(): if row >= self.signals_table.rowCount(): break def _item(text, align=True): item = QTableWidgetItem(str(text)) item.setFlags(item.flags() & ~Qt.ItemIsEditable) if align: item.setTextAlignment(Qt.AlignCenter) return item # 0: Pair self.signals_table.setItem(row, 0, _item(pair_key, False)) # 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) self.signals_table.setItem(row, 6, _item(f"{z:.2f}")) # 7: Confidence strength = signal.get('strength', confluence_strength) 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 def _setup_auto_refresh(self): """Setup automatic refresh timer.""" self.refresh_timer = QTimer() self.refresh_timer.timeout.connect(self._refresh_signals) self.refresh_timer.start(5000) # Refresh every 5 seconds