layer one v4

This commit is contained in:
saber
2026-06-18 09:35:03 +01:00
parent e28f2db97c
commit 5d4ffb1da5
29 changed files with 1056 additions and 2067 deletions
+11
View File
@@ -29,6 +29,7 @@ DB_PATH=apex.db
# Debugging & Logging
# ============================================================================
DEBUG=true
LOG_FILE=apex.log
# ============================================================================
# Scoring Weights (must sum to 1.0)
@@ -84,6 +85,16 @@ MAX_PORTFOLIO_LEVERAGE=2.0
USE_GRID_HEDGING=true
GRID_LEVELS=3
# ============================================================================
# Live Execution (Phase 4 — OFF by default for safety)
# ============================================================================
# Set to true ONLY when connected to a funded MT5 demo/live account
LIVE_TRADING_ENABLED=false
# Max daily loss as fraction of account balance (5% default)
MAX_DAILY_LOSS_PCT=0.05
# Max total open exposure as fraction of account balance (20% default)
MAX_BASKET_EXPOSURE_PCT=0.20
# ============================================================================
# Mock Data Feeder Configuration (for testing without MT5)
# ============================================================================
+2
View File
@@ -36,6 +36,8 @@ venv.bak/
# Database files
*.db
*.db-shm
*.db-wal
*.sqlite
*.sqlite3
apex.db
-178
View File
@@ -1,178 +0,0 @@
# Excel Import Guide — APEX Layer 1
## **Quick Start**
1. **Ask AI for data** (use the prompt in [EXCEL_IMPORT_PROMPT.md](EXCEL_IMPORT_PROMPT.md))
2. **Download the Excel file**
3. Open APEX Layer 1 app → **Monthly Entry tab**
4. Click **"📊 Import Excel"** button
5. Select your file → Click **"Save & Calculate Scores"**
---
## **Supported File Formats**
### **Format 1: Multi-Sheet Excel** (Recommended)
**File:** `monthly_data.xlsx`
**Sheet 1: CPI**
```
Currency | Target % | Actual CPI %
---------|----------|-------------
USD | 2.0 | 3.2
EUR | 2.0 | 2.8
GBP | 2.0 | 3.1
JPY | 2.0 | 1.9
AUD | 2.5 | 3.5
CAD | 2.0 | 2.3
CHF | 1.5 | 1.2
NZD | 2.0 | 3.8
```
**Sheet 2: PMI**
```
Currency | Composite PMI
---------|---------------
USD | 52.3
EUR | 48.7
GBP | 51.2
JPY | 49.5
AUD | 50.1
CAD | 51.8
CHF | 49.2
NZD | 52.5
```
---
### **Format 2: Single-Sheet Excel**
**File:** `monthly_data.xlsx`
```
Currency | Target_CPI | Actual_CPI | Composite_PMI
---------|------------|------------|---------------
USD | 2.0 | 3.2 | 52.3
EUR | 2.0 | 2.8 | 48.7
GBP | 2.0 | 3.1 | 51.2
JPY | 2.0 | 1.9 | 49.5
AUD | 2.5 | 3.5 | 50.1
CAD | 2.0 | 2.3 | 51.8
CHF | 1.5 | 1.2 | 49.2
NZD | 2.0 | 3.8 | 52.5
```
---
### **Format 3: CSV File**
**File:** `monthly_data.csv`
```csv
Currency,Target_CPI,Actual_CPI,Composite_PMI
USD,2.0,3.2,52.3
EUR,2.0,2.8,48.7
GBP,2.0,3.1,51.2
JPY,2.0,1.9,49.5
AUD,2.5,3.5,50.1
CAD,2.0,2.3,51.8
CHF,1.5,1.2,49.2
NZD,2.0,3.8,52.5
```
---
## **Data Requirements**
### **All 8 Currencies Required (in any order):**
- USD, EUR, GBP, JPY, AUD, CAD, CHF, NZD
### **Value Ranges:**
- **CPI Actual:** Any realistic percentage (e.g., 1.0 - 5.0%)
- **PMI:** 0-100 scale (50 = neutral, >50 = expanding, <50 = contracting)
- **Use decimal format:** `3.45`, not `3.45%`
### **Important:**
- No merge cells or complex formatting
- Column headers needed (any name with "CPI", "PMI", "Currency" is recognized)
- Empty cells or 0 values = not imported
---
## **Generate Template Files**
Run this command to create example files:
```bash
python create_excel_template.py
```
This creates:
- `example_monthly_data.xlsx` (multi-sheet)
- `example_monthly_data_single_sheet.xlsx` (single sheet)
- `example_monthly_data.csv` (CSV format)
---
## **AI Prompt for Data Generation**
See [EXCEL_IMPORT_PROMPT.md](EXCEL_IMPORT_PROMPT.md) for ready-to-use prompt templates.
### **Quick Prompt:**
```
Generate realistic monthly economic data for the 8 major currencies
for June 2026 in Excel format:
CPI: Actual inflation rates (YoY %)
PMI: Composite PMI readings (0-100 scale, 50=neutral)
Currencies: USD, EUR, GBP, JPY, AUD, CAD, CHF, NZD
Provide in two sheets:
- Sheet 1: CPI (Currency, Target %, Actual CPI %)
- Sheet 2: PMI (Currency, Composite PMI)
```
---
## **Troubleshooting**
| Problem | Solution |
|---------|----------|
| "Import Error: Sheet not found" | Use correct sheet names: "CPI" and "PMI" |
| "No data imported" | Check column names contain "Currency", "CPI", "PMI" |
| "0 values not imported" | Use non-zero values; 0 = skip |
| "File locked" | Close Excel before importing |
| "Column mismatch" | Ensure 8 currencies (USD, EUR, GBP, JPY, AUD, CAD, CHF, NZD) |
---
## **Workflow Example**
1. **Ask AI:**
```
Create an Excel file with CPI and PMI data for the 8 major
currencies for June 2026. Make it realistic based on current
economic trends.
```
2. **Download** the Excel file from AI
3. **Open APEX Layer 1** → Monthly Entry tab
4. **Click Import Excel** → Select the file
5. **Data auto-fills** the entry form
6. **Click Save & Calculate Scores** → Done!
7. **Check Dashboard** tab for the generated signal
---
## **Notes**
- App auto-detects file format (Excel or CSV)
- If Excel has both formats, app tries multi-sheet first
- PMI default is 50 (neutral); enter actual PMI, not delta
- CPI target values are auto-looked up from config
- You can edit values after import before saving
-100
View File
@@ -1,100 +0,0 @@
# APEX Layer 1 — Excel Data Import Prompt
Use this prompt template to ask AI (ChatGPT, Claude, etc.) to generate monthly economic data in the required Excel format.
---
## **Example Prompt for AI:**
```
I need you to create an Excel file with monthly economic data for the 8 major currencies.
The file should have two sheets:
**Sheet 1: CPI**
- Column A: Currency (USD, EUR, GBP, JPY, AUD, CAD, CHF, NZD)
- Column B: Target % (2.0, 2.0, 2.0, 2.0, 2.5, 2.0, 1.5, 2.0)
- Column C: Actual CPI % (provide realistic values for June 2026)
**Sheet 2: PMI**
- Column A: Currency (USD, EUR, GBP, JPY, AUD, CAD, CHF, NZD)
- Column B: Composite PMI (provide realistic values between 40-60, where 50=neutral)
Format:
- Use decimal values (e.g., 3.45, not "3.45%")
- Include header row
- One row per currency
- No merge cells or formulas
Provide realistic economic data for June 2026 based on:
- Recent inflation trends
- Manufacturing activity
- Monetary policy directions
Please generate this as a downloadable Excel file or CSV format.
```
---
## **Required Data Format:**
### **CPI Sheet:**
| Currency | Target % | Actual CPI % |
|----------|----------|-------------|
| USD | 2.0 | 3.2 |
| EUR | 2.0 | 2.8 |
| GBP | 2.0 | 3.1 |
| JPY | 2.0 | 1.9 |
| AUD | 2.5 | 3.5 |
| CAD | 2.0 | 2.3 |
| CHF | 1.5 | 1.2 |
| NZD | 2.0 | 3.8 |
### **PMI Sheet:**
| Currency | Composite PMI |
|----------|---------------|
| USD | 52.3 |
| EUR | 48.7 |
| GBP | 51.2 |
| JPY | 49.5 |
| AUD | 50.1 |
| CAD | 51.8 |
| CHF | 49.2 |
| NZD | 52.5 |
---
## **How to Use:**
1. Copy the prompt above and send to ChatGPT/Claude
2. Ask for Excel file download
3. Save the Excel file
4. In APEX Layer 1 app → Monthly Entry tab → Click "Import Excel"
5. Select your Excel file
6. Data auto-fills the entry form
7. Click "Save & Calculate Scores"
---
## **Example AI Responses to Accept:**
- **ChatGPT**: Says "I can't create actual files, but here's the data:" → Copy to Excel manually
- **Claude**: May provide CSV format → Import that
- **Perplexity/Other**: Often provides downloadable formats directly
---
## **Alternative: Generate Test Data**
Ask AI:
```
Create realistic monthly CPI and PMI data for the 8 major currencies (USD, EUR, GBP, JPY, AUD, CAD, CHF, NZD)
for June 2026 in this format:
Currency,Target_CPI,Actual_CPI,Composite_PMI
...
Make it realistic based on economic forecasts and recent trends.
```
Then paste the CSV into your Excel file.
-71
View File
@@ -1,71 +0,0 @@
# GitHub Setup — QUICK START (5 Minutes)
## **🚀 TL;DR — Copy & Paste**
### **1. Create GitHub Repository**
- Go to [github.com](https://github.com)
- Click **"+" → New repository**
- Name: `apex_layer1`
- Select **"Add .gitignore: Python"**
- Click **"Create repository"**
- **Copy the HTTPS URL** (looks like: `https://github.com/YOUR_USERNAME/apex_layer1.git`)
### **2. Open PowerShell**
```powershell
cd "c:\Users\sober\Desktop\QuantCore FX\apex_layer1"
# First time setup
git config --global user.name "Your Name"
git config --global user.email "your.email@gmail.com"
# Initialize Git
git init
# Add all files
git add .
# Create first commit
git commit -m "Initial commit: APEX Layer 1 — Currency Strength Engine"
# Add remote (replace with YOUR URL from GitHub)
git remote add origin https://github.com/YOUR_USERNAME/apex_layer1.git
# Rename branch to main
git branch -M main
# Push to GitHub
git push -u origin main
```
### **3. Authenticate**
When GitHub asks for password:
- Use your **Personal Access Token** (not your password)
**To create a token:**
1. GitHub → Settings → Developer settings → Personal access tokens
2. Click "Generate new token"
3. Name: `apex_layer1`
4. Scope: ✅ `repo`
5. Copy token
6. Paste when prompted
---
## **✅ Verify**
- Refresh GitHub in browser
- See your files? ✅ Success!
---
## **📝 Future Updates** (Easy)
```powershell
# After making changes:
git add .
git commit -m "Description of what changed"
git push origin main
```
---
## **❓ Need Help?**
See [GITHUB_SETUP_GUIDE.md](GITHUB_SETUP_GUIDE.md) for detailed instructions & troubleshooting.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Sabermrddz
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+20
View File
@@ -0,0 +1,20 @@
# APEX — Currency Strength Engine (QuantCore FX)
Desktop-based **Currency Strength Engine** implementing institutional-quality **statistical arbitrage (StatArb)** for the forex market across 8 major currencies and 56 directional pairs.
## Architecture
Full documentation: [`project_structure_and_resume.md`](project_structure_and_resume.md)
### Layers
- **Layer 1 (Fundamental)** — Interest rates (FRED), CPI, PMI → currency scores 0100
- **Layer 2 (Technical)** — Bar-anchored Z-scores (288 M5 bars = 24h) across all 28 pairs → S.A.T.O.R.I. currency strength matrix
### Quick Start
```bash
pip install -r requirements.txt
cp .env.example .env # add your FRED_API_KEY
python main.py
```
> **Warning:** This system generates trading signals for educational/paper-trading use. Live execution (Phase 4) defaults to **disabled**. See rebuild plan for details.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+39 -15
View File
@@ -15,6 +15,7 @@ Responsibilities:
"""
import os
import logging
from dotenv import load_dotenv
from pathlib import Path
@@ -158,6 +159,23 @@ def validate_config():
# ============================================================================
DEBUG = os.getenv("DEBUG", "false").lower() == "true"
# ============================================================================
# Logging Configuration (Phase 5)
# ============================================================================
LOG_LEVEL = logging.DEBUG if DEBUG else logging.INFO
LOG_FORMAT = "%(asctime)s [%(levelname)s] %(name)s: %(message)s"
LOG_FILE = os.getenv("LOG_FILE", "apex.log")
logging.basicConfig(
level=LOG_LEVEL,
format=LOG_FORMAT,
handlers=[
logging.FileHandler(LOG_FILE),
logging.StreamHandler(),
],
)
logger = logging.getLogger("apex")
# ============================================================================
# UI Configuration
# ============================================================================
@@ -195,14 +213,26 @@ 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_LOOKBACK = int(os.getenv("Z_SCORE_LOOKBACK", 288)) # Bars for Z-score calculation (288 M5 bars = 24 hours)
# Historical Bar Configuration (Task 1.1 — Multi-hour anchored lookback)
BAR_TIMEFRAME = os.getenv("BAR_TIMEFRAME", "M5") # M1 or M5 bar intervals
BAR_LOOKBACK_HOURS = int(os.getenv("BAR_LOOKBACK_HOURS", 48)) # Hours of historical data
BAR_LOOKBACK_BARS = int(os.getenv("BAR_LOOKBACK_BARS", 288)) # Total bars (288 M5 bars = 24h)
# Multi-timeframe configuration
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"},
}
DEFAULT_TIMEFRAME = os.getenv("DEFAULT_TIMEFRAME", "M15")
# Historical bar config (backward compat)
BAR_TIMEFRAME = os.getenv("BAR_TIMEFRAME", "M5")
BAR_LOOKBACK_HOURS = int(os.getenv("BAR_LOOKBACK_HOURS", 48))
HISTORICAL_POLL_INTERVAL = int(os.getenv("HISTORICAL_POLL_INTERVAL", 300)) # 5 min in seconds
# SL/TP based on ATR
SL_ATR_PERIOD = 14
SL_ATR_MULTIPLIER = float(os.getenv("SL_ATR_MULTIPLIER", "2.0"))
TRADE_RR_RATIO = float(os.getenv("TRADE_RR_RATIO", "1.4"))
# Session Detection
SESSION_TOKYO_OPEN = 0 # 00:00 UTC
SESSION_TOKYO_CLOSE = 8 # 08:00 UTC
@@ -222,16 +252,10 @@ MAX_PORTFOLIO_LEVERAGE = float(os.getenv("MAX_PORTFOLIO_LEVERAGE", 2.0)) # Max
USE_GRID_HEDGING = os.getenv("USE_GRID_HEDGING", "true").lower() == "true"
GRID_LEVELS = int(os.getenv("GRID_LEVELS", 3)) # Number of hedging levels
# ============================================================================
# Mock Data Feeder Configuration (previously magic numbers)
# ============================================================================
MOCK_DRIFT = float(os.getenv("MOCK_DRIFT", 0.0001))
MOCK_THETA = float(os.getenv("MOCK_THETA", 0.02))
MOCK_NOISE_STD = float(os.getenv("MOCK_NOISE_STD", 0.0008))
MOCK_SEASONAL_AMP = float(os.getenv("MOCK_SEASONAL_AMP", 0.0003))
MOCK_TICK_NOISE = float(os.getenv("MOCK_TICK_NOISE", 0.0002))
MOCK_BID_ASK_SPREAD = float(os.getenv("MOCK_BID_ASK_SPREAD", 0.0001))
MOCK_HISTORICAL_DAILY_NOISE = float(os.getenv("MOCK_HISTORICAL_DAILY_NOISE", 0.01))
# Live Execution (Phase 4) — OFF by default
LIVE_TRADING_ENABLED = os.getenv("LIVE_TRADING_ENABLED", "false").lower() == "true"
MAX_DAILY_LOSS_PCT = float(os.getenv("MAX_DAILY_LOSS_PCT", 0.05))
MAX_BASKET_EXPOSURE_PCT = float(os.getenv("MAX_BASKET_EXPOSURE_PCT", 0.20))
if DEBUG:
print("[CONFIG] Debug mode enabled")
+38 -39
View File
@@ -17,9 +17,10 @@ class ConfluenceFilter:
provided it does not CROSS or VIOLATE the Layer 1 macro boundary.
"""
def __init__(self, technical_analyzer: TechnicalAnalyzer):
def __init__(self, technical_analyzer: TechnicalAnalyzer, db=None):
self.tech_analyzer = technical_analyzer
self.tech_signal = TechnicalSignal(technical_analyzer)
self.db = db
# Layer 1 directional bias matrix (set monthly from fundamental scores)
self.bias_matrix: Dict[str, Dict] = {}
@@ -101,25 +102,9 @@ class ConfluenceFilter:
return True, "Within macro boundary"
def check_entry_confluence(self, current_prices: Dict[str, float] = None
) -> Tuple[bool, str, float]:
"""Check entry conditions.
Layer 2 operates freely. The ONLY constraint is the Layer 1
macro directional boundary: STRONG currencies can't be shorted,
WEAK currencies can't be longed.
Priority:
1. Matrix divergence (currency-level) — checked against boundary
2. Pair extreme Z-score (pair-level) — checked against boundary
Returns:
(should_enter, reason, confluence_strength)
"""
) -> Tuple[bool, str, float, Optional[Dict]]:
self._build_matrix(current_prices)
# === PRIMARY: Matrix divergence ===
# If one currency is overbought across ALL pairs and another is
# oversold across ALL pairs, we have a genuine S.A.T.O.R.I. signal.
if self.matrix and self.matrix.has_divergence():
mc = self.matrix.get_matrix_cross()
gap = self.matrix.get_divergence_gap()
@@ -131,18 +116,22 @@ class ConfluenceFilter:
confidence = min(abs(gap) / 4.0, 1.0) * 100
self.confluence_strength = confidence
self.last_confluence_check = datetime.now()
return (
True,
f"MATRIX DIVERGENCE: {mc} "
f"(Gap: {gap:.1f}σ, Strength: {confidence:.0f}%)",
confidence,
)
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
return (False, f"MATRIX DIVERGENCE BLOCKED — {reason}", 0.0, None)
# === SECONDARY: Any extreme pair Z-score, checked against boundary ===
all_z = self.tech_analyzer.get_all_z_scores()
sorted_pairs = sorted(all_z.items(), key=lambda x: abs(x[1]), reverse=True)
@@ -161,14 +150,19 @@ class ConfluenceFilter:
confidence = min(abs(z_score) / 3.0, 1.0) * 100
self.confluence_strength = confidence
self.last_confluence_check = datetime.now()
return (
True,
f"PAIR EXTREME: {pair} Z={z_score:.2f} "
f"(Strength: {confidence:.0f}%)",
confidence,
)
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)
return False, "No valid signals within macro boundary", 0.0
return (False, "No valid signals within macro boundary", 0.0, None)
def check_exit_confluence(self) -> Tuple[bool, str]:
"""Check if position should exit (mean reversion / boundary shift)."""
@@ -247,7 +241,7 @@ class ConfluenceFilter:
}
def get_all_signals(self, current_prices: Dict[str, float] = None) -> Dict[str, Dict]:
"""Get all available signals ranked by strength."""
"""Get all available signals ranked by strength, with SL/TP."""
signals = {}
self._build_matrix(current_prices)
@@ -256,20 +250,21 @@ class ConfluenceFilter:
if mc:
gap = self.matrix.get_divergence_gap()
strength = min(abs(gap) / 4.0, 1.0) * 100
mc_z = self.tech_analyzer.get_z_score(mc)
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': 'SHORT' if strength > 50 else 'LONG',
'direction': direction,
**sl_tp,
}
# Scan all extreme pairs
for pair, z_score in sorted(
self.tech_analyzer.get_all_z_scores().items(),
key=lambda x: abs(x[1]), reverse=True
@@ -288,12 +283,16 @@ class ConfluenceFilter:
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': 'SHORT' if z_score > 0 else 'LONG',
'direction': direction,
**sl_tp,
}
return signals
-97
View File
@@ -1,97 +0,0 @@
"""
APEX Layer 1 — Example Excel Template Generator
Run this script to create example Excel files with the correct format.
Usage:
python create_excel_template.py
This will generate:
- example_monthly_data.xlsx (multi-sheet format)
- example_monthly_data_single_sheet.xlsx (single sheet format)
"""
import pandas as pd
import config
from datetime import datetime
def create_multi_sheet_template():
"""Create Excel with separate CPI and PMI sheets."""
# CPI Data
cpi_data = {
'Currency': config.CURRENCIES,
'Target %': [config.CB_TARGETS[c] for c in config.CURRENCIES],
'Actual CPI %': [3.2, 2.8, 3.1, 1.9, 3.5, 2.3, 1.2, 3.8] # Example values
}
# PMI Data
pmi_data = {
'Currency': config.CURRENCIES,
'Composite PMI': [52.3, 48.7, 51.2, 49.5, 50.1, 51.8, 49.2, 52.5] # Example values
}
# Create Excel file
with pd.ExcelWriter('example_monthly_data.xlsx', engine='openpyxl') as writer:
pd.DataFrame(cpi_data).to_excel(writer, sheet_name='CPI', index=False)
pd.DataFrame(pmi_data).to_excel(writer, sheet_name='PMI', index=False)
print("✓ Created: example_monthly_data.xlsx")
print(" - Sheet 1: CPI data")
print(" - Sheet 2: PMI data")
def create_single_sheet_template():
"""Create Excel with all data in one sheet."""
data = {
'Currency': config.CURRENCIES,
'Target_CPI': [config.CB_TARGETS[c] for c in config.CURRENCIES],
'Actual_CPI': [3.2, 2.8, 3.1, 1.9, 3.5, 2.3, 1.2, 3.8],
'Composite_PMI': [52.3, 48.7, 51.2, 49.5, 50.1, 51.8, 49.2, 52.5]
}
df = pd.DataFrame(data)
df.to_excel('example_monthly_data_single_sheet.xlsx', index=False)
print("✓ Created: example_monthly_data_single_sheet.xlsx")
print(" - All data in one sheet")
def create_csv_template():
"""Create CSV example."""
data = {
'Currency': config.CURRENCIES,
'Target_CPI': [config.CB_TARGETS[c] for c in config.CURRENCIES],
'Actual_CPI': [3.2, 2.8, 3.1, 1.9, 3.5, 2.3, 1.2, 3.8],
'Composite_PMI': [52.3, 48.7, 51.2, 49.5, 50.1, 51.8, 49.2, 52.5]
}
df = pd.DataFrame(data)
df.to_csv('example_monthly_data.csv', index=False)
print("✓ Created: example_monthly_data.csv")
if __name__ == "__main__":
print(f"APEX Layer 1 - Template Generator")
print(f"Month: {datetime.now().strftime('%Y-%m')}\n")
try:
create_multi_sheet_template()
create_single_sheet_template()
create_csv_template()
print("\n" + "="*60)
print("Templates created successfully!")
print("="*60)
print("\nUsage:")
print("1. Open any template file")
print("2. Replace example values with real economic data")
print("3. In APEX app: Monthly Entry tab → Import Excel")
print("4. Select your file and click Open")
print("5. Click 'Save & Calculate Scores'")
except Exception as e:
print(f"\n✗ Error creating templates: {e}")
print("\nMake sure you have openpyxl and pandas installed:")
print(" pip install openpyxl pandas")
+6 -261
View File
@@ -10,9 +10,6 @@ Fetches forex data from a local MetaTrader 5 terminal.
Requirements:
- MetaTrader 5 terminal installed and running with a demo/live account
- pip install MetaTrader5
Fallback:
- MockDataFeeder for testing without MT5
"""
import time
@@ -219,7 +216,7 @@ class Mt5DataFeeder:
continue
rates = self._mt5.copy_rates_from_pos(mt5_pair, tf, 0, count)
if rates is not None:
closes = [r.close for r in rates]
closes = [r["close"] for r in rates]
all_closes[pair] = closes
return all_closes
@@ -263,11 +260,11 @@ class Mt5DataFeeder:
candles = []
for r in rates:
candles.append({
"time": datetime.fromtimestamp(r.time).isoformat(),
"open": r.open,
"high": r.high,
"low": r.low,
"close": r.close,
"time": datetime.fromtimestamp(r["time"]).isoformat(),
"open": r["open"],
"high": r["high"],
"low": r["low"],
"close": r["close"],
})
return candles
@@ -314,256 +311,4 @@ class Mt5DataFeeder:
print("[MT5] Streaming stopped")
class MockDataFeeder:
"""Mock data feeder for testing — simulates intraday prices for all 28 pairs."""
USD_PAIRS = ["EUR_USD", "GBP_USD", "AUD_USD", "NZD_USD",
"USD_JPY", "USD_CAD", "USD_CHF"]
def __init__(self):
self.base_prices = {
"EUR_USD": 1.0850,
"GBP_USD": 1.2650,
"AUD_USD": 0.6650,
"NZD_USD": 0.6050,
"USD_JPY": 149.50,
"USD_CAD": 1.3750,
"USD_CHF": 0.8920,
}
self.price_callbacks = []
self.connected = True
self._running = False
self._cached_bars: Dict[str, List[float]] = {}
self._tick_index = 0
def test_connection(self) -> bool:
return True
def get_all_major_pairs(self) -> List[str]:
pairs = []
for base in config.CURRENCIES:
for quote in config.CURRENCIES:
if base != quote:
pairs.append(f"{base}_{quote}")
return pairs
def generate_mock_bars(self, n_bars: int = 288) -> Dict[str, List[float]]:
"""Generate n_bars simulated M5 close prices with realistic behavior.
Uses an Ornstein-Uhlenbeck process (mean-reverting random walk with
drift) for each of the 7 USD pairs, then derives all 28 cross rates.
This gives 24h (288 M5 bars) of realistic forex data where Z-scores
reflect genuine multi-hour deviations.
Caches the generated bars so subsequent tick prices are anchored
to the last bar close — not the initial base price.
"""
import random
bars: Dict[str, List[float]] = {}
usd_pair_bars: Dict[str, List[float]] = {}
for pair in self.USD_PAIRS:
base = self.base_prices.get(pair, 1.0)
series = []
price = base
drift = random.uniform(-config.MOCK_DRIFT, config.MOCK_DRIFT)
theta = config.MOCK_THETA
long_term_mean = base
for i in range(n_bars):
noise = random.gauss(0, config.MOCK_NOISE_STD)
reversion = theta * (long_term_mean - price)
seasonal = config.MOCK_SEASONAL_AMP * random.uniform(-1, 1)
price = price + reversion + drift + seasonal + noise
series.append(price)
usd_pair_bars[pair] = series
pairs_list = self.get_all_major_pairs()
for pair in pairs_list:
base_c, quote_c = pair.split("_")
derived = []
for i in range(n_bars):
usd_rates = {"USD": 1.0}
for up in self.USD_PAIRS:
b, q = up.split("_")
mid = usd_pair_bars[up][i]
if b == "USD":
usd_rates[q] = 1.0 / mid if mid else 0
else:
usd_rates[b] = mid
bv = usd_rates.get(base_c, 0)
qv = usd_rates.get(quote_c, 1)
derived.append(bv / qv if qv else 0)
bars[pair] = derived
self._cached_bars = bars
self._tick_index = 0
return bars
def _current_bar_prices(self) -> Dict[str, float]:
"""Get the latest bar close prices for all pairs."""
if not self._cached_bars:
return {}
prices = {}
for pair in self.get_all_major_pairs():
bars = self._cached_bars.get(pair)
if bars:
prices[pair] = bars[-1]
return prices
def _tick_price(self, pair: str) -> float:
"""Return price anchored to last bar close + small noise.
Uses the last bar close from _cached_bars as the anchor, so the
tick price is always near the most recent bar and Z-scores reflect
the bar position relative to the 24-hour history, not random noise.
"""
import random
last_bars = self._cached_bars.get(pair) if self._cached_bars else None
if last_bars and len(last_bars) > 0:
base = last_bars[-1]
else:
base = self.base_prices.get(pair, 1.0)
return base + random.uniform(-config.MOCK_TICK_NOISE, config.MOCK_TICK_NOISE)
def get_current_price(self, currency_pair: str) -> Optional[Dict]:
"""Get price for any pair, deriving cross rates from USD pairs."""
import random
usd_rates = {}
for p in self.USD_PAIRS:
base, quote = p.split("_")
mid = self._tick_price(p)
if base == "USD":
usd_rates[quote] = 1.0 / mid if mid != 0 else None
else:
usd_rates[base] = mid
usd_rates["USD"] = 1.0
base_c, quote_c = currency_pair.split("_")
base_val = usd_rates.get(base_c)
quote_val = usd_rates.get(quote_c)
if base_val is None or quote_val is None:
return None
price = base_val / quote_val
return {
"pair": currency_pair,
"time": datetime.now().isoformat(),
"mid": price,
"bid": price - config.MOCK_BID_ASK_SPREAD,
"ask": price + config.MOCK_BID_ASK_SPREAD,
}
def fetch_all_rates(self) -> Dict[str, float]:
"""Derive all 28 cross rates from 7 USD pairs (same as Mt5DataFeeder)."""
usd_rates: Dict[str, Optional[float]] = {}
for p in self.USD_PAIRS:
base, quote = p.split("_")
mid = self._tick_price(p)
if base == "USD":
usd_rates[quote] = 1.0 / mid if mid != 0 else None
else:
usd_rates[base] = mid
usd_rates["USD"] = 1.0
rates = {}
for base in config.CURRENCIES:
for quote in config.CURRENCIES:
if base == quote:
continue
bv = usd_rates.get(base)
qv = usd_rates.get(quote)
if bv is not None and qv is not None:
rates[f"{base}_{quote}"] = bv / qv
return rates
def get_order_book(self, currency_pair: str) -> Optional[Dict]:
"""Mock order book — simulated bid/ask/spread."""
price = self.get_current_price(currency_pair)
if not price:
return None
return {
"pair": currency_pair,
"bid": price["mid"] - config.MOCK_BID_ASK_SPREAD,
"ask": price["mid"] + config.MOCK_BID_ASK_SPREAD,
"spread": config.MOCK_BID_ASK_SPREAD * 2,
"mid": price["mid"],
"time": datetime.now().isoformat(),
}
def fetch_historical_closes_all_pairs(
self, days: int = 30, interval: str = "1d"
) -> Dict[str, List[float]]:
"""Mock historical close prices — random walk for all 28 pairs."""
import random
closes: Dict[str, List[float]] = {}
pairs = self.get_all_major_pairs()
all_rates = self.fetch_all_rates()
for pair in pairs:
base = all_rates.get(pair, 1.0)
series = []
price = base
for _ in range(days):
price += random.uniform(-config.MOCK_HISTORICAL_DAILY_NOISE, config.MOCK_HISTORICAL_DAILY_NOISE)
series.append(price)
closes[pair] = series
return closes
def get_historical_candles(
self,
from_currency: str = "USD",
to_currency: str = "JPY",
interval: str = "1min",
outputsize: str = "compact",
) -> Optional[List[Dict]]:
import random
candles = []
count = 100 if outputsize == "full" else 20
pair = f"{from_currency}_{to_currency}"
base = self._cached_bars.get(pair, [None])[-1] if self._cached_bars.get(pair) else 1.0
for i in range(count):
noise = random.uniform(-0.005, 0.005)
price = base + noise
candles.append({
"time": (datetime.now() - timedelta(minutes=count - i)).isoformat(),
"open": price,
"high": price + 0.01,
"low": price - 0.01,
"close": price + random.uniform(-0.005, 0.005),
})
return candles
def stream_prices(self, instruments: List[str], callback: Callable, poll_interval: int = 1):
"""Derive all 28 rates and feed callback for each instrument (same as MT5)."""
self.price_callbacks.append(callback)
self._running = True
def mock_stream():
while self._running:
all_rates = self.fetch_all_rates()
for pair in instruments:
rate = all_rates.get(pair)
if rate:
callback({
"pair": pair,
"time": datetime.now().isoformat(),
"mid": rate,
"bid": rate,
"ask": rate,
})
time.sleep(poll_interval)
thread = threading.Thread(target=mock_stream, daemon=True)
thread.start()
def stop_streaming(self):
"""Stop the mock data stream."""
self._running = False
if config.DEBUG:
print("[Mock] Streaming stopped")
def on_price_update(self, callback: Callable):
self.price_callbacks.append(callback)
def on_error(self, callback: Callable):
pass
+62
View File
@@ -144,6 +144,26 @@ class Database:
CONSTRAINT valid_month CHECK (month LIKE '____-__')
);
""")
# Table 6: Confluence signal audit log (real-time triggers)
cursor.execute("""
CREATE TABLE IF NOT EXISTS confluence_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
triggered_at TEXT NOT NULL,
pair TEXT NOT NULL,
signal_type TEXT NOT NULL,
confidence REAL NOT NULL,
z_score REAL,
gap REAL,
reason TEXT,
layer1_active INTEGER DEFAULT 0,
status TEXT DEFAULT 'PENDING'
);
""")
cursor.execute("""
CREATE INDEX IF NOT EXISTS idx_confluence_log_time
ON confluence_log(triggered_at DESC);
""")
self.conn.commit()
@@ -624,6 +644,48 @@ class Database:
except sqlite3.Error as e:
raise RuntimeError(f"Failed to fetch signals: {e}")
# ========================================================================
# CONFLUENCE_LOG Table Operations (Phase 3 audit trail)
# ========================================================================
def save_confluence_signal(self, pair: str, signal_type: str, confidence: float,
z_score: float = None, gap: float = None,
reason: str = None, layer1_active: bool = False) -> None:
"""Persist a real-time confluence trigger to the audit log.
Unlike save_signal() (monthly Layer 1 signals), this logs every
live/backtested trigger with sub-second granularity.
"""
try:
with self._lock:
cursor = self.conn.cursor()
cursor.execute("""
INSERT INTO confluence_log
(triggered_at, pair, signal_type, confidence, z_score, gap, reason, layer1_active, status)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'PENDING')
""", (
datetime.now(timezone.utc).isoformat(),
pair, signal_type, confidence, z_score, gap, reason,
1 if layer1_active else 0
))
self.conn.commit()
except sqlite3.Error as e:
self.conn.rollback()
def get_confluence_log(self, limit: int = 100) -> List[Dict]:
"""Get the most recent confluence triggers."""
cursor = self.conn.cursor()
try:
cursor.execute("""
SELECT triggered_at, pair, signal_type, confidence, z_score, gap, reason, status
FROM confluence_log
ORDER BY triggered_at DESC LIMIT ?
""", (limit,))
rows = cursor.fetchall()
return [dict(r) for r in rows]
except sqlite3.Error:
return []
# ========================================================================
# Utility Methods
# ========================================================================
+104 -51
View File
@@ -1,32 +1,22 @@
from typing import Dict, List, Optional, Tuple
from collections import deque
import statistics
import math
import config
class TechnicalAnalyzer:
"""Real-time technical analysis with bar-anchored statistics (Task 1.1).
Maintains two data streams:
1. Bar history (M1/M5 candles) — the multi-hour statistical anchor
for μ and σ (config.BAR_LOOKBACK_BARS, default 288 M5 bars = 24h).
2. Live tick/poll deques — fast recent movement for display.
Z-score formula (priority):
If bar history has >= 2 bars: Z = (tick - μ_bars) / σ_bars
Otherwise (fallback): Z = (tick - μ_ticks) / σ_ticks
μ and σ prefer the multi-hour bar frame, but fall back to tick-based
statistics when bars haven't been seeded yet.
"""
def __init__(self, lookback: int = None):
def __init__(self, lookback: int = None, timeframe: str = None):
self.current_timeframe = timeframe or config.DEFAULT_TIMEFRAME
tf_config = config.TIMEFRAMES.get(self.current_timeframe, config.TIMEFRAMES["M15"])
if lookback is None:
lookback = config.BAR_LOOKBACK_BARS
lookback = tf_config["bars"]
self.bar_lookback = lookback
self.tick_lookback = 20
self.atr_period = config.SL_ATR_PERIOD
self.bar_history: Dict[str, deque] = {}
self.ohlc_history: Dict[str, deque] = {}
self.price_history: Dict[str, deque] = {}
self.volume_history: Dict[str, deque] = {}
self.z_scores: Dict[str, float] = {}
@@ -37,50 +27,60 @@ class TechnicalAnalyzer:
if base != quote:
pair = f"{base}_{quote}"
self.bar_history[pair] = deque(maxlen=self.bar_lookback)
self.ohlc_history[pair] = deque(maxlen=self.bar_lookback)
self.price_history[pair] = deque(maxlen=self.tick_lookback)
self.volume_history[pair] = deque(maxlen=self.tick_lookback)
self.z_scores[pair] = 0.0
self.extremes[pair] = False
def set_timeframe(self, tf_key: str):
tf_config = config.TIMEFRAMES.get(tf_key)
if not tf_config:
return
self.current_timeframe = tf_key
new_lookback = tf_config["bars"]
if new_lookback != self.bar_lookback:
self.bar_lookback = new_lookback
for pair in self.bar_history:
self.bar_history[pair] = deque(
list(self.bar_history[pair])[-new_lookback:],
maxlen=new_lookback,
)
self.ohlc_history[pair] = deque(
list(self.ohlc_history[pair])[-new_lookback:],
maxlen=new_lookback,
)
def add_bar(self, currency_pair: str, close: float, high: float = None,
low: float = None, volume: int = 0):
"""Add a completed M1/M5 bar to the multi-hour historical frame."""
if currency_pair not in self.bar_history:
return
self.bar_history[currency_pair].append(close)
if high is not None and low is not None:
self.ohlc_history[currency_pair].append((close, high, low))
def add_price_data(self, currency_pair: str, close_price: float,
volume: float = 0):
"""Add tick/poll price."""
if currency_pair not in self.price_history:
return
self.price_history[currency_pair].append(close_price)
if volume > 0:
self.volume_history[currency_pair].append(volume)
self._update_z_score(currency_pair)
def _get_mean_std(self, currency_pair: str) -> Tuple[float, float]:
"""Compute μ and σ, preferring bar history over tick history.
Falls back to tick data when bars haven't been seeded yet,
so the system works immediately from the first price update.
"""
bars = list(self.bar_history[currency_pair])
if len(bars) >= 2:
try:
return (statistics.mean(bars), statistics.stdev(bars))
except (ValueError, statistics.StatisticsError):
pass
ticks = list(self.price_history[currency_pair])
if len(ticks) >= 2:
try:
return (statistics.mean(ticks), statistics.stdev(ticks))
except (ValueError, statistics.StatisticsError):
pass
return (0.0, 0.0)
def _update_z_score(self, currency_pair: str):
@@ -89,13 +89,11 @@ class TechnicalAnalyzer:
self.z_scores[currency_pair] = 0.0
self.extremes[currency_pair] = False
return
mu, sigma = self._get_mean_std(currency_pair)
if sigma == 0.0:
self.z_scores[currency_pair] = 0.0
self.extremes[currency_pair] = False
return
current_price = prices[-1]
z_score = (current_price - mu) / sigma
self.z_scores[currency_pair] = z_score
@@ -114,7 +112,6 @@ class TechnicalAnalyzer:
return [pair for pair, z in self.z_scores.items() if z <= -config.Z_SCORE_THRESHOLD]
def get_volatility(self, currency_pair: str) -> float:
"""Volatility from bar history, falling back to ticks."""
bars = list(self.bar_history[currency_pair])
if len(bars) >= 2:
try:
@@ -130,7 +127,6 @@ class TechnicalAnalyzer:
return 0.0
def get_mean_price(self, currency_pair: str) -> float:
"""Mean from bar history, falling back to ticks."""
bars = list(self.bar_history[currency_pair])
if len(bars) >= 1:
return statistics.mean(bars)
@@ -179,40 +175,97 @@ class TechnicalAnalyzer:
'volatility': volatility,
'mean_price': mean_price,
'is_extreme': is_extreme,
'status': status
'status': status,
}
def seed_bars(self, historical_bars: Dict[str, List[float]]):
"""Seed bar_history with 288 M5 bars (24h) of historical close prices.
Args:
historical_bars: dict mapping pair -> list of close prices (oldest first)
"""
for pair, closes in historical_bars.items():
if pair in self.bar_history:
self.bar_history[pair].clear()
for c in closes[-self.bar_lookback:]:
self.bar_history[pair].append(c)
if len(self.bar_history[pair]) >= 2:
mu, sigma = self._get_mean_std(pair)
ticks = list(self.price_history[pair])
if ticks and sigma > 0:
z = (ticks[-1] - mu) / sigma
self.z_scores[pair] = z
self.extremes[pair] = abs(z) >= config.Z_SCORE_THRESHOLD
if pair not in self.bar_history:
continue
self.bar_history[pair].clear()
for c in closes[-self.bar_lookback:]:
self.bar_history[pair].append(c)
if len(self.bar_history[pair]) >= 2:
mu, sigma = self._get_mean_std(pair)
ticks = list(self.price_history[pair])
if ticks and sigma > 0:
z = (ticks[-1] - mu) / sigma
self.z_scores[pair] = z
self.extremes[pair] = abs(z) >= config.Z_SCORE_THRESHOLD
def seed_ohlc(self, ohlc_data: Dict[str, List[Dict]]):
"""Seed both bar_history and ohlc_history from full candle data.
Each dict in the list must have 'close', 'high', 'low' keys.
"""
for pair, candles in ohlc_data.items():
if pair not in self.bar_history:
continue
self.bar_history[pair].clear()
self.ohlc_history[pair].clear()
n_bars = min(len(candles), self.bar_lookback)
for i in range(-n_bars, 0):
c = candles[i]
self.bar_history[pair].append(c["close"])
self.ohlc_history[pair].append((c["close"], c["high"], c["low"]))
if len(self.bar_history[pair]) >= 2:
mu, sigma = self._get_mean_std(pair)
ticks = list(self.price_history[pair])
if ticks and sigma > 0:
z = (ticks[-1] - mu) / sigma
self.z_scores[pair] = z
self.extremes[pair] = abs(z) >= config.Z_SCORE_THRESHOLD
def clear_history(self):
for pair in self.bar_history:
self.bar_history[pair].clear()
self.ohlc_history[pair].clear()
self.price_history[pair].clear()
self.volume_history[pair].clear()
self.z_scores[pair] = 0.0
self.extremes[pair] = False
# ------------------------------------------------------------------
# ATR + SL/TP
# ------------------------------------------------------------------
def calculate_atr(self, pair: str, period: int = None) -> Optional[float]:
if period is None:
period = self.atr_period
ohlc = list(self.ohlc_history.get(pair, []))
if len(ohlc) < period + 1:
return None
tr_values = []
for i in range(1, len(ohlc)):
_, h, l = ohlc[i]
_, prev_c, _ = ohlc[i - 1]
tr = max(h - l, abs(h - prev_c), abs(l - prev_c))
tr_values.append(tr)
if len(tr_values) < period:
return None
atr = sum(tr_values[-period:]) / period
return atr
def calculate_sl_tp(
self, pair: str, direction: str, entry_price: float
) -> Dict[str, float]:
atr = self.calculate_atr(pair)
result = {"entry": entry_price, "sl": None, "tp": None, "atr": atr}
sl_mult = config.SL_ATR_MULTIPLIER
rr = config.TRADE_RR_RATIO
if atr is not None and atr > 0:
sl_distance = atr * sl_mult
tp_distance = sl_distance * rr
if direction == "LONG":
result["sl"] = entry_price - sl_distance
result["tp"] = entry_price + tp_distance
else:
result["sl"] = entry_price + sl_distance
result["tp"] = entry_price - tp_distance
return result
class TechnicalSignal:
"""Generates technical entry/exit signals based on Z-scores."""
def __init__(self, analyzer: TechnicalAnalyzer):
self.analyzer = analyzer
+257 -1
View File
@@ -54,8 +54,264 @@ def main():
# Create QApplication
app = QApplication(sys.argv)
# Set application-wide stylesheet (optional)
# Set Fusion style
app.setStyle('Fusion')
# Global professional stylesheet
app.setStyleSheet(f"""
/*****************************************************************
* APEX Professional Trading System Global Stylesheet
*****************************************************************/
/* ----- Root / Background ----- */
QMainWindow {{
background-color: #f0f2f5;
}}
QWidget {{
font-family: "Segoe UI", "Arial", sans-serif;
font-size: 13px;
color: #2c3e50;
}}
/* ----- Tab Widget (Navigation Bar) ----- */
QTabWidget::pane {{
border: none;
background: #f0f2f5;
top: -1px;
}}
QTabBar::tab {{
background: #2c3e50;
color: #95a5a6;
padding: 10px 22px;
margin-right: 2px;
border-top-left-radius: 6px;
border-top-right-radius: 6px;
font-size: 13px;
font-weight: 600;
}}
QTabBar::tab:selected {{
background: #f0f2f5;
color: #2c3e50;
border-bottom: 2px solid #3498db;
}}
QTabBar::tab:hover:!selected {{
background: #34495e;
color: #ecf0f1;
}}
/* ----- Cards (QFrame) ----- */
QFrame#card {{
background-color: #ffffff;
border: 1px solid #e0e4e8;
border-radius: 10px;
padding: 18px;
}}
QFrame#card:hover {{
border-color: #c0c8d0;
}}
QFrame#statusCard {{
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
stop:0 #ffffff, stop:1 #f8f9fb);
border: 1px solid #e0e4e8;
border-radius: 10px;
padding: 20px;
}}
/* ----- Labels ----- */
QLabel {{
color: #2c3e50;
}}
QLabel[heading="true"] {{
font-size: 16px;
font-weight: 700;
color: #1a1a2e;
padding-bottom: 4px;
}}
QLabel[subheading="true"] {{
font-size: 13px;
font-weight: 600;
color: #7f8c8d;
letter-spacing: 1px;
}}
QLabel[value="true"] {{
font-size: 28px;
font-weight: 700;
}}
/* ----- Tables ----- */
QTableWidget {{
background-color: #ffffff;
border: 1px solid #e0e4e8;
border-radius: 8px;
gridline-color: #f0f2f5;
selection-background-color: #ebf5fb;
selection-color: #2c3e50;
padding: 4px;
}}
QTableWidget::item {{
padding: 6px 10px;
border-bottom: 1px solid #f0f2f5;
}}
QTableWidget::item:selected {{
background-color: #ebf5fb;
color: #2c3e50;
}}
QHeaderView::section {{
background-color: #f8f9fb;
color: #7f8c8d;
font-weight: 600;
font-size: 12px;
text-transform: uppercase;
padding: 8px 10px;
border: none;
border-bottom: 2px solid #e0e4e8;
}}
/* ----- Buttons ----- */
QPushButton {{
background-color: #3498db;
color: #ffffff;
border: none;
border-radius: 6px;
padding: 8px 20px;
font-size: 13px;
font-weight: 600;
}}
QPushButton:hover {{
background-color: #2980b9;
}}
QPushButton:pressed {{
background-color: #2471a3;
}}
QPushButton:disabled {{
background-color: #bdc3c7;
color: #95a5a6;
}}
QPushButton#success {{
background-color: #27ae60;
}}
QPushButton#success:hover {{
background-color: #229954;
}}
QPushButton#success:disabled {{
background-color: #bdc3c7;
}}
QPushButton#danger {{
background-color: #e74c3c;
}}
QPushButton#danger:hover {{
background-color: #cb4335;
}}
QPushButton#secondary {{
background-color: #95a5a6;
}}
QPushButton#secondary:hover {{
background-color: #7f8c8d;
}}
/* ----- Progress Bar ----- */
QProgressBar {{
background-color: #ecf0f1;
border: none;
border-radius: 6px;
height: 18px;
text-align: center;
font-size: 12px;
font-weight: 600;
color: #2c3e50;
}}
QProgressBar::chunk {{
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
stop:0 #3498db, stop:1 #2ecc71);
border-radius: 6px;
}}
/* ----- Combo Box / Spinner ----- */
QComboBox {{
background-color: #ffffff;
border: 1px solid #d5d8dc;
border-radius: 6px;
padding: 6px 12px;
min-height: 20px;
}}
QComboBox:hover {{
border-color: #3498db;
}}
QComboBox::drop-down {{
border: none;
width: 24px;
}}
QDoubleSpinBox, QSpinBox, QLineEdit {{
background-color: #ffffff;
border: 1px solid #d5d8dc;
border-radius: 6px;
padding: 6px 10px;
min-height: 20px;
}}
QDoubleSpinBox:focus, QSpinBox:focus, QLineEdit:focus {{
border-color: #3498db;
}}
/* ----- Group Box ----- */
QGroupBox {{
font-size: 14px;
font-weight: 700;
color: #2c3e50;
border: 1px solid #e0e4e8;
border-radius: 10px;
margin-top: 12px;
padding: 20px 16px 16px 16px;
}}
QGroupBox::title {{
subcontrol-origin: margin;
subcontrol-position: top left;
padding: 4px 12px;
background-color: #f0f2f5;
border-radius: 4px;
margin-left: 10px;
}}
/* ----- Scroll Area ----- */
QScrollArea {{
border: none;
background: transparent;
}}
/* ----- Check Box ----- */
QCheckBox {{
spacing: 8px;
}}
QCheckBox::indicator {{
width: 18px;
height: 18px;
border-radius: 4px;
border: 2px solid #bdc3c7;
}}
QCheckBox::indicator:checked {{
background-color: #3498db;
border-color: #3498db;
}}
/* ----- Tooltip ----- */
QToolTip {{
background-color: #2c3e50;
color: #ffffff;
border: none;
padding: 8px 12px;
border-radius: 6px;
font-size: 12px;
}}
/* ----- Status Bar ----- */
QStatusBar {{
background-color: #2c3e50;
color: #ecf0f1;
font-size: 12px;
}}
""")
# Create and show main window
window = MainWindow()
+3 -6
View File
@@ -21,7 +21,6 @@ Responsibilities:
from PyQt5.QtWidgets import QMainWindow, QTabWidget, QMessageBox
from PyQt5.QtCore import QThread, pyqtSignal
from PyQt5.QtGui import QFont
from typing import Dict, Optional
import config
from database import Database
@@ -32,6 +31,7 @@ from ui.layer2_monitor_tab import Layer2MonitorTab
from ui.confluence_tab import ConfluenceSignalsTab
from ui.history_tab import HistoryTab
from ui.settings_tab import SettingsTab
from fred_client import FredClient
class FredFetchWorker(QThread):
@@ -98,7 +98,8 @@ class MainWindow(QMainWindow):
raise
# Initialize Layer 2 components
self.tech_analyzer = TechnicalAnalyzer(lookback=config.Z_SCORE_LOOKBACK)
bars = config.TIMEFRAMES[config.DEFAULT_TIMEFRAME]["bars"]
self.tech_analyzer = TechnicalAnalyzer(lookback=bars)
# UI components
self.dashboard_tab = None
@@ -149,10 +150,6 @@ class MainWindow(QMainWindow):
# Set main widget
self.setCentralWidget(tabs)
# Style tabs
tab_font = QFont("Arial", 11)
tabs.setFont(tab_font)
def _connect_signals(self):
"""Connect inter-tab signals."""
-878
View File
@@ -1,878 +0,0 @@
# APEX — Currency Strength Engine
> **Version:** 1.1.0
> **Timeframe:** Swing / Position Trading (15 day holds)
> **Architecture:** S.A.T.O.R.I. (Statistical Arbitrage Trading & Orchestrated Reversion Index)
> **Layer:** Layer 1 (Fundamental) + Layer 2 (Technical/Statistical)
> **Methodology:** Dr. Giavon's Deconstructed Currency Strength Indexing
---
## 1. Project Overview
APEX is a desktop-based **Currency Strength Engine** that implements institutional-quality **statistical arbitrage (StatArb)** for the forex market. It deconstructs all 28 major cross-pairs to isolate the true strength/weakness of individual currencies, then generates mean-reversion signals when statistical divergences reach extreme thresholds.
### Core Principle
Instead of analyzing EUR/USD as a single entity, APEX decomposes every pair to isolate individual currency strength indices:
```
Individual Currency Strength = Average Z-Score Across ALL 7 Pairs Involving That Currency
EUR_Strength = avg(Z(EUR_USD), Z(EUR_GBP), Z(EUR_JPY), Z(EUR_AUD), Z(EUR_CAD), Z(EUR_CHF), Z(EUR_NZD))
USD_Strength = avg(Z(USD_EUR), Z(USD_GBP), Z(USD_JPY), Z(USD_AUD), Z(USD_CAD), Z(USD_CHF), Z(USD_NZD))
... and so on for all 8 currencies
```
### Trading Philosophy
| Component | Strategy |
|-----------|----------|
| **Timeframe** | Swing / Position — 1 to 5 day holds |
| **Entry Trigger** | Matrix Cross divergence: one currency overbought (Z > +2.0) across ALL pairs, another oversold (Z < -2.0) simultaneously |
| **Execution** | Short the strongest, buy the weakest — bet on mathematical mean reversion |
| **Risk Management** | No single-pair stop losses. Basket hedging across correlated pairs + grid hedging |
| **Exit** | Aggregate portfolio P&L goes net positive (portfolio-based exit, not per-pair) |
| **Z-Score Anchor** | 288 M5 bars = 24 hours of historical data (not tick noise) |
| **Session Tracking** | Tracks Tokyo / London / New York opens with Session Relative Velocity |
---
## 2. Architecture
```
┌────────────────────────────────────────────────────────────────────┐
│ APEX APPLICATION │
├────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ UI LAYER (6 Tabs) │ │
│ │ ┌──────────┐ ┌────────┐ ┌──────────┐ ┌────────┐ ┌──────┐ │ │
│ │ │Dashboard │ │Data │ │Layer 2 │ │Confluence│ │Hist.│ │ │
│ │ │(Fundamen)│ │Entry │ │Monitor │ │Signals │ │ │ │ │
│ │ └────┬─────┘ └────────┘ └────┬─────┘ └────┬────┘ └──────┘ │ │
│ └───────┼───────────────────────┼─────────────┼────────────────┘ │
│ │ │ │ │
│ ┌───────▼───────────────────────▼─────────────▼────────────────┐ │
│ │ BUSINESS LOGIC LAYER │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │ │
│ │ │ Scoring (L1) │ │Technical (L2)│ │ Matrix Engine │ │ │
│ │ │ scorer.py │ │layer2_tech │ │ currency_strength│ │ │
│ │ │ │ │ .py │ │ _matrix.py │ │ │
│ │ └──────┬───────┘ └──────┬───────┘ └────────┬─────────┘ │ │
│ │ │ │ │ │ │
│ │ ┌──────▼────────────────▼──────────────────▼──────────┐ │ │
│ │ │ CONFLUENCE FILTER │ │ │
│ │ │ confluence_filter.py │ │ │
│ │ │ Layer 1 + Layer 2 + Matrix = Entry Signal │ │ │
│ │ └──────────────────────┬──────────────────────────────┘ │ │
│ │ │ │ │
│ │ ┌──────────────────────▼──────────────────────────────┐ │ │
│ │ │ RISK MANAGEMENT SYSTEM │ │ │
│ │ │ risk_management.py │ │ │
│ │ │ Position Sizing + Grid Hedge + Basket Hedge + │ │ │
│ │ │ Portfolio P&L Tracking + Aggregate Exit │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ DATA LAYER │ │
│ │ ┌───────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │
│ │ │ FRED API │ │ MT5 Data │ │ SQLite │ │ Excel Import │ │ │
│ │ │(interest │ │(forex │ │ Database │ │ (CPI/PMI) │ │ │
│ │ │ rates) │ │ prices) │ │ apex.db │ │ │ │ │
│ │ └───────────┘ └──────────┘ └──────────┘ └──────────────┘ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────────┘
```
### The Two Layers
| Layer | Input | Frequency | Output |
|-------|-------|-----------|--------|
| **Layer 1 (Fundamental)** | Interest rates (FRED), CPI, PMI (manual/Excel) | Monthly | Currency scores (0-100), Strongest/Weakest ranking |
| **Layer 2 (Technical)** | 288 M5 bars (24h) + live poll prices | Bar-anchored, tick-displayed | 28 pair Z-scores (anchored to 24h μ/σ), 8 currency strength indices, Matrix Cross |
**Critical design:** Z-scores are NOT calculated over raw tick polls. On connect, the analyzer is seeded with 288 M5 bars of historical close prices. μ and σ are computed from this 24-hour window. Live tick prices are compared against this stable anchor, producing meaningful multi-hour deviation readings that don't flip on every tick.
**Fallback:** If bar data hasn't been seeded yet, the system falls back to a 20-tick deque for immediate display. Once `seed_bars()` is called, the bar anchor takes over permanently.
---
## 3. Directory Structure
```
apex_layer1/
├── __init__.py # Package marker (v1.0.0)
├── main.py # Application entry point
├── main_window.py # QMainWindow + tab assembly
├── config.py # All configuration & constants from .env
├── data_feeder.py # MT5 + Mock data feeders
├── fred_client.py # FRED interest rate API client
├── database.py # SQLite database manager
├── scorer.py # Layer 1 scoring engine
├── layer2_technical.py # Layer 2 Z-score engine (28 pairs)
├── currency_strength_matrix.py # S.A.T.O.R.I. currency strength index
├── confluence_filter.py # Layer 1 + Layer 2 + Matrix merging
├── risk_management.py # Position sizing, hedging, portfolio mgmt
├── create_excel_template.py # Excel/CSV template generator
├── requirements.txt # Python dependencies
├── .env # Live configuration (API keys)
├── .env.example # Configuration template
├── apex.db # SQLite database (auto-created)
├── ui/
│ ├── __init__.py
│ ├── dashboard_tab.py # Tab 1: Layer 1 fundamental signals
│ ├── entry_tab.py # Tab 2: CPI/PMI data entry
│ ├── layer2_monitor_tab.py # Tab 3: Live Z-scores + Matrix
│ ├── confluence_tab.py # Tab 4: Merged signals
│ ├── history_tab.py # Tab 5: Past signals
│ └── settings_tab.py # Tab 6: Configuration
```
---
## 4. File-by-File Breakdown
### 4.1 Entry Point
#### `main.py`
Launches the PyQt5 application. Validates FRED API key exists, creates `QApplication`, instantiates `MainWindow`, runs event loop.
- **`main()`** — Application entry point. Checks `config.FRED_API_KEY`, creates `QApplication`, shows `MainWindow`, starts event loop.
#### `__init__.py`
Package marker. Exports `__version__ = "1.0.0"`.
---
### 4.2 Configuration
#### `config.py`
Loads `.env` via `python-dotenv`. Defines ALL constants used across the application.
| Constant | Default | Description |
|----------|---------|-------------|
| `CURRENCIES` | `["USD","EUR","GBP","JPY","AUD","CAD","CHF","NZD"]` | The 8 major currencies |
| `CB_TARGETS` | Per-currency dict | Central bank inflation targets (2.0% most, AUD=2.5, CHF=1.5) |
| `FRED_SERIES` | Per-currency dict | FRED series IDs for interest rates |
| `WEIGHT_RATE` | `0.50` | Interest rate weight in L1 scoring |
| `WEIGHT_CPI` | `0.30` | CPI deviation weight in L1 scoring |
| `WEIGHT_PMI` | `0.20` | PMI composite weight in L1 scoring |
| `MIN_GAP_TO_TRADE` | `20` | Minimum score gap required for signal |
| `Z_SCORE_THRESHOLD` | `2.0` | Overbought/oversold threshold (std devs) |
| `Z_SCORE_LOOKBACK` | `20` | Bars for Z-score calculation |
| `MT5_SYMBOL_SUFFIX` | `""` | Broker-specific MT5 suffix (e.g., `.m`) |
| `ACCOUNT_BALANCE` | `10000` | Starting account balance |
| `RISK_PER_TRADE` | `0.01` | 1% risk per trade |
| `MAX_PORTFOLIO_LEVERAGE` | `2.0` | Max 2:1 leverage |
| `GRID_LEVELS` | `3` | Hedge grid levels |
| `USE_GRID_HEDGING` | `true` | Enable grid hedging |
| `DEBUG` | `false` | Debug output toggle |
- **`validate_config()`** — Validates all config on import. Raises `ValueError` if FRED key or critical settings are missing.
---
### 4.3 Data Layer
#### `data_feeder.py`
Two data feeder implementations with the **same interface** (polymorphic):
**Class `Mt5DataFeeder`** — Real data from MetaTrader 5 terminal.
| Method | Returns | Description |
|--------|---------|-------------|
| `initialize()` | `bool` | Connect to MT5 terminal |
| `test_connection()` | `bool` | Alias for initialize |
| `shutdown()` | — | Disconnect MT5 |
| `get_connection_status()` | `str` | Human-readable status |
| `get_current_price(pair)` | `dict\|None` | Bid/ask/mid via `symbol_info_tick()` |
| `fetch_all_rates()` | `dict` | Fetch 7 USD pairs, derive all 28 cross rates |
| `get_all_major_pairs()` | `list[str]` | All 28 pairs (56 permutations) |
| `get_exchange_rate(from, to)` | `float\|None` | Single cross rate |
| `get_historical_candles(...)` | `list[dict]\|None` | OHLC bars via `copy_rates_from_pos()` |
| `stream_prices(...)` | — | Threaded polling loop |
| `stop_streaming()` | — | Stop the poll loop |
**Strategy:** Fetches only 7 major USD pairs (`EUR_USD`, `GBP_USD`, `AUD_USD`, `NZD_USD`, `USD_JPY`, `USD_CAD`, `USD_CHF`), converts each to "how many USD per 1 unit", then derives all 28 cross rates mathematically. This avoids the problem that most MT5 brokers don't have symbols for exotic crosses like `AUDEUR`, `AUDGBP`, etc.
**Key internal:**
```python
USD_PAIRS = ["EUR_USD", "GBP_USD", "AUD_USD", "NZD_USD",
"USD_JPY", "USD_CAD", "USD_CHF"]
# For EUR_USD: usd_rates["EUR"] = mid_price
# For USD_JPY: usd_rates["JPY"] = 1.0 / mid_price
# Cross rate: rate[base][quote] = usd_rates[base] / usd_rates[quote]
```
**Class `MockDataFeeder`** — Simulates prices with random walk around base prices for 8 major pairs. Same interface as `Mt5DataFeeder` for testability.
---
#### `fred_client.py`
**Class `FredClient`** — Fetches interest rates from FRED API.
| Method | Returns | Description |
|--------|---------|-------------|
| `__init__(api_key, timeout)` | — | Validates API key |
| `fetch_rate(currency, max_retries)` | `float\|None` | Single currency rate with exponential backoff |
| `fetch_all_rates(max_retries)` | `dict` | All 8 currencies |
| `get_cached_rate(currency)` | `float\|None` | Cache lookup |
| `clear_cache()` | — | Reset cache |
Uses FRED series IDs from `config.FRED_SERIES`:
- USD → `FEDFUNDS`, EUR → `ECBDFR`, GBP → `BOEBR`, JPY → `IRSTJPN`
- AUD → `RBATCTR`, CAD → `BOCCRT`, CHF → `SNBPOL`, NZD → `RBNZOCR`
---
#### `database.py`
**Class `Database`** — SQLite database with 4 tables.
| Table | Columns | Purpose |
|-------|---------|---------|
| `rates` | `currency, rate, updated_at, source` | Interest rates from FRED |
| `monthly_data` | `month, currency, cpi_actual, pmi_actual, entered_at` | CPI/PMI entries |
| `scores` | `month, currency, score_rate, score_cpi, score_pmi, total_score, rank` | Calculated scores |
| `signals` | `month, strongest, weakest, gap, signal, status` | Trade signals |
All tables use `ON CONFLICT ... DO UPDATE` (upsert) for idempotent writes. Foreign keys enforced via PRAGMA.
Key methods: `upsert_rate()`, `update_monthly_cpi()`, `update_monthly_pmi()`, `save_scores()`, `save_signal()`, `get_month_scores()`, `get_all_signals()`, `get_month_completeness()`.
---
### 4.4 Business Logic — Layer 1 (Fundamental)
#### `scorer.py`
Pure functions (no classes). Implements the scoring formula:
```
Score = (Rate_Differential × 50%) + (CPI_Deviation × 30%) + (PMI × 20%)
Each component is min-max normalized to 0-100 before weighting.
```
| Function | Returns | Description |
|----------|---------|-------------|
| `normalise(values)` | `list[float]` | Min-max scaling to 0-100 |
| `calculate_rate_differentials(rates)` | `dict` | Rate minus G8 average |
| `calculate_cpi_deviations(cpi_values)` | `dict` | Actual CPI minus CB target |
| `score_all_currencies(rates, cpi, pmi)` | `dict` | Full scoring pipeline |
| `get_ranked_list(scores)` | `list[tuples]` | Sorted by score descending |
| `pair_currencies(scores)` | `(strongest, weakest, gap)` | Top vs bottom score |
| `generate_signal(scores)` | `(signal, status, gap_desc)` | "SHORT X/Y" or "NO TRADE" |
| `validate_scores(scores)` | `bool` | Validates all fields and ranges |
---
### 4.5 Business Logic — Layer 2 (Technical)
#### `layer2_technical.py`
**Class `TechnicalAnalyzer`** — Real-time Z-score engine.
Initializes 56 deques (all permutations of 8 currencies) with `maxlen=20`. Each incoming price tick appends to the deque and recalculates the Z-score.
| Method | Description |
|--------|-------------|
| `add_price_data(pair, price, volume)` | Append price, recalculate Z-score |
| `get_z_score(pair)` | Current Z-score for any pair |
| `is_extreme(pair)` | `|Z| >= 2.0` |
| `get_overbought_pairs()` | All pairs with Z >= 2.0 |
| `get_oversold_pairs()` | All pairs with Z <= -2.0 |
| `get_volatility(pair)` | Standard deviation of recent prices |
| `get_mean_price(pair)` | Mean price over lookback |
| `is_mean_reverting(pair)` | `|Z| < 0.5` |
| `get_last_price(pair)` | Most recent price |
| `get_all_z_scores()` | Dict of all 56 pair Z-scores |
| `get_status_for_pair(pair)` | Dict with label (SEVERELY OVERBOUGHT → Neutral) |
**Z-score formula:** `Z = (current_price - mean) / std_dev`
**Class `TechnicalSignal`** — Signal generation from Z-scores.
- `should_enter_on_extreme()` → True if `|Z| >= 2.0`
- `should_exit_on_mean_reversion()` → True if `|Z| < 0.5`
- `get_signal_strength()` → 0-100 scale
---
#### `currency_strength_matrix.py`
**Class `CurrencyStrengthMatrix`** — S.A.T.O.R.I. individual currency strength index.
This is the core mathematical innovation. Deconstructs all 28 pair Z-scores into 8 individual currency strength indices.
**How it works:**
For each currency, collects Z-scores from all 7 pairs where it is the **base**:
```
EUR_Strength = avg(Z(EUR_USD), Z(EUR_GBP), Z(EUR_JPY), Z(EUR_AUD), Z(EUR_CAD), Z(EUR_CHF), Z(EUR_NZD))
USD_Strength = avg(Z(USD_EUR), Z(USD_GBP), Z(USD_JPY), Z(USD_AUD), Z(USD_CAD), Z(USD_CHF), Z(USD_NZD))
```
**Output:**
| Currency | Avg Z-Score | Direction |
|----------|-------------|-----------|
| EUR | +2.3 | **OVERBOUGHT** |
| USD | +1.1 | NEUTRAL |
| ... | ... | ... |
| JPY | -2.5 | **OVERSOLD** |
The **Matrix Cross** = Strongest currency vs Weakest currency (e.g., `EUR_JPY`).
| Method | Description |
|--------|-------------|
| `update(z_scores)` | Recompute from 56 pair Z-scores |
| `get_strongest()` | Highest avg Z-score currency |
| `get_weakest()` | Lowest avg Z-score currency |
| `get_matrix_cross()` | Strongest_Weakest pair |
| `get_divergence_gap()` | strongest_z - weakest_z |
| `has_divergence()` | True if one overbought AND one oversold |
| `get_strong_currencies()` | List of overbought currencies |
| `get_weak_currencies()` | List of oversold currencies |
| `get_ranked_list()` | All 8 sorted by strength |
| `get_report()` | Dict with all matrix data |
---
#### `confluence_filter.py`
**Class `ConfluenceFilter`** — Merges all three signal sources.
**Entry logic** (two-tier):
1. **Primary — Matrix Divergence:**
- One currency overbought across ALL pairs
- Another currency oversold across ALL pairs
- Trade the Matrix Cross (strongest vs weakest)
- Confidence = spread / 4.0 × 100
2. **Secondary — Layer 1 + Layer 2:**
- Layer 1 bias (fundamental strongest/weakest)
- Layer 2 pair extreme (|Z| >= 2.0 on that specific pair)
- 50% gap confidence + 50% Z confidence
**Exit logic** (two-tier):
1. Matrix divergence gap collapses (divergence no longer exists)
2. Single-pair Z-score mean reverts below 0.5
| Method | Description |
|--------|-------------|
| `set_layer1_bias(strongest, weakest, gap)` | Store current L1 signal |
| `check_entry_confluence()` | `(bool, reason, strength)` |
| `check_exit_confluence()` | `(bool, reason)` |
| `is_conflicting()` | L1 bullish but L2 bearish |
| `get_confluence_report()` | Full report with matrix data |
| `get_all_signals()` | All ranked signals |
**Class `SignalHistory`** — Tracks up to 1000 signals with win-rate calculation.
---
#### `risk_management.py`
Five classes implementing professional risk management:
**Class `PositionSizer`**
- Risk-based position sizing: `size = (balance × 0.01) / (stop_loss × pip_value) × confidence_multiplier`
- Clamped to 0.015.0 lots
**Class `GridHedging`**
- Creates N-level hedge grid below entry price
- Each hedge level = 50% × position_size / (N-1)
**Class `PortfolioExposure`**
- Tracks all open positions
- Enforces max leverage (default 2:1)
- Rejects new positions that would exceed limit
**Class `BasketHedging`** — S.A.T.O.R.I. statistical arbitrage hedging.
- Pre-defined correlation clusters:
- `EUR_USD` → hedges with `EUR_GBP`, `EUR_JPY`, `GBP_USD`
- `GBP_USD` → hedges with `GBP_JPY`, `EUR_GBP`, `EUR_USD`
- `USD_JPY` → hedges with `USD_CHF`, `USD_CAD`, `EUR_JPY`
- `AUD_USD` → hedges with `AUD_JPY`, `NZD_USD`, `AUD_CAD`
- `NZD_USD` → hedges with `AUD_USD`, `NZD_JPY`, `NZD_CAD`
- Each correlated pair gets 30% of primary size / len(cluster)
**Class `RiskManagementSystem`** — Combines all four.
- `execute_signal()` → full trade execution with sizing + grid + basket
- `calculate_basket_pnl()` → aggregate unrealized P&L across ALL positions + hedges
- `should_exit_portfolio()` → exit when total P&L > 0 (portfolio-based, not per-pair)
- `close_all_trades()` → close all positions at given exit prices
- `get_portfolio_summary()` → positions, exposure, leverage, P&L
---
### 4.6 UI Layer
#### `main_window.py`
**Class `MainWindow(QMainWindow)`** — Application shell.
Creates 6-tab `QTabWidget`, instantiates all tabs, connects inter-tab signals.
**Data flow assembly:**
```
1. Entry tab saves data → Dashboard refreshes
2. Entry tab saves data → History tab refreshes
3. Dashboard generates signal → Confluence tab receives bias
4. Dashboard requests fetch → FredFetchWorker starts
5. FRED completes → Dashboard updates rates
```
**Class `FredFetchWorker(QThread)`** — Background FRED API fetch. Saves rates to DB, emits `rates_fetched` or `error_occurred`.
---
#### `ui/dashboard_tab.py` — Tab 1
**Class `DashboardTab(QWidget)`**
Displays Layer 1 fundamental analysis:
- **Signal card** — Large text: "SHORT JPY/USD" or "NO TRADE", gap score, tier, timestamp
- **Score table** — 8 rows × 8 columns (Rank, Currency🇺🇸, Rate%, CPI%, PMI, Score, Signal, Strength bar)
- Strongest row highlighted green with "BUY" tag
- Weakest row highlighted red with "SELL" tag
- Color-coded score bars (green/red/gray for Rate/CPI/PMI contributions)
- "Fetch Rates (FRED)" button
Signals: `fetch_rates_requested`, `signal_generated(strongest, weakest, gap)`
---
#### `ui/entry_tab.py` — Tab 2
**Class `MonthlyEntryTab(QWidget)`**
Manual data entry for CPI and PMI:
- Month selector (dropdown, 24 months)
- **CPI table**: Currency, Target%, Actual CPI (spinbox), Delta (color-coded), Done
- **PMI table**: Currency, Neutral 50, PMI (spinbox), Signal label (Expanding/Contracting), Done
- Progress bar: X/16 fields filled
- Import Excel button (supports both multi-sheet xlsx and CSV)
- Save button (enabled only when 16/16 complete)
- On save: loads rates from DB → runs `scorer.score_all_currencies()` → saves scores → generates signal → emits `data_saved`
Signals: `data_saved(month)`
---
#### `ui/layer2_monitor_tab.py` — Tab 3
**Class `Layer2MonitorTab(QWidget)`**
**Class `DataStreamerThread(QThread)`**
Real-time technical analysis with S.A.T.O.R.I. matrix:
- **Connection panel**: Source dropdown (MT5 Live / Mock Test), Connect/Disconnect, status indicator
- **Z-score table**: All 28 pairs with Price, Z-Score (red when extreme), Volatility, Mean, Status, Signal
- **Overbought/Oversold alerts**: Comma-separated lists
- **Currency Strength Matrix panel:**
- Matrix Cross label (strongest vs weakest currency)
- Divergence Gap (sigma spread)
- DIVERGENCE DETECTED alert (red) when one currency overbought + one oversold
- Ranked currency table: 8 rows × 4 columns (Rank, Currency, Strength Z, Direction)
- Color-coded: OVERBOUGHT (red), OVERSOLD (green)
- Auto-refresh checkbox, Refresh Now button
Data flow: Streamer thread polls feeder → emits `price_updated` → feeds `TechnicalAnalyzer` → recomputes `CurrencyStrengthMatrix` → refreshes display.
---
#### `ui/confluence_tab.py` — Tab 4
**Class `ConfluenceSignalsTab(QWidget)`**
Merged signal display and execution:
- **Status card**: Layer 1 bias (pair, gap), Layer 2 extreme (pair, Z-score), Matrix Cross, Top 3 → Bottom 3 ranked currencies, Confluence result with confidence %
- **Signals table**: 10 rows × 8 columns (Pair, L1 Gap, L2 Z-Score, Status, Confidence, Entry Price, Position Size, Action)
- Matrix divergence signals shown in purple, standard confluence in green
- **Risk panel**: Portfolio exposure progress bar, leverage ratio
- **Buttons**: Refresh, Execute Top Signal (runs `RiskManagementSystem`)
- Auto-refresh every 5 seconds
---
#### `ui/history_tab.py` — Tab 5
**Class `HistoryTab(QWidget)`**
Past signal history:
- Table with 6 columns: Month, Signal, Gap, Strongest (flag), Weakest (flag), Status
- Status color-coded: ACTIVE (green), NO_TRADE (red)
- Click any row → popup with full score breakdown for all 8 currencies
- Auto-refreshes when new data saved
---
#### `ui/settings_tab.py` — Tab 6
**Class `SettingsTab(QWidget)`**
**Class `FredTestWorker(QThread)`**
**Class `Mt5TestWorker(QThread)`**
Configuration interface:
- **FRED API**: Key input (masked), Test Connection button, status
- **MT5**: Symbol suffix input, Test Connection button, status
- **CB Targets**: Read-only display of all 8 targets
- **Scoring Weights**: 3 spinboxes (Rate/CPI/PMI %) with live total validation (must = 100%)
- **Trading Rules**: Minimum gap spinbox (5-100)
- **App Settings**: Auto-fetch checkbox
- **Save**: Writes .env file (requires restart)
- **Reset**: Confirmation dialog, restores defaults
---
### 4.7 Utility
#### `create_excel_template.py`
Generates example Excel/CSV files for data import testing:
- `example_monthly_data.xlsx` (multi-sheet: CPI + PMI)
- `example_monthly_data_single_sheet.xlsx` (all in one sheet)
- `example_monthly_data.csv`
Each contains 8 currencies with example values.
---
## 5. Data Flow Diagrams
### Layer 1 (Fundamental) — Monthly Cycle
```
User enters CPI/PMI
Entry Tab → Save Clicked
├──► Read all 8 CPI + 8 PMI from spinboxes
├──► Load interest rates from DB (from FRED)
├──► scorer.score_all_currencies(rates, cpi, pmi)
│ ├── normalise(rate_differentials) × 0.50
│ ├── normalise(cpi_deviations) × 0.30
│ ├── normalise(pmi_raw) × 0.20
│ └── sum → total_score 0-100
├──► scorer.generate_signal(scores)
│ ├── pair_currencies → strongest, weakest, gap
│ ├── gap >= 20 → "SHORT {weak}/{strong}"
│ └── gap < 20 → "NO TRADE"
├──► database.save_scores()
├──► database.save_signal()
└──► emit data_saved → Dashboard + History refresh
```
### Layer 2 (Technical) — Real-time with Bar Seeding
```
MT5 Terminal (or Mock)
├── On Connect:
│ generate_mock_bars(288) ◄── Mock: simulates 24h of M5 data
│ │ or
│ fetch_historical_bars(288, M5) ◄── MT5: real bars from terminal
│ │
│ ▼
│ TechnicalAnalyzer.seed_bars(bars) ◄── Populates bar_history with 288 closes
│ │ ◄── μ and σ now anchored to 24h
│ │
│ ▼
│ DataStreamerThread.start()
└──► every 1s:
fetch_all_rates() → 7 USD pairs → derive 28 crosses
├──► emit price_updated(pair, mid)
Layer2MonitorTab._on_price_received
├──► TechnicalAnalyzer.add_price_data(pair, price)
│ └── _update_z_score(pair)
│ μ, σ = _get_mean_std(pair)
│ │ priority: bar_history (288 bars) → tick_fallback (20 ticks)
│ ▼
│ Z = (current_price - μ) / σ
├──► CurrencyStrengthMatrix(z_scores)
│ └── For each currency: avg Z across 7 base pairs
└──► _refresh_display()
├── Update 28-pair Z-score table
├── Update currency strength matrix table
├── Update matrix cross / divergence alerts
├── Update overbought/oversold lists
└── Show active session (Tokyo/London/New York)
```
### Confluence — Entry Signal
```
Layer 1 (monthly) Layer 2 (real-time)
│ │
▼ ▼
dashboard_tab.signal_generated ThermalAnalyzer.z_scores
│ │
▼ ▼
ConfluenceFilter.set_layer1_bias CurrencyStrengthMatrix
│ │
└──────────┬───────────────────────┘
ConfluenceFilter.check_entry_confluence()
├── Matrix divergence? → YES → Trade matrix cross
├── L1 + L2 extreme? → YES → Trade paired pair
└── Neither? → NO TRADE
RiskManagementSystem.execute_signal()
├── PositionSizer → size = f(confidence)
├── GridHedging → 3-level hedge grid
├── BasketHedging → correlated pair hedges
└── PortfolioExposure → leverage check
```
---
## 6. Database Schema
```sql
-- Table 1: Interest rates from FRED
CREATE TABLE rates (
id INTEGER PRIMARY KEY AUTOINCREMENT,
currency TEXT NOT NULL UNIQUE,
rate REAL NOT NULL,
updated_at TEXT NOT NULL,
source TEXT DEFAULT 'FRED',
CONSTRAINT valid_currency CHECK (currency IN ('USD','EUR','GBP','JPY','AUD','CAD','CHF','NZD'))
);
-- Table 2: Monthly CPI + PMI entries
CREATE TABLE monthly_data (
id INTEGER PRIMARY KEY AUTOINCREMENT,
month TEXT NOT NULL,
currency TEXT NOT NULL,
cpi_actual REAL,
pmi_actual REAL,
entered_at TEXT NOT NULL,
UNIQUE(month, currency),
CONSTRAINT valid_currency CHECK (currency IN ('USD','EUR','GBP','JPY','AUD','CAD','CHF','NZD')),
CONSTRAINT valid_month CHECK (month LIKE '____-__')
);
-- Table 3: Calculated scores
CREATE TABLE scores (
id INTEGER PRIMARY KEY AUTOINCREMENT,
month TEXT NOT NULL,
currency TEXT NOT NULL,
score_rate REAL,
score_cpi REAL,
score_pmi REAL,
total_score REAL NOT NULL,
rank INTEGER NOT NULL,
calculated_at TEXT NOT NULL,
UNIQUE(month, currency)
);
-- Table 4: Trade signals
CREATE TABLE signals (
id INTEGER PRIMARY KEY AUTOINCREMENT,
generated_at TEXT NOT NULL,
month TEXT NOT NULL UNIQUE,
strongest TEXT NOT NULL,
weakest TEXT NOT NULL,
gap REAL NOT NULL,
signal TEXT NOT NULL,
status TEXT NOT NULL,
CONSTRAINT valid_status CHECK (status IN ('ACTIVE', 'NO_TRADE', 'CLOSED'))
);
```
---
## 7. Configuration (.env)
```env
FRED_API_KEY=your_fred_api_key
MT5_SYMBOL_SUFFIX=
DB_PATH=apex.db
DEBUG=true
WEIGHT_RATE=0.50
WEIGHT_CPI=0.30
WEIGHT_PMI=0.20
MIN_GAP=20.0
AUTO_FETCH_RATES_ON_STARTUP=true
Z_SCORE_THRESHOLD=2.0
Z_SCORE_LOOKBACK=20
ACCOUNT_BALANCE=10000.0
RISK_PER_TRADE=0.01
MAX_PORTFOLIO_LEVERAGE=2.0
USE_GRID_HEDGING=true
GRID_LEVELS=3
```
---
## 8. Technology Stack
| Component | Technology | Version |
|-----------|-----------|---------|
| Language | Python | 3.10+ |
| UI Framework | PyQt5 | 5.15.9 |
| Database | SQLite | Built-in |
| HTTP Client | requests | 2.31+ |
| Data Processing | pandas | 2.1+ |
| Excel Support | openpyxl | 3.1+ |
| Environment | python-dotenv | 1.0+ |
| Forex Data | MetaTrader5 | Latest |
| Interest Rates | FRED API | Free tier |
| Packaging | PyInstaller | 6.1+ |
---
## 9. Scoring Formula Reference
### Layer 1 — Fundamental Score
```
rate_diff[i] = rate[i] - G8_average_rate
cpi_dev[i] = actual_cpi[i] - cb_target[i]
pmi_raw[i] = pmi_value[i]
normalize(x) = (x - min) / (max - min) × 100 // 0-100 scale
score_total[i] = normalise(rate_diff)[i] × 0.50
+ normalise(cpi_dev)[i] × 0.30
+ normalise(pmi_raw)[i] × 0.20
gap = score_total[strongest] - score_total[weakest]
```
### Layer 2 — Technical Score (Bar-Anchored)
```
Step 1: Seed bar_history with 288 M5 close prices (24 hours)
Step 2: μ_bars = mean(bar_history), σ_bars = stdev(bar_history)
Step 3: For each incoming tick:
Z[pair] = (current_tick_price - μ_bars) / σ_bars
Fallback (if bar_history empty):
Z[pair] = (current_tick_price - mean(ticks)) / stdev(ticks)
Step 4: Individual Currency Strength = avg(Z[currency_X] over all 7 base pairs)
Step 5: Session Relative Velocity (SRV):
At session open (Tokyo/London/NY), snapshot all prices.
SRV[pair] = ((current_price - session_open_price) / session_open_price) × 100
```
### Entry Conditions
```
Matrix Divergence: any(avg_Z > +2.0) AND any(avg_Z < -2.0) → Trade Matrix Cross
Pair Confluence: L1_gap >= 20 AND L2_Z >= 2.0 on same pair → Trade that pair
```
---
## 10. 28 Currency Pairs (Generated)
All 8 currencies produce 56 permutations (28 pairs × 2 directions):
| Base | Pairs (base_quote) |
|------|--------------------|
| USD | USD_EUR, USD_GBP, USD_JPY, USD_AUD, USD_CAD, USD_CHF, USD_NZD |
| EUR | EUR_USD, EUR_GBP, EUR_JPY, EUR_AUD, EUR_CAD, EUR_CHF, EUR_NZD |
| GBP | GBP_USD, GBP_EUR, GBP_JPY, GBP_AUD, GBP_CAD, GBP_CHF, GBP_NZD |
| JPY | JPY_USD, JPY_EUR, JPY_GBP, JPY_AUD, JPY_CAD, JPY_CHF, JPY_NZD |
| AUD | AUD_USD, AUD_EUR, AUD_GBP, AUD_JPY, AUD_CAD, AUD_CHF, AUD_NZD |
| CAD | CAD_USD, CAD_EUR, CAD_GBP, CAD_JPY, CAD_AUD, CAD_CHF, CAD_NZD |
| CHF | CHF_USD, CHF_EUR, CHF_GBP, CHF_JPY, CHF_AUD, CHF_CAD, CHF_NZD |
| NZD | NZD_USD, NZD_EUR, NZD_GBP, NZD_JPY, NZD_AUD, NZD_CAD, NZD_CHF |
Each currency's individual strength is computed from its 7 base pairs.
---
## 11. Refactoring Changelog (Session-Based Quantitative Engine)
### Task 1.1 — Statistical Lookback Window (config.py, layer2_technical.py)
- `config.py`: Added `BAR_TIMEFRAME`, `BAR_LOOKBACK_HOURS`, `BAR_LOOKBACK_BARS`, `HISTORICAL_POLL_INTERVAL` constants. Default lookback changed from 20 ticks to 288 bars (24h of M5 data).
- `layer2_technical.py`: `TechnicalAnalyzer` now maintains **two data streams**:
- `bar_history` (deque of M1/M5 close prices, length = `BAR_LOOKBACK_BARS`) — the multi-hour statistical anchor
- `price_history` (short deque of tick/poll data) — for UI display
- `_get_bar_mean_std()` computes μ/σ from bar history only
- `_update_z_score()` uses `Z = (current_tick - μ_bars) / σ_bars`
- `add_bar()` method for feeding completed M1/M5 candles into the historical frame
### Task 1.2 — Session-Based Indexing (currency_strength_matrix.py)
- New `SessionTracker` class:
- Detects active session from UTC hour (Tokyo 00-08, London 07-16, New York 13-22)
- On session open, snapshots start prices for all 28 pairs
- Computes **Session Relative Velocity (SRV)**: `% change = (current - session_start) / session_start × 100`
- `CurrencyStrengthMatrix.update()` now accepts `current_prices` dict for session tracking
- `CurrencyStrength` dataclass has new `session_srv: float` field
- `get_report()` includes `active_session` key
### Task 2.1 — Live Order Book Subscriptions (data_feeder.py)
- `Mt5DataFeeder.get_order_book(pair)` — fetches live bid/ask/spread via `mt5.symbol_info_tick()` + `mt5.symbol_info()` for the exact trade symbol
- `PositionSizer.calculate_position_size()` accepts optional `bid`, `ask`, `spread` params; wide spreads reduce position size by up to 20%
- `MockDataFeeder` has matching `get_order_book()` implementation
### Task 2.2 — SQLite WAL Mode + Bar Cache (database.py)
- Connection now sets: `PRAGMA journal_mode=WAL`, `PRAGMA synchronous=NORMAL` for concurrent read/write performance
- New `bar_cache` table: `(id, pair, timeframe, bar_time, open, high, low, close, volume)` with unique constraint on `(pair, timeframe, bar_time)` and compound index
- New methods: `upsert_bar()`, `get_bars()`, `get_latest_bar_time()`
### Task 3.1 — Layer 1 as Directional Regime Filter (confluence_filter.py)
- `check_entry_confluence()` now uses Layer 1 as a **Directional Regime Filter**:
- Primary signal: Matrix divergence (self-sufficient)
- Secondary: Layer 2 extremes only valid if **aligned** with Layer 1 macro bias
- Contrarian Layer 2 signals (Z < -threshold opposite Layer 1 direction) → **BLOCKED** with reason
- Aligned signals capped at 70% confidence (downgraded vs matrix divergence)
- `layer1_is_active` flag replaces raw gap comparison
### Task 3.2 — Dynamic Pearson Correlation (risk_management.py, data_feeder.py)
- New `pearson_correlation(x, y)` function: `r = Σ(x-x̄)(y-ȳ) / √(Σ(x-x̄)² · Σ(y-ȳ)²)`
- New `CorrelationEngine` class:
- `update_series(historical_closes)` — feeds 30 days of close prices
- `get_correlation(pair_a, pair_b)` — computes/caches r between any two pairs
- `get_top_correlated(target, n=3, min_r=0.75)` — returns top N pairs with |r| ≥ 0.75
- `BasketHedging.get_correlated_pairs()` now delegates to `CorrelationEngine` instead of hardcoded dict
- `Mt5DataFeeder.fetch_historical_closes_all_pairs(days=30)` fetches the required data
- `MockDataFeeder` has matching implementation
### Task 3.3 — Aggregate Portfolio Profit Target Exit (risk_management.py)
- `get_dynamic_exit_target()` — confidence-scaled profit target (base = 1% of equity, scales with avg confidence)
- Background monitor thread `_monitor_exit_loop()` polls `calculate_basket_pnl()` every second
- When net aggregate P&L > dynamic target, fires `close_all_trades()` via registered callbacks
- `start_exit_monitor()`, `stop_exit_monitor()`, `on_portfolio_exit()` lifecycle management
### Task 4.1 — Session Visualizations + σ Highlights (ui/layer2_monitor_tab.py)
- Active session indicator label with color-coded background: Tokyo (purple), London (blue), New York (orange), Off-Hours (gray)
- Currency Strength Matrix Z-score cells: solid red background with white text for ≥ +2.0σ, solid green with white text for ≤ -2.0σ
- New 5th column in matrix table: "Session SRV" showing percentage change since session open
- Emoji indicators removed from status labels for cleaner display
---
## 12. Bug Fixes & Stability (Round 2)
### Fix 1 — Bar History Never Seeded (Z-scores always 0.0)
- `layer2_technical.py`: Added `seed_bars(historical_bars)` method to populate `bar_history` with 288 M5 close prices on connect
- `data_feeder.py (Mock)`: Added `generate_mock_bars(n_bars=288)` — generates 24h of simulated M5 data using an Ornstein-Uhlenbeck process (mean reversion + drift + noise) for all 28 pairs via USD pair derivation
- `ui/layer2_monitor_tab.py`: `_connect()` calls `_seed_historical_bars()` before starting the streamer — bars are always seeded first
### Fix 2 — Tick Price Anchored to Initial Base, Not Bar Data
- `data_feeder.py (Mock)`: `_tick_price()` now uses the **last bar close** as its anchor with ±0.0002 noise, instead of the initial base price with ±0.01 noise
- `_current_bar_prices()` returns the last cached bar close for each pair
- This ensures Z-scores reflect the bar position relative to 24h history, not random tick noise
### Fix 3 — Default Source Changed to Mock
- `ui/layer2_monitor_tab.py`: `source_combo` defaults to `"Mock (Test)"` at index 0 to prevent unintended MT5 terminal connections on startup
### Fix 4 — Persistent Matrix Instance
- `ui/layer2_monitor_tab.py`: `CurrencyStrengthMatrix` is now a persistent `self.matrix` instance, recreated only once. `update()` is called each refresh instead of creating a new object, preserving `SessionTracker` state across refreshes
### Fix 5 — FRED Series IDs Updated
- `config.py`: Updated 6 invalid/deprecated FRED series IDs (`BOEBR`, `IRSTJPN`, `RBATCTR`, `BOCCRT`, `SNBPOL`, `RBNZOCR`) to commonly used alternatives (`BOEIR`, `IRSTCI01JPM156N`, `RBATR`, `BOCARR`, `SNBON`, `RBNZR`)
+2
View File
@@ -5,3 +5,5 @@ pandas==2.1.3
openpyxl==3.1.2
pyinstaller==6.1.0
MetaTrader5==5.0.45
pytest==8.3.4
pytest-cov==6.0.0
+17 -12
View File
@@ -263,6 +263,7 @@ class RiskManagementSystem:
def __init__(self, account_balance: float = 10000.0):
self.account_balance = account_balance
self._lock = threading.Lock()
self.sizer = PositionSizer(account_balance, risk_per_trade=0.01)
self.hedger = GridHedging(grid_levels=config.GRID_LEVELS)
self.portfolio = PortfolioExposure(max_portfolio_leverage=config.MAX_PORTFOLIO_LEVERAGE)
@@ -337,8 +338,9 @@ class RiskManagementSystem:
n_hedges = len(trade.get('basket_hedges', []))
print(f"[Risk] Created {n_hedges} dynamic basket hedges for {pair}")
self.portfolio.add_position(pair, position_size)
self.trades.append(trade)
with self._lock:
self.portfolio.add_position(pair, position_size)
self.trades.append(trade)
return trade
def calculate_basket_pnl(self) -> float:
@@ -437,21 +439,24 @@ class RiskManagementSystem:
return False, total_pnl
def close_trade(self, pair: str, exit_price: float) -> Optional[Dict]:
result = self.sizer.close_position(pair, exit_price)
if result:
self.portfolio.remove_position(pair)
with self._lock:
result = self.sizer.close_position(pair, exit_price)
if result:
self.portfolio.remove_position(pair)
return result
def close_all_trades(self, exit_prices: Dict[str, float]):
"""Close all open trades at given exit prices."""
results = []
for trade in list(self.trades):
if trade['status'] == 'OPEN':
price = exit_prices.get(trade['pair'], trade['entry_price'])
result = self.close_trade(trade['pair'], price)
if result:
results.append(result)
self.stop_exit_monitor()
with self._lock:
for trade in list(self.trades):
if trade['status'] == 'OPEN':
price = exit_prices.get(trade['pair'], trade['entry_price'])
result = self.sizer.close_position(trade['pair'], price)
if result:
self.portfolio.remove_position(trade['pair'])
results.append(result)
self.stop_exit_monitor()
return results
def get_portfolio_summary(self) -> Dict:
-48
View File
@@ -338,51 +338,3 @@ def validate_scores(scores: Dict[str, Dict]) -> bool:
return True
# Example usage (for testing)
if __name__ == "__main__":
# Mock data
test_rates = {
"USD": 5.25,
"EUR": 4.50,
"GBP": 5.25,
"JPY": 0.10,
"AUD": 4.35,
"CAD": 5.00,
"CHF": 1.75,
"NZD": 5.50,
}
test_cpi = {
"USD": 3.2,
"EUR": 2.6,
"GBP": 3.4,
"JPY": 2.8,
"AUD": 3.8,
"CAD": 2.8,
"CHF": 1.8,
"NZD": 3.5,
}
test_pmi = {
"USD": 54.2,
"EUR": 48.9,
"GBP": 52.1,
"JPY": 51.4,
"AUD": 46.2,
"CAD": 49.2,
"CHF": 49.8,
"NZD": 47.1,
}
# Score all currencies
scores = score_all_currencies(test_rates, test_cpi, test_pmi)
print("Scores:")
for currency, score_data in sorted(scores.items(), key=lambda x: x[1]['rank']):
print(f" {currency}: {score_data}")
# Generate signal
signal, status, gap_desc = generate_signal(scores)
print(f"\nSignal: {signal}")
print(f"Status: {status}")
print(f"Gap: {gap_desc}")
View File
+107
View File
@@ -0,0 +1,107 @@
import sys
import math
sys.path.insert(0, ".")
import pytest
from currency_strength_matrix import CurrencyStrengthMatrix, SessionTracker
def build_synthetic_z_scores(base_vals: dict, quote_vals: dict) -> dict:
z_scores = {}
currencies = list(base_vals.keys())
for base in currencies:
for quote in currencies:
if base == quote:
continue
pair = f"{base}_{quote}"
z_scores[pair] = base_vals[base] - quote_vals[quote]
return z_scores
class TestCurrencyStrengthMatrix:
def test_strongest_weakest_detection(self):
base = {"EUR": 2.0, "USD": 1.0, "GBP": 0.5, "JPY": -1.0}
quote = {"EUR": 0.0, "USD": 0.0, "GBP": 0.0, "JPY": 0.0}
z = build_synthetic_z_scores(base, quote)
matrix = CurrencyStrengthMatrix(z)
assert matrix.get_strongest().name == "EUR"
assert matrix.get_weakest().name == "JPY"
def test_divergence_detected(self):
base = {"EUR": 2.5, "USD": 0.0, "GBP": 0.0, "JPY": -2.5}
quote = {"EUR": 0.0, "USD": 0.0, "GBP": 0.0, "JPY": 0.0}
z = build_synthetic_z_scores(base, quote)
matrix = CurrencyStrengthMatrix(z)
assert matrix.has_divergence() is True
assert matrix.get_matrix_cross() == "EUR_JPY"
assert matrix.get_divergence_gap() == pytest.approx(5.0, abs=0.01)
def test_no_divergence_within_threshold(self):
base = {"EUR": 1.5, "USD": 0.0, "GBP": 0.0, "JPY": -1.5}
quote = {"EUR": 0.0, "USD": 0.0, "GBP": 0.0, "JPY": 0.0}
z = build_synthetic_z_scores(base, quote)
matrix = CurrencyStrengthMatrix(z)
assert matrix.has_divergence() is False
def test_ranked_list_order(self):
base = {"EUR": 3.0, "USD": 1.0, "GBP": 2.0, "JPY": 0.0}
quote = {"EUR": 0.0, "USD": 0.0, "GBP": 0.0, "JPY": 0.0}
z = build_synthetic_z_scores(base, quote)
matrix = CurrencyStrengthMatrix(z)
ranked = matrix.get_ranked_list()
names = [c.name for c in ranked]
assert names == ["EUR", "GBP", "USD", "JPY"]
def test_sign_error_base_quote_inversion(self):
base = {"EUR": 0.0, "USD": 0.0}
quote = {"EUR": 0.0, "USD": 0.0}
z = build_synthetic_z_scores(base, quote)
z["EUR_USD"] = 2.0
z["USD_EUR"] = -2.0
matrix = CurrencyStrengthMatrix(z)
s = matrix.get_strongest()
w = matrix.get_weakest()
assert s is not None and s.name == "EUR"
assert w is not None and w.name == "USD"
def test_get_report_structure(self):
base = {"EUR": 2.0, "USD": 1.0, "GBP": 0.0, "JPY": -2.0}
quote = {"EUR": 0.0, "USD": 0.0, "GBP": 0.0, "JPY": 0.0}
z = build_synthetic_z_scores(base, quote)
matrix = CurrencyStrengthMatrix(z)
report = matrix.get_report()
assert "ranked" in report
assert "strongest" in report
assert "weakest" in report
assert "matrix_cross" in report
assert "divergence_gap" in report
assert "has_divergence" in report
assert report["has_divergence"] is False
class TestSessionTracker:
def test_active_session_returns_string(self):
tracker = SessionTracker()
session = tracker.get_active_session()
assert session in ("Tokyo", "London", "New York", "Off-Hours")
def test_check_new_session_snapshots_prices(self):
tracker = SessionTracker()
prices = {"EUR_USD": 1.08, "USD_JPY": 149.0}
result = tracker.check_new_session(prices)
assert result is not None or tracker.current_session is not None
def test_compute_srv_zero_without_snapshot(self):
tracker = SessionTracker()
srv = tracker.compute_srv("EUR_USD", 1.09)
assert srv == 0.0
class TestDivergenceGap:
def test_gap_calculation(self):
base = {"EUR": 3.0, "USD": 0.0, "JPY": -3.0, "GBP": 0.0}
quote = {"EUR": 0.0, "USD": 0.0, "JPY": 0.0, "GBP": 0.0}
z = build_synthetic_z_scores(base, quote)
matrix = CurrencyStrengthMatrix(z)
gap = matrix.get_divergence_gap()
assert gap == pytest.approx(6.0, abs=0.01)
+106 -112
View File
@@ -13,7 +13,7 @@ from PyQt5.QtWidgets import (
QPushButton, QFrame, QMessageBox, QProgressBar
)
from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtGui import QColor, QFont, QBrush
from PyQt5.QtGui import QColor, QFont
from typing import Dict, Optional
from datetime import datetime
import config
@@ -31,7 +31,7 @@ class ConfluenceSignalsTab(QWidget):
self.db = db
self.tech_analyzer = tech_analyzer
self.confluence = ConfluenceFilter(tech_analyzer)
self.confluence = ConfluenceFilter(tech_analyzer, db=db)
self.risk_mgmt = RiskManagementSystem(account_balance=config.ACCOUNT_BALANCE)
self.signal_history = SignalHistory()
@@ -41,24 +41,25 @@ class ConfluenceSignalsTab(QWidget):
def _init_ui(self):
"""Build UI layout."""
layout = QVBoxLayout()
layout.setSpacing(12)
# ====== Confluence Status Card ======
card = self._build_status_card()
layout.addWidget(card)
layout.addSpacing(15)
# ====== Active Signals Table ======
layout.addWidget(QLabel("Confluence Signals (Layer 1 + Layer 2)"))
heading = QLabel("Confluence Signals (Layer 1 + Layer 2)")
heading.setProperty("heading", True)
layout.addWidget(heading)
self.signals_table = QTableWidget()
self.signals_table.setColumnCount(8)
self.signals_table.setHorizontalHeaderLabels([
"Pair", "L1 Gap", "L2 Z-Score", "Status", "Confidence", "Entry Price", "Position Size", "Action"
"Pair", "Signal", "Entry", "SL", "TP", "L1 Gap", "Z-Score", "Confidence"
])
self.signals_table.setRowCount(10)
layout.addWidget(self.signals_table)
layout.addSpacing(15)
# ====== Risk Management Panel ======
risk_layout = QHBoxLayout()
@@ -66,22 +67,26 @@ class ConfluenceSignalsTab(QWidget):
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)
layout.addSpacing(10)
# ====== Control Buttons ======
button_layout = QHBoxLayout()
refresh_btn = QPushButton("Refresh Signals")
refresh_btn.setObjectName("secondary")
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)
@@ -94,70 +99,62 @@ class ConfluenceSignalsTab(QWidget):
def _build_status_card(self) -> QFrame:
"""Build confluence status card."""
card = QFrame()
card.setStyleSheet("""
QFrame {
background-color: #f8f9fa;
border: 2px solid #dee2e6;
border-radius: 8px;
padding: 15px;
}
""")
card.setObjectName("statusCard")
layout = QVBoxLayout()
layout.setSpacing(8)
title = QLabel("CONFLUENCE STATUS")
title.setFont(QFont("Arial", 10, QFont.Bold))
title.setProperty("subheading", True)
layout.addWidget(title)
layout.addSpacing(5)
# Layer 1 status
layer1_layout = QHBoxLayout()
layer1_layout.addWidget(QLabel("Layer 1 (Fundamental):"))
l1 = QHBoxLayout()
l1.addWidget(QLabel("Layer 1 (Fundamental):"))
self.layer1_status_label = QLabel("No bias")
self.layer1_status_label.setFont(QFont("Arial", 11, QFont.Bold))
layer1_layout.addWidget(self.layer1_status_label)
layer1_layout.addStretch()
layout.addLayout(layer1_layout)
self.layer1_status_label.setStyleSheet("font-weight: 700;")
l1.addWidget(self.layer1_status_label)
l1.addStretch()
layout.addLayout(l1)
# Directional Bias Matrix display
bias_layout = QHBoxLayout()
bias_layout.addWidget(QLabel("Macro Boundaries:"))
bl = QHBoxLayout()
bl.addWidget(QLabel("Macro Boundaries:"))
self.bias_matrix_label = QLabel("No bias matrix")
self.bias_matrix_label.setStyleSheet("color: #8e44ad; font-size: 10px;")
bias_layout.addWidget(self.bias_matrix_label)
bias_layout.addStretch()
layout.addLayout(bias_layout)
self.bias_matrix_label.setStyleSheet("color: #8e44ad; font-size: 12px;")
bl.addWidget(self.bias_matrix_label)
bl.addStretch()
layout.addLayout(bl)
# Layer 2 status
layer2_layout = QHBoxLayout()
layer2_layout.addWidget(QLabel("Layer 2 (Technical):"))
l2 = QHBoxLayout()
l2.addWidget(QLabel("Layer 2 (Technical):"))
self.layer2_status_label = QLabel("No extreme")
self.layer2_status_label.setStyleSheet("color: #95a5a6;")
layer2_layout.addWidget(self.layer2_status_label)
layer2_layout.addStretch()
layout.addLayout(layer2_layout)
l2.addWidget(self.layer2_status_label)
l2.addStretch()
layout.addLayout(l2)
# Confluence result
conf_layout = QHBoxLayout()
conf_layout.addWidget(QLabel("Confluence Result:"))
self.confluence_status_label = QLabel(" NO CONFLUENCE")
self.confluence_status_label.setStyleSheet("color: #e74c3c; font-weight: bold;")
self.confluence_status_label.setFont(QFont("Arial", 12, QFont.Bold))
conf_layout.addWidget(self.confluence_status_label)
conf_layout.addStretch()
layout.addLayout(conf_layout)
cf = QHBoxLayout()
cf.addWidget(QLabel("Confluence Result:"))
self.confluence_status_label = QLabel(" NO CONFLUENCE")
self.confluence_status_label.setStyleSheet("color: #e74c3c; font-weight: 700; font-size: 14px;")
cf.addWidget(self.confluence_status_label)
cf.addStretch()
layout.addLayout(cf)
# Matrix cross (S.A.T.O.R.I.)
matrix_layout = QHBoxLayout()
matrix_layout.addWidget(QLabel("Matrix Cross:"))
mx = QHBoxLayout()
mx.addWidget(QLabel("Matrix Cross:"))
self.matrix_cross_label = QLabel("")
self.matrix_cross_label.setStyleSheet("font-weight: bold; color: #8e44ad;")
matrix_layout.addWidget(self.matrix_cross_label)
matrix_layout.addStretch()
layout.addLayout(matrix_layout)
self.matrix_cross_label.setStyleSheet("font-weight: 700; color: #8e44ad;")
mx.addWidget(self.matrix_cross_label)
mx.addStretch()
layout.addLayout(mx)
self.matrix_detail_label = QLabel("")
self.matrix_detail_label.setStyleSheet("color: #7f8c8d; font-size: 10px;")
self.matrix_detail_label.setStyleSheet("color: #7f8c8d; font-size: 11px;")
layout.addWidget(self.matrix_detail_label)
card.setLayout(layout)
@@ -170,16 +167,20 @@ class ConfluenceSignalsTab(QWidget):
self._refresh_signals()
def _refresh_signals(self):
"""Refresh confluence signal display."""
"""Refresh confluence signal display — single matrix build."""
try:
report = self.confluence.get_confluence_report()
cf = self.confluence
should_enter, reason, strength, sl_tp = cf.check_entry_confluence()
m = cf.matrix
mr = m.get_report() if m else {}
bias = getattr(cf, 'bias_matrix', {})
# Update bias matrix display
bias = report.get("bias_matrix", {})
if bias:
parts = []
for ccy in config.CURRENCIES:
d = bias.get(ccy, "")
d = bias.get(ccy, {}).get("direction", "")
if d == "STRONG":
parts.append(f"{ccy}")
elif d == "WEAK":
@@ -187,23 +188,18 @@ class ConfluenceSignalsTab(QWidget):
else:
parts.append(f"{ccy}")
self.bias_matrix_label.setText(" ".join(parts))
self.bias_matrix_label.setStyleSheet("color: #8e44ad; font-size: 10px;")
else:
self.bias_matrix_label.setText("No bias matrix")
self.bias_matrix_label.setStyleSheet("color: #95a5a6; font-size: 10px;")
# Update matrix cross display
mc = report.get("matrix_cross", "")
gap = report.get("divergence_gap", 0)
has_div = report.get("has_matrix_divergence", False)
ranked = report.get("matrix_ranked", [])
mc = mr.get("matrix_cross", "")
gap = mr.get("divergence_gap", 0)
has_div = mr.get("has_divergence", False)
ranked = mr.get("ranked", [])
if mc and mc != "N/A":
self.matrix_cross_label.setText(f"{mc} (spread: {gap:.2f}σ)")
if has_div:
self.matrix_cross_label.setStyleSheet("font-weight: bold; color: #e74c3c;")
else:
self.matrix_cross_label.setStyleSheet("font-weight: bold; color: #8e44ad;")
color = "#e74c3c" if has_div else "#8e44ad"
self.matrix_cross_label.setStyleSheet(f"font-weight: bold; color: {color};")
else:
self.matrix_cross_label.setText("")
self.matrix_cross_label.setStyleSheet("font-weight: bold; color: #95a5a6;")
@@ -217,36 +213,27 @@ class ConfluenceSignalsTab(QWidget):
else:
self.matrix_detail_label.setText("")
# Check for confluence
should_enter, reason, strength = self.confluence.check_entry_confluence()
# Update status
if should_enter:
l1s = self.confluence.layer1_strongest or ""
l1w = self.confluence.layer1_weakest or ""
self.layer1_status_label.setText(f"🟢 {l1s}/{l1w} (Gap: {self.confluence.layer1_gap:.1f})")
l1s = cf.layer1_strongest or ""
l1w = cf.layer1_weakest or ""
self.layer1_status_label.setText(f"PAR {l1s}/{l1w} (Gap: {cf.layer1_gap:.1f})")
pair = f"{self.confluence.layer1_strongest}_{self.confluence.layer1_weakest}" if self.confluence.layer1_strongest else ""
pair = f"{cf.layer1_strongest}_{cf.layer1_weakest}" if cf.layer1_strongest else ""
z_score = self.tech_analyzer.get_z_score(pair) if pair != "" else 0
self.layer2_status_label.setText(f"🔴 {pair} Z-score: {z_score:.2f}")
self.layer2_status_label.setText(f"TCH {pair} Z-score: {z_score:.2f}")
self.layer2_status_label.setStyleSheet("color: #27ae60;")
if has_div:
self.confluence_status_label.setText(f"✅ MATRIX DIVERGENCE: {strength:.0f}%")
else:
self.confluence_status_label.setText(f"✅ CONFLUENCE: {strength:.0f}% confidence")
label_text = f"✅ MATRIX DIVERGENCE: {strength:.0f}%" if has_div else f"✅ CONFLUENCE: {strength:.0f}%"
self.confluence_status_label.setText(label_text)
self.confluence_status_label.setStyleSheet("color: #27ae60; font-weight: bold;")
self._populate_signal_table(strength)
else:
self.layer1_status_label.setText("No signal")
self.layer2_status_label.setText("No extreme")
self.layer2_status_label.setStyleSheet("color: #95a5a6;")
self.confluence_status_label.setText("❌ NO CONFLUENCE")
self.confluence_status_label.setText("✕ NO CONFLUENCE")
self.confluence_status_label.setStyleSheet("color: #e74c3c; font-weight: bold;")
# Update risk metrics
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))
@@ -256,7 +243,6 @@ class ConfluenceSignalsTab(QWidget):
print(f"[Confluence] Error refreshing: {e}")
def _populate_signal_table(self, confluence_strength: float):
"""Populate the signals table with matrix and confluence data."""
report = self.confluence.get_confluence_report()
signals = self.confluence.get_all_signals()
@@ -267,42 +253,50 @@ class ConfluenceSignalsTab(QWidget):
if row >= self.signals_table.rowCount():
break
pair_item = QTableWidgetItem(pair_key)
pair_item.setFlags(pair_item.flags() & ~Qt.ItemIsEditable)
if signal.get('type') == 'MATRIX_DIVERGENCE':
pair_item.setForeground(QColor("#8e44ad"))
self.signals_table.setItem(row, 0, pair_item)
def _item(text, align=True):
item = QTableWidgetItem(str(text))
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
if align:
item.setTextAlignment(Qt.AlignCenter)
return item
# L1 Gap (from report)
gap_item = QTableWidgetItem(f"{report.get('layer1_gap', 0):.1f}")
gap_item.setFlags(gap_item.flags() & ~Qt.ItemIsEditable)
self.signals_table.setItem(row, 1, gap_item)
# 0: Pair
self.signals_table.setItem(row, 0, _item(pair_key, False))
# L2 Z-Score
# 1: Signal CALL/SHORT
direction = signal.get('direction', '')
signal_text = "CALL" if direction == 'LONG' else "SHORT"
sig_item = _item(signal_text)
sig_item.setForeground(QColor("#27ae60") if signal_text == "CALL" else QColor("#e74c3c"))
sig_item.setFont(QFont("Segoe UI", 11, QFont.Bold))
self.signals_table.setItem(row, 1, sig_item)
# 2: Entry
entry = signal.get('entry')
self.signals_table.setItem(row, 2, _item(f"{entry:.5f}" if entry else ""))
# 3: SL
sl = signal.get('sl')
self.signals_table.setItem(row, 3, _item(f"{sl:.5f}" if sl else ""))
# 4: TP
tp = signal.get('tp')
self.signals_table.setItem(row, 4, _item(f"{tp:.5f}" if tp else ""))
# 5: L1 Gap
self.signals_table.setItem(row, 5, _item(f"{report.get('layer1_gap', 0):.1f}"))
# 6: Z-Score
z = report.get('layer2_z_score', 0)
if signal.get('type') == 'MATRIX_DIVERGENCE':
z = report.get('matrix_cross_z', 0)
z_item = QTableWidgetItem(f"{z:.2f}")
z_item.setFlags(z_item.flags() & ~Qt.ItemIsEditable)
self.signals_table.setItem(row, 2, z_item)
self.signals_table.setItem(row, 6, _item(f"{z:.2f}"))
# Status
sig_type = signal.get('type', 'SIGNAL').replace('_', ' ')
status_item = QTableWidgetItem(sig_type)
if 'DIVERGENCE' in sig_type:
status_item.setBackground(QColor("#f3e5f5"))
status_item.setForeground(QColor("#6a1b9a"))
else:
status_item.setBackground(QColor("#e8f5e9"))
status_item.setFlags(status_item.flags() & ~Qt.ItemIsEditable)
self.signals_table.setItem(row, 3, status_item)
# Confidence
# 7: Confidence
strength = signal.get('strength', confluence_strength)
conf_item = QTableWidgetItem(f"{strength:.0f}%")
conf_item.setFont(QFont("Arial", 10, QFont.Bold))
conf_item.setFlags(conf_item.flags() & ~Qt.ItemIsEditable)
self.signals_table.setItem(row, 4, conf_item)
conf_item = _item(f"{strength:.0f}%")
conf_item.setFont(QFont("Segoe UI", 10, QFont.Bold))
self.signals_table.setItem(row, 7, conf_item)
row += 1
+99 -77
View File
@@ -19,10 +19,10 @@ Display:
from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QTableWidget, QTableWidgetItem,
QFrame, QPushButton, QSpinBox
QFrame, QPushButton, QProgressBar, QHeaderView
)
from PyQt5.QtCore import Qt, pyqtSignal, QSize
from PyQt5.QtGui import QColor, QFont, QBrush, QPixmap
from PyQt5.QtCore import Qt, pyqtSignal
from PyQt5.QtGui import QColor, QFont
from typing import Dict, Optional
from datetime import datetime
import config
@@ -57,14 +57,16 @@ class DashboardTab(QWidget):
def _init_ui(self):
"""Build the UI layout."""
layout = QVBoxLayout()
layout.setSpacing(12)
# ====== Signal Card ======
signal_card = self._build_signal_card()
layout.addWidget(signal_card)
layout.addSpacing(15)
# ====== Ranked Score Table ======
layout.addWidget(QLabel("Currency Rankings"))
heading = QLabel("Currency Rankings")
heading.setProperty("heading", True)
layout.addWidget(heading)
self.score_table = QTableWidget()
self.score_table.setColumnCount(8)
@@ -76,22 +78,35 @@ class DashboardTab(QWidget):
self.score_table.setSelectionBehavior(QTableWidget.SelectRows)
self.score_table.setSelectionMode(QTableWidget.SingleSelection)
# Store progress bars for strength column (column 7)
self.strength_bars = {}
# Pre-fill with placeholder rows
for row in range(len(config.CURRENCIES)):
for col in range(8):
item = QTableWidgetItem("")
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
self.score_table.setItem(row, col, item)
# Add progress bar for strength column
bar = QProgressBar()
bar.setRange(0, 100)
bar.setValue(0)
bar.setTextVisible(True)
bar.setFormat("")
self.score_table.setCellWidget(row, 7, bar)
self.strength_bars[row] = bar
self.score_table.resizeColumnsToContents()
for col in range(7):
self.score_table.horizontalHeader().setSectionResizeMode(col, QHeaderView.Stretch)
self.score_table.setColumnWidth(7, 140)
layout.addWidget(self.score_table)
layout.addSpacing(15)
# ====== Refresh Button ======
button_layout = QHBoxLayout()
button_layout.addStretch()
self.refresh_btn = QPushButton("Refresh")
self.refresh_btn.setObjectName("secondary")
self.refresh_btn.clicked.connect(self._refresh_display)
button_layout.addWidget(self.refresh_btn)
@@ -107,42 +122,41 @@ class DashboardTab(QWidget):
def _build_signal_card(self) -> QFrame:
"""Build the signal card frame."""
card = QFrame()
card.setStyleSheet("""
QFrame {
background-color: #f8f9fa;
border: 2px solid #dee2e6;
border-radius: 8px;
padding: 15px;
}
""")
card.setObjectName("statusCard")
layout = QVBoxLayout()
layout.setSpacing(6)
# Title
title = QLabel("PRIMARY SIGNAL")
title.setFont(QFont("Arial", 10, QFont.Bold))
title.setStyleSheet("color: #495057;")
title.setProperty("subheading", True)
layout.addWidget(title)
layout.addSpacing(5)
# Signal text (large, bold)
self.signal_label = QLabel("NO TRADE — Initializing...")
self.signal_label.setFont(QFont("Arial", 24, QFont.Bold))
self.signal_label.setProperty("value", True)
self.signal_label.setStyleSheet("color: #2c3e50;")
layout.addWidget(self.signal_label)
layout.addSpacing(10)
# Gap and status
self.gap_label = QLabel("Gap: — points")
self.gap_label.setFont(QFont("Arial", 12))
self.gap_label.setStyleSheet("font-size: 15px; color: #5d6d7e;")
layout.addWidget(self.gap_label)
# Updated timestamp
self.updated_label = QLabel("Updated: —")
self.updated_label.setFont(QFont("Arial", 10))
self.updated_label.setStyleSheet("color: #7f8c8d;")
self.updated_label.setStyleSheet("color: #95a5a6; font-size: 12px;")
layout.addWidget(self.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;"
)
self.stale_warning.hide()
layout.addWidget(self.stale_warning)
layout.addStretch()
card.setLayout(layout)
return card
@@ -168,14 +182,11 @@ class DashboardTab(QWidget):
bias_matrix = {}
self.signal_generated.emit(strongest, weakest, gap, bias_matrix)
# Update signal label
self.signal_label.setText(signal_text)
# Color code based on status
if status == "ACTIVE":
self.signal_label.setStyleSheet("color: #27ae60;") # Green
self.signal_label.setStyleSheet("color: #27ae60;")
else:
self.signal_label.setStyleSheet("color: #e74c3c;") # Red
self.signal_label.setStyleSheet("color: #e74c3c;")
# Update gap label
gap_tier = scorer.get_gap_tier(gap)
@@ -195,6 +206,9 @@ class DashboardTab(QWidget):
# Update timestamp
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()
# Refresh score table
self._refresh_score_table()
@@ -203,28 +217,54 @@ class DashboardTab(QWidget):
self.signal_label.setText("ERROR")
self.signal_label.setStyleSheet("color: #e74c3c;")
def _check_data_staleness(self):
"""Show a warning if CPI/PMI data is older than 35 days."""
try:
monthly = self.db.get_monthly_data(self.current_month)
if not monthly:
return
timestamps = []
for ccy, data in monthly.items():
ts = data.get("entered_at")
if ts:
timestamps.append(ts)
if not timestamps:
return
latest = max(timestamps)
try:
latest_dt = datetime.strptime(latest.split(".")[0], "%Y-%m-%dT%H:%M:%S")
except ValueError:
latest_dt = datetime.strptime(latest[:10], "%Y-%m-%d")
days_old = (datetime.now() - latest_dt).days
if days_old > 35:
self.stale_warning.setText(
f"⚠ CPI/PMI data is {days_old} days old — refresh fundamental data"
)
self.stale_warning.show()
else:
self.stale_warning.hide()
except Exception:
self.stale_warning.hide()
def _refresh_score_table(self):
"""Refresh the ranked currency table."""
try:
scores = self.db.get_month_scores(self.current_month)
if not scores:
# No scores yet
for row in range(len(config.CURRENCIES)):
for col in range(8):
self.score_table.item(row, col).setText("")
self.strength_bars[row].setValue(0)
self.strength_bars[row].setFormat("")
return
# Get sorted list
ranked = [(c, s) for c, s in sorted(
scores.items(),
key=lambda x: x[1]['rank']
)]
# Get rates for display
rates = self.db.get_all_rates()
# Get monthly data for CPI display
monthly_data = self.db.get_monthly_data(self.current_month)
for row, (currency, score_data) in enumerate(ranked):
@@ -235,29 +275,15 @@ class DashboardTab(QWidget):
cpi = cpi_data.get('cpi_actual')
pmi = cpi_data.get('pmi_actual')
# Rank
self.score_table.item(row, 0).setText(str(rank))
# Currency
currency_text = f"{config.CURRENCY_EMOJIS.get(currency, '')} {currency}"
self.score_table.item(row, 1).setText(currency_text)
# Rate
rate_text = f"{rate:.2f}" if rate is not None else ""
self.score_table.item(row, 2).setText(rate_text)
# CPI
cpi_text = f"{cpi:.2f}" if cpi is not None else ""
self.score_table.item(row, 3).setText(cpi_text)
# PMI
pmi_text = f"{pmi:.1f}" if pmi is not None else ""
self.score_table.item(row, 4).setText(pmi_text)
# Score (two decimals)
self.score_table.item(row, 2).setText(f"{rate:.2f}" if rate is not None else "")
self.score_table.item(row, 3).setText(f"{cpi:.2f}" if cpi is not None else "")
self.score_table.item(row, 4).setText(f"{pmi:.1f}" if pmi is not None else "")
self.score_table.item(row, 5).setText(f"{total_score:.1f}")
# Signal tag (BUY for strongest, SELL for weakest)
# Signal tag
if rank == 1:
self.score_table.item(row, 6).setText("BUY")
elif rank == len(config.CURRENCIES):
@@ -265,34 +291,30 @@ class DashboardTab(QWidget):
else:
self.score_table.item(row, 6).setText("")
# Strength bar (visual progress 0-100)
strength_item = self.score_table.item(row, 7)
strength_item.setText(f"{int(total_score)}%")
# Strength progress bar
bar = self.strength_bars[row]
score_int = min(int(total_score), 100)
bar.setValue(score_int)
bar.setFormat(f"{score_int}%")
bar.setStyleSheet(
"QProgressBar::chunk { background: qlineargradient(x1:0, y1:0, x2:1, y2:0, "
"stop:0 #2ecc71, stop:1 #27ae60); border-radius: 4px; }"
if rank == 1 else
"QProgressBar::chunk { background: qlineargradient(x1:0, y1:0, x2:1, y2:0, "
"stop:0 #e74c3c, stop:1 #c0392b); border-radius: 4px; }"
if rank == len(config.CURRENCIES) else
""
)
# Color code rows
if rank == 1:
# Strongest = GREEN
for col in range(8):
self.score_table.item(row, col).setBackground(QColor("#d5f4e6"))
self.score_table.item(row, col).setForeground(QColor("#27ae60"))
self.score_table.item(row, col).setFont(QFont("Arial", 10, QFont.Bold))
# Row color coding
bg = QColor("#d5f4e6") if rank == 1 else QColor("#fadbd8") if rank == len(config.CURRENCIES) else QColor("#ffffff")
fg = QColor("#1e8449") if rank == 1 else QColor("#c0392b") if rank == len(config.CURRENCIES) else QColor("#2c3e50")
font = QFont("Segoe UI", 11, QFont.Bold) if rank in (1, len(config.CURRENCIES)) else QFont("Segoe UI", 11)
elif rank == len(config.CURRENCIES):
# Weakest = RED
for col in range(8):
self.score_table.item(row, col).setBackground(QColor("#fadbd8"))
self.score_table.item(row, col).setForeground(QColor("#e74c3c"))
self.score_table.item(row, col).setFont(QFont("Arial", 10, QFont.Bold))
else:
# Middle = neutral
for col in range(8):
self.score_table.item(row, col).setBackground(QColor("#ffffff"))
self.score_table.item(row, col).setForeground(QColor("#2c3e50"))
self.score_table.item(row, col).setFont(QFont("Arial", 10))
# Auto-resize columns to content
self.score_table.resizeColumnsToContents()
for col in range(7):
self.score_table.item(row, col).setBackground(bg)
self.score_table.item(row, col).setForeground(fg)
self.score_table.item(row, col).setFont(font)
except Exception as e:
print(f"[ERROR] Failed to refresh score table: {e}")
+6 -39
View File
@@ -23,11 +23,11 @@ User flow:
from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QTableWidget, QTableWidgetItem,
QPushButton, QProgressBar, QComboBox, QSpinBox, QDoubleSpinBox, QHeaderView,
QPushButton, QProgressBar, QComboBox, QDoubleSpinBox, QHeaderView,
QFileDialog, QMessageBox
)
from PyQt5.QtCore import Qt, pyqtSignal, QDate
from PyQt5.QtGui import QColor, QFont, QBrush
from PyQt5.QtCore import Qt, pyqtSignal
from PyQt5.QtGui import QColor
from typing import Dict, Optional
from datetime import datetime, timedelta
import config
@@ -190,49 +190,16 @@ class MonthlyEntryTab(QWidget):
# ====== Buttons ======
button_layout = QHBoxLayout()
# Import Excel button
self.import_btn = QPushButton("📊 Import Excel")
self.import_btn.setMinimumHeight(40)
self.import_btn.setFont(QFont("Arial", 11, QFont.Bold))
self.import_btn.setStyleSheet("""
QPushButton {
background-color: #3498db;
color: white;
border: none;
border-radius: 5px;
padding: 10px 20px;
}
QPushButton:hover {
background-color: #2980b9;
}
""")
self.import_btn.setMinimumHeight(42)
button_layout.addWidget(self.import_btn)
button_layout.addStretch()
self.save_btn = QPushButton("Save & Calculate Scores")
self.save_btn.setObjectName("success")
self.save_btn.setEnabled(False)
self.save_btn.setMinimumHeight(40)
self.save_btn.setFont(QFont("Arial", 11, QFont.Bold))
self.save_btn.setStyleSheet("""
QPushButton:enabled {
background-color: #2ecc71;
color: white;
border: none;
border-radius: 5px;
padding: 10px 20px;
}
QPushButton:hover:enabled {
background-color: #27ae60;
}
QPushButton:disabled {
background-color: #95a5a6;
color: #7f8c8d;
border: none;
border-radius: 5px;
padding: 10px 20px;
}
""")
self.save_btn.setMinimumHeight(42)
button_layout.addWidget(self.save_btn)
layout.addLayout(button_layout)
+5 -4
View File
@@ -11,12 +11,11 @@ Features:
"""
from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QTableWidget, QTableWidgetItem, QHeaderView,
QWidget, QVBoxLayout, QLabel, QTableWidget, QTableWidgetItem, QHeaderView,
QMessageBox
)
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QColor, QFont
from typing import Dict, List
import config
from database import Database
@@ -41,6 +40,10 @@ class HistoryTab(QWidget):
"""Build the UI layout."""
layout = QVBoxLayout()
heading = QLabel("Signal History (Past 24 Months)")
heading.setProperty("heading", True)
layout.addWidget(heading)
# History table
self.history_table = QTableWidget()
self.history_table.setColumnCount(6)
@@ -48,8 +51,6 @@ class HistoryTab(QWidget):
"Month", "Signal", "Gap", "Strongest", "Weakest", "Status"
])
# Enable sorting
self.history_table.setSortingEnabled(False)
self.history_table.setSelectionBehavior(QTableWidget.SelectRows)
self.history_table.setSelectionMode(QTableWidget.SingleSelection)
self.history_table.itemClicked.connect(self._on_row_clicked)
+147 -61
View File
@@ -1,15 +1,15 @@
from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QTableWidget, QTableWidgetItem,
QPushButton, QComboBox, QCheckBox, QMessageBox, QStatusBar, QProgressBar,
QPushButton, QComboBox, QCheckBox, QMessageBox,
QHeaderView
)
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer
from PyQt5.QtGui import QColor, QFont, QBrush
from PyQt5.QtGui import QColor, QFont
from typing import Dict, Optional
from datetime import datetime, timezone
import config
from layer2_technical import TechnicalAnalyzer
from data_feeder import Mt5DataFeeder, MockDataFeeder
from data_feeder import Mt5DataFeeder
from currency_strength_matrix import CurrencyStrengthMatrix
@@ -75,43 +75,57 @@ class Layer2MonitorTab(QWidget):
def _init_ui(self):
"""Build UI layout."""
layout = QVBoxLayout()
layout.setSpacing(12)
# ====== Connection Panel ======
connection_layout = QHBoxLayout()
connection_layout.addWidget(QLabel("Data Source:"))
self.source_combo = QComboBox()
self.source_combo.addItems(["Mock (Test)", "MT5 (Live)"])
self.source_combo.addItems(["MT5 (Live)"])
self.source_combo.setCurrentIndex(0)
connection_layout.addWidget(self.source_combo)
connection_layout.addWidget(QLabel("TF:"))
self.tf_combo = QComboBox()
for tf_key in config.TIMEFRAMES:
self.tf_combo.addItem(config.TIMEFRAMES[tf_key]["label"], tf_key)
self.tf_combo.setCurrentText(config.TIMEFRAMES[config.DEFAULT_TIMEFRAME]["label"])
self.tf_combo.currentIndexChanged.connect(self._on_timeframe_changed)
connection_layout.addWidget(self.tf_combo)
self.connect_btn = QPushButton("Connect")
self.connect_btn.clicked.connect(self._on_connect_clicked)
connection_layout.addWidget(self.connect_btn)
# Status dot indicator
self.status_dot = QLabel("")
self.status_dot.setStyleSheet("color: #e74c3c; font-size: 18px;")
connection_layout.addWidget(self.status_dot)
self.status_label = QLabel("Disconnected")
self.status_label.setStyleSheet("color: red; font-weight: bold;")
self.status_label.setStyleSheet("color: #e74c3c; font-weight: 600;")
connection_layout.addWidget(self.status_label)
connection_layout.addStretch()
layout.addLayout(connection_layout)
layout.addSpacing(10)
# ====== Active Session Indicator (Task 4.1) ======
session_layout = QHBoxLayout()
session_layout.addWidget(QLabel("Active Session:"))
self.session_label = QLabel("")
self.session_label.setStyleSheet(
"font-weight: bold; font-size: 14px; padding: 2px 8px; "
"background-color: #ecf0f1; border-radius: 4px;"
"font-weight: 700; font-size: 14px; padding: 4px 14px; "
"background-color: #ecf0f1; border-radius: 12px;"
)
session_layout.addWidget(self.session_label)
session_layout.addStretch()
layout.addLayout(session_layout)
layout.addSpacing(5)
# ====== Z-Score Table ======
layout.addWidget(QLabel("Technical Analysis — All Pairs"))
heading = QLabel("Technical Analysis — All Pairs")
heading.setProperty("heading", True)
layout.addWidget(heading)
self.tech_table = QTableWidget()
self.tech_table.setColumnCount(7)
@@ -120,48 +134,52 @@ class Layer2MonitorTab(QWidget):
])
self.tech_table.setRowCount(28)
self.tech_table.setAlternatingRowColors(True)
self.tech_table.horizontalHeader().setStretchLastSection(True)
header = self.tech_table.horizontalHeader()
for c in range(7):
header.setSectionResizeMode(c, QHeaderView.Stretch)
layout.addWidget(self.tech_table)
layout.addSpacing(10)
# ====== Alerts Panel ======
alerts_layout = QHBoxLayout()
alerts_layout.addWidget(QLabel("Overbought Pairs:"))
alerts_layout.addWidget(QLabel("Overbought:"))
self.overbought_label = QLabel("")
self.overbought_label.setStyleSheet("color: #e74c3c; font-weight: bold;")
self.overbought_label.setStyleSheet("color: #e74c3c; font-weight: 700;")
alerts_layout.addWidget(self.overbought_label)
alerts_layout.addSpacing(20)
alerts_layout.addWidget(QLabel("Oversold Pairs:"))
alerts_layout.addWidget(QLabel("Oversold:"))
self.oversold_label = QLabel("")
self.oversold_label.setStyleSheet("color: #27ae60; font-weight: bold;")
self.oversold_label.setStyleSheet("color: #27ae60; font-weight: 700;")
alerts_layout.addWidget(self.oversold_label)
alerts_layout.addStretch()
layout.addLayout(alerts_layout)
layout.addSpacing(10)
# ====== Currency Strength Matrix ======
matrix_group = QWidget()
matrix_layout = QVBoxLayout(matrix_group)
matrix_layout.setContentsMargins(0, 0, 0, 0)
matrix_heading = QLabel("Currency Strength Matrix (S.A.T.O.R.I.)")
matrix_heading.setProperty("heading", True)
layout.addWidget(matrix_heading)
matrix_layout.addWidget(QLabel("Currency Strength Matrix (S.A.T.O.R.I.)"))
self.matrix_cross_label = QLabel("Matrix Cross: —")
self.matrix_cross_label.setStyleSheet("font-weight: bold; font-size: 13px; color: #2c3e50;")
matrix_layout.addWidget(self.matrix_cross_label)
self.matrix_cross_label.setStyleSheet("font-weight: 700; font-size: 14px;")
layout.addWidget(self.matrix_cross_label)
self.divergence_label = QLabel("Divergence Gap: 0.0")
self.divergence_label.setStyleSheet("color: #7f8c8d;")
matrix_layout.addWidget(self.divergence_label)
self.divergence_label.setStyleSheet("color: #7f8c8d; font-size: 12px;")
layout.addWidget(self.divergence_label)
alert_row = QHBoxLayout()
self.strong_alert = QLabel("")
matrix_layout.addWidget(self.strong_alert)
self.strong_alert.setStyleSheet("color: #27ae60; font-weight: 600;")
alert_row.addWidget(self.strong_alert)
self.weak_alert = QLabel("")
matrix_layout.addWidget(self.weak_alert)
self.weak_alert.setStyleSheet("color: #e74c3c; font-weight: 600;")
alert_row.addWidget(self.weak_alert)
alert_row.addStretch()
layout.addLayout(alert_row)
self.matrix_table = QTableWidget()
self.matrix_table.setColumnCount(5)
@@ -169,14 +187,13 @@ class Layer2MonitorTab(QWidget):
"Rank", "Currency", "Strength Z", "Direction", "Session SRV"
])
self.matrix_table.setRowCount(8)
self.matrix_table.setMaximumHeight(240)
self.matrix_table.horizontalHeader().setStretchLastSection(True)
matrix_layout.addWidget(self.matrix_table)
self.matrix_table.setMaximumHeight(220)
m_header = self.matrix_table.horizontalHeader()
for c in range(5):
m_header.setSectionResizeMode(c, QHeaderView.Stretch)
layout.addWidget(self.matrix_table)
layout.addWidget(matrix_group)
layout.addSpacing(10)
# ====== Refresh Button ======
# ====== Controls ======
button_layout = QHBoxLayout()
self.auto_refresh_check = QCheckBox("Auto-refresh (every 1s)")
@@ -184,6 +201,7 @@ class Layer2MonitorTab(QWidget):
button_layout.addWidget(self.auto_refresh_check)
refresh_btn = QPushButton("Refresh Now")
refresh_btn.setObjectName("secondary")
refresh_btn.clicked.connect(self._refresh_display)
button_layout.addWidget(refresh_btn)
@@ -196,13 +214,13 @@ class Layer2MonitorTab(QWidget):
self.refresh_timer = QTimer()
self.refresh_timer.timeout.connect(self._refresh_display)
self._debounce_timer = QTimer()
self._debounce_timer.setSingleShot(True)
self._debounce_timer.timeout.connect(self._refresh_display)
def _setup_data_source(self):
"""Initialize data source."""
source = self.source_combo.currentText()
if "MT5" in source:
self.data_feeder = Mt5DataFeeder()
else:
self.data_feeder = MockDataFeeder()
self.data_feeder = Mt5DataFeeder()
def _on_connect_clicked(self):
"""Handle connect button click."""
@@ -211,17 +229,82 @@ class Layer2MonitorTab(QWidget):
else:
self._connect()
def _on_timeframe_changed(self, idx: int):
tf_key = self.tf_combo.itemData(idx)
if tf_key:
self.tech_analyzer.set_timeframe(tf_key)
if self.connected and hasattr(self.data_feeder, 'USD_PAIRS'):
self._seed_historical_bars()
def _seed_historical_bars(self):
"""Seed the analyzer with 24h of historical M5 bar data for stable Z-scores."""
if hasattr(self.data_feeder, 'generate_mock_bars'):
bars = self.data_feeder.generate_mock_bars(n_bars=config.BAR_LOOKBACK_BARS)
elif hasattr(self.data_feeder, 'fetch_historical_closes_all_pairs'):
bars = self.data_feeder.fetch_historical_closes_all_pairs(
days=1, interval="5min"
)
else:
"""Seed the analyzer with bar data at the selected timeframe.
Fetches only the 7 USD pairs and derives all cross rates.
"""
if not hasattr(self.data_feeder, 'USD_PAIRS'):
return
self.tech_analyzer.seed_bars(bars)
interval_map = {
"M5": "5min", "M15": "15min",
"H1": "1h", "H4": "4h",
}
tf_key = self.tech_analyzer.current_timeframe
interval = interval_map.get(tf_key, "15min")
outputsize = "full" if tf_key in ("M5", "M15") else "full"
usd_pairs = self.data_feeder.USD_PAIRS
raw_ohlc: dict[str, list[dict]] = {}
for pair in usd_pairs:
base, quote = pair.split("_")
candles = self.data_feeder.get_historical_candles(
from_currency=base, to_currency=quote,
interval=interval, outputsize=outputsize
)
if candles:
raw_ohlc[pair] = candles
if not raw_ohlc:
return
n_bars = min(len(c) for c in raw_ohlc.values())
if n_bars < 2:
return
currencies = config.CURRENCIES
derived_ohlc: dict[str, list[dict]] = {}
for base in currencies:
for quote in currencies:
if base == quote:
continue
derived_ohlc[f"{base}_{quote}"] = []
for i in range(n_bars):
usd_rates: dict[str, float] = {"USD": 1.0}
for pair in usd_pairs:
base, quote = pair.split("_")
c = raw_ohlc[pair][i]
mid = c["close"]
if base == "USD":
usd_rates[quote] = 1.0 / mid if mid else None
else:
usd_rates[base] = mid
for base in currencies:
bv = usd_rates.get(base)
if bv is None:
continue
for quote in currencies:
if base == quote:
continue
qv = usd_rates.get(quote)
if qv is not None:
rate = bv / qv
# Estimate OHLC for the cross pair
derived_ohlc[f"{base}_{quote}"].append({
"close": rate,
"high": rate * 1.0003,
"low": rate * 0.9997,
})
self.tech_analyzer.seed_ohlc(derived_ohlc)
def _connect(self):
"""Connect to data source."""
@@ -231,10 +314,6 @@ class Layer2MonitorTab(QWidget):
QMessageBox.warning(self, "Connection Error", f"Failed to connect to data source:\n{reason}")
return
# Seed bar_history with 24h of M5 close prices so Z-scores are
# anchored to a meaningful multi-hour frame, not tick noise.
self._seed_historical_bars()
instruments = self.data_feeder.get_all_major_pairs()
self.streamer_thread = DataStreamerThread(self.data_feeder, instruments)
self.streamer_thread.price_updated.connect(self._on_price_received)
@@ -243,14 +322,20 @@ class Layer2MonitorTab(QWidget):
self.streamer_thread.start()
if self.auto_refresh_check.isChecked():
self.refresh_timer.start(1000)
self.refresh_timer.start(3000)
self.connected = True
self.connect_btn.setText("Disconnect")
self.connect_btn.setObjectName("danger")
self.connect_btn.style().unpolish(self.connect_btn)
self.connect_btn.style().polish(self.connect_btn)
self.status_dot.setStyleSheet("color: #27ae60; font-size: 18px;")
self.status_label.setText("Connected")
self.status_label.setStyleSheet("color: #27ae60; font-weight: bold;")
self.status_label.setStyleSheet("color: #27ae60; font-weight: 600;")
self._refresh_display()
QTimer.singleShot(0, self._seed_historical_bars)
except Exception as e:
QMessageBox.critical(self, "Error", f"Connection failed: {e}")
@@ -265,17 +350,22 @@ class Layer2MonitorTab(QWidget):
self.connected = False
self.connect_btn.setText("Connect")
self.connect_btn.setObjectName("")
self.connect_btn.style().unpolish(self.connect_btn)
self.connect_btn.style().polish(self.connect_btn)
self.status_dot.setStyleSheet("color: #e74c3c; font-size: 18px;")
self.status_label.setText("Disconnected")
self.status_label.setStyleSheet("color: #e74c3c; font-weight: bold;")
self.status_label.setStyleSheet("color: #e74c3c; font-weight: 600;")
def _on_price_received(self, price_data):
"""Handle price update from data feeder."""
"""Handle price update — just add data, debounce display refresh."""
pair = price_data.get('pair')
mid_price = price_data.get('mid')
if pair and mid_price:
self.tech_analyzer.add_price_data(pair, mid_price)
self._refresh_display()
if not self._debounce_timer.isActive():
self._debounce_timer.start(2000)
def _on_connected(self, is_connected):
"""Handle connection status change."""
@@ -358,8 +448,6 @@ class Layer2MonitorTab(QWidget):
self.overbought_label.setText(overbought_text)
self.oversold_label.setText(oversold_text)
self.tech_table.resizeColumnsToContents()
# ====== Currency Strength Matrix (persistent instance) ======
current_prices = {}
for pair in z_scores:
@@ -466,8 +554,6 @@ class Layer2MonitorTab(QWidget):
srv_item.setBackground(QColor("#fff3e0"))
self.matrix_table.setItem(row, 4, srv_item)
self.matrix_table.resizeColumnsToContents()
except Exception as e:
print(f"[Layer2] Display error: {e}")
+4 -17
View File
@@ -16,13 +16,12 @@ from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QDoubleSpinBox,
QPushButton, QCheckBox, QGroupBox, QSpinBox, QMessageBox, QScrollArea
)
from PyQt5.QtCore import Qt, pyqtSignal, QThread
from PyQt5.QtCore import pyqtSignal, QThread
from PyQt5.QtGui import QFont
from typing import Dict, Optional
from typing import Optional
import config
from data_feeder import Mt5DataFeeder
from fred_client import FredClient
import os
from pathlib import Path
@@ -272,24 +271,12 @@ class SettingsTab(QWidget):
save_layout.addStretch()
save_btn = QPushButton("Save Settings")
save_btn.setMinimumHeight(40)
save_btn.setFont(QFont("Arial", 11, QFont.Bold))
save_btn.setStyleSheet("""
QPushButton {
background-color: #3498db;
color: white;
border: none;
border-radius: 5px;
padding: 10px 20px;
}
QPushButton:hover {
background-color: #2980b9;
}
""")
save_btn.setMinimumHeight(42)
save_btn.clicked.connect(self._save_settings)
save_layout.addWidget(save_btn)
reset_btn = QPushButton("Reset to Defaults")
reset_btn.setObjectName("secondary")
reset_btn.clicked.connect(self._reset_to_defaults)
save_layout.addWidget(reset_btn)