diff --git a/config.py b/config.py index e981b7d..67f246d 100644 --- a/config.py +++ b/config.py @@ -212,16 +212,18 @@ CURRENCY_EMOJIS = { MT5_SYMBOL_SUFFIX = os.getenv("MT5_SYMBOL_SUFFIX", "") # Technical Analysis Settings -Z_SCORE_THRESHOLD = float(os.getenv("Z_SCORE_THRESHOLD", 2.0)) # Overbought/oversold level +Z_SCORE_THRESHOLD = float(os.getenv("Z_SCORE_THRESHOLD", 2.0)) # Overbought/oversold level (legacy/macro) +SCALP_Z_SCORE_THRESHOLD = float(os.getenv("SCALP_Z_SCORE_THRESHOLD", 1.5)) # Intraday threshold (more sensitive) +SCALP_MIN_GAP_TO_TRADE = float(os.getenv("SCALP_MIN_GAP", 2.0)) # Intraday min gap (sigma units) -# Multi-timeframe configuration +# Multi-timeframe configuration (short lookbacks for scalping) TIMEFRAMES = { - "M5": {"interval": "5min", "bars": 288, "label": "5 min"}, - "M15": {"interval": "15min", "bars": 96, "label": "15 min"}, - "H1": {"interval": "1h", "bars": 48, "label": "1 hour"}, - "H4": {"interval": "4h", "bars": 24, "label": "4 hour"}, + "M5": {"interval": "5min", "bars": 48, "label": "5 min"}, + "M15": {"interval": "15min", "bars": 16, "label": "15 min"}, + "H1": {"interval": "1h", "bars": 12, "label": "1 hour"}, + "H4": {"interval": "4h", "bars": 6, "label": "4 hour"}, } -DEFAULT_TIMEFRAME = os.getenv("DEFAULT_TIMEFRAME", "M15") +DEFAULT_TIMEFRAME = os.getenv("DEFAULT_TIMEFRAME", "M5") # Historical bar config (backward compat) BAR_TIMEFRAME = os.getenv("BAR_TIMEFRAME", "M5") @@ -241,7 +243,17 @@ SESSION_LONDON_CLOSE = 16 # 16:00 UTC SESSION_NEWYORK_OPEN = 13 # 13:00 UTC SESSION_NEWYORK_CLOSE = 21# 21:00 UTC -# Confluence Settings +# ============================================================================ +# Confluence Layer Weights (Scalper Profile) +# ============================================================================ +# Effective weight distribution for signal display: +# - Market Structure + Order Flow (Currency Strength Matrix / Z-scores): ~65% +# - Currency Power Matrix (Session SRV + momentum): ~25% +# - Macro / Fundamental Backdrop (Layer 1 scorer, advisory only): ~10% +# +# The macro layer is DISPLAY ONLY — it never blocks or vetoes a trade signal. +# Currency Power Matrix refers to CurrencyStrengthMatrix (this engine). +# ============================================================================ CONFLUENCE_ENABLED = os.getenv("CONFLUENCE_ENABLED", "true").lower() == "true" MIN_CONFLUENCE_STRENGTH = float(os.getenv("MIN_CONFLUENCE_STRENGTH", 60.0)) # 60% confidence threshold diff --git a/confluence_filter.py b/confluence_filter.py index cd97f8b..4b7fea8 100644 --- a/confluence_filter.py +++ b/confluence_filter.py @@ -72,13 +72,10 @@ class ConfluenceFilter: print(f"[Confluence] Monthly bias matrix set: {directions}") def _check_boundary(self, short_ccy: str, long_ccy: str) -> Tuple[bool, str]: - """Check if a proposed trade crosses the Layer 1 macro boundary. + """Advisory-only macro boundary check — does NOT block any signal. - A trade proposes SHORT short_ccy + LONG long_ccy. - Boundary rules: - - STRONG currencies cannot be shorted - - WEAK currencies cannot be longed - - NEUTRAL currencies have no restriction + Returns (True, reason) always. The bias matrix is displayed for + context in the UI but never gates/ vetoes a trade signal. Returns: (allowed: bool, reason: str) @@ -89,17 +86,13 @@ class ConfluenceFilter: short_dir = self.bias_matrix.get(short_ccy, {}).get("direction", "NEUTRAL") long_dir = self.bias_matrix.get(long_ccy, {}).get("direction", "NEUTRAL") + notes = [] if short_dir == "STRONG": - return ( - False, - f"Cannot short {short_ccy}: classified STRONG by Layer 1 macro bias" - ) + notes.append(f"{short_ccy}=STRONG (advisory)") if long_dir == "WEAK": - return ( - False, - f"Cannot long {long_ccy}: classified WEAK by Layer 1 macro bias" - ) - return True, "Within macro boundary" + notes.append(f"{long_ccy}=WEAK (advisory)") + advisory = f"Within macro boundary — {' | '.join(notes) if notes else 'neutral'}" + return True, advisory def check_entry_confluence(self, current_prices: Dict[str, float] = None ) -> Tuple[bool, str, float, Optional[Dict]]: @@ -109,58 +102,45 @@ class ConfluenceFilter: mc = self.matrix.get_matrix_cross() gap = self.matrix.get_divergence_gap() - if mc and "_" in mc: + if mc and "_" in mc and gap >= config.SCALP_MIN_GAP_TO_TRADE: short_ccy, long_ccy = mc.split("_", 1) - allowed, reason = self._check_boundary(short_ccy, long_ccy) - if allowed: - confidence = min(abs(gap) / 4.0, 1.0) * 100 - self.confluence_strength = confidence - self.last_confluence_check = datetime.now() - direction = "SHORT" if confidence > 50 else "LONG" - entry = self.tech_analyzer.get_last_price(mc) or 0.0 - sl_tp = self.tech_analyzer.calculate_sl_tp(mc, direction, entry) - if self.db: - self.db.save_confluence_signal( - pair=mc, signal_type="MATRIX_DIVERGENCE", - confidence=confidence, z_score=None, gap=gap, - reason=f"Matrix cross {mc} gap={gap:.1f}σ", - layer1_active=self.layer1_is_active, - ) - return (True, f"MATRIX DIVERGENCE: {mc} (Gap: {gap:.1f}σ)", confidence, sl_tp) - else: - self.confluence_strength = 0.0 - self.last_confluence_check = datetime.now() - return (False, f"MATRIX DIVERGENCE BLOCKED — {reason}", 0.0, None) + _, advisory = self._check_boundary(short_ccy, long_ccy) + confidence = min(abs(gap) / 4.0, 1.0) * 100 + self.confluence_strength = confidence + self.last_confluence_check = datetime.now() + direction = "SHORT" if confidence > 50 else "LONG" + entry = self.tech_analyzer.get_last_price(mc) or 0.0 + sl_tp = self.tech_analyzer.calculate_sl_tp(mc, direction, entry) + if self.db: + self.db.save_confluence_signal( + pair=mc, signal_type="MATRIX_DIVERGENCE", + confidence=confidence, z_score=None, gap=gap, + reason=f"Matrix cross {mc} gap={gap:.1f}σ | {advisory}", + layer1_active=self.layer1_is_active, + ) + return (True, f"MATRIX DIVERGENCE: {mc} (Gap: {gap:.1f}σ)", confidence, sl_tp) all_z = self.tech_analyzer.get_all_z_scores() sorted_pairs = sorted(all_z.items(), key=lambda x: abs(x[1]), reverse=True) for pair, z_score in sorted_pairs: - if abs(z_score) < config.Z_SCORE_THRESHOLD: + if abs(z_score) < config.SCALP_Z_SCORE_THRESHOLD: continue - base, quote = pair.split("_") - if z_score > 0: - short_ccy, long_ccy = base, quote - else: - short_ccy, long_ccy = quote, base - - allowed, reason = self._check_boundary(short_ccy, long_ccy) - if allowed: - confidence = min(abs(z_score) / 3.0, 1.0) * 100 - self.confluence_strength = confidence - self.last_confluence_check = datetime.now() - direction = "SHORT" if z_score > 0 else "LONG" - entry = self.tech_analyzer.get_last_price(pair) or 0.0 - sl_tp = self.tech_analyzer.calculate_sl_tp(pair, direction, entry) - if self.db: - self.db.save_confluence_signal( - pair=pair, signal_type="PAIR_EXTREME", - confidence=confidence, z_score=z_score, - reason=f"Z={z_score:.2f} within macro boundary", - layer1_active=self.layer1_is_active, - ) - return (True, f"PAIR EXTREME: {pair} Z={z_score:.2f}", confidence, sl_tp) + confidence = min(abs(z_score) / 3.0, 1.0) * 100 + self.confluence_strength = confidence + self.last_confluence_check = datetime.now() + direction = "SHORT" if z_score > 0 else "LONG" + entry = self.tech_analyzer.get_last_price(pair) or 0.0 + sl_tp = self.tech_analyzer.calculate_sl_tp(pair, direction, entry) + if self.db: + self.db.save_confluence_signal( + pair=pair, signal_type="PAIR_EXTREME", + confidence=confidence, z_score=z_score, + reason=f"Z={z_score:.2f}", + layer1_active=self.layer1_is_active, + ) + return (True, f"PAIR EXTREME: {pair} Z={z_score:.2f}", confidence, sl_tp) return (False, "No valid signals within macro boundary", 0.0, None) @@ -181,23 +161,7 @@ class ConfluenceFilter: return False, "Position still valid" def is_conflicting(self) -> bool: - """Check if any extreme Layer 2 signal crosses the macro boundary.""" - if not self.bias_matrix: - return False - - all_z = self.tech_analyzer.get_all_z_scores() - for pair, z_score in all_z.items(): - if abs(z_score) < config.Z_SCORE_THRESHOLD: - continue - base, quote = pair.split("_") - if z_score > 0: - short_dir = self.bias_matrix.get(base, {}).get("direction", "NEUTRAL") - long_dir = self.bias_matrix.get(quote, {}).get("direction", "NEUTRAL") - else: - short_dir = self.bias_matrix.get(quote, {}).get("direction", "NEUTRAL") - long_dir = self.bias_matrix.get(base, {}).get("direction", "NEUTRAL") - if short_dir == "STRONG" or long_dir == "WEAK": - return True + """Advisory-only check — always returns False (does not block signals).""" return False def get_confluence_report(self, current_prices: Dict[str, float] = None) -> Dict: @@ -250,50 +214,39 @@ class ConfluenceFilter: if mc: gap = self.matrix.get_divergence_gap() strength = min(abs(gap) / 4.0, 1.0) * 100 - short_ccy, long_ccy = mc.split("_", 1) - allowed, _ = self._check_boundary(short_ccy, long_ccy) - if allowed: - direction = 'SHORT' if strength > 50 else 'LONG' - entry = self.tech_analyzer.get_last_price(mc) or 0.0 - sl_tp = self.tech_analyzer.calculate_sl_tp(mc, "LONG" if direction == "LONG" else "SHORT", entry) - signals[mc] = { - 'pair': mc, - 'type': 'MATRIX_DIVERGENCE', - 'strength': strength, - 'reason': f"Matrix cross {mc} (spread: {gap:.2f}σ)", - 'direction': direction, - **sl_tp, - } + direction = 'SHORT' if strength > 50 else 'LONG' + entry = self.tech_analyzer.get_last_price(mc) or 0.0 + sl_tp = self.tech_analyzer.calculate_sl_tp(mc, "LONG" if direction == "LONG" else "SHORT", entry) + signals[mc] = { + 'pair': mc, + 'type': 'MATRIX_DIVERGENCE', + 'strength': strength, + 'reason': f"Matrix cross {mc} (spread: {gap:.2f}σ)", + 'direction': direction, + **sl_tp, + } for pair, z_score in sorted( self.tech_analyzer.get_all_z_scores().items(), key=lambda x: abs(x[1]), reverse=True ): - if abs(z_score) < config.Z_SCORE_THRESHOLD: + if abs(z_score) < config.SCALP_Z_SCORE_THRESHOLD: continue if pair in signals: continue - base, quote = pair.split("_") - if z_score > 0: - short_ccy, long_ccy = base, quote - else: - short_ccy, long_ccy = quote, base - - allowed, _ = self._check_boundary(short_ccy, long_ccy) - if allowed: - strength = min(abs(z_score) / 3.0, 1.0) * 100 - direction = 'SHORT' if z_score > 0 else 'LONG' - entry = self.tech_analyzer.get_last_price(pair) or 0.0 - sl_tp = self.tech_analyzer.calculate_sl_tp(pair, "LONG" if direction == "LONG" else "SHORT", entry) - signals[pair] = { - 'pair': pair, - 'type': 'PAIR_EXTREME', - 'strength': strength, - 'reason': f"{pair} Z={z_score:.2f} within macro boundary", - 'direction': direction, - **sl_tp, - } + strength = min(abs(z_score) / 3.0, 1.0) * 100 + direction = 'SHORT' if z_score > 0 else 'LONG' + entry = self.tech_analyzer.get_last_price(pair) or 0.0 + sl_tp = self.tech_analyzer.calculate_sl_tp(pair, "LONG" if direction == "LONG" else "SHORT", entry) + signals[pair] = { + 'pair': pair, + 'type': 'PAIR_EXTREME', + 'strength': strength, + 'reason': f"{pair} Z={z_score:.2f}", + 'direction': direction, + **sl_tp, + } return signals diff --git a/currency_strength_matrix.py b/currency_strength_matrix.py index 4aff531..27f2ebe 100644 --- a/currency_strength_matrix.py +++ b/currency_strength_matrix.py @@ -80,7 +80,7 @@ class CurrencyStrengthMatrix: def __init__(self, z_scores: Dict[str, float] = None): self.currencies = config.CURRENCIES - self.threshold = config.Z_SCORE_THRESHOLD + self.threshold = config.SCALP_Z_SCORE_THRESHOLD self._raw_scores: Dict[str, List[float]] = {} self._strengths: Dict[str, CurrencyStrength] = {} self.session_tracker = SessionTracker() diff --git a/layer2_technical.py b/layer2_technical.py index ac056d4..b9075a2 100644 --- a/layer2_technical.py +++ b/layer2_technical.py @@ -97,7 +97,7 @@ class TechnicalAnalyzer: current_price = prices[-1] z_score = (current_price - mu) / sigma self.z_scores[currency_pair] = z_score - self.extremes[currency_pair] = abs(z_score) >= config.Z_SCORE_THRESHOLD + self.extremes[currency_pair] = abs(z_score) >= config.SCALP_Z_SCORE_THRESHOLD def get_z_score(self, currency_pair: str) -> float: return self.z_scores.get(currency_pair, 0.0) @@ -106,10 +106,10 @@ class TechnicalAnalyzer: return self.extremes.get(currency_pair, False) def get_overbought_pairs(self) -> List[str]: - return [pair for pair, z in self.z_scores.items() if z >= config.Z_SCORE_THRESHOLD] + return [pair for pair, z in self.z_scores.items() if z >= config.SCALP_Z_SCORE_THRESHOLD] def get_oversold_pairs(self) -> List[str]: - return [pair for pair, z in self.z_scores.items() if z <= -config.Z_SCORE_THRESHOLD] + return [pair for pair, z in self.z_scores.items() if z <= -config.SCALP_Z_SCORE_THRESHOLD] def get_volatility(self, currency_pair: str) -> float: bars = list(self.bar_history[currency_pair]) @@ -191,7 +191,7 @@ class TechnicalAnalyzer: if ticks and sigma > 0: z = (ticks[-1] - mu) / sigma self.z_scores[pair] = z - self.extremes[pair] = abs(z) >= config.Z_SCORE_THRESHOLD + self.extremes[pair] = abs(z) >= config.SCALP_Z_SCORE_THRESHOLD def seed_ohlc(self, ohlc_data: Dict[str, List[Dict]]): """Seed both bar_history and ohlc_history from full candle data. @@ -213,7 +213,7 @@ class TechnicalAnalyzer: if ticks and sigma > 0: z = (ticks[-1] - mu) / sigma self.z_scores[pair] = z - self.extremes[pair] = abs(z) >= config.Z_SCORE_THRESHOLD + self.extremes[pair] = abs(z) >= config.SCALP_Z_SCORE_THRESHOLD def clear_history(self): for pair in self.bar_history: diff --git a/main_window.py b/main_window.py index c954748..c880694 100644 --- a/main_window.py +++ b/main_window.py @@ -20,11 +20,12 @@ Responsibilities: """ from PyQt5.QtWidgets import QMainWindow, QTabWidget, QMessageBox -from PyQt5.QtCore import QThread, pyqtSignal +from PyQt5.QtCore import QThread, pyqtSignal, QTimer from typing import Dict, Optional import config from database import Database from layer2_technical import TechnicalAnalyzer +from currency_strength_matrix import CurrencyStrengthMatrix from ui.dashboard_tab import DashboardTab from ui.entry_tab import MonthlyEntryTab from ui.layer2_monitor_tab import Layer2MonitorTab @@ -115,6 +116,7 @@ class MainWindow(QMainWindow): self._init_ui() self._connect_signals() self._setup_auto_fetch() + self._setup_live_signal_timer() def _init_ui(self): """Build the main window UI.""" @@ -124,8 +126,8 @@ class MainWindow(QMainWindow): # Tab widget tabs = QTabWidget() - # Tab 1: Dashboard (Layer 1) - self.dashboard_tab = DashboardTab(self.db) + # Tab 1: Dashboard (Live + Macro Backdrop) + self.dashboard_tab = DashboardTab(self.db, tech_analyzer=self.tech_analyzer) tabs.addTab(self.dashboard_tab, config.TAB_NAMES["dashboard"]) # Tab 2: Monthly Entry (Data input) @@ -172,6 +174,17 @@ class MainWindow(QMainWindow): print("[Main] Auto-fetch enabled, fetching rates on startup...") self._fetch_rates() + def _setup_live_signal_timer(self): + """Periodically refresh the live intraday signal on the dashboard.""" + self._live_signal_timer = QTimer() + self._live_signal_timer.timeout.connect(self._tick_live_signal) + self._live_signal_timer.start(3000) + + def _tick_live_signal(self): + """Refresh live signal on dashboard.""" + if self.dashboard_tab: + self.dashboard_tab.update_live_signal() + def _fetch_rates(self): """ Trigger background FRED rate fetch. diff --git a/scorer.py b/scorer.py index 82986e0..7337a85 100644 --- a/scorer.py +++ b/scorer.py @@ -253,17 +253,14 @@ def generate_signal(scores: Dict[str, Dict]) -> Tuple[str, str, str]: def build_directional_bias_matrix(scores: Dict[str, Dict]) -> Dict[str, Dict]: - """Build a permanent monthly directional bias matrix from Layer 1 scores. + """Build an advisory directional bias matrix from Layer 1 scores. - Rules: - - Top 2 scores → "STRONG" — currency must only be longed, never shorted - - Bottom 2 scores → "WEAK" — currency must only be shorted, never longed - - Middle 4 scores → "NEUTRAL" — no directional restriction + DISPLAY ONLY — does not gate or block any trade signal anywhere in the system. + Top 2 → "STRONG", Bottom 2 → "WEAK", Middle 4 → "NEUTRAL". Returns: { "USD": {"direction": "STRONG", "score": 85.2, "rank": 1}, - "EUR": {"direction": "NEUTRAL", "score": 55.0, "rank": 4}, "JPY": {"direction": "WEAK", "score": 22.1, "rank": 8}, ... } diff --git a/ui/confluence_tab.py b/ui/confluence_tab.py index e95cdcb..c35cc6d 100644 --- a/ui/confluence_tab.py +++ b/ui/confluence_tab.py @@ -1,16 +1,13 @@ """ APEX Confluence Signals Tab — Layer 1 + Layer 2 Merging -Displays: -- Current Layer 1 fundamental bias -- Layer 2 technical extremes -- Confluence signals (both aligned) -- Risk management details +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, QMessageBox, QProgressBar + QPushButton, QFrame, QHeaderView ) from PyQt5.QtCore import Qt, QTimer from PyQt5.QtGui import QColor, QFont @@ -18,13 +15,12 @@ from typing import Dict, Optional from datetime import datetime import config from layer2_technical import TechnicalAnalyzer -from confluence_filter import ConfluenceFilter, SignalHistory -from risk_management import RiskManagementSystem +from confluence_filter import ConfluenceFilter from database import Database class ConfluenceSignalsTab(QWidget): - """Confluence signals monitoring and execution.""" + """Confluence signals monitoring — display only, no execution.""" def __init__(self, db: Database, tech_analyzer: TechnicalAnalyzer): super().__init__() @@ -32,8 +28,6 @@ class ConfluenceSignalsTab(QWidget): self.db = db self.tech_analyzer = tech_analyzer self.confluence = ConfluenceFilter(tech_analyzer, db=db) - self.risk_mgmt = RiskManagementSystem(account_balance=config.ACCOUNT_BALANCE) - self.signal_history = SignalHistory() self._init_ui() self._setup_auto_refresh() @@ -61,23 +55,7 @@ class ConfluenceSignalsTab(QWidget): layout.addWidget(self.signals_table) - # ====== Risk Management Panel ====== - risk_layout = QHBoxLayout() - risk_layout.addWidget(QLabel("Portfolio Exposure:")) - - 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) - - # ====== Control Buttons ====== + # ====== Controls ====== button_layout = QHBoxLayout() refresh_btn = QPushButton("Refresh Signals") @@ -85,11 +63,6 @@ class ConfluenceSignalsTab(QWidget): 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) - button_layout.addStretch() layout.addLayout(button_layout) layout.addStretch() @@ -234,11 +207,6 @@ class ConfluenceSignalsTab(QWidget): self.confluence_status_label.setText("✕ NO CONFLUENCE") self.confluence_status_label.setStyleSheet("color: #e74c3c; font-weight: bold;") - 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)) - self.leverage_label.setText(f"Leverage: {portfolio['leverage_ratio']:.2f}x") - except Exception as e: print(f"[Confluence] Error refreshing: {e}") @@ -300,55 +268,6 @@ class ConfluenceSignalsTab(QWidget): row += 1 - def _execute_signal(self): - """Execute the top confluence signal.""" - signals = self.confluence.get_all_signals() - if not signals: - QMessageBox.warning(self, "No Signal", "No valid confluence signal to execute") - return - - try: - best = max(signals.values(), key=lambda s: s.get('strength', 0)) - pair = best['pair'] - strength = best['strength'] - - current_price = 1.0 - - trade = self.risk_mgmt.execute_signal( - pair, - strength, - current_price, - use_hedging=config.USE_GRID_HEDGING - ) - - if trade: - msg = ( - f"Trade Executed:\n" - f"Pair: {trade['pair']}\n" - f"Entry: {trade['entry_price']:.4f}\n" - f"Size: {trade['position_size']:.2f} lots\n" - f"Confidence: {trade['confluence_strength']:.0f}%" - ) - QMessageBox.information(self, "Trade Executed", msg) - - self.signal_history.add_signal({ - 'pair': pair, - 'type': best.get('type', 'SIGNAL'), - 'entry_price': current_price, - 'confluence_strength': strength, - }) - else: - QMessageBox.warning( - self, - "Execution Failed", - "Position size would exceed portfolio leverage limits" - ) - - self._refresh_signals() - - except Exception as e: - QMessageBox.critical(self, "Error", f"Execution failed: {e}") - def _setup_auto_refresh(self): """Setup automatic refresh timer.""" self.refresh_timer = QTimer() diff --git a/ui/dashboard_tab.py b/ui/dashboard_tab.py index 312279a..30e1be0 100644 --- a/ui/dashboard_tab.py +++ b/ui/dashboard_tab.py @@ -1,20 +1,11 @@ """ -APEX Layer 1 — Tab 1: Dashboard +APEX Dashboard — Tab 1: Primary View -This is the main screen the user sees every day. +Top section: LIVE SIGNAL (intraday, from CurrencyStrengthMatrix Z-scores) +Bottom section: MACRO BACKDROP (monthly, from scorer.py fundamental data) -Features: -- Signal card at top (shows PRIMARY SIGNAL, gap, status, updated date) -- Ranked score table below with all 8 currencies -- Strongest row highlighted GREEN (BUY) -- Weakest row highlighted RED (SELL) -- Score bar charts per row (visual progress) -- Auto-refresh when data updated from Entry tab or FRED API - -Display: -- Rank, Currency, Rate, CPI, PMI, Score columns -- Color-coded rows, "BUY" and "SELL" tags -- Last updated timestamp +The live signal is the primary trading reference. Macro Backdrop is slow-moving +context for display only. """ from PyQt5.QtWidgets import ( @@ -27,29 +18,34 @@ from typing import Dict, Optional from datetime import datetime import config from database import Database +from currency_strength_matrix import CurrencyStrengthMatrix +from layer2_technical import TechnicalAnalyzer import scorer class DashboardTab(QWidget): - """Main dashboard showing current signal and currency rankings.""" + """Dashboard: live intraday signal (CurrencyStrengthMatrix) + macro backdrop (scorer).""" # Signal to request FRED fetch fetch_rates_requested = pyqtSignal() # Signal emitted when new signal generated (for Layer 2 confluence) - # Emits: strongest, weakest, gap, directional_bias_matrix signal_generated = pyqtSignal(str, str, float, dict) - def __init__(self, db: Database): + def __init__(self, db: Database, tech_analyzer: TechnicalAnalyzer = None): """ Initialize Dashboard tab. Args: db: Database instance + tech_analyzer: TechnicalAnalyzer instance for live signal data """ super().__init__() self.db = db + self.tech_analyzer = tech_analyzer or TechnicalAnalyzer() + self.matrix = CurrencyStrengthMatrix() self.current_month = datetime.now().strftime("%Y-%m") + self._last_matrix_report = None self._init_ui() self._refresh_display() @@ -59,12 +55,16 @@ class DashboardTab(QWidget): layout = QVBoxLayout() layout.setSpacing(12) - # ====== Signal Card ====== - signal_card = self._build_signal_card() - layout.addWidget(signal_card) + # ====== Live Signal Card (intraday, from CurrencyStrengthMatrix) ====== + live_card = self._build_live_signal_card() + layout.addWidget(live_card) - # ====== Ranked Score Table ====== - heading = QLabel("Currency Rankings") + # ====== Macro Backdrop Card (monthly, from scorer.py) ====== + backdrop_card = self._build_macro_backdrop_card() + layout.addWidget(backdrop_card) + + # ====== Ranked Score Table (Macro Backdrop detail) ====== + heading = QLabel("Macro Backdrop — Currency Rankings") heading.setProperty("heading", True) layout.addWidget(heading) @@ -119,36 +119,77 @@ class DashboardTab(QWidget): self.setLayout(layout) - def _build_signal_card(self) -> QFrame: - """Build the signal card frame.""" + def _build_live_signal_card(self) -> QFrame: + """Build the live intraday signal card (from CurrencyStrengthMatrix).""" card = QFrame() card.setObjectName("statusCard") + + layout = QVBoxLayout() + layout.setSpacing(6) + + title = QLabel("LIVE SIGNAL (Intraday)") + title.setProperty("subheading", True) + layout.addWidget(title) + + # Matrix Cross pair (largest divergence) + self.live_signal_label = QLabel("Waiting for Layer 2 data...") + self.live_signal_label.setProperty("value", True) + self.live_signal_label.setStyleSheet("color: #2c3e50;") + layout.addWidget(self.live_signal_label) + + # Divergence gap + self.live_gap_label = QLabel("Divergence: — σ") + self.live_gap_label.setStyleSheet("font-size: 15px; color: #5d6d7e;") + layout.addWidget(self.live_gap_label) + + # Matrix ranked currencies (top/bottom 2) + self.live_ranked_label = QLabel("") + self.live_ranked_label.setStyleSheet("font-size: 13px; color: #7f8c8d;") + layout.addWidget(self.live_ranked_label) + + # Session + SRV + self.live_session_label = QLabel("Session: — | SRV: —") + self.live_session_label.setStyleSheet("font-size: 12px; color: #95a5a6;") + layout.addWidget(self.live_session_label) + + # Entry zones (SL/TP text-described, not executable) + self.live_entry_zones = QLabel("") + self.live_entry_zones.setStyleSheet("font-size: 12px; color: #8e44ad;") + layout.addWidget(self.live_entry_zones) + + # Updated timestamp + self.live_updated_label = QLabel("Updated: —") + self.live_updated_label.setStyleSheet("color: #95a5a6; font-size: 12px;") + layout.addWidget(self.live_updated_label) + + layout.addStretch() + card.setLayout(layout) + return card + + def _build_macro_backdrop_card(self) -> QFrame: + """Build the macro backdrop card frame (from scorer.py fundamental data).""" + card = QFrame() + card.setObjectName("card") layout = QVBoxLayout() layout.setSpacing(6) - # Title - title = QLabel("PRIMARY SIGNAL") + title = QLabel("MACRO BACKDROP (Fundamental — slow context)") title.setProperty("subheading", True) layout.addWidget(title) - # Signal text (large, bold) - self.signal_label = QLabel("NO TRADE — Initializing...") - self.signal_label.setProperty("value", True) - self.signal_label.setStyleSheet("color: #2c3e50;") - layout.addWidget(self.signal_label) + self.macro_signal_label = QLabel("NO TRADE — Initializing...") + self.macro_signal_label.setStyleSheet("font-size: 18px; font-weight: 600; color: #2c3e50;") + layout.addWidget(self.macro_signal_label) - # Gap and status - self.gap_label = QLabel("Gap: — points") - self.gap_label.setStyleSheet("font-size: 15px; color: #5d6d7e;") - layout.addWidget(self.gap_label) + self.macro_gap_label = QLabel("Gap: — points") + self.macro_gap_label.setStyleSheet("font-size: 13px; color: #5d6d7e;") + layout.addWidget(self.macro_gap_label) - # Updated timestamp - self.updated_label = QLabel("Updated: —") - self.updated_label.setStyleSheet("color: #95a5a6; font-size: 12px;") - layout.addWidget(self.updated_label) + self.macro_updated_label = QLabel("Updated: —") + self.macro_updated_label.setStyleSheet("color: #95a5a6; font-size: 12px;") + layout.addWidget(self.macro_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;" @@ -161,9 +202,8 @@ class DashboardTab(QWidget): return card def _refresh_display(self): - """Refresh dashboard with latest data.""" + """Refresh macro backdrop with latest fundamental data.""" try: - # Get signal for current month signal_data = self.db.get_signal(self.current_month) if signal_data: @@ -173,7 +213,6 @@ class DashboardTab(QWidget): strongest = signal_data.get("strongest") weakest = signal_data.get("weakest") - # Build directional bias matrix and emit to confluence tab if strongest and weakest: scores = self.db.get_month_scores(self.current_month) if scores: @@ -182,13 +221,10 @@ class DashboardTab(QWidget): bias_matrix = {} self.signal_generated.emit(strongest, weakest, gap, bias_matrix) - self.signal_label.setText(signal_text) - if status == "ACTIVE": - self.signal_label.setStyleSheet("color: #27ae60;") - else: - self.signal_label.setStyleSheet("color: #e74c3c;") + self.macro_signal_label.setText(signal_text) + color = "#27ae60" if status == "ACTIVE" else "#e74c3c" + self.macro_signal_label.setStyleSheet(f"font-size: 18px; font-weight: 600; color: {color};") - # Update gap label gap_tier = scorer.get_gap_tier(gap) tier_name = { "no_trade": "Too narrow", @@ -197,26 +233,112 @@ class DashboardTab(QWidget): "strong": "Strong signal" }.get(gap_tier, "Unknown") - self.gap_label.setText(f"Gap: {gap:.1f} points · {tier_name}") + self.macro_gap_label.setText(f"Gap: {gap:.1f} points · {tier_name}") else: - self.signal_label.setText("NO TRADE — No data yet") - self.signal_label.setStyleSheet("color: #e74c3c;") - self.gap_label.setText("Gap: — points") + self.macro_signal_label.setText("NO TRADE — No data yet") + self.macro_signal_label.setStyleSheet("font-size: 18px; font-weight: 600; color: #e74c3c;") + self.macro_gap_label.setText("Gap: — points") - # Update timestamp - self.updated_label.setText(f"Updated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + self.macro_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() except Exception as e: print(f"[ERROR] Failed to refresh dashboard: {e}") - self.signal_label.setText("ERROR") - self.signal_label.setStyleSheet("color: #e74c3c;") + self.macro_signal_label.setText("ERROR") + self.macro_signal_label.setStyleSheet("font-size: 18px; font-weight: 600; color: #e74c3c;") + def _refresh_live_signal(self): + """Refresh the live intraday signal from CurrencyStrengthMatrix.""" + try: + z_scores = self.tech_analyzer.get_all_z_scores() + if not z_scores: + self.live_signal_label.setText("Waiting for Layer 2 data...") + self.live_signal_label.setStyleSheet("font-size: 28px; font-weight: 700; color: #95a5a6;") + self.live_gap_label.setText("Divergence: — σ") + self.live_ranked_label.setText("") + self.live_session_label.setText("Session: — | SRV: —") + self.live_entry_zones.setText("") + return + + current_prices = {} + for pair in z_scores: + lp = self.tech_analyzer.get_last_price(pair) + if lp is not None: + current_prices[pair] = lp + + self.matrix.update(z_scores, current_prices=current_prices) + report = self.matrix.get_report() + self._last_matrix_report = report + + mc = report.get("matrix_cross") + gap = report.get("divergence_gap", 0) + has_div = report.get("has_divergence", False) + ranked = report.get("ranked", []) + + if mc and mc != "N/A": + signal_text = mc + color = "#e74c3c" if has_div else "#2c3e50" + self.live_signal_label.setText(signal_text) + self.live_signal_label.setStyleSheet(f"font-size: 28px; font-weight: 700; color: {color};") + self.live_gap_label.setText(f"Divergence: {gap:.2f}σ{' — EXTREME' if has_div else ''}") + else: + self.live_signal_label.setText("No divergence") + self.live_signal_label.setStyleSheet("font-size: 28px; font-weight: 700; color: #95a5a6;") + self.live_gap_label.setText("Divergence: — σ") + + if ranked: + top2 = [f"{c[0]}({c[1]:+.1f}σ)" for c in ranked[:2]] + bot2 = [f"{c[0]}({c[1]:+.1f}σ)" for c in ranked[-2:]] + self.live_ranked_label.setText( + f"Strongest: {' '.join(top2)} | Weakest: {' '.join(bot2)}" + ) + else: + self.live_ranked_label.setText("") + + session = report.get("active_session", "—") + srv_data = self.matrix.get_srv_map() + srv_parts = [] + if srv_data: + for ccy in ranked[:3]: + name = ccy[0] + if name in srv_data: + s = srv_data[name] + sign = "+" if s >= 0 else "" + srv_parts.append(f"{name}: {sign}{s:.3f}%") + srv_str = " | ".join(srv_parts) + self.live_session_label.setText( + f"Session: {session} | SRV: {srv_str}" if srv_str + else f"Session: {session} | SRV: —" + ) + + # Text-described entry zones (advisory only, no position sizing) + entry_parts = [] + if mc and mc != "N/A": + entry_price = self.tech_analyzer.get_last_price(mc) + if entry_price: + sl_tp = self.tech_analyzer.calculate_sl_tp( + mc, "LONG" if has_div else "SHORT", entry_price + ) + if sl_tp.get("sl") and sl_tp.get("tp"): + entry_parts.append( + f"Entry zones — {mc}: ~{entry_price:.5f} " + f"(SL: {sl_tp['sl']:.5f}, TP: {sl_tp['tp']:.5f})" + ) + self.live_entry_zones.setText(" | ".join(entry_parts)) + + self.live_updated_label.setText( + f"Updated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + ) + + except Exception as e: + print(f"[Dashboard] Live signal error: {e}") + + def update_live_signal(self): + """Called externally to trigger live signal refresh.""" + self._refresh_live_signal() + def _check_data_staleness(self): """Show a warning if CPI/PMI data is older than 35 days.""" try: diff --git a/ui/layer2_monitor_tab.py b/ui/layer2_monitor_tab.py index 38c2962..8dbf806 100644 --- a/ui/layer2_monitor_tab.py +++ b/ui/layer2_monitor_tab.py @@ -407,7 +407,7 @@ class Layer2MonitorTab(QWidget): z_item.setFlags(z_item.flags() & ~Qt.ItemIsEditable) z_item.setTextAlignment(Qt.AlignCenter) - if abs(z_score) >= config.Z_SCORE_THRESHOLD: + if abs(z_score) >= config.SCALP_Z_SCORE_THRESHOLD: z_item.setBackground(QColor("#ffebee")) z_item.setForeground(QColor("#c62828")) @@ -525,7 +525,7 @@ class Layer2MonitorTab(QWidget): z_item.setTextAlignment(Qt.AlignCenter) # High-contrast σ formatting (Task 4.1) - threshold = config.Z_SCORE_THRESHOLD + threshold = config.SCALP_Z_SCORE_THRESHOLD if z_val >= threshold: z_item.setBackground(QColor("#c62828")) z_item.setForeground(QColor("white"))