mirror of
https://github.com/Sabermrddz/QuantCore-FX.git
synced 2026-08-19 05:38:07 +00:00
layer one v5
This commit is contained in:
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
Reference in New Issue
Block a user