mirror of
https://github.com/Sabermrddz/QuantCore-FX.git
synced 2026-08-13 18:58:05 +00:00
879 lines
40 KiB
Markdown
879 lines
40 KiB
Markdown
# APEX — Currency Strength Engine
|
||
|
||
> **Version:** 1.1.0
|
||
> **Timeframe:** Swing / Position Trading (1–5 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.01–5.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`)
|