Files

276 lines
10 KiB
Python
Raw Permalink Normal View History

2026-06-17 11:26:57 +01:00
"""
APEX Confluence Signals Tab — Layer 1 + Layer 2 Merging
2026-06-18 10:16:51 +01:00
DISPLAY ONLY — no position sizing, no auto-execution, no hedging.
Shows pair, direction, strength/gap, confluence agreement, and text-described zones.
2026-06-17 11:26:57 +01:00
"""
from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QTableWidget, QTableWidgetItem,
2026-06-18 10:16:51 +01:00
QPushButton, QFrame, QHeaderView
2026-06-17 11:26:57 +01:00
)
from PyQt5.QtCore import Qt, QTimer
2026-06-18 09:35:03 +01:00
from PyQt5.QtGui import QColor, QFont
2026-06-17 11:26:57 +01:00
from typing import Dict, Optional
from datetime import datetime
import config
from layer2_technical import TechnicalAnalyzer
2026-06-18 10:16:51 +01:00
from confluence_filter import ConfluenceFilter
2026-06-17 11:26:57 +01:00
from database import Database
class ConfluenceSignalsTab(QWidget):
2026-06-18 10:16:51 +01:00
"""Confluence signals monitoring — display only, no execution."""
2026-06-17 11:26:57 +01:00
def __init__(self, db: Database, tech_analyzer: TechnicalAnalyzer):
super().__init__()
self.db = db
self.tech_analyzer = tech_analyzer
2026-06-18 09:35:03 +01:00
self.confluence = ConfluenceFilter(tech_analyzer, db=db)
2026-06-17 11:26:57 +01:00
self._init_ui()
self._setup_auto_refresh()
def _init_ui(self):
"""Build UI layout."""
layout = QVBoxLayout()
2026-06-18 09:35:03 +01:00
layout.setSpacing(12)
2026-06-17 11:26:57 +01:00
# ====== Confluence Status Card ======
card = self._build_status_card()
layout.addWidget(card)
# ====== Active Signals Table ======
2026-06-18 09:35:03 +01:00
heading = QLabel("Confluence Signals (Layer 1 + Layer 2)")
heading.setProperty("heading", True)
layout.addWidget(heading)
2026-06-17 11:26:57 +01:00
self.signals_table = QTableWidget()
self.signals_table.setColumnCount(8)
self.signals_table.setHorizontalHeaderLabels([
2026-06-18 09:35:03 +01:00
"Pair", "Signal", "Entry", "SL", "TP", "L1 Gap", "Z-Score", "Confidence"
2026-06-17 11:26:57 +01:00
])
self.signals_table.setRowCount(10)
layout.addWidget(self.signals_table)
2026-06-18 10:16:51 +01:00
# ====== Controls ======
2026-06-17 11:26:57 +01:00
button_layout = QHBoxLayout()
refresh_btn = QPushButton("Refresh Signals")
2026-06-18 09:35:03 +01:00
refresh_btn.setObjectName("secondary")
2026-06-17 11:26:57 +01:00
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()
2026-06-18 09:35:03 +01:00
card.setObjectName("statusCard")
2026-06-17 11:26:57 +01:00
layout = QVBoxLayout()
2026-06-18 09:35:03 +01:00
layout.setSpacing(8)
2026-06-17 11:26:57 +01:00
title = QLabel("CONFLUENCE STATUS")
2026-06-18 09:35:03 +01:00
title.setProperty("subheading", True)
2026-06-17 11:26:57 +01:00
layout.addWidget(title)
# Layer 1 status
2026-06-18 09:35:03 +01:00
l1 = QHBoxLayout()
l1.addWidget(QLabel("Layer 1 (Fundamental):"))
2026-06-17 11:26:57 +01:00
self.layer1_status_label = QLabel("No bias")
2026-06-18 09:35:03 +01:00
self.layer1_status_label.setStyleSheet("font-weight: 700;")
l1.addWidget(self.layer1_status_label)
l1.addStretch()
layout.addLayout(l1)
2026-06-17 11:26:57 +01:00
# Directional Bias Matrix display
2026-06-18 09:35:03 +01:00
bl = QHBoxLayout()
bl.addWidget(QLabel("Macro Boundaries:"))
2026-06-17 11:26:57 +01:00
self.bias_matrix_label = QLabel("No bias matrix")
2026-06-18 09:35:03 +01:00
self.bias_matrix_label.setStyleSheet("color: #8e44ad; font-size: 12px;")
bl.addWidget(self.bias_matrix_label)
bl.addStretch()
layout.addLayout(bl)
2026-06-17 11:26:57 +01:00
# Layer 2 status
2026-06-18 09:35:03 +01:00
l2 = QHBoxLayout()
l2.addWidget(QLabel("Layer 2 (Technical):"))
2026-06-17 11:26:57 +01:00
self.layer2_status_label = QLabel("No extreme")
self.layer2_status_label.setStyleSheet("color: #95a5a6;")
2026-06-18 09:35:03 +01:00
l2.addWidget(self.layer2_status_label)
l2.addStretch()
layout.addLayout(l2)
2026-06-17 11:26:57 +01:00
# Confluence result
2026-06-18 09:35:03 +01:00
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)
2026-06-17 11:26:57 +01:00
# Matrix cross (S.A.T.O.R.I.)
2026-06-18 09:35:03 +01:00
mx = QHBoxLayout()
mx.addWidget(QLabel("Matrix Cross:"))
2026-06-17 11:26:57 +01:00
self.matrix_cross_label = QLabel("—")
2026-06-18 09:35:03 +01:00
self.matrix_cross_label.setStyleSheet("font-weight: 700; color: #8e44ad;")
mx.addWidget(self.matrix_cross_label)
mx.addStretch()
layout.addLayout(mx)
2026-06-17 11:26:57 +01:00
self.matrix_detail_label = QLabel("")
2026-06-18 09:35:03 +01:00
self.matrix_detail_label.setStyleSheet("color: #7f8c8d; font-size: 11px;")
2026-06-17 11:26:57 +01:00
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):
2026-06-18 09:35:03 +01:00
"""Refresh confluence signal display — single matrix build."""
2026-06-17 11:26:57 +01:00
try:
2026-06-18 09:35:03 +01:00
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', {})
2026-06-17 11:26:57 +01:00
if bias:
parts = []
for ccy in config.CURRENCIES:
2026-06-18 09:35:03 +01:00
d = bias.get(ccy, {}).get("direction", "—")
2026-06-17 11:26:57 +01:00
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")
2026-06-18 09:35:03 +01:00
mc = mr.get("matrix_cross", "—")
gap = mr.get("divergence_gap", 0)
has_div = mr.get("has_divergence", False)
ranked = mr.get("ranked", [])
2026-06-17 11:26:57 +01:00
if mc and mc != "N/A":
self.matrix_cross_label.setText(f"{mc} (spread: {gap:.2f}σ)")
2026-06-18 09:35:03 +01:00
color = "#e74c3c" if has_div else "#8e44ad"
self.matrix_cross_label.setStyleSheet(f"font-weight: bold; color: {color};")
2026-06-17 11:26:57 +01:00
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:
2026-06-18 09:35:03 +01:00
l1s = cf.layer1_strongest or "—"
l1w = cf.layer1_weakest or "—"
self.layer1_status_label.setText(f"PAR {l1s}/{l1w} (Gap: {cf.layer1_gap:.1f})")
2026-06-17 11:26:57 +01:00
2026-06-18 09:35:03 +01:00
pair = f"{cf.layer1_strongest}_{cf.layer1_weakest}" if cf.layer1_strongest else "—"
2026-06-17 11:26:57 +01:00
z_score = self.tech_analyzer.get_z_score(pair) if pair != "—" else 0
2026-06-18 09:35:03 +01:00
self.layer2_status_label.setText(f"TCH {pair} Z-score: {z_score:.2f}")
2026-06-17 11:26:57 +01:00
self.layer2_status_label.setStyleSheet("color: #27ae60;")
2026-06-18 09:35:03 +01:00
label_text = f"✅ MATRIX DIVERGENCE: {strength:.0f}%" if has_div else f"✅ CONFLUENCE: {strength:.0f}%"
self.confluence_status_label.setText(label_text)
2026-06-17 11:26:57 +01:00
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;")
2026-06-18 09:35:03 +01:00
self.confluence_status_label.setText("✕ NO CONFLUENCE")
2026-06-17 11:26:57 +01:00
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
2026-06-18 09:35:03 +01:00
def _item(text, align=True):
item = QTableWidgetItem(str(text))
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
if align:
item.setTextAlignment(Qt.AlignCenter)
return item
2026-06-17 11:26:57 +01:00
2026-06-18 09:35:03 +01:00
# 0: Pair
self.signals_table.setItem(row, 0, _item(pair_key, False))
2026-06-17 11:26:57 +01:00
2026-06-18 09:35:03 +01:00
# 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
2026-06-17 11:26:57 +01:00
z = report.get('layer2_z_score', 0)
if signal.get('type') == 'MATRIX_DIVERGENCE':
z = report.get('matrix_cross_z', 0)
2026-06-18 09:35:03 +01:00
self.signals_table.setItem(row, 6, _item(f"{z:.2f}"))
2026-06-17 11:26:57 +01:00
2026-06-18 09:35:03 +01:00
# 7: Confidence
2026-06-17 11:26:57 +01:00
strength = signal.get('strength', confluence_strength)
2026-06-18 09:35:03 +01:00
conf_item = _item(f"{strength:.0f}%")
conf_item.setFont(QFont("Segoe UI", 10, QFont.Bold))
self.signals_table.setItem(row, 7, conf_item)
2026-06-17 11:26:57 +01:00
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