mirror of
https://github.com/Sabermrddz/QuantCore-FX.git
synced 2026-07-28 02:57:51 +00:00
463 lines
18 KiB
Python
463 lines
18 KiB
Python
"""
|
|
APEX Layer 1 — Tab 4: Settings
|
|
|
|
Configuration editor for:
|
|
- FRED API key (with test connection)
|
|
- Central Bank inflation targets (read-only display, edit only if CB changes mandate)
|
|
- Scoring weights (Rate %, CPI %, PMI %)
|
|
- Minimum gap to trade
|
|
- Auto-fetch rates on startup toggle
|
|
- Application info
|
|
|
|
Settings are stored in the .env file.
|
|
"""
|
|
|
|
from PyQt5.QtWidgets import (
|
|
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QDoubleSpinBox,
|
|
QPushButton, QCheckBox, QGroupBox, QSpinBox, QMessageBox, QScrollArea
|
|
)
|
|
from PyQt5.QtCore import pyqtSignal, QThread
|
|
from PyQt5.QtGui import QFont
|
|
from typing import Optional
|
|
import config
|
|
from data_feeder import Mt5DataFeeder
|
|
from fred_client import FredClient
|
|
from pathlib import Path
|
|
|
|
|
|
class FredTestWorker(QThread):
|
|
"""Background thread for testing FRED API connection."""
|
|
|
|
test_complete = pyqtSignal(bool, str) # (success, message)
|
|
|
|
def __init__(self, api_key: str):
|
|
super().__init__()
|
|
self.api_key = api_key
|
|
|
|
def run(self):
|
|
"""Test FRED connectivity."""
|
|
try:
|
|
client = FredClient(self.api_key, timeout=5)
|
|
rate = client.fetch_rate("USD")
|
|
|
|
if rate is not None:
|
|
self.test_complete.emit(True, f"✓ Connection successful! USD rate: {rate}%")
|
|
else:
|
|
self.test_complete.emit(False, "✗ No data returned for USD")
|
|
except Exception as e:
|
|
self.test_complete.emit(False, f"✗ Connection failed: {str(e)}")
|
|
|
|
|
|
class Mt5TestWorker(QThread):
|
|
"""Background thread for testing MT5 connection."""
|
|
|
|
test_complete = pyqtSignal(bool, str)
|
|
|
|
def __init__(self, symbol_suffix: str):
|
|
super().__init__()
|
|
self.symbol_suffix = symbol_suffix
|
|
|
|
def run(self):
|
|
try:
|
|
feeder = Mt5DataFeeder(symbol_suffix=self.symbol_suffix)
|
|
if feeder.initialize():
|
|
price = feeder.get_current_price("EUR_USD")
|
|
if price:
|
|
self.test_complete.emit(True, f"✓ Connected! EUR/USD bid={price['bid']:.5f} ask={price['ask']:.5f}")
|
|
else:
|
|
self.test_complete.emit(True, "✓ Connected! (no EUR/USD tick data)")
|
|
feeder.shutdown()
|
|
else:
|
|
self.test_complete.emit(False, f"✗ {feeder.last_error}")
|
|
except Exception as e:
|
|
self.test_complete.emit(False, f"✗ {e}")
|
|
|
|
|
|
class SettingsTab(QWidget):
|
|
"""Settings and configuration tab."""
|
|
|
|
# Signal triggered when settings change
|
|
settings_changed = pyqtSignal()
|
|
|
|
def __init__(self):
|
|
"""Initialize Settings tab."""
|
|
super().__init__()
|
|
self.env_path = Path(__file__).parent.parent / ".env"
|
|
|
|
self._init_ui()
|
|
self._load_settings()
|
|
|
|
def _init_ui(self):
|
|
"""Build the UI layout."""
|
|
scroll = QScrollArea()
|
|
scroll.setWidgetResizable(True)
|
|
|
|
main_widget = QWidget()
|
|
layout = QVBoxLayout()
|
|
|
|
# ====== FRED API Configuration ======
|
|
api_group = QGroupBox("FRED API Configuration")
|
|
api_layout = QVBoxLayout()
|
|
|
|
api_layout.addWidget(QLabel(
|
|
"Enter your FRED API key for automatic interest rate fetching.\n"
|
|
"Get a free key from https://fred.stlouisfed.org"
|
|
))
|
|
|
|
key_layout = QHBoxLayout()
|
|
key_layout.addWidget(QLabel("API Key:"))
|
|
self.api_key_input = QLineEdit()
|
|
self.api_key_input.setEchoMode(QLineEdit.Password)
|
|
self.api_key_input.setPlaceholderText("Paste your FRED API key here...")
|
|
key_layout.addWidget(self.api_key_input)
|
|
|
|
test_btn = QPushButton("Test Connection")
|
|
test_btn.clicked.connect(self._test_fred_connection)
|
|
key_layout.addWidget(test_btn)
|
|
|
|
api_layout.addLayout(key_layout)
|
|
|
|
self.test_status = QLabel("")
|
|
self.test_status.setStyleSheet("color: #95a5a6; font-style: italic;")
|
|
api_layout.addWidget(self.test_status)
|
|
|
|
api_group.setLayout(api_layout)
|
|
layout.addWidget(api_group)
|
|
layout.addSpacing(10)
|
|
|
|
# ====== MetaTrader 5 Connection ======
|
|
mt5_group = QGroupBox("MetaTrader 5 (Layer 2 Forex Data)")
|
|
mt5_layout = QVBoxLayout()
|
|
mt5_layout.addWidget(QLabel(
|
|
"MT5 provides real-time forex data from your local MetaTrader 5 terminal.\n"
|
|
"Ensure MT5 is installed and running with a demo/live account.\n"
|
|
"Symbol suffix is used by some brokers (e.g., .m for OANDA MT5)."
|
|
))
|
|
suffix_layout = QHBoxLayout()
|
|
suffix_layout.addWidget(QLabel("Symbol Suffix:"))
|
|
self.mt5_suffix_input = QLineEdit()
|
|
self.mt5_suffix_input.setPlaceholderText("e.g., .m (leave empty if unsure)")
|
|
suffix_layout.addWidget(self.mt5_suffix_input)
|
|
mt5_layout.addLayout(suffix_layout)
|
|
mt5_status_layout = QHBoxLayout()
|
|
self.mt5_status_label = QLabel("Status: Not tested")
|
|
self.mt5_status_label.setStyleSheet("color: #95a5a6; font-style: italic;")
|
|
mt5_status_layout.addWidget(self.mt5_status_label)
|
|
mt5_test_btn = QPushButton("Test Connection")
|
|
mt5_test_btn.clicked.connect(self._test_mt5_connection)
|
|
mt5_status_layout.addWidget(mt5_test_btn)
|
|
mt5_layout.addLayout(mt5_status_layout)
|
|
mt5_group.setLayout(mt5_layout)
|
|
layout.addWidget(mt5_group)
|
|
layout.addSpacing(10)
|
|
|
|
# ====== Central Bank Targets ======
|
|
cb_group = QGroupBox("Central Bank Inflation Targets (%)")
|
|
cb_layout = QVBoxLayout()
|
|
|
|
cb_layout.addWidget(QLabel(
|
|
"These are hardcoded constants. Edit only if a central bank officially changes its mandate.\n"
|
|
"Most central banks maintain these targets for years."
|
|
))
|
|
|
|
# Display in a grid-like format
|
|
targets_text = " ".join([f"{c}: {config.CB_TARGETS[c]}%" for c in config.CURRENCIES])
|
|
targets_label = QLabel(targets_text)
|
|
targets_label.setFont(QFont("Courier", 10))
|
|
targets_label.setStyleSheet("background-color: #ecf0f1; padding: 10px; border-radius: 4px;")
|
|
cb_layout.addWidget(targets_label)
|
|
|
|
cb_layout.addWidget(QLabel("To edit: Manually update the CB_TARGETS dict in config.py"))
|
|
cb_group.setLayout(cb_layout)
|
|
layout.addWidget(cb_group)
|
|
layout.addSpacing(10)
|
|
|
|
# ====== Scoring Weights ======
|
|
weights_group = QGroupBox("Scoring Weights")
|
|
weights_layout = QVBoxLayout()
|
|
|
|
weights_layout.addWidget(QLabel(
|
|
"Adjust the influence of each input. Must sum to 100%.\n"
|
|
"Default: Rate 50%, CPI 30%, PMI 20%"
|
|
))
|
|
|
|
# Rate weight
|
|
rate_layout = QHBoxLayout()
|
|
rate_layout.addWidget(QLabel("Rate Differential:"))
|
|
self.weight_rate_spin = QDoubleSpinBox()
|
|
self.weight_rate_spin.setRange(0, 100)
|
|
self.weight_rate_spin.setValue(config.WEIGHT_RATE * 100)
|
|
self.weight_rate_spin.setSuffix("%")
|
|
self.weight_rate_spin.setDecimals(1)
|
|
rate_layout.addWidget(self.weight_rate_spin)
|
|
rate_layout.addStretch()
|
|
weights_layout.addLayout(rate_layout)
|
|
|
|
# CPI weight
|
|
cpi_layout = QHBoxLayout()
|
|
cpi_layout.addWidget(QLabel("CPI Deviation:"))
|
|
self.weight_cpi_spin = QDoubleSpinBox()
|
|
self.weight_cpi_spin.setRange(0, 100)
|
|
self.weight_cpi_spin.setValue(config.WEIGHT_CPI * 100)
|
|
self.weight_cpi_spin.setSuffix("%")
|
|
self.weight_cpi_spin.setDecimals(1)
|
|
cpi_layout.addWidget(self.weight_cpi_spin)
|
|
cpi_layout.addStretch()
|
|
weights_layout.addLayout(cpi_layout)
|
|
|
|
# PMI weight
|
|
pmi_layout = QHBoxLayout()
|
|
pmi_layout.addWidget(QLabel("PMI Composite:"))
|
|
self.weight_pmi_spin = QDoubleSpinBox()
|
|
self.weight_pmi_spin.setRange(0, 100)
|
|
self.weight_pmi_spin.setValue(config.WEIGHT_PMI * 100)
|
|
self.weight_pmi_spin.setSuffix("%")
|
|
self.weight_pmi_spin.setDecimals(1)
|
|
pmi_layout.addWidget(self.weight_pmi_spin)
|
|
pmi_layout.addStretch()
|
|
weights_layout.addLayout(pmi_layout)
|
|
|
|
# Total validation label
|
|
self.weights_total_label = QLabel("Total: 0%")
|
|
self.weights_total_label.setStyleSheet("color: #e74c3c; font-weight: bold;")
|
|
weights_layout.addWidget(self.weights_total_label)
|
|
|
|
# Connect to update total
|
|
self.weight_rate_spin.valueChanged.connect(self._update_weights_total)
|
|
self.weight_cpi_spin.valueChanged.connect(self._update_weights_total)
|
|
self.weight_pmi_spin.valueChanged.connect(self._update_weights_total)
|
|
|
|
weights_group.setLayout(weights_layout)
|
|
layout.addWidget(weights_group)
|
|
layout.addSpacing(10)
|
|
|
|
# ====== Trading Rules ======
|
|
rules_group = QGroupBox("Trading Rules")
|
|
rules_layout = QVBoxLayout()
|
|
|
|
rules_layout.addWidget(QLabel(
|
|
"Minimum gap between strongest and weakest currency to generate a trade signal.\n"
|
|
"If gap < minimum, output 'NO TRADE'. Default: 20 points."
|
|
))
|
|
|
|
min_gap_layout = QHBoxLayout()
|
|
min_gap_layout.addWidget(QLabel("Minimum gap to trade:"))
|
|
self.min_gap_spin = QSpinBox()
|
|
self.min_gap_spin.setRange(5, 100)
|
|
self.min_gap_spin.setValue(int(config.MIN_GAP_TO_TRADE))
|
|
self.min_gap_spin.setSuffix(" points")
|
|
min_gap_layout.addWidget(self.min_gap_spin)
|
|
min_gap_layout.addStretch()
|
|
rules_layout.addLayout(min_gap_layout)
|
|
|
|
rules_group.setLayout(rules_layout)
|
|
layout.addWidget(rules_group)
|
|
layout.addSpacing(10)
|
|
|
|
# ====== Application Settings ======
|
|
app_group = QGroupBox("Application Settings")
|
|
app_layout = QVBoxLayout()
|
|
|
|
self.auto_fetch_check = QCheckBox("Auto-fetch interest rates on startup")
|
|
self.auto_fetch_check.setChecked(config.AUTO_FETCH_RATES_ON_STARTUP)
|
|
app_layout.addWidget(self.auto_fetch_check)
|
|
|
|
app_group.setLayout(app_layout)
|
|
layout.addWidget(app_group)
|
|
layout.addSpacing(15)
|
|
|
|
# ====== Save Button ======
|
|
save_layout = QHBoxLayout()
|
|
save_layout.addStretch()
|
|
|
|
save_btn = QPushButton("Save Settings")
|
|
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)
|
|
|
|
layout.addLayout(save_layout)
|
|
layout.addStretch()
|
|
|
|
main_widget.setLayout(layout)
|
|
scroll.setWidget(main_widget)
|
|
|
|
main_layout = QVBoxLayout()
|
|
main_layout.addWidget(scroll)
|
|
self.setLayout(main_layout)
|
|
|
|
def _load_settings(self):
|
|
"""Load settings from .env file."""
|
|
try:
|
|
env_vars = {}
|
|
if self.env_path.exists():
|
|
with open(self.env_path, 'r') as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if line and not line.startswith('#') and '=' in line:
|
|
key, value = line.split('=', 1)
|
|
env_vars[key.strip()] = value.strip()
|
|
|
|
# Load API key
|
|
api_key = env_vars.get('FRED_API_KEY', '')
|
|
self.api_key_input.setText(api_key)
|
|
|
|
mt5_suffix = env_vars.get('MT5_SYMBOL_SUFFIX', '')
|
|
self.mt5_suffix_input.setText(mt5_suffix)
|
|
|
|
# Load weights (convert from decimal to percentage)
|
|
weight_rate = float(env_vars.get('WEIGHT_RATE', config.WEIGHT_RATE)) * 100
|
|
weight_cpi = float(env_vars.get('WEIGHT_CPI', config.WEIGHT_CPI)) * 100
|
|
weight_pmi = float(env_vars.get('WEIGHT_PMI', config.WEIGHT_PMI)) * 100
|
|
|
|
self.weight_rate_spin.blockSignals(True)
|
|
self.weight_cpi_spin.blockSignals(True)
|
|
self.weight_pmi_spin.blockSignals(True)
|
|
|
|
self.weight_rate_spin.setValue(weight_rate)
|
|
self.weight_cpi_spin.setValue(weight_cpi)
|
|
self.weight_pmi_spin.setValue(weight_pmi)
|
|
|
|
self.weight_rate_spin.blockSignals(False)
|
|
self.weight_cpi_spin.blockSignals(False)
|
|
self.weight_pmi_spin.blockSignals(False)
|
|
|
|
# Load min gap
|
|
min_gap = float(env_vars.get('MIN_GAP', config.MIN_GAP_TO_TRADE))
|
|
self.min_gap_spin.setValue(int(min_gap))
|
|
|
|
# Load auto-fetch setting
|
|
auto_fetch = env_vars.get('AUTO_FETCH_RATES_ON_STARTUP', 'true').lower() == 'true'
|
|
self.auto_fetch_check.setChecked(auto_fetch)
|
|
|
|
self._update_weights_total()
|
|
|
|
except Exception as e:
|
|
print(f"[ERROR] Failed to load settings: {e}")
|
|
|
|
def _update_weights_total(self):
|
|
"""Update weights total display and color."""
|
|
total = (self.weight_rate_spin.value() +
|
|
self.weight_cpi_spin.value() +
|
|
self.weight_pmi_spin.value())
|
|
|
|
self.weights_total_label.setText(f"Total: {total:.1f}%")
|
|
|
|
if abs(total - 100) < 0.1:
|
|
self.weights_total_label.setStyleSheet("color: #27ae60; font-weight: bold;")
|
|
else:
|
|
self.weights_total_label.setStyleSheet("color: #e74c3c; font-weight: bold;")
|
|
|
|
def _test_fred_connection(self):
|
|
"""Test FRED API connection in background."""
|
|
api_key = self.api_key_input.text().strip()
|
|
|
|
if not api_key:
|
|
QMessageBox.warning(self, "Missing API Key", "Please enter a FRED API key first.")
|
|
return
|
|
|
|
self.test_status.setText("Testing connection...")
|
|
|
|
self.test_worker = FredTestWorker(api_key)
|
|
self.test_worker.test_complete.connect(self._on_test_complete)
|
|
self.test_worker.start()
|
|
|
|
def _on_test_complete(self, success: bool, message: str):
|
|
"""Handle FRED test completion."""
|
|
self.test_status.setText(message)
|
|
|
|
if success:
|
|
self.test_status.setStyleSheet("color: #27ae60; font-weight: bold;")
|
|
else:
|
|
self.test_status.setStyleSheet("color: #e74c3c; font-weight: bold;")
|
|
|
|
def _test_mt5_connection(self):
|
|
"""Test MT5 connection in background."""
|
|
suffix = self.mt5_suffix_input.text().strip()
|
|
self.mt5_status_label.setText("Testing connection...")
|
|
self.mt5_status_label.setStyleSheet("color: #95a5a6; font-style: italic;")
|
|
self.mt5_test_worker = Mt5TestWorker(suffix)
|
|
self.mt5_test_worker.test_complete.connect(self._on_mt5_test_complete)
|
|
self.mt5_test_worker.start()
|
|
|
|
def _on_mt5_test_complete(self, success: bool, message: str):
|
|
"""Handle MT5 test completion."""
|
|
self.mt5_status_label.setText(message)
|
|
if success:
|
|
self.mt5_status_label.setStyleSheet("color: #27ae60; font-weight: bold;")
|
|
else:
|
|
self.mt5_status_label.setStyleSheet("color: #e74c3c; font-weight: bold;")
|
|
|
|
def _save_settings(self):
|
|
"""Save settings to .env file."""
|
|
try:
|
|
# Validate weights sum to 100%
|
|
total = (self.weight_rate_spin.value() +
|
|
self.weight_cpi_spin.value() +
|
|
self.weight_pmi_spin.value())
|
|
|
|
if abs(total - 100) > 0.1:
|
|
QMessageBox.warning(
|
|
self,
|
|
"Invalid Weights",
|
|
f"Weights must sum to 100%. Current total: {total:.1f}%"
|
|
)
|
|
return
|
|
|
|
# Prepare new .env content
|
|
api_key = self.api_key_input.text().strip()
|
|
mt5_suffix = self.mt5_suffix_input.text().strip()
|
|
weight_rate = self.weight_rate_spin.value() / 100
|
|
weight_cpi = self.weight_cpi_spin.value() / 100
|
|
weight_pmi = self.weight_pmi_spin.value() / 100
|
|
min_gap = self.min_gap_spin.value()
|
|
auto_fetch = "true" if self.auto_fetch_check.isChecked() else "false"
|
|
|
|
env_content = f"""FRED_API_KEY={api_key}
|
|
MT5_SYMBOL_SUFFIX={mt5_suffix}
|
|
DB_PATH=apex.db
|
|
MIN_GAP={min_gap}
|
|
WEIGHT_RATE={weight_rate:.2f}
|
|
WEIGHT_CPI={weight_cpi:.2f}
|
|
WEIGHT_PMI={weight_pmi:.2f}
|
|
AUTO_FETCH_RATES_ON_STARTUP={auto_fetch}
|
|
DEBUG=false
|
|
"""
|
|
|
|
# Write to .env
|
|
with open(self.env_path, 'w') as f:
|
|
f.write(env_content)
|
|
|
|
QMessageBox.information(
|
|
self,
|
|
"Settings Saved",
|
|
"Settings have been saved to .env\nPlease restart the application for changes to take effect."
|
|
)
|
|
|
|
self.settings_changed.emit()
|
|
|
|
except Exception as e:
|
|
QMessageBox.critical(self, "Error", f"Failed to save settings: {e}")
|
|
|
|
def _reset_to_defaults(self):
|
|
"""Reset all settings to defaults."""
|
|
reply = QMessageBox.question(
|
|
self,
|
|
"Reset to Defaults",
|
|
"Are you sure? This will reset all settings to factory defaults.",
|
|
QMessageBox.Yes | QMessageBox.No
|
|
)
|
|
|
|
if reply == QMessageBox.Yes:
|
|
self.api_key_input.clear()
|
|
self.mt5_suffix_input.clear()
|
|
self.weight_rate_spin.setValue(50)
|
|
self.weight_cpi_spin.setValue(30)
|
|
self.weight_pmi_spin.setValue(20)
|
|
self.min_gap_spin.setValue(20)
|
|
self.auto_fetch_check.setChecked(True)
|