Files
QuantCore-FX/project_structure_and_resume.md
T
2026-06-17 11:26:57 +01:00

40 KiB
Raw Blame History

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:

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) `
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) `
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

-- 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)

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)