layer one v5

This commit is contained in:
saber
2026-06-18 10:16:51 +01:00
parent 5d4ffb1da5
commit 08f047adb5
9 changed files with 299 additions and 283 deletions
+20 -8
View File
@@ -212,16 +212,18 @@ CURRENCY_EMOJIS = {
MT5_SYMBOL_SUFFIX = os.getenv("MT5_SYMBOL_SUFFIX", "") MT5_SYMBOL_SUFFIX = os.getenv("MT5_SYMBOL_SUFFIX", "")
# Technical Analysis Settings # 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 = { TIMEFRAMES = {
"M5": {"interval": "5min", "bars": 288, "label": "5 min"}, "M5": {"interval": "5min", "bars": 48, "label": "5 min"},
"M15": {"interval": "15min", "bars": 96, "label": "15 min"}, "M15": {"interval": "15min", "bars": 16, "label": "15 min"},
"H1": {"interval": "1h", "bars": 48, "label": "1 hour"}, "H1": {"interval": "1h", "bars": 12, "label": "1 hour"},
"H4": {"interval": "4h", "bars": 24, "label": "4 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) # Historical bar config (backward compat)
BAR_TIMEFRAME = os.getenv("BAR_TIMEFRAME", "M5") 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_OPEN = 13 # 13:00 UTC
SESSION_NEWYORK_CLOSE = 21# 21: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" CONFLUENCE_ENABLED = os.getenv("CONFLUENCE_ENABLED", "true").lower() == "true"
MIN_CONFLUENCE_STRENGTH = float(os.getenv("MIN_CONFLUENCE_STRENGTH", 60.0)) # 60% confidence threshold MIN_CONFLUENCE_STRENGTH = float(os.getenv("MIN_CONFLUENCE_STRENGTH", 60.0)) # 60% confidence threshold
+64 -111
View File
@@ -72,13 +72,10 @@ class ConfluenceFilter:
print(f"[Confluence] Monthly bias matrix set: {directions}") print(f"[Confluence] Monthly bias matrix set: {directions}")
def _check_boundary(self, short_ccy: str, long_ccy: str) -> Tuple[bool, str]: 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. Returns (True, reason) always. The bias matrix is displayed for
Boundary rules: context in the UI but never gates/ vetoes a trade signal.
- STRONG currencies cannot be shorted
- WEAK currencies cannot be longed
- NEUTRAL currencies have no restriction
Returns: Returns:
(allowed: bool, reason: str) (allowed: bool, reason: str)
@@ -89,17 +86,13 @@ class ConfluenceFilter:
short_dir = self.bias_matrix.get(short_ccy, {}).get("direction", "NEUTRAL") short_dir = self.bias_matrix.get(short_ccy, {}).get("direction", "NEUTRAL")
long_dir = self.bias_matrix.get(long_ccy, {}).get("direction", "NEUTRAL") long_dir = self.bias_matrix.get(long_ccy, {}).get("direction", "NEUTRAL")
notes = []
if short_dir == "STRONG": if short_dir == "STRONG":
return ( notes.append(f"{short_ccy}=STRONG (advisory)")
False,
f"Cannot short {short_ccy}: classified STRONG by Layer 1 macro bias"
)
if long_dir == "WEAK": if long_dir == "WEAK":
return ( notes.append(f"{long_ccy}=WEAK (advisory)")
False, advisory = f"Within macro boundary — {' | '.join(notes) if notes else 'neutral'}"
f"Cannot long {long_ccy}: classified WEAK by Layer 1 macro bias" return True, advisory
)
return True, "Within macro boundary"
def check_entry_confluence(self, current_prices: Dict[str, float] = None def check_entry_confluence(self, current_prices: Dict[str, float] = None
) -> Tuple[bool, str, float, Optional[Dict]]: ) -> Tuple[bool, str, float, Optional[Dict]]:
@@ -109,58 +102,45 @@ class ConfluenceFilter:
mc = self.matrix.get_matrix_cross() mc = self.matrix.get_matrix_cross()
gap = self.matrix.get_divergence_gap() 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) short_ccy, long_ccy = mc.split("_", 1)
allowed, reason = self._check_boundary(short_ccy, long_ccy) _, advisory = self._check_boundary(short_ccy, long_ccy)
if allowed: confidence = min(abs(gap) / 4.0, 1.0) * 100
confidence = min(abs(gap) / 4.0, 1.0) * 100 self.confluence_strength = confidence
self.confluence_strength = confidence self.last_confluence_check = datetime.now()
self.last_confluence_check = datetime.now() direction = "SHORT" if confidence > 50 else "LONG"
direction = "SHORT" if confidence > 50 else "LONG" entry = self.tech_analyzer.get_last_price(mc) or 0.0
entry = self.tech_analyzer.get_last_price(mc) or 0.0 sl_tp = self.tech_analyzer.calculate_sl_tp(mc, direction, entry)
sl_tp = self.tech_analyzer.calculate_sl_tp(mc, direction, entry) if self.db:
if self.db: self.db.save_confluence_signal(
self.db.save_confluence_signal( pair=mc, signal_type="MATRIX_DIVERGENCE",
pair=mc, signal_type="MATRIX_DIVERGENCE", confidence=confidence, z_score=None, gap=gap,
confidence=confidence, z_score=None, gap=gap, reason=f"Matrix cross {mc} gap={gap:.1f}σ | {advisory}",
reason=f"Matrix cross {mc} gap={gap:.1f}σ", layer1_active=self.layer1_is_active,
layer1_active=self.layer1_is_active, )
) return (True, f"MATRIX DIVERGENCE: {mc} (Gap: {gap:.1f}σ)", confidence, sl_tp)
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)
all_z = self.tech_analyzer.get_all_z_scores() all_z = self.tech_analyzer.get_all_z_scores()
sorted_pairs = sorted(all_z.items(), key=lambda x: abs(x[1]), reverse=True) sorted_pairs = sorted(all_z.items(), key=lambda x: abs(x[1]), reverse=True)
for pair, z_score in sorted_pairs: 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 continue
base, quote = pair.split("_") confidence = min(abs(z_score) / 3.0, 1.0) * 100
if z_score > 0: self.confluence_strength = confidence
short_ccy, long_ccy = base, quote self.last_confluence_check = datetime.now()
else: direction = "SHORT" if z_score > 0 else "LONG"
short_ccy, long_ccy = quote, base entry = self.tech_analyzer.get_last_price(pair) or 0.0
sl_tp = self.tech_analyzer.calculate_sl_tp(pair, direction, entry)
allowed, reason = self._check_boundary(short_ccy, long_ccy) if self.db:
if allowed: self.db.save_confluence_signal(
confidence = min(abs(z_score) / 3.0, 1.0) * 100 pair=pair, signal_type="PAIR_EXTREME",
self.confluence_strength = confidence confidence=confidence, z_score=z_score,
self.last_confluence_check = datetime.now() reason=f"Z={z_score:.2f}",
direction = "SHORT" if z_score > 0 else "LONG" layer1_active=self.layer1_is_active,
entry = self.tech_analyzer.get_last_price(pair) or 0.0 )
sl_tp = self.tech_analyzer.calculate_sl_tp(pair, direction, entry) return (True, f"PAIR EXTREME: {pair} Z={z_score:.2f}", confidence, sl_tp)
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)
return (False, "No valid signals within macro boundary", 0.0, None) return (False, "No valid signals within macro boundary", 0.0, None)
@@ -181,23 +161,7 @@ class ConfluenceFilter:
return False, "Position still valid" return False, "Position still valid"
def is_conflicting(self) -> bool: def is_conflicting(self) -> bool:
"""Check if any extreme Layer 2 signal crosses the macro boundary.""" """Advisory-only check — always returns False (does not block signals)."""
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
return False return False
def get_confluence_report(self, current_prices: Dict[str, float] = None) -> Dict: def get_confluence_report(self, current_prices: Dict[str, float] = None) -> Dict:
@@ -250,50 +214,39 @@ class ConfluenceFilter:
if mc: if mc:
gap = self.matrix.get_divergence_gap() gap = self.matrix.get_divergence_gap()
strength = min(abs(gap) / 4.0, 1.0) * 100 strength = min(abs(gap) / 4.0, 1.0) * 100
short_ccy, long_ccy = mc.split("_", 1) direction = 'SHORT' if strength > 50 else 'LONG'
allowed, _ = self._check_boundary(short_ccy, long_ccy) entry = self.tech_analyzer.get_last_price(mc) or 0.0
if allowed: sl_tp = self.tech_analyzer.calculate_sl_tp(mc, "LONG" if direction == "LONG" else "SHORT", entry)
direction = 'SHORT' if strength > 50 else 'LONG' signals[mc] = {
entry = self.tech_analyzer.get_last_price(mc) or 0.0 'pair': mc,
sl_tp = self.tech_analyzer.calculate_sl_tp(mc, "LONG" if direction == "LONG" else "SHORT", entry) 'type': 'MATRIX_DIVERGENCE',
signals[mc] = { 'strength': strength,
'pair': mc, 'reason': f"Matrix cross {mc} (spread: {gap:.2f}σ)",
'type': 'MATRIX_DIVERGENCE', 'direction': direction,
'strength': strength, **sl_tp,
'reason': f"Matrix cross {mc} (spread: {gap:.2f}σ)", }
'direction': direction,
**sl_tp,
}
for pair, z_score in sorted( for pair, z_score in sorted(
self.tech_analyzer.get_all_z_scores().items(), self.tech_analyzer.get_all_z_scores().items(),
key=lambda x: abs(x[1]), reverse=True 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 continue
if pair in signals: if pair in signals:
continue continue
base, quote = pair.split("_") strength = min(abs(z_score) / 3.0, 1.0) * 100
if z_score > 0: direction = 'SHORT' if z_score > 0 else 'LONG'
short_ccy, long_ccy = base, quote entry = self.tech_analyzer.get_last_price(pair) or 0.0
else: sl_tp = self.tech_analyzer.calculate_sl_tp(pair, "LONG" if direction == "LONG" else "SHORT", entry)
short_ccy, long_ccy = quote, base signals[pair] = {
'pair': pair,
allowed, _ = self._check_boundary(short_ccy, long_ccy) 'type': 'PAIR_EXTREME',
if allowed: 'strength': strength,
strength = min(abs(z_score) / 3.0, 1.0) * 100 'reason': f"{pair} Z={z_score:.2f}",
direction = 'SHORT' if z_score > 0 else 'LONG' 'direction': direction,
entry = self.tech_analyzer.get_last_price(pair) or 0.0 **sl_tp,
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,
}
return signals return signals
+1 -1
View File
@@ -80,7 +80,7 @@ class CurrencyStrengthMatrix:
def __init__(self, z_scores: Dict[str, float] = None): def __init__(self, z_scores: Dict[str, float] = None):
self.currencies = config.CURRENCIES 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._raw_scores: Dict[str, List[float]] = {}
self._strengths: Dict[str, CurrencyStrength] = {} self._strengths: Dict[str, CurrencyStrength] = {}
self.session_tracker = SessionTracker() self.session_tracker = SessionTracker()
+5 -5
View File
@@ -97,7 +97,7 @@ class TechnicalAnalyzer:
current_price = prices[-1] current_price = prices[-1]
z_score = (current_price - mu) / sigma z_score = (current_price - mu) / sigma
self.z_scores[currency_pair] = z_score 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: def get_z_score(self, currency_pair: str) -> float:
return self.z_scores.get(currency_pair, 0.0) return self.z_scores.get(currency_pair, 0.0)
@@ -106,10 +106,10 @@ class TechnicalAnalyzer:
return self.extremes.get(currency_pair, False) return self.extremes.get(currency_pair, False)
def get_overbought_pairs(self) -> List[str]: 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]: 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: def get_volatility(self, currency_pair: str) -> float:
bars = list(self.bar_history[currency_pair]) bars = list(self.bar_history[currency_pair])
@@ -191,7 +191,7 @@ class TechnicalAnalyzer:
if ticks and sigma > 0: if ticks and sigma > 0:
z = (ticks[-1] - mu) / sigma z = (ticks[-1] - mu) / sigma
self.z_scores[pair] = z 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]]): def seed_ohlc(self, ohlc_data: Dict[str, List[Dict]]):
"""Seed both bar_history and ohlc_history from full candle data. """Seed both bar_history and ohlc_history from full candle data.
@@ -213,7 +213,7 @@ class TechnicalAnalyzer:
if ticks and sigma > 0: if ticks and sigma > 0:
z = (ticks[-1] - mu) / sigma z = (ticks[-1] - mu) / sigma
self.z_scores[pair] = z 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): def clear_history(self):
for pair in self.bar_history: for pair in self.bar_history:
+16 -3
View File
@@ -20,11 +20,12 @@ Responsibilities:
""" """
from PyQt5.QtWidgets import QMainWindow, QTabWidget, QMessageBox 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 from typing import Dict, Optional
import config import config
from database import Database from database import Database
from layer2_technical import TechnicalAnalyzer from layer2_technical import TechnicalAnalyzer
from currency_strength_matrix import CurrencyStrengthMatrix
from ui.dashboard_tab import DashboardTab from ui.dashboard_tab import DashboardTab
from ui.entry_tab import MonthlyEntryTab from ui.entry_tab import MonthlyEntryTab
from ui.layer2_monitor_tab import Layer2MonitorTab from ui.layer2_monitor_tab import Layer2MonitorTab
@@ -115,6 +116,7 @@ class MainWindow(QMainWindow):
self._init_ui() self._init_ui()
self._connect_signals() self._connect_signals()
self._setup_auto_fetch() self._setup_auto_fetch()
self._setup_live_signal_timer()
def _init_ui(self): def _init_ui(self):
"""Build the main window UI.""" """Build the main window UI."""
@@ -124,8 +126,8 @@ class MainWindow(QMainWindow):
# Tab widget # Tab widget
tabs = QTabWidget() tabs = QTabWidget()
# Tab 1: Dashboard (Layer 1) # Tab 1: Dashboard (Live + Macro Backdrop)
self.dashboard_tab = DashboardTab(self.db) self.dashboard_tab = DashboardTab(self.db, tech_analyzer=self.tech_analyzer)
tabs.addTab(self.dashboard_tab, config.TAB_NAMES["dashboard"]) tabs.addTab(self.dashboard_tab, config.TAB_NAMES["dashboard"])
# Tab 2: Monthly Entry (Data input) # Tab 2: Monthly Entry (Data input)
@@ -172,6 +174,17 @@ class MainWindow(QMainWindow):
print("[Main] Auto-fetch enabled, fetching rates on startup...") print("[Main] Auto-fetch enabled, fetching rates on startup...")
self._fetch_rates() 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): def _fetch_rates(self):
""" """
Trigger background FRED rate fetch. Trigger background FRED rate fetch.
+3 -6
View File
@@ -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]: 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: DISPLAY ONLY does not gate or block any trade signal anywhere in the system.
- Top 2 scores "STRONG" currency must only be longed, never shorted Top 2 "STRONG", Bottom 2 "WEAK", Middle 4 "NEUTRAL".
- Bottom 2 scores "WEAK" currency must only be shorted, never longed
- Middle 4 scores "NEUTRAL" no directional restriction
Returns: Returns:
{ {
"USD": {"direction": "STRONG", "score": 85.2, "rank": 1}, "USD": {"direction": "STRONG", "score": 85.2, "rank": 1},
"EUR": {"direction": "NEUTRAL", "score": 55.0, "rank": 4},
"JPY": {"direction": "WEAK", "score": 22.1, "rank": 8}, "JPY": {"direction": "WEAK", "score": 22.1, "rank": 8},
... ...
} }
+6 -87
View File
@@ -1,16 +1,13 @@
""" """
APEX Confluence Signals Tab Layer 1 + Layer 2 Merging APEX Confluence Signals Tab Layer 1 + Layer 2 Merging
Displays: DISPLAY ONLY no position sizing, no auto-execution, no hedging.
- Current Layer 1 fundamental bias Shows pair, direction, strength/gap, confluence agreement, and text-described zones.
- Layer 2 technical extremes
- Confluence signals (both aligned)
- Risk management details
""" """
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QTableWidget, QTableWidgetItem, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QTableWidget, QTableWidgetItem,
QPushButton, QFrame, QMessageBox, QProgressBar QPushButton, QFrame, QHeaderView
) )
from PyQt5.QtCore import Qt, QTimer from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtGui import QColor, QFont from PyQt5.QtGui import QColor, QFont
@@ -18,13 +15,12 @@ from typing import Dict, Optional
from datetime import datetime from datetime import datetime
import config import config
from layer2_technical import TechnicalAnalyzer from layer2_technical import TechnicalAnalyzer
from confluence_filter import ConfluenceFilter, SignalHistory from confluence_filter import ConfluenceFilter
from risk_management import RiskManagementSystem
from database import Database from database import Database
class ConfluenceSignalsTab(QWidget): class ConfluenceSignalsTab(QWidget):
"""Confluence signals monitoring and execution.""" """Confluence signals monitoring — display only, no execution."""
def __init__(self, db: Database, tech_analyzer: TechnicalAnalyzer): def __init__(self, db: Database, tech_analyzer: TechnicalAnalyzer):
super().__init__() super().__init__()
@@ -32,8 +28,6 @@ class ConfluenceSignalsTab(QWidget):
self.db = db self.db = db
self.tech_analyzer = tech_analyzer self.tech_analyzer = tech_analyzer
self.confluence = ConfluenceFilter(tech_analyzer, db=db) self.confluence = ConfluenceFilter(tech_analyzer, db=db)
self.risk_mgmt = RiskManagementSystem(account_balance=config.ACCOUNT_BALANCE)
self.signal_history = SignalHistory()
self._init_ui() self._init_ui()
self._setup_auto_refresh() self._setup_auto_refresh()
@@ -61,23 +55,7 @@ class ConfluenceSignalsTab(QWidget):
layout.addWidget(self.signals_table) layout.addWidget(self.signals_table)
# ====== Risk Management Panel ====== # ====== Controls ======
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 ======
button_layout = QHBoxLayout() button_layout = QHBoxLayout()
refresh_btn = QPushButton("Refresh Signals") refresh_btn = QPushButton("Refresh Signals")
@@ -85,11 +63,6 @@ class ConfluenceSignalsTab(QWidget):
refresh_btn.clicked.connect(self._refresh_signals) refresh_btn.clicked.connect(self._refresh_signals)
button_layout.addWidget(refresh_btn) 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() button_layout.addStretch()
layout.addLayout(button_layout) layout.addLayout(button_layout)
layout.addStretch() layout.addStretch()
@@ -234,11 +207,6 @@ class ConfluenceSignalsTab(QWidget):
self.confluence_status_label.setText("✕ NO CONFLUENCE") self.confluence_status_label.setText("✕ NO CONFLUENCE")
self.confluence_status_label.setStyleSheet("color: #e74c3c; font-weight: bold;") 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: except Exception as e:
print(f"[Confluence] Error refreshing: {e}") print(f"[Confluence] Error refreshing: {e}")
@@ -300,55 +268,6 @@ class ConfluenceSignalsTab(QWidget):
row += 1 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): def _setup_auto_refresh(self):
"""Setup automatic refresh timer.""" """Setup automatic refresh timer."""
self.refresh_timer = QTimer() self.refresh_timer = QTimer()
+182 -60
View File
@@ -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: The live signal is the primary trading reference. Macro Backdrop is slow-moving
- Signal card at top (shows PRIMARY SIGNAL, gap, status, updated date) context for display only.
- 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
""" """
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
@@ -27,29 +18,34 @@ from typing import Dict, Optional
from datetime import datetime from datetime import datetime
import config import config
from database import Database from database import Database
from currency_strength_matrix import CurrencyStrengthMatrix
from layer2_technical import TechnicalAnalyzer
import scorer import scorer
class DashboardTab(QWidget): class DashboardTab(QWidget):
"""Main dashboard showing current signal and currency rankings.""" """Dashboard: live intraday signal (CurrencyStrengthMatrix) + macro backdrop (scorer)."""
# Signal to request FRED fetch # Signal to request FRED fetch
fetch_rates_requested = pyqtSignal() fetch_rates_requested = pyqtSignal()
# Signal emitted when new signal generated (for Layer 2 confluence) # Signal emitted when new signal generated (for Layer 2 confluence)
# Emits: strongest, weakest, gap, directional_bias_matrix
signal_generated = pyqtSignal(str, str, float, dict) signal_generated = pyqtSignal(str, str, float, dict)
def __init__(self, db: Database): def __init__(self, db: Database, tech_analyzer: TechnicalAnalyzer = None):
""" """
Initialize Dashboard tab. Initialize Dashboard tab.
Args: Args:
db: Database instance db: Database instance
tech_analyzer: TechnicalAnalyzer instance for live signal data
""" """
super().__init__() super().__init__()
self.db = db self.db = db
self.tech_analyzer = tech_analyzer or TechnicalAnalyzer()
self.matrix = CurrencyStrengthMatrix()
self.current_month = datetime.now().strftime("%Y-%m") self.current_month = datetime.now().strftime("%Y-%m")
self._last_matrix_report = None
self._init_ui() self._init_ui()
self._refresh_display() self._refresh_display()
@@ -59,12 +55,16 @@ class DashboardTab(QWidget):
layout = QVBoxLayout() layout = QVBoxLayout()
layout.setSpacing(12) layout.setSpacing(12)
# ====== Signal Card ====== # ====== Live Signal Card (intraday, from CurrencyStrengthMatrix) ======
signal_card = self._build_signal_card() live_card = self._build_live_signal_card()
layout.addWidget(signal_card) layout.addWidget(live_card)
# ====== Ranked Score Table ====== # ====== Macro Backdrop Card (monthly, from scorer.py) ======
heading = QLabel("Currency Rankings") 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) heading.setProperty("heading", True)
layout.addWidget(heading) layout.addWidget(heading)
@@ -119,36 +119,77 @@ class DashboardTab(QWidget):
self.setLayout(layout) self.setLayout(layout)
def _build_signal_card(self) -> QFrame: def _build_live_signal_card(self) -> QFrame:
"""Build the signal card frame.""" """Build the live intraday signal card (from CurrencyStrengthMatrix)."""
card = QFrame() card = QFrame()
card.setObjectName("statusCard") 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 = QVBoxLayout()
layout.setSpacing(6) layout.setSpacing(6)
# Title title = QLabel("MACRO BACKDROP (Fundamental — slow context)")
title = QLabel("PRIMARY SIGNAL")
title.setProperty("subheading", True) title.setProperty("subheading", True)
layout.addWidget(title) layout.addWidget(title)
# Signal text (large, bold) self.macro_signal_label = QLabel("NO TRADE — Initializing...")
self.signal_label = QLabel("NO TRADE — Initializing...") self.macro_signal_label.setStyleSheet("font-size: 18px; font-weight: 600; color: #2c3e50;")
self.signal_label.setProperty("value", True) layout.addWidget(self.macro_signal_label)
self.signal_label.setStyleSheet("color: #2c3e50;")
layout.addWidget(self.signal_label)
# Gap and status self.macro_gap_label = QLabel("Gap: — points")
self.gap_label = QLabel("Gap: — points") self.macro_gap_label.setStyleSheet("font-size: 13px; color: #5d6d7e;")
self.gap_label.setStyleSheet("font-size: 15px; color: #5d6d7e;") layout.addWidget(self.macro_gap_label)
layout.addWidget(self.gap_label)
# Updated timestamp self.macro_updated_label = QLabel("Updated: —")
self.updated_label = QLabel("Updated: —") self.macro_updated_label.setStyleSheet("color: #95a5a6; font-size: 12px;")
self.updated_label.setStyleSheet("color: #95a5a6; font-size: 12px;") layout.addWidget(self.macro_updated_label)
layout.addWidget(self.updated_label)
# Staleness warning (hidden by default)
self.stale_warning = QLabel("") self.stale_warning = QLabel("")
self.stale_warning.setStyleSheet( self.stale_warning.setStyleSheet(
"color: #e74c3c; font-weight: 700; font-size: 13px; padding: 6px 0;" "color: #e74c3c; font-weight: 700; font-size: 13px; padding: 6px 0;"
@@ -161,9 +202,8 @@ class DashboardTab(QWidget):
return card return card
def _refresh_display(self): def _refresh_display(self):
"""Refresh dashboard with latest data.""" """Refresh macro backdrop with latest fundamental data."""
try: try:
# Get signal for current month
signal_data = self.db.get_signal(self.current_month) signal_data = self.db.get_signal(self.current_month)
if signal_data: if signal_data:
@@ -173,7 +213,6 @@ class DashboardTab(QWidget):
strongest = signal_data.get("strongest") strongest = signal_data.get("strongest")
weakest = signal_data.get("weakest") weakest = signal_data.get("weakest")
# Build directional bias matrix and emit to confluence tab
if strongest and weakest: if strongest and weakest:
scores = self.db.get_month_scores(self.current_month) scores = self.db.get_month_scores(self.current_month)
if scores: if scores:
@@ -182,13 +221,10 @@ class DashboardTab(QWidget):
bias_matrix = {} bias_matrix = {}
self.signal_generated.emit(strongest, weakest, gap, bias_matrix) self.signal_generated.emit(strongest, weakest, gap, bias_matrix)
self.signal_label.setText(signal_text) self.macro_signal_label.setText(signal_text)
if status == "ACTIVE": color = "#27ae60" if status == "ACTIVE" else "#e74c3c"
self.signal_label.setStyleSheet("color: #27ae60;") self.macro_signal_label.setStyleSheet(f"font-size: 18px; font-weight: 600; color: {color};")
else:
self.signal_label.setStyleSheet("color: #e74c3c;")
# Update gap label
gap_tier = scorer.get_gap_tier(gap) gap_tier = scorer.get_gap_tier(gap)
tier_name = { tier_name = {
"no_trade": "Too narrow", "no_trade": "Too narrow",
@@ -197,26 +233,112 @@ class DashboardTab(QWidget):
"strong": "Strong signal" "strong": "Strong signal"
}.get(gap_tier, "Unknown") }.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: else:
self.signal_label.setText("NO TRADE — No data yet") self.macro_signal_label.setText("NO TRADE — No data yet")
self.signal_label.setStyleSheet("color: #e74c3c;") self.macro_signal_label.setStyleSheet("font-size: 18px; font-weight: 600; color: #e74c3c;")
self.gap_label.setText("Gap: — points") self.macro_gap_label.setText("Gap: — points")
# Update timestamp self.macro_updated_label.setText(f"Updated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
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() self._check_data_staleness()
# Refresh score table
self._refresh_score_table() self._refresh_score_table()
except Exception as e: except Exception as e:
print(f"[ERROR] Failed to refresh dashboard: {e}") print(f"[ERROR] Failed to refresh dashboard: {e}")
self.signal_label.setText("ERROR") self.macro_signal_label.setText("ERROR")
self.signal_label.setStyleSheet("color: #e74c3c;") 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): def _check_data_staleness(self):
"""Show a warning if CPI/PMI data is older than 35 days.""" """Show a warning if CPI/PMI data is older than 35 days."""
try: try:
+2 -2
View File
@@ -407,7 +407,7 @@ class Layer2MonitorTab(QWidget):
z_item.setFlags(z_item.flags() & ~Qt.ItemIsEditable) z_item.setFlags(z_item.flags() & ~Qt.ItemIsEditable)
z_item.setTextAlignment(Qt.AlignCenter) 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.setBackground(QColor("#ffebee"))
z_item.setForeground(QColor("#c62828")) z_item.setForeground(QColor("#c62828"))
@@ -525,7 +525,7 @@ class Layer2MonitorTab(QWidget):
z_item.setTextAlignment(Qt.AlignCenter) z_item.setTextAlignment(Qt.AlignCenter)
# High-contrast σ formatting (Task 4.1) # High-contrast σ formatting (Task 4.1)
threshold = config.Z_SCORE_THRESHOLD threshold = config.SCALP_Z_SCORE_THRESHOLD
if z_val >= threshold: if z_val >= threshold:
z_item.setBackground(QColor("#c62828")) z_item.setBackground(QColor("#c62828"))
z_item.setForeground(QColor("white")) z_item.setForeground(QColor("white"))